Skip to content

fix(plugin-auth): attach the session sign-in/sign-up already committed - #17922

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-17234-signin-session-envelope
Sep 13, 2026
Merged

fix(plugin-auth): attach the session sign-in/sign-up already committed#17922
os-project-manager merged 2 commits into
mainfrom
claude/issue-17234-signin-session-envelope

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17234

What

/sign-in/email and /sign-up/email answered { token, user } (/sign-in/email also carries redirect), with no session anywhere in the body or the response headers — so SessionResponseSchema.safeParse on auth.login() / auth.register()'s return value always reported a data.session issue, alongside the success gap the previous round (#17791) closed.

The ruling this implements (director seat, decision batch #125 item 4): "the server holds the session it just created (the token is its credential), so one request can answer the declared envelope."

The measurement: better-auth stores sessions in the database by default (this deployment wires no secondaryStorage), and internalAdapter.createSession is await-ed to completion — including the write — before either endpoint returns its { token, user } body (better-auth@1.7.3, dist/db/internal-adapter.mjs:247-319). So by the time this repo's global after hook runs, the row the response's own token names is already committed.

The fix (packages/plugins/plugin-auth/src/session-envelope-completion.ts) reads it back through internalAdapter.findSession(token) — the exact seam /get-session already uses for data.session — and attaches it in the after hook, right beside the existing two-factor-rotated-token-echo.ts response-completion step. No id or expiry is ever fabricated: a read that fails for any reason (no internalAdapter, no row, any error) leaves the response exactly as better-auth wrote it — the honest fallback the ruling names in advance, though it was not needed here.

FROM  POST /api/v1/auth/sign-in/email -> 200 { redirect, token, user }
TO    POST /api/v1/auth/sign-in/email -> 200 { redirect, token, user, session }

FROM  POST /api/v1/auth/sign-up/email -> 200 { token, user }
TO    POST /api/v1/auth/sign-up/email -> 200 { token, user, session }

packages/client's auth.login / auth.register needed no code changenormalizeSessionResponse already lifts body.session into data.session when the body carries one (it just never had). auth.me / auth.refreshToken (/get-session, #16760) are untouched — see the byte-identity proof below and the rider note on why packages/client/src/index.ts is in this diff at all.

Rider — one docblock paragraph corrected (#17234, authorised by the dispatching seat)

This PR's own fix falsified a paragraph in packages/client/src/index.ts's normalizeSessionResponse docblock. The dispatching seat authorised correcting it here rather than filing it out-of-scope, on the same same-file/same-subject basis the previous round (#17791) used for two corrections to this identical docblock.

What was false, read on origin/main: the ⚠️ Known residue — data.session on /sign-in|sign-up/email (#17234) paragraph said those two routes serve no session anywhere, that a second call to /get-session is the only place one is obtainable, and that the card stays open for a shape decision. All three are false after this PR: the routes now serve one, the server-side read makes a second call unnecessary, and ruling A′ already closed the shape decision.

What changed: that one paragraph, rewritten to state the session is attached server-side from the already-committed row, that login/register now parse as the full declared SessionResponse except for the separately-tracked data.user.image residue (#17235), and — explicitly kept intact and unweakened — that session.token is the same unsigned string data.token already carried, never a second credential, and data.token is still never synthesized FROM a session.

Scope discipline: comment-only, one paragraph, nothing else in the file touched (git diff shows a single hunk). auth.me / auth.refreshToken — both their own docblocks and their bodies — are re-proven byte-identical against this branch's pre-rider commit by extracting each method's full text (docblock + body) from both trees and diffing: both diffs are empty. The full client suite (43 files / 523 tests) and both envelope test files (22 tests) stay green after the edit.

Test evidence

packages/client/src/auth-login-register-envelope.test.ts (real AuthManager / ObjectQL / SqliteWasmDriver, no doubles) flips block ⑤ from pinning the residue to pinning the fix, and blocks ②/③ drop data.session from the exhaustive issue list:

  • Before: SessionResponseSchema.safeParse on both methods' return value reported ['data.session', 'data.user.image'].
  • After: ['data.user.image'] only (SessionUser.image is declared z.string().optional(), but every /auth/* session route serves "image": null — no real session body parses as SessionResponse #17235, explicitly out of scope, still pinned).
  • The attached session parses as SessionSchema, names the right userId, and is the same row (id, expiresAt, userId) a following /get-session reads — proof this is a read, not an invention.
  • session.token is the same unsigned credential already at data.token — no second credential introduced.
  • Negative control (criterion 3(a)): a fabricated body with session stripped back out still fails the parse, on the same instrument.
  • data.token byte-identical and client.token still auto-set — both pinned.
  • Reverse verification: reverting auth-manager.ts's one call site (git checkout HEAD~1 -- <path>, trap-guarded, restored byte-identical afterward — blob hash confirmed both ways) reddens exactly 7 of 15 cases, all and only the ones asserting data.session; the other 8, including the new generic SessionSchema negative control, correctly stay green since they need no production code.
pnpm --filter @objectstack/client exec vitest run --maxWorkers=2 src/auth-login-register-envelope.test.ts src/auth-get-session-envelope.test.ts
  Test Files  2 passed (2) · Tests  22 passed (22)   (#16760's suite included, untouched)

pnpm --filter @objectstack/client test
  Test Files  43 passed (43) · Tests  523 passed (523)

pnpm --filter @objectstack/plugin-auth test
  Test Files  108 passed (108) · Tests  2287 passed (2287)

pnpm --filter @objectstack/plugin-auth typecheck   -> OK
pnpm --filter @objectstack/client typecheck        -> OK, 0 file(s) / 0 error(s)

Correction to an earlier reading of mine: @objectstack/client typecheck first read as 9 failures in unrelated files. That was a build-closure artifact — this worktree had built plugin-auth's dependency closure but not client's own (pnpm --filter '@objectstack/client^...' build); with the correct closure built it is clean, 0 errors, in every file including the 9 previously misread as failing.

dispatch-gates-derived family

Re-derived after packages/client/src/index.ts entered the diff: the command list is unchanged (63 commands, byte-identical to the pre-rider derivation — diffed to confirm). Re-ran all 63 on the new commit: 63 of 63 pass.

Correction to my first sweep on this PR: two commands (check:dual-build-cjs-loads, check:type-check-debt) first read PREREQUISITE NOT MET (exit 3) for missing built dependencies. That was the same build-closure gap named above, compounded by @objectstack/spec having been built without its .d.ts output in that worktree (its dist/index.d.ts / dist/index.d.mts were absent even though dist/index.js existed — rebuilding @objectstack/spec directly restored both). With @objectstack/spec rebuilt and @objectstack/client's real dependency closure in place, both gates are green: check:dual-build-cjs-loads reports 104 require entry points across 67 packages loading, and check:type-check-debt --re-measure reports 55 raw tsc errors total, none above its recorded number (not a regression). Neither PREREQUISITE reading was ever reported as a pass at the time; this is the re-measurement on corrected build state, not a retraction of a false one.

Scope

Clause-②: no

↑ declared by the domain:services execution seat; the bare column-0 line is the machine judgement — this seat omitted that requirement from its dispatch order and the decorated form below cost one red gate.

  • Re-derived from the delivered diff, including the rider: no new exported symbol, no new type, no new public method. session was already declared on SessionResponseSchema.data in @objectstack/spec; this closes the gap between that declaration and what the two routes actually served. The rider is a comment-only edit to an existing docblock — no code, no export, no type. No spec change.
  • #17235 (data.user.image served null) stays pinned as residue, unfixed, out of scope.
  • #16760's methods (auth.me / auth.refreshToken) untouched — see the byte-identity proof above (the earlier git diff --stat proof no longer applies now that index.ts is in this diff for the rider paragraph).

Generated by Claude Code

…ommitted

The route wrappers now read back the session internalAdapter.createSession
already wrote (via internalAdapter.findSession(token), the same seam
/get-session uses) and attach it to the response, closing the data.session
half of #17234. success was closed in the previous round; data.user.image
(#17235) stays pinned as residue. No schema change, no new exported symbol.

Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/client/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/client/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 25 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 5741ff10c3068a84e9099d3a3eb3b533054bbc50packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7f6cc637ea43063e2b146c825297e1e38eebd79c — the merge of head 1c0ef4a93e4cf825ded5aec483784d3b93aab6fb into base 5741ff10c3068a84e9099d3a3eb3b533054bbc50, 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 7f6cc637ea43063e2b146c825297e1e38eebd79c && git checkout 7f6cc637ea43063e2b146c825297e1e38eebd79c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5741ff10c3068a84e9099d3a3eb3b533054bbc50 1c0ef4a93e4cf825ded5aec483784d3b93aab6fb && git checkout -B drift-repro 5741ff10c3068a84e9099d3a3eb3b533054bbc50 && git merge --no-ff 1c0ef4a93e4cf825ded5aec483784d3b93aab6fb

node scripts/docs-audit/affected-docs.mjs --json 5741ff10c3068a84e9099d3a3eb3b533054bbc50

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

…alsifies

The 'Known residue -- data.session' paragraph said /sign-in/email and
/sign-up/email serve no session anywhere and that the card stays open for
a shape decision. Both are now false: this PR's server-side fix attaches
the session, and ruling A' already closed the shape decision. Rewritten to
state what is now true, keeping the data.token / unsigned-vs-signed rule
in the surrounding paragraph intact and unweakened. Comment-only; no code
change. Rider authorised by the dispatching seat on PR #17922.

Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
Co-authored-by: Claude <noreply@anthropic.com>
@os-project-manager
os-project-manager marked this pull request as ready for review September 13, 2026 05:26
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit c9246fa Sep 13, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-17234-signin-session-envelope branch September 13, 2026 05:55
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

Development

Successfully merging this pull request may close these issues.

client SDK auth.login / auth.register normalize into data but never set success — neither satisfies the SessionResponse they declare

2 participants