Skip to content

feat(spec): AuthSessionApi.getSession declares the optional query.disableRefresh its readers send - #22406

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22384-getsession-input-query
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22384-getsession-input-query

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22384

Clause-②: yes (widening)

AuthSessionApi.getSession now declares the optional query.disableRefresh that the in-process readers send. A type-level pin in packages/types keeps the helper's return inside that declaration, key for key.

What changes

  • packages/spec/src/contracts/auth-service.ts.
    • The declaration: getSession?(input: { headers: unknown; query?: { disableRefresh?: boolean } }). It was { headers: unknown }. The return type and the rest of AuthSessionApi are unchanged.
    • The docblock now states the two read forms. A request carrying a better-auth session cookie reads with { headers, query: { disableRefresh: true } }. A bearer-only request reads with { headers } alone.
    • It names 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.
    • The old line "Widening this is for whoever needs more, with the call site to prove it" stays. The docblock now cites the ten call sites from PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367 that send query: rest-server.ts (x2), http-dispatcher.ts (x2), resolve-session-principal.ts, resolve-execution-context.ts, current-user-endpoints.ts, cloud-connection-plugin.ts and marketplace-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 (comment 6072295794). It is described below.
  • .changeset/22384-auth-session-api-getsession-input-query.md. @objectstack/spec minor. @objectstack/types gets no entry, because the only file changed there is a test, and files[] ships only dist, README.md and CHANGELOG.md.

No consumer was touched. No reader changed in domain:cli or domain: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 sending query.

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. FitsDeclared applies the object-literal rule to a type:

  • the value is assignable;
  • it carries no key the declaration does not name;
  • this holds at every depth where the declaration names a shape (headers is unknown, so it is not looked into);
  • a union is judged one member at a time, never by its common keys.

The file carries:

  • three holds(true) lines, each typed with FitsDeclared applied to HelperInput of a header type and DeclaredInput. There is one line per header shape the readers hand the helper: Web Headers, a Node header record, and unknown;
  • four @ts-expect-error controls that prove the instrument can fail at all. If FitsDeclared ever went vacuous, each directive would stop matching an error and tsc would report TS2578;
  • one runtime case: an implementer typed by the contract reads 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 typecheck script (tsc --noEmit). packages/types/tsconfig.json includes src/**/*, tests included, and --listFiles lists the new file once. CI's TypeScript Type Check runs it, and it reads @objectstack/spec from its built .d.ts. This follows the @ts-expect-error compile-time pins already in response-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 used scripts/ablation-replace.mjs in wrap mode and scripts/ablation-dist-preflight.mjs, under the shared verify lock. The predicted direction was red on the three holds and on the query read, with the controls unchanged. That is what happened.

leg reading
pristine dist marker disableRefresh?: boolean present in dist/contracts/index.d.ts and .d.mts; tree clean
mutate anchor { headers: unknown; query?: { disableRefresh?: boolean } } x1 -> x0; blob 698dd54bd9e8 -> 2df53d7829bd; spec rebuilt (exit 0); preflight --absent: marker absent from all 232 built files
tsc --noEmit in packages/types, mutated exit 2: TS2344 at :76, :77, :78 (the three holds), TS2339 at :96 (Property 'query' does not exist on type '{ headers: unknown; }'); 0 errors in the plain-assignability probe
restore blob after restore 698dd54bd9e8 == HEAD; git diff HEAD empty; spec rebuilt; preflight (present) green; tree clean
tsc --noEmit in packages/types, restored exit 0

The head after that run (57d9d9a8c) changes only docblock text in auth-service.ts: 6 lines added and 5 removed, all inside the comment. The declaration line is byte-identical.

Consumers

Every consumer that types against AuthSessionApi keeps compiling, and none was edited:

The typecheck of both edited packages is green (below). The downstream consumer typecheck is left to CI's TypeScript Type Check.

Verification

At 57d9d9a8c (final head):

  • Build. pnpm exec turbo run build --filter='!@objectstack/docs' --concurrency=2: 72 of 72 tasks successful, and git status --porcelain was empty afterwards. packages/spec/dist/contracts/index.d.ts carries the new declaration and docblock.
  • @objectstack/types.
    • typecheck: exit 0. tsc --noEmit --listFiles lists in-process-session-read.contract.test.ts once.
    • test (local): 26 files, 750 tests passed.
    • test:repo: 1 file, 11 tests passed.
    • The pin file run alone: 1 file, 1 test passed.
  • @objectstack/spec.
    • typecheck: exit 0. This covers tsc --noEmit, check:scripts-typecheck and check:test-typecheck.
    • test:repo: 54 files, 915 tests passed.
    • test (local, --maxWorkers=2): 626 files, 18742 tests passed, 1 todo.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 85 commands at 57d9d9a8c. All 85 ran, each exit code captured before any pipe, and all 85 exited 0. The --ran reconciliation 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-closure and check:doc-formula-expressions were measured after the full build. Their earlier runs at 769d9f4db exited 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 no pull_request payload. The reading of Clause-②: yes (widening) against minor is CI's on this PR.
  • Lint, narrowed (CI owns the full run). I ran pnpm exec eslint --no-inline-config --format json on the two changed .ts files: 2 files, 0 errors, 0 warnings. The .changeset file falls outside eslint's config (eslint reports it as ignored). eslint.config.mjs enables no type-aware linting (no parserOptions.project; see its comment near :326), so this diff cannot change a verdict on any untouched file.
  • Base. The branch is 8 commits behind origin/main (11d119ab1). None of those commits touches packages/spec/src/contracts/ or packages/types/, and the merge queue rebuilds the merged generation.

