Repository navigation
fix(rest): the API-description endpoints refuse an anonymous caller (#22430) - #22446
objectstack-fleet[bot] merged 9 commits into
Conversation
RestServer.registerOpenApiEndpoints now opens both handlers (the document and its viewer) with the shared anonymous-deny floor, before any work, so an anonymous caller gets the same 401 UNAUTHENTICATED body the /data and /meta routes answer. A signed-in caller is served as before. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…signed-in service Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
… two new bare identity sites Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…us refusal on the booted showcase Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…aller; pin the inherited security statement Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 17 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 a5a74ae43a7c8906d4846cc22dc77adcd5ef493a && git checkout a5a74ae43a7c8906d4846cc22dc77adcd5ef493a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3ca71b6e05efbfc6ec5908c8c263fee6cceba389 9aaebffb35891b6eb4d170fad3610550521af398 && git checkout -B drift-repro 3ca71b6e05efbfc6ec5908c8c263fee6cceba389 && git merge --no-ff 9aaebffb35891b6eb4d170fad3610550521af398
node scripts/docs-audit/affected-docs.mjs --json 3ca71b6e05efbfc6ec5908c8c263fee6cceba389 |
…PI-description family The anonymous-deny-api-description row covers one more key (17 -> 18 covers keys; 8 ledger keys classified, 31 baselined), and the two API-description handlers add two enforceAuth call sites to rest-server.ts (56 -> 58). Population, reach and blind spot re-derive unchanged (71 / 19 / 52). Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
The rest-route-ledger row's note named the issue inside a string literal, which check:doc-authoring refuses in sibling-package prose. The anchor moves to an adjacent comment; the note keeps its counts. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…nor, breaking) Closing a door that served an anonymous caller narrows what the door accepts: the changeset becomes minor with the breaking mark, declares Clause-② no (narrowing), and carries its ADR-0087 disposition. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22430
Clause-②: no (narrowing)
What changed
RestServer.registerOpenApiEndpointsregisters the API-description endpoints: the document and its viewer page, on every base the server mounts. Neither handler checked who was calling. Both now open with the package's existing anonymous-deny floor (enforceAuth, which asksshouldDenyAnonymousfrom@objectstack/core). The check runs first, before the bundled artifact is loaded or a protocol is resolved, so a refused request does no work.401with codeUNAUTHENTICATED. The body isANONYMOUS_DENY_BODY, byte-identical to the anonymous 401 the data routes answer.403, as on every other protected route.Clause-②: no.This executes ruling
6074960686on #22146, item 2, verbatim:Measured first: a real boot, before and after
pnpm dev -- --fresh(the showcase) on this branch, before the fix (baseb9222dc70) and after it. Statuses only. "Gated" is a member created by the admin, still under the must-change-password policy, probed before the password change. "Member" is the same account after it.UNAUTHENTICATEDPASSWORD_EXPIREDUNAUTHENTICATEDPASSWORD_EXPIREDThe environment-scoped base is registered only under project scoping, and the showcase does not enable it. So on this boot both bases answer the same way for the scoped twin: it is not mounted. The scoped twin runs the same handler closures. It is measured at the handler level in
rest-api-description-anonymous-deny.test.ts, for both bases. Against the baserest-server.ts, all four anonymous cells answer 200, and the test turns red.The viewer carries the session (H4)
The viewer is an HTML page whose script fetches the document from the browser. I measured it in Chromium with the real viewer bundle. The CDN the page names is unreachable from this container, so the browser was served the same package (
@scalar/api-reference1.73.1) from its npm tarball.A signed-in browser is served on both endpoints, as before.
The ruling's confidence gap: an anonymous consumer in the tree?
Not found. I searched objectstack at this branch, objectui
origin/main049012bf, and the objectui pina58626c88for anything that fetches the document or the viewer.@objectstack/client, the CLI,scripts/,.github/,examples/,apps/docs(its reference pages are generated from the Zod schemas, not from the served document): zero hits.apps/**andpackages/**, at both commits: zero fetches. The onlyopenapi.jsonmentions are the@objectstack/specpackage-export name in build tooling and its tests. The Console's/docsroutes are its own book portal, not this viewer.showcase-declarative-endpoints.dogfood.test.ts. It reads it with an admin token, so it is unaffected./discoverydoes not advertise the document's URL.A deployed public API portal outside this repository is NOT MEASURED.
Translation-flip sweep
Pins and prose that held the old meaning, all in this PR:
packages/rest/src/openapi-builtin-paths.ts: the coverage note listed these endpoints among "the routes that answer anonymously". It now names only the discovery routes and says the inherited document-level requirement is true of these two. A new case pins that neither operation carries its ownsecurity, and that the document states a requirement.rest-openapi-route,rest-openapi-info-overlay,direct-mount-introspectionanddirect-mount-base-follows-apipath. A fifth,rest-endpoint-surfaces-served-only, was already signed in. The four test the document's contents, so they now read it as a signed-in caller. The refusal itself is asserted in the new suite: status,code, the whole body, no document keys, no page, and no artifact load or protocol read.execctx-consumer-census.test.ts: 66 to 68 sites, 45 to 47 bare. Both new sites are bare, with the shared floor on the next line. Its §3 drives them: an absent context gets 401UNAUTHENTICATED, an entitled one clears the floor.openapiREST family moves from the shrink-only unclassified baseline (32 to 31) to a newenforcedrow,anonymous-deny-api-description. Its cited proof isshowcase-anonymous-deny-surfaces.dogfood.test.ts, which now drives both endpoints on the booted showcase. Anonymous gets 401, the REST flat envelope, the wholeANONYMOUS_DENY_BODY, nothing served. A signed-in member gets 200 for the document and 200 for the viewer page. Both endpoints joined the envelope-family table. The population pin inauthz-conformance.test.tslists the family as classified.authz-probe-blind-spot.census.tsand its test), re-measured fromderiveProbeFileCensus()on the tree, as in the/analyticsprecedent:PROBE_TABLEmoves from{ entries: 19, files: 14, keys: 17 }to{ 19, 14, 18 }. The new row covers one more key; no probe and no file joined.rest-server.tscontrolenforceAuthmoves from 56 to 58: the two handlers' call sites, with no prose mention.coverskeys. The ledgers still mint 39 keys: 8 classified, 31 baselined.rest-route-ledger.tsrow's note now says 2 families classified (metadata and openapi) and 16 baselined.Tests and gates (at
d1cc56281)rest-server.ts. 7 failed and 4 passed. The 4 anonymous cells readexpected 200 to be 401. The signed-in controls passed. Restored to the HEAD blob,git diff HEADempty. After the fix: 13 of 13.dist/(scripts/ablation-replace.mjs+ablation-dist-preflight.mjs): I removed only the document's floor, rebuilt@objectstack/rest, and confirmed the marker in both built files. The booted-showcase proof then failed exactly its two document-anonymous cases (2 failed / 65 passed); the viewer cases and the controls stayed green. Restored to the HEAD blob, rebuilt, and confirmed the marker absent fromdist/.@objectstack/rest, at1d4ad614f: the full suite gave 271 files, 5164 passed, 327 skipped; typecheck passed (tests are covered throughtsconfig.test.json, 6 of 6 touched files). Nothing underpackages/restchanged after that commit.@objectstack/dogfood, the WHOLE suite, through the verify lock: 233 files (232 passed, 1 skipped), 1847 tests (1838 passed, 9 skipped), exit 0. It ran at0c0b1f7e5; the next commit,d1cc56281, changes only a string and a comment in the census note. Atd1cc56281,authz-probe-blind-spot.test.ts+authz-conformance.test.tspassed 90 of 90.pnpm check:doc-authoringexits 0 atd1cc56281. It was red at0c0b1f7e5on a tracker id inside the census note's string prose. The id now sits in an adjacent comment.dispatch-gates.mjs --commandsderives the same 69 commands atd1cc56281, and all 69 exited 0 there. Three gates read built output and first answeredPREREQUISITE NOT MET(exit 3) in the fresh worktree:check:dual-build-cjs-loads,check:dts-closureandcheck:sourcemap-no-sources-content(the last two swept 63 and 60 packages). I built the eight packages that had nodist/(all turbo cache hits) and re-ran them: exit 0, sweeping 66, 71 and 68 packages.--ranreconciles 69 derived, 69 run, 0 not measured.pnpm lint(the full run) exited 0 atd1cc56281.@objectstack/dogfoodtypecheck exited 0 atd1cc56281.Acceptance notes
/discoverystill answers anonymously, as before. On the environment-scoped twin, a signed-in caller is judged by the auth service of the environment the URL names. These handlers do not add the ownership comparison the UI-view route makes. I did not measure that, because the showcase mounts no scoped base. It is a question for whoever re-measures the everything-else class for ADR-0138.rest-route-ledger.tscould carry the optionalauthz: 'anonymous-deny-api-description'field. I left that file alone because another hold covers its notes.Generated by Claude Code