Skip to content

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

Description

@claude

Found while implementing #16760, which fixed the two SIBLING methods on the same route family (auth.me, auth.refreshToken). Out of that card's ruled scope — the dispatch explicitly bounded it to those two methods and directed this to be filed separately — so recorded here rather than fixed there. No assignee, no priority, no grade: left for triage.

The contract these methods declare

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

  • auth.loginPOST /api/v1/auth/sign-in/email
  • auth.registerPOST /api/v1/auth/sign-up/email

SessionResponse is packages/spec/src/api/auth.zod.ts's SessionResponseSchema:

export const SessionResponseSchema = lazySchema(() => BaseResponseSchema.extend({
  data: z.object({
    session: SessionSchema.describe('Active Session Info'),
    user: SessionUserSchema.describe('Current User Details'),
    token: z.string().optional().describe('Bearer token if not using cookies'),
  }),
}));

and BaseResponseSchema (packages/spec/src/api/contract.zod.ts) declares success REQUIRED — no .optional(), no .default():

export const BaseResponseSchema = lazySchema(() => z.object({
  success: z.boolean().describe('Operation success status'),
  ...
}));

So the declared type requires success: boolean and data.session.

What the methods actually produce

Both carry the same normalization, which fills data from better-auth's bare { token, user } and stops there:

const data = raw && (raw.data ?? (raw.token || raw.user ? { token: raw.token, user: raw.user } : undefined));
const normalized = data ? { ...raw, data } : raw;

No success is ever written, and the data it builds carries token and user but no session.

Measured

Driven through a real AuthManager (better-auth 1.7.2, organization plugin) over a real ObjectQL on a real SqliteWasmDriver, with an ObjectStackClient whose fetch is that manager:

register -> top-level keys ["token","user","data"]            success === undefined
login    -> top-level keys ["redirect","token","user","data"]  success === undefined
login    -> data keys ["token","user"]

SessionResponseSchema.safeParse on each, identical issue lists:

success         : Invalid input: expected boolean, received undefined
data.session    : Invalid input: expected object, received undefined
data.user.image : Invalid input: expected string, received null

BaseResponseSchema.safeParse alone fails on success for both.

Class

Declared contract not delivered (b). Two independent departures, and the second is the one triage did not anticipate when it flagged this:

  1. success is absent. Every consumer keying on the envelope flag — ObjectStackClient.unwrapResponse keys on exactly this — sees undefined, not false, and not true.
  2. data.session is absent. The declared payload names session as required, and these two routes serve none: better-auth's sign-in and sign-up answer { token, user }. So a caller following the declared type to result.data.session.expiresAt gets a TypeError, not a wrong value.

The third issue in the list, data.user.image, is a separate declaration problem filed on its own and is NOT specific to these two methods.

Relationship to #16760

#16760 fixed auth.me / auth.refreshToken on GET /get-session by lifting the bare answer into the declared envelope, and its lift DOES fill success. It deliberately did not touch login / register. Net effect today: within one auth.* family, two methods deliver the declared envelope and two do not.

A fix here has a decision in it that #16760's did not: /sign-in/email and /sign-up/email genuinely have no session object to put under data.session, so satisfying the declared type means either fetching the session, or changing the declaration for these two methods. That is why this is filed rather than fixed as a rider.

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


Generated by Claude Code

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