@headless-lms/adapter-auth 0.2.0 → 0.3.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.
@@ -1,19 +1,73 @@
1
+ import { accessTokenAuthorization, consentOrgId, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, permissionsToScopes, scopesToPermissions } from "./access-grant.js";
1
2
  import { ac, roles } from "./access.js";
3
+ import { withLoopbackNativeDefault } from "./client-registration.js";
4
+ import { ACCESS_TOKEN_TTL_SECONDS, ORG_CLAIM } from "@headless-lms/core/identity";
5
+ import { ACCESS_SCOPES, OrganizationRuleError, parseRole } from "@headless-lms/core/organizations";
6
+ import { createDpopReplayStore, enforceDpopBinding, isDpopBindingError, verifyJwsAccessToken } from "better-auth/oauth2";
2
7
  import { fromNodeHeaders } from "better-auth/node";
3
8
  import { APIError, betterAuth } from "better-auth";
9
+ import { createAuthMiddleware } from "better-auth/api";
4
10
  import { drizzleAdapter } from "better-auth/adapters/drizzle";
5
11
  import * as authSchema from "@headless-lms/adapter-db/schema/better-auth";
6
12
  import { prefixId } from "@headless-lms/core/shared/id";
7
- import { customSession, magicLink, organization } from "better-auth/plugins";
13
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
14
+ import { customSession, jwt, magicLink, organization } from "better-auth/plugins";
15
+ import { oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider";
16
+ import { mcp } from "@better-auth/mcp";
17
+ import { cimd } from "@better-auth/cimd";
18
+ import { fetchClientMetadataResource } from "@better-auth/cimd/node";
19
+ import { apiKey } from "@better-auth/api-key";
8
20
  import { eq } from "drizzle-orm";
9
- import { OrganizationRuleError, parseRole } from "@headless-lms/core/organizations";
10
21
  //#region src/better-auth.ts
11
- function createAuth(opts) {
22
+ const JWT_ALG = "EdDSA";
23
+ const UNMIRRORED_ORG_ENDPOINTS = /* @__PURE__ */ new Set([
24
+ "/organization/leave",
25
+ "/organization/invite-member",
26
+ "/organization/accept-invitation"
27
+ ]);
28
+ const OAUTH_CLIENT_WRITES = /* @__PURE__ */ new Set([
29
+ "/oauth2/create-client",
30
+ "/oauth2/update-client",
31
+ "/oauth2/delete-client"
32
+ ]);
33
+ function activeOrgOf(session) {
34
+ const value = session?.activeOrganizationId;
35
+ return typeof value === "string" && value.length > 0 ? value : null;
36
+ }
37
+ async function memberOrgIds(db, userId) {
38
+ return (await db.select({ organizationId: authSchema.member.organizationId }).from(authSchema.member).where(eq(authSchema.member.userId, userId))).map((row) => row.organizationId);
39
+ }
40
+ function mcpOAuthPlugins(opts) {
41
+ return [mcp({
42
+ resource: opts.mcp.resource,
43
+ loginPage: opts.mcp.loginPage,
44
+ consentPage: opts.mcp.consentPage,
45
+ scopes: [...ACCESS_SCOPES, "offline_access"],
46
+ clientRegistrationDefaultScopes: [...ACCESS_SCOPES, "offline_access"],
47
+ accessTokenExpiresIn: ACCESS_TOKEN_TTL_SECONDS,
48
+ allowDynamicClientRegistration: true,
49
+ allowUnauthenticatedClientRegistration: true,
50
+ postLogin: {
51
+ page: opts.mcp.orgPickerPage,
52
+ shouldRedirect: ({ session, user }) => needsOrgChoice(activeOrgOf(session), () => memberOrgIds(opts.db, user.id)),
53
+ consentReferenceId: async ({ session, user }) => {
54
+ const orgId = await consentOrgId(activeOrgOf(session), () => memberOrgIds(opts.db, user.id));
55
+ if (!orgId) throw new APIError("FORBIDDEN", { message: "Choose an organization you are a member of" });
56
+ return orgId;
57
+ }
58
+ },
59
+ customAccessTokenClaims: ({ referenceId }) => referenceId ? { [ORG_CLAIM]: referenceId } : {}
60
+ }), cimd({
61
+ fetchClientMetadataResource,
62
+ metadataProfile: "mcp-2026-07-28"
63
+ })];
64
+ }
65
+ function buildAuthOptions(opts) {
12
66
  const { sendResetPassword, onPasswordReset, sendMagicLink, beforeUserCreate, beforeCreateSession } = opts.hooks;
13
67
  const detached = (name, hook) => async (...args) => {
14
68
  hook(...args).catch((err) => opts.logger.error(`auth hook ${name} failed`, { err }));
15
69
  };
16
- return betterAuth({
70
+ return {
17
71
  baseURL: opts.baseUrl,
18
72
  secret: opts.secret,
19
73
  trustedOrigins: opts.trustedOrigins,
@@ -54,6 +108,15 @@ function createAuth(opts) {
54
108
  creatorRole: "owner",
55
109
  organizationHooks: opts.hooks
56
110
  }),
111
+ jwt({
112
+ disableSettingJwtHeader: true,
113
+ jwks: { keyPairConfig: { alg: JWT_ALG } }
114
+ }),
115
+ ...opts.mcp.oauthEnabled ? mcpOAuthPlugins(opts) : [],
116
+ apiKey({
117
+ enableMetadata: true,
118
+ rateLimit: { enabled: false }
119
+ }),
57
120
  customSession(async ({ user, session }) => {
58
121
  const memberships = await opts.db.select({
59
122
  role: authSchema.member.role,
@@ -80,22 +143,220 @@ function createAuth(opts) {
80
143
  };
81
144
  })
82
145
  ],
146
+ hooks: { before: createAuthMiddleware(async (ctx) => {
147
+ if (ctx.path === "/api-key/create" && (ctx.request || ctx.headers)) throw new APIError("FORBIDDEN", { message: "API keys are created through the LMS" });
148
+ if (UNMIRRORED_ORG_ENDPOINTS.has(ctx.path)) throw new APIError("FORBIDDEN", { message: "Membership changes go through the LMS" });
149
+ if (OAUTH_CLIENT_WRITES.has(ctx.path) && (ctx.request || ctx.headers)) throw new APIError("FORBIDDEN", { message: "Apps register themselves when they connect" });
150
+ if (ctx.path === "/api-key/update" && ctx.body?.metadata !== void 0) throw new APIError("FORBIDDEN", { message: "An API key stays in the organization it was made for" });
151
+ if (ctx.path !== "/oauth2/register") return;
152
+ const body = withLoopbackNativeDefault(ctx.body);
153
+ return body ? { context: { body } } : void 0;
154
+ }) },
83
155
  databaseHooks: {
84
156
  user: { create: { before: beforeUserCreate } },
85
157
  session: { create: { before: beforeCreateSession } }
86
158
  }
87
- });
159
+ };
160
+ }
161
+ function createAuth(opts) {
162
+ return betterAuth(buildAuthOptions(opts));
88
163
  }
89
164
  var BetterAuth = class {
90
165
  auth;
91
166
  logger;
92
- constructor(opts) {
93
- this.auth = createAuth(opts);
167
+ mcp;
168
+ issuer;
169
+ jwksCacheKey = {};
170
+ constructor(opts, auth) {
171
+ this.auth = auth ?? createAuth(opts);
94
172
  this.logger = opts.logger;
173
+ this.mcp = opts.mcp;
174
+ this.issuer = `${opts.baseUrl.replace(/\/+$/, "")}/api/auth`;
175
+ }
176
+ get pluginApi() {
177
+ return this.auth.api;
95
178
  }
96
179
  async handler(request) {
97
180
  return this.auth.handler(request);
98
181
  }
182
+ async authorizationServerMetadata(request) {
183
+ return oauthProviderAuthServerMetadata(this.auth)(request);
184
+ }
185
+ async verifyAccess(request) {
186
+ const apiKeyHeader = request.headers["x-api-key"];
187
+ if (typeof apiKeyHeader === "string" && apiKeyHeader.length > 0) return this.verifyApiKey(apiKeyHeader);
188
+ const authorization = accessTokenAuthorization(request.headers.authorization);
189
+ if (!authorization) return null;
190
+ if (authorization.scheme === "Bearer" && !isCompactJws(authorization.token)) return this.verifyApiKey(authorization.token);
191
+ return this.mcp.oauthEnabled ? this.verifyAccessToken(authorization, request) : null;
192
+ }
193
+ async verifyAccessToken(authorization, request) {
194
+ let claims;
195
+ try {
196
+ claims = await verifyJwsAccessToken(authorization.token, {
197
+ jwksFetch: async () => this.pluginApi.getJwks(),
198
+ jwksCacheKey: this.jwksCacheKey,
199
+ verifyOptions: {
200
+ audience: this.mcp.resource,
201
+ issuer: this.issuer,
202
+ algorithms: [JWT_ALG]
203
+ }
204
+ });
205
+ } catch {
206
+ return null;
207
+ }
208
+ const ctx = await this.auth.$context;
209
+ const proof = request.headers.dpop;
210
+ try {
211
+ await enforceDpopBinding({
212
+ payload: claims,
213
+ authorization,
214
+ proofJwt: typeof proof === "string" ? proof : void 0,
215
+ method: request.method,
216
+ url: request.url,
217
+ replayStore: createDpopReplayStore(ctx.internalAdapter)
218
+ });
219
+ } catch (error) {
220
+ if (isDpopBindingError(error)) return null;
221
+ throw error;
222
+ }
223
+ const grant = grantFromClaims(claims);
224
+ if (!grant) return null;
225
+ const consent = await ctx.adapter.findOne({
226
+ model: "oauthConsent",
227
+ where: [
228
+ {
229
+ field: "clientId",
230
+ value: grant.clientId
231
+ },
232
+ {
233
+ field: "userId",
234
+ value: grant.userId
235
+ },
236
+ {
237
+ field: "referenceId",
238
+ value: grant.orgId
239
+ }
240
+ ]
241
+ });
242
+ return consent ? {
243
+ ...grant,
244
+ scopes: grant.scopes.filter((scope) => consent.scopes.includes(scope))
245
+ } : null;
246
+ }
247
+ async verifyApiKey(key) {
248
+ const result = await this.pluginApi.verifyApiKey({ body: { key } });
249
+ if (!result.valid || !result.key) return null;
250
+ const orgId = orgIdFromMetadata(result.key.metadata);
251
+ if (!orgId) return null;
252
+ return {
253
+ userId: result.key.referenceId,
254
+ orgId,
255
+ scopes: permissionsToScopes(result.key.permissions),
256
+ clientId: `api-key:${result.key.id}`
257
+ };
258
+ }
259
+ async sessionUserId(headers) {
260
+ const session = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });
261
+ if (!session) throw new OrganizationRuleError("No session");
262
+ return session.user.id;
263
+ }
264
+ async listConnectedApps(headers, orgId) {
265
+ const userId = await this.sessionUserId(headers);
266
+ if (!this.mcp.oauthEnabled) return [];
267
+ const ctx = await this.auth.$context;
268
+ const consents = await ctx.adapter.findMany({
269
+ model: "oauthConsent",
270
+ where: [{
271
+ field: "userId",
272
+ value: userId
273
+ }, {
274
+ field: "referenceId",
275
+ value: orgId
276
+ }]
277
+ });
278
+ return Promise.all(consents.map(async (consent) => {
279
+ const client = await ctx.adapter.findOne({
280
+ model: "oauthClient",
281
+ where: [{
282
+ field: "clientId",
283
+ value: consent.clientId
284
+ }]
285
+ });
286
+ return {
287
+ clientId: consent.clientId,
288
+ name: client?.name ?? consent.clientId,
289
+ iconUrl: client?.icon ?? null,
290
+ scopes: consent.scopes,
291
+ connectedAt: consent.createdAt ?? /* @__PURE__ */ new Date(0)
292
+ };
293
+ }));
294
+ }
295
+ async revokeConnectedApp(headers, orgId, clientId) {
296
+ const userId = await this.sessionUserId(headers);
297
+ if (!this.mcp.oauthEnabled) return;
298
+ const ctx = await this.auth.$context;
299
+ const where = [
300
+ {
301
+ field: "clientId",
302
+ value: clientId
303
+ },
304
+ {
305
+ field: "userId",
306
+ value: userId
307
+ },
308
+ {
309
+ field: "referenceId",
310
+ value: orgId
311
+ }
312
+ ];
313
+ await ctx.adapter.deleteMany({
314
+ model: "oauthAccessToken",
315
+ where
316
+ });
317
+ await ctx.adapter.deleteMany({
318
+ model: "oauthRefreshToken",
319
+ where
320
+ });
321
+ await ctx.adapter.deleteMany({
322
+ model: "oauthConsent",
323
+ where
324
+ });
325
+ }
326
+ async listApiKeys(headers, orgId) {
327
+ return (await this.pluginApi.listApiKeys({ headers: fromNodeHeaders(headers) })).apiKeys.filter((key) => orgIdFromMetadata(key.metadata) === orgId).map((key) => ({
328
+ id: key.id,
329
+ name: key.name ?? null,
330
+ start: key.start ?? null,
331
+ scopes: permissionsToScopes(key.permissions),
332
+ createdAt: new Date(key.createdAt),
333
+ lastUsedAt: key.lastRequest ? new Date(key.lastRequest) : null
334
+ }));
335
+ }
336
+ async createApiKey(input) {
337
+ const created = await this.pluginApi.createApiKey({ body: {
338
+ name: input.name,
339
+ userId: input.authUserId,
340
+ metadata: { organizationId: input.orgId },
341
+ permissions: scopesToPermissions(input.scopes)
342
+ } });
343
+ return {
344
+ id: created.id,
345
+ name: created.name ?? null,
346
+ start: created.start ?? null,
347
+ scopes: permissionsToScopes(created.permissions),
348
+ createdAt: new Date(created.createdAt),
349
+ lastUsedAt: null,
350
+ key: created.key
351
+ };
352
+ }
353
+ async deleteApiKey(headers, orgId, keyId) {
354
+ if (!(await this.listApiKeys(headers, orgId)).some((key) => key.id === keyId)) throw new NotFoundError("API key", keyId);
355
+ await this.pluginApi.deleteApiKey({
356
+ body: { keyId },
357
+ headers: fromNodeHeaders(headers)
358
+ });
359
+ }
99
360
  async verify(headers) {
100
361
  const res = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });
101
362
  if (!res) return null;
@@ -227,6 +488,6 @@ var BetterAuth = class {
227
488
  }
228
489
  };
229
490
  //#endregion
230
- export { BetterAuth, createAuth };
491
+ export { BetterAuth, buildAuthOptions, createAuth };
231
492
 
232
493
  //# sourceMappingURL=better-auth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"better-auth.js","names":[],"sources":["../src/better-auth.ts"],"sourcesContent":["import type {\n ActiveSession,\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 } from \"better-auth\";\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 { customSession, magicLink, organization } from \"better-auth/plugins\";\nimport { eq } from \"drizzle-orm\";\nimport { ac, roles } from \"./access.js\";\nimport {\n type AuthHeaders,\n type AuthOrganization,\n type MemberWriteContext,\n type OrgAdmin,\n OrganizationRuleError,\n parseRole,\n type Role,\n type UpdateOrganizationInput,\n} from \"@headless-lms/core/organizations\";\nimport type { SessionAdmin } from \"@headless-lms/core/identity\";\n\nexport function createAuth(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 auth = betterAuth({\n baseURL: opts.baseUrl,\n secret: opts.secret,\n trustedOrigins: opts.trustedOrigins,\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 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\",\n organizationHooks: opts.hooks,\n }),\n // Enrich get-session with the caller's memberships (role + org) so BFFs\n // resolve auth in one call instead of fanning out to get-active-member /\n // get-full-organization / organization/list. Custom fields bypass the\n // cookie cache, so this costs one indexed query per get-session.\n customSession(async ({ user, session }) => {\n const memberships = await opts.db\n .select({\n role: authSchema.member.role,\n id: authSchema.organization.id,\n name: authSchema.organization.name,\n slug: authSchema.organization.slug,\n })\n .from(authSchema.member)\n .innerJoin(\n authSchema.organization,\n eq(authSchema.member.organizationId, authSchema.organization.id),\n )\n .where(eq(authSchema.member.userId, user.id));\n const activeOrgId =\n (session as { activeOrganizationId?: string | null }).activeOrganizationId ?? null;\n const active = memberships.find((m) => m.id === activeOrgId) ?? null;\n return {\n user,\n session,\n activeMemberRole: active?.role ?? null,\n activeOrganization: active\n ? { id: active.id, name: active.name, slug: active.slug }\n : null,\n organizations: memberships.map(({ id, name, slug }) => ({ id, name, slug })),\n };\n }),\n ],\n databaseHooks: {\n user: {\n create: {\n before: beforeUserCreate,\n },\n },\n session: {\n create: {\n before: beforeCreateSession,\n },\n },\n },\n });\n return auth;\n}\n\nexport type Auth = ReturnType<typeof createAuth>;\n\nexport class BetterAuth implements SessionVerifier, OrgAdmin, SessionAdmin {\n protected auth: Auth;\n protected logger: Logger;\n\n constructor(opts: CreateAuthOptions) {\n this.auth = createAuth(opts);\n this.logger = opts.logger;\n }\n\n async handler(request: Request): Promise<Response> {\n return this.auth.handler(request);\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 // customSession erases the org plugin's session extension from the\n // inferred type; the field is still present at runtime.\n activeOrganizationId:\n (res.session as { activeOrganizationId?: string | null }).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 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 async updateOrganization(\n headers: AuthHeaders,\n externalId: string,\n input: UpdateOrganizationInput,\n ): Promise<AuthOrganization> {\n try {\n const org = await this.auth.api.updateOrganization({\n body: { organizationId: externalId, data: { name: input.name, slug: input.slug } },\n headers: fromNodeHeaders(headers),\n });\n if (!org) {\n throw new Error(`Couldn't update organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n } catch (err) {\n // Surface Better Auth validation/permission failures (e.g. slug taken) as\n // a domain rule error the route maps to 409 rather than a raw 500.\n if (err instanceof APIError) {\n throw new OrganizationRuleError(err.message || \"Could not update organization\");\n }\n throw err;\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(\n orgExternalId: string,\n userExternalId: string,\n role: string,\n ): Promise<void> {\n // Server-side (no session): the accepted invitation is the authorisation.\n try {\n await this.auth.api.addMember({\n body: { userId: userExternalId, organizationId: orgExternalId, role: parseRole(role) },\n });\n } catch (err) {\n if (\n err instanceof APIError &&\n err.body?.code === \"USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION\"\n ) {\n return;\n }\n throw err;\n }\n }\n async updateRole(ctx: MemberWriteContext, userExternalId: string, role: Role): Promise<void> {\n await this.auth.api.updateMemberRole({\n body: {\n memberId: await this.resolveMemberId(ctx, userExternalId),\n role,\n organizationId: ctx.authOrgId,\n },\n headers: this.headersOf(ctx),\n });\n }\n async removeMember(ctx: MemberWriteContext, userExternalId: string): Promise<void> {\n await this.auth.api.removeMember({\n body: {\n memberIdOrEmail: await this.resolveMemberId(ctx, userExternalId),\n organizationId: ctx.authOrgId,\n },\n headers: this.headersOf(ctx),\n });\n }\n\n protected headersOf(ctx: MemberWriteContext): Headers {\n return fromNodeHeaders(ctx.headers);\n }\n\n // Better Auth's member endpoints key on its own member id, which the domain\n // deliberately does not store — `(org, user)` already identifies the record,\n // so it is looked up here instead of mirrored into org_users.\n protected async resolveMemberId(\n ctx: MemberWriteContext,\n userExternalId: string,\n ): Promise<string> {\n const res = await this.auth.api.listMembers({\n query: {\n organizationId: ctx.authOrgId,\n filterField: \"userId\",\n filterValue: userExternalId,\n limit: 1,\n },\n headers: this.headersOf(ctx),\n });\n const memberId = res?.members?.[0]?.id;\n if (!memberId) {\n throw new OrganizationRuleError(\"That person is no longer a member of this organization\");\n }\n return memberId;\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 ApiKeySummary,\n AppAccessAdmin,\n ConnectedApp,\n CreatedApiKey,\n HeaderBag,\n Logger,\n NewApiKey,\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 { NotFoundError } from \"@headless-lms/core/shared/errors\";\nimport { customSession, jwt, magicLink, organization } from \"better-auth/plugins\";\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 { eq } from \"drizzle-orm\";\nimport { ac, roles } from \"./access.js\";\nimport {\n accessTokenAuthorization,\n consentOrgId,\n grantFromClaims,\n isCompactJws,\n needsOrgChoice,\n orgIdFromMetadata,\n permissionsToScopes,\n scopesToPermissions,\n} from \"./access-grant.js\";\nimport { withLoopbackNativeDefault } from \"./client-registration.js\";\nimport {\n ACCESS_SCOPES,\n type AuthHeaders,\n type AuthOrganization,\n type MemberWriteContext,\n type OrgAdmin,\n OrganizationRuleError,\n parseRole,\n type Role,\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\nconst UNMIRRORED_ORG_ENDPOINTS = new Set([\n \"/organization/leave\",\n \"/organization/invite-member\",\n \"/organization/accept-invitation\",\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: [...ACCESS_SCOPES, \"offline_access\"],\n clientRegistrationDefaultScopes: [...ACCESS_SCOPES, \"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 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 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\",\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({ enableMetadata: true, rateLimit: { enabled: false } }) as BetterAuthPlugin,\n // Enrich get-session with the caller's memberships (role + org) so BFFs\n // resolve auth in one call instead of fanning out to get-active-member /\n // get-full-organization / organization/list. Custom fields bypass the\n // cookie cache, so this costs one indexed query per get-session.\n customSession(async ({ user, session }) => {\n const memberships = await opts.db\n .select({\n role: authSchema.member.role,\n id: authSchema.organization.id,\n name: authSchema.organization.name,\n slug: authSchema.organization.slug,\n })\n .from(authSchema.member)\n .innerJoin(\n authSchema.organization,\n eq(authSchema.member.organizationId, authSchema.organization.id),\n )\n .where(eq(authSchema.member.userId, user.id));\n const activeOrgId =\n (session as { activeOrganizationId?: string | null }).activeOrganizationId ?? null;\n const active = memberships.find((m) => m.id === activeOrgId) ?? null;\n return {\n user,\n session,\n activeMemberRole: active?.role ?? null,\n activeOrganization: active\n ? { id: active.id, name: active.name, slug: active.slug }\n : null,\n organizations: memberships.map(({ id, name, slug }) => ({ id, name, slug })),\n };\n }),\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 (UNMIRRORED_ORG_ENDPOINTS.has(ctx.path)) {\n throw new APIError(\"FORBIDDEN\", { message: \"Membership changes go 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 PluginApiKey = {\n id: string;\n name: string | null;\n start: string | null;\n referenceId: string;\n metadata: unknown;\n permissions: Record<string, string[]> | null;\n createdAt: Date;\n lastRequest: Date | null;\n};\n\ntype PluginApi = {\n getJwks(): Promise<JSONWebKeySet>;\n verifyApiKey(input: {\n body: { key: string };\n }): Promise<{ valid: boolean; key: PluginApiKey | null }>;\n createApiKey(input: {\n body: {\n name: string;\n userId: string;\n metadata: Record<string, unknown>;\n permissions: Record<string, string[]>;\n };\n }): Promise<PluginApiKey & { key: string }>;\n listApiKeys(input: { headers: Headers }): Promise<{ apiKeys: PluginApiKey[] }>;\n deleteApiKey(input: { body: { keyId: string }; headers: Headers }): Promise<unknown>;\n};\n\nexport class BetterAuth\n implements SessionVerifier, OrgAdmin, SessionAdmin, AccessTokenVerifier, AppAccessAdmin\n{\n protected auth: Auth;\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.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 apiKeyHeader = request.headers[\"x-api-key\"];\n if (typeof apiKeyHeader === \"string\" && apiKeyHeader.length > 0) {\n return this.verifyApiKey(apiKeyHeader);\n }\n const authorization = accessTokenAuthorization(request.headers.authorization);\n if (!authorization) {\n return null;\n }\n if (authorization.scheme === \"Bearer\" && !isCompactJws(authorization.token)) {\n return this.verifyApiKey(authorization.token);\n }\n return this.mcp.oauthEnabled ? this.verifyAccessToken(authorization, request) : 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 return consent\n ? { ...grant, scopes: grant.scopes.filter((scope) => consent.scopes.includes(scope)) }\n : null;\n }\n\n protected async verifyApiKey(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 if (!orgId) {\n return null;\n }\n return {\n userId: result.key.referenceId,\n orgId,\n scopes: permissionsToScopes(result.key.permissions),\n clientId: `api-key:${result.key.id}`,\n };\n }\n\n protected async sessionUserId(headers: HeaderBag): Promise<string> {\n const session = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n if (!session) {\n throw new OrganizationRuleError(\"No session\");\n }\n return session.user.id;\n }\n\n async listConnectedApps(headers: HeaderBag, orgId: string): Promise<ConnectedApp[]> {\n const userId = await this.sessionUserId(headers);\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: userId },\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 revokeConnectedApp(headers: HeaderBag, orgId: string, clientId: string): Promise<void> {\n const userId = await this.sessionUserId(headers);\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: userId },\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 listApiKeys(headers: HeaderBag, orgId: string): Promise<ApiKeySummary[]> {\n const res = await this.pluginApi.listApiKeys({ headers: fromNodeHeaders(headers) });\n return res.apiKeys\n .filter((key) => orgIdFromMetadata(key.metadata) === orgId)\n .map((key) => ({\n id: key.id,\n name: key.name ?? null,\n start: key.start ?? null,\n scopes: permissionsToScopes(key.permissions),\n createdAt: new Date(key.createdAt),\n lastUsedAt: key.lastRequest ? new Date(key.lastRequest) : null,\n }));\n }\n\n async createApiKey(input: NewApiKey): Promise<CreatedApiKey> {\n const created = await this.pluginApi.createApiKey({\n body: {\n name: input.name,\n userId: input.authUserId,\n metadata: { organizationId: input.orgId },\n permissions: scopesToPermissions(input.scopes),\n },\n });\n return {\n id: created.id,\n name: created.name ?? null,\n start: created.start ?? null,\n scopes: permissionsToScopes(created.permissions),\n createdAt: new Date(created.createdAt),\n lastUsedAt: null,\n key: created.key,\n };\n }\n\n async deleteApiKey(headers: HeaderBag, orgId: string, keyId: string): Promise<void> {\n const owned = await this.listApiKeys(headers, orgId);\n if (!owned.some((key) => key.id === keyId)) {\n throw new NotFoundError(\"API key\", keyId);\n }\n await this.pluginApi.deleteApiKey({ body: { keyId }, headers: fromNodeHeaders(headers) });\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 // customSession erases the org plugin's session extension from the\n // inferred type; the field is still present at runtime.\n activeOrganizationId:\n (res.session as { activeOrganizationId?: string | null }).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 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 async updateOrganization(\n headers: AuthHeaders,\n externalId: string,\n input: UpdateOrganizationInput,\n ): Promise<AuthOrganization> {\n try {\n const org = await this.auth.api.updateOrganization({\n body: { organizationId: externalId, data: { name: input.name, slug: input.slug } },\n headers: fromNodeHeaders(headers),\n });\n if (!org) {\n throw new Error(`Couldn't update organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n } catch (err) {\n // Surface Better Auth validation/permission failures (e.g. slug taken) as\n // a domain rule error the route maps to 409 rather than a raw 500.\n if (err instanceof APIError) {\n throw new OrganizationRuleError(err.message || \"Could not update organization\");\n }\n throw err;\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(\n orgExternalId: string,\n userExternalId: string,\n role: string,\n ): Promise<void> {\n // Server-side (no session): the accepted invitation is the authorisation.\n try {\n await this.auth.api.addMember({\n body: { userId: userExternalId, organizationId: orgExternalId, role: parseRole(role) },\n });\n } catch (err) {\n if (\n err instanceof APIError &&\n err.body?.code === \"USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION\"\n ) {\n return;\n }\n throw err;\n }\n }\n async updateRole(ctx: MemberWriteContext, userExternalId: string, role: Role): Promise<void> {\n await this.auth.api.updateMemberRole({\n body: {\n memberId: await this.resolveMemberId(ctx, userExternalId),\n role,\n organizationId: ctx.authOrgId,\n },\n headers: this.headersOf(ctx),\n });\n }\n async removeMember(ctx: MemberWriteContext, userExternalId: string): Promise<void> {\n await this.auth.api.removeMember({\n body: {\n memberIdOrEmail: await this.resolveMemberId(ctx, userExternalId),\n organizationId: ctx.authOrgId,\n },\n headers: this.headersOf(ctx),\n });\n }\n\n protected headersOf(ctx: MemberWriteContext): Headers {\n return fromNodeHeaders(ctx.headers);\n }\n\n // Better Auth's member endpoints key on its own member id, which the domain\n // deliberately does not store — `(org, user)` already identifies the record,\n // so it is looked up here instead of mirrored into org_users.\n protected async resolveMemberId(\n ctx: MemberWriteContext,\n userExternalId: string,\n ): Promise<string> {\n const res = await this.auth.api.listMembers({\n query: {\n organizationId: ctx.authOrgId,\n filterField: \"userId\",\n filterValue: userExternalId,\n limit: 1,\n },\n headers: this.headersOf(ctx),\n });\n const memberId = res?.members?.[0]?.id;\n if (!memberId) {\n throw new OrganizationRuleError(\"That person is no longer a member of this organization\");\n }\n return memberId;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ //#region src/client-registration.d.ts
