Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/23050-s13d-meta-item-history-routes-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/rest': patch
---

The `/meta` history family routes live in their own module, with no route, handler or answer changed

Clause-②: no (a move: the five routes per base, their place in the `/meta` registration order, every handler, every response and the anonymous-deny gate in front of them are unchanged, and the published declarations are byte-identical)

- `RestServer#registerMetadataEndpointsInner` keeps its name, docblock, place and head, its per-item registrar included. The five statements that register `GET {metaPath}/:type/:name/history` (the change log), `GET …/audit` (the audit trail), `POST …/publish` (the promotion of a pending draft), `POST …/rollback` (the restore of an earlier version) and `GET …/diff` (the comparison of two stored versions) moved, re-indented, to `packages/rest/src/meta-item-history-routes.ts`, and the method calls them at their old position, inside the umbrella's guarded registration. They still register through that per-item registrar, so the `metadata.endpoints.item` switch gates the same mounts.
- Nothing to do for a consumer: no export of the package entry, no route, no configuration key and no response moved.
2 changes: 1 addition & 1 deletion content/docs/permissions/system-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ One of the largest consumers of the flag;
|:--|:---|:---|:---|:---|
| 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes. The gate is `refuseUnexposedDataAction`, which `callData` asks first and the `/data` domain asks before its request gate; `callData` reads the same bypass once more for each nested-relation condition's target | `packages/runtime/src/action-execution.ts#callData`, `#refuseUnexposedDataAction` |
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `packages/runtime/src/action-execution.ts#actionPermissionError` |
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability (the permission-set clone door's save included) — and, because REST's save door and the stored-version read doors ask one save verdict, an app's full stored version on `/layers`, `?layers=true` and `/diff`, whatever its entry gates withhold | `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner`, `packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes`, `packages/rest/src/meta-item-write-routes.ts#registerMetaItemWriteRoutes`, `packages/rest/src/rest-server.ts#metaSaveVerdict`, `packages/runtime/src/domains/permission-set-clone.ts#handlePermissionSetCloneWrite` |
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability (the permission-set clone door's save included) — and, because REST's save door and the stored-version read doors ask one save verdict, an app's full stored version on `/layers`, `?layers=true` and `/diff`, whatever its entry gates withhold | `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes`, `packages/rest/src/meta-item-write-routes.ts#registerMetaItemWriteRoutes`, `packages/rest/src/meta-item-history-routes.ts#registerMetaItemHistoryRoutes`, `packages/rest/src/rest-server.ts#metaSaveVerdict`, `packages/runtime/src/domains/permission-set-clone.ts#handlePermissionSetCloneWrite` |
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `packages/metadata-core/src/meta-write-capability.ts#metaWriteCapabilityVerdict` |
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `packages/runtime/src/domains/actions.ts#handleActionsRequest`, `packages/runtime/src/domains/ai.ts#handleAIRequest`, `packages/runtime/src/domains/automation.ts#handleAutomationRequest`, `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/runtime/src/domains/security.ts#handleSecurityRequest`, `packages/runtime/src/domains/packages.ts#handlePackagesRequest`, `packages/runtime/src/domains/analytics.ts#handleAnalyticsRequest`, `packages/runtime/src/domains/i18n.ts#handleI18nRequest`, `packages/rest/src/external-datasource-routes.ts#registerExternalDatasourceRoutes`, `packages/rest/src/package-routes.ts#refusePackageRequest` |
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `packages/runtime/src/domains/mcp.ts#handleMcpRequest` |
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/studio-authoring.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@
"packages/spec/src/api/protocol.zod.ts#SaveMetaItemResponse (SaveMetaItemResponse: state draft|active, version as If-Match/409 OCC token, 'staged only — not served until published')",
"packages/runtime/src/domains/packages.ts#packages (POST /packages/:id/publish-drafts, ADR-0033/ADR-0045 visibility flip + its failure warning)",
"packages/objectql/src/protocol-publish-package-drafts.test.ts (atomic namespace gate; #4463 author-time rules gate the draft→active transition)",
"packages/rest/src/rest-server.ts#toVersion (GET /meta/:type/:name/{history,audit,diff} + POST .../rollback: overlay-type history vs { events: [] } for non-overlay; rollback body { toVersion }, 400 on a missing/invalid toVersion; actor from resolveMetaWriteActor — the authenticated session identity ONLY, [#7941] — X-Actor is ignored outright, not read as a fallback either)",
"packages/rest/src/meta-item-history-routes.ts#registerMetaItemHistoryRoutes (GET /meta/:type/:name/{history,audit,diff} + POST .../rollback: overlay-type history vs { events: [] } for non-overlay; rollback body { toVersion }, 400 on a missing/invalid toVersion; actor from resolveMetaWriteActor — the authenticated session identity ONLY, [#7941] — X-Actor is ignored outright, not read as a fallback either)",
"packages/rest/src/meta-write-actor-identity.test.ts#Actor (unit pin: an explicit X-Actor does NOT outrank the authenticated identity, and is inert on the machine-write path too)",
"packages/rest/src/rest-route-ledger.ts#getHistory (client bindings meta.getHistory / getAudit / diffItem / rollbackItem; the 'dispatcher /meta swallows /history as a compound name and 404s' note — routes hunter #12)",
"docs/audits/2026-07-studio-package-create-ux-dogfood.md ('Unpublished draft' badge, 'Changes · n', 'one atomic release' toast)"
Expand Down
3 changes: 2 additions & 1 deletion packages/qa/dogfood/test/authz-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1637,7 +1637,8 @@ describe('#9084 — the rest-server.ts transport tripwire keys off mechanics', (
// [#23050 slice 13a] Length 13 → 14 (`meta-collection-routes.ts`), still 0 over it.
// [#23050 slice 13b] Length 14 → 15 (`meta-item-read-routes.ts`), still 0 over it.
// [#23050 slice 13c] Length 15 → 16 (`meta-item-write-routes.ts`), still 0 over it.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(16);
// [#23050 slice 13d] Length 16 → 17 (`meta-item-history-routes.ts`), still 0 over it.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(17);
expect(REST_SERVER_REGISTRAR_PATHS[0]).toBe(REST_SERVER);
const listed = REST_SERVER_REGISTRAR_PATHS.map((file) => readFileSync(join(REPO_ROOT, file), 'utf8'));
for (const probe of [MECHANICS, ROUTES]) {
Expand Down
31 changes: 26 additions & 5 deletions packages/qa/dogfood/test/authz-probe-blind-spot.census.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,17 +541,17 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
'`data-action-routes.ts`, `batch-routes.ts`, `approvals-routes.ts`, `sharing-routes.ts`, ' +
'`analytics-routes.ts`, `security-routes.ts`, `discovery-routes.ts`, `email-routes.ts`, ' +
'`search-routes.ts`, `ui-routes.ts`, `form-routes.ts`, `crud-routes.ts`, `meta-collection-routes.ts`, ' +
'`meta-item-read-routes.ts` and `meta-item-write-routes.ts`, ' +
'`meta-item-read-routes.ts`, `meta-item-write-routes.ts` and `meta-item-history-routes.ts`, ' +
'the modules the bodies of ' +
'`registerDataActionEndpoints`, `registerBatchEndpoints`, `registerApprovalsEndpoints`, ' +
'`registerSharingEndpoints`, `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, ' +
'`registerSecurityExplainEndpoints`, `registerSecurityEndpoints`, `registerDiscoveryEndpoints`, ' +
'`registerOpenApiEndpoints`, `registerEmailEndpoints`, `registerSearchEndpoints`, ' +
'`registerUiEndpoints`, `registerFormEndpoints` and `registerCrudEndpoints`, and the first five `/meta` ' +
'`registerUiEndpoints`, `registerFormEndpoints` and `registerCrudEndpoints`, and the first six `/meta` ' +
'groups of `registerMetadataEndpointsInner`, moved to; ' +
'reachable = those inside registerMetadataEndpoints, PLUS every site in the registrar list\'s named `/meta` ' +
'subset (`REST_SERVER_META_ROUTE_FILES`: `meta-collection-routes.ts`, `meta-item-read-routes.ts` and ' +
'`meta-item-write-routes.ts`), whose ' +
'subset (`REST_SERVER_META_ROUTE_FILES`: `meta-collection-routes.ts`, `meta-item-read-routes.ts`, ' +
'`meta-item-write-routes.ts` and `meta-item-history-routes.ts`), whose ' +
'registrations the inner method ' +
'runs inside the umbrella\'s swap',
// [#15542 / #15854] ⭐ THE POPULATION RULE LEARNED A SECOND SPELLING, and
Expand Down Expand Up @@ -891,10 +891,29 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
// spells it nowhere), `private register*Endpoints(` stays 16,
// `const registerPerItemRoute =` stays 1 and `enforceAuth` stays 112: the
// moved group spells none of them, and neither does the seam.
// [#23050 slice 13d] 70 / 19 / 51 UNCHANGED, `reachable` held by 13a's rule
// and every control keyed as 13c keyed them, with no rule change. The sixth
// `/meta` group, the history family (`/history`, `/audit`, `/publish`,
// `/rollback` and `/diff`, five helper-routed registration sites, REACHABLE
// through the same umbrella key), moved to `meta-item-history-routes.ts`,
// which joins the list (length 16 -> 17) and the named `/meta` subset
// (3 -> 4). Like the per-item writes' module it has NO direct site, so its
// own control counts `registerPerItemRoute(` (5), and the whole-population
// `registerPerItemRoute(` control stays 8 over this file plus every module on
// the list (1 here, the published read's, + 2 + 5); over this file alone it
// would now read 1. The subset's length and the module calls inside the
// inner method move together (both 4). Measured at the move commit, before
// the list learned the module: population 65, the five sites out of reach,
// and the `.call(` control at 4 against a subset of 3.
// `this.routeManager.register(` stays 63 over the seventeen files (the module
// spells it nowhere), `private register*Endpoints(` stays 16,
// `const registerPerItemRoute =` stays 1, `forwarder slice:` stays 1 and
// `enforceAuth` stays 112: the moved group spells none of them, and neither
// does the seam.
controls: {
'private register*Endpoints(': 16,
'this.routeManager.register(': 63,
'registrar list length': 16,
'registrar list length': 17,
'data-action-routes.ts: this.routeManager.register(': 9,
'batch-routes.ts: this.routeManager.register(': 5,
'approvals-routes.ts: this.routeManager.register(': 7,
Expand All @@ -912,6 +931,8 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
// [#23050 slice 13c] Controlled on the helper's call spelling: this module
// has no direct site (see above).
'meta-item-write-routes.ts: registerPerItemRoute(': 2,
// [#23050 slice 13d] The same: the history family's module has no direct site.
'meta-item-history-routes.ts: registerPerItemRoute(': 5,
// [#23050 slice 13a] Recorded AS the subset's length, not as a literal: the
// measured side is the module calls inside the inner method, so the two
// can only move together.
Expand Down
26 changes: 23 additions & 3 deletions packages/rest/src/execctx-consumer-census.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ const SOURCE = readFileSync(resolve(HERE, 'rest-server.ts'), 'utf8');
* [#23050 slice 13c] …and the fifth, the per-item writes, to
* `meta-item-write-routes.ts`, with their two: the item save's and the reset's,
* each locally caught on its invocation line, as before.
*
* [#23050 slice 13d] …and the sixth, the history family, to
* `meta-item-history-routes.ts`, with its five, each locally caught as before:
* the audit trail's, the publish's and the rollback's on their invocation
* lines, the change log's and the comparison's on their continuation lines —
* two of §7's and §8's four continuation sites, which those sections find there
* because they read every file here.
*/
const SITE_FILES = REST_SERVER_REGISTRAR_FILES;
const SOURCES = new Map<string, string>(SITE_FILES.map((file) => [
Expand All @@ -150,7 +157,7 @@ const siteKey = (file: string, line: number) => `${file}:${line}`;
* `registerSearchEndpoints`, `registerUiEndpoints`,
* `registerCrudEndpoints`) hands its moved body — and each one
* `registerMetadataEndpointsInner` builds for a moved `/meta` group's module
* (#23050 slices 13a, 13b and 13c), counted as that module's registrar —
* (#23050 slices 13a, 13b, 13c and 13d), counted as that module's registrar —
* forwards each member to the server with `(...a) => this.<member>(...a)`, so
* a moved site's stack shows
* that forwarder above the site itself. The site is the first censused frame
Expand Down Expand Up @@ -782,9 +789,22 @@ describe('[#13160] §2 the consumer surface, counted from the tree', () => {
// one forwarder's two mentions in). Sixteen forwarders now: (16 - 1) + 3 - 2,
// the module carrying one host literal that reads the seam, so neither map
// gains an entry.
expect(SITE_FILES).toHaveLength(16);
//
// [#23050 slice 13d] 62 sites UNCHANGED / 141 → 145 mentions — the same MOVE.
// The sixth `/meta` group, the history family, left for
// `meta-item-history-routes.ts` with its five LOCALLY CAUGHT sites (the audit
// trail's, the publish's and the rollback's on their invocation lines, the
// change log's and the comparison's on their continuation lines, as before):
// 5 + 9 + 5 + 7 + 8 + 1 + 5 + 2 + 1 + 1 + 1 + 0 + 6 + 3 + 1 + 2 + 5 = 62 sites.
// The four new mentions are this slice's seam, the two spellings once (the
// fourth host literal the inner method builds, its forwarder, two; the host
// interface's member, two): `rest-server.ts` goes 55 → 52 (the five sites
// out, one forwarder's two mentions in). Seventeen forwarders now:
// (17 - 1) + 3 - 2, the module carrying one host literal that reads the seam,
// so neither map gains an entry.
expect(SITE_FILES).toHaveLength(17);
expect(SITES.length).toBe(62);
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(141);
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(145);
const extraMovedRegistrars = Object.values(EXTRA_MOVED_REGISTRARS).reduce((n, k) => n + (k ?? 0), 0);
expect(extraMovedRegistrars).toBe(3);
const registrarsReadingNoExecCtx = Object.values(MOVED_REGISTRARS_READING_NO_EXECCTX).reduce((n, k) => n + (k ?? 0), 0);
Expand Down
6 changes: 5 additions & 1 deletion packages/rest/src/meta-draft-read-door-census.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,11 @@ describe('[#20338] census: every draft switch in rest-server.ts is ledgered', ()
// with the last one `rest-server.ts` held: the reset's `?state=draft` (a
// `write` row). Its key names the route, so again no ledger row changed:
// 8 = 0 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3 + 2 + 1, length 15 → 16.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(16);
// [#23050 slice 13d] The history family moved to `meta-item-history-routes.ts`
// carrying no draft switch: none of its five doors reads one, so no ledger row
// changed:
// 8 = 0 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3 + 2 + 1 + 0, length 16 → 17.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(17);
expect(SOURCE_FILES.map((file) => file.fileName)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
expect(SITES).toHaveLength(8);
});
Expand Down
Loading
Loading