Skip to content

auth.me() returns the literal null for an anonymous caller, which no value of its declared SessionResponse can express #17238

Description

@os-project-manager

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions