@headless-lms/adapter-auth 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ declare const API_KEY_PREFIX = "hlms_";
10
10
  declare function buildAuthOptions(opts: CreateAuthOptions): {
11
11
  baseURL: string;
12
12
  secret: string;
13
- trustedOrigins: string[];
13
+ trustedOrigins: string[] | ((request?: Request) => Promise<string[]>);
14
14
  logger: {
15
15
  log: (level: "debug" | "info" | "warn" | "error", message: string, ...args: any[]) => void;
16
16
  };
@@ -30,7 +30,10 @@ declare function buildAuthOptions(opts: CreateAuthOptions): {
30
30
  };
31
31
  crossSubDomainCookies: {
32
32
  enabled: true;
33
- domain: string | undefined;
33
+ domain: string;
34
+ } | {
35
+ enabled: false;
36
+ domain?: undefined;
34
37
  };
35
38
  defaultCookieAttributes: {
36
39
  sameSite: "lax";
@@ -53,7 +56,7 @@ declare function buildAuthOptions(opts: CreateAuthOptions): {
53
56
  };
54
57
  url: string;
55
58
  token: string;
56
- }, request?: Request | undefined) => Promise<void>;
59
+ }, request: Request | undefined) => Promise<void>;
57
60
  onPasswordReset: (data: {
58
61
  user: {
59
62
  id: string;
@@ -256,7 +259,7 @@ declare function buildAuthOptions(opts: CreateAuthOptions): {
256
259
  declare function createAuth(opts: CreateAuthOptions): import("better-auth").Auth<{
257
260
  baseURL: string;
258
261
  secret: string;
259
- trustedOrigins: string[];
262
+ trustedOrigins: string[] | ((request?: Request) => Promise<string[]>);
260
263
  logger: {
261
264
  log: (level: "debug" | "info" | "warn" | "error", message: string, ...args: any[]) => void;
262
265
  };
@@ -276,7 +279,10 @@ declare function createAuth(opts: CreateAuthOptions): import("better-auth").Auth
276
279
  };
277
280
  crossSubDomainCookies: {
278
281
  enabled: true;
279
- domain: string | undefined;
282
+ domain: string;
283
+ } | {
284
+ enabled: false;
285
+ domain?: undefined;
280
286
  };
281
287
  defaultCookieAttributes: {
282
288
  sameSite: "lax";
@@ -299,7 +305,7 @@ declare function createAuth(opts: CreateAuthOptions): import("better-auth").Auth
299
305
  };
300
306
  url: string;
301
307
  token: string;
302
- }, request?: Request | undefined) => Promise<void>;
308
+ }, request: Request | undefined) => Promise<void>;
303
309
  onPasswordReset: (data: {
304
310
  user: {
305
311
  id: string;
@@ -40,6 +40,25 @@ const OAUTH_CLIENT_WRITES = /* @__PURE__ */ new Set([
40
40
  "/oauth2/update-client",
41
41
  "/oauth2/delete-client"
42
42
  ]);
43
+ const CALLBACK_PARAMS = [
44
+ "callbackURL",
45
+ "errorCallbackURL",
46
+ "newUserCallbackURL"
47
+ ];
48
+ function webOrigin(value) {
49
+ if (!value || !URL.canParse(value)) return null;
50
+ const url = new URL(value);
51
+ return url.protocol === "https:" || url.protocol === "http:" ? url.origin : null;
52
+ }
53
+ function callbackOrigins(url) {
54
+ if (!URL.canParse(url)) return [];
55
+ const { searchParams } = new URL(url);
56
+ return CALLBACK_PARAMS.map((name) => webOrigin(searchParams.get(name))).filter((origin) => origin !== null);
57
+ }
58
+ function requestOrigins(request) {
59
+ const page = webOrigin(request.headers.get("origin") || request.headers.get("referer"));
60
+ return [.../* @__PURE__ */ new Set([...page ? [page] : [], ...callbackOrigins(request.url)])];
61
+ }
43
62
  function activeOrgOf(session) {
44
63
  const value = session?.activeOrganizationId;
45
64
  return typeof value === "string" && value.length > 0 ? value : null;
@@ -77,10 +96,28 @@ function buildAuthOptions(opts) {
77
96
  const detached = (name, hook) => async (...args) => {
78
97
  hook(...args).catch((err) => opts.logger.error(`auth hook ${name} failed`, { err }));
79
98
  };
99
+ const orgHosts = opts.orgHosts;
100
+ const orgHostsOf = async (request) => {
101
+ if (!orgHosts || !request) return [];
102
+ const trusted = [];
103
+ for (const origin of requestOrigins(request)) if (!opts.trustedOrigins.includes(origin) && await orgHosts.has(origin)) trusted.push(origin);
104
+ return trusted;
105
+ };
106
+ const linkAllowed = async (email, url) => {
107
+ if (!orgHosts) return true;
108
+ for (const origin of callbackOrigins(url)) {
109
+ if (opts.trustedOrigins.includes(origin) || !await orgHosts.has(origin)) continue;
110
+ if (!await orgHosts.hasFor(email, origin)) {
111
+ opts.logger.warn("auth link not sent: its host is not one of the person's organizations", { origin });
112
+ return false;
113
+ }
114
+ }
115
+ return true;
116
+ };
80
117
  return {
81
118
  baseURL: opts.baseUrl,
82
119
  secret: opts.secret,
83
- trustedOrigins: opts.trustedOrigins,
120
+ trustedOrigins: orgHosts ? async (request) => [...opts.trustedOrigins, ...await orgHostsOf(request)] : opts.trustedOrigins,
84
121
  logger: { log: (level, message, ...args) => {
85
122
  const [first] = args;
86
123
  opts.logger[level](message, first instanceof Error ? { err: first } : args.length > 0 ? { details: args } : void 0);
@@ -95,10 +132,10 @@ function buildAuthOptions(opts) {
95
132
  ...opts.ipAddressHeaders?.length && { ipAddressHeaders: opts.ipAddressHeaders }
96
133
  },
97
134
  database: { generateId: () => prefixId("id") },
98
- crossSubDomainCookies: {
135
+ crossSubDomainCookies: opts.cookieDomain ? {
99
136
  enabled: true,
100
- domain: opts.cookieDomain || void 0
101
- },
137
+ domain: opts.cookieDomain
138
+ } : { enabled: false },
102
139
  defaultCookieAttributes: {
103
140
  sameSite: "lax",
104
141
  secure: opts.secureCookies ?? false,
@@ -111,14 +148,18 @@ function buildAuthOptions(opts) {
111
148
  }),
112
149
  emailAndPassword: {
113
150
  enabled: true,
114
- sendResetPassword: detached("sendResetPassword", sendResetPassword),
151
+ sendResetPassword: detached("sendResetPassword", async (data, request) => {
152
+ if (await linkAllowed(data.user.email, data.url)) await sendResetPassword(data, request);
153
+ }),
115
154
  onPasswordReset: detached("onPasswordReset", onPasswordReset),
116
155
  revokeSessionsOnPasswordReset: true
117
156
  },
118
157
  plugins: [
119
158
  magicLink({
120
159
  disableSignUp: true,
121
- sendMagicLink
160
+ sendMagicLink: async (data, ctx) => {
161
+ if (await linkAllowed(data.email, data.url)) await sendMagicLink(data, ctx);
162
+ }
122
163
  }),
123
164
  organization({
124
165
  ac,
@@ -1 +1 @@
1
- {"version":3,"file":"better-auth.js","names":[],"sources":["../src/better-auth.ts"],"sourcesContent":["import type {\n AccessGrant,\n AccessRequest,\n AccessTokenVerifier,\n ActiveSession,\n ConnectedApp,\n HeaderBag,\n Logger,\n NewOrganizationInput,\n SessionVerifier,\n} from \"@headless-lms/core/types\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport { fromNodeHeaders } from \"better-auth/node\";\nimport { APIError, betterAuth, type BetterAuthOptions, type BetterAuthPlugin } from \"better-auth\";\nimport { createAuthMiddleware } from \"better-auth/api\";\nimport type { CreateAuthOptions } from \"./types.js\";\nimport { drizzleAdapter } from \"better-auth/adapters/drizzle\";\nimport * as authSchema from \"@headless-lms/adapter-db/schema/better-auth\";\nimport { prefixId } from \"@headless-lms/core/shared/id\";\nimport { ConflictError, NotFoundError } from \"@headless-lms/core/shared/errors\";\nimport { jwt, magicLink, organization } from \"better-auth/plugins\";\nimport { getOrgAdapter } from \"better-auth/plugins/organization\";\nimport {\n type AccessTokenAuthorization,\n createDpopReplayStore,\n enforceDpopBinding,\n isDpopBindingError,\n verifyJwsAccessToken,\n} from \"better-auth/oauth2\";\nimport { oauthProviderAuthServerMetadata } from \"@better-auth/oauth-provider\";\nimport { mcp } from \"@better-auth/mcp\";\nimport { cimd } from \"@better-auth/cimd\";\nimport { fetchClientMetadataResource } from \"@better-auth/cimd/node\";\nimport { apiKey } from \"@better-auth/api-key\";\nimport { and, count, eq, sql } from \"drizzle-orm\";\nimport { ac, OWNER_MEMBERSHIP, PARTICIPANT_MEMBERSHIP, roles } from \"./access.js\";\nimport {\n accessTokenAuthorization,\n consentOrgId,\n grantFromClaims,\n isCompactJws,\n needsOrgChoice,\n orgIdFromMetadata,\n roleFromMetadata,\n} from \"./access-grant.js\";\nimport { withLoopbackNativeDefault } from \"./client-registration.js\";\nimport {\n ALL_PERMISSIONS,\n type Accounts,\n type ApiKeys,\n type AuthHeaders,\n type AuthOrganization,\n type NewApiKey,\n type OrgAdmin,\n OrganizationRuleError,\n type PlatformAccount,\n type StoredApiKey,\n type UpdateOrganizationInput,\n} from \"@headless-lms/core/organizations\";\nimport {\n ACCESS_TOKEN_TTL_SECONDS,\n ORG_CLAIM,\n type SessionAdmin,\n} from \"@headless-lms/core/identity\";\n\nconst JWT_ALG = \"EdDSA\";\n\nexport const API_KEY_PREFIX = \"hlms_\";\nconst API_KEY_NAME_MAX = 80;\nconst DAY_SECONDS = 24 * 60 * 60;\n\nconst OPEN_ORG_ENDPOINTS = new Set([\n \"/organization/set-active\",\n \"/organization/list\",\n \"/organization/get-active-member\",\n]);\n\nconst CORE_API_KEY_ENDPOINTS = new Set([\n \"/api-key/get\",\n \"/api-key/list\",\n \"/api-key/update\",\n \"/api-key/delete\",\n]);\n\nconst OAUTH_CLIENT_WRITES = new Set([\n \"/oauth2/create-client\",\n \"/oauth2/update-client\",\n \"/oauth2/delete-client\",\n]);\n\nfunction activeOrgOf(session: unknown): string | null {\n const value = (session as { activeOrganizationId?: string | null } | null)?.activeOrganizationId;\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\nasync function memberOrgIds(db: CreateAuthOptions[\"db\"], userId: string): Promise<string[]> {\n const rows = await db\n .select({ organizationId: authSchema.member.organizationId })\n .from(authSchema.member)\n .where(eq(authSchema.member.userId, userId));\n return rows.map((row) => row.organizationId);\n}\n\nfunction mcpOAuthPlugins(opts: CreateAuthOptions): BetterAuthPlugin[] {\n return [\n mcp({\n resource: opts.mcp.resource,\n loginPage: opts.mcp.loginPage,\n consentPage: opts.mcp.consentPage,\n scopes: [...ALL_PERMISSIONS, \"offline_access\"],\n clientRegistrationDefaultScopes: [...ALL_PERMISSIONS, \"offline_access\"],\n accessTokenExpiresIn: ACCESS_TOKEN_TTL_SECONDS,\n allowDynamicClientRegistration: true,\n allowUnauthenticatedClientRegistration: true,\n postLogin: {\n page: opts.mcp.orgPickerPage,\n shouldRedirect: ({ session, user }) =>\n needsOrgChoice(activeOrgOf(session), () => memberOrgIds(opts.db, user.id)),\n consentReferenceId: async ({ session, user }) => {\n const orgId = await consentOrgId(activeOrgOf(session), () =>\n memberOrgIds(opts.db, user.id),\n );\n if (!orgId) {\n throw new APIError(\"FORBIDDEN\", {\n message: \"Choose an organization you are a member of\",\n });\n }\n return orgId;\n },\n },\n customAccessTokenClaims: ({ referenceId }) =>\n referenceId ? { [ORG_CLAIM]: referenceId } : {},\n }) as BetterAuthPlugin,\n cimd({ fetchClientMetadataResource, metadataProfile: \"mcp-2026-07-28\" }) as BetterAuthPlugin,\n ];\n}\n\nexport function buildAuthOptions(opts: CreateAuthOptions) {\n const {\n sendResetPassword,\n onPasswordReset,\n sendMagicLink,\n beforeUserCreate,\n beforeCreateSession,\n } = opts.hooks;\n const detached =\n <A extends unknown[]>(name: string, hook: (...args: A) => Promise<void>) =>\n async (...args: A): Promise<void> => {\n void hook(...args).catch((err: unknown) =>\n opts.logger.error(`auth hook ${name} failed`, { err }),\n );\n };\n\n return {\n baseURL: opts.baseUrl,\n secret: opts.secret,\n trustedOrigins: opts.trustedOrigins,\n logger: {\n log: (level, message, ...args) => {\n const [first] = args;\n opts.logger[level](\n message,\n first instanceof Error ? { err: first } : args.length > 0 ? { details: args } : undefined,\n );\n },\n },\n session: {\n // Signed short-lived cookie cache: avoids a Postgres session lookup on\n // every request. The BFF verifies the session per request (each API call\n // + every SSR getSession), so without this each one is a DB round-trip.\n // The cache holds for maxAge; sign-out / expiry still invalidate it.\n cookieCache: {\n enabled: true,\n maxAge: 5 * 60,\n },\n },\n advanced: {\n ipAddress: {\n ...(opts.trustedProxies?.length && { trustedProxies: opts.trustedProxies }),\n ...(opts.ipAddressHeaders?.length && { ipAddressHeaders: opts.ipAddressHeaders }),\n },\n database: {\n // Prefixed, KSUID-bodied ids for every better-auth table (usr_, org_, …).\n generateId: () => prefixId(\"id\"),\n },\n // Cross-subdomain shared session cookie for admin/api/web on one parent\n // domain (e.g. `.example.com` in prod). Left unset in local dev so the\n // host-only `localhost` cookie is used, which is already shared across\n // ports (cookies are not port-scoped).\n crossSubDomainCookies: {\n enabled: true,\n domain: opts.cookieDomain || undefined,\n },\n // Same-site cookie for the shared-parent-domain plan. Only switch to\n // `sameSite: \"none\"` + `secure` if admin and api are genuinely cross-site\n // (different registrable domains).\n defaultCookieAttributes: {\n sameSite: \"lax\",\n secure: opts.secureCookies ?? false,\n httpOnly: true,\n },\n },\n database: drizzleAdapter(opts.db, {\n provider: \"pg\",\n schema: authSchema,\n }),\n emailAndPassword: {\n enabled: true,\n // Detached so send time can't reveal which emails have accounts, and a failed notice can't fail the reset.\n sendResetPassword: detached(\"sendResetPassword\", sendResetPassword),\n onPasswordReset: detached(\"onPasswordReset\", onPasswordReset),\n revokeSessionsOnPasswordReset: true,\n },\n plugins: [\n magicLink({\n disableSignUp: true,\n sendMagicLink,\n }),\n organization({\n ac,\n roles,\n creatorRole: OWNER_MEMBERSHIP,\n membershipLimit: Number.MAX_SAFE_INTEGER,\n organizationHooks: opts.hooks,\n }),\n jwt({\n disableSettingJwtHeader: true,\n jwks: { keyPairConfig: { alg: JWT_ALG } },\n }) as BetterAuthPlugin,\n ...(opts.mcp.oauthEnabled ? mcpOAuthPlugins(opts) : []),\n apiKey({\n enableMetadata: true,\n defaultPrefix: API_KEY_PREFIX,\n maximumNameLength: API_KEY_NAME_MAX,\n rateLimit: { enabled: false },\n }) as BetterAuthPlugin,\n ],\n hooks: {\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === \"/api-key/create\" && (ctx.request || ctx.headers)) {\n throw new APIError(\"FORBIDDEN\", { message: \"API keys are created through the LMS\" });\n }\n if (\n ctx.request &&\n ctx.path.startsWith(\"/organization/\") &&\n !OPEN_ORG_ENDPOINTS.has(ctx.path)\n ) {\n throw new APIError(\"FORBIDDEN\", { message: \"Organizations are managed through the LMS\" });\n }\n if (ctx.request && CORE_API_KEY_ENDPOINTS.has(ctx.path)) {\n throw new APIError(\"FORBIDDEN\", { message: \"API keys are managed through the LMS\" });\n }\n if (OAUTH_CLIENT_WRITES.has(ctx.path) && (ctx.request || ctx.headers)) {\n throw new APIError(\"FORBIDDEN\", {\n message: \"Apps register themselves when they connect\",\n });\n }\n if (ctx.path === \"/api-key/update\" && ctx.body?.metadata !== undefined) {\n throw new APIError(\"FORBIDDEN\", {\n message: \"An API key stays in the organization it was made for\",\n });\n }\n if (ctx.path !== \"/oauth2/register\") {\n return;\n }\n const body = withLoopbackNativeDefault(ctx.body);\n return body ? { context: { body } } : undefined;\n }),\n },\n databaseHooks: {\n user: {\n create: {\n before: beforeUserCreate,\n },\n },\n session: {\n create: {\n before: beforeCreateSession,\n },\n },\n },\n } satisfies BetterAuthOptions;\n}\n\nexport function createAuth(opts: CreateAuthOptions) {\n return betterAuth(buildAuthOptions(opts));\n}\n\nexport type Auth = ReturnType<typeof createAuth>;\n\ntype JSONWebKeySet = { keys: Array<Record<string, unknown>> };\n\ntype ApiKeyRow = {\n id: string;\n name: string | null;\n start: string | null;\n referenceId: string;\n metadata: unknown;\n createdAt: Date | string;\n lastRequest: Date | string | null;\n expiresAt: Date | string | null;\n};\n\ntype PluginApi = {\n getJwks(): Promise<JSONWebKeySet>;\n verifyApiKey(input: {\n body: { key: string };\n }): Promise<{ valid: boolean; key: ApiKeyRow | null }>;\n createApiKey(input: {\n body: {\n name: string;\n userId: string;\n metadata: Record<string, unknown>;\n expiresIn?: number;\n };\n }): Promise<ApiKeyRow & { key: string }>;\n};\n\nfunction toStored(key: ApiKeyRow): StoredApiKey {\n return {\n id: key.id,\n name: key.name ?? null,\n start: key.start ?? null,\n roleKey: roleFromMetadata(key.metadata),\n createdAt: new Date(key.createdAt),\n lastUsedAt: key.lastRequest ? new Date(key.lastRequest) : null,\n expiresAt: key.expiresAt ? new Date(key.expiresAt) : null,\n };\n}\n\nfunction apiKeyHeader(headers: HeaderBag): string | null {\n const header = headers[\"x-api-key\"];\n if (typeof header === \"string\" && header.length > 0) {\n return header;\n }\n const authorization = accessTokenAuthorization(headers.authorization);\n if (authorization?.scheme === \"Bearer\" && !isCompactJws(authorization.token)) {\n return authorization.token;\n }\n return null;\n}\n\nexport class BetterAuth\n implements SessionVerifier, OrgAdmin, SessionAdmin, AccessTokenVerifier, ApiKeys, Accounts\n{\n protected auth: Auth;\n protected db: CreateAuthOptions[\"db\"];\n protected logger: Logger;\n protected mcp: CreateAuthOptions[\"mcp\"];\n protected issuer: string;\n protected jwksCacheKey = {};\n\n constructor(opts: CreateAuthOptions, auth?: Auth) {\n this.auth = auth ?? createAuth(opts);\n this.db = opts.db;\n this.logger = opts.logger;\n this.mcp = opts.mcp;\n this.issuer = `${opts.baseUrl.replace(/\\/+$/, \"\")}/api/auth`;\n }\n\n protected get pluginApi(): PluginApi {\n return this.auth.api as unknown as PluginApi;\n }\n\n async handler(request: Request): Promise<Response> {\n return this.auth.handler(request);\n }\n\n async authorizationServerMetadata(request: Request): Promise<Response> {\n return oauthProviderAuthServerMetadata(\n this.auth as unknown as Parameters<typeof oauthProviderAuthServerMetadata>[0],\n )(request);\n }\n\n async verifyAccess(request: AccessRequest): Promise<AccessGrant | null> {\n const key = apiKeyHeader(request.headers);\n if (key) {\n return this.grantForKey(key);\n }\n const authorization = accessTokenAuthorization(request.headers.authorization);\n if (!authorization) {\n return null;\n }\n return this.mcp.oauthEnabled ? this.verifyAccessToken(authorization, request) : null;\n }\n\n async verifyApiKey(headers: HeaderBag): Promise<AccessGrant | null> {\n const key = apiKeyHeader(headers);\n return key ? this.grantForKey(key) : null;\n }\n\n protected async verifyAccessToken(\n authorization: AccessTokenAuthorization,\n request: AccessRequest,\n ): Promise<AccessGrant | null> {\n let claims: Awaited<ReturnType<typeof verifyJwsAccessToken>>;\n try {\n claims = await verifyJwsAccessToken(authorization.token, {\n jwksFetch: async () => this.pluginApi.getJwks(),\n jwksCacheKey: this.jwksCacheKey,\n verifyOptions: {\n audience: this.mcp.resource,\n issuer: this.issuer,\n algorithms: [JWT_ALG],\n },\n });\n } catch {\n return null;\n }\n const ctx = await this.auth.$context;\n const proof = request.headers.dpop;\n try {\n await enforceDpopBinding({\n payload: claims,\n authorization,\n proofJwt: typeof proof === \"string\" ? proof : undefined,\n method: request.method,\n url: request.url,\n replayStore: createDpopReplayStore(ctx.internalAdapter),\n });\n } catch (error) {\n if (isDpopBindingError(error)) {\n return null;\n }\n throw error;\n }\n const grant = grantFromClaims(claims);\n if (!grant) {\n return null;\n }\n const consent = await ctx.adapter.findOne<{ scopes: string[] }>({\n model: \"oauthConsent\",\n where: [\n { field: \"clientId\", value: grant.clientId },\n { field: \"userId\", value: grant.userId },\n { field: \"referenceId\", value: grant.orgId },\n ],\n });\n if (!consent || !(\"scopes\" in grant.limit)) {\n return null;\n }\n return {\n ...grant,\n limit: { scopes: grant.limit.scopes.filter((scope) => consent.scopes.includes(scope)) },\n };\n }\n\n protected async grantForKey(key: string): Promise<AccessGrant | null> {\n const result = await this.pluginApi.verifyApiKey({ body: { key } });\n if (!result.valid || !result.key) {\n return null;\n }\n const orgId = orgIdFromMetadata(result.key.metadata);\n const roleKey = roleFromMetadata(result.key.metadata);\n if (!orgId || !roleKey) {\n return null;\n }\n return {\n userId: result.key.referenceId,\n orgId,\n clientId: `api-key:${result.key.id}`,\n limit: { roleKey },\n };\n }\n\n protected async ownKeys(orgId: string, userExternalId: string): Promise<ApiKeyRow[]> {\n const ctx = await this.auth.$context;\n const keys = await ctx.adapter.findMany<ApiKeyRow>({\n model: \"apikey\",\n where: [{ field: \"referenceId\", value: userExternalId }],\n });\n return keys.filter((key) => orgIdFromMetadata(key.metadata) === orgId);\n }\n\n async list(orgId: string, userExternalId: string): Promise<StoredApiKey[]> {\n return (await this.ownKeys(orgId, userExternalId)).map(toStored);\n }\n\n async create(\n orgId: string,\n userExternalId: string,\n input: NewApiKey,\n ): Promise<StoredApiKey & { key: string }> {\n const created = await this.pluginApi.createApiKey({\n body: {\n name: input.name,\n userId: userExternalId,\n metadata: { organizationId: orgId, role: input.roleKey },\n ...(input.expiresInDays !== undefined && {\n expiresIn: input.expiresInDays * DAY_SECONDS,\n }),\n },\n });\n return { ...toStored(created), key: created.key };\n }\n\n async countWithRole(orgId: string, roleKey: string): Promise<number> {\n const metadata = sql`${authSchema.apikey.metadata}::jsonb`;\n const [row] = await this.db\n .select({ value: count() })\n .from(authSchema.apikey)\n .where(\n and(\n sql`${metadata} ->> 'organizationId' = ${orgId}`,\n sql`${metadata} ->> 'role' = ${roleKey}`,\n ),\n );\n return row?.value ?? 0;\n }\n\n async createAccount(input: PlatformAccount): Promise<{ externalId: string }> {\n const ctx = await this.auth.$context;\n const user = await ctx.internalAdapter.createUser(\n { email: input.email, name: input.name, emailVerified: true },\n { method: \"admin\" },\n );\n return { externalId: user.id };\n }\n\n async revoke(orgId: string, userExternalId: string, keyId: string): Promise<void> {\n const owned = await this.ownKeys(orgId, userExternalId);\n if (!owned.some((key) => key.id === keyId)) {\n throw new NotFoundError(\"API key\", keyId);\n }\n const ctx = await this.auth.$context;\n await ctx.adapter.delete({ model: \"apikey\", where: [{ field: \"id\", value: keyId }] });\n }\n\n async listApps(orgId: string, userExternalId: string): Promise<ConnectedApp[]> {\n if (!this.mcp.oauthEnabled) {\n return [];\n }\n const ctx = await this.auth.$context;\n const consents = await ctx.adapter.findMany<{\n clientId: string;\n scopes: string[];\n createdAt: Date | null;\n }>({\n model: \"oauthConsent\",\n where: [\n { field: \"userId\", value: userExternalId },\n { field: \"referenceId\", value: orgId },\n ],\n });\n return Promise.all(\n consents.map(async (consent) => {\n const client = await ctx.adapter.findOne<{ name: string | null; icon: string | null }>({\n model: \"oauthClient\",\n where: [{ field: \"clientId\", value: consent.clientId }],\n });\n return {\n clientId: consent.clientId,\n name: client?.name ?? consent.clientId,\n iconUrl: client?.icon ?? null,\n scopes: consent.scopes,\n connectedAt: consent.createdAt ?? new Date(0),\n };\n }),\n );\n }\n\n async revokeApp(orgId: string, userExternalId: string, clientId: string): Promise<void> {\n if (!this.mcp.oauthEnabled) {\n return;\n }\n const ctx = await this.auth.$context;\n const where = [\n { field: \"clientId\", value: clientId },\n { field: \"userId\", value: userExternalId },\n { field: \"referenceId\", value: orgId },\n ];\n await ctx.adapter.deleteMany({ model: \"oauthAccessToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthRefreshToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthConsent\", where });\n }\n\n async revokeAllFor(orgId: string, userExternalId: string): Promise<void> {\n const ctx = await this.auth.$context;\n const keys = await this.ownKeys(orgId, userExternalId);\n for (const key of keys) {\n await ctx.adapter.delete({ model: \"apikey\", where: [{ field: \"id\", value: key.id }] });\n }\n if (this.mcp.oauthEnabled) {\n const where = [\n { field: \"userId\", value: userExternalId },\n { field: \"referenceId\", value: orgId },\n ];\n await ctx.adapter.deleteMany({ model: \"oauthAccessToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthRefreshToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthConsent\", where });\n }\n this.logger.info(\"access revoked for person\", {\n orgId,\n userExternalId,\n apiKeys: keys.length,\n });\n }\n\n async verify(headers: IncomingHttpHeaders): Promise<ActiveSession | null> {\n const res = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n if (!res) {\n return null;\n }\n return {\n user: {\n id: res.user.id,\n createdAt: res.user.createdAt,\n email: res.user.email,\n firstName: res.user.name,\n externalId: res.user.id,\n lastName: res.user.name,\n updatedAt: res.user.updatedAt,\n },\n session: {\n activeOrganizationId: res.session.activeOrganizationId ?? null,\n },\n };\n }\n\n /** Stamps the org onto the caller's session row. The 5-minute cookie cache\n * still holds the pre-stamp session, so callers refresh it client-side with\n * `getSession({ disableCookieCache: true })`. */\n async stampActiveOrganization(headers: AuthHeaders, orgId: string): Promise<boolean> {\n const session = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n const token = (session?.session as { token?: string } | undefined)?.token;\n if (!token) {\n return false;\n }\n const ctx = await this.auth.$context;\n await ctx.internalAdapter.updateSession(token, { activeOrganizationId: orgId });\n return true;\n }\n\n async createOrganization(\n headers: AuthHeaders,\n input: NewOrganizationInput,\n ): Promise<AuthOrganization> {\n const org = await this.auth.api.createOrganization({\n body: { name: input.name, slug: input.slug },\n headers: fromNodeHeaders(headers),\n });\n if (!org) {\n throw new Error(`Couldn't create organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n }\n\n async createOrganizationFor(\n ownerExternalId: string,\n input: NewOrganizationInput,\n ): Promise<AuthOrganization> {\n let org: Awaited<ReturnType<Auth[\"api\"][\"createOrganization\"]>>;\n try {\n org = await this.auth.api.createOrganization({\n body: { name: input.name, slug: input.slug, userId: ownerExternalId },\n });\n } catch (err) {\n if (err instanceof APIError && err.body?.code === \"ORGANIZATION_ALREADY_EXISTS\") {\n throw new ConflictError(\"That slug is already in use\");\n }\n throw err;\n }\n if (!org) {\n throw new Error(`Couldn't create organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n }\n async setActiveOrganization(headers: AuthHeaders, externalId: string): Promise<void> {\n await this.auth.api.setActiveOrganization({\n body: { organizationId: externalId },\n headers: fromNodeHeaders(headers),\n });\n }\n protected async orgAdapter() {\n const context = await this.auth.$context;\n return getOrgAdapter(context as unknown as Parameters<typeof getOrgAdapter>[0], {});\n }\n\n async updateOrganization(externalId: string, input: UpdateOrganizationInput): Promise<void> {\n const data = {\n ...(input.name !== undefined && { name: input.name }),\n ...(input.slug !== undefined && { slug: input.slug }),\n };\n if (Object.keys(data).length === 0) {\n return;\n }\n const adapter = await this.orgAdapter();\n const updated = await adapter.updateOrganization(externalId, data);\n if (!updated) {\n throw new NotFoundError(\"Organization\", externalId);\n }\n }\n async deleteOrganization(headers: AuthHeaders, externalId: string): Promise<void> {\n try {\n await this.auth.api.deleteOrganization({\n body: { organizationId: externalId },\n headers: fromNodeHeaders(headers),\n });\n } catch (err) {\n if (err instanceof APIError) {\n throw new OrganizationRuleError(err.message || \"Could not delete organization\");\n }\n throw err;\n }\n }\n async grantMembership(orgExternalId: string, userExternalId: string): Promise<void> {\n const adapter = await this.orgAdapter();\n const existing = await adapter.findMemberByOrgId({\n userId: userExternalId,\n organizationId: orgExternalId,\n });\n if (existing) {\n return;\n }\n await adapter.createMember({\n organizationId: orgExternalId,\n userId: userExternalId,\n role: PARTICIPANT_MEMBERSHIP,\n });\n }\n\n async setMembershipOwner(\n orgExternalId: string,\n userExternalId: string,\n owner: boolean,\n ): Promise<void> {\n const adapter = await this.orgAdapter();\n const role = owner ? OWNER_MEMBERSHIP : PARTICIPANT_MEMBERSHIP;\n const existing = await adapter.findMemberByOrgId({\n userId: userExternalId,\n organizationId: orgExternalId,\n });\n if (!existing) {\n await adapter.createMember({ organizationId: orgExternalId, userId: userExternalId, role });\n return;\n }\n if (existing.role !== role) {\n await adapter.updateMember(existing.id, role);\n }\n }\n\n async removeMembership(orgExternalId: string, userExternalId: string): Promise<void> {\n const adapter = await this.orgAdapter();\n const existing = await adapter.findMemberByOrgId({\n userId: userExternalId,\n organizationId: orgExternalId,\n });\n if (!existing) {\n return;\n }\n await adapter.deleteMember({\n memberId: existing.id,\n organizationId: orgExternalId,\n userId: userExternalId,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"better-auth.js","names":[],"sources":["../src/better-auth.ts"],"sourcesContent":["import type {\n AccessGrant,\n AccessRequest,\n AccessTokenVerifier,\n ActiveSession,\n ConnectedApp,\n HeaderBag,\n Logger,\n NewOrganizationInput,\n SessionVerifier,\n} from \"@headless-lms/core/types\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport { fromNodeHeaders } from \"better-auth/node\";\nimport { APIError, betterAuth, type BetterAuthOptions, type BetterAuthPlugin } from \"better-auth\";\nimport { createAuthMiddleware } from \"better-auth/api\";\nimport type { CreateAuthOptions } from \"./types.js\";\nimport { drizzleAdapter } from \"better-auth/adapters/drizzle\";\nimport * as authSchema from \"@headless-lms/adapter-db/schema/better-auth\";\nimport { prefixId } from \"@headless-lms/core/shared/id\";\nimport { ConflictError, NotFoundError } from \"@headless-lms/core/shared/errors\";\nimport { jwt, magicLink, organization } from \"better-auth/plugins\";\nimport { getOrgAdapter } from \"better-auth/plugins/organization\";\nimport {\n type AccessTokenAuthorization,\n createDpopReplayStore,\n enforceDpopBinding,\n isDpopBindingError,\n verifyJwsAccessToken,\n} from \"better-auth/oauth2\";\nimport { oauthProviderAuthServerMetadata } from \"@better-auth/oauth-provider\";\nimport { mcp } from \"@better-auth/mcp\";\nimport { cimd } from \"@better-auth/cimd\";\nimport { fetchClientMetadataResource } from \"@better-auth/cimd/node\";\nimport { apiKey } from \"@better-auth/api-key\";\nimport { and, count, eq, sql } from \"drizzle-orm\";\nimport { ac, OWNER_MEMBERSHIP, PARTICIPANT_MEMBERSHIP, roles } from \"./access.js\";\nimport {\n accessTokenAuthorization,\n consentOrgId,\n grantFromClaims,\n isCompactJws,\n needsOrgChoice,\n orgIdFromMetadata,\n roleFromMetadata,\n} from \"./access-grant.js\";\nimport { withLoopbackNativeDefault } from \"./client-registration.js\";\nimport {\n ALL_PERMISSIONS,\n type Accounts,\n type ApiKeys,\n type AuthHeaders,\n type AuthOrganization,\n type NewApiKey,\n type OrgAdmin,\n OrganizationRuleError,\n type PlatformAccount,\n type StoredApiKey,\n type UpdateOrganizationInput,\n} from \"@headless-lms/core/organizations\";\nimport {\n ACCESS_TOKEN_TTL_SECONDS,\n ORG_CLAIM,\n type SessionAdmin,\n} from \"@headless-lms/core/identity\";\n\nconst JWT_ALG = \"EdDSA\";\n\nexport const API_KEY_PREFIX = \"hlms_\";\nconst API_KEY_NAME_MAX = 80;\nconst DAY_SECONDS = 24 * 60 * 60;\n\nconst OPEN_ORG_ENDPOINTS = new Set([\n \"/organization/set-active\",\n \"/organization/list\",\n \"/organization/get-active-member\",\n]);\n\nconst CORE_API_KEY_ENDPOINTS = new Set([\n \"/api-key/get\",\n \"/api-key/list\",\n \"/api-key/update\",\n \"/api-key/delete\",\n]);\n\nconst OAUTH_CLIENT_WRITES = new Set([\n \"/oauth2/create-client\",\n \"/oauth2/update-client\",\n \"/oauth2/delete-client\",\n]);\n\nconst CALLBACK_PARAMS = [\"callbackURL\", \"errorCallbackURL\", \"newUserCallbackURL\"];\n\nfunction webOrigin(value: string | null): string | null {\n if (!value || !URL.canParse(value)) {\n return null;\n }\n const url = new URL(value);\n return url.protocol === \"https:\" || url.protocol === \"http:\" ? url.origin : null;\n}\n\nfunction callbackOrigins(url: string): string[] {\n if (!URL.canParse(url)) {\n return [];\n }\n const { searchParams } = new URL(url);\n return CALLBACK_PARAMS.map((name) => webOrigin(searchParams.get(name))).filter(\n (origin): origin is string => origin !== null,\n );\n}\n\nfunction requestOrigins(request: Request): string[] {\n const page = webOrigin(request.headers.get(\"origin\") || request.headers.get(\"referer\"));\n return [...new Set([...(page ? [page] : []), ...callbackOrigins(request.url)])];\n}\n\nfunction activeOrgOf(session: unknown): string | null {\n const value = (session as { activeOrganizationId?: string | null } | null)?.activeOrganizationId;\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\nasync function memberOrgIds(db: CreateAuthOptions[\"db\"], userId: string): Promise<string[]> {\n const rows = await db\n .select({ organizationId: authSchema.member.organizationId })\n .from(authSchema.member)\n .where(eq(authSchema.member.userId, userId));\n return rows.map((row) => row.organizationId);\n}\n\nfunction mcpOAuthPlugins(opts: CreateAuthOptions): BetterAuthPlugin[] {\n return [\n mcp({\n resource: opts.mcp.resource,\n loginPage: opts.mcp.loginPage,\n consentPage: opts.mcp.consentPage,\n scopes: [...ALL_PERMISSIONS, \"offline_access\"],\n clientRegistrationDefaultScopes: [...ALL_PERMISSIONS, \"offline_access\"],\n accessTokenExpiresIn: ACCESS_TOKEN_TTL_SECONDS,\n allowDynamicClientRegistration: true,\n allowUnauthenticatedClientRegistration: true,\n postLogin: {\n page: opts.mcp.orgPickerPage,\n shouldRedirect: ({ session, user }) =>\n needsOrgChoice(activeOrgOf(session), () => memberOrgIds(opts.db, user.id)),\n consentReferenceId: async ({ session, user }) => {\n const orgId = await consentOrgId(activeOrgOf(session), () =>\n memberOrgIds(opts.db, user.id),\n );\n if (!orgId) {\n throw new APIError(\"FORBIDDEN\", {\n message: \"Choose an organization you are a member of\",\n });\n }\n return orgId;\n },\n },\n customAccessTokenClaims: ({ referenceId }) =>\n referenceId ? { [ORG_CLAIM]: referenceId } : {},\n }) as BetterAuthPlugin,\n cimd({ fetchClientMetadataResource, metadataProfile: \"mcp-2026-07-28\" }) as BetterAuthPlugin,\n ];\n}\n\nexport function buildAuthOptions(opts: CreateAuthOptions) {\n const {\n sendResetPassword,\n onPasswordReset,\n sendMagicLink,\n beforeUserCreate,\n beforeCreateSession,\n } = opts.hooks;\n const detached =\n <A extends unknown[]>(name: string, hook: (...args: A) => Promise<void>) =>\n async (...args: A): Promise<void> => {\n void hook(...args).catch((err: unknown) =>\n opts.logger.error(`auth hook ${name} failed`, { err }),\n );\n };\n\n const orgHosts = opts.orgHosts;\n const orgHostsOf = async (request: Request | undefined): Promise<string[]> => {\n if (!orgHosts || !request) {\n return [];\n }\n const trusted: string[] = [];\n for (const origin of requestOrigins(request)) {\n if (!opts.trustedOrigins.includes(origin) && (await orgHosts.has(origin))) {\n trusted.push(origin);\n }\n }\n return trusted;\n };\n const linkAllowed = async (email: string, url: string): Promise<boolean> => {\n if (!orgHosts) {\n return true;\n }\n for (const origin of callbackOrigins(url)) {\n if (opts.trustedOrigins.includes(origin) || !(await orgHosts.has(origin))) {\n continue;\n }\n if (!(await orgHosts.hasFor(email, origin))) {\n opts.logger.warn(\"auth link not sent: its host is not one of the person's organizations\", {\n origin,\n });\n return false;\n }\n }\n return true;\n };\n\n return {\n baseURL: opts.baseUrl,\n secret: opts.secret,\n trustedOrigins: orgHosts\n ? async (request?: Request) => [...opts.trustedOrigins, ...(await orgHostsOf(request))]\n : opts.trustedOrigins,\n logger: {\n log: (level, message, ...args) => {\n const [first] = args;\n opts.logger[level](\n message,\n first instanceof Error ? { err: first } : args.length > 0 ? { details: args } : undefined,\n );\n },\n },\n session: {\n // Signed short-lived cookie cache: avoids a Postgres session lookup on\n // every request. The BFF verifies the session per request (each API call\n // + every SSR getSession), so without this each one is a DB round-trip.\n // The cache holds for maxAge; sign-out / expiry still invalidate it.\n cookieCache: {\n enabled: true,\n maxAge: 5 * 60,\n },\n },\n advanced: {\n ipAddress: {\n ...(opts.trustedProxies?.length && { trustedProxies: opts.trustedProxies }),\n ...(opts.ipAddressHeaders?.length && { ipAddressHeaders: opts.ipAddressHeaders }),\n },\n database: {\n // Prefixed, KSUID-bodied ids for every better-auth table (usr_, org_, …).\n generateId: () => prefixId(\"id\"),\n },\n // Cross-subdomain shared session cookie for admin/api/web on one parent\n // domain (e.g. `.example.com` in prod). Without a domain the cookie is\n // host-only: set for whichever host served the request.\n crossSubDomainCookies: opts.cookieDomain\n ? { enabled: true, domain: opts.cookieDomain }\n : { enabled: false },\n // Same-site cookie for the shared-parent-domain plan. Only switch to\n // `sameSite: \"none\"` + `secure` if admin and api are genuinely cross-site\n // (different registrable domains).\n defaultCookieAttributes: {\n sameSite: \"lax\",\n secure: opts.secureCookies ?? false,\n httpOnly: true,\n },\n },\n database: drizzleAdapter(opts.db, {\n provider: \"pg\",\n schema: authSchema,\n }),\n emailAndPassword: {\n enabled: true,\n // Detached so send time can't reveal which emails have accounts, and a failed notice can't fail the reset.\n sendResetPassword: detached(\"sendResetPassword\", async (data, request) => {\n if (await linkAllowed(data.user.email, data.url)) {\n await sendResetPassword(data, request);\n }\n }),\n onPasswordReset: detached(\"onPasswordReset\", onPasswordReset),\n revokeSessionsOnPasswordReset: true,\n },\n plugins: [\n magicLink({\n disableSignUp: true,\n sendMagicLink: async (data, ctx) => {\n if (await linkAllowed(data.email, data.url)) {\n await sendMagicLink(data, ctx);\n }\n },\n }),\n organization({\n ac,\n roles,\n creatorRole: OWNER_MEMBERSHIP,\n membershipLimit: Number.MAX_SAFE_INTEGER,\n organizationHooks: opts.hooks,\n }),\n jwt({\n disableSettingJwtHeader: true,\n jwks: { keyPairConfig: { alg: JWT_ALG } },\n }) as BetterAuthPlugin,\n ...(opts.mcp.oauthEnabled ? mcpOAuthPlugins(opts) : []),\n apiKey({\n enableMetadata: true,\n defaultPrefix: API_KEY_PREFIX,\n maximumNameLength: API_KEY_NAME_MAX,\n rateLimit: { enabled: false },\n }) as BetterAuthPlugin,\n ],\n hooks: {\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === \"/api-key/create\" && (ctx.request || ctx.headers)) {\n throw new APIError(\"FORBIDDEN\", { message: \"API keys are created through the LMS\" });\n }\n if (\n ctx.request &&\n ctx.path.startsWith(\"/organization/\") &&\n !OPEN_ORG_ENDPOINTS.has(ctx.path)\n ) {\n throw new APIError(\"FORBIDDEN\", { message: \"Organizations are managed through the LMS\" });\n }\n if (ctx.request && CORE_API_KEY_ENDPOINTS.has(ctx.path)) {\n throw new APIError(\"FORBIDDEN\", { message: \"API keys are managed through the LMS\" });\n }\n if (OAUTH_CLIENT_WRITES.has(ctx.path) && (ctx.request || ctx.headers)) {\n throw new APIError(\"FORBIDDEN\", {\n message: \"Apps register themselves when they connect\",\n });\n }\n if (ctx.path === \"/api-key/update\" && ctx.body?.metadata !== undefined) {\n throw new APIError(\"FORBIDDEN\", {\n message: \"An API key stays in the organization it was made for\",\n });\n }\n if (ctx.path !== \"/oauth2/register\") {\n return;\n }\n const body = withLoopbackNativeDefault(ctx.body);\n return body ? { context: { body } } : undefined;\n }),\n },\n databaseHooks: {\n user: {\n create: {\n before: beforeUserCreate,\n },\n },\n session: {\n create: {\n before: beforeCreateSession,\n },\n },\n },\n } satisfies BetterAuthOptions;\n}\n\nexport function createAuth(opts: CreateAuthOptions) {\n return betterAuth(buildAuthOptions(opts));\n}\n\nexport type Auth = ReturnType<typeof createAuth>;\n\ntype JSONWebKeySet = { keys: Array<Record<string, unknown>> };\n\ntype ApiKeyRow = {\n id: string;\n name: string | null;\n start: string | null;\n referenceId: string;\n metadata: unknown;\n createdAt: Date | string;\n lastRequest: Date | string | null;\n expiresAt: Date | string | null;\n};\n\ntype PluginApi = {\n getJwks(): Promise<JSONWebKeySet>;\n verifyApiKey(input: {\n body: { key: string };\n }): Promise<{ valid: boolean; key: ApiKeyRow | null }>;\n createApiKey(input: {\n body: {\n name: string;\n userId: string;\n metadata: Record<string, unknown>;\n expiresIn?: number;\n };\n }): Promise<ApiKeyRow & { key: string }>;\n};\n\nfunction toStored(key: ApiKeyRow): StoredApiKey {\n return {\n id: key.id,\n name: key.name ?? null,\n start: key.start ?? null,\n roleKey: roleFromMetadata(key.metadata),\n createdAt: new Date(key.createdAt),\n lastUsedAt: key.lastRequest ? new Date(key.lastRequest) : null,\n expiresAt: key.expiresAt ? new Date(key.expiresAt) : null,\n };\n}\n\nfunction apiKeyHeader(headers: HeaderBag): string | null {\n const header = headers[\"x-api-key\"];\n if (typeof header === \"string\" && header.length > 0) {\n return header;\n }\n const authorization = accessTokenAuthorization(headers.authorization);\n if (authorization?.scheme === \"Bearer\" && !isCompactJws(authorization.token)) {\n return authorization.token;\n }\n return null;\n}\n\nexport class BetterAuth\n implements SessionVerifier, OrgAdmin, SessionAdmin, AccessTokenVerifier, ApiKeys, Accounts\n{\n protected auth: Auth;\n protected db: CreateAuthOptions[\"db\"];\n protected logger: Logger;\n protected mcp: CreateAuthOptions[\"mcp\"];\n protected issuer: string;\n protected jwksCacheKey = {};\n\n constructor(opts: CreateAuthOptions, auth?: Auth) {\n this.auth = auth ?? createAuth(opts);\n this.db = opts.db;\n this.logger = opts.logger;\n this.mcp = opts.mcp;\n this.issuer = `${opts.baseUrl.replace(/\\/+$/, \"\")}/api/auth`;\n }\n\n protected get pluginApi(): PluginApi {\n return this.auth.api as unknown as PluginApi;\n }\n\n async handler(request: Request): Promise<Response> {\n return this.auth.handler(request);\n }\n\n async authorizationServerMetadata(request: Request): Promise<Response> {\n return oauthProviderAuthServerMetadata(\n this.auth as unknown as Parameters<typeof oauthProviderAuthServerMetadata>[0],\n )(request);\n }\n\n async verifyAccess(request: AccessRequest): Promise<AccessGrant | null> {\n const key = apiKeyHeader(request.headers);\n if (key) {\n return this.grantForKey(key);\n }\n const authorization = accessTokenAuthorization(request.headers.authorization);\n if (!authorization) {\n return null;\n }\n return this.mcp.oauthEnabled ? this.verifyAccessToken(authorization, request) : null;\n }\n\n async verifyApiKey(headers: HeaderBag): Promise<AccessGrant | null> {\n const key = apiKeyHeader(headers);\n return key ? this.grantForKey(key) : null;\n }\n\n protected async verifyAccessToken(\n authorization: AccessTokenAuthorization,\n request: AccessRequest,\n ): Promise<AccessGrant | null> {\n let claims: Awaited<ReturnType<typeof verifyJwsAccessToken>>;\n try {\n claims = await verifyJwsAccessToken(authorization.token, {\n jwksFetch: async () => this.pluginApi.getJwks(),\n jwksCacheKey: this.jwksCacheKey,\n verifyOptions: {\n audience: this.mcp.resource,\n issuer: this.issuer,\n algorithms: [JWT_ALG],\n },\n });\n } catch {\n return null;\n }\n const ctx = await this.auth.$context;\n const proof = request.headers.dpop;\n try {\n await enforceDpopBinding({\n payload: claims,\n authorization,\n proofJwt: typeof proof === \"string\" ? proof : undefined,\n method: request.method,\n url: request.url,\n replayStore: createDpopReplayStore(ctx.internalAdapter),\n });\n } catch (error) {\n if (isDpopBindingError(error)) {\n return null;\n }\n throw error;\n }\n const grant = grantFromClaims(claims);\n if (!grant) {\n return null;\n }\n const consent = await ctx.adapter.findOne<{ scopes: string[] }>({\n model: \"oauthConsent\",\n where: [\n { field: \"clientId\", value: grant.clientId },\n { field: \"userId\", value: grant.userId },\n { field: \"referenceId\", value: grant.orgId },\n ],\n });\n if (!consent || !(\"scopes\" in grant.limit)) {\n return null;\n }\n return {\n ...grant,\n limit: { scopes: grant.limit.scopes.filter((scope) => consent.scopes.includes(scope)) },\n };\n }\n\n protected async grantForKey(key: string): Promise<AccessGrant | null> {\n const result = await this.pluginApi.verifyApiKey({ body: { key } });\n if (!result.valid || !result.key) {\n return null;\n }\n const orgId = orgIdFromMetadata(result.key.metadata);\n const roleKey = roleFromMetadata(result.key.metadata);\n if (!orgId || !roleKey) {\n return null;\n }\n return {\n userId: result.key.referenceId,\n orgId,\n clientId: `api-key:${result.key.id}`,\n limit: { roleKey },\n };\n }\n\n protected async ownKeys(orgId: string, userExternalId: string): Promise<ApiKeyRow[]> {\n const ctx = await this.auth.$context;\n const keys = await ctx.adapter.findMany<ApiKeyRow>({\n model: \"apikey\",\n where: [{ field: \"referenceId\", value: userExternalId }],\n });\n return keys.filter((key) => orgIdFromMetadata(key.metadata) === orgId);\n }\n\n async list(orgId: string, userExternalId: string): Promise<StoredApiKey[]> {\n return (await this.ownKeys(orgId, userExternalId)).map(toStored);\n }\n\n async create(\n orgId: string,\n userExternalId: string,\n input: NewApiKey,\n ): Promise<StoredApiKey & { key: string }> {\n const created = await this.pluginApi.createApiKey({\n body: {\n name: input.name,\n userId: userExternalId,\n metadata: { organizationId: orgId, role: input.roleKey },\n ...(input.expiresInDays !== undefined && {\n expiresIn: input.expiresInDays * DAY_SECONDS,\n }),\n },\n });\n return { ...toStored(created), key: created.key };\n }\n\n async countWithRole(orgId: string, roleKey: string): Promise<number> {\n const metadata = sql`${authSchema.apikey.metadata}::jsonb`;\n const [row] = await this.db\n .select({ value: count() })\n .from(authSchema.apikey)\n .where(\n and(\n sql`${metadata} ->> 'organizationId' = ${orgId}`,\n sql`${metadata} ->> 'role' = ${roleKey}`,\n ),\n );\n return row?.value ?? 0;\n }\n\n async createAccount(input: PlatformAccount): Promise<{ externalId: string }> {\n const ctx = await this.auth.$context;\n const user = await ctx.internalAdapter.createUser(\n { email: input.email, name: input.name, emailVerified: true },\n { method: \"admin\" },\n );\n return { externalId: user.id };\n }\n\n async revoke(orgId: string, userExternalId: string, keyId: string): Promise<void> {\n const owned = await this.ownKeys(orgId, userExternalId);\n if (!owned.some((key) => key.id === keyId)) {\n throw new NotFoundError(\"API key\", keyId);\n }\n const ctx = await this.auth.$context;\n await ctx.adapter.delete({ model: \"apikey\", where: [{ field: \"id\", value: keyId }] });\n }\n\n async listApps(orgId: string, userExternalId: string): Promise<ConnectedApp[]> {\n if (!this.mcp.oauthEnabled) {\n return [];\n }\n const ctx = await this.auth.$context;\n const consents = await ctx.adapter.findMany<{\n clientId: string;\n scopes: string[];\n createdAt: Date | null;\n }>({\n model: \"oauthConsent\",\n where: [\n { field: \"userId\", value: userExternalId },\n { field: \"referenceId\", value: orgId },\n ],\n });\n return Promise.all(\n consents.map(async (consent) => {\n const client = await ctx.adapter.findOne<{ name: string | null; icon: string | null }>({\n model: \"oauthClient\",\n where: [{ field: \"clientId\", value: consent.clientId }],\n });\n return {\n clientId: consent.clientId,\n name: client?.name ?? consent.clientId,\n iconUrl: client?.icon ?? null,\n scopes: consent.scopes,\n connectedAt: consent.createdAt ?? new Date(0),\n };\n }),\n );\n }\n\n async revokeApp(orgId: string, userExternalId: string, clientId: string): Promise<void> {\n if (!this.mcp.oauthEnabled) {\n return;\n }\n const ctx = await this.auth.$context;\n const where = [\n { field: \"clientId\", value: clientId },\n { field: \"userId\", value: userExternalId },\n { field: \"referenceId\", value: orgId },\n ];\n await ctx.adapter.deleteMany({ model: \"oauthAccessToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthRefreshToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthConsent\", where });\n }\n\n async revokeAllFor(orgId: string, userExternalId: string): Promise<void> {\n const ctx = await this.auth.$context;\n const keys = await this.ownKeys(orgId, userExternalId);\n for (const key of keys) {\n await ctx.adapter.delete({ model: \"apikey\", where: [{ field: \"id\", value: key.id }] });\n }\n if (this.mcp.oauthEnabled) {\n const where = [\n { field: \"userId\", value: userExternalId },\n { field: \"referenceId\", value: orgId },\n ];\n await ctx.adapter.deleteMany({ model: \"oauthAccessToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthRefreshToken\", where });\n await ctx.adapter.deleteMany({ model: \"oauthConsent\", where });\n }\n this.logger.info(\"access revoked for person\", {\n orgId,\n userExternalId,\n apiKeys: keys.length,\n });\n }\n\n async verify(headers: IncomingHttpHeaders): Promise<ActiveSession | null> {\n const res = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n if (!res) {\n return null;\n }\n return {\n user: {\n id: res.user.id,\n createdAt: res.user.createdAt,\n email: res.user.email,\n firstName: res.user.name,\n externalId: res.user.id,\n lastName: res.user.name,\n updatedAt: res.user.updatedAt,\n },\n session: {\n activeOrganizationId: res.session.activeOrganizationId ?? null,\n },\n };\n }\n\n /** Stamps the org onto the caller's session row. The 5-minute cookie cache\n * still holds the pre-stamp session, so callers refresh it client-side with\n * `getSession({ disableCookieCache: true })`. */\n async stampActiveOrganization(headers: AuthHeaders, orgId: string): Promise<boolean> {\n const session = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n const token = (session?.session as { token?: string } | undefined)?.token;\n if (!token) {\n return false;\n }\n const ctx = await this.auth.$context;\n await ctx.internalAdapter.updateSession(token, { activeOrganizationId: orgId });\n return true;\n }\n\n async createOrganization(\n headers: AuthHeaders,\n input: NewOrganizationInput,\n ): Promise<AuthOrganization> {\n const org = await this.auth.api.createOrganization({\n body: { name: input.name, slug: input.slug },\n headers: fromNodeHeaders(headers),\n });\n if (!org) {\n throw new Error(`Couldn't create organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n }\n\n async createOrganizationFor(\n ownerExternalId: string,\n input: NewOrganizationInput,\n ): Promise<AuthOrganization> {\n let org: Awaited<ReturnType<Auth[\"api\"][\"createOrganization\"]>>;\n try {\n org = await this.auth.api.createOrganization({\n body: { name: input.name, slug: input.slug, userId: ownerExternalId },\n });\n } catch (err) {\n if (err instanceof APIError && err.body?.code === \"ORGANIZATION_ALREADY_EXISTS\") {\n throw new ConflictError(\"That slug is already in use\");\n }\n throw err;\n }\n if (!org) {\n throw new Error(`Couldn't create organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n }\n async setActiveOrganization(headers: AuthHeaders, externalId: string): Promise<void> {\n await this.auth.api.setActiveOrganization({\n body: { organizationId: externalId },\n headers: fromNodeHeaders(headers),\n });\n }\n protected async orgAdapter() {\n const context = await this.auth.$context;\n return getOrgAdapter(context as unknown as Parameters<typeof getOrgAdapter>[0], {});\n }\n\n async updateOrganization(externalId: string, input: UpdateOrganizationInput): Promise<void> {\n const data = {\n ...(input.name !== undefined && { name: input.name }),\n ...(input.slug !== undefined && { slug: input.slug }),\n };\n if (Object.keys(data).length === 0) {\n return;\n }\n const adapter = await this.orgAdapter();\n const updated = await adapter.updateOrganization(externalId, data);\n if (!updated) {\n throw new NotFoundError(\"Organization\", externalId);\n }\n }\n async deleteOrganization(headers: AuthHeaders, externalId: string): Promise<void> {\n try {\n await this.auth.api.deleteOrganization({\n body: { organizationId: externalId },\n headers: fromNodeHeaders(headers),\n });\n } catch (err) {\n if (err instanceof APIError) {\n throw new OrganizationRuleError(err.message || \"Could not delete organization\");\n }\n throw err;\n }\n }\n async grantMembership(orgExternalId: string, userExternalId: string): Promise<void> {\n const adapter = await this.orgAdapter();\n const existing = await adapter.findMemberByOrgId({\n userId: userExternalId,\n organizationId: orgExternalId,\n });\n if (existing) {\n return;\n }\n await adapter.createMember({\n organizationId: orgExternalId,\n userId: userExternalId,\n role: PARTICIPANT_MEMBERSHIP,\n });\n }\n\n async setMembershipOwner(\n orgExternalId: string,\n userExternalId: string,\n owner: boolean,\n ): Promise<void> {\n const adapter = await this.orgAdapter();\n const role = owner ? OWNER_MEMBERSHIP : PARTICIPANT_MEMBERSHIP;\n const existing = await adapter.findMemberByOrgId({\n userId: userExternalId,\n organizationId: orgExternalId,\n });\n if (!existing) {\n await adapter.createMember({ organizationId: orgExternalId, userId: userExternalId, role });\n return;\n }\n if (existing.role !== role) {\n await adapter.updateMember(existing.id, role);\n }\n }\n\n async removeMembership(orgExternalId: string, userExternalId: string): Promise<void> {\n const adapter = await this.orgAdapter();\n const existing = await adapter.findMemberByOrgId({\n userId: userExternalId,\n organizationId: orgExternalId,\n });\n if (!existing) {\n return;\n }\n await adapter.deleteMember({\n memberId: existing.id,\n organizationId: orgExternalId,\n userId: userExternalId,\n });\n }\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { accessTokenAuthorization, consentOrgId, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, roleFromMetadata } from "./access-grant.js";
2
- import { CreateAuthOptions, Hooks, McpAuthOptions } from "./types.js";
2
+ import { CreateAuthOptions, Hooks, McpAuthOptions, OrgHosts } from "./types.js";
3
3
  import { API_KEY_PREFIX, Auth, BetterAuth, buildAuthOptions, createAuth } from "./better-auth.js";
4
- export { API_KEY_PREFIX, Auth, BetterAuth, CreateAuthOptions, Hooks, McpAuthOptions, accessTokenAuthorization, buildAuthOptions, consentOrgId, createAuth, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, roleFromMetadata };
4
+ export { API_KEY_PREFIX, Auth, BetterAuth, CreateAuthOptions, Hooks, McpAuthOptions, OrgHosts, accessTokenAuthorization, buildAuthOptions, consentOrgId, createAuth, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, roleFromMetadata };
package/dist/types.d.ts CHANGED
@@ -34,17 +34,25 @@ interface McpAuthOptions {
34
34
  consentPage: string;
35
35
  orgPickerPage: string;
36
36
  }
37
+ interface OrgHosts {
38
+ /** Whether the origin is a host of any organization. */
39
+ has(origin: string): Promise<boolean>;
40
+ /** Whether the origin is a host of an organization the person with this email is active in. */
41
+ hasFor(email: string, origin: string): Promise<boolean>;
42
+ }
37
43
  interface CreateAuthOptions {
38
44
  db: NodePgDatabase;
39
45
  baseUrl: string;
40
46
  secret: string;
41
47
  trustedOrigins: string[];
48
+ /** Organizations' hosts, trusted beside `trustedOrigins` and asked on every request. Absent → only `trustedOrigins`. */
49
+ orgHosts?: OrgHosts;
42
50
  /** Logs failures that must not abort an auth flow (e.g. a failed invite email). */
43
51
  logger: Logger;
44
52
  /** Handlers for what the engine reports. The adapter owns no rules. */
45
53
  hooks: Hooks;
46
54
  mcp: McpAuthOptions;
47
- /** Parent domain for cross-subdomain session cookies (e.g. ".example.com"); undefined → host-only cookie. */
55
+ /** Parent domain for cross-subdomain session cookies (e.g. ".example.com"); undefined → host-only cookie, set for whichever host served the request. */
48
56
  cookieDomain?: string;
49
57
  /** Mark session cookies Secure (set behind HTTPS / in production). */
50
58
  secureCookies?: boolean;
@@ -54,5 +62,5 @@ interface CreateAuthOptions {
54
62
  ipAddressHeaders?: string[];
55
63
  }
56
64
  //#endregion
57
- export { CreateAuthOptions, Hooks, McpAuthOptions };
65
+ export { CreateAuthOptions, Hooks, McpAuthOptions, OrgHosts };
58
66
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-lms/adapter-auth",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,8 +22,8 @@
22
22
  "better-auth": "1.7.7",
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "zod": "4.6.7",
25
- "@headless-lms/adapter-db": "0.6.3",
26
- "@headless-lms/core": "0.9.0"
25
+ "@headless-lms/adapter-db": "0.7.0",
26
+ "@headless-lms/core": "0.11.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.2",