kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
286 lines (284 loc) • 10.2 kB
JavaScript
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 };