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-s13c-meta-item-write-routes-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/rest': patch
---

The `/meta` per-item write routes live in their own module, with no route, handler or answer changed

Clause-②: no (a move: the two 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 two statements that register `PUT {metaPath}/:type/:name` (the item save) and `DELETE {metaPath}/:type/:name` (the reset to the artifact default) moved, re-indented, to `packages/rest/src/meta-item-write-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/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/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` |
| 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
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 @@ -1636,7 +1636,8 @@ describe('#9084 — the rest-server.ts transport tripwire keys off mechanics', (
// [#23050 slice 12] Length 12 → 13 (`crud-routes.ts`), still 0 over it.
// [#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.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(15);
// [#23050 slice 13c] Length 15 → 16 (`meta-item-write-routes.ts`), still 0 over it.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(16);
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
56 changes: 48 additions & 8 deletions packages/qa/dogfood/test/authz-probe-blind-spot.census.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,17 +540,18 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
'module on the registrar list (`packages/rest/src/rest-server-registrar-files.ts`) — ' +
'`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` and ' +
'`meta-item-read-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`, ' +
'the modules the bodies of ' +
'`registerDataActionEndpoints`, `registerBatchEndpoints`, `registerApprovalsEndpoints`, ' +
'`registerSharingEndpoints`, `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, ' +
'`registerSecurityExplainEndpoints`, `registerSecurityEndpoints`, `registerDiscoveryEndpoints`, ' +
'`registerOpenApiEndpoints`, `registerEmailEndpoints`, `registerSearchEndpoints`, ' +
'`registerUiEndpoints`, `registerFormEndpoints` and `registerCrudEndpoints`, and the first four `/meta` ' +
'`registerUiEndpoints`, `registerFormEndpoints` and `registerCrudEndpoints`, and the first five `/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` and `meta-item-read-routes.ts`), whose ' +
'subset (`REST_SERVER_META_ROUTE_FILES`: `meta-collection-routes.ts`, `meta-item-read-routes.ts` and ' +
'`meta-item-write-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 @@ -866,10 +867,34 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
// 6 + 6 + 4), `private register*Endpoints(` stays 16, `registerPerItemRoute(`
// stays 8 and `enforceAuth` stays 112: the moved group spells neither term,
// and neither does the seam.
// [#23050 slice 13c] 70 / 19 / 51 UNCHANGED, `reachable` again held by 13a's
// rule with no rule change, and TWO controls re-keyed, as ruled for this
// slice. The fifth `/meta` group, the per-item writes (`PUT` and `DELETE
// {metaPath}/:type/:name`, two helper-routed registration sites, REACHABLE
// through the same umbrella key), moved to `meta-item-write-routes.ts`, which
// joins the list (length 15 -> 16) and the named `/meta` subset (2 -> 3). It
// is the first module on the list with NO direct site: both of its routes
// register through the inner method's per-item registrar, which reaches it in
// the `/meta` scope as the one closure the inner head declares and which
// still reads the route manager at call time, inside the swap. So (1) its own
// control counts `registerPerItemRoute(` (2), the population rule's second
// spelling, instead of `this.routeManager.register(`, which reads 0 there and
// would be a zero read as a reading; and (2) the `registerPerItemRoute(`
// control is RE-ANCHORED, not relaxed, over the same haystack as
// `this.routeManager.register(` and the population rule (this file plus every
// module on the list), where it stays 8 (6 here + 2 in the module); over this
// file alone it would read 6. The subset's length and the module calls
// inside the inner method move together (both 3). Measured at the move
// commit, before the list learned the module: population 68, the two sites
// out of reach, and the `.call(` control at 3 against a subset of 2.
// `this.routeManager.register(` stays 63 over the sixteen files (the module
// 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.
controls: {
'private register*Endpoints(': 16,
'this.routeManager.register(': 63,
'registrar list length': 15,
'registrar list length': 16,
'data-action-routes.ts: this.routeManager.register(': 9,
'batch-routes.ts: this.routeManager.register(': 5,
'approvals-routes.ts: this.routeManager.register(': 7,
Expand All @@ -884,6 +909,9 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
'crud-routes.ts: this.routeManager.register(': 6,
'meta-collection-routes.ts: this.routeManager.register(': 6,
'meta-item-read-routes.ts: this.routeManager.register(': 4,
// [#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 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 Expand Up @@ -1405,9 +1433,21 @@ export function deriveProbeFileCensus(): {
// [#23050 slice 1] The moved module's own share, so a reading that
// silently stopped appending it cannot hide behind the total above.
// [#23050 slice 3] One such control per module on the list.
...Object.fromEntries(movedModules.map(([file, text]) =>
[`${file}: this.routeManager.register(`, occurrences(text, /this\.routeManager\.register\(/g)])),
'registerPerItemRoute(': occurrences(src, /registerPerItemRoute\(/g),
// [#23050 slice 13c] …one per SPELLING the module registers through, as
// the population rule counts both: a module whose routes all go through
// the per-item registrar has no direct site, and a control that read 0
// there would be instrument failure by this census's own rule. A
// spelling a module does not spell mints no control, so a module that
// stops spelling the one its control was recorded on makes that control
// VANISH, which is as loud as moving it.
...Object.fromEntries(movedModules.flatMap(([file, text]) => [
[`${file}: this.routeManager.register(`, occurrences(text, /this\.routeManager\.register\(/g)] as const,
[`${file}: registerPerItemRoute(`, occurrences(text, /registerPerItemRoute\(/g)] as const,
].filter(([, count]) => count > 0))),
// [#23050 slice 13c] Over `all`, the population rule's own haystack, since
// a `/meta` module now carries helper call sites too (re-anchored, not
// relaxed: 6 here + 2 in `meta-item-write-routes.ts` = 8).
'registerPerItemRoute(': occurrences(all, /registerPerItemRoute\(/g),
'const registerPerItemRoute =': occurrences(src, /const\s+registerPerItemRoute\s*=/g),
// ⭐ The one control here that is NOT a whole-file count, deliberately:
// it is the SHAPE of the slice the population rule subtracts, and it is
Expand Down
22 changes: 19 additions & 3 deletions packages/rest/src/execctx-consumer-census.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ const SOURCE = readFileSync(resolve(HERE, 'rest-server.ts'), 'utf8');
* `meta-item-read-routes.ts`, with its one: the item read's, locally caught on
* its continuation line, as before — one of §7's and §8's four continuation
* sites, which those sections find there because they read every file here.
*
* [#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.
*/
const SITE_FILES = REST_SERVER_REGISTRAR_FILES;
const SOURCES = new Map<string, string>(SITE_FILES.map((file) => [
Expand All @@ -146,7 +150,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 and 13b), counted as that module's registrar —
* (#23050 slices 13a, 13b and 13c), 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 @@ -766,9 +770,21 @@ describe('[#13160] §2 the consumer surface, counted from the tree', () => {
// out, one forwarder's two mentions in). Fifteen forwarders now:
// (15 - 1) + 3 - 2, the module carrying one host literal that reads the
// seam, so neither map gains an entry.
expect(SITE_FILES).toHaveLength(15);
//
// [#23050 slice 13c] 62 sites UNCHANGED / 137 → 141 mentions — the same MOVE.
// The fifth `/meta` group, the per-item writes, left for
// `meta-item-write-routes.ts` with its two LOCALLY CAUGHT sites (the item
// save's and the reset's, each on its invocation line, as before):
// 10 + 9 + 5 + 7 + 8 + 1 + 5 + 2 + 1 + 1 + 1 + 0 + 6 + 3 + 1 + 2 = 62 sites. The
// four new mentions are this slice's seam, the two spellings once (the third
// host literal the inner method builds, its forwarder, two; the host
// interface's member, two): `rest-server.ts` stays at 55 (the two sites out,
// 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);
expect(SITES.length).toBe(62);
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(137);
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(141);
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 @@ -272,7 +272,11 @@ describe('[#20338] census: every draft switch in rest-server.ts is ledgered', ()
// with two more: the item read's `?state=draft` and `?preview=draft`. Keys
// name the route, so again no ledger row changed:
// 8 = 1 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3 + 2, length 14 → 15.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(15);
// [#23050 slice 13c] The per-item writes moved to `meta-item-write-routes.ts`
// 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);
expect(SOURCE_FILES.map((file) => file.fileName)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
expect(SITES).toHaveLength(8);
});
Expand Down
Loading
Loading