Summary
The convex plugin's wrapper around jwt.definePayload spreads the user-supplied function's return value synchronously, even though the type signature allows an async function. When a consumer returns a Promise, the spread yields nothing and all custom JWT claims are silently dropped and only the plugin's own sessionId and iat survive.
Reproduction
convex({
jwt: {
definePayload: async ({ user }) => {
const roles = await ctx.runQuery(api.auth.getRoles, { userId: user.id });
return { roles };
},
},
})
After sign-in, the decoded JWT contains only sessionId, iat, sub, exp, iss, aud.roles is missing. Convex logs show [WARN] 1 unawaited operation: [query] because the inner async work is never awaited.
Root cause
src/plugins/convex/index.ts (same in dist/plugins/convex/index.js of 0.11.4):
definePayload: ({ user, session }) => ({
...(opts.jwt?.definePayload
? opts.jwt.definePayload({ user, session }) // not awaited
: omit(user, ["id", "image"])),
sessionId: session.id,
iat: Math.floor(new Date().getTime() / 1000),
}),
Meanwhile the type signature explicitly allows async (Promise<Record<string, any>> | Record<string, any>).
Proposed fix
- definePayload: ({ user, session }) => ({
+ definePayload: async ({ user, session }) => ({
...(opts.jwt?.definePayload
- ? opts.jwt.definePayload({ user, session })
+ ? await opts.jwt.definePayload({ user, session })
: omit(user, ["id", "image"])),
sessionId: session.id,
iat: Math.floor(new Date().getTime() / 1000),
}),
Sync definePayload still works, awaiting a non-Promise is a no-op. PR to follow.
Environment
@convex-dev/better-auth: 0.10.10 and 0.11.4 (both affected)
better-auth: 1.4.9
Possibly related
#291 reports custom payload fields missing from the first JWT. The reporter hypothesized trigger/transaction timing, but this async-spread bug is a plausible alternative root cause, worth checking whether their definePayload (or any trigger populating the user doc) is async.
Summary
The convex plugin's wrapper around
jwt.definePayloadspreads the user-supplied function's return value synchronously, even though the type signature allows an async function. When a consumer returns aPromise, the spread yields nothing and all custom JWT claims are silently dropped and only the plugin's ownsessionIdandiatsurvive.Reproduction
After sign-in, the decoded JWT contains only
sessionId,iat,sub,exp,iss,aud.rolesis missing. Convex logs show[WARN] 1 unawaited operation: [query]because the inner async work is never awaited.Root cause
src/plugins/convex/index.ts(same indist/plugins/convex/index.jsof 0.11.4):Meanwhile the type signature explicitly allows async (
Promise<Record<string, any>> | Record<string, any>).Proposed fix
Sync
definePayloadstill works,awaiting a non-Promise is a no-op. PR to follow.Environment
@convex-dev/better-auth: 0.10.10 and 0.11.4 (both affected)better-auth: 1.4.9Possibly related
#291 reports custom payload fields missing from the first JWT. The reporter hypothesized trigger/transaction timing, but this async-spread bug is a plausible alternative root cause, worth checking whether their
definePayload(or any trigger populating the user doc) is async.