Skip to content

Commit 1b3b65e

Browse files
feat(rest)!: retire GET {base}/docs, the OpenAPI document's HTML viewer page; /openapi.json stays (#25124)
Fixes #25104 Clause-②: no (narrowing) Metadata-protocol: none ## What and why `GET {base}/docs`, the HTML viewer page for the OpenAPI document, is retired on both route shapes: the unscoped `{base}/docs` and the environment-scoped `{base}/environments/:environmentId/docs`. Nothing read it, and the API origin serves no HTML viewer. This is the maintainer's choice on the card, 「C 下线页面」. `GET {base}/openapi.json`, its `api.enableOpenApi` gate and its answer stay. The dispatcher's security-header defaults are not touched. ## Contract Every input whose answer changes, old → new. Measured on this branch's build (`packages/rest/dist` carries 0 viewer markers and 1 `openapi.json` registration). Hosts were booted with REST and dispatcher project scoping on, as `serve.ts` forwards it. | Input | Old | New | | --- | --- | --- | | `GET {base}/docs`, signed-in caller, `plugin-hono-server` | 200 `text/html`, the viewer page | 404 `application/json`, `{"success":false,"error":{"code":"ENDPOINT_NOT_FOUND","message":"Not found"}}` | | `GET {base}/docs`, anonymous caller, `plugin-hono-server` | 401 `UNAUTHENTICATED` | the same 404 `ENDPOINT_NOT_FOUND` | | `GET {base}/environments/:environmentId/docs`, signed-in and anonymous, `plugin-hono-server` | 200 page / 401 | the same 404 `ENDPOINT_NOT_FOUND` | | both `/docs` shapes, signed-in and anonymous, node `IHttpServer` (the private conformance adapter) | 200 page / 401 | 404 `ENDPOINT_NOT_FOUND`, the same body | | `GET /api/v1/docs` on a booted showcase (dogfood `bootStack`), anonymous and member | 401 / 200 page | 404, byte-identical to the body a never-served path gets on that boot | | `GET {base}/docs` with a session held by an auth policy, or during a permission-store outage | that policy's 403 / 503 | no handler exists, so the host's not-found applies. Not measured separately on a host. | | `paths` of `GET {base}/openapi.json` (both shapes) | lists `{base}/docs` | does not list it | | `api.enableOpenApi: false` | unmounts 2 routes | unmounts 1 route, the document | | `RestServer.getRoutes()` | one `GET {base}/docs` per mounted base | none | **Unchanged, measured on both RestServer hosts and both shapes:** `GET {base}/openapi.json` answers a signed-in caller 200 with the OpenAPI 3.1 document and an anonymous caller 401 `UNAUTHENTICATED`. A path that was never served answers the same 404 body as the retired path. `@objectstack/hono`'s `createHonoApp` mounts no RestServer route. It answers 404 `ROUTE_NOT_FOUND` on `/docs`, on `/openapi.json` and on a never-served path, as it did before this change. ## The pins **New: `packages/rest/src/rest-docs-viewer-retired.pin.test.ts`.** It asserts the following on both route shapes: - `{base}/docs` has no registration: none in `getRoutes()`, none in the route manager under any verb, and no path handed to the host `IHttpServer`. - `{base}/openapi.json` is still registered on the server and on the host. - The served document is the OpenAPI 3.1 body. As a control it lists its own `openapi.json` path, and it lists no `/docs` path. Red on the base tree `f2b6cf230b` with only the pin added (commit `5fb3bdb3f0`): `Tests 4 failed | 2 passed (6)`. The four not-served and not-described cases fail; the two kept-document cases pass. Green after the retirement: 6/6. **Updated: what each asserted before, and what it asserts now.** No assertion that is still true was loosened. - `rest-api-description-anonymous-deny.test.ts` - Before: two endpoints × two bases. Anonymous 401 for the document and the viewer, the viewer's 401 body byte-identical to `/data`'s, the signed-in viewer serving HTML pointed at its sibling document, the document describing both endpoints, and the gated-session 403 and outage 503 on both endpoints. - Now: the document only, on both bases, with every document assertion unchanged. The viewer cases left with the viewer, and the new pin holds its absence. - `rest-config-mount-table.pin.test.ts`: the `api.enableOpenApi` row went from removing `GET /api/v1/docs` and `GET /api/v1/openapi.json` to removing `GET /api/v1/openapi.json` alone. The table still has 20 switches. - `rest-api-config-dead-keys-refused.test.ts` - Before: the default mounts both routes the switch owns, and `false` removes exactly both. - Now: the default mounts the document, and `false` removes exactly it. - `rest-route-ledger.ts` and its conformance test: the `GET /api/v1/docs` row is removed. The conformance test is unchanged. It still asserts that the ledger and the mounted routes match in both directions, and it went red in the retirement ablation only because the row named a route that was no longer mounted. - `execctx-consumer-census.test.ts`: sites 62 → 61, mentions 145 → 144, bare 46 → 45, caught unchanged at 16. The §3 floor `rows ≥ 46` stays at 46: 51 rows were measured after the retirement, so the old floor still holds and is not lowered. - `html-outlets.pin.test.ts` (from PR #25093) - The outlet list goes from `discovery-routes.ts` to empty. - The non-outlet list drops `html-escape.ts` (retired) and `rest-route-ledger.ts`, whose only HTML-touching string was the viewer row's note. It keeps `import-template.ts`. - The census is unchanged. - The positive control used to read `discovery-routes.ts` from disk. It now runs the census over this package's tree plus a fixture source shaped like the retired page, and asserts that the census finds the fixture and that the fixture is exactly the one unclassified file. **Beyond the measured starting map: `packages/qa/dogfood`.** Main's full battery runs these files; PR CI does not. Without these edits they would go red on `main` after the merge: - `authz-probe-blind-spot.census.ts` - Ledger row: 81 / 81 / 0 → 80 / 80 / 0. - `rest-server.ts` row: 70 / 19 / 51 → 69 / 19 / 50. Controls: `this.routeManager.register(` 63 → 62, `discovery-routes.ts` 4 → 3, `enforceAuth` 112 → 111. - Totals: 65 / 70 → 64 / 69. - All numbers re-derived by its own `deriveProbeFileCensus()`. - `authz-conformance.matrix.ts`: the `anonymous-deny-api-description` row now covers the one route left, and the docblock's ledger figure goes from 81 to 80 rows, which its pin reads. - `showcase-anonymous-deny-surfaces.dogfood.test.ts`: the viewer row and its 200 member control are replaced by a retired-path case. It asserts 404 for both the anonymous caller and the member, with a body equal to that of a never-served path. ## Also in this change - `packages/rest/src/html-escape.ts` is retired with its only caller. `git grep escapeHtml` over `packages/rest` finds no other caller. The repo's other escaper is private to `plugin-email`. - Comments that described the page are updated in `discovery-routes.ts`, `rest-server.ts`, `openapi-builtin-paths.ts` and `rest-route-ledger.ts`. The ADR anchor `scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json` now names the one API-description door left. - QA checklist `access-security.anonymous-floor-v18-doors` goes to revision 3. `/docs` leaves the floored-door step, and a new step and clause drive the retired path to the host's ordinary 404, following the file's revision and history convention. - The changeset for `@objectstack/rest` is `patch`, because this is a non-protocol change (`Metadata-protocol: none`), and keeps its BREAKING banner and its ADR-0087 `not-required (no-migration-prescription)` disposition. ## Not in this change - The `packages/spec` text that names the viewer belongs to `domain:spec`'s #25121, which is blocked by this card. That text is the `api.enableOpenApi` describe and the `api.documentation.enabled` tombstone prescription in `rest-server.zod.ts`. Further `packages/spec` sites, seen by grep and left for that card: the `liveness/rest_api.json` evidence line, `scripts/lib/openapi-self-consistency.ts` (a comment and a refusal message) and the prose in `migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts`. The generated `content/docs/references/api/rest-server.mdx` follows that card's regeneration. - `/openapi.json`'s gate and shape, the dispatcher's security-header defaults, and the export/import files are untouched. ## Verification (code at `1379d30df4`, a merge of `origin/main` `edcc8ceae6`; the head `520df1bfa6` adds only the changeset-level fix above it) - `@objectstack/rest` local project, run at the pre-merge head `b55b8f782d`: `Test Files 312 passed (312)`, `Tests 6185 passed | 326 skipped (6511)`. The merge brought one `packages/spec` describe-text commit only, so it was not re-run. - `pnpm --filter @objectstack/rest typecheck` exit 0 (`tsc --noEmit` plus `check:test-typecheck`). `pnpm --filter @objectstack/dogfood typecheck` exit 0. Both programs list the edited test files. - dogfood, run once on the final head: `authz-probe-blind-spot.test.ts` and `authz-conformance.test.ts` 108/108; `showcase-anonymous-deny-surfaces.dogfood.test.ts` 74/74, including the new retired-path case. - `@objectstack/spec` was rebuilt after the merge. `check:generated` reports all 14 artifacts up to date. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `1379d30df4` derived 86 commands. All 86 were run, plus the 10 families listed under "Declared WIDE population", and every one exited 0. `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET because 8 unrelated packages had no `dist`; it was re-run after building them (all turbo cache hits) and exited 0. Reconciliation: `✓ dispatch-gates --ran: 86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED (a DERIVED zero — all 86 recorded an exit code and none of them is 3).` - `pnpm lint`: exit 0 on `1379d30df4` (`eslint . --no-inline-config`, the whole tree). ## Acceptance notes - NOT MEASURED: the new dogfood retired-path case against `main`. Its red direction follows from main serving `/docs` with 401 to an anonymous caller and 200 to a member, which this file asserted until now. - This narrowing owes one `CONTRACT_REVIEW_TIER` review of record before the queue, as the claim states. --- _Generated by [Claude Code](https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d5d8146 commit 1b3b65e