Acceptance notes

  • The declaration's value is boolean, but the helper only ever sends true. This follows the card and the claim. FitsDeclared accepts true against boolean. A helper that started sending false would also fit, and false would mean renewal.
  • The docblock names files rather than lines. The call sites move often, so a line number would go stale on the next edit. The pin names the helper, not the readers, and nothing checks the list of ten. A reader added or moved later leaves the list stale but the declaration correct.
  • check:entry-nameability prints a standing NOT MEASURED for @objectstack/spec/api-assembled and @objectstack/spec/qa (no callable export). This is unrelated to contracts. The gate exits 0.

Generated by Claude Code

claude added 2 commits October 9, 2026 01:24
…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>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 11d119ab1868a658c5f43538f3026a56438b2ed6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0fa647f17013dde347ee40927b4ca07ee593b72a — the merge of head 57d9d9a8c3c2adab740ebcb0bf11977388f956f1 into base 11d119ab1868a658c5f43538f3026a56438b2ed6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 57d9d9a8c3c2adab740ebcb0bf11977388f956f1
Local-runs: none

Inputs, and nothing else: card #22384 (its body; its three comments, 6072291092 the claim, 6073445987 the os-dev report, 6073468161 the seat acceptance), PR #22406 (its body, its file list, and the net diff against main from merge-base 117d34de3: 3 files, +145 / -5), and the check-runs on this head. The diff was read from a privately fetched ref; nothing was built, run, re-run or ablated. The dispatch order and the seat's own acceptance were not inputs to any judgment below.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. AuthSessionApi.getSession input: { headers: unknown } becomes { headers: unknown; query?: { disableRefresh?: boolean } } (packages/spec/src/contracts/auth-service.ts:201) — RIGHT. A pure widening of the accepted input: query is optional, so every existing { headers } caller types unchanged, and an implementer that accepts { headers: unknown } still satisfies the contract because the declared input is assignable to that narrower parameter. The return type and every other member of AuthSessionApi and IAuthService are byte-identical in the diff. This is exactly the shape card spec(contracts): AuthSessionApi.getSession declares its input as { headers } only, but the in-process readers now pass query.disableRefresh (PR #22367, #22258) #22384 asked for.
  2. Value type boolean, not the literal true the helper sends — RIGHT. The contract is the slice of the library's session handle the platform uses, and boolean is that handle's own shape; the helper's true fits it, and false means what absent means (renew). Nothing narrows.
  3. No consumer edited; none owed — RIGHT. On the head tree the typed consumers are plugin-sharing/src/sharing-plugin.ts:938 (AuthSessionApi | undefined), plugin-auth/src/auth-plugin.ts x4 (a hand-built { headers }), and the fixtures service-datasource/src/__tests__/entitled-caller.fixture.ts:75 (implementer parameter { headers: Headers }), cloud-connection/src/install-local-principal.fixtures.ts:70 and plugin-sharing/src/exec-context-seam.testkit.ts:134. Each keeps compiling against an optional key; none types against the narrow shape in a way the widening can break.
  4. check:api-surface unchanged — RIGHT. packages/spec/api-surface/contracts.json:52 records AuthSessionApi (interface) by existence only; a parameter-type change is below that artifact's resolution, so no regeneration is owed. CI carries that gate in Type Check · consumer gates, concluded success.
  5. No ADR governs the surface; no ADR-0087 entry owed — RIGHT. A git grep of docs/adr/** at the head for AuthSessionApi, disableRefresh and inProcessSessionReadInput returns zero hits. The change is non-breaking, so no disposition marker is owed and check-adr-0087-registration has nothing to read.
  6. Docs: nothing owed — RIGHT. The hand-written content/docs/kernel/contracts/auth-service.mdx names AuthSessionApi only as a field type (:33, :36) and never reproduces the getSession signature; content/docs/references/ is generated from Zod schemas, not from contracts/ interfaces. check:docs runs in Type Check · source gates, concluded success.
  7. The docblock — RIGHT. It states the two read forms, names inProcessSessionReadInput as their one source, says what a hand-built { headers } costs on a cookie request, and drops the sentence that had become false ("exactly getSession({ headers })"). Its list of ten query-sending call sites is TRUE at this head: a git grep for non-test inProcessSessionReadInput( finds exactly rest-server.ts x2, http-dispatcher.ts x2, security/resolve-session-principal.ts, security/resolve-execution-context.ts, current-user-endpoints.ts, cloud-connection-plugin.ts and marketplace-install-local-plugin.ts x2 — ten, in the packages the docblock names.
  8. The pin, packages/types/src/in-process-session-read.contract.test.ts — RIGHT, on five counts.
    • Placement. @objectstack/types depends on @objectstack/spec (workspace:*) and spec imports nothing from types, so this is the one package that sees both the helper and the contract. It is the one test file the claim declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024.
    • Compiled, so its four @ts-expect-error directives are not phantom checks. packages/types/tsconfig.json includes src/**/* with no test exclusion, no test-typecheck-debt.json exists in the package, and its typecheck script is tsc --noEmit. The Headers global already resolves under this tsconfig: the package's existing in-process-session-read.test.ts constructs it seventeen times.
    • Mechanism. The dev's finding that plain assignability cannot fail here is correct TypeScript: excess-property checks apply to object literals only, which is how ten readers compiled against the narrow declaration. FitsDeclared applies the literal's rule to a type. I traced the three holds to true (helper input over web Headers, over a Node header record, over unknown; headers: unknown short-circuits; { disableRefresh: true } passes key for key against { disableRefresh?: boolean }) and each of the four controls to false (an undeclared top-level key; an undeclared key under query; a union member carrying an undeclared key beside one that does not; a value type the declaration does not accept). So every directive matches a real TS2344, and a vacuous instrument would surface as TS2578. Narrowing the declaration back makes query an undeclared key of the helper's return, so the three holds go red and the runtime case's input.query read goes TS2339 — the direction the recorded ablation reports. I did not re-run it.
    • Runtime case. An implementer contextually typed by the contract reads input.query?.disableRefresh, a compile-time dependency on the widened key. A better-auth.session_token= cookie matches the helper's SESSION_TOKEN_COOKIE; a bearer header does not; [false, true] is what the helper's rule yields.
    • Not published, not cross-package. The tsup entries are src/index.ts and src/node.ts only, and files[] is dist, README.md, CHANGELOG.md, so the file never ships. It reads nothing outside its package by path (its only external input is the @objectstack/spec dependency's build), so it belongs to the local vitest project, not repo, and check:cross-package-test-inputs is not implicated.
  9. Scope and surfaces — RIGHT. Three files, none on a governed surface (Governed Surface Queue Guard: success); the head repository is the base repository; 150 changed lines; Fixes #22384, and the card's newest Claim: names this branch (The card this PR closes must claim this branch: success). No REST route is added, so the closed-query-set rule does not apply.

② Semver level

  • Changeset .changeset/22384-auth-session-api-getsession-input-query.md: @objectstack/spec minor — matches what the diff publishes. One optional key joins a published contract's accepted input; nothing is removed, renamed or narrowed, so the level is at least minor and not major. Its body states "Nothing to migrate", which is true. Check Changeset: success.
  • @objectstack/types: no entry, correctly. Its only change is a test file that neither tsup entry bundles and that files[] does not ship, so nothing publishes from it.
  • No skip-changeset label, correctly: @objectstack/spec publishes.
  • Clause-②: yes (widening) — RIGHT. The PR body and the changeset body carry the identical line. yes because a published contract's accept-set changes; (widening) because every previously accepted input is still accepted and more is. Not (narrowing), so no ADR-0087 disposition marker is owed, and yes with minor is the pairing the rule requires.

③ Boundary flags

open_questions on the os-dev report (6073445987): empty — nothing to answer.

Every dev flag (the PR's Acceptance notes and the report's out-of-scope findings), answered:

  • boolean declared while the helper only sends true — answered at ①.2: right as declared. No escalation.
  • The docblock's list of ten call sites has no pin — answered: the list is true at this head (①.7), it is informational, and the declaration's correctness does not depend on it. Accepted as a noted staleness risk; not a defect, no card.
  • The dispatch's plain-assignability assumption was falsified — answered at ①.8: the dev is right, and the key-for-key pin with its non-vacuity controls is the right instrument. No escalation.
  • Nine domain:services readers still hand-build { headers } (plugin-auth x4, plugin-webhooks, plugin-sharing, service-storage, service-settings, service-datasource, plus the dogfood armed.ts harness) — they compile unchanged against the widened declaration; the carrier 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 domain:services half, which the seat names. Not this card.
  • check:entry-nameability standing NOT MEASURED for api-assembled and qa — unrelated to this diff, exits 0. Dropped.

Check-runs on this head, read at 2026-10-09T03:19Z; their conclusions are the gate verdicts:

  • Concluded success: Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core (5/6), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, filter.
  • Concluded skipped (neither green nor red): Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in).
  • Not yet concluded at that reading — not read as green: Lint & Repo Gates; Test Core (1/6), (2/6), (3/6), (4/6) and (6/6); Type Check · workspace (the lane whose turbo run typecheck compiles the new pin file).
  • Not yet created on the head at that reading: the required aggregators TypeScript Type Check (needs: the four Type Check · lanes, if: always()) and Test Core; each appears only once its lanes finish.

This record judges the contract. It vouches for no pending check: landing waits on every one of those concluding success.

Implemented-by: claude/issue-22384-getsession-input-query
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 03:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 03:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit e75dced Oct 9, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22384-getsession-input-query branch October 9, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants