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-s13a-meta-collection-liveness-citations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/spec': patch
---

The liveness ledger's `/meta` type-list, per-type-list and maintenance switch rows cite the code that reads their keys where it now lives

Clause-②: no (only the rows' evidence paths move and every status stays `live`: `MetadataEndpointsConfigSchema` accepts `endpoints.types`, `endpoints.items` and `endpoints.maintenance` exactly as before, and no state count moves)

- `liveness/metadata_endpoints.json`: the `endpoints.types`, `endpoints.items` and `endpoints.maintenance` rows cited `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` for the gates on their mounts. `@objectstack/rest` moved those three route groups to `packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes` with no behaviour change, so those three path references follow them.
- The quoted code, the producers, the verdicts and the notes are unchanged. The `prefix` row keeps its citation: `metaPath` is still built in the inner method's head.
10 changes: 10 additions & 0 deletions .changeset/23050-s13a-meta-collection-routes-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/rest': patch
---

The `/meta` type list, whole-store maintenance and per-type list routes live in their own module, with no route, handler or answer changed

Clause-②: no (a move: the seven 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. The statements that register `GET {metaPath}` and `GET {metaPath}/types`, `GET {metaPath}/diagnostics`, `GET {metaPath}/_drafts`, `POST {metaPath}/_migrate-stored` and `GET {metaPath}/:type` moved, re-indented, to `packages/rest/src/meta-collection-routes.ts`, and the method calls them at their old position, inside the umbrella's guarded registration. The `metadata.endpoints.types`, `.maintenance` and `.items` switches gate 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/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/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/api-backend.json
Original file line number Diff line number Diff line change
Expand Up @@ -2446,7 +2446,7 @@
"source": [
"packages/spec/liveness/metadata_endpoints.json (the ADR-0049 ledger this item answers: prefix, enableCache, maskObjectFields and three endpoints.* live, cacheTtl and endpoints.schema dead)",
"packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema (the declared contract — four endpoints.* switches whose describe() strings enumerate every mount they gate since #15542/#15854, the ADR-0106 D8 docblock and the two retiredKey tombstones)",
"packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (metaPath, the four endpoint gates and registerPerItemRoute — the per-item switch spelled as a call so the later members of that face carry it), (resolveObjectMasker — the metaType/maskObjectFields fork), (normalizeConfig — isObjectSchemaMaskingEnabled and the OS_ALLOW_UNMASKED_OBJECT_METADATA escape hatch), (getRoutes) + packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints (discovery.routes.metadata built from prefix)",
"packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (metaPath, the item endpoint gate and registerPerItemRoute — the per-item switch spelled as a call so the later members of that face carry it), (resolveObjectMasker — the metaType/maskObjectFields fork), (normalizeConfig — isObjectSchemaMaskingEnabled and the OS_ALLOW_UNMASKED_OBJECT_METADATA escape hatch), (getRoutes) + packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes (the types, items and maintenance endpoint gates) + packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints (discovery.routes.metadata built from prefix)",
"ADR-0106 D8 (per-caller field masking of served object schemas) · #14691 (the two retirements) · #7526 (the /meta/types ordering fix the `types` switch takes with it) · ADR-0076 D12 · #15542 / #15854 (the switch-radius ruling that created `maintenance` and gave `item` its own writes) · #15543 (why the measured blast radius of that move is zero: no shipped boot path authors a RestServerConfig)",
"sibling clause: platform-core.metadata-registry-serving (it drives the served REGISTRY on the default config; this item covers the config that decides what is served and to whom) · platform-core.docs-audience-gate (the audience-gated types that are a cache carve-out here)"
],
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 @@ -1394,7 +1394,7 @@
"ref": "packages/metadata-protocol/src/protocol.diagnostics-store-outage.test.ts — pins the outage clause ONLY (503 rethrown, never '0 problems'); the live sweep, scoping parity, refusals, and both browser surfaces still need the run"
},
"source": [
"packages/rest/src/rest-server.ts#route (the route: registered BEFORE /meta/:type so 'diagnostics' is not captured as a type; severity defaults 'error'; ?type/?severity/?package each single-valued via refuseRepeatedQueryParams #6877; 501 NOT_IMPLEMENTED when the kernel's protocol lacks getMetaDiagnostics; answers res.json(result) BARE)",
"packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes (the route: registered BEFORE /meta/:type so 'diagnostics' is not captured as a type; severity defaults 'error'; ?type/?severity/?package each single-valued via refuseRepeatedQueryParams #6877; 501 NOT_IMPLEMENTED when the kernel's protocol lacks getMetaDiagnostics; answers res.json(result) BARE)",
"packages/rest/src/rest-route-ledger.ts#REST (REST-only route, bare body = GetMetaDiagnosticsResponseSchema, client meta.getDiagnostics, #12038)",
"packages/metadata-protocol/src/protocol.ts#getMetaDiagnostics (getMetaDiagnostics: registry-derived type set filtered to registered schemas; reuses the _diagnostics read decoration; stats { count, locked, packages } computed in the same sweep 'so the Studio directory page can render tile counts and a package filter in one round-trip'; 503 outage rethrown #8855 / ADR-0110 D3; 400 unrecognised-spelling rethrown #8924; warnings 'reserved for a future lint layer')",
"packages/metadata-protocol/src/metadata-diagnostics.ts#computeMetadataDiagnostics (computeMetadataDiagnostics — errors only, undefined = no opinion for schema-less types; zodIssuesToMetadataIssues names union branches #5598 so Studio has a path to highlight)",
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 @@ -1634,7 +1634,8 @@ describe('#9084 — the rest-server.ts transport tripwire keys off mechanics', (
// [#23050 slice 10] Length 10 → 11 (`ui-routes.ts`), still 0 over it.
// [#23050 slice 11] Length 11 → 12 (`form-routes.ts`), still 0 over it.
// [#23050 slice 12] Length 12 → 13 (`crud-routes.ts`), still 0 over it.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(13);
// [#23050 slice 13a] Length 13 → 14 (`meta-collection-routes.ts`), still 0 over it.
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(14);
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
64 changes: 59 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 @@ -240,7 +240,10 @@ import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// [#23050 slice 3] THE list of the files `RestServer`'s registrars live in —
// see the `rest-server.ts` row's population rule.
// [#23050 slice 13a] …and its named subset of the modules that hold a moved
// `/meta` group — see the same row's `reachable` rule.
import {
REST_SERVER_META_ROUTE_FILES,
REST_SERVER_REGISTRAR_DIR,
REST_SERVER_REGISTRAR_FILES,
} from '../../../rest/src/rest-server-registrar-files.js';
Expand Down Expand Up @@ -537,13 +540,17 @@ 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` and `crud-routes.ts`, the modules the bodies of ' +
'`search-routes.ts`, `ui-routes.ts`, `form-routes.ts`, `crud-routes.ts` and `meta-collection-routes.ts`, ' +
'the modules the bodies of ' +
'`registerDataActionEndpoints`, `registerBatchEndpoints`, `registerApprovalsEndpoints`, ' +
'`registerSharingEndpoints`, `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, ' +
'`registerSecurityExplainEndpoints`, `registerSecurityEndpoints`, `registerDiscoveryEndpoints`, ' +
'`registerOpenApiEndpoints`, `registerEmailEndpoints`, `registerSearchEndpoints`, ' +
'`registerUiEndpoints`, `registerFormEndpoints` and `registerCrudEndpoints` moved to; ' +
'reachable = those inside registerMetadataEndpoints',
'`registerUiEndpoints`, `registerFormEndpoints` and `registerCrudEndpoints`, and the first three `/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`), whose registrations the inner method ' +
'runs inside the umbrella\'s swap',
// [#15542 / #15854] ⭐ THE POPULATION RULE LEARNED A SECOND SPELLING, and
// the numbers it produces did NOT move: 80 / 19 / 61, exactly as before.
//
Expand Down Expand Up @@ -815,10 +822,37 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
// four new occurrences are this slice's seam, the same two spellings once
// (the host literal's forwarder, two; the host interface's member, two). No
// call site was added.
// [#23050 slice 13a] 70 / 19 / 51 UNCHANGED, and this time `reachable` is the
// reading that had to be taught: the first three `/meta` groups of
// `registerMetadataEndpointsInner` (six direct registration sites: `GET
// {metaPath}` and `/types`, `/diagnostics`, `/_drafts`, `POST
// /_migrate-stored` and `GET /:type`, all REACHABLE through the umbrella key
// `meta:rest-server.ts:registerMetadataEndpoints`) moved to
// `meta-collection-routes.ts`, which joins the list (length 13 -> 14) with
// its own control. The six still register inside the umbrella's swap — the
// inner method builds their host literal, capturing the wrapping registrar
// by value, and calls the module at their old position — but no line of
// them is lexically inside `registerMetadataEndpoints` any more, so the
// lexical rule alone read 70 / 13 / 57: six guarded routes counted as blind
// spots, a false reading. Measured, not argued, before the rule widened: an
// anonymous request at each of the six (both bases) answers `401` with the
// module in place, and hoisting the route manager's capture above the swap
// makes every one of them answer without the gate. The rule therefore adds
// every site in `REST_SERVER_META_ROUTE_FILES`, the list's NAMED `/meta`
// subset — ⛔ never a file-name convention — and the new control
// `/meta subset ↔ .call( inside registerMetadataEndpointsInner` holds that
// subset's length against the module calls inside the inner method, so a
// `/meta` module called without joining the subset (or a subset entry the
// inner method no longer calls) moves it. `this.routeManager.register(`
// stays 63 over the fourteen files
// (6 here + 9 + 5 + 7 + 8 + 1 + 6 + 4 + 1 + 1 + 1 + 2 + 6 + 6),
// `private register*Endpoints(` stays 16, `registerPerItemRoute(` stays 8 and
// `enforceAuth` stays 112: the moved groups spell neither term, and the seam
// spells neither (the umbrella's wrapper is what gates them).
controls: {
'private register*Endpoints(': 16,
'this.routeManager.register(': 63,
'registrar list length': 13,
'registrar list length': 14,
'data-action-routes.ts: this.routeManager.register(': 9,
'batch-routes.ts: this.routeManager.register(': 5,
'approvals-routes.ts: this.routeManager.register(': 7,
Expand All @@ -831,6 +865,11 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
'ui-routes.ts: this.routeManager.register(': 1,
'form-routes.ts: this.routeManager.register(': 2,
'crud-routes.ts: this.routeManager.register(': 6,
'meta-collection-routes.ts: this.routeManager.register(': 6,
// [#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.
'/meta subset ↔ .call( inside registerMetadataEndpointsInner': REST_SERVER_META_ROUTE_FILES.length,
// Both halves of the new rule carry their own control, so neither can go
// silently to zero: a helper deleted and its routes inlined back would
// still read population 80, and only these two controls would notice the
Expand Down Expand Up @@ -1324,9 +1363,21 @@ export function deriveProbeFileCensus(): {
const metaIdx = decls.findIndex((d) => d.text.includes('registerMetadataEndpoints'));
const start = decls[metaIdx]?.at ?? 0;
const end = decls[metaIdx + 1]?.at ?? src.length;
// [#23050 slice 13a] ONE REACH, NOW TWO PLACES. A `/meta` group moved out of
// `registerMetadataEndpointsInner` still registers inside the umbrella's
// swap — the inner method builds its host literal (the wrapping registrar
// captured by value) and calls its module from inside its own body — so its
// sites are as reachable as the ones still lexically inside the slice above.
// They are read off the registrar list's NAMED `/meta` subset, never off a
// file-name convention, and its control below holds that subset against the
// module calls inside the inner method (that method's own extent: from its
// declaration to the umbrella slice's end).
const metaModules = movedModules.filter(([file]) => REST_SERVER_META_ROUTE_FILES.includes(file));
const innerAt = src.search(/private\s+registerMetadataEndpointsInner\s*\(/);
const inner = innerAt < start || innerAt >= end ? '' : src.slice(innerAt, end);
files.set('packages/rest/src/rest-server.ts', {
population: sites(all),
reachable: sites(src.slice(start, end)),
reachable: sites(src.slice(start, end)) + metaModules.reduce((n, [, text]) => n + sites(text), 0),
controls: {
'private register*Endpoints(': occurrences(src, /private\s+register[A-Za-z]*Endpoints\s*\(/g),
'this.routeManager.register(': occurrences(all, /this\.routeManager\.register\(/g),
Expand All @@ -1346,6 +1397,9 @@ export function deriveProbeFileCensus(): {
// real population drop. The four counts around it cannot — all four are
// green while the slice is running 3873 lines long.
'forwarder slice: this.routeManager.register(': occurrences(forwarderSlice(src), mountRe),
// [#23050 slice 13a] The module calls inside the inner method — one per
// moved `/meta` group's module — recorded against the subset's length.
'/meta subset ↔ .call( inside registerMetadataEndpointsInner': occurrences(inner, /\.call\(/g),
enforceAuth: occurrences(all, /enforceAuth/g),
},
});
Expand Down
Loading
Loading