Skip to content

[haxcms-php] v1 API conformance normalization (spec-driven auth + PHP fixes) - #616

Merged
btopro merged 5 commits into
masterfrom
conformance/v1-normalization
Aug 6, 2026
Merged

[haxcms-php] v1 API conformance normalization (spec-driven auth + PHP fixes)#616
btopro merged 5 commits into
masterfrom
conformance/v1-normalization

Conversation

@btopro

@btopro btopro commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

Normalizes the haxcms-php v1 API surface against the NodeJS backend and the shared OpenAPI specs, addressing all High and Medium findings from the conformance audit. The spec files are synced from haxcms-nodejs (canonical) in a separate commit.

Changes

Structural (C1 — resolves A1):

  • Site + system auth policy is now spec-driven (reads OpenAPI at runtime, fail-closed to authenticated)
  • Startup mutation-security guard mirrors Node's assertSiteApiMutationRoutesAreSecured
  • Retires the hand-maintained regex table in SiteApiSecurity / SystemApiSecurity
  • GET /x/api/v1/files + GET /x/api/v1/reports now require site token (authenticated-site) — resolving the security-stance gap

Site fixes:

  • B2: PATCH content maps top-level body/content/schema/detailsnode.* + 400 on missing body
  • D1: PATCH files rejects {operation:delete} (400) and ignores client body.path (UUID only)
  • E2: haxElementSchema returns rich per-element schema (tag + properties + content)
  • E7: outline 400 message aligned to Node wording
  • E8: escaped-quote bug fixed in search/exports error messages
  • E9: revisions GET uses findItemByIdOrSlug (slug resolution)
  • E10: items PATCH adds overridePathauto; drops nested node.details.*
  • E11: POST items returns 400 on missing payload
  • A4: views results (items/tags/search) + blocks (usage/list/detail) enforce anonymous-visibility

System fixes:

  • B1: adds GET session/refresh
  • D2/Q7: un-strips jwt (revalidate) in v1 login
  • D2/Q8: drops legacy u/p branch; login/refresh failures return 401 with JSON envelopes
  • D6: single-site siteName resolution (so siteToken mints for user:sitename)
  • F1: removes undocumented routes (GET session/login, GET session/logout, POST provider-search)
  • F2/Q14+F3: provider-search site token enforced in the router
  • F6: cloneSite uses configured basePath/sitesDirectory; listSites drops title filter

Spec sync:

  • site-spec.yaml + system-spec.yaml copied byte-identical from haxcms-nodejs via scripts/sync-openapi-specs.js

Decisions applied

  • Spec-driven auth policy (Q6) — structural fix, prevents future drift
  • validateHaxiamManagedUserIdentityForRequest stays Node-only (S5) — no PHP equivalent added

Validation

  • php -l on all 23 changed files → OK
  • Route maps + security classes load → OK
  • Spec-driven policy verified: files/reports GET → authenticated-site, mutations → authenticated-site, unknown → authenticated (fail-closed)
  • Mutation guard finds no offending routes
  • Conformance validator: 26/28 PASS; A4 FAIL fixed in follow-up commit (now merged into this branch)

Related

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

btopro and others added 5 commits August 6, 2026 13:36
Structural (C1):
- Site + system auth policy now spec-driven (reads OpenAPI at runtime)
- Fail-closed to authenticated; startup mutation-security guard
- Retires hand-maintained regex table (resolves files/reports site-token gap)

Site fixes:
- PATCH content: maps top-level body/content/schema/details to node.* + 400
- PATCH files: rejects delete-on-PATCH; ignores client body.path (UUID only)
- items: rich haxElementSchema (tag+properties+content)
- items: 400 on empty POST; overridePathauto in detail keys; drops nested node.details.*
- revisions GET: findItemByIdOrSlug (slug resolution)
- views: anonymous-visibility context to applyItemFilters
- search/exports: escaped-quote bug fix
- outline 400 message aligned to Node wording

System fixes:
- Adds GET session/refresh
- Un-strips jwt (revalidate) in v1 login
- Drops legacy u/p branch; login/refresh 401 + JSON envelopes
- connectionSettings: single-site siteName resolution
- Removes undocumented routes (GET session/login, GET session/logout, POST provider-search, GET v1/openapi* aliases)
- provider-search site token enforced in router
- cloneSite: configured basePath/sitesDirectory; listSites drops title filter

