Skip to content

Enforce X-HAXCMS-User-Token on system READ operations (spec-driven) - #19

Merged
btopro merged 2 commits into
mainfrom
system-read-usertoken
Aug 6, 2026
Merged

Enforce X-HAXCMS-User-Token on system READ operations (spec-driven)#19
btopro merged 2 commits into
mainfrom
system-read-usertoken

Conversation

@btopro

@btopro btopro commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

Closes the system-read userToken enforcement gap: system READ operations that declare userTokenHeader in the OpenAPI spec now actually enforce the X-HAXCMS-User-Token header, mirroring the spec-driven pattern already used by the SITE API. This is the security-alignment counterpart to the dashboard-write userToken enforcement landed in spec-conformance-cleanup.

Canonical system READs (15) — now enforce bearerAuth + userTokenHeader

listSites, 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 — add userTokenHeader to getApiKeys + getMediaSettings GET reads so all 15 canonical reads declare bearerAuth + 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 both systemRouteHandler and siteScopedSystemRouteHandler after JWT/basic + admin-gate succeeds. Missing → 403 X-HAXCMS-User-Token header is required for this endpoint; invalid → 403 Invalid X-HAXCMS-User-Token header (matches the existing SITE API strings at app.js:2335/2342 exactly for cross-repo parity). Validator = HAXCMS.validateRequestToken with bearer/basic-derived userName.
  • test/api-conformance/site-spec.conformance.test.cjs — (a) remove stale SITE integrationDescriptor assertions from listEntityDescriptors (D38 moved provider-search to the SYSTEM API); (b) new canonical-reads suite asserting all 15 declare bearerAuth + userTokenHeader + bearer-only→403 + invalid userToken→403 + valid userToken→200 + the SYSTEM integration entity descriptor (auth: public, /integrations/app-store); (c) createHarnessSite always sends userToken.
  • test/api-conformance/export-endpoints-php.integration.test.cjs — item 3a: assert body.data.supportedFormats (D1 envelope) instead of top-level body.supportedFormats.
  • test/api-conformance/actions-spec.conformance.test.cjs, export-endpoints.integration.test.cjs, ssrf.conformance.test.cjs — send X-HAXCMS-User-Token for action/import/createSite endpoints that declare userTokenHeader.
  • test/e2e/helpers/harness.cjs, test/e2e/create-site.e2e.test.cjs — fetch connectionSettings so runtime.userToken is available for direct (non-registry) API calls; the listSites cross-check now sends userToken.

Behavior note

The spec-driven gate enforces userToken on all system routes declaring userTokenHeader, which includes createSite + 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-conformanceGREEN (169 tests, 0 fail, 0 skip)
  • npm run test:e2eGREEN (40 pass, 1 skip, 0 fail) — critical boot-flow tripwire: sessionUserGet now requires userToken; the frontend registry (app-hax-system-api-registry.js) auto-attaches userToken for any op declaring userTokenHeader, so login/dashboard boot passes unchanged.
  • node --check clean on every touched .js/.cjs file.

Cross-repo parity

The 15 canonical read opIds declare bearerAuth + userTokenHeader identically 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-php system-read-usertoken.

Co-Authored-By: Oz oz-agent@warp.dev

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>
Copilot AI lite review requested due to automatic review settings August 6, 2026 05:04
@codesandbox

codesandbox Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in src/app.js.
  • Updated system-spec.yaml so getApiKeys and getMediaSettings are included in the canonical “bearerAuth + userTokenHeader” read set.
  • Updated API conformance and E2E tests/harnesses to fetch connection settings and attach X-HAXCMS-User-Token where 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
}
@btopro
btopro merged commit e2e55d1 into main Aug 6, 2026
2 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants