@storyshelf/auth 0.5.3

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/auth-tables-pg.d.mts +1971 -0
  4. package/dist/auth-tables-pg.mjs +167 -0
  5. package/dist/auth-tables-pg.mjs.map +1 -0
  6. package/dist/auth-tables.d.mts +1092 -0
  7. package/dist/auth-tables.mjs +122 -0
  8. package/dist/auth-tables.mjs.map +1 -0
  9. package/dist/bootstrap.d.mts +15 -0
  10. package/dist/bootstrap.mjs +40 -0
  11. package/dist/bootstrap.mjs.map +1 -0
  12. package/dist/config.d.mts +27 -0
  13. package/dist/config.mjs +204 -0
  14. package/dist/config.mjs.map +1 -0
  15. package/dist/db-bridge.d.mts +20 -0
  16. package/dist/db-bridge.mjs +208 -0
  17. package/dist/db-bridge.mjs.map +1 -0
  18. package/dist/db-values.mjs +41 -0
  19. package/dist/db-values.mjs.map +1 -0
  20. package/dist/db-where.mjs +85 -0
  21. package/dist/db-where.mjs.map +1 -0
  22. package/dist/engine.d.mts +208 -0
  23. package/dist/engine.mjs +444 -0
  24. package/dist/engine.mjs.map +1 -0
  25. package/dist/groups.d.mts +54 -0
  26. package/dist/groups.mjs +129 -0
  27. package/dist/groups.mjs.map +1 -0
  28. package/dist/index.d.mts +18 -0
  29. package/dist/index.mjs +17 -0
  30. package/dist/invites.d.mts +41 -0
  31. package/dist/invites.mjs +225 -0
  32. package/dist/invites.mjs.map +1 -0
  33. package/dist/passkeys.d.mts +21 -0
  34. package/dist/passkeys.mjs +27 -0
  35. package/dist/passkeys.mjs.map +1 -0
  36. package/dist/presets/enterprise.d.mts +24 -0
  37. package/dist/presets/enterprise.mjs +44 -0
  38. package/dist/presets/enterprise.mjs.map +1 -0
  39. package/dist/presets/index.d.mts +5 -0
  40. package/dist/presets/social.d.mts +34 -0
  41. package/dist/presets/social.mjs +60 -0
  42. package/dist/presets/social.mjs.map +1 -0
  43. package/dist/presets/sso.d.mts +72 -0
  44. package/dist/presets/sso.mjs +136 -0
  45. package/dist/presets/sso.mjs.map +1 -0
  46. package/dist/sessions.d.mts +23 -0
  47. package/dist/sessions.mjs +51 -0
  48. package/dist/sessions.mjs.map +1 -0
  49. package/package.json +82 -0
