UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

286 lines (284 loc) 10.2 kB
import { r as omit } from "./upstream-BR6sBLg3.js"; import { createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api"; import { bearer } from "better-auth/plugins/bearer"; import { jwt } from "better-auth/plugins/jwt"; import { oidcProvider } from "better-auth/plugins/oidc-provider"; //#region src/auth/internal/convex-plugin.ts const JWT_COOKIE_NAME = "convex_jwt"; const normalizeAfterHooks = (hooks) => { return hooks.map((hook) => ({ ...hook, matcher: (ctx) => Boolean(hook.matcher(ctx)) })); }; const getJwksAlg = (authProvider) => { const isCustomJwt = "type" in authProvider && authProvider.type === "customJwt"; if (isCustomJwt && authProvider.algorithm !== "RS256") throw new Error("Only RS256 is supported for custom JWT with Better Auth"); return isCustomJwt ? authProvider.algorithm : "EdDSA"; }; const parseAuthConfig = (authConfig, opts) => { const providerConfigs = authConfig.providers.filter((provider) => provider.applicationID === "convex"); if (providerConfigs.length > 1) throw new Error("Multiple auth providers with applicationID 'convex' detected. Please use only one."); const providerConfig = providerConfigs[0]; if (!providerConfig) throw new Error("No auth provider with applicationID 'convex' found. Please add one to your auth config."); if (!("type" in providerConfig) || providerConfig.type !== "customJwt") return providerConfig; const isDataUriJwks = providerConfig.jwks?.startsWith("data:text/"); if (isDataUriJwks && !opts.jwks) throw new Error("Static JWKS detected in auth config, but missing from Convex plugin"); if (!isDataUriJwks && opts.jwks) console.warn("Static JWKS provided to Convex plugin, but not to auth config. This adds an unnecessary network request for token verification."); return providerConfig; }; const convex = (opts) => { const jwtExpirationSeconds = opts.jwt?.expirationSeconds ?? opts.jwtExpirationSeconds ?? 900; const oidcProvider$1 = oidcProvider({ loginPage: "/not-used", metadata: { issuer: `${process.env.CONVEX_SITE_URL}`, jwks_uri: `${process.env.CONVEX_SITE_URL}${opts.options?.basePath ?? "/api/auth"}/convex/jwks` }, __skipDeprecationWarning: true }); const providerConfig = parseAuthConfig(opts.authConfig, opts); const jwtOptions = { jwt: { issuer: `${process.env.CONVEX_SITE_URL}`, audience: "convex", expirationTime: `${jwtExpirationSeconds}s`, definePayload: async ({ user, session }) => ({ ...opts.jwt?.definePayload ? await opts.jwt.definePayload({ session, user }) : omit(user, ["id", "image"]), sessionId: session.id, iat: Math.floor(Date.now() / 1e3) }) }, jwks: { keyPairConfig: { alg: getJwksAlg(providerConfig) } } }; const jwks = opts.jwks ? JSON.parse(opts.jwks) : void 0; const jwt$1 = jwt({ ...jwtOptions, adapter: { createJwk: async (webKey, ctx) => { if (opts.jwks) throw new Error("Not implemented"); return await ctx.context.adapter.create({ model: "jwks", data: { ...webKey, createdAt: /* @__PURE__ */ new Date() } }); }, getJwks: async (ctx) => { if (opts.jwks) return jwks; return (await ctx.context.adapter.findMany({ model: "jwks", sortBy: { direction: "desc", field: "createdAt" } })).map((key) => ({ ...key, createdAt: new Date(key.createdAt), ...key.expiresAt ? { expiresAt: new Date(key.expiresAt) } : {} })); } } }); const bearer$1 = bearer(); const schema = { user: { fields: { userId: { type: "string", required: false, input: false } } }, ...jwt$1.schema }; return { id: "convex", init: (ctx) => { const { options } = ctx; if (options.basePath !== "/api/auth" && !opts.options?.basePath) console.warn(`Better Auth basePath set to ${options.basePath} but no basePath is set in the Convex plugin. This is probably a mistake.`); if (opts.options?.basePath && options.basePath !== opts.options?.basePath) console.warn(`Better Auth basePath ${options.basePath} does not match Convex plugin basePath ${opts.options?.basePath}. This is probably a mistake.`); }, hooks: { before: [...bearer$1.hooks.before, { matcher: (ctx) => { return !ctx.context.adapter.options?.isRunMutationCtx; }, handler: createAuthMiddleware(async (ctx) => { ctx.query = { ...ctx.query, disableRefresh: true }; ctx.context.internalAdapter.deleteSession = async (..._args) => {}; const knownSafePaths = ["/api-key/list", "/api-key/get"]; const noopWrite = (method) => { return async (..._args) => { if (ctx.path && !knownSafePaths.includes(ctx.path)) console.warn(`[convex-better-auth] Write operation "${method}" skipped in query context for ${ctx.path}`); return 0; }; }; ctx.context.adapter.create = noopWrite("create"); ctx.context.adapter.update = noopWrite("update"); ctx.context.adapter.updateMany = noopWrite("updateMany"); ctx.context.adapter.delete = noopWrite("delete"); ctx.context.adapter.deleteMany = noopWrite("deleteMany"); return { context: ctx }; }) }], after: [ ...normalizeAfterHooks(oidcProvider$1.hooks.after), { matcher: (ctx) => { return Boolean(ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/oauth2/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/phone-number/verify") || ctx.path?.startsWith("/siwe/verify") || ctx.path?.startsWith("/update-session") || ctx.path?.startsWith("/get-session") && ctx.context.session); }, handler: createAuthMiddleware(async (ctx) => { const originalSession = ctx.context.session; try { ctx.context.session = ctx.context.session ?? ctx.context.newSession; const { token } = await jwt$1.endpoints.getToken({ ...ctx, asResponse: false, headers: {}, method: "GET", returnHeaders: false, returnStatus: false }); const jwtCookie = ctx.context.createAuthCookie(JWT_COOKIE_NAME, { maxAge: jwtExpirationSeconds }); ctx.setCookie(jwtCookie.name, token, jwtCookie.attributes); } catch (_error) {} ctx.context.session = originalSession; }) }, { matcher: (ctx) => { return Boolean(ctx.path?.startsWith("/sign-out") || ctx.path?.startsWith("/delete-user") || ctx.path?.startsWith("/get-session") && !ctx.context.session); }, handler: createAuthMiddleware(async (ctx) => { const jwtCookie = ctx.context.createAuthCookie(JWT_COOKIE_NAME, { maxAge: 0 }); ctx.setCookie(jwtCookie.name, "", jwtCookie.attributes); }) } ] }, endpoints: { getOpenIdConfig: createAuthEndpoint("/convex/.well-known/openid-configuration", { method: "GET", metadata: { isAction: false } }, async (ctx) => { return await oidcProvider$1.endpoints.getOpenIdConfig({ ...ctx, asResponse: false, returnHeaders: false, returnStatus: false }); }), getJwks: createAuthEndpoint("/convex/jwks", { method: "GET", metadata: { openapi: { description: "Get the JSON Web Key Set", responses: { "200": { description: "JSON Web Key Set retrieved successfully" } } } } }, async (ctx) => { return await jwt$1.endpoints.getJwks({ ...ctx, asResponse: false, returnHeaders: false, returnStatus: false }); }), getLatestJwks: createAuthEndpoint("/convex/latest-jwks", { isAction: true, method: "POST", metadata: { SERVER_ONLY: true, openapi: { description: "Delete and regenerate JWKS, and return the new JWKS for static usage" } } }, async (ctx) => { await jwt(jwtOptions).endpoints.getJwks({ ...ctx, asResponse: false, method: "GET", returnHeaders: false, returnStatus: false }); const jwks = await ctx.context.adapter.findMany({ model: "jwks", limit: 1, sortBy: { direction: "desc", field: "createdAt" } }); jwks[0].alg = jwtOptions.jwks.keyPairConfig.alg; return jwks; }), rotateKeys: createAuthEndpoint("/convex/rotate-keys", { isAction: true, method: "POST", metadata: { SERVER_ONLY: true, openapi: { description: "Delete and regenerate JWKS, and return the new JWKS for static usage" } } }, async (ctx) => { await ctx.context.adapter.deleteMany({ model: "jwks", where: [] }); await jwt(jwtOptions).endpoints.getJwks({ ...ctx, asResponse: false, method: "GET", returnHeaders: false, returnStatus: false }); const jwks = await ctx.context.adapter.findMany({ model: "jwks", limit: 1, sortBy: { direction: "desc", field: "createdAt" } }); jwks[0].alg = jwtOptions.jwks.keyPairConfig.alg; return jwks; }), getToken: createAuthEndpoint("/convex/token", { method: "GET", requireHeaders: true, use: [sessionMiddleware], metadata: { openapi: { description: "Get a JWT token" } } }, async (ctx) => { const runEndpoint = async () => { const response = await jwt$1.endpoints.getToken({ ...ctx, asResponse: false, returnHeaders: false, returnStatus: false }); const jwtCookie = ctx.context.createAuthCookie(JWT_COOKIE_NAME, { maxAge: jwtExpirationSeconds }); ctx.setCookie(jwtCookie.name, response.token, jwtCookie.attributes); return response; }; try { return await runEndpoint(); } catch (error) { if (!opts.jwks && error?.code === "ERR_JOSE_NOT_SUPPORTED") { if (opts.jwksRotateOnTokenGenerationError) { await ctx.context.adapter.deleteMany({ model: "jwks", where: [] }); return await runEndpoint(); } console.error("Try temporarily setting jwksRotateOnTokenGenerationError: true on the Convex Better Auth plugin."); } throw error; } }) }, schema }; }; //#endregion export { convex as n, JWT_COOKIE_NAME as t };