Skip to content

client SDK auth.me / auth.refreshToken declare the REST { success, data } envelope for /get-session, which answers the bare { user, session } — and refreshToken never captures a token because of it #16760

Description

@os-project-manager

Found while implementing #16568 (per-organization addressing for organizations.getActiveMember), which needed the caller's own user id off /get-session. Out of that card's ruled scope — it is runtime addressing on one organizations.* method — so recorded here rather than fixed there. No assignee, no label: left for triage.

The contract the SDK declares

packages/client/src/index.ts, both methods annotate their return as SessionResponse:

  • auth.meGET /api/v1/auth/get-session
  • auth.refreshToken — the same route (the JSDoc says so: better-auth has no separate refresh endpoint)

SessionResponse is packages/spec/src/api/auth.zod.ts's SessionResponseSchema, which is BaseResponseSchema.extend({ data: { session, user, token? } }) — the REST envelope, i.e. a success flag with the payload parked under data.

What the route answers

better-auth owns the /api/v1/auth/* bytes (the plugin mounts one catch-all straight onto better-auth's handler), and better-auth does not use ObjectStack's REST envelope. Measured through a real AuthManager (better-auth 1.7.2, organization plugin, teams enabled) over a real SqlDriver (better-sqlite3 :memory:):

GET /api/v1/auth/get-session   (signed in)   -> 200 {"user":{...,"id":"..."},"session":{...,"activeOrganizationId":null}}
GET /api/v1/auth/get-session   (anonymous)   -> 200 null

Bare. No success, no data. And the anonymous answer — the literal null at 200 — is outside the declared type in a second, independent way.

Class

Declared contract not delivered (b). Two consequences, and the second is not a type-only complaint:

  1. (await client.auth.me()).data.user type-checks and is undefined at runtime; .user, which is the real payload, does not type-check. The annotation points every caller at the wrong key.
  2. auth.refreshToken acts on that annotation: its body reads data.data?.token and only then assigns this.token. Against the measured body that read is always undefined, so the method never captures a token — it is a no-op that returns successfully. A bearer-mode client calling it to refresh keeps whatever token it already had, with nothing said.

Not the same card as #14313

#14313 (auth.* family binding, card 2 of 3 of the #12104 family) binds the 14 methods that have no return annotation and end return res.json(), and its own text says to delete exactly that family's ledger entries and no others. These two methods are annotated already — annotated wrongly — so they are outside that enumerated set. They do sit in the same lane and the same hard-serial hot file (packages/client/src/index.ts), so scheduling them together is natural even though the scopes are distinct.

Options seen, not decided

  • Bind both methods to the bare shape better-auth actually sends, and widen for the anonymous null, the way the organizations.* family bound its wire types (a published return-type change, so contract review applies).
  • Or decide that /get-session should be enveloped like the rest of /api/v1 and fix it at the producer — a much larger call, since better-auth owns those bytes and every other /auth/* method is bare too.

Either way refreshToken's data.data?.token read is wrong today and can be corrected on its own.

Refs: #16568 (where it was measured), #14313, #12104 (family head).

Activity

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

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions