Repository navigation
feat(rest)!: retire GET {base}/docs, the OpenAPI document's HTML viewer page; /openapi.json stays - #25124
Conversation
…and /openapi.json as kept The pin is red on the current tree (the viewer page is still registered) and turns green with the retirement in the next commit. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…er page
The viewer page had no consumer, and the API origin serves no HTML viewer.
GET {base}/openapi.json, its gate and its answer stay. The page's escaper
leaves with its only caller, the outlet census keeps running over a fixture,
and the route ledger row and every pin that named the page move to the
retired state.
Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…ocs viewer page The blind-spot census reads one fewer registration site and ledger row, and the API-description matrix row covers the one route left. The execctx census keeps its 46-row floor, which still holds. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
… on a booted showcase The anonymous-deny surfaces file drops the viewer from its API-description endpoints and drives the retired path instead: anonymous and signed-in both get the same 404 body a path nothing ever served gets on that boot. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…r 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>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 5 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 26ad5305788a1b0eb1f5f97222b1547c76b0a398 && git checkout 26ad5305788a1b0eb1f5f97222b1547c76b0a398
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e 520df1bfa636cb2595168f4137c8df4fe78dadd5 && git checkout -B drift-repro 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e && git merge --no-ff 520df1bfa636cb2595168f4137c8df4fe78dadd5
node scripts/docs-audit/affected-docs.mjs --json 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e
|
Contract reviewServed-tier: ① Derived judgmentsCheck-runs on the head, latest per name, read twice (15:26Z and 15:33Z):
Accept-set and public-surface changes the diff implies, each judged:
② Semver level
The arm is RIGHT: a published route stops being served and nothing widens. The level is WRONG at this head. The diff moves nothing a metadata author can write — no spec key, accepted value, type, export, config field or response shape — so its kind is Remedy, a push rather than a body edit alone: grade Clause-②: no (narrowing) ③ Boundary flagsThe dev's deviations:
open_questions: none in report 6319625250; round 22's single question was answered by the maintainer's choice 6317642533 (C, retire the page and keep /openapi.json), which this diff implements. ESCALATED to the dispatching seat: the Test Core 2/6 to 6/6 were still running at the second read and are NOT MEASURED here; the @objectstack/rest suite figure (312 files, 6185 passed at the pre-merge head b55b8f7) is the dev's verify-lock report, not a check-run. Implemented-by: VERDICT: FAIL |
…rotocol change The retirement changes what a REST caller is served, not what a metadata author can write, so the changeset declares Metadata-protocol: none and ships @objectstack/rest at patch. The BREAKING banner, the ADR-0087 disposition and the migration line stay. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsThe delta, proven. Carried by citation. Because no non-prose path moved between the two heads, record 6319920291's ① judgments 1 to 11 on head 1379d30 carry whole to this head: the route retirement on both shapes with the host's own not-found answer (1); Check-runs on this head, latest run per name, second read at 2026-10-21T17:41:21Z (47 runs):
② Semver levelJudged afresh against AGENTS.md Post-Task Checklist step 3 on
Clause-②: no (narrowing) ③ Boundary flagsRound 26's deviations (report 6322448018), each answered:
The prior record's escalation (the Still out of this PR by rule and carried by #25121 ( One prose nit, not a verdict: the PR body's "Verification" heading still names open_questions: none in round 26; none open on the card. For the landing seat, not for this verdict: eight gates were still running at 17:41:21Z (Test Core ×6, Type Check · workspace, Type Check · consumer gates). This record judges the contract; "every check is green" is read separately before the queue, and Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT
Accepted deviations:
Out-of-scope dispositions (one line each):
Landing: through the merge queue by this seat once every check on this head reads Generated by Claude Code |
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}/docsand 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, itsapi.enableOpenApigate 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/distcarries 0 viewer markers and 1openapi.jsonregistration). Hosts were booted with REST and dispatcher project scoping on, asserve.tsforwards it.GET {base}/docs, signed-in caller,plugin-hono-servertext/html, the viewer pageapplication/json,{"success":false,"error":{"code":"ENDPOINT_NOT_FOUND","message":"Not found"}}GET {base}/docs, anonymous caller,plugin-hono-serverUNAUTHENTICATEDENDPOINT_NOT_FOUNDGET {base}/environments/:environmentId/docs, signed-in and anonymous,plugin-hono-serverENDPOINT_NOT_FOUND/docsshapes, signed-in and anonymous, nodeIHttpServer(the private conformance adapter)ENDPOINT_NOT_FOUND, the same bodyGET /api/v1/docson a booted showcase (dogfoodbootStack), anonymous and memberGET {base}/docswith a session held by an auth policy, or during a permission-store outagepathsofGET {base}/openapi.json(both shapes){base}/docsapi.enableOpenApi: falseRestServer.getRoutes()GET {base}/docsper mounted baseUnchanged, measured on both RestServer hosts and both shapes:
GET {base}/openapi.jsonanswers a signed-in caller 200 with the OpenAPI 3.1 document and an anonymous caller 401UNAUTHENTICATED. A path that was never served answers the same 404 body as the retired path.@objectstack/hono'screateHonoAppmounts no RestServer route. It answers 404ROUTE_NOT_FOUNDon/docs, on/openapi.jsonand 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}/docshas no registration: none ingetRoutes(), none in the route manager under any verb, and no path handed to the hostIHttpServer.{base}/openapi.jsonis still registered on the server and on the host.openapi.jsonpath, and it lists no/docspath.Red on the base tree
f2b6cf230bwith only the pin added (commit5fb3bdb3f0):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/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.rest-config-mount-table.pin.test.ts: theapi.enableOpenApirow went from removingGET /api/v1/docsandGET /api/v1/openapi.jsonto removingGET /api/v1/openapi.jsonalone. The table still has 20 switches.rest-api-config-dead-keys-refused.test.tsfalseremoves exactly both.falseremoves exactly it.rest-route-ledger.tsand its conformance test: theGET /api/v1/docsrow 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 floorrows ≥ 46stays 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 fix(rest): escape the API docs viewer URL value for its HTML attribute context (ruling 3C) #25093)discovery-routes.tsto empty.html-escape.ts(retired) andrest-route-ledger.ts, whose only HTML-touching string was the viewer row's note. It keepsimport-template.ts.discovery-routes.tsfrom 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 onmainafter the merge:authz-probe-blind-spot.census.tsrest-server.tsrow: 70 / 19 / 51 → 69 / 19 / 50. Controls:this.routeManager.register(63 → 62,discovery-routes.ts4 → 3,enforceAuth112 → 111.deriveProbeFileCensus().authz-conformance.matrix.ts: theanonymous-deny-api-descriptionrow 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.tsis retired with its only caller.git grep escapeHtmloverpackages/restfinds no other caller. The repo's other escaper is private toplugin-email.discovery-routes.ts,rest-server.ts,openapi-builtin-paths.tsandrest-route-ledger.ts. The ADR anchorscripts/adr-anchors/packages__rest__src__discovery-routes.ts.jsonnow names the one API-description door left.access-security.anonymous-floor-v18-doorsgoes to revision 3./docsleaves 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.@objectstack/restispatch, because this is a non-protocol change (Metadata-protocol: none), and keeps its BREAKING banner and its ADR-0087not-required (no-migration-prescription)disposition.Not in this change
packages/spectext that names the viewer belongs todomain:spec's spec(api): after #25104 retires GET {base}/docs, rest-server.zod.ts still names the docs viewer (enableOpenApi's TSDoc and describe, and the documentation.enabled tombstone prescription) #25121, which is blocked by this card. That text is theapi.enableOpenApidescribe and theapi.documentation.enabledtombstone prescription inrest-server.zod.ts. Furtherpackages/specsites, seen by grep and left for that card: theliveness/rest_api.jsonevidence line,scripts/lib/openapi-self-consistency.ts(a comment and a refusal message) and the prose inmigrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts. The generatedcontent/docs/references/api/rest-server.mdxfollows 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 oforigin/mainedcc8ceae6; the head520df1bfa6adds only the changeset-level fix above it)@objectstack/restlocal project, run at the pre-merge headb55b8f782d:Test Files 312 passed (312),Tests 6185 passed | 326 skipped (6511). The merge brought onepackages/specdescribe-text commit only, so it was not re-run.pnpm --filter @objectstack/rest typecheckexit 0 (tsc --noEmitpluscheck:test-typecheck).pnpm --filter @objectstack/dogfood typecheckexit 0. Both programs list the edited test files.authz-probe-blind-spot.test.tsandauthz-conformance.test.ts108/108;showcase-anonymous-deny-surfaces.dogfood.test.ts74/74, including the new retired-path case.@objectstack/specwas rebuilt after the merge.check:generatedreports all 14 artifacts up to date.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat1379d30df4derived 86 commands. All 86 were run, plus the 10 families listed under "Declared WIDE population", and every one exited 0.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET because 8 unrelated packages had nodist; 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 on1379d30df4(eslint . --no-inline-config, the whole tree).Acceptance notes
main. Its red direction follows from main serving/docswith 401 to an anonymous caller and 200 to a member, which this file asserted until now.CONTRACT_REVIEW_TIERreview of record before the queue, as the claim states.Generated by Claude Code