Refs: haxtheweb/issues conformance audit (site/system parity)

Co-Authored-By: Oz <oz-agent@warp.dev>
site-spec.yaml + system-spec.yaml copied byte-identical via
scripts/sync-openapi-specs.js after node-backend merge.

Co-Authored-By: Oz <oz-agent@warp.dev>
…-driven auth + PHP fixes)

Co-Authored-By: Oz <oz-agent@warp.dev>
…/list/detail

- views.php tags + search sources: inline anon-visibility filter
  (isAnonymousSiteApiRequest + isItemVisibleToAnonymous), mirroring Node
- blocks.php blockUsage + listBlocks: pass  to applyItemFilters
- blocks.php blockDetail: wrap getOrderedItems in applyItemFilters with

Refs: conformance audit A4/S6 (PHP side was incomplete)

Co-Authored-By: Oz <oz-agent@warp.dev>
…tags/search + blocks)

Co-Authored-By: Oz <oz-agent@warp.dev>
Copilot AI lite review requested due to automatic review settings August 6, 2026 18:01
@btopro
btopro merged commit a5844ba into master Aug 6, 2026
1 check failed

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

Note

Copilot was unable to run its full agentic suite in this review.

Aligns PHP system/site APIs with the Node/OpenAPI contract by making auth/security decisions spec-driven and tightening handler behavior to match documented request/response semantics.

