Skip to content

Commit d0f7eb5

Browse files
refactor(rest): move registerSecurityExplainEndpoints and registerSecurityEndpoints into security-routes.ts (#23050 slice 6) (#23729)
Part of #23050 Clause-②: no Slice 6 of the `rest-server.ts` split, by slice 1's pattern (landed note `6136102437`, steps 1–6), slice 2's getter refinement (`6189011700`) and slices 3–5's shape (PR #23670, PR #23705's two registrars in one module, PR #23719): the bodies of `RestServer#registerSecurityExplainEndpoints` (`GET` / `POST {basePath}/security/explain`, `GET {basePath}/security/my-delegable-scope`) and `RestServer#registerSecurityEndpoints` (`GET {basePath}/security/suggested-bindings` and its `/:id/confirm` and `/:id/dismiss`) move, unedited, to ONE new module, `packages/rest/src/security-routes.ts`. It is move-only: routes, the ordered registrations, every handler, the published `dist/*.d.ts` and both bundles' export lists are byte-identical, and every reader whose reading included a moved line now reads it where it is, with its sum unchanged. `rest-server.ts` goes from 11,042 to 10,636 lines (base `a515abdd8c`). The card stays open for the later slices. ## The pattern, applied **Members the moved bodies read**, from an AST walk of each method (TypeScript compiler API, base `a515abdd8c`): `registerSecurityExplainEndpoints` (JSDoc `:10282`, method `:10308`-`:10601`, body `:10309`-`:10600`) and `registerSecurityEndpoints` (JSDoc `:10662`, method `:10694`-`:10838`, body `:10695`-`:10837`). | kind | member | reads (explain / suggested-binding) | class declaration | |---|---|---|---| | field | `routeManager` | 3 / 3, the `register` calls | `:2010`, private | | field | `kernelManager` | 2 / 0, inside the explain family's `resolveService` | `:2019`, private, not `readonly` | | field | `securityServiceProvider` | 2 / 2, inside each family's `resolveService` | `:2089`, private, not `readonly` | | method | `resolveRequestEnvironmentId` | 1 / 0 | `:2306`, private | | method | `resolveExecCtx` | 2 / 3 | `:2856`, private | | method | `enforceAuth` | 2 / 3 | `:2456`, private | Neither body has a bare `this`, an element access on `this`, or a `this`-rebinding `function`. Their module-level reads: - **Imports still used elsewhere in `rest-server.ts`** (AST count outside both methods): `logError` (12), `sendError as sendEnvelopeError` (4) and `refuseRepeatedQueryParams` (15). They stay there; the module imports the same names from the same modules. Its `sendError` import carries a comment pointing at the dialect note on `rest-server.ts`'s import, which the explain family's emitter docblock names. - **Imports ONLY these bodies read** (zero uses outside the two methods): `ErrorCode` (type, `@objectstack/spec/api`, with its `[#8073]` comment), `standardErrorCodeForHttpStatus` (`@objectstack/spec/api`, `[#11683]`), `classifiedRefusalAnswer`, `nestedRefusalEnvelope`, `sandboxFaultAnswer` (`./error-response.js`), and `isAudienceBindingSuggestionStatus` with `unknownAudienceBindingSuggestionStatusMessage` (`@objectstack/core`, `[#7678]`). They moved with the bodies, as slices 3 and 5 moved theirs, and each left a pointer comment. Leaving them would fail `noUnusedLocals`. The `[#8073]` and `[#7678]` comments moved verbatim. The `[#11683]` comment pointed at `registerSharingEndpoints` as its use, so in the module it names the explain family's classified-refusal arm, the use it has there. - **Nothing declared in `rest-server.ts` is read**, so no `export` was added anywhere. The explain body's dynamic `import('@objectstack/spec/security')` moved inside it. **Host shape (slice 1's shape B, twice, as slice 4):** one host interface per registrar, `SecurityExplainRouteHost` and `SecurityRouteHost`. Each lists exactly the members its body reads, typed `RestServer['m']`. The class keeps both private methods with their names, docblocks and positions. Each body is now a host literal plus `.call(host, basePath)`. Methods go as late-bound closures, `(...a) => this.m(...a)`. The call sites in `registerRoutes` (`:4812`-`:4813` at base) are unchanged. **Fields, checked one by one:** | field | read when | reassigned after registration? | handed over as | |---|---|---|---| | `routeManager` | registration only (six `register` calls, top-level statements of the two bodies) | its one swap (`:5719` / `:5723`) wraps `registerMetadataEndpointsInner`, not these registrars | value | | `kernelManager` | **request time** (the explain family's `resolveService`) | not by the class (constructor only), but not `readonly` | **getter** | | `securityServiceProvider` | **request time** (each family's `resolveService`) | not by the class (constructor only), but not `readonly` | **getter**, in both host literals | The getters are measured, not argued. A one-off vitest file (copied in, run, deleted) boots a source `RestServer`, drives the door, assigns the field on the instance AFTER `registerRoutes()`, and drives it again: - this head (`96504dab66`): `securityServiceProvider` on the explain family (`GET /api/v1/security/my-delegable-scope`) reads `501 NOT_IMPLEMENTED`, then `200 {"businessUnits":["bu_1"],"positions":[]}`. On the suggested-binding family (`GET /api/v1/security/suggested-bindings`) it reads `501 NOT_IMPLEMENTED`, then `200 {"data":[{"id":"sug_1"}]}`. `kernelManager` on the scoped explain door (`environmentId: env_1`) reads `501`, then `200 {"env":"env_1"}`. `Tests 3 passed (3)`. - by-value ablations through `scripts/ablation-replace.mjs`, each getter in turn replaced by `X: this.X,` with a two-line anchor that names its host literal. Each landed: anchor 1 → 0, replacement 0 → 1, blob `4867a14302` → `5c7ebad6df` / `48f2660f00` / `f3c9b53365`. In each run the ablated door reads `501` then `501` (frozen at registration) while the other two still read `200` (`Tests 1 failed | 2 passed (3)`, each run). Restored by the tool and a trap: blob `4867a14302` equals `HEAD`, `git diff HEAD` empty, the one-off file deleted. The path is source (relative import), so no `dist` leg applies. **Mechanism hypotheses from the dispatch, measured:** - (a) **Confirmed.** The explain family resolves the security service at request time through its `resolveService`. That reads TWO fields: the environment kernel's `security` service via `kernelManager` first, then `securityServiceProvider` as the fallback. Naming a principal other than the caller is the service's question (`explainAccessForCaller`); the door hands it the parsed request and the caller's context and reads no further provider field. Both fields go as getters. The suggested-binding family reads `securityServiceProvider` only, also a getter, measured separately. - (b) **Confirmed.** `registerSecurityEndpoints` builds its paths from `const dataPath = basePath;`, so the suggested-binding doors answer at `{basePath}/security/suggested-bindings`, not under `/data` (slice 4's share family appended `crud.dataPrefix`; this one does not). Every path expression moved unedited, and the route dumps below are identical. - (c) **Confirmed.** The bodies, and the `[#7678]` import comment that moved with them, carry 10 `ADR-NNNN Dk` citations: ADR-0090 D5 ×2, D6 ×2 and D12 ×2, ADR-0105 D8 ×2, and ADR-0112 D5 ×2. They left the anchored `rest-server.ts`, so `check-adr-anchors` read 1761 decision-letter citations at base and 1751 at the move-only commit `d6fc91a97d`, **green both times**. That is the silent narrowing the card forbids. A new anchor (below) restores 1761 at head. ## The registrar list `packages/rest/src/rest-server-registrar-files.ts` gains `security-routes.ts` with a `[#23050 slice 6]` comment (length 6 → 7). Every reader that pins its length re-reads its number, and every sum is unchanged: | reader | sum over the list | change | |---|---|---| | `rest-server-query-number-census.test.ts` | 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 | length 6 → 7 | | `rest-server-canonical-query-ast.test.ts` | 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0 `query:` slots | floor `security-routes.ts: 0`, length 6 → 7 | | `meta-draft-read-door-census.test.ts` | 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 | the two bodies read no draft; length 6 → 7 | | `rest-server-docblock-position.test.ts` | floors 4 + 3 + 3 + 8 + 3 + 8 = 29 | floor `security-routes.ts: 8`: header, two host interfaces, two functions, and the three blocks that moved with the bodies (the explain family's `respondError` and `delegableHandler` blocks, the suggested-binding family's `respondError` block). `rest-server.ts` 167 → 164 blocks, floor 100; length 6 → 7 | | `execctx-consumer-census.test.ts` | 62 sites = 27 + 9 + 5 + 7 + 8 + 1 + 5 | mentions 101 → 109 (the seam twice: two forwarders ×2, two interface members ×2); forwarders 8 = (7 − 1) + `EXTRA_MOVED_REGISTRARS` 2, the map gaining `security-routes.ts: 1` beside `sharing-routes.ts: 1`, as slice 4 counted two registrars in one module | | `ui-view-route-identity.measurement.test.ts` / `ui-view-route-tenancy.measurement.test.ts` | `this.enforceAuth(` 55, `this.resolveExecCtx(` 70 over the list (53 / 68 before, +1 each per new forwarder); ownership guard 1 | length 6 → 7 | | dogfood `authz-conformance.test.ts` (realtime tripwires) | 0 hits over the seven files | length 6 → 7 | | dogfood `authz-probe-blind-spot.census.ts`, `rest-server.ts` row | 70 / 19 / 51 unchanged; `this.routeManager.register(` 63 = 27 + 9 + 5 + 7 + 8 + 1 + 6; `private register*Endpoints(` 16 | new control `security-routes.ts: this.routeManager.register(` 6; `registrar list length` 7; `enforceAuth` 84 → 92 re-anchored (five call sites moved and are counted in both readings; the eight new occurrences are the seam twice; no call site added); `populationRule` names the new module | Each number above was also counted statically on the tree (`grep -oF` per file). ## Proof of move-only - **`.d.ts`:** `packages/rest/dist/index.d.ts` and `index.d.cts` built at base `a515abdd8c` (this worktree, before any edit), at `96504dab66` and at the merge head `a12cdcf4e4` are byte-identical: sha256 `7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9` for all six files, `cmp` silent. The head bundles carry a head-only marker (`readSecurityServiceProvider`, 4 hits in each of `index.js` and `index.cjs`), so the declaration was rebuilt from the moved source, not replayed. `SecurityExplainRouteHost`, `SecurityRouteHost` and `security-routes` appear 0 times in the `.d.ts`; `src/index.ts` is untouched. - **Export lists:** the base and head ESM and CJS bundles each load with 39 exports and one sorted export-name list (sha256 `b3246be6a171…` on all four), and none of the two functions or two host types is among them. - **Routes:** a source `RestServer` on a recording mock server, with a protocol that answers every member, dumped under four configs at base and at head: `getRoutes()` (functions as arity), the ordered server registration calls, and a whitespace-free sha256 of every handler's source. **12 dumps of 12 are byte-identical**, at `96504dab66` and again at the merge head. default: routes `6931d1a5d182…`, calls `ccb3bd12f10b…`, handlers `6d4926ef1465…` (75 routes, 6 security); scoped-optional: `275c72f46c30…` / `339e8c3abc05…` / `92790e91927d…` (150, 12); scoped-required: `a2aa8183e213…` / `734f33133150…` / `f6e3fd853aea…` (75, 6); every-provider-set equals default. A second base run was identical to the first. - **The handler digest can fail:** a positive control changed one character of one moved handler (`Delegable scope error:` → `!`, through `ablation-replace.mjs`; anchor 1 → 0, blob `01fe3b195d` → `0c2c62e946`). Routes and calls were unchanged, and the handler list differed in exactly `GET /api/v1/security/my-delegable-scope` (and its scoped twin). Restored, blob `01fe3b195d` equals `HEAD`. - **Moved bodies:** base `rest-server.ts` lines 10309-10600 and 10695-10837, one 4-space indent removed, against `security-routes.ts` lines 110-401 and 411-553: `diff` exits 0 both times (292 + 143 lines). No moved line has an odd number of backticks, so no template literal spans the cut and re-indenting changes no runtime string. - **Moved-line accounting:** `git diff --color-moved=plain --color-moved-ws=allow-indentation-change` base → move commit over the two files: 442 deleted and 470 added lines detected as moved. `rest-server.ts` is +43 / −449: the two stub bodies (host literal plus `.call`), the slice-6 import and its comment, and four import-pointer comments. ## Readers re-pointed (sums unchanged, no gate loosened) - `scripts/check-route-envelope.mjs` (a gate's target paths, `domain:spec`): `security-routes.ts` is discovered by the `*-routes.ts` convention and declared `dialectOnly` with ratchet #9559, as the five earlier modules are, at its measured `stringError 0`, `siblingCode 0`. Every arm of both families emits the nested `{ error: { code, message } }` through its family's one emitter (the emitters #7981 and #8073 introduced). So `rest-server.ts` stays at `stringError 17`, `siblingCode 17`, and the sums 17 + 13 + 8 + 1 + 0 + 0 + 0 = 39 and 17 + 15 + 8 + 9 + 4 + 1 + 0 = 54 are unchanged. Output differs from base only in the new row (16 → 17 modules, 7 → 8 ratcheted). `--self-test` green. - `scripts/adr-anchors/packages__rest__src__security-routes.ts.json` (**new**, ADR-0090 + ADR-0105 + ADR-0112): `check-adr-anchors` decision-letter citations on the anchored surface 1761 (base) → 1751 (move) → 1761 (head); anchored files 67 → 68. Every listed id appears in the module, and every cited letter is a decision of its record (the gate's own check, green). The invariant writes "its D6", never "ADR-0090 D6", so it adds no citation of its own (measured with the gate's regex: 0). - The nine registrar-list readers above. - `error-response.ts`, the `nestedRefusalEnvelope` docblock (rest lane, not in the published `.d.ts`): it named `rest-server.ts` as the home of three families, the suggested-binding one among them. Since slices 3, 4 and this one moved all three, the sentence now names each family's current home. **Red before the re-points**, each measured with the move in place (`d6fc91a97d`): the six `local` list readers read `Test Files 6 failed (6)`, `Tests 7 failed | 103 passed (110)` (five length pins, plus the docblock pin's length pin and its new module with no floor). `rest-server-canonical-query-ast.test.ts` (`repo`) read `Tests 2 failed | 53 passed | 7 skipped (62)`. The dogfood pair read `Tests 2 failed | 106 passed (108)` (list length; the census row's `registrar list length` control). `tsc --noEmit` was clean, but `check:test-typecheck` found 2 type errors, the two typed floor tables without the new module. `check:route-envelope` read `security-routes.ts NOT DECLARED` (MODULES and EXPRESS_RESPONSE_MODULES). `check-adr-anchors` was green at 1751. ## Readers examined and not re-pointed - `packages/spec` readers: none reads a moved line by path. No liveness anchor names either registrar or a symbol of the moved bodies. `packages/spec/liveness/doc.json`'s note names `this.registerSecurityEndpoints(bp)`, a call site that stays, as dated history. The `packages/spec` pointers slices 1–4 carried to the CRUD-doors slice are unchanged by this slice. - `objectql-slot-consumer-census`, `auth-gate-allowlist-fault-window`, `execctx-authz-input-seam-reachability`, runtime `meta-list-projection-parity`, core `authz-store-unavailable` and `check-single-authz-resolver`'s `DELEGATORS` row: they read members or blocks of `rest-server.ts` the moved bodies do not touch. - No `rest-server.ts#symbol` anchor in the tree names either registrar or a symbol only the moved ranges hold. No `rest-server.ts:NNNN` line citation falls in or after the moved ranges (the highest is `:5872`). `docs/qa/platform-checklist` has no anchor on these doors. - Gate outputs, base `a515abdd8c` vs merge head `a12cdcf4e4`, identical: `check-system-context-census`, `check-single-authz-resolver`, `check-rest-log-declared`, `check:test-source-alias`, `check:rest-log-spy-declared`, `check:platform-checklist`, `check-filter-alias-parity`, `check:liveness`, `check:cross-package-test-inputs`. Identical except the scanned-file count: `check-meta-type-normalized` (+1, the new module), `check-dispatcher-error-vocabulary` (+1; 57 sites, 57 classified) and `check:error-status-conformance` (+2, the new module and one file from `main`; 463 producer sites). `check-undeclared-dep-imports` +2 specifiers and 1 file (the module's package imports). `check-scripts-symbol-anchors` +4 file-level anchors (the gate's new comments). `check-affected-docs` 24 → 25 route sources. The other differences come from `main`'s two merged commits (`check-durability-degradation-log-level`: one `objectql/src/plugin.ts` line shift; `check:nul-bytes` and `check:doc-authoring`: new files). ## Acceptance notes - `rest-server.ts`'s `resolveSecurityService` docblock (about `:9506`) says it "Mirrors the resolver in registerSecurityExplainEndpoints". That docblock ships in the published `.d.ts`, so editing it would break the byte-identity this slice must keep. It is not edited, and the name still resolves to the function of that name in `security-routes.ts`, which holds the resolver. - Other-lane prose that names `rest-server.ts` for the suggested-binding route, read by nothing: `packages/core/src/security/audience-binding-suggestion-status.ts` (about `:10`, and about `:18`, which says `rest-server.ts` imports the vocabulary; the importer is now `security-routes.ts`) and `packages/runtime/src/domains/security.ts` (about `:144`). Not edited (承接者:无). - `docs/adr/0112-…`'s Consumers line names `rest-server.ts` for `@objectstack/rest` as a whole, not as these doors' home. It is a governed surface and is not edited. - `scripts/check-route-envelope.mjs`'s count-history comments for `rest-server.ts` (the #7981 and #8073 entries) say the two registrars sat in that file when those counts moved. They are dated history, unchanged. - `docs/adr/0076-…` (status line and D11) and `docs/adr/0111-…` (Consumers line) are still carried to the maintainer by the seat; this slice adds nothing to them. ## Gates and tests Measured at this branch: base `a515abdd8c`, the move `d6fc91a97d`, the re-points `96504dab66`, and one merge of `origin/main` `f40d877f81` (`a12cdcf4e4`, two commits in `objectql`, `plugins/organizations` and one dogfood test, no file in this diff). All builds and tests went through `os-verify-lock`. Every exit code was captured to a file before any pipe. - At `a12cdcf4e4`: `pnpm install --frozen-lockfile` exit 0. `pnpm turbo run build --concurrency=2`, the whole workspace, exit 0 (76 tasks, 33 cached). - `pnpm --filter @objectstack/rest typecheck`: exit 0 (`tsc --noEmit`, then `check:test-typecheck`: 0 file(s) / 0 error(s)), at `96504dab66` and at `a12cdcf4e4`. - `pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2`: Test Files 285 passed (285), Tests 5870 passed / 326 skipped (6196), at both heads. - `pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2`: Test Files 5 passed (5), Tests 234 passed / 7 skipped (241), at both heads. - `pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts`: Test Files 2 passed (2), Tests 108 passed (108), at both heads. - `dispatch-gates --commands --repo objectstack-ai/objectstack`, no paths, at the final head `a12cdcf4e4` (stderr names that commit; the `--repo` assertion holds): 79 commands, all run at `a12cdcf4e4`, all exit 0. `--ran`: `Run reconciliation — 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN.` Among them are `pnpm check:route-envelope` (its `--self-test`, then the scan), `pnpm check:adr-anchors`, `pnpm check:durability-log-level`, `pnpm check:dispatcher-error-vocabulary`, `pnpm check:dts-closure`, `pnpm check:dual-build-cjs-loads` and `pnpm check:pm-dispatch-gates`. - Lint, the proven narrowing: `eslint --no-inline-config --format json` over `packages/rest`, the two dogfood files, `scripts/check-route-envelope.mjs` and the new anchor JSON: 330 files, 0 errors, 0 lint warnings. Its one warning is `File ignored because no matching configuration was supplied` for the JSON, which `eslint .` never reads. All 15 changed files are in the result set at 0 / 0. The published `.d.ts` is byte-identical, so no consumer outside `packages/rest` can lint differently. Repo-wide `pnpm lint` is CI's. - `check:dev-prereqs` and `check:skill-frame-freshness` (local-only) were not run: no `packages/spec` build input and no four-axis frame file is touched. - No changeset: nothing any released package publishes changes (`.d.ts`, export lists and routes byte-identical above). `skip-changeset` is the seat's to apply on its own check. **Cross-lane files in this diff:** `scripts/check-route-envelope.mjs` (`domain:spec`, a gate's target paths), `scripts/adr-anchors/packages__rest__src__security-routes.ts.json` (a new entry in `check-adr-anchors`' registry, `domain:devx`), and the dogfood pair (`packages/qa/dogfood/test/authz-conformance.test.ts`, `authz-probe-blind-spot.census.ts`). No platform-checklist file is edited. --- _Generated by [Claude Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d0c09e4 commit d0f7eb5

15 files changed

Lines changed: 721 additions & 469 deletions

‎packages/qa/dogfood/test/authz-conformance.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1627,7 +1627,8 @@ describe('#9084 — the rest-server.ts transport tripwire keys off mechanics', (
16271627
// text is every file on it, in order — so the zero is not a list ignored.
16281628
// [#23050 slice 4] Length 4 → 5 (`sharing-routes.ts`), still 0 over it.
16291629
// [#23050 slice 5] Length 5 → 6 (`analytics-routes.ts`), still 0 over it.
1630-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(6);
1630+
// [#23050 slice 6] Length 6 → 7 (`security-routes.ts`), still 0 over it.
1631+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
16311632
expect(REST_SERVER_REGISTRAR_PATHS[0]).toBe(REST_SERVER);
16321633
const listed = REST_SERVER_REGISTRAR_PATHS.map((file) => readFileSync(join(REPO_ROOT, file), 'utf8'));
16331634
for (const probe of [MECHANICS, ROUTES]) {

‎packages/qa/dogfood/test/authz-probe-blind-spot.census.ts‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,10 +519,11 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
519519
'indentation and the subtrahend is pinned at 1 by the `forwarder slice:` control, never inferred from a ' +
520520
'terminator spelling), PLUS `registerPerItemRoute(` call sites; counted over this file AND every ' +
521521
'module on the registrar list (`packages/rest/src/rest-server-registrar-files.ts`) — ' +
522-
'`data-action-routes.ts`, `batch-routes.ts`, `approvals-routes.ts`, `sharing-routes.ts` and ' +
523-
'`analytics-routes.ts`, the modules the bodies of `registerDataActionEndpoints`, `registerBatchEndpoints`, ' +
524-
'`registerApprovalsEndpoints`, `registerSharingEndpoints`, `registerSharingRuleEndpoints` and ' +
525-
'`registerAnalyticsEndpoints` moved to; ' +
522+
'`data-action-routes.ts`, `batch-routes.ts`, `approvals-routes.ts`, `sharing-routes.ts`, ' +
523+
'`analytics-routes.ts` and `security-routes.ts`, the modules the bodies of `registerDataActionEndpoints`, ' +
524+
'`registerBatchEndpoints`, `registerApprovalsEndpoints`, `registerSharingEndpoints`, ' +
525+
'`registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, `registerSecurityExplainEndpoints` and ' +
526+
'`registerSecurityEndpoints` moved to; ' +
526527
'reachable = those inside registerMetadataEndpoints',
527528
// [#15542 / #15854] ⭐ THE POPULATION RULE LEARNED A SECOND SPELLING, and
528529
// the numbers it produces did NOT move: 80 / 19 / 61, exactly as before.
@@ -703,23 +704,37 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
703704
// counted in both readings; the four new occurrences are this slice's
704705
// seam, the same two spellings once (the host literal's forwarder, two;
705706
// the host interface's member, two). No call site was added.
707+
// [#23050 slice 6] 70 / 19 / 51 UNCHANGED, the same measurement again:
708+
// `registerSecurityExplainEndpoints`' and `registerSecurityEndpoints`'
709+
// bodies (six registration sites, three and three, all outside
710+
// `registerMetadataEndpoints`, so all six in the blind spot) moved to
711+
// `security-routes.ts`, which joins the list (length 6 -> 7) with its own
712+
// control. `this.routeManager.register(` stays 63 over the seven files
713+
// (27 here + 9 + 5 + 7 + 8 + 1 + 6), `private register*Endpoints(` stays
714+
// 16 (both registrars are still declared here), and `enforceAuth` 84 -> 92
715+
// is RE-ANCHORED, not relaxed: the five call sites moved with their routes
716+
// and are counted in both readings; the eight new occurrences are this
717+
// slice's seam, the same two spellings TWICE, because the module carries
718+
// two registrars (two host literals' forwarders, four; two host
719+
// interfaces' members, four). No call site was added.
706720
controls: {
707721
'private register*Endpoints(': 16,
708722
'this.routeManager.register(': 63,
709-
'registrar list length': 6,
723+
'registrar list length': 7,
710724
'data-action-routes.ts: this.routeManager.register(': 9,
711725
'batch-routes.ts: this.routeManager.register(': 5,
712726
'approvals-routes.ts: this.routeManager.register(': 7,
713727
'sharing-routes.ts: this.routeManager.register(': 8,
714728
'analytics-routes.ts: this.routeManager.register(': 1,
729+
'security-routes.ts: this.routeManager.register(': 6,
715730
// Both halves of the new rule carry their own control, so neither can go
716731
// silently to zero: a helper deleted and its routes inlined back would
717732
// still read population 80, and only these two controls would notice the
718733
// shape moved and force this provenance to be re-read.
719734
'registerPerItemRoute(': 8,
720735
'const registerPerItemRoute =': 1,
721736
'forwarder slice: this.routeManager.register(': 1,
722-
enforceAuth: 84,
737+
enforceAuth: 92,
723738
},
724739
note:
725740
'The single non-tripwire probe names ONE registrar of 16. The other 15 can never mint a key: ' +

‎packages/rest/src/error-response.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,9 @@ export function sandboxFaultAnswer(
760760
* ## Why it exists
761761
*
762762
* Three hand-built route families in `rest-server.ts` — the sharing-rule, the
763-
* suggested-binding (and the since-retired overlay-discard) and the approval arms — answered a
763+
* suggested-binding (and the since-retired overlay-discard) and the approval arms, which
764+
* have since moved to `sharing-routes.ts`, `security-routes.ts` and `approvals-routes.ts`
765+
* (#23050 slices 4, 6 and 3) — answered a
764766
* refusal their own arms could not read as a server fault, relaying
765767
* `.message`. For a sandboxed hook's refusal `.message` is the QuickJS debug
766768
* wrapper, so each answered `500` with `hook '<name>' threw: Error: …` where

‎packages/rest/src/execctx-consumer-census.test.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ const SOURCE = readFileSync(resolve(HERE, 'rest-server.ts'), 'utf8');
9393
*
9494
* [#23050 slice 5] …and `registerAnalyticsEndpoints`' body to
9595
* `analytics-routes.ts`, with its one.
96+
*
97+
* [#23050 slice 6] …and `registerSecurityExplainEndpoints`' and
98+
* `registerSecurityEndpoints`' bodies to `security-routes.ts`, with their five
99+
* (two and three) — the second module on the list to carry TWO registrars.
96100
*/
97101
const SITE_FILES = REST_SERVER_REGISTRAR_FILES;
98102
const SOURCES = new Map<string, string>(SITE_FILES.map((file) => [
@@ -104,7 +108,8 @@ const siteKey = (file: string, line: number) => `${file}:${line}`;
104108
* The frames in `rest-server.ts` that are NOT consumers: the host literal each
105109
* moved registrar (`registerDataActionEndpoints`, `registerBatchEndpoints`,
106110
* `registerApprovalsEndpoints`, `registerSharingEndpoints`,
107-
* `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`) hands
111+
* `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`,
112+
* `registerSecurityExplainEndpoints`, `registerSecurityEndpoints`) hands
108113
* its moved body forwards each member to the server with
109114
* `(...a) => this.<member>(...a)`, so a moved site's stack shows that forwarder
110115
* above the site itself. The site is the first censused frame that is not one.
@@ -120,9 +125,12 @@ const FORWARDERS = new Set(FORWARDER_LINES.map((line) => siteKey('rest-server.ts
120125
* [#23050 slice 4] The moved registrars beyond one per moved module:
121126
* `sharing-routes.ts` carries two registrars' bodies, each handed over by its
122127
* own host literal and so through its own forwarder.
128+
*
129+
* [#23050 slice 6] `security-routes.ts` carries two as well, on the same terms.
123130
*/
124131
const EXTRA_MOVED_REGISTRARS: Readonly<Partial<Record<(typeof SITE_FILES)[number], number>>> = {
125132
'sharing-routes.ts': 1,
133+
'security-routes.ts': 1,
126134
};
127135

128136
/** A stack frame in any censused file, as `<file>:<line>:<column>`. */
@@ -598,11 +606,21 @@ describe('[#13160] §2 the consumer surface, counted from the tree', () => {
598606
// mentions are this slice's seam, the same two spellings once (its host
599607
// literal's forwarder, two; its host interface's member, two). Six
600608
// forwarders now, one per moved registrar: (6 - 1) + 1.
601-
expect(SITE_FILES).toHaveLength(6);
609+
//
610+
// [#23050 slice 6] 62 sites UNCHANGED / 101 → 109 mentions — the same MOVE.
611+
// `registerSecurityExplainEndpoints`' and `registerSecurityEndpoints`'
612+
// bodies left for `security-routes.ts` with their five BARE sites (two
613+
// and three): 27 + 9 + 5 + 7 + 8 + 1 + 5 = 62 sites. The eight new
614+
// mentions are this slice's seam, the same two spellings TWICE, as slice
615+
// 4's (two host literals' forwarders, four; two host interfaces'
616+
// members, four). Eight forwarders now, one per moved registrar:
617+
// (7 - 1) + 2, the second module carrying two registrars counted in
618+
// `EXTRA_MOVED_REGISTRARS`.
619+
expect(SITE_FILES).toHaveLength(7);
602620
expect(SITES.length).toBe(62);
603-
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(101);
621+
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(109);
604622
const extraMovedRegistrars = Object.values(EXTRA_MOVED_REGISTRARS).reduce((n, k) => n + (k ?? 0), 0);
605-
expect(extraMovedRegistrars).toBe(1);
623+
expect(extraMovedRegistrars).toBe(2);
606624
expect(FORWARDER_LINES, 'one host-literal resolveExecCtx forwarder per moved registrar: one was respelt or removed, or a module moved without joining SITE_FILES')
607625
.toHaveLength(SITE_FILES.length - 1 + extraMovedRegistrars);
608626
});

‎packages/rest/src/meta-draft-read-door-census.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,10 @@ describe('[#20338] census: every draft switch in rest-server.ts is ledgered', ()
246246
// and `req.query?.preview === 'draft'`, admitted in one declaration)
247247
// moved with its body to `analytics-routes.ts`. A key names its route,
248248
// never its file, so no ledger row changed: 8 = 6 + 0 + 0 + 0 + 0 + 2,
249-
// length 5 → 6.
250-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(6);
249+
// length 5 → 6. [#23050 slice 6] The two security bodies
250+
// (`security-routes.ts`) read no draft: 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0,
251+
// length 6 → 7.
252+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
251253
expect(SOURCE_FILES.map((file) => file.fileName)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
252254
expect(SITES).toHaveLength(8);
253255
});

‎packages/rest/src/rest-server-canonical-query-ast.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ interface CensusEntry {
135135
* [#23050 slice 5] 4 = 1 + 3 + 0 + 0 + 0 + 0: the analytics dataset body
136136
* (`analytics-routes.ts`) builds no `query:` literal either — it hands
137137
* `queryDataset` the caller's own `selection`, never a `query:` slot.
138+
*
139+
* [#23050 slice 6] 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0: the two security bodies
140+
* (`security-routes.ts`) build no `query:` literal either — the explain doors
141+
* hand the security service a parsed explain request, and the suggestion list
142+
* a `status` / `packageId` pair read off `req.query`, never a `query:` slot.
138143
*/
139144
const REGISTRAR_QUERY_SLOT_FLOORS: Readonly<Record<RestServerRegistrarFile, number>> = {
140145
'rest-server.ts': 1,
@@ -143,6 +148,7 @@ const REGISTRAR_QUERY_SLOT_FLOORS: Readonly<Record<RestServerRegistrarFile, numb
143148
'approvals-routes.ts': 0,
144149
'sharing-routes.ts': 0,
145150
'analytics-routes.ts': 0,
151+
'security-routes.ts': 0,
146152
};
147153

148154
const CENSUS: CensusEntry[] = [
@@ -332,7 +338,8 @@ describe('[#16638] §1 CONTROLS on the census instrument itself', () => {
332338
// A module added to the list makes this re-read its number rather than
333339
// widening unnoticed. [#23050 slice 4] Length 4 → 5, the sum unchanged.
334340
// [#23050 slice 5] Length 5 → 6, the sum unchanged.
335-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(6);
341+
// [#23050 slice 6] Length 6 → 7, the sum unchanged.
342+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
336343
expect(Object.keys(REGISTRAR_QUERY_SLOT_FLOORS).sort()).toEqual([...REST_SERVER_REGISTRAR_FILES].sort());
337344
expect(CENSUS.filter(({ file }) => REGISTRAR_SOURCES.has(file)).map(({ file }) => file))
338345
.toEqual([...REST_SERVER_REGISTRAR_FILES]);

‎packages/rest/src/rest-server-docblock-position.test.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,21 @@ describe('rest-server.ts docblock position', () => {
153153
* approval modules do: the module header, the host interface's block and the
154154
* function's. The analytics body held no TSDoc block of its own, so
155155
* `rest-server.ts` keeps every block it had.
156+
*
157+
* [#23050 slice 6] `security-routes.ts` carries eight: the module header, two
158+
* host interfaces' blocks and two functions' (it holds two registrars' bodies),
159+
* plus the three blocks that moved with those bodies (the explain family's
160+
* `respondError` and `delegableHandler` blocks, and the suggested-binding
161+
* family's `respondError` block), which `rest-server.ts` therefore no longer
162+
* holds (167 → 164 there, still far above its floor).
156163
*/
157164
const MOVED_FILE_FLOORS: Readonly<Record<Exclude<RestServerRegistrarFile, typeof FILE>, number>> = {
158165
'data-action-routes.ts': 4,
159166
'batch-routes.ts': 3,
160167
'approvals-routes.ts': 3,
161168
'sharing-routes.ts': 8,
162169
'analytics-routes.ts': 3,
170+
'security-routes.ts': 8,
163171
};
164172
const MOVED_FILES: ReadonlyArray<{ file: string; minBlocks: number }> = REST_SERVER_REGISTRAR_FILES
165173
.filter((file): file is Exclude<RestServerRegistrarFile, typeof FILE> => file !== FILE)
@@ -189,9 +197,10 @@ describe('[#23050 slice 3] the registrar list this pin reads', () => {
189197
// widening unnoticed: 4 + 3 + 3 = 10 blocks over the three modules.
190198
// [#23050 slice 4] 4 + 3 + 3 + 8 = 18 over four, length 4 → 5.
191199
// [#23050 slice 5] 4 + 3 + 3 + 8 + 3 = 21 over five, length 5 → 6.
192-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(6);
200+
// [#23050 slice 6] 4 + 3 + 3 + 8 + 3 + 8 = 29 over six, length 6 → 7.
201+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
193202
expect(Object.keys(MOVED_FILE_FLOORS).sort())
194203
.toEqual(REST_SERVER_REGISTRAR_FILES.filter((file) => file !== FILE).sort());
195-
expect(MOVED_FILES.reduce((n, { minBlocks }) => n + minBlocks, 0)).toBe(21);
204+
expect(MOVED_FILES.reduce((n, { minBlocks }) => n + minBlocks, 0)).toBe(29);
196205
});
197206
});

‎packages/rest/src/rest-server-query-number-census.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,9 @@ describe('#20139 census — every numeric coercion in rest-server.ts is classifi
321321
// record-sharing bodies (`sharing-routes.ts`): 9 = 8 + 1 + 0 + 0 + 0,
322322
// length 4 → 5. [#23050 slice 5] Nor does the analytics dataset body
323323
// (`analytics-routes.ts`): 9 = 8 + 1 + 0 + 0 + 0 + 0, length 5 → 6.
324-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(6);
324+
// [#23050 slice 6] Nor do the two security bodies (`security-routes.ts`):
325+
// 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0, length 6 → 7.
326+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
325327
expect(SOURCES.map(([file]) => file)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
326328
expect(sites).toHaveLength(9);
327329
});

‎packages/rest/src/rest-server-registrar-files.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export const REST_SERVER_REGISTRAR_FILES = [
3939
'sharing-routes.ts',
4040
// [#23050 slice 5] `registerAnalyticsEndpoints`' body.
4141
'analytics-routes.ts',
42+
// [#23050 slice 6] `registerSecurityExplainEndpoints`' and `registerSecurityEndpoints`' bodies.
43+
'security-routes.ts',
4244
] as const;
4345

4446
/** One file on {@link REST_SERVER_REGISTRAR_FILES}. */

0 commit comments

Comments
 (0)