17 files changed

Lines changed: 372 additions & 316 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
feat(rest)!: `GET {base}/docs`, the OpenAPI document's HTML viewer page, is retired; `GET {base}/openapi.json` stays
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) A runtime route retirement in RestServer.registerOpenApiEndpoints, not a metadata change. No spec key, export, option, config field, response field or stored shape is removed, renamed or re-shaped: the page was an HTML answer on one REST path, and `objectstack migrate meta` has nothing to rewrite. The machine-readable document, its `api.enableOpenApi` gate and its answer are unchanged. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this route and this diff adds none (not registered / already-registered); and no published interface or type changes (not runtime-interface-only / type-surface-only / runtime-export-retired). -->
11+
12+
**BREAKING** (a published route stops being served), shipped as `patch` because it is a non-protocol change: it changes what a REST caller is served, not what a metadata author can write.
13+
14+
`RestServer.registerOpenApiEndpoints` mounted two routes on every base: the OpenAPI document and an HTML viewer page for it. The viewer page is retired on both route shapes, the unscoped `{base}/docs` and the environment-scoped `{base}/environments/:environmentId/docs`. Nothing read it, and the API origin serves no HTML viewer.
15+
16+
- **Not served now:** `GET {base}/docs` has no route. A request to it gets the host's ordinary not-found answer, the same one any unmounted path gets: on the standard server (`@objectstack/plugin-hono-server`) that is `404` with code `ENDPOINT_NOT_FOUND`, for a signed-in caller and an anonymous one alike.
17+
- **Unchanged:** `GET {base}/openapi.json` on both route shapes, its `api.enableOpenApi` gate, its anonymous `401` and the document a signed-in caller receives. The document no longer lists the `/docs` path, because it describes only mounted routes.
18+
- `api.enableOpenApi` now gates one route, the document.
19+
20+
What changes for you. If you opened the page, load the document from `GET {base}/openapi.json` into an OpenAPI viewer of your choice.

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3434,10 +3434,10 @@
34343434
},
34353435
{
34363436
"id": "access-security.anonymous-floor-v18-doors",
3437-
"title": "The anonymous floor answers 401 FIRST on the doors v18 added to it — /notifications (was 501), non-POST /keys (was 405), share-link management (was 501), /openapi.json and /docs, every /i18n face and the settings family — while the token faces and the public-form submit stay anonymous and the submit echoes only {id}",
3437+
"title": "The anonymous floor answers 401 FIRST on the doors v18 added to it — /notifications (was 501), non-POST /keys (was 405), share-link management (was 501), /openapi.json, every /i18n face and the settings family — while the token faces and the public-form submit stay anonymous and the submit echoes only {id}",
34383438
"since": "v18",
34393439
"status": "active",
3440-
"revision": 2,
3440+
"revision": 3,
34413441
"priority": "P1",
34423442
"surface": "api",
34433443
"personas": [
@@ -3456,7 +3456,8 @@
34563456
]
34573457
},
34583458
"steps": [
3459-
"with NO credentials call: GET /api/v1/notifications, GET /api/v1/keys, GET /api/v1/share-links, GET /api/v1/openapi.json, GET /api/v1/docs, GET /api/v1/i18n/locales, GET /api/settings/branding",
3459+
"with NO credentials call: GET /api/v1/notifications, GET /api/v1/keys, GET /api/v1/share-links, GET /api/v1/openapi.json, GET /api/v1/i18n/locales, GET /api/settings/branding",
3460+
"with NO credentials, then as admin, call the retired viewer path GET /api/v1/docs, and a path nothing serves (GET /api/v1/this-route-does-not-exist) as the control",
34603461
"with no credentials call a token face: GET /api/v1/share-links/BOGUS/resolve",
34613462
"with no credentials POST /api/v1/forms/contact-us/submit with the four whitelisted fields plus a forged owner_id and a non-whitelisted status",
34623463
"repeat two of the 401 calls as admin to prove the door itself serves"
@@ -3474,6 +3475,12 @@
34743475
"verify": "status + error.code per door, anonymous",
34753476
"evidence": "one status+code line per door"
34763477
},
3478+
{
3479+
"clause": "the retired viewer path GET /api/v1/docs is routed nowhere: to the anonymous caller and to the admin it answers 404 with the same body the never-served control path answers on that boot — never 401 (a door is back) and never 200 or text/html (the page is back)",
3480+
"oracle": "api",
3481+
"verify": "status + content-type + body of GET /api/v1/docs, anonymous and as admin, compared with the control path's",
3482+
"evidence": "one status+code line per call, the control included"
3483+
},
34773484
{
34783485
"clause": "the share-link token face is NOT behind the floor (it answers its own invalid-token refusal, not 401)",
34793486
"oracle": "api",
@@ -3496,7 +3503,7 @@
34963503
],
34973504
"automated": {
34983505
"kind": "dogfood",
3499-
"ref": "packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts + packages/rest/src/rest-api-description-anonymous-deny.test.ts + packages/runtime/src/domains/i18n-anonymous-deny.test.ts + packages/services/service-settings/src/settings-routes.anonymous-floor.pin.test.ts + packages/rest/src/public-form-submit-answer.test.ts"
3506+
"ref": "packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts + packages/rest/src/rest-api-description-anonymous-deny.test.ts + packages/rest/src/rest-docs-viewer-retired.pin.test.ts + packages/runtime/src/domains/i18n-anonymous-deny.test.ts + packages/services/service-settings/src/settings-routes.anonymous-floor.pin.test.ts + packages/rest/src/public-form-submit-answer.test.ts"
35003507
},
35013508
"source": [
35023509
"packages/core/src/security/anonymous-deny.ts#shouldDenyAnonymous",
@@ -3516,6 +3523,12 @@
35163523
"date": "2026-10-19",
35173524
"change": "fixtures.knownGaps[1] marked CLOSED — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.anonymous-floor-v18-doors; wave anchor #24579 §3): the gap said a non-POST /keys never reaches the floor on a stock boot and answers 405; since #23909 the dispatcher registers /keys for every verb (packages/runtime/src/dispatcher-plugin.ts) and the run measured anonymous GET /api/v1/keys → 401. No clause changed: acceptance[1] already demanded 401",
35183525
"ref": "#24632"
3526+
},
3527+
{
3528+
"revision": 3,
3529+
"date": "2026-10-21",
3530+
"change": "re-pointed for the retirement of GET /api/v1/docs, the OpenAPI document's HTML viewer page (no consumer; the API origin serves no HTML viewer). /docs left the title and the floored-door step, so acceptance[1] no longer demands 401 of it; a new step and acceptance[2] drive the retired path anonymously and as admin and demand the host's ordinary 404, the never-served control path's body, on that boot. automated.ref gains packages/rest/src/rest-docs-viewer-retired.pin.test.ts, the registrar-level not-served pin on both route shapes. /openapi.json stays on the floor, unchanged",
3531+
"ref": "#25104"
35193532
}
35203533
]
35213534
},

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
// hand-curated regex table reaching 1 of 17 REST registrars and 4 of 17
2525
// dispatcher domain files.
2626
//
27-
// The population comes from `packages/rest/src/rest-route-ledger.ts` (81 rows
27+
// The population comes from `packages/rest/src/rest-route-ledger.ts` (80 rows
2828
// / 18 families) and `packages/runtime/src/route-ledger.ts` (105 rows / 23
2929
// domains) because those two are enumerated from a RUNNING server and guarded
3030
// in both directions by their own conformance tests — so a new family or
@@ -56,7 +56,9 @@
5656
// read/delete rows out of the ledger — 94 rows → 91, `sdk` 84 → 81, with
5757
// families unmoved at 19 because all three departing rows carried
5858
// `family: packages`, which survives on the publish row. (91 → 83 rows and
59-
// 19 → 18 families later, when #20102 retired the whole `reports` family.)
59+
// 19 → 18 families later, when #20102 retired the whole `reports` family;
60+
// 81 → 80 rows, families unmoved, when #25104 retired the `/docs` viewer
61+
// page, whose `openapi` family survives on the document row.)
6062
// Two stale figures in
6163
// one docblock with two entirely different causes is the failure mode the pin
6264
// above ends. Widening a regex instead was refused: it rots on the next
@@ -294,14 +296,16 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
294296
note: 'Ungated, an unauthenticated caller — carried to the domain as the guest envelope — was answered 200 on every analytics face on a stock boot, with aggregates over arbitrary objects, while the record doors and the dataset door on the same boot answered it 401. Gating the DOMAIN rather than each face keeps a newly added analytics face from arriving ungated. This row is the door half only: a non-system caller who resolves NO permission set being granted object admission and an unscoped read by that absence is a security-layer question for every door at once, deliberately not copied into service-analytics; it is tracked on its own card. The per-face unit pins (anonymous 401 with the service never consulted, malformed body and empty slot still 401, signed-in control unchanged) live in runtime/domains/analytics-anonymous-deny.test.ts.' },
295297
// #22430 — the API-description endpoints, under the ruling recorded on #22146
296298
// (the API-description endpoints refuse an anonymous caller with 401).
297-
{ id: 'anonymous-deny-api-description', summary: 'anonymous-deny on the API-description endpoints (the document and its viewer)', state: 'enforced',
298-
enforcement: 'rest/discovery-routes.ts registerOpenApiEndpoints — BOTH handlers (the document and its viewer page, on every base the server mounts) open with enforceAuth (→ shouldDenyAnonymous) as their first act, ahead of the bundled-artifact load and the protocol resolution, so a refused request does no work and does not reveal whether a document is bundled; an anonymous caller gets the REST seam\'s flat 401 UNAUTHENTICATED, a signed-in caller is served as before, and a session held by the ADR-0069 auth-policy gate gets that gate\'s 403',
299+
// #25104 — the document's `/docs` viewer page, the family's second route,
300+
// was retired with its handler; the row now covers the one route left.
301+
{ id: 'anonymous-deny-api-description', summary: 'anonymous-deny on the API-description endpoint (the OpenAPI document)', state: 'enforced',
302+
enforcement: 'rest/discovery-routes.ts registerOpenApiEndpoints — the document handler (on every base the server mounts) opens with enforceAuth (→ shouldDenyAnonymous) as its first act, ahead of the bundled-artifact load and the protocol resolution, so a refused request does no work and does not reveal whether a document is bundled; an anonymous caller gets the REST seam\'s flat 401 UNAUTHENTICATED, a signed-in caller is served as before, and a session held by the ADR-0069 auth-policy gate gets that gate\'s 403',
299303
proof: 'showcase-anonymous-deny-surfaces.dogfood.test.ts',
300-
// The REST family at ledger granularity. The family is exactly these two
301-
// routes and both handlers open with the floor, which is the property a
304+
// The REST family at ledger granularity. The family is exactly this one
305+
// route and its handler opens with the floor, which is the property a
302306
// family key needs.
303307
covers: ['rest-family:rest-route-ledger.ts:openapi'],
304-
note: 'Measured on a booted showcase before the change: an unauthenticated caller was served the document and the viewer page with 200, just as a signed-in caller was, while the record doors on the same boot answered it 401 — ADR-0056 D2 is default-deny and no ADR-0138 D2 door class names these endpoints. The cited proof drives both endpoints anonymously (401, the REST flat envelope, nothing of either served) and with a signed-in member (200, the document / the viewer page) on one boot. The environment-scoped twin mounts only under project scoping, which the showcase does not enable; it is the same handler closure, pinned per base in rest/src/rest-api-description-anonymous-deny.test.ts beside the gated-session 403 and the permission-store-outage 503. The viewer page fetches the document from the browser, and that request carries the browser\'s session (measured with the real viewer in a browser), so a signed-in browser is served on both endpoints; a deployment that publishes its API description to readers without an account publishes a static copy of the document instead.' },
308+
note: 'Measured on a booted showcase before the change: an unauthenticated caller was served the document (and its viewer page, since retired) with 200, just as a signed-in caller was, while the record doors on the same boot answered it 401 — ADR-0056 D2 is default-deny and no ADR-0138 D2 door class names this endpoint. The cited proof drives the document anonymously (401, the REST flat envelope, nothing of it served) and with a signed-in member (200, the document) on one boot. The environment-scoped twin mounts only under project scoping, which the showcase does not enable; it is the same handler closure, pinned per base in rest/src/rest-api-description-anonymous-deny.test.ts beside the gated-session 403 and the permission-store-outage 503. A deployment that publishes its API description to readers without an account publishes a static copy of the document instead.' },
305309
// #22432 — the `/i18n` dispatcher domain, under the ruling recorded on #22146
306310
// (the domain gains the domain-level anonymous refusal every other dispatcher
307311
// domain has, with an objectui companion for the Console's sign-in page).

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

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,15 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
382382
// permission-set overlay-discard row left with its route. It carried
383383
// `family: 'security'`, which the surviving security rows still carry, so
384384
// `reachable` moves with `population` and `keys` stays 18.
385-
population: 81,
386-
reachable: 81,
385+
// [#25104] 81 / 81 / 0 -> 80 / 80 / 0: the OpenAPI document's `/docs`
386+
// viewer page row (`GET /api/v1/docs`) left the ledger with its route. It
387+
// carried `family: 'openapi'`, which the surviving document row still
388+
// carries, so `reachable` moves with `population` and `keys` stays 18.
389+
population: 80,
390+
reachable: 80,
387391
blindSpot: 0,
388392
populationRule: 'ledger rows inside REST_ROUTE_LEDGER; reachable = rows carrying a `family` (each distinct value mints a key)',
389-
controls: { "route: '": 81, "family: '": 81, RestRouteLedgerEntry: 2 },
393+
controls: { "route: '": 80, "family: '": 80, RestRouteLedgerEntry: 2 },
390394
// [#22430] The `openapi` family is the second classified one, by the
391395
// `anonymous-deny-api-description` row; the note's 2 / 16 counts it.
392396
// [ADR-0138 E2] The `forms` family is the third, by the D2 class 1 row
@@ -397,7 +401,9 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
397401
'That guard is why this file can be a population source and a regex table cannot: a mounted route with no ' +
398402
'row here is already RED in another package, so a new family cannot be silently absent from this file, ' +
399403
'and therefore cannot be silently absent from the authz ratchet either. 18 families; 3 classified by ' +
400-
'matrix rows (metadata, openapi and forms), 15 enumerated in the shrink-only baseline. Re-measured 82 -> 81 when the permission-set ' +
404+
'matrix rows (metadata, openapi and forms), 15 enumerated in the shrink-only baseline. Re-measured 81 -> 80 when the ' +
405+
'OpenAPI document viewer page row left with its route; it carried `family: openapi`, which the surviving document row ' +
406+
'still carries. Earlier re-measured 82 -> 81 when the permission-set ' +
401407
'overlay-discard row left with its route; it carried `family: security`, which the surviving security rows still carry. ' +
402408
'Earlier re-measured 83 -> 82 when the anonymous ' +
403409
'public-form lookup-picker row left with its route; it carried `family: forms`, which the surviving form rows ' +
@@ -529,9 +535,9 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
529535
kinds: ['ROUTE_ENUMERATION', 'TRIPWIRE'],
530536
probes: 3,
531537
keys: 1,
532-
population: 70,
538+
population: 69,
533539
reachable: 19,
534-
blindSpot: 51,
540+
blindSpot: 50,
535541
populationRule:
536542
'route registration sites — `this.routeManager.register(` call sites, LESS the one inside ' +
537543
'`registerPerItemRoute` (the shared forwarder, not a route; its extent is bounded by the declaration\'s own ' +
@@ -931,17 +937,24 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
931937
// `private register*Endpoints(` stays 16, `const registerPerItemRoute =`
932938
// stays 1, `forwarder slice:` stays 1 and `enforceAuth` stays 112: the moved
933939
// groups spell none of the last four, and neither does the seam.
940+
// [#25104] 70 / 19 / 51 -> 69 / 19 / 50, and `enforceAuth` 112 -> 111: the
941+
// OpenAPI document's `/docs` viewer page was deleted with its handler, one
942+
// direct `this.routeManager.register(` site (63 -> 62; 4 -> 3 in
943+
// `discovery-routes.ts`) inside `registerOpenApiEndpoints`, NOT inside
944+
// `registerMetadataEndpoints`, so it was a blind spot; its handler opened
945+
// with `enforceAuth`, so that control drops by one. The registrar survives
946+
// for the document, so `private register*Endpoints(` stays 16.
934947
controls: {
935948
'private register*Endpoints(': 16,
936-
'this.routeManager.register(': 63,
949+
'this.routeManager.register(': 62,
937950
'registrar list length': 18,
938951
'data-action-routes.ts: this.routeManager.register(': 9,
939952
'batch-routes.ts: this.routeManager.register(': 5,
940953
'approvals-routes.ts: this.routeManager.register(': 7,
941954
'sharing-routes.ts: this.routeManager.register(': 8,
942955
'analytics-routes.ts: this.routeManager.register(': 1,
943956
'security-routes.ts: this.routeManager.register(': 6,
944-
'discovery-routes.ts: this.routeManager.register(': 4,
957+
'discovery-routes.ts: this.routeManager.register(': 3,
945958
'email-routes.ts: this.routeManager.register(': 1,
946959
'search-routes.ts: this.routeManager.register(': 1,
947960
'ui-routes.ts: this.routeManager.register(': 1,
@@ -970,7 +983,7 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
970983
'registerPerItemRoute(': 8,
971984
'const registerPerItemRoute =': 1,
972985
'forwarder slice: this.routeManager.register(': 1,
973-
enforceAuth: 112,
986+
enforceAuth: 111,
974987
},
975988
note:
976989
'The single non-tripwire probe names ONE registrar of 16. The other 15 can never mint a key: ' +
@@ -1247,7 +1260,7 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
12471260
/**
12481261
* Entry points inside the probe table's OWN files that no mintable key can
12491262
* reach, counting only the route/handler surfaces the ratchet's completeness
1250-
* claim is about: rest-server.ts (51 static / 56 runtime), http-dispatcher.ts
1263+
* claim is about: rest-server.ts (50 static / 55 runtime), http-dispatcher.ts
12511264
* (13) and domains/mcp.ts (1).
12521265
*
12531266
* hono-plugin.ts's 6 mounts are deliberately EXCLUDED from this total and
@@ -1262,8 +1275,11 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
12621275
// [ADR-0131 cutover, stage 7-pre] 66 / 71 -> 65 / 70: the retired permission-set
12631276
// overlay-discard route left rest-server.ts as one blind-spot call site and
12641277
// one runtime mount.
1265-
export const BLIND_SPOT_TOTAL_STATIC = 65;
1266-
export const BLIND_SPOT_TOTAL_RUNTIME = 70;
1278+
// [#25104] 65 / 70 -> 64 / 69: the retired `/docs` viewer page left
1279+
// `discovery-routes.ts` (on rest-server.ts's registrar list) as one blind-spot
1280+
// call site and one runtime mount.
1281+
export const BLIND_SPOT_TOTAL_STATIC = 64;
1282+
export const BLIND_SPOT_TOTAL_RUNTIME = 69;
12671283

12681284
/**
12691285
* Re-measure every row above from the same sources the probes read.

0 commit comments

Comments
 (0)