Enforce X-HAXCMS-User-Token on system READ operations (spec-driven) - #19
Merged
Conversation
Add spec-driven userToken enforcement on system API reads that declare userTokenHeader in system-spec.yaml, mirroring the SITE API pattern (readSiteApiAuthPoliciesFromOpenApiSpec -> getSiteApiRouteAuthPolicy -> validateSiteApiRouteAccess). New system equivalents: readSystemApiAuthPoliciesFromOpenApiSpec(), getSystemApiRouteAuthPolicy(), enforceSystemApiUserTokenPolicy() — wired into both systemRouteHandler and siteScopedSystemRouteHandler after JWT/basic auth + admin-route referer gate succeed. Policy 'authenticated-user' routes enforce X-HAXCMS-User-Token: missing -> 403 'X-HAXCMS-User-Token header is required for this endpoint'; invalid -> 403 'Invalid X-HAXCMS-User-Token header' (matches existing SITE API code strings exactly for cross-repo parity). Uses the same validator as the SITE API (HAXCMS.validateRequestToken with bearer/basic-derived userName). system-spec.yaml: add userTokenHeader to getApiKeys + getMediaSettings GET reads so all 15 canonical userToken-requiring reads declare bearerAuth + userTokenHeader. Skeleton/theme/block reads stay bearer-only (D10). Writes unchanged (per-handler userToken enforcement kept). ITEM 3a: export-endpoints-php test asserts body.data.supportedFormats (D1 envelope) instead of body.supportedFormats. ITEM 3b: remove stale SITE integration-entity assertions from listEntityDescriptors subtest (D38 moved app-store provider-search to the system API). Verified systemEntities handler returns an integration entity descriptor (auth 'public', .../integrations/app-store) and added a system-side assertion in the new canonical-reads suite. New canonical-reads conformance block: asserts all 15 opIds declare bearerAuth + userTokenHeader, bearer-only -> 403, invalid userToken -> 403, valid userToken -> 200 for each read. Test helpers updated to send X-HAXCMS-User-Token for system writes/actions that declare userTokenHeader (createSite, action/import endpoints) whose handlers did not previously enforce userToken — the spec-driven gate now closes that spec/impl gap. Write handlers themselves unchanged. Validation: - npm run test:api-conformance -> 169 tests, 0 fail, 0 skip, EXIT=0 - npm run test:e2e -> 41 tests, 0 fail, 1 skip, EXIT=0 - node --check on all touched .js/.cjs files -> pass Co-Authored-By: Oz <oz-agent@warp.dev>
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
There was a problem hiding this comment.
Pull request overview
Aligns the Node.js system API implementation with the OpenAPI spec by enforcing X-HAXCMS-User-Token for system endpoints that declare userTokenHeader, mirroring the existing spec-driven SITE API pattern and tightening auth behavior for canonical system reads.
Changes:
- Added spec-driven system-route auth policy parsing and runtime enforcement for
authenticated-user(bearer + userToken) operations insrc/app.js. - Updated
system-spec.yamlsogetApiKeysandgetMediaSettingsare included in the canonical “bearerAuth + userTokenHeader” read set. - Updated API conformance and E2E tests/harnesses to fetch connection settings and attach
X-HAXCMS-User-Tokenwhere required.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/app.js |
Adds system OpenAPI policy parsing and enforces userToken on authenticated-user system routes. |
src/openapi/system-spec.yaml |
Declares userTokenHeader for getApiKeys and getMediaSettings reads. |
test/api-conformance/site-spec.conformance.test.cjs |
Adds canonical system-read userToken enforcement suite; removes stale SITE integration descriptor assertions. |
test/api-conformance/actions-spec.conformance.test.cjs |
Fetches connection settings and attaches userToken header for userToken-secured endpoints. |
test/api-conformance/ssrf.conformance.test.cjs |
Fetches connection settings and attaches userToken header for relevant requests. |
test/api-conformance/export-endpoints.integration.test.cjs |
Ensures create-site harness requests include userToken when present in settings. |
test/api-conformance/export-endpoints-php.integration.test.cjs |
Updates assertion to match the D1 envelope (body.data.supportedFormats). |
test/e2e/helpers/harness.cjs |
Fetches connection settings in E2E runtime so direct API calls can include userToken. |
test/e2e/create-site.e2e.test.cjs |
Adds userToken header to the direct /system/api/v1/sites cross-check. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+2638
to
+2655
| assert.ok( | ||
| security.some( | ||
| (entry) => | ||
| entry && | ||
| typeof entry === 'object' && | ||
| Object.prototype.hasOwnProperty.call(entry, 'bearerAuth'), | ||
| ), | ||
| `${operationId} must declare bearerAuth security`, | ||
| ) | ||
| assert.ok( | ||
| security.some( | ||
| (entry) => | ||
| entry && | ||
| typeof entry === 'object' && | ||
| Object.prototype.hasOwnProperty.call(entry, 'userTokenHeader'), | ||
| ), | ||
| `${operationId} must declare userTokenHeader security (canonical user-token read)`, | ||
| ) |
Comment on lines
+528
to
+531
| const listHeaders = { Authorization: 'Bearer ' + runtime.jwt } | ||
| if (runtime.userToken) { | ||
| listHeaders[runtime.userTokenHeader || 'X-HAXCMS-User-Token'] = runtime.userToken | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the system-read
userTokenenforcement gap: system READ operations that declareuserTokenHeaderin the OpenAPI spec now actually enforce theX-HAXCMS-User-Tokenheader, mirroring the spec-driven pattern already used by the SITE API. This is the security-alignment counterpart to the dashboard-write userToken enforcement landed inspec-conformance-cleanup.Canonical system READs (15) — now enforce
bearerAuth + userTokenHeaderlistSites,siteInfoGet,siteInfoPost,systemStatusGet,systemStatusPost,systemVersionGet,systemVersionPost,sessionUserGet,sessionUserPost,systemEntitiesGet,systemEntitiesPost,systemSchemasGet,systemSchemasPost,getApiKeys,getMediaSettings.Skeleton/theme/block reads stay bearer-only (D10) — unchanged. Writes keep their existing per-handler userToken enforcement — unchanged.
Changes
src/openapi/system-spec.yaml— adduserTokenHeadertogetApiKeys+getMediaSettingsGET reads so all 15 canonical reads declarebearerAuth + userTokenHeader.src/app.js— spec-driven system userToken enforcement mirroring the SITE API:SYSTEM_API_OPENAPI_SPEC_PATH,systemApiAuthPoliciesByMethodAndRoute,convertOpenApiPathToSystemRoute(),readSystemApiAuthPoliciesFromOpenApiSpec(),getSystemApiRouteAuthPolicy(),resolveSystemApiAuthenticatedUserName(),enforceSystemApiUserTokenPolicy(). Wired into bothsystemRouteHandlerandsiteScopedSystemRouteHandlerafter JWT/basic + admin-gate succeeds. Missing → 403X-HAXCMS-User-Token header is required for this endpoint; invalid → 403Invalid X-HAXCMS-User-Token header(matches the existing SITE API strings at app.js:2335/2342 exactly for cross-repo parity). Validator =HAXCMS.validateRequestTokenwith bearer/basic-derived userName.test/api-conformance/site-spec.conformance.test.cjs— (a) remove stale SITEintegrationDescriptorassertions fromlistEntityDescriptors(D38 moved provider-search to the SYSTEM API); (b) new canonical-reads suite asserting all 15 declarebearerAuth + userTokenHeader+ bearer-only→403 + invalid userToken→403 + valid userToken→200 + the SYSTEMintegrationentity descriptor (auth: public,/integrations/app-store); (c)createHarnessSitealways sends userToken.test/api-conformance/export-endpoints-php.integration.test.cjs— item 3a: assertbody.data.supportedFormats(D1 envelope) instead of top-levelbody.supportedFormats.test/api-conformance/actions-spec.conformance.test.cjs,export-endpoints.integration.test.cjs,ssrf.conformance.test.cjs— sendX-HAXCMS-User-Tokenfor action/import/createSite endpoints that declareuserTokenHeader.test/e2e/helpers/harness.cjs,test/e2e/create-site.e2e.test.cjs— fetchconnectionSettingssoruntime.userTokenis available for direct (non-registry) API calls; thelistSitescross-check now sends userToken.Behavior note
The spec-driven gate enforces
userTokenon all system routes declaringuserTokenHeader, which includescreateSite+ action/import endpoints whose handlers previously did not enforce it (a spec/impl gap the gate closes). Write handlers themselves were not modified. This is the intended behavior of a spec-driven approach (spec declares it → enforce it).Validation
npm run test:api-conformance— GREEN (169 tests, 0 fail, 0 skip)npm run test:e2e— GREEN (40 pass, 1 skip, 0 fail) — critical boot-flow tripwire:sessionUserGetnow requires userToken; the frontend registry (app-hax-system-api-registry.js) auto-attachesuserTokenfor any op declaringuserTokenHeader, so login/dashboard boot passes unchanged.node --checkclean on every touched.js/.cjsfile.Cross-repo parity
The 15 canonical read opIds declare
bearerAuth + userTokenHeaderidentically on both this spec and the PHP system spec. Both backends reject bearer-only with 403 and accept bearer+userToken with 200, with identical message strings (verified). Companion PR: haxcms-phpsystem-read-usertoken.Co-Authored-By: Oz oz-agent@warp.dev