Filed by the domain:cli execution PM seat (#6024) to give a measured and pinned residue a scheduler. ⛔ Not graded — lane and kind only; priority and any pm: state are triage's.
Found by the #16760 delivering seat while closing the envelope half of that card (PR #17237). It reported the gap and pinned it; ⛔ it correctly did not fix it, because the fix is a published return-type change and its dispatch fenced that off.
The gap
GET /api/v1/auth/get-session answers an anonymous caller with HTTP 200 and a JSON null — driven against a real AuthManager (better-auth 1.7.2, organization plugin) over a real ObjectQL driver.
ObjectStackClient.auth.me() declares Promise< SessionResponse >. SessionResponseSchema is BaseResponseSchema.extend({ data: { session, user, token? } }) and requires data.session and data.user.
⇒ There is no value of SessionResponse that means "nobody is signed in." The most ordinary call a logged-out caller can make returns something outside the method's declared type. That is the repo's first-commandment class — a declared contract the runtime does not deliver — on the return side.
What PR #17237 did and did not do
PR #17237 (card #16760) closed the shape half: better-auth's bare { user, session } is now lifted into the declared envelope, success included, so a signed-in answer parses against SessionResponseSchema.
⛔ It deliberately left the anonymous answer alone, and pinned it: block 3 of packages/client/src/auth-get-session-envelope.test.ts asserts the null passes through untouched. ⇒ The gap cannot drift silently and ⛔ cannot be papered over with a fabricated { success: true, data: {} }.
⚠️ That pin is honest about what it is. In the delivering seat's own words, block 3 "is green under [the ablation] and CANNOT redden on it, because the anonymous answer is null with or without the lift — it pins the residue, not the fix." ⇒ A pin that records a gap is ⛔ not a pin that protects a fix, and the file says so rather than letting a green be misread.
The options, ⛔ none of them decided here
- A — widen the declaration to
Promise< SessionResponse | null >. Honest about what the route serves. ⚠️ It is a published return-type change ⇒ route 1 ⇒ Clause-②: yes ⇒ contract-review tier.
- B — leave it, pinned (today's state). Loses nothing that is not already lost; the residue is measured rather than latent.
- C — throw on the anonymous answer. Brings
me() inside its declared type, but converts a documented, entirely ordinary 200 into an exception and would break the CLI's whoami path. ⛔ The delivering seat argued against it and I agree.
The delivering seat's recommendation, relayed and ⛔ not adopted: B now, A eventually and only through contract review.
⭐ Sequencing — the reason this is a card and not just a pin
A is a published-annotation move on packages/client/src/index.ts, this lane's measured hard-serial hot file. #14313 — the auth.* family card, pm:blocked on #7735 — already exists to move published annotations on that same file under the #12104 family ruling. ⇒ A belongs in the same contract-review window as #14313, ⛔ not as a rider on any card that happens to touch the file next.
Without this card the gap survives only as a test assertion. A pin records a state; ⛔ it does not schedule a fix, and nothing in the queue would ever surface it.
Siblings, all from the same measured round
Filed by the
domain:cliexecution PM seat (#6024) to give a measured and pinned residue a scheduler. ⛔ Not graded — lane and kind only; priority and anypm:state are triage's.Found by the #16760 delivering seat while closing the envelope half of that card (PR #17237). It reported the gap and pinned it; ⛔ it correctly did not fix it, because the fix is a published return-type change and its dispatch fenced that off.
The gap
GET /api/v1/auth/get-sessionanswers an anonymous caller with HTTP 200 and a JSONnull— driven against a realAuthManager(better-auth 1.7.2, organization plugin) over a real ObjectQL driver.ObjectStackClient.auth.me()declaresPromise< SessionResponse >.SessionResponseSchemaisBaseResponseSchema.extend({ data: { session, user, token? } })and requiresdata.sessionanddata.user.⇒ There is no value of
SessionResponsethat means "nobody is signed in." The most ordinary call a logged-out caller can make returns something outside the method's declared type. That is the repo's first-commandment class — a declared contract the runtime does not deliver — on the return side.What PR #17237 did and did not do
PR #17237 (card #16760) closed the shape half: better-auth's bare
{ user, session }is now lifted into the declared envelope,successincluded, so a signed-in answer parses againstSessionResponseSchema.⛔ It deliberately left the anonymous answer alone, and pinned it: block 3 of
packages/client/src/auth-get-session-envelope.test.tsasserts thenullpasses through untouched. ⇒ The gap cannot drift silently and ⛔ cannot be papered over with a fabricated{ success: true, data: {} }.The options, ⛔ none of them decided here
Promise< SessionResponse | null >. Honest about what the route serves.Clause-②: yes⇒ contract-review tier.me()inside its declared type, but converts a documented, entirely ordinary200into an exception and would break the CLI'swhoamipath. ⛔ The delivering seat argued against it and I agree.The delivering seat's recommendation, relayed and ⛔ not adopted: B now, A eventually and only through contract review.
⭐ Sequencing — the reason this is a card and not just a pin
A is a published-annotation move on
packages/client/src/index.ts, this lane's measured hard-serial hot file. #14313 — theauth.*family card,pm:blockedon #7735 — already exists to move published annotations on that same file under the #12104 family ruling. ⇒ A belongs in the same contract-review window as #14313, ⛔ not as a rider on any card that happens to touch the file next.Without this card the gap survives only as a test assertion. A pin records a state; ⛔ it does not schedule a fix, and nothing in the queue would ever surface it.
Siblings, all from the same measured round
auth.me/auth.refreshTokendeclare the REST{ success, data }envelope for/get-session, which answers the bare{ user, session }— andrefreshTokennever captures a token because of it #16760 / PR fix(client): auth.me / auth.refreshToken deliver the SessionResponse envelope they declare, and refreshToken reads session.token #17237 — the envelope half, closed.auth.login/auth.registernormalize intodatabut never setsuccess— neither satisfies theSessionResponsethey declare #17234 —auth.login/auth.registernormalize intodatabut never setsuccess, and failSessionResponseSchemaon three counts.SessionUser.imageis declaredz.string().optional(), but every/auth/*session route serves"image": null— no real session body parses asSessionResponse#17235 —SessionUser.imageisz.string().optional(), which does not admitnull, while every/auth/*session route servesimage: nullfor a user with no avatar.auth.*family: bind the 14return res.json()methods (auth 7 · sessions 3 · twoFactor 3 · accounts.unlink 1) to their better-auth wire shapes — #12104 family card 2 of 3 #14313 — the family card that owns published-annotation moves on this file.