@@ -0,0 +1,444 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { baseAuthDateFields, baseAuthTables } from "./auth-tables.mjs";
5
+ import { acceptInvite, issueInvite, verifyInvite } from "./invites.mjs";
6
+ import { createShelfDbBridge } from "./db-bridge.mjs";
7
+ import { DEFAULT_GROUP_CLAIM, provisionHook } from "./groups.mjs";
8
+ import { hasPasswordCredential, listUserPasskeys } from "./passkeys.mjs";
9
+ import { deleteSessionByToken, deleteUserSessions, listUserSessions } from "./sessions.mjs";
10
+ import { ulid } from "@storyshelf/core/utils";
11
+ import { passkey } from "@better-auth/passkey";
12
+ import { sso } from "@better-auth/sso";
13
+ import { UserModel } from "@storyshelf/core/models";
14
+ import { SESSION_COOKIE } from "@storyshelf/core/types";
15
+ import { betterAuth } from "better-auth";
16
+ import { genericOAuth } from "better-auth/plugins";
17
+ //#region src/engine.ts
18
+ /**
19
+ * Shelf auth engine: Better Auth protocol plumbing behind the Auth singleton.
20
+ *
21
+ * Sessions are wholesale Better Auth DB rows under our cookie name;
22
+ * identities mirror into our `users` table (source of truth for roles,
23
+ * memberships, and profile). All logins flow through engine endpoints
24
+ * (mounted at `/api/auth/*`) — never through `createSession`, which exists
25
+ * only for interface conformance and fails loudly.
26
+ */
27
+ /** Provider id minted on every engine identity (method attribution is future work). */
28
+ const ENGINE_PROVIDER_ID = "better-auth";
29
+ function toAuthUser(sessionUser, role, displayName) {
30
+ return {
31
+ id: sessionUser.id,
32
+ email: sessionUser.email,
33
+ name: displayName === "" || displayName === null ? sessionUser.name : displayName,
34
+ avatarUrl: sessionUser.image ?? void 0,
35
+ role,
36
+ providerId: ENGINE_PROVIDER_ID
37
+ };
38
+ }
39
+ async function mirrorUser(db, created, hooks) {
40
+ const user = created;
41
+ const existing = await new UserModel(db).get(user.id);
42
+ await new UserModel(db).upsert({
43
+ id: user.id,
44
+ email: user.email,
45
+ name: user.name,
46
+ avatarUrl: user.image ?? null,
47
+ role: existing?.role ?? "member"
48
+ });
49
+ if (existing) return;
50
+ try {
51
+ await hooks.onAuthSystemEvent?.("sys:user-created", {
52
+ userId: user.id,
53
+ email: user.email,
54
+ name: user.name
55
+ });
56
+ } catch (error) {
57
+ hooks.logger?.warn({
58
+ err: error,
59
+ userId: user.id
60
+ }, "auth system hook failed");
61
+ }
62
+ }
63
+ function toOAuthConfig(provider) {
64
+ return {
65
+ providerId: provider.id,
66
+ name: provider.label,
67
+ clientId: provider.clientId,
68
+ clientSecret: provider.clientSecret,
69
+ discoveryUrl: provider.discoveryUrl,
70
+ authorizationUrl: provider.authorizationUrl,
71
+ tokenUrl: provider.tokenUrl,
72
+ userInfoUrl: provider.userInfoUrl,
73
+ scopes: provider.scopes ?? [
74
+ "openid",
75
+ "email",
76
+ "profile"
77
+ ]
78
+ };
79
+ }
80
+ function buildPlugins(options, db) {
81
+ const oauth = (options.oauth ?? []).map((provider) => toOAuthConfig(provider));
82
+ const base = new URL(options.baseURL);
83
+ return [
84
+ ...oauth.length > 0 ? [genericOAuth({ config: oauth })] : [],
85
+ ...options.passkeys ? [passkey({
86
+ rpID: options.passkeys.rpID ?? base.hostname,
87
+ rpName: options.passkeys.rpName ?? "StoryShelf",
88
+ origin: options.passkeys.origin ?? base.origin
89
+ })] : [],
90
+ ...options.sso ? [buildSsoPlugin(options.sso, db)] : [],
91
+ ...options.plugins ?? []
92
+ ];
93
+ }
94
+ /** SSO plugin with static providers plus the shelf group-sync hook. */
95
+ function buildSsoPlugin(options, db) {
96
+ const providers = options.providers;
97
+ const syncing = providers.some((provider) => provider.groups !== void 0);
98
+ return sso({
99
+ defaultSSO: toSSODefaultEntries(options),
100
+ disableImplicitSignUp: options.disableImplicitSignUp,
101
+ ...syncing ? {
102
+ provisionUserOnEveryLogin: true,
103
+ provisionUser: provisionHook(db, providers)
104
+ } : {}
105
+ });
106
+ }
107
+ function toSSOOidcEntry(oidc) {
108
+ return {
109
+ issuer: oidc.issuer,
110
+ discoveryEndpoint: oidc.discoveryUrl ?? `${oidc.issuer.replace(/\/+$/u, "")}/.well-known/openid-configuration`,
111
+ pkce: oidc.pkce ?? true,
112
+ clientId: oidc.clientId,
113
+ clientSecret: oidc.clientSecret,
114
+ scopes: oidc.scopes ?? [
115
+ "openid",
116
+ "email",
117
+ "profile"
118
+ ]
119
+ };
120
+ }
121
+ function toSSOSamlShared(saml) {
122
+ return {
123
+ issuer: saml.issuer,
124
+ entryPoint: saml.entryPoint,
125
+ cert: saml.cert,
126
+ audience: saml.audience,
127
+ wantAssertionsSigned: saml.wantAssertionsSigned
128
+ };
129
+ }
130
+ function toSSOSamlEntry(provider, saml) {
131
+ const shared = toSSOSamlShared(saml);
132
+ const mapping = provider.groups === void 0 ? void 0 : { extraFields: { [groupClaimAlias(provider.groups)]: groupClaimAlias(provider.groups) } };
133
+ if (saml.metadataXml !== void 0) return {
134
+ ...shared,
135
+ ...mapping === void 0 ? {} : { mapping },
136
+ idpMetadata: {
137
+ metadata: saml.metadataXml,
138
+ entityID: saml.entityID
139
+ }
140
+ };
141
+ if (saml.entityID === void 0) throw new Error("Shelf SSO SAML needs entityID or metadataXml");
142
+ return {
143
+ ...shared,
144
+ ...mapping === void 0 ? {} : { mapping },
145
+ idpMetadata: { entityID: saml.entityID }
146
+ };
147
+ }
148
+ /** First claim name: the userInfo alias for the groups attribute. */
149
+ function groupClaimAlias(groups) {
150
+ const claim = groups.claim;
151
+ if (claim === void 0) return DEFAULT_GROUP_CLAIM;
152
+ const first = Array.isArray(claim) ? claim[0] : claim;
153
+ if (!first) throw new Error("Shelf SSO groups.claim must name at least one claim");
154
+ return first;
155
+ }
156
+ function toSSODefaultEntry(provider) {
157
+ if (!provider.oidc && !provider.saml) throw new Error(`Shelf SSO provider "${provider.id}" needs oidc or saml`);
158
+ return {
159
+ domain: provider.domain,
160
+ providerId: provider.id,
161
+ ...provider.oidc ? { oidcConfig: toSSOOidcEntry(provider.oidc) } : {},
162
+ ...provider.saml ? { samlConfig: toSSOSamlEntry(provider, provider.saml) } : {}
163
+ };
164
+ }
165
+ function toSSODefaultEntries(options) {
166
+ return options.providers.map((provider) => toSSODefaultEntry(provider));
167
+ }
168
+ function toSocialEntry(provider) {
169
+ const entry = { clientId: provider.clientId };
170
+ if (provider.clientSecret !== void 0) entry["clientSecret"] = provider.clientSecret;
171
+ for (const [key, value] of Object.entries(provider.extra ?? {})) if (value !== void 0) entry[key] = value;
172
+ return entry;
173
+ }
174
+ function buildSocialProviders(options) {
175
+ const providers = options.social ?? [];
176
+ if (providers.length === 0) return;
177
+ const out = {};
178
+ for (const provider of providers) out[provider.id] = toSocialEntry(provider);
179
+ return out;
180
+ }
181
+ function describeLoginMethods(options) {
182
+ const methods = [
183
+ ...options.emailPassword ?? true ? [{
184
+ kind: "password",
185
+ id: "password",
186
+ label: "Email"
187
+ }] : [],
188
+ ...options.passkeys ? [{
189
+ kind: "passkey",
190
+ id: "passkey",
191
+ label: "Passkey"
192
+ }] : [],
193
+ ...(options.social ?? []).map((provider) => ({
194
+ kind: "oauth",
195
+ id: provider.id,
196
+ label: provider.label
197
+ })),
198
+ ...(options.oauth ?? []).map((provider) => ({
199
+ kind: "oauth",
200
+ id: provider.id,
201
+ label: provider.label
202
+ })),
203
+ ...(options.sso?.providers ?? []).map((provider) => ({
204
+ kind: "sso",
205
+ id: provider.id,
206
+ label: provider.label
207
+ }))
208
+ ];
209
+ const seen = /* @__PURE__ */ new Set();
210
+ for (const method of methods) {
211
+ if (seen.has(method.id)) throw new Error(`Duplicate shelf login method id "${method.id}" (provider ids must be unique across social, oauth, and sso)`);
212
+ seen.add(method.id);
213
+ }
214
+ return methods;
215
+ }
216
+ /**
217
+ * Engine paths the shelf calls directly: custom plugins (`options.plugins`)
218
+ * must not shadow them (requests would silently hit the wrong handler).
219
+ * Built-in plugins (passkey, SSO, genericOAuth) are shelf-owned and exempt.
220
+ */
221
+ const RESERVED_ENGINE_PATHS = [
222
+ "/sign-up/email",
223
+ "/sign-in/email",
224
+ "/sign-in/social",
225
+ "/sign-in/sso",
226
+ "/change-password",
227
+ "/list-sessions",
228
+ "/revoke-session",
229
+ "/revoke-sessions",
230
+ "/revoke-other-sessions",
231
+ "/sign-out"
232
+ ];
233
+ /** Fail fast when a custom plugin shadows a shelf-reserved engine path. */
234
+ function assertNoReservedEndpointCollision(plugins) {
235
+ const reserved = new Set(RESERVED_ENGINE_PATHS);
236
+ for (const plugin of plugins ?? []) for (const endpoint of Object.values(plugin.endpoints ?? {})) if (reserved.has(endpoint.path)) throw new Error(`Shelf plugin "${plugin.id}" shadows reserved engine path "${endpoint.path}" (rename the endpoint)`);
237
+ }
238
+ /**
239
+ * SSO provider-management paths blocked at the engine mount: shelf SSO
240
+ * providers are code-driven recipes (`defaultSSO`), so runtime registration,
241
+ * mutation, and domain verification stay off (any signed-in member could
242
+ * otherwise register a rogue IdP for an arbitrary domain).
243
+ */
244
+ const SSO_MANAGEMENT_PATHS = [
245
+ "/sso/register",
246
+ "/sso/providers",
247
+ "/sso/get-provider",
248
+ "/sso/update-provider",
249
+ "/sso/delete-provider",
250
+ "/sso/request-domain-verification",
251
+ "/sso/verify-domain"
252
+ ];
253
+ /** Default sender address derived from the public base URL host. */
254
+ function defaultFromEmail(baseURL) {
255
+ try {
256
+ return `storyshelf@${new URL(baseURL).hostname}`;
257
+ } catch {
258
+ return "storyshelf@localhost";
259
+ }
260
+ }
261
+ /** Send one auth mail; throws so callers decide best-effort policy. */
262
+ async function sendAuthMail(options, message) {
263
+ await options.emailSender?.send({
264
+ to: message.to,
265
+ subject: message.subject,
266
+ text: message.text,
267
+ from: options.fromEmail ?? defaultFromEmail(options.baseURL)
268
+ });
269
+ }
270
+ /**
271
+ * Build the Better Auth instance with shelf-curated defaults: email/password,
272
+ * DB sessions under our cookie name, no cookie cache (revocation and
273
+ * disabled-users bite immediately), ULID ids shared with our `users` table,
274
+ * mirror hook on user creation.
275
+ */
276
+ function buildAuthInstance(options, db) {
277
+ const expiresIn = 86400 * (options.sessionExpiresInDays ?? 7);
278
+ const socialProviders = buildSocialProviders(options);
279
+ return betterAuth({
280
+ baseURL: options.baseURL,
281
+ secret: options.secret,
282
+ database: createShelfDbBridge(db, {
283
+ tables: baseAuthTables,
284
+ dateFields: baseAuthDateFields
285
+ }),
286
+ emailAndPassword: {
287
+ enabled: options.emailPassword ?? true,
288
+ requireEmailVerification: false,
289
+ disableSignUp: true,
290
+ ...options.emailSender ? { sendResetPassword: async (input) => {
291
+ await sendAuthMail(options, {
292
+ to: input.user.email,
293
+ subject: "Reset your StoryShelf password",
294
+ text: `Reset your password here (single use): ${input.url}`
295
+ });
296
+ } } : {}
297
+ },
298
+ ...socialProviders ? { socialProviders } : {},
299
+ ...options.sso ? { disabledPaths: SSO_MANAGEMENT_PATHS } : {},
300
+ session: {
301
+ expiresIn,
302
+ updateAge: 86400,
303
+ cookieCache: { enabled: false }
304
+ },
305
+ advanced: {
306
+ database: { generateId: () => ulid() },
307
+ cookies: { session_token: { name: SESSION_COOKIE } }
308
+ },
309
+ databaseHooks: { user: { create: { after: async (created) => {
310
+ await mirrorUser(db, created, options);
311
+ } } } },
312
+ plugins: buildPlugins(options, db)
313
+ });
314
+ }
315
+ async function resolveAuthUser(shelf, db, request) {
316
+ const result = await shelf.api.getSession({ headers: request.headers });
317
+ if (!result?.user) return null;
318
+ const stored = await new UserModel(db).get(result.user.id);
319
+ if (stored?.disabled) return null;
320
+ if (stored && (stored.name !== result.user.name || stored.email !== result.user.email || (stored.avatarUrl ?? null) !== (result.user.image ?? null))) await db.update(db.tables.users, stored.id, {
321
+ name: result.user.name,
322
+ email: result.user.email,
323
+ avatarUrl: result.user.image ?? null
324
+ });
325
+ return toAuthUser({
326
+ id: result.user.id,
327
+ email: result.user.email,
328
+ name: result.user.name,
329
+ image: result.user.image
330
+ }, stored?.role ?? "member", stored?.displayNameOverride ?? null);
331
+ }
332
+ async function revokeSession(db, shelf, sessionToken) {
333
+ if (sessionToken === "") return;
334
+ await deleteSessionByToken(db, sessionToken);
335
+ await shelf.api.signOut({ headers: new Headers({ cookie: `${SESSION_COOKIE}=${sessionToken}` }) }).catch(() => {});
336
+ }
337
+ async function setUserDisabled(db, userId, disabled) {
338
+ await db.update(db.tables.users, userId, { disabled });
339
+ if (disabled) await deleteUserSessions(db, userId);
340
+ }
341
+ /** Send the invite link best-effort; the token is always still returned. */
342
+ async function sendInviteMail(options, email, issued) {
343
+ try {
344
+ await options.onAuthSystemEvent?.("sys:invite-issued", {
345
+ email,
346
+ inviteId: issued.inviteId,
347
+ expiresAt: issued.expiresAt
348
+ });
349
+ } catch (error) {
350
+ options.logger?.warn({
351
+ err: error,
352
+ email
353
+ }, "auth system hook failed");
354
+ }
355
+ if (!options.emailSender) return;
356
+ const link = `${options.baseURL.replace(/\/+$/u, "")}/auth/invites/${issued.inviteId}?token=${issued.token}`;
357
+ try {
358
+ await sendAuthMail(options, {
359
+ to: email,
360
+ subject: "Your StoryShelf invite",
361
+ text: `Accept your invite here (expires ${issued.expiresAt}): ${link}`
362
+ });
363
+ } catch (error) {
364
+ options.logger?.warn({
365
+ err: error,
366
+ email
367
+ }, "invite email failed");
368
+ }
369
+ }
370
+ /** Invite lifecycle methods for the Auth singleton (best-effort mail). */
371
+ function buildInviteMethods(db, options) {
372
+ return {
373
+ issueInvite: async (input) => {
374
+ const issued = await issueInvite(db, input);
375
+ await sendInviteMail(options, input.email, issued);
376
+ return issued;
377
+ },
378
+ verifyInvite: async (input) => await verifyInvite(db, input),
379
+ acceptInvite: async (input) => {
380
+ const user = await acceptInvite(db, input);
381
+ try {
382
+ await options.onAuthSystemEvent?.("sys:user-created", {
383
+ userId: user.id,
384
+ email: user.email,
385
+ name: user.name
386
+ });
387
+ } catch (error) {
388
+ options.logger?.warn({
389
+ err: error,
390
+ userId: user.id
391
+ }, "auth system hook failed");
392
+ }
393
+ return {
394
+ ...user,
395
+ providerId: ENGINE_PROVIDER_ID
396
+ };
397
+ }
398
+ };
399
+ }
400
+ /** Auth singleton: session reads, revocation, invites, and descriptors. */
401
+ function buildAdapter(shelf, db, options) {
402
+ return {
403
+ handler: async (request) => await shelf.handler(request),
404
+ loginMethods: () => describeLoginMethods(options),
405
+ setup: async () => {
406
+ if (options.secret.length < 32) throw new Error("Shelf auth requires a secret of at least 32 characters");
407
+ },
408
+ ...buildInviteMethods(db, options),
409
+ passkeysEnabled: () => options.passkeys !== void 0,
410
+ listSessions: async (userId) => await listUserSessions(db, userId),
411
+ listPasskeys: async (userId) => await listUserPasskeys(db, userId),
412
+ hasPassword: async (userId) => await hasPasswordCredential(db, userId),
413
+ setDisabled: async (userId, disabled) => {
414
+ await setUserDisabled(db, userId, disabled);
415
+ },
416
+ check: async (request) => await resolveAuthUser(shelf, db, request),
417
+ createSession: async () => {
418
+ throw new Error("Shelf auth sessions are minted by engine endpoints only; mount auth.handler at /api/auth/*.");
419
+ },
420
+ destroySession: async (sessionToken) => {
421
+ await revokeSession(db, shelf, sessionToken);
422
+ }
423
+ };
424
+ }
425
+ /**
426
+ * Create the shelf auth engine over any DatabaseAdapter.
427
+ *
428
+ * Returns the raw instance (mount `auth.handler`, call `auth.api.*`) plus
429
+ * the Auth singleton (check/destroySession, invites, descriptors).
430
+ */
431
+ function createShelfAuth(options) {
432
+ const { db } = options;
433
+ describeLoginMethods(options);
434
+ assertNoReservedEndpointCollision(options.plugins);
435
+ const shelf = buildAuthInstance(options, db);
436
+ return {
437
+ auth: shelf,
438
+ adapter: buildAdapter(shelf, db, options)
439
+ };
440
+ }
441
+ //#endregion
442
+ export { ENGINE_PROVIDER_ID, createShelfAuth };
443
+
444
+ //# sourceMappingURL=engine.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.mjs","names":[],"sources":["../src/engine.ts"],"sourcesContent":["import { passkey } from \"@better-auth/passkey\";\nimport { sso, type SSOOptions } from \"@better-auth/sso\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { EmailSender } from \"@storyshelf/core/adapter/email-sender\";\nimport type { Auth, EngineLoginMethod } from \"@storyshelf/core/auth\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { UserModel } from \"@storyshelf/core/models\";\n/**\n * Shelf auth engine: Better Auth protocol plumbing behind the Auth singleton.\n *\n * Sessions are wholesale Better Auth DB rows under our cookie name;\n * identities mirror into our `users` table (source of truth for roles,\n * memberships, and profile). All logins flow through engine endpoints\n * (mounted at `/api/auth/*`) — never through `createSession`, which exists\n * only for interface conformance and fails loudly.\n */\nimport { SESSION_COOKIE, type AuthUser } from \"@storyshelf/core/types\";\nimport { ulid } from \"@storyshelf/core/utils\";\nimport { betterAuth, type BetterAuthPlugin } from \"better-auth\";\nimport { genericOAuth, type GenericOAuthConfig } from \"better-auth/plugins\";\nimport { baseAuthDateFields, baseAuthTables } from \"./auth-tables.ts\";\nimport { createShelfDbBridge } from \"./db-bridge.ts\";\nimport { DEFAULT_GROUP_CLAIM, provisionHook } from \"./groups.ts\";\nimport { acceptInvite, issueInvite, verifyInvite } from \"./invites.ts\";\nimport { hasPasswordCredential, listUserPasskeys } from \"./passkeys.ts\";\nimport { deleteSessionByToken, deleteUserSessions, listUserSessions } from \"./sessions.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/** Provider id minted on every engine identity (method attribution is future work). */\nexport const ENGINE_PROVIDER_ID = \"better-auth\";\n\n/** Options for {@link createShelfAuth}. */\nexport interface ShelfAuthOptions {\n /** Shelf database (any driver; auth tables migrate via driver DDL). */\n db: DatabaseAdapter;\n /** Session/cookie signing secret (non-empty; Better Auth validates strength). */\n secret: string;\n /** Public base URL issuing auth responses (callback + cookie scope). */\n baseURL: string;\n /** Enable email/password login. Defaults to true. */\n emailPassword?: boolean;\n /**\n * Email transport shared with notification channels. When set, invite\n * links are emailed best-effort and password-reset requests send mail;\n * when unset, invites stay out-of-band links and resets stay re-invite.\n */\n emailSender?: EmailSender;\n /** Sender address for auth mail (defaults to `storyshelf@<baseURL host>`). */\n fromEmail?: string;\n /**\n * System-event hook for host-owned fan-out (e.g. `sys:user-created`,\n * `sys:invite-issued` to admin channels). Best-effort: failures are\n * logged and swallowed, never break auth flows.\n */\n onAuthSystemEvent?: (\n event: \"sys:user-created\" | \"sys:invite-issued\",\n data: Record<string, unknown>,\n ) => unknown;\n /** Host-owned logger for auth diagnostics (explicit override only). */\n logger?: Logger;\n /** Native social providers (GitHub, Google, Entra, Cognito...), each one button. */\n social?: ShelfSocialProvider[];\n /** OAuth/OIDC providers, each rendered as one login button. Defaults to none. */\n oauth?: ShelfOAuthProvider[];\n /** WebAuthn passkeys (profile enrollment; sign-in ceremony is E8). Defaults to off. */\n passkeys?: ShelfPasskeyOptions;\n /** Domain-routed SSO (OIDC discovery + SAML via samlify). Defaults to none. */\n sso?: ShelfSSOOptions;\n /** Extra Better Auth plugins (passkey, SSO, ...). Defaults to none. */\n plugins?: BetterAuthPlugin[];\n /** Session lifetime in days. Defaults to 7 (current parity). */\n sessionExpiresInDays?: number;\n}\n\n/** One OAuth/OIDC login method (Keycloak preset or explicit endpoints). */\nexport interface ShelfOAuthProvider {\n /** Stable id used in routes and descriptors (e.g. \"keycloak\"). */\n id: string;\n /** Human label rendered on the login button. */\n label: string;\n /** OAuth client ID registered with the provider. */\n clientId: string;\n /** OAuth client secret (omit for public clients). */\n clientSecret?: string;\n /** OIDC discovery URL; prefer over explicit endpoints when available. */\n discoveryUrl?: string;\n /** Explicit endpoint overrides (skip discovery for these). */\n authorizationUrl?: string;\n /** Explicit endpoint overrides (skip discovery for these). */\n tokenUrl?: string;\n /** Explicit endpoint overrides (skip discovery for these). */\n userInfoUrl?: string;\n /** OAuth scopes. Defaults to `[\"openid\", \"email\", \"profile\"]`. */\n scopes?: string[];\n}\n\n/** One native social login method (Better Auth built-in provider). */\nexport interface ShelfSocialProvider {\n /** Better Auth provider key (e.g. \"github\", \"microsoft\", \"cognito\"). */\n id: \"github\" | \"gitlab\" | \"google\" | \"microsoft\" | \"cognito\";\n /** Human label rendered on the login button. */\n label: string;\n /** OAuth client ID registered with the provider. */\n clientId: string;\n /** OAuth client secret (omit for public clients). */\n clientSecret?: string;\n /** Provider-specific options (tenantId, domain, region, userPoolId, issuer...). */\n extra?: Record<string, string | undefined>;\n}\n\n/** WebAuthn passkey config (profile enrollment; sign-in ceremony is E8). */\nexport interface ShelfPasskeyOptions {\n /**\n * Relying-party id. Defaults to the baseURL hostname.\n *\n * WebAuthn needs a secure context: `localhost` (any port) or HTTPS.\n * Plain-HTTP LAN/IP deployments cannot use passkeys — the UI hides\n * passkey affordances there (see `passkey-ceremony.ts`).\n */\n rpID?: string;\n /** Human-readable site title. Defaults to \"StoryShelf\". */\n rpName?: string;\n /** Allowed origins (no trailing slash). Defaults to the baseURL origin. */\n origin?: string | string[];\n}\n\n/** OIDC SSO connection (discovery-based, resolved by email domain). */\nexport interface ShelfSSOOidc {\n /** IdP issuer (e.g. the Keycloak realm URL). */\n issuer: string;\n /** Discovery document URL. Defaults to `issuer + /.well-known/openid-configuration`. */\n discoveryUrl?: string;\n /** OAuth client ID registered with the provider. */\n clientId: string;\n /** OAuth client secret (omit for public clients). */\n clientSecret?: string;\n /** OAuth scopes. Defaults to `[\"openid\", \"email\", \"profile\"]`. */\n scopes?: string[];\n /** Use PKCE. Defaults to true. */\n pkce?: boolean;\n}\n\n/** SAML SSO connection (samlify via the SSO plugin; never hand-rolled XML). */\nexport interface ShelfSSOSaml {\n /** SP entity ID (our audience; also the metadata default). */\n issuer: string;\n /** IdP SSO URL (required by the plugin type; unused when `metadataXml` is set). */\n entryPoint: string;\n /** IdP signing cert(s), PEM (ignored when `metadataXml` is set). */\n cert?: string | string[];\n /** IdP metadata XML (preferred: carries entity ID, endpoints, and certs). */\n metadataXml?: string;\n /** IdP entity ID (required without `metadataXml`). */\n entityID?: string;\n /** Expected audience (defaults to the SP issuer). */\n audience?: string;\n /** Reject unsigned assertions. Defaults to false. */\n wantAssertionsSigned?: boolean;\n}\n\n/** IdP group sync for one SSO provider (site role + project memberships). */\nexport interface ShelfSSOGroupSync {\n /**\n * Claim (OIDC) or attribute (SAML) name(s) carrying group memberships.\n * First hit wins across names; arrays pass through, a lone string counts\n * as one group (never whitespace-split). Defaults to `[\"groups\"]`.\n */\n claim?: string | string[];\n /**\n * Exact group names granting the site `admin` role. Full reconcile:\n * members not matched reset to `member` on every SSO sign-in.\n * Defaults to none (roles untouched).\n */\n admins?: string[];\n}\n\n/** One domain-routed SSO login method (OIDC, SAML, or both). */\nexport interface ShelfSSOProvider {\n /** Stable id used in routes and descriptors (e.g. \"acme-sso\"). */\n id: string;\n /** Human label rendered on the login button. */\n label: string;\n /** Email domain routing key (e.g. \"acme.example\"). */\n domain: string;\n /** OIDC connection. Defaults to none. */\n oidc?: ShelfSSOOidc;\n /** SAML connection. Defaults to none. */\n saml?: ShelfSSOSaml;\n /** IdP group sync. Defaults to none (no group reads, roles untouched). */\n groups?: ShelfSSOGroupSync;\n}\n\n/** Domain-routed SSO (OIDC discovery + SAML). Defaults to none. */\nexport interface ShelfSSOOptions {\n /** SSO providers, each rendered as one login button. */\n providers: ShelfSSOProvider[];\n /** Require explicit sign-up confirmation for new SSO users. Defaults to false. */\n disableImplicitSignUp?: boolean;\n}\n\n/** Session user fields the shelf reads back. */\nexport interface EngineSessionUser {\n id: string;\n email: string;\n name: string;\n image?: string | null;\n}\n\n/** Subset of the Better Auth instance surface the shelf uses. */\nexport interface ShelfAuthInstance {\n /** HTTP handler (mount at `/api/auth/*`). */\n handler: (request: Request) => Response | Promise<Response>;\n api: {\n /** Resolve the session user from request headers (null when anonymous). */\n getSession(input: { headers: Headers }): Promise<{ user: EngineSessionUser } | null>;\n /** Revoke the session carried by the headers. */\n signOut(input: { headers: Headers }): Promise<unknown>;\n };\n}\n\n/** Engine handle: raw instance for mounting plus the Auth singleton. */\nexport interface ShelfAuth {\n /** Better Auth instance (mount `auth.handler`, call `auth.api.*`). */\n auth: ShelfAuthInstance;\n /** Auth singleton (check/destroySession, invites, sessions, descriptors). */\n adapter: Auth;\n}\n\nfunction toAuthUser(\n sessionUser: { id: string; email: string; name: string; image?: string | null },\n role: AuthUser[\"role\"],\n displayName: string | null,\n): AuthUser {\n return {\n id: sessionUser.id,\n email: sessionUser.email,\n // Empty overrides fall back (writes normalize \"\" to null, but legacy\n // rows may still hold one).\n name: displayName === \"\" || displayName === null ? sessionUser.name : displayName,\n avatarUrl: sessionUser.image ?? undefined,\n role,\n providerId: ENGINE_PROVIDER_ID,\n };\n}\n\nasync function mirrorUser(\n db: DatabaseAdapter,\n created: unknown,\n hooks: Pick<ShelfAuthOptions, \"onAuthSystemEvent\" | \"logger\">,\n): Promise<void> {\n const user = created as { id: string; email: string; name: string; image?: string | null };\n // Never demote: an engine row created for a staged invitee (admin role)\n // would otherwise overwrite the role with the default.\n const existing = await new UserModel(db).get(user.id);\n await new UserModel(db).upsert({\n id: user.id,\n email: user.email,\n name: user.name,\n avatarUrl: user.image ?? null,\n role: existing?.role ?? \"member\",\n });\n if (existing) {\n return;\n }\n try {\n await hooks.onAuthSystemEvent?.(\"sys:user-created\", {\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n } catch (error) {\n hooks.logger?.warn({ err: error, userId: user.id }, \"auth system hook failed\");\n }\n}\n\nfunction toOAuthConfig(provider: ShelfOAuthProvider): GenericOAuthConfig {\n return {\n providerId: provider.id,\n name: provider.label,\n clientId: provider.clientId,\n clientSecret: provider.clientSecret,\n discoveryUrl: provider.discoveryUrl,\n authorizationUrl: provider.authorizationUrl,\n tokenUrl: provider.tokenUrl,\n userInfoUrl: provider.userInfoUrl,\n scopes: provider.scopes ?? [\"openid\", \"email\", \"profile\"],\n };\n}\n\nfunction buildPlugins(options: ShelfAuthOptions, db: DatabaseAdapter): BetterAuthPlugin[] {\n const oauth = (options.oauth ?? []).map((provider) => toOAuthConfig(provider));\n const base = new URL(options.baseURL);\n return [\n ...(oauth.length > 0 ? [genericOAuth({ config: oauth })] : []),\n ...(options.passkeys\n ? [\n passkey({\n rpID: options.passkeys.rpID ?? base.hostname,\n rpName: options.passkeys.rpName ?? \"StoryShelf\",\n origin: options.passkeys.origin ?? base.origin,\n }),\n ]\n : []),\n ...(options.sso ? [buildSsoPlugin(options.sso, db)] : []),\n ...(options.plugins ?? []),\n ];\n}\n\n/** SSO plugin with static providers plus the shelf group-sync hook. */\nfunction buildSsoPlugin(options: ShelfSSOOptions, db: DatabaseAdapter): BetterAuthPlugin {\n const providers = options.providers;\n const syncing = providers.some((provider) => provider.groups !== undefined);\n return sso({\n defaultSSO: toSSODefaultEntries(options),\n disableImplicitSignUp: options.disableImplicitSignUp,\n // Group sync runs inside the plugin's own transaction on every sign-in\n // (first login and beyond); plain db ops join the ambient transaction.\n ...(syncing\n ? { provisionUserOnEveryLogin: true, provisionUser: provisionHook(db, providers) }\n : {}),\n });\n}\n\n/** Static SSO entries for the plugin (code-driven; take precedence over DB rows). */\ntype SSODefaultEntry = NonNullable<SSOOptions[\"defaultSSO\"]>[number];\n\nfunction toSSOOidcEntry(oidc: ShelfSSOOidc): NonNullable<SSODefaultEntry[\"oidcConfig\"]> {\n return {\n issuer: oidc.issuer,\n discoveryEndpoint:\n oidc.discoveryUrl ?? `${oidc.issuer.replace(/\\/+$/u, \"\")}/.well-known/openid-configuration`,\n pkce: oidc.pkce ?? true,\n clientId: oidc.clientId,\n clientSecret: oidc.clientSecret,\n // Explicit default (the plugin would add offline_access): matches the\n // ShelfSSOOidc contract and the E6 genericOAuth recipes.\n scopes: oidc.scopes ?? [\"openid\", \"email\", \"profile\"],\n };\n}\n\nfunction toSSOSamlShared(saml: ShelfSSOSaml): {\n issuer: string;\n entryPoint: string;\n cert: string | string[] | undefined;\n audience: string | undefined;\n wantAssertionsSigned: boolean | undefined;\n} {\n return {\n issuer: saml.issuer,\n entryPoint: saml.entryPoint,\n cert: saml.cert,\n audience: saml.audience,\n wantAssertionsSigned: saml.wantAssertionsSigned,\n };\n}\n\nfunction toSSOSamlEntry(\n provider: ShelfSSOProvider,\n saml: ShelfSSOSaml,\n): NonNullable<SSODefaultEntry[\"samlConfig\"]> {\n const shared = toSSOSamlShared(saml);\n // Surface the groups attribute into userInfo (the plugin only forwards\n // mapped fields): alias equals attribute name, read back by extractGroups.\n const mapping =\n provider.groups === undefined\n ? undefined\n : { extraFields: { [groupClaimAlias(provider.groups)]: groupClaimAlias(provider.groups) } };\n if (saml.metadataXml !== undefined) {\n return {\n ...shared,\n ...(mapping === undefined ? {} : { mapping }),\n idpMetadata: { metadata: saml.metadataXml, entityID: saml.entityID },\n };\n }\n if (saml.entityID === undefined) {\n throw new Error(\"Shelf SSO SAML needs entityID or metadataXml\");\n }\n return {\n ...shared,\n ...(mapping === undefined ? {} : { mapping }),\n idpMetadata: { entityID: saml.entityID },\n };\n}\n\n/** First claim name: the userInfo alias for the groups attribute. */\nfunction groupClaimAlias(groups: ShelfSSOGroupSync): string {\n const claim = groups.claim;\n if (claim === undefined) {\n return DEFAULT_GROUP_CLAIM;\n }\n const first = Array.isArray(claim) ? claim[0] : claim;\n if (!first) {\n throw new Error(\"Shelf SSO groups.claim must name at least one claim\");\n }\n return first;\n}\n\nfunction toSSODefaultEntry(provider: ShelfSSOProvider): SSODefaultEntry {\n if (!provider.oidc && !provider.saml) {\n throw new Error(`Shelf SSO provider \"${provider.id}\" needs oidc or saml`);\n }\n return {\n domain: provider.domain,\n providerId: provider.id,\n ...(provider.oidc ? { oidcConfig: toSSOOidcEntry(provider.oidc) } : {}),\n ...(provider.saml ? { samlConfig: toSSOSamlEntry(provider, provider.saml) } : {}),\n };\n}\n\nfunction toSSODefaultEntries(options: ShelfSSOOptions): SSODefaultEntry[] {\n return options.providers.map((provider) => toSSODefaultEntry(provider));\n}\n\nfunction toSocialEntry(provider: ShelfSocialProvider): Record<string, unknown> {\n const entry: Record<string, unknown> = { clientId: provider.clientId };\n if (provider.clientSecret !== undefined) {\n entry[\"clientSecret\"] = provider.clientSecret;\n }\n for (const [key, value] of Object.entries(provider.extra ?? {})) {\n if (value !== undefined) {\n entry[key] = value;\n }\n }\n return entry;\n}\n\nfunction buildSocialProviders(\n options: ShelfAuthOptions,\n): Record<string, Record<string, unknown>> | undefined {\n const providers = options.social ?? [];\n if (providers.length === 0) {\n return undefined;\n }\n const out: Record<string, Record<string, unknown>> = {};\n for (const provider of providers) {\n out[provider.id] = toSocialEntry(provider);\n }\n return out;\n}\n\nfunction describeLoginMethods(options: ShelfAuthOptions): EngineLoginMethod[] {\n const methods: EngineLoginMethod[] = [\n ...((options.emailPassword ?? true)\n ? [{ kind: \"password\", id: \"password\", label: \"Email\" } as const]\n : []),\n ...(options.passkeys ? [{ kind: \"passkey\", id: \"passkey\", label: \"Passkey\" } as const] : []),\n ...(options.social ?? []).map(\n (provider) => ({ kind: \"oauth\", id: provider.id, label: provider.label }) as const,\n ),\n ...(options.oauth ?? []).map(\n (provider) => ({ kind: \"oauth\", id: provider.id, label: provider.label }) as const,\n ),\n ...(options.sso?.providers ?? []).map(\n (provider) => ({ kind: \"sso\", id: provider.id, label: provider.label }) as const,\n ),\n ];\n const seen = new Set<string>();\n for (const method of methods) {\n if (seen.has(method.id)) {\n throw new Error(\n `Duplicate shelf login method id \"${method.id}\" (provider ids must be unique across social, oauth, and sso)`,\n );\n }\n seen.add(method.id);\n }\n return methods;\n}\n\n/**\n * Engine paths the shelf calls directly: custom plugins (`options.plugins`)\n * must not shadow them (requests would silently hit the wrong handler).\n * Built-in plugins (passkey, SSO, genericOAuth) are shelf-owned and exempt.\n */\nconst RESERVED_ENGINE_PATHS = [\n \"/sign-up/email\",\n \"/sign-in/email\",\n \"/sign-in/social\",\n \"/sign-in/sso\",\n \"/change-password\",\n \"/list-sessions\",\n \"/revoke-session\",\n \"/revoke-sessions\",\n \"/revoke-other-sessions\",\n \"/sign-out\",\n];\n\n/** Fail fast when a custom plugin shadows a shelf-reserved engine path. */\nfunction assertNoReservedEndpointCollision(plugins: BetterAuthPlugin[] | undefined): void {\n const reserved = new Set(RESERVED_ENGINE_PATHS);\n for (const plugin of plugins ?? []) {\n for (const endpoint of Object.values(plugin.endpoints ?? {})) {\n if (reserved.has(endpoint.path)) {\n throw new Error(\n `Shelf plugin \"${plugin.id}\" shadows reserved engine path \"${endpoint.path}\" (rename the endpoint)`,\n );\n }\n }\n }\n}\n\n/**\n * SSO provider-management paths blocked at the engine mount: shelf SSO\n * providers are code-driven recipes (`defaultSSO`), so runtime registration,\n * mutation, and domain verification stay off (any signed-in member could\n * otherwise register a rogue IdP for an arbitrary domain).\n */\nconst SSO_MANAGEMENT_PATHS = [\n \"/sso/register\",\n \"/sso/providers\",\n \"/sso/get-provider\",\n \"/sso/update-provider\",\n \"/sso/delete-provider\",\n \"/sso/request-domain-verification\",\n \"/sso/verify-domain\",\n];\n\n/** Default sender address derived from the public base URL host. */\nfunction defaultFromEmail(baseURL: string): string {\n try {\n return `storyshelf@${new URL(baseURL).hostname}`;\n } catch {\n return \"storyshelf@localhost\";\n }\n}\n\n/** Send one auth mail; throws so callers decide best-effort policy. */\nasync function sendAuthMail(\n options: Pick<ShelfAuthOptions, \"emailSender\" | \"fromEmail\" | \"baseURL\">,\n message: { to: string; subject: string; text: string },\n): Promise<void> {\n await options.emailSender?.send({\n to: message.to,\n subject: message.subject,\n text: message.text,\n from: options.fromEmail ?? defaultFromEmail(options.baseURL),\n });\n}\n\n/**\n * Build the Better Auth instance with shelf-curated defaults: email/password,\n * DB sessions under our cookie name, no cookie cache (revocation and\n * disabled-users bite immediately), ULID ids shared with our `users` table,\n * mirror hook on user creation.\n */\nfunction buildAuthInstance(options: ShelfAuthOptions, db: DatabaseAdapter): ShelfAuthInstance {\n const expiresIn = 60 * 60 * 24 * (options.sessionExpiresInDays ?? 7);\n const socialProviders = buildSocialProviders(options);\n const auth = betterAuth({\n baseURL: options.baseURL,\n secret: options.secret,\n database: createShelfDbBridge(db, {\n tables: baseAuthTables,\n dateFields: baseAuthDateFields,\n }),\n emailAndPassword: {\n enabled: options.emailPassword ?? true,\n requireEmailVerification: false,\n // Invite-only local accounts: public email signup stays off so the\n // only credential origins are invite accept, env bootstrap, and SSO\n // provisioning (all write identities directly, never via sign-up).\n disableSignUp: true,\n // User-requested resets work only when a sender is configured;\n // admins can always re-invite regardless.\n ...(options.emailSender\n ? {\n sendResetPassword: async (input: {\n user: { email: string };\n url: string;\n }): Promise<void> => {\n await sendAuthMail(options, {\n to: input.user.email,\n subject: \"Reset your StoryShelf password\",\n text: `Reset your password here (single use): ${input.url}`,\n });\n },\n }\n : {}),\n },\n // Native socials are option bags per provider key; our preset-built\n // records match those shapes, so narrow past the generated union.\n ...(socialProviders ? { socialProviders: socialProviders as never } : {}),\n // Shelf SSO providers are code-driven recipes; runtime provider\n // management stays off (see SSO_MANAGEMENT_PATHS).\n ...(options.sso ? { disabledPaths: SSO_MANAGEMENT_PATHS } : {}),\n session: { expiresIn, updateAge: 60 * 60 * 24, cookieCache: { enabled: false } },\n advanced: {\n database: { generateId: () => ulid() },\n cookies: { session_token: { name: SESSION_COOKIE } },\n },\n databaseHooks: {\n user: {\n create: {\n after: async (created: unknown): Promise<void> => {\n await mirrorUser(db, created, options);\n },\n },\n },\n },\n plugins: buildPlugins(options, db),\n });\n // Narrowed to the shelf surface: handler mounting plus session reads.\n return auth;\n}\n\nasync function resolveAuthUser(\n shelf: ShelfAuthInstance,\n db: DatabaseAdapter,\n request: Request,\n): Promise<AuthUser | null> {\n const result = await shelf.api.getSession({ headers: request.headers });\n if (!result?.user) {\n return null;\n }\n const stored = await new UserModel(db).get(result.user.id);\n // Disabled accounts lose their sessions immediately (no cookie cache, so\n // this check runs on every request). A missing row is a mirror-race, not\n // a disable: keep the member fallback.\n if (stored?.disabled) {\n return null;\n }\n // Refresh IdP-sourced profile fields on drift only (writes are rare: just\n // when the provider profile changes). The user-edited display name lives\n // in displayNameOverride and is never clobbered.\n if (\n stored &&\n (stored.name !== result.user.name ||\n stored.email !== result.user.email ||\n (stored.avatarUrl ?? null) !== (result.user.image ?? null))\n ) {\n await db.update(db.tables.users, stored.id, {\n name: result.user.name,\n email: result.user.email,\n avatarUrl: result.user.image ?? null,\n });\n }\n return toAuthUser(\n {\n id: result.user.id,\n email: result.user.email,\n name: result.user.name,\n image: result.user.image,\n },\n stored?.role ?? \"member\",\n stored?.displayNameOverride ?? null,\n );\n}\n\nasync function revokeSession(\n db: DatabaseAdapter,\n shelf: ShelfAuthInstance,\n sessionToken: string,\n): Promise<void> {\n if (sessionToken === \"\") {\n return;\n }\n // Table delete by raw token (accepts the signed cookie value): row removal\n // invalidates the cookie since session reads are DB-backed with no cache.\n await deleteSessionByToken(db, sessionToken);\n await shelf.api\n .signOut({ headers: new Headers({ cookie: `${SESSION_COOKIE}=${sessionToken}` }) })\n .catch(() => {}); // Intentionally empty — table delete above is the revocation\n}\n\nasync function setUserDisabled(\n db: DatabaseAdapter,\n userId: string,\n disabled: boolean,\n): Promise<void> {\n await db.update(db.tables.users, userId, { disabled });\n if (disabled) {\n await deleteUserSessions(db, userId);\n }\n}\n\n/** Send the invite link best-effort; the token is always still returned. */\nasync function sendInviteMail(\n options: ShelfAuthOptions,\n email: string,\n issued: { inviteId: string; token: string; expiresAt: string },\n): Promise<void> {\n try {\n await options.onAuthSystemEvent?.(\"sys:invite-issued\", {\n email,\n inviteId: issued.inviteId,\n expiresAt: issued.expiresAt,\n });\n } catch (error) {\n options.logger?.warn({ err: error, email }, \"auth system hook failed\");\n }\n if (!options.emailSender) {\n return;\n }\n const link = `${options.baseURL.replace(/\\/+$/u, \"\")}/auth/invites/${issued.inviteId}?token=${issued.token}`;\n try {\n await sendAuthMail(options, {\n to: email,\n subject: \"Your StoryShelf invite\",\n text: `Accept your invite here (expires ${issued.expiresAt}): ${link}`,\n });\n } catch (error) {\n // Fictional/internal addresses never deliver: log and keep the token\n // usable out-of-band.\n options.logger?.warn({ err: error, email }, \"invite email failed\");\n }\n}\n\n/** Invite lifecycle methods for the Auth singleton (best-effort mail). */\nfunction buildInviteMethods(\n db: DatabaseAdapter,\n options: ShelfAuthOptions,\n): Pick<Auth, \"issueInvite\" | \"verifyInvite\" | \"acceptInvite\"> {\n return {\n issueInvite: async (input) => {\n const issued = await issueInvite(db, input);\n await sendInviteMail(options, input.email, issued);\n return issued;\n },\n verifyInvite: async (input) => await verifyInvite(db, input),\n acceptInvite: async (input) => {\n // Direct credential writes bypass Better Auth's databaseHooks, so the\n // mirror hook never sees invite accepts: emit the creation here.\n const user = await acceptInvite(db, input);\n try {\n await options.onAuthSystemEvent?.(\"sys:user-created\", {\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n } catch (error) {\n options.logger?.warn({ err: error, userId: user.id }, \"auth system hook failed\");\n }\n return { ...user, providerId: ENGINE_PROVIDER_ID };\n },\n };\n}\n\n/** Auth singleton: session reads, revocation, invites, and descriptors. */\nfunction buildAdapter(\n shelf: ShelfAuthInstance,\n db: DatabaseAdapter,\n options: ShelfAuthOptions,\n): Auth {\n return {\n handler: async (request: Request): Promise<Response> => await shelf.handler(request),\n loginMethods: (): EngineLoginMethod[] => describeLoginMethods(options),\n // eslint-disable-next-line require-await -- Auth.setup is async; validation is sync\n setup: async () => {\n // Better Auth merely warns on short secrets; the shelf fails fast\n // (32 chars ≈ 128+ bits for generated values; forgeable cookies else).\n if (options.secret.length < 32) {\n throw new Error(\"Shelf auth requires a secret of at least 32 characters\");\n }\n },\n ...buildInviteMethods(db, options),\n passkeysEnabled: () => options.passkeys !== undefined,\n listSessions: async (userId) => await listUserSessions(db, userId),\n listPasskeys: async (userId) => await listUserPasskeys(db, userId),\n hasPassword: async (userId) => await hasPasswordCredential(db, userId),\n setDisabled: async (userId, disabled) => {\n await setUserDisabled(db, userId, disabled);\n },\n check: async (request: Request): Promise<AuthUser | null> =>\n await resolveAuthUser(shelf, db, request),\n // eslint-disable-next-line require-await -- interface is async; minting forbidden by design\n createSession: async (): Promise<string> => {\n throw new Error(\n \"Shelf auth sessions are minted by engine endpoints only; mount auth.handler at /api/auth/*.\",\n );\n },\n destroySession: async (sessionToken: string): Promise<void> => {\n await revokeSession(db, shelf, sessionToken);\n },\n };\n}\n\n/**\n * Create the shelf auth engine over any DatabaseAdapter.\n *\n * Returns the raw instance (mount `auth.handler`, call `auth.api.*`) plus\n * the Auth singleton (check/destroySession, invites, descriptors).\n */\nexport function createShelfAuth(options: ShelfAuthOptions): ShelfAuth {\n const { db } = options;\n // Fail fast at boot (login descriptors and plugin paths are fixed at creation).\n describeLoginMethods(options);\n assertNoReservedEndpointCollision(options.plugins);\n const shelf = buildAuthInstance(options, db);\n return { auth: shelf, adapter: buildAdapter(shelf, db, options) };\n}\n"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { ShelfSSOProvider } from "./engine.mjs";
4
+ import { SiteRole } from "@storyshelf/core/types";
5
+ import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
6
+ //#region src/groups.d.ts
7
+ /** Default claim name when recipes leave `groups.claim` unset. */
8
+ declare const DEFAULT_GROUP_CLAIM = "groups";
9
+ /**
10
+ * Read group names from provider userInfo. Arrays pass through; a lone
11
+ * string counts as exactly one group (never whitespace-split — real names
12
+ * contain spaces). Non-string entries are ignored. First claim hit wins
13
+ * across names; results de-duplicated.
14
+ */
15
+ declare function extractGroups(userInfo: Record<string, unknown>, claim?: string | string[]): string[];
16
+ /**
17
+ * Site role from admin-group intersection (full reconcile). Without
18
+ * configured admins the current role passes through untouched.
19
+ */
20
+ declare function resolveSiteRole(current: SiteRole, groups: string[], admins?: string[]): SiteRole;
21
+ /** Input mapped from the SSO plugin's provisionUser payload. */
22
+ interface GroupProvisionData {
23
+ user: {
24
+ id: string;
25
+ email: string;
26
+ name: string;
27
+ };
28
+ userInfo: Record<string, unknown>;
29
+ provider: {
30
+ providerId: string;
31
+ };
32
+ }
33
+ /** Normalized input for the sync (plugin payload flattened). */
34
+ interface SsoGroupSyncInput {
35
+ userId: string;
36
+ email: string;
37
+ name: string;
38
+ userInfo: Record<string, unknown>;
39
+ providerId: string;
40
+ }
41
+ /**
42
+ * Build the plugin `provisionUser` hook bound to shelf providers.
43
+ * Unknown provider ids are no-ops (defensive; the plugin resolves first).
44
+ */
45
+ declare function provisionHook(db: DatabaseAdapter, providers: ShelfSSOProvider[]): (data: GroupProvisionData) => Promise<void>;
46
+ /**
47
+ * Sync IdP groups into shelf state: site role (full reconcile) plus project
48
+ * memberships. No-op for providers without `groups` config. Creates the
49
+ * shelf row on first SSO login when the mirror hook has not run yet.
50
+ */
51
+ declare function syncSsoGroups(db: DatabaseAdapter, providers: ShelfSSOProvider[], input: SsoGroupSyncInput): Promise<void>;
52
+ //#endregion
53
+ export { DEFAULT_GROUP_CLAIM, GroupProvisionData, SsoGroupSyncInput, extractGroups, provisionHook, resolveSiteRole, syncSsoGroups };
54
+ //# sourceMappingURL=groups.d.mts.map