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.login — POST /api/v1/auth/sign-in/email
auth.register — POST /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:
success is absent. Every consumer keying on the envelope flag — ObjectStackClient.unwrapResponse keys on exactly this — sees undefined, not false, and not true.
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
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 asSessionResponse:auth.login—POST /api/v1/auth/sign-in/emailauth.register—POST /api/v1/auth/sign-up/emailSessionResponseispackages/spec/src/api/auth.zod.ts'sSessionResponseSchema:and
BaseResponseSchema(packages/spec/src/api/contract.zod.ts) declaressuccessREQUIRED — no.optional(), no.default():So the declared type requires
success: booleananddata.session.What the methods actually produce
Both carry the same normalization, which fills
datafrom better-auth's bare{ token, user }and stops there:No
successis ever written, and thedatait builds carriestokenanduserbut nosession.Measured
Driven through a real
AuthManager(better-auth 1.7.2, organization plugin) over a realObjectQLon a realSqliteWasmDriver, with anObjectStackClientwhosefetchis that manager:SessionResponseSchema.safeParseon each, identical issue lists:BaseResponseSchema.safeParsealone fails onsuccessfor both.Class
Declared contract not delivered (b). Two independent departures, and the second is the one triage did not anticipate when it flagged this:
successis absent. Every consumer keying on the envelope flag —ObjectStackClient.unwrapResponsekeys on exactly this — seesundefined, notfalse, and nottrue.data.sessionis absent. The declared payload namessessionas 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 toresult.data.session.expiresAtgets 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.refreshTokenonGET /get-sessionby lifting the bare answer into the declared envelope, and its lift DOES fillsuccess. It deliberately did not touchlogin/register. Net effect today: within oneauth.*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/emailand/sign-up/emailgenuinely have nosessionobject to put underdata.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