Skip to content

Commit b55b8f7

Browse files
committed
docs(rest): the ADR anchor, the QA checklist row and the changeset for the retired /docs viewer page
The discovery-routes ADR anchor names the one API-description door left, the v18 anonymous-floor checklist row drops /docs from its floored doors and drives the retired path to the host's ordinary 404 (revision 3), the authz matrix docblock reads 80 ledger rows, and the @objectstack/rest changeset declares the narrowing. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent bd0dca1 commit b55b8f7

4 files changed

Lines changed: 41 additions & 7 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/rest': minor
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+
9+
<!-- 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). -->
10+
11+
**BREAKING** (a published route stops being served), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
`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.
14+
15+
- **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.
16+
- **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.
17+
- `api.enableOpenApi` now gates one route, the document.
18+
19+
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: 4 additions & 2 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

‎scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"ADR-0056",
66
"ADR-0138"
77
],
8-
"invariant": "The API-description REST doors, moved here whole from `rest-server.ts` (#23050 slice 7): the discovery document (`GET {base}` and `GET {base}/discovery`) and the OpenAPI pair (`GET {base}/openapi.json` and its `GET {base}/docs` viewer), each mounted once per base. ADR-0076 — `/discovery` advertises only what this server actually mounted, and mounts what it advertises (its D12): each `routes.*` entry is rewritten to the mounted path, the direct-mount and email entries are projections of the recorded mounts, `services.*.route` is a projection of the finished `routes` map, and `transactionalBatch` and `search` are the conjunction of the protocol's capability and this server's own mount flag; and this package is the SOLE owner of `/openapi.json`, whose built-in section is produced from this server's own route table rather than inherited from the static artifact. ADR-0056 — both API-description doors are default-deny: each handler's first act refuses an anonymous caller with the shared anonymous-deny 401 (its D2), and ADR-0138's closed list of anonymous doors names neither (its D2). An advertised route that is not mounted, a second owner of the OpenAPI document, or an API-description door that answers before the anonymous refusal is the drift these records forbid; the bodies' own comments carry each citation."
8+
"invariant": "The API-description REST doors, moved here whole from `rest-server.ts` (#23050 slice 7): the discovery document (`GET {base}` and `GET {base}/discovery`) and the OpenAPI document (`GET {base}/openapi.json`), each mounted once per base; the document's HTML viewer page (`GET {base}/docs`) is retired (#25104) and answers the host's ordinary not-found. ADR-0076 — `/discovery` advertises only what this server actually mounted, and mounts what it advertises (its D12): each `routes.*` entry is rewritten to the mounted path, the direct-mount and email entries are projections of the recorded mounts, `services.*.route` is a projection of the finished `routes` map, and `transactionalBatch` and `search` are the conjunction of the protocol's capability and this server's own mount flag; and this package is the SOLE owner of `/openapi.json`, whose built-in section is produced from this server's own route table rather than inherited from the static artifact. ADR-0056 — the API-description door is default-deny: its handler's first act refuses an anonymous caller with the shared anonymous-deny 401 (its D2), and ADR-0138's closed list of anonymous doors does not name it (its D2). An advertised route that is not mounted, a second owner of the OpenAPI document, or an API-description door that answers before the anonymous refusal is the drift these records forbid; the bodies' own comments carry each citation."
99
}

0 commit comments

Comments
 (0)