2
+ declare function withLoopbackNativeDefault(body: unknown): Record<string, unknown> | null;
3
+ //#endregion
4
+ export { withLoopbackNativeDefault };
5
+ //# sourceMappingURL=client-registration.d.ts.map
@@ -0,0 +1,15 @@
1
+ //#region src/client-registration.ts
2
+ function withLoopbackNativeDefault(body) {
3
+ if (body === null || typeof body !== "object") return null;
4
+ const registration = body;
5
+ const redirectUris = registration.redirect_uris;
6
+ if (registration.application_type !== void 0 || !Array.isArray(redirectUris)) return null;
7
+ return redirectUris.every((uri) => typeof uri === "string" && uri.toLowerCase().startsWith("https:")) ? null : {
8
+ ...registration,
9
+ application_type: "native"
10
+ };
11
+ }
12
+ //#endregion
13
+ export { withLoopbackNativeDefault };
14
+
15
+ //# sourceMappingURL=client-registration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-registration.js","names":[],"sources":["../src/client-registration.ts"],"sourcesContent":["export function withLoopbackNativeDefault(body: unknown): Record<string, unknown> | null {\n if (body === null || typeof body !== \"object\") {\n return null;\n }\n const registration = body as Record<string, unknown>;\n const redirectUris = registration.redirect_uris;\n if (registration.application_type !== undefined || !Array.isArray(redirectUris)) {\n return null;\n }\n const httpsOnly = redirectUris.every(\n (uri) => typeof uri === \"string\" && uri.toLowerCase().startsWith(\"https:\"),\n );\n return httpsOnly ? null : { ...registration, application_type: \"native\" };\n}\n"],"mappings":";AAAA,SAAgB,0BAA0B,MAA+C;CACvF,IAAI,SAAS,QAAQ,OAAO,SAAS,UACnC,OAAO;CAET,MAAM,eAAe;CACrB,MAAM,eAAe,aAAa;CAClC,IAAI,aAAa,qBAAqB,KAAA,KAAa,CAAC,MAAM,QAAQ,YAAY,GAC5E,OAAO;CAKT,OAHkB,aAAa,OAC5B,QAAQ,OAAO,QAAQ,YAAY,IAAI,YAAY,CAAC,CAAC,WAAW,QAAQ,CAE5D,IAAI,OAAO;EAAE,GAAG;EAAc,kBAAkB;CAAS;AAC1E"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- import { CreateAuthOptions, Hooks } from "./types.js";
2
- import { Auth, BetterAuth, createAuth } from "./better-auth.js";
3
- export { Auth, BetterAuth, CreateAuthOptions, Hooks, createAuth };
1
+ import { accessTokenAuthorization, consentOrgId, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, permissionsToScopes, scopesToPermissions } from "./access-grant.js";
2
+ import { CreateAuthOptions, Hooks, McpAuthOptions } from "./types.js";
3
+ import { Auth, BetterAuth, buildAuthOptions, createAuth } from "./better-auth.js";
4
+ export { Auth, BetterAuth, CreateAuthOptions, Hooks, McpAuthOptions, accessTokenAuthorization, buildAuthOptions, consentOrgId, createAuth, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, permissionsToScopes, scopesToPermissions };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
- import { BetterAuth, createAuth } from "./better-auth.js";
1
+ import { accessTokenAuthorization, consentOrgId, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, permissionsToScopes, scopesToPermissions } from "./access-grant.js";
2
+ import { BetterAuth, buildAuthOptions, createAuth } from "./better-auth.js";
2
3
  import "./types.js";
3
- export { BetterAuth, createAuth };
4
+ export { BetterAuth, accessTokenAuthorization, buildAuthOptions, consentOrgId, createAuth, grantFromClaims, isCompactJws, needsOrgChoice, orgIdFromMetadata, permissionsToScopes, scopesToPermissions };
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { GenericEndpointContext, Session, User } from "better-auth";
2
2
  import { OrganizationOptions } from "better-auth/plugins";
3
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
3
  import { Logger } from "@headless-lms/core/types";
4
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
5
5
  //#region src/types.d.ts
6
6
  type Hooks = NonNullable<OrganizationOptions["organizationHooks"]> & {
7
7
  beforeUserCreate: (user: User & Record<string, unknown>, context: GenericEndpointContext | null) => Promise<boolean | void | {
@@ -27,6 +27,13 @@ type Hooks = NonNullable<OrganizationOptions["organizationHooks"]> & {
27
27
  metadata?: Record<string, unknown>;
28
28
  }, ctx?: GenericEndpointContext | undefined) => Promise<void>;
29
29
  };
30
+ interface McpAuthOptions {
31
+ oauthEnabled: boolean;
32
+ resource: string;
33
+ loginPage: string;
34
+ consentPage: string;
35
+ orgPickerPage: string;
36
+ }
30
37
  interface CreateAuthOptions {
31
38
  db: NodePgDatabase;
32
39
  baseUrl: string;
@@ -36,11 +43,12 @@ interface CreateAuthOptions {
36
43
  logger: Logger;
37
44
  /** Handlers for what the engine reports. The adapter owns no rules. */
38
45
  hooks: Hooks;
46
+ mcp: McpAuthOptions;
39
47
  /** Parent domain for cross-subdomain session cookies (e.g. ".example.com"); undefined → host-only cookie. */
40
48
  cookieDomain?: string;
41
49
  /** Mark session cookies Secure (set behind HTTPS / in production). */
42
50
  secureCookies?: boolean;
43
51
  }
44
52
  //#endregion
45
- export { CreateAuthOptions, Hooks };
53
+ export { CreateAuthOptions, Hooks, McpAuthOptions };
46
54
  //# 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.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,10 +14,16 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "better-auth": "^1.6.25",
17
+ "@better-auth/api-key": "1.7.7",
18
+ "@better-auth/cimd": "1.7.7",
19
+ "@better-auth/mcp": "1.7.7",
20
+ "@better-auth/oauth-provider": "1.7.7",
21
+ "@better-auth/utils": "0.4.2",
22
+ "better-auth": "1.7.7",
18
23
  "drizzle-orm": "^0.45.2",
19
- "@headless-lms/adapter-db": "0.2.0",
20
- "@headless-lms/core": "0.2.0"
24
+ "zod": "4.6.7",
25
+ "@headless-lms/adapter-db": "0.3.0",
26
+ "@headless-lms/core": "0.3.0"
21
27
  },
22
28
  "devDependencies": {
23
29
  "@types/node": "^22.10.2",
@@ -1,8 +0,0 @@
1
- //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.d.cts
2
- type $strip = {
3
- out: {};
4
- in: {};
5
- };
6
- //#endregion
7
- export { $strip };
8
- //# sourceMappingURL=schemas.d.ts.map