Repository navigation
feat(spec): AuthSessionApi.getSession declares the optional query.disableRefresh its readers send - #22406
Conversation
…ableRefresh its readers send
The in-process readers call api.getSession(inProcessSessionReadInput(headers)),
which adds query: { disableRefresh: true } for a request carrying a session
cookie. The contract declared { headers } alone. Widen the declared input to
{ headers: unknown; query?: { disableRefresh?: boolean } }, restate the
docblock as the two read forms with the helper as their one source and the
call sites that send query, and pin the helper's return inside the
declaration key for key from packages/types.
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…describing every reader State the helper as the way to read and what a hand-built input costs on a cookie-carrying request, rather than asserting that every reader already reads through it. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 139 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 0fa647f17013dde347ee40927b4ca07ee593b72a && git checkout 0fa647f17013dde347ee40927b4ca07ee593b72a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 11d119ab1868a658c5f43538f3026a56438b2ed6 57d9d9a8c3c2adab740ebcb0bf11977388f956f1 && git checkout -B drift-repro 11d119ab1868a658c5f43538f3026a56438b2ed6 && git merge --no-ff 57d9d9a8c3c2adab740ebcb0bf11977388f956f1
node scripts/docs-audit/affected-docs.mjs --json 11d119ab1868a658c5f43538f3026a56438b2ed6 |
Contract reviewServed-tier: Inputs, and nothing else: card #22384 (its body; its three comments, ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flags
Every dev flag (the PR's Acceptance notes and the report's out-of-scope findings), answered:
Check-runs on this head, read at 2026-10-09T03:19Z; their conclusions are the gate verdicts:
This record judges the contract. It vouches for no pending check: landing waits on every one of those concluding Implemented-by: VERDICT: PASS |
Fixes #22384
Clause-②: yes (widening)
AuthSessionApi.getSessionnow declares the optionalquery.disableRefreshthat the in-process readers send. A type-level pin inpackages/typeskeeps the helper's return inside that declaration, key for key.What changes
packages/spec/src/contracts/auth-service.ts.getSession?(input: { headers: unknown; query?: { disableRefresh?: boolean } }). It was{ headers: unknown }. The return type and the rest ofAuthSessionApiare unchanged.{ headers, query: { disableRefresh: true } }. A bearer-only request reads with{ headers }alone.inProcessSessionReadInput(packages/types/src/in-process-session-read.ts) as the one source of both forms, and says what a hand-built{ headers }costs on a cookie request.query:rest-server.ts(x2),http-dispatcher.ts(x2),resolve-session-principal.ts,resolve-execution-context.ts,current-user-endpoints.ts,cloud-connection-plugin.tsandmarketplace-install-local-plugin.ts(x2).packages/types/src/in-process-session-read.contract.test.ts(new). This is the pin, and it is the one file declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 (comment6072295794). It is described below..changeset/22384-auth-session-api-getsession-input-query.md.@objectstack/specminor.@objectstack/typesgets no entry, because the only file changed there is a test, andfiles[]ships onlydist,README.mdandCHANGELOG.md.No consumer was touched. No reader changed in
domain:cliordomain:services.The pin, and a dispatch assumption it falsified
The dispatch asked for a type-level test that the helper's return is assignable to the declared input, and asked that narrowing the declaration back should make it fail. Plain assignability cannot fail here. TypeScript refuses an undeclared key only on an object literal. A non-literal value with an extra optional key is assignable to a type that omits that key. That is how all ten readers compiled against
{ headers: unknown }while sendingquery.This was measured in the ablation below. With the declaration narrowed back, a throwaway probe compiled with 0 errors. The probe held
const x: DeclaredInput = inProcessSessionReadInput(new Headers())and the readers' own call shape,api.getSession?.(inProcessSessionReadInput(...)).So the pin checks assignability key for key.
FitsDeclaredapplies the object-literal rule to a type:headersisunknown, so it is not looked into);The file carries:
holds(true)lines, each typed withFitsDeclaredapplied toHelperInputof a header type andDeclaredInput. There is one line per header shape the readers hand the helper: WebHeaders, a Node header record, andunknown;@ts-expect-errorcontrols that prove the instrument can fail at all. IfFitsDeclaredever went vacuous, each directive would stop matching an error and tsc would report TS2578;input.query?.disableRefresh. That read is itself a compile-time check, and it asserts a cookie read does not renew while a bearer read does.Mechanism. The test runs under the package's existing
typecheckscript (tsc --noEmit).packages/types/tsconfig.jsonincludessrc/**/*, tests included, and--listFileslists the new file once. CI'sTypeScript Type Checkruns it, and it reads@objectstack/specfrom its built.d.ts. This follows the@ts-expect-errorcompile-time pins already inresponse-envelope.test.ts. No new runner was added.Reverse verification (one-off; nothing kept)
The run is at
769d9f4db, after the fix was committed. It usedscripts/ablation-replace.mjsin wrap mode andscripts/ablation-dist-preflight.mjs, under the shared verify lock. The predicted direction was red on the threeholdsand on thequeryread, with the controls unchanged. That is what happened.disableRefresh?: booleanpresent indist/contracts/index.d.tsand.d.mts; tree clean{ headers: unknown; query?: { disableRefresh?: boolean } }x1 -> x0; blob698dd54bd9e8->2df53d7829bd; spec rebuilt (exit 0); preflight--absent: marker absent from all 232 built filestsc --noEmitinpackages/types, mutatedTS2344at:76,:77,:78(the threeholds),TS2339at:96(Property 'query' does not exist on type '{ headers: unknown; }'); 0 errors in the plain-assignability probe698dd54bd9e8== HEAD;git diff HEADempty; spec rebuilt; preflight (present) green; tree cleantsc --noEmitinpackages/types, restoredThe head after that run (
57d9d9a8c) changes only docblock text inauth-service.ts: 6 lines added and 5 removed, all inside the comment. The declaration line is byte-identical.Consumers
Every consumer that types against
AuthSessionApikeeps compiling, and none was edited:inProcessSessionReadInput(...). That input is now declared instead of tolerated.{ headers }by hand:plugin-auth(x4),plugin-webhooks,plugin-sharing,service-storage,service-settings,service-datasource, and the dogfoodarmed.tsharness.queryis optional, so they type as before. Moving them to the helper is auth: server-side auth.api.getSession reads renew the session without forwarding the renewed cookie, so the browser cookie expires before the session (split session) #22258's remaining half and is not done here.getSessionthat declares{ headers: unknown }still satisfies the contract, because the wider input is assignable to it.The
typecheckof both edited packages is green (below). The downstream consumer typecheck is left to CI'sTypeScript Type Check.Verification
At
57d9d9a8c(final head):pnpm exec turbo run build --filter='!@objectstack/docs' --concurrency=2: 72 of 72 tasks successful, andgit status --porcelainwas empty afterwards.packages/spec/dist/contracts/index.d.tscarries the new declaration and docblock.@objectstack/types.typecheck: exit 0.tsc --noEmit --listFileslistsin-process-session-read.contract.test.tsonce.test(local): 26 files, 750 tests passed.test:repo: 1 file, 11 tests passed.@objectstack/spec.typecheck: exit 0. This coverstsc --noEmit,check:scripts-typecheckandcheck:test-typecheck.test:repo: 54 files, 915 tests passed.test(local,--maxWorkers=2): 626 files, 18742 tests passed, 1 todo.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 85 commands at57d9d9a8c. All 85 ran, each exit code captured before any pipe, and all 85 exited 0. The--ranreconciliation reads: "85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASURED (a DERIVED zero — all 85 recorded an exit code and none of them is 3)". Selected verdict lines:check:api-surface: "public API surface + factory signatures unchanged".check-adr-0087-registration: "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)".check:nul-bytes: OK.check:dual-build-cjs-loads,check:lean-entry-closureandcheck:doc-formula-expressionswere measured after the full build. Their earlier runs at769d9f4dbexited 3 ("prerequisite not met"); those runs are not counted.check-changeset-no-major --base origin/main. Run locally, it prints "LEVEL AXIS: NOT APPLICABLE" because there is nopull_requestpayload. The reading ofClause-②: yes (widening)againstminoris CI's on this PR.pnpm exec eslint --no-inline-config --format jsonon the two changed.tsfiles: 2 files, 0 errors, 0 warnings. The.changesetfile falls outside eslint's config (eslint reports it as ignored).eslint.config.mjsenables no type-aware linting (noparserOptions.project; see its comment near:326), so this diff cannot change a verdict on any untouched file.origin/main(11d119ab1). None of those commits touchespackages/spec/src/contracts/orpackages/types/, and the merge queue rebuilds the merged generation.Acceptance notes
boolean, but the helper only ever sendstrue. This follows the card and the claim.FitsDeclaredacceptstrueagainstboolean. A helper that started sendingfalsewould also fit, andfalsewould mean renewal.check:entry-nameabilityprints a standingNOT MEASUREDfor@objectstack/spec/api-assembledand@objectstack/spec/qa(no callable export). This is unrelated tocontracts. The gate exits 0.Generated by Claude Code