Changes:

  • Moves site + system route auth policy determination to runtime OpenAPI spec parsing (with added user-token enforcement and a startup “mutation routes must not be public” guard).
  • Updates multiple PHP v1 handlers to mirror Node/spec behaviors (JWT revalidate flow, anonymous visibility filtering, slug→UUID revision resolution, stricter mutation validation, UUID-only file operations).
  • Updates OpenAPI specs to clarify security, aliases, and required parameters (e.g., session endpoints public access, provider-search security, login request shape).

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
system/backend/php/lib/systemRoutes/v1/session.php Keeps jwt in params to allow JWT revalidation branch to execute.
system/backend/php/lib/systemRoutes/openapi/system-spec.yaml Documents updated security requirements and read-only POST aliases; expands login schema.
system/backend/php/lib/systemRoutes/SystemRoutesMap.php Adjusts mapped routes (session refresh; removes provider-search mapping).
system/backend/php/lib/systemRoutes/SystemApiSecurity.php Replaces static route lists with spec-driven auth; adds provider-search site-token enforcement.
system/backend/php/lib/systemRoutes/SystemApiRouter.php Adds router-level enforcement for provider-search site token with consistent 403 envelope.
system/backend/php/lib/siteRoutes/v1/views.php Applies anonymous-visibility filtering for tags/search and passes request context into item filters.
system/backend/php/lib/siteRoutes/v1/search.php Normalizes error message quoting.
system/backend/php/lib/siteRoutes/v1/revisions.php Resolves slug to UUID before delegating revision operations.
system/backend/php/lib/siteRoutes/v1/itemsMutation.php Validates POST payload presence; normalizes node.details behavior; adds overridePathauto support.
system/backend/php/lib/siteRoutes/v1/items.php Implements richer haxElementSchema extraction from HTML using DOMDocument.
system/backend/php/lib/siteRoutes/v1/filesMutation.php Enforces UUID-only resolution and disallows client-supplied body.path; rejects delete-on-PATCH.
system/backend/php/lib/siteRoutes/v1/exports.php Normalizes error message quoting.
system/backend/php/lib/siteRoutes/v1/contentMutation.php Maps spec-conformant top-level content/schema/details into node.* and requires body content.
system/backend/php/lib/siteRoutes/v1/blocks.php Ensures anon filtering by passing context into item filtering.
system/backend/php/lib/siteRoutes/openapi/site-spec.yaml Adds filename query parameter component and references it in a files listing route.
system/backend/php/lib/siteRoutes/SiteRoutesMap.php Removes v1 discovery/openapi aliases from the site route map.
system/backend/php/lib/siteRoutes/SiteRouteUtils.php Adds OpenAPI security normalization + spec parsing + policy lookup + mutation security assertion.
system/backend/php/lib/siteRoutes/SiteApiSecurity.php Switches to spec-driven route policy lookup; adds authenticated-user header enforcement.
system/backend/php/lib/siteRoutes/SiteApiRouter.php Passes matched route pattern (not concrete path) into auth; adds startup mutation-security check.
system/backend/php/lib/routes/saveOutline.php Updates 400 error message text.
system/backend/php/lib/routes/login.php Returns 401 for login failures; supports JWT revalidation via body jwt.
system/backend/php/lib/routes/listSites.php Includes sites even without title; guards metadata/pageCount assignment.
system/backend/php/lib/routes/cloneSite.php Rewrites file URLs using configured basePath/sitesDirectory with legacy fallback.
system/backend/php/lib/HAXCMS.php Fixes single-site mode sitename resolution for token minting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +218 to +226
public static function enforceProviderSearchSiteToken($routeName, $method, $context)
{
$normalizedMethod = strtoupper((string) $method);
if ($normalizedMethod !== 'GET') {
return null;
}
if ($routeName !== 'v1/integrations/app-store/providers/:provider/search') {
return null;
}
Comment on lines 169 to 179
if (isset($this->params['username']) && isset($this->params['password'])) {
return $this->processCredentialLogin($this->params['username'], $this->params['password'], false);
}
//old way
// if we don't have a user and the don't answer, bail
else if (isset($this->params['u']) && isset($this->params['p'])) {
return $this->processCredentialLogin($this->params['u'], $this->params['p'], true);
}
// login end point requested yet a jwt already exists
// this is something of a revalidate case
// D2/Q7: login end point requested yet a jwt already exists — this is a
// revalidate case. The body jwt was previously stripped in the session v1
// handler; it now reaches this branch so JWT-revalidate works in v1.
// D2/Q8: use validateJWT(false) so an invalid jwt returns a 401 envelope
// instead of exiting with 403. Set sessionJwt from the body jwt first so
// validateJWT can decode it (sessionJwt is normally set from the bearer
// header in the HAXCMS constructor).
else if (isset($this->params['jwt'])) {
Comment on lines 18 to 21
'openapi' => dirname(__FILE__) . '/discovery/openapi.php',
'openapi.json' => dirname(__FILE__) . '/discovery/openapi.php',
'openapi.yaml' => dirname(__FILE__) . '/discovery/openapi.php',
'v1' => dirname(__FILE__) . '/discovery/api.php',
'v1/openapi' => dirname(__FILE__) . '/discovery/openapi.php',
'v1/openapi.json' => dirname(__FILE__) . '/discovery/openapi.php',
'v1/openapi.yaml' => dirname(__FILE__) . '/discovery/openapi.php',
'v1/site' => dirname(__FILE__) . '/v1/site.php',
Comment on lines +943 to +954
private static $siteApiAuthPolicies = null;
/**
* Read site-spec.yaml once per request and build a map of
* 'method:routeKey => policy' mirroring Node
* readSiteApiAuthPoliciesFromOpenApiSpec (app.js:1658). Cached in a
* static property so repeated lookups in the same request skip re-parsing.
*/
public static function readSiteApiAuthPoliciesFromOpenApiSpec()
{
if (self::$siteApiAuthPolicies !== null) {
return self::$siteApiAuthPolicies;
}
Comment on lines +240 to +245
// Resolve siteName from the query param for token validation. If
// siteName is missing, defer to the handler's siteName validation (F3).
$siteName = isset($_GET['siteName']) ? (string) $_GET['siteName'] : '';
if ($siteName === '') {
return null;
}
Comment on lines +208 to +213
* F2/Q14+F3: enforce the X-HAXCMS-Site-Token header on the app-store
* provider-search GET route in the router (not the handler) for a
* consistent 403 envelope. The current spec declares security: [] for
* this route; Q14 requires a site token. The spec will be updated by the
* node-backend and synced at merge; until then this explicit router-level
* check enforces the decision. siteName validation stays in the handler.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants