fix(plugin-auth): attach the session sign-in/sign-up already committed - #17922
Conversation
…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>
📓 Docs Drift Check5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 25 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 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 |
…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>
Fixes #17234
What
/sign-in/emailand/sign-up/emailanswered{ token, user }(/sign-in/emailalso carriesredirect), with nosessionanywhere in the body or the response headers — soSessionResponseSchema.safeParseonauth.login()/auth.register()'s return value always reported adata.sessionissue, alongside thesuccessgap 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), andinternalAdapter.createSessionisawait-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 globalafterhook runs, the row the response's owntokennames is already committed.The fix (
packages/plugins/plugin-auth/src/session-envelope-completion.ts) reads it back throughinternalAdapter.findSession(token)— the exact seam/get-sessionalready uses fordata.session— and attaches it in theafterhook, right beside the existingtwo-factor-rotated-token-echo.tsresponse-completion step. No id or expiry is ever fabricated: a read that fails for any reason (nointernalAdapter, 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.packages/client'sauth.login/auth.registerneeded no code change —normalizeSessionResponsealready liftsbody.sessionintodata.sessionwhen 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 whypackages/client/src/index.tsis 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'snormalizeSessionResponsedocblock. 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-sessionis 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/registernow parse as the full declaredSessionResponseexcept for the separately-trackeddata.user.imageresidue (#17235), and — explicitly kept intact and unweakened — thatsession.tokenis the same unsigned stringdata.tokenalready carried, never a second credential, anddata.tokenis still never synthesized FROM a session.Scope discipline: comment-only, one paragraph, nothing else in the file touched (
git diffshows 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(realAuthManager/ObjectQL/SqliteWasmDriver, no doubles) flips block ⑤ from pinning the residue to pinning the fix, and blocks ②/③ dropdata.sessionfrom the exhaustive issue list:SessionResponseSchema.safeParseon both methods' return value reported['data.session', 'data.user.image'].['data.user.image']only (SessionUser.imageis declaredz.string().optional(), but every/auth/*session route serves"image": null— no real session body parses asSessionResponse#17235, explicitly out of scope, still pinned).SessionSchema, names the rightuserId, and is the same row (id,expiresAt,userId) a following/get-sessionreads — proof this is a read, not an invention.session.tokenis the same unsigned credential already atdata.token— no second credential introduced.sessionstripped back out still fails the parse, on the same instrument.data.tokenbyte-identical andclient.tokenstill auto-set — both pinned.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 assertingdata.session; the other 8, including the new genericSessionSchemanegative control, correctly stay green since they need no production code.Correction to an earlier reading of mine:
@objectstack/client typecheckfirst 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.tsentered 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 readPREREQUISITE NOT MET(exit 3) for missing built dependencies. That was the same build-closure gap named above, compounded by@objectstack/spechaving been built without its.d.tsoutput in that worktree (itsdist/index.d.ts/dist/index.d.mtswere absent even thoughdist/index.jsexisted — rebuilding@objectstack/specdirectly restored both). With@objectstack/specrebuilt and@objectstack/client's real dependency closure in place, both gates are green:check:dual-build-cjs-loadsreports 104 require entry points across 67 packages loading, andcheck:type-check-debt --re-measurereports 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:servicesexecution 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.sessionwas already declared onSessionResponseSchema.datain@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.imageservednull) stays pinned as residue, unfixed, out of scope.#16760's methods (auth.me/auth.refreshToken) untouched — see the byte-identity proof above (the earliergit diff --statproof no longer applies now thatindex.tsis in this diff for the rider paragraph).Generated by Claude Code