@lalternative/auth 0.13.3 → 0.14.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.
package/dist/server.js CHANGED
@@ -16,6 +16,170 @@ import {
16
16
  import { betterAuth, APIError } from "better-auth";
17
17
  import { emailOTP, admin, magicLink, twoFactor, genericOAuth } from "better-auth/plugins";
18
18
 
19
+ // src/kratos-credentials.ts
20
+ var KRATOS_SENTINEL_HASH = "kratos:external-credential:no-local-hash";
21
+ function isKratosSentinel(hash) {
22
+ return hash === KRATOS_SENTINEL_HASH;
23
+ }
24
+ var FLOW_TIMEOUT_MS = 5e3;
25
+ function csrfTokenOf(flow) {
26
+ return flow.ui?.nodes?.find((n) => n.attributes?.name === "csrf_token")?.attributes?.value;
27
+ }
28
+ function outcomeFromMessages(body) {
29
+ const ids = (body.ui?.messages ?? []).map((m) => m.id);
30
+ if (ids.includes(4000010)) return { status: "account_disabled" };
31
+ if (ids.includes(4000006) || ids.includes(4000002)) {
32
+ return { status: "invalid_credentials" };
33
+ }
34
+ const errorId = body.error?.id;
35
+ if (errorId === "session_aal2_required") {
36
+ return { status: "second_factor_required" };
37
+ }
38
+ if (errorId === "session_verified_address_required") {
39
+ return { status: "email_not_verified" };
40
+ }
41
+ if (errorId === "browser_location_change_required") {
42
+ return { status: "second_factor_required" };
43
+ }
44
+ return void 0;
45
+ }
46
+ async function verifyAgainstKratos(publicUrl, check, fetchImpl = fetch) {
47
+ const base = publicUrl.replace(/\/$/, "");
48
+ let flow;
49
+ try {
50
+ const started = await fetchImpl(`${base}/self-service/login/api`, {
51
+ method: "GET",
52
+ headers: { Accept: "application/json" },
53
+ signal: AbortSignal.timeout(FLOW_TIMEOUT_MS)
54
+ });
55
+ if (!started.ok) return { status: "unavailable" };
56
+ flow = await started.json();
57
+ if (!flow?.id) return { status: "unavailable" };
58
+ } catch {
59
+ return { status: "unavailable" };
60
+ }
61
+ const csrf = csrfTokenOf(flow);
62
+ try {
63
+ const submitted = await fetchImpl(
64
+ `${base}/self-service/login?flow=${encodeURIComponent(flow.id)}`,
65
+ {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
68
+ body: JSON.stringify({
69
+ method: "password",
70
+ identifier: check.email.trim().toLowerCase(),
71
+ password: check.password,
72
+ ...csrf ? { csrf_token: csrf } : {}
73
+ }),
74
+ signal: AbortSignal.timeout(FLOW_TIMEOUT_MS)
75
+ }
76
+ );
77
+ if (submitted.ok) {
78
+ const body2 = await submitted.json();
79
+ const identity = body2.session?.identity;
80
+ if (!identity?.id) return { status: "unavailable" };
81
+ if (identity.state && identity.state !== "active") {
82
+ return { status: "account_disabled" };
83
+ }
84
+ return { status: "valid", identityId: identity.id };
85
+ }
86
+ if (submitted.status >= 500) return { status: "unavailable" };
87
+ const body = await submitted.json().catch(() => ({}));
88
+ const mapped = outcomeFromMessages(body);
89
+ if (mapped) return mapped;
90
+ if (submitted.status === 400 || submitted.status === 401) {
91
+ return { status: "invalid_credentials" };
92
+ }
93
+ if (submitted.status === 403) return { status: "second_factor_required" };
94
+ return { status: "unavailable" };
95
+ } catch {
96
+ return { status: "unavailable" };
97
+ }
98
+ }
99
+
100
+ // src/identity-provisioning.ts
101
+ var REQUEST_TIMEOUT_MS = 5e3;
102
+ var TOKEN_EXPIRY_MARGIN_S = 30;
103
+ var tokenCache = /* @__PURE__ */ new Map();
104
+ async function accessToken(config, fetchImpl) {
105
+ const key = `${config.issuer}|${config.clientId}`;
106
+ const cached = tokenCache.get(key);
107
+ const now = Date.now() / 1e3;
108
+ if (cached && cached.expiresAt > now) return cached.value;
109
+ const base = config.issuer.replace(/\/$/, "");
110
+ const credentials = btoa(`${config.clientId}:${config.clientSecret}`);
111
+ try {
112
+ const response = await fetchImpl(`${base}/oauth2/token`, {
113
+ method: "POST",
114
+ headers: {
115
+ Authorization: `Basic ${credentials}`,
116
+ "Content-Type": "application/x-www-form-urlencoded"
117
+ },
118
+ body: new URLSearchParams({
119
+ grant_type: "client_credentials",
120
+ audience: "urbangate",
121
+ scope: "urbangate:identities:provision"
122
+ }).toString(),
123
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
124
+ });
125
+ if (!response.ok) return void 0;
126
+ const body = await response.json();
127
+ if (!body.access_token) return void 0;
128
+ tokenCache.set(key, {
129
+ value: body.access_token,
130
+ expiresAt: now + (body.expires_in ?? 900) - TOKEN_EXPIRY_MARGIN_S
131
+ });
132
+ return body.access_token;
133
+ } catch {
134
+ return void 0;
135
+ }
136
+ }
137
+ async function provisionIdentity(config, request, fetchImpl = fetch) {
138
+ const token = await accessToken(config, fetchImpl);
139
+ if (!token) return { status: "unavailable" };
140
+ const base = config.issuer.replace(/\/$/, "");
141
+ try {
142
+ const response = await fetchImpl(`${base}/api/machine/identities`, {
143
+ method: "POST",
144
+ headers: {
145
+ Authorization: `Bearer ${token}`,
146
+ "Content-Type": "application/json"
147
+ },
148
+ body: JSON.stringify({
149
+ email: request.email.trim().toLowerCase(),
150
+ email_verified: true,
151
+ name: request.name ?? "",
152
+ role: config.role,
153
+ product: config.product
154
+ }),
155
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
156
+ });
157
+ if (response.ok) {
158
+ const body2 = await response.json();
159
+ if (!body2.identity_id) return { status: "unavailable" };
160
+ return {
161
+ status: "provisioned",
162
+ identityId: body2.identity_id,
163
+ created: body2.created === true
164
+ };
165
+ }
166
+ if (response.status === 503 || response.status >= 500) {
167
+ return { status: "unavailable" };
168
+ }
169
+ if (response.status === 401) {
170
+ tokenCache.delete(`${config.issuer}|${config.clientId}`);
171
+ return { status: "unavailable" };
172
+ }
173
+ const body = await response.json().catch(() => ({}));
174
+ return {
175
+ status: "rejected",
176
+ reason: body.error ?? body.message ?? `http_${response.status}`
177
+ };
178
+ } catch {
179
+ return { status: "unavailable" };
180
+ }
181
+ }
182
+
19
183
  // src/google-defaults.ts
20
184
  function withGoogleDefaults(google) {
21
185
  if (typeof google === "function") return google;
@@ -140,7 +304,8 @@ function createPlatformAuth(config) {
140
304
  rateLimit,
141
305
  twoFactor: twoFactorConfig,
142
306
  trustedOrigins,
143
- sso
307
+ sso,
308
+ kratosPasswords
144
309
  } = config;
145
310
  const ssoProviderId = sso?.providerId ?? "urbangate";
146
311
  const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects };
@@ -153,7 +318,16 @@ function createPlatformAuth(config) {
153
318
  rateLimit: resolveRateLimit(rateLimit),
154
319
  emailAndPassword: {
155
320
  enabled: true,
156
- requireEmailVerification: true
321
+ requireEmailVerification: true,
322
+ ...kratosPasswords ? {
323
+ password: {
324
+ // Sign-in refuses before reaching the verifier when the account
325
+ // carries no hash, so a sign-up must still write one. It is a
326
+ // constant that validates nothing, never a hash of the password.
327
+ hash: async () => KRATOS_SENTINEL_HASH,
328
+ verify: kratosVerifier(kratosPasswords)
329
+ }
330
+ } : {}
157
331
  },
158
332
  // Never auto-merge a social identity into an existing account by matching
159
333
  // email. Better Auth links by default (email-verified providers are trusted),
@@ -166,6 +340,17 @@ function createPlatformAuth(config) {
166
340
  account: {
167
341
  accountLinking: sso ? { enabled: true, trustedProviders: [ssoProviderId] } : { enabled: false }
168
342
  },
343
+ ...kratosPasswords ? {
344
+ user: {
345
+ additionalFields: {
346
+ identityId: {
347
+ type: "string",
348
+ required: false,
349
+ input: false
350
+ }
351
+ }
352
+ }
353
+ } : {},
169
354
  // Naming happens here rather than on /sign-up/email so that every way in
170
355
  // is covered: a magic link that signs up bypasses the endpoint entirely
171
356
  // and calls createUser straight, with `name: name || ""`.
@@ -176,6 +361,10 @@ function createPlatformAuth(config) {
176
361
  ...databaseHooks?.user,
177
362
  create: {
178
363
  ...databaseHooks?.user?.create,
364
+ after: withIdentityProvisioning(
365
+ databaseHooks?.user?.create?.after,
366
+ kratosPasswords
367
+ ),
179
368
  before: async (user, ctx) => {
180
369
  const named = withSignUpName(user);
181
370
  const appHook = databaseHooks?.user?.create?.before;
@@ -192,6 +381,12 @@ function createPlatformAuth(config) {
192
381
  hooks: {
193
382
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
383
  before: async (ctx) => {
384
+ if (kratosPasswords && ctx.path === "/sign-in/email") {
385
+ const body2 = ctx.body;
386
+ if (body2?.email && body2?.password) {
387
+ rememberSignInIdentifier(body2.password, body2.email);
388
+ }
389
+ }
195
390
  if (!betaMode) return;
196
391
  if (ctx.path !== "/sign-up/email") return;
197
392
  const body = ctx.body;
@@ -298,6 +493,25 @@ function createPlatformAuth(config) {
298
493
  }
299
494
  });
300
495
  }
496
+ function withIdentityProvisioning(own, config) {
497
+ if (!config) return own;
498
+ return async (user, ctx) => {
499
+ await own?.(user, ctx);
500
+ const record = user;
501
+ if (!record.id || !record.email) return;
502
+ const outcome = await provisionIdentity(config, {
503
+ email: record.email,
504
+ name: record.name
505
+ });
506
+ if (outcome.status === "provisioned" && ctx) {
507
+ await ctx.context.internalAdapter.updateUser(record.id, {
508
+ identityId: outcome.identityId
509
+ });
510
+ return;
511
+ }
512
+ await config.onProvisioningDeferred?.({ userId: record.id, email: record.email });
513
+ };
514
+ }
301
515
  function withSsoRoleSync(hooks, sso, providerId) {
302
516
  if (!sso) return hooks;
303
517
  const sync = async (account, ctx) => {
@@ -316,7 +530,64 @@ function withSsoRoleSync(hooks, sso, providerId) {
316
530
  update: { ...hooks?.update, after: chain(hooks?.update?.after) }
317
531
  };
318
532
  }
533
+ var pendingIdentifiers = /* @__PURE__ */ new Map();
534
+ function rememberSignInIdentifier(password, email) {
535
+ pendingIdentifiers.set(password, email.trim().toLowerCase());
536
+ }
537
+ function takeSignInIdentifier(password) {
538
+ const email = pendingIdentifiers.get(password);
539
+ pendingIdentifiers.delete(password);
540
+ return email;
541
+ }
542
+ var KratosSignInError = class extends APIError {
543
+ constructor(status, code, message) {
544
+ super(status, { code, message });
545
+ }
546
+ };
547
+ function refusalFor(outcome) {
548
+ switch (outcome.status) {
549
+ case "no_credential":
550
+ return new KratosSignInError(
551
+ "FORBIDDEN",
552
+ "IDENTITY_HAS_NO_PASSWORD",
553
+ "This account has no password yet at the identity provider. Use the password recovery to set one."
554
+ );
555
+ case "second_factor_required":
556
+ return new KratosSignInError(
557
+ "FORBIDDEN",
558
+ "SECOND_FACTOR_REQUIRED",
559
+ "A second factor is required to sign in."
560
+ );
561
+ case "account_disabled":
562
+ return new KratosSignInError(
563
+ "FORBIDDEN",
564
+ "ACCOUNT_DISABLED",
565
+ "This account is deactivated."
566
+ );
567
+ case "unavailable":
568
+ return new KratosSignInError(
569
+ "SERVICE_UNAVAILABLE",
570
+ "IDENTITY_PROVIDER_UNAVAILABLE",
571
+ "The identity service is unavailable. Your password has not been refused \u2014 try again shortly."
572
+ );
573
+ default:
574
+ return void 0;
575
+ }
576
+ }
577
+ function kratosVerifier(config) {
578
+ return async ({ password }) => {
579
+ const email = takeSignInIdentifier(password);
580
+ if (!email) return false;
581
+ const outcome = await verifyAgainstKratos(config.publicUrl, { email, password });
582
+ if (outcome.status === "valid") return true;
583
+ const refusal = refusalFor(outcome);
584
+ if (refusal) throw refusal;
585
+ return false;
586
+ };
587
+ }
319
588
  export {
589
+ KRATOS_SENTINEL_HASH,
590
+ KratosSignInError,
320
591
  bootstrapFirstAdmin,
321
592
  claimInvitation,
322
593
  completesSignup,
@@ -325,8 +596,11 @@ export {
325
596
  invitationOutcomeCookie,
326
597
  inviteTokenFrom,
327
598
  isInvitationFailure,
599
+ isKratosSentinel,
328
600
  mapSsoProfile,
329
601
  pinInviteToken,
330
- releaseInviteTokenCookie
602
+ provisionIdentity,
603
+ releaseInviteTokenCookie,
604
+ rememberSignInIdentifier
331
605
  };
332
606
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/google-defaults.ts","../src/sso-endpoints.ts","../src/rate-limit.ts","../src/bootstrap-admin.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin, magicLink, twoFactor, genericOAuth } from \"better-auth/plugins\"\nimport type { PlatformAuthConfig, PlatformAuthMailerType, PlatformSsoConfig } from \"./types\"\nimport { withGoogleDefaults } from \"./google-defaults\"\nimport { mapSsoProfile, roleFromIdToken, type SsoProfile } from \"./sso-profile\"\nimport { ssoEndpoints } from \"./sso-endpoints\"\nimport { withSignUpName } from \"./signup-name\"\nimport { resolveRateLimit } from \"./rate-limit\"\n\nconst DEFAULT_EMAIL_SUBJECTS: Record<string, string> = {\n \"email-verification\": \"Verify your account\",\n \"forget-password\": \"Reset your password\",\n \"sign-in\": \"Your sign-in code\",\n}\n\nfunction defaultRenderOtpEmail(otp: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your verification code</h2>\n <p style=\"color:#555;margin-bottom:24px\">Use the code below to continue. It expires in 5 minutes.</p>\n <div style=\"background:#f5f5f5;border-radius:8px;padding:24px;text-align:center;letter-spacing:8px;font-size:32px;font-weight:700\">\n ${otp}\n </div>\n <p style=\"color:#999;font-size:12px;margin-top:24px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\nfunction defaultRenderMagicLinkEmail(url: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your sign-in link</h2>\n <p style=\"color:#555;margin-bottom:24px\">Click the button below to sign in. The link expires in 5 minutes and works once.</p>\n <a href=\"${url}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;border-radius:8px;padding:14px 28px;font-weight:600\">Sign in</a>\n <p style=\"color:#999;font-size:12px;margin-top:24px;word-break:break-all\">Or paste this address into your browser:<br>${url}</p>\n <p style=\"color:#999;font-size:12px;margin-top:16px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\n/**\n * Creates a Better Auth instance with platform defaults.\n * Each app calls this with its own config (DB, secret, providers, plugins).\n */\nexport function createPlatformAuth(\n config: PlatformAuthConfig,\n): Auth<BetterAuthOptions> {\n const {\n database,\n baseURL,\n secret,\n appName,\n mailer,\n google,\n github,\n plugins = [],\n databaseHooks,\n betaMode = false,\n isInvited,\n emailSubjects,\n renderOtpEmail,\n magicLink: magicLinkConfig,\n rateLimit,\n twoFactor: twoFactorConfig,\n trustedOrigins,\n sso,\n } = config\n const ssoProviderId = sso?.providerId ?? \"urbangate\"\n\n const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects }\n const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail\n\n // The concrete instance type (with email-otp/admin plugins) is widened to\n // the base Auth type so the published .d.ts stays portable (inferring the\n // full plugin type triggers TS2742 — it can't be named without a zod ref).\n // The admin() plugin's user.role field is re-exposed via module augmentation\n // below, so consumers (e.g. transcript-web me.ts) still see session.user.role.\n return betterAuth({\n database,\n baseURL,\n secret,\n ...(trustedOrigins ? { trustedOrigins } : {}),\n rateLimit: resolveRateLimit(rateLimit),\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n },\n // Never auto-merge a social identity into an existing account by matching\n // email. Better Auth links by default (email-verified providers are trusted),\n // so signing in with Google/GitHub on an email already registered would fold\n // that identity into the existing account. We keep each sign-in method its\n // own account: a social login on a taken email is refused, not linked.\n // The suite's own identity provider is the one exception: it verifies\n // emails itself, and the same person must land on the same account\n // whether they signed in here before the SSO existed or not.\n account: {\n accountLinking: sso\n ? { enabled: true, trustedProviders: [ssoProviderId] }\n : { enabled: false },\n },\n // Naming happens here rather than on /sign-up/email so that every way in\n // is covered: a magic link that signs up bypasses the endpoint entirely\n // and calls createUser straight, with `name: name || \"\"`.\n databaseHooks: {\n ...databaseHooks,\n account: withSsoRoleSync(databaseHooks?.account, sso, ssoProviderId),\n user: {\n ...databaseHooks?.user,\n create: {\n ...databaseHooks?.user?.create,\n before: async (user: Record<string, unknown>, ctx: unknown) => {\n const named = withSignUpName(user)\n const appHook = databaseHooks?.user?.create?.before\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const applied = await appHook?.(named as any, ctx as any)\n if (applied === false) return false\n if (applied && typeof applied === \"object\" && \"data\" in applied) {\n return { data: withSignUpName(applied.data) }\n }\n return { data: named }\n },\n },\n },\n },\n hooks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n before: async (ctx: any) => {\n if (!betaMode) return\n if (ctx.path !== \"/sign-up/email\") return\n const body = ctx.body as { email?: string; inviteToken?: string } | undefined\n const email = body?.email\n const inviteToken = body?.inviteToken\n if (email && inviteToken && isInvited) {\n const ok = await isInvited(email, inviteToken)\n if (ok) return\n }\n throw new APIError(\"FORBIDDEN\", {\n message: \"Registration is invite-only during the private beta.\",\n })\n },\n },\n plugins: [\n emailOTP({\n async sendVerificationOTP({ email, otp, type }) {\n const subject = subjects[type]\n ? `${subjects[type]} - ${appName}`\n : `Your ${appName} code`\n const html = renderEmail(otp, type as PlatformAuthMailerType)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: type as PlatformAuthMailerType,\n otp,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging OTP to stdout for ${email} (${type}): ${otp}`,\n )\n },\n otpLength: 6,\n expiresIn: 300,\n overrideDefaultEmailVerification: true,\n }),\n ...(magicLinkConfig\n ? [\n magicLink({\n expiresIn: magicLinkConfig.expiresIn ?? 300,\n // A magic link that signs up walks past both gates the platform\n // puts on the front door: requireEmailVerification, and the\n // invite-only hook, which only guards /sign-up/email.\n disableSignUp: !magicLinkConfig.allowSignUp,\n async sendMagicLink({ email, url }) {\n const subject =\n magicLinkConfig.subject ?? `Your sign-in link - ${appName}`\n const html = (\n magicLinkConfig.render ?? defaultRenderMagicLinkEmail\n )(url, email)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: \"magic-link\",\n url,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging magic link to stdout for ${email}: ${url}`,\n )\n },\n }),\n ]\n : []),\n admin(),\n ...(sso\n ? [\n genericOAuth({\n config: [\n {\n providerId: ssoProviderId,\n ...ssoEndpoints(sso.issuer),\n clientId: sso.clientId,\n clientSecret: sso.clientSecret,\n scopes: [\"openid\", \"email\", \"profile\", \"offline_access\"],\n pkce: true,\n overrideUserInfo: true,\n disableSignUp: sso.allowSignUp === false,\n mapProfileToUser: (profile) =>\n mapSsoProfile(profile as SsoProfile, sso.adminRole),\n },\n ],\n }),\n ]\n : []),\n ...(twoFactorConfig?.enabled\n ? [\n twoFactor({\n issuer: twoFactorConfig.issuer ?? appName,\n skipVerificationOnEnable:\n twoFactorConfig.skipVerificationOnEnable ?? false,\n }),\n ]\n : []),\n ...plugins, // app-specific plugins (e.g. tanstackStartCookies)\n ],\n socialProviders: {\n // Spread as given rather than rebuilt field by field: anything Better\n // Auth accepts belongs to the app, and a config silently dropped on the\n // way through is how an app ends up writing a plugin to put it back.\n //\n // The two defaults below are the platform's, not Google's: without\n // accessType 'offline' Google never mints a refresh token, and without\n // 'consent' it stops minting one for an account that already consented.\n // A NULL refreshToken means deleting an account can revoke the access\n // token but cannot remove the app from myaccount.google.com/permissions,\n // so the grant outlives the account it belonged to.\n ...(google ? { google: withGoogleDefaults(google) } : {}),\n ...(github ? { github } : {}),\n },\n }) as unknown as Auth<BetterAuthOptions>\n}\n\ntype AccountHooks = NonNullable<NonNullable<BetterAuthOptions[\"databaseHooks\"]>[\"account\"]>\ntype AccountAfterHook = NonNullable<NonNullable<AccountHooks[\"create\"]>[\"after\"]>\n\n// The admin plugin declares `role` as not settable from input, so the role\n// mapProfileToUser returns is dropped when the OAuth path creates the user.\n// The account row, created then refreshed on every sign-in, carries the ID\n// token: its roles claim is what sets the local role, each time.\nfunction withSsoRoleSync(\n hooks: AccountHooks | undefined,\n sso: PlatformSsoConfig | undefined,\n providerId: string,\n): AccountHooks | undefined {\n if (!sso) return hooks\n const sync: AccountAfterHook = async (account, ctx) => {\n if (account.providerId !== providerId || !ctx) return\n const role = roleFromIdToken(account.idToken, sso.adminRole)\n if (!role) return\n await ctx.context.internalAdapter.updateUser(account.userId, { role })\n }\n const chain =\n (own: AccountAfterHook | undefined): AccountAfterHook =>\n async (account, ctx) => {\n await own?.(account, ctx)\n await sync(account, ctx)\n }\n return {\n ...hooks,\n create: { ...hooks?.create, after: chain(hooks?.create?.after) },\n update: { ...hooks?.update, after: chain(hooks?.update?.after) },\n }\n}\n\nexport type PlatformAuth = ReturnType<typeof createPlatformAuth>\n\n// Re-export the session contract from /server so consumers that import the\n// auth factory can type api.getSession() without a second import path.\nexport type {\n PlatformUser,\n PlatformSession,\n PlatformSessionData,\n PlatformRateLimitConfig,\n PlatformRateLimitRule,\n PlatformTwoFactorConfig,\n} from \"./types\"\n\n// Invitation claiming runs on the auth callback, where the session is\n// established — the one place every sign-up flow passes through.\nexport {\n claimInvitation,\n completesSignup,\n holdInviteTokenCookie,\n invitationOutcomeCookie,\n inviteTokenFrom,\n isInvitationFailure,\n pinInviteToken,\n releaseInviteTokenCookie,\n} from \"./invitation\"\nexport type { ClaimOutcome, ClaimInvitationOptions } from \"./invitation\"\n\nexport { mapSsoProfile } from \"./sso-profile\"\nexport type { SsoProfile, SsoMappedUser } from \"./sso-profile\"\n\nexport { bootstrapFirstAdmin } from \"./bootstrap-admin\"\nexport type {\n BootstrapAdminPool,\n BootstrapAdminClient,\n BootstrapFirstAdminInput,\n BootstrapFirstAdminResult,\n} from \"./bootstrap-admin\"\n","import type { PlatformAuthConfig } from \"./types\"\n\n/**\n * Applies the platform's Google defaults without touching what the app set.\n *\n * Better Auth also accepts a function returning the options, which cannot be\n * amended without calling it — such a config is passed straight through and\n * owns its own defaults.\n */\nexport function withGoogleDefaults(\n google: NonNullable<PlatformAuthConfig[\"google\"]>,\n): NonNullable<PlatformAuthConfig[\"google\"]> {\n if (typeof google === \"function\") return google\n return {\n ...google,\n accessType: google.accessType ?? \"offline\",\n prompt: google.prompt ?? \"select_account consent\",\n }\n}\n\n","export interface SsoEndpoints {\n discoveryUrl: string\n accountIssuer: string\n authorizationUrl: string\n tokenUrl: string\n userInfoUrl: string\n}\n\n/**\n * Hydra's endpoints, derived from the issuer so that the provider comes up\n * without a discovery fetch: Better Auth refuses to initialise a provider\n * whose discovery failed unless it already knows the account issuer and the\n * endpoints, and that failure would take the whole app down at boot.\n */\nexport function ssoEndpoints(issuer: string): SsoEndpoints {\n const base = issuer.replace(/\\/$/, \"\")\n return {\n discoveryUrl: `${base}/.well-known/openid-configuration`,\n accountIssuer: base,\n authorizationUrl: `${base}/oauth2/auth`,\n tokenUrl: `${base}/oauth2/token`,\n userInfoUrl: `${base}/userinfo`,\n }\n}\n","import type { PlatformRateLimitConfig, PlatformRateLimitRule } from \"./types\"\n\n// Better Auth keys its own rate limiter on NODE_ENV === \"production\", so a\n// deployment that forgets the variable serves sign-in with no brute-force\n// protection and nothing reports it. These are the platform's rules, applied\n// regardless of the environment.\nconst PLATFORM_RULES: Record<string, PlatformRateLimitRule> = {\n \"/sign-in/email\": { window: 60, max: 5 },\n \"/sign-up/email\": { window: 60, max: 5 },\n \"/email-otp/send-verification-otp\": { window: 60, max: 3 },\n \"/email-otp/verify-email\": { window: 60, max: 5 },\n \"/email-otp/reset-password\": { window: 60, max: 5 },\n \"/forget-password\": { window: 60, max: 3 },\n \"/reset-password\": { window: 60, max: 5 },\n \"/sign-in/magic-link\": { window: 60, max: 3 },\n \"/two-factor/verify-totp\": { window: 60, max: 5 },\n \"/two-factor/verify-otp\": { window: 60, max: 5 },\n \"/two-factor/verify-backup-code\": { window: 60, max: 5 },\n \"/two-factor/send-otp\": { window: 60, max: 3 },\n}\n\nexport interface ResolvedRateLimit {\n enabled: boolean\n window: number\n max: number\n storage?: \"memory\" | \"database\" | \"secondary-storage\"\n modelName?: string\n customRules: Record<string, PlatformRateLimitRule>\n}\n\nexport function resolveRateLimit(\n config?: PlatformRateLimitConfig,\n): ResolvedRateLimit {\n return {\n enabled: config?.enabled ?? true,\n window: config?.window ?? 10,\n max: config?.max ?? 100,\n ...(config?.storage ? { storage: config.storage } : {}),\n ...(config?.modelName ? { modelName: config.modelName } : {}),\n customRules: { ...PLATFORM_RULES, ...config?.customRules },\n }\n}\n\nexport { PLATFORM_RULES }\n","import type { PlatformAuth } from \"./server\"\n\n/**\n * Minimal Postgres pool contract this module needs — kept structural so the\n * package doesn't pull in `pg` as a dependency; any `pg.Pool` satisfies it.\n */\nexport interface BootstrapAdminPool {\n connect(): Promise<BootstrapAdminClient>\n}\n\nexport interface BootstrapAdminClient {\n query(text: string, values?: unknown[]): Promise<{ rowCount: number | null }>\n release(): void\n}\n\nexport interface BootstrapFirstAdminInput {\n email: string\n password: string\n name: string\n}\n\nexport type BootstrapFirstAdminResult =\n | { ok: true }\n | { ok: false; error: \"already_completed\" }\n\n// The admin() plugin's createUser endpoint isn't in PlatformAuth's published\n// type (see the widening note on createPlatformAuth in ./server), even though\n// it's mounted at runtime by every app that enables admin().\ninterface AuthWithAdminCreateUser {\n api: {\n createUser: (input: {\n body: {\n email: string\n password: string\n name: string\n role: string\n data?: Record<string, unknown>\n }\n }) => Promise<unknown>\n }\n}\n\n/**\n * Creates the very first admin for an app, before any admin exists — the one\n * case the admin() plugin's own `createUser` endpoint can't cover, since it\n * requires an already-authenticated admin session to call.\n *\n * Delegates the actual user/account creation to `auth.api.createUser` (called\n * server-side, with no request/session, so the plugin's own auth check is\n * skipped the same way a trusted server script would be) instead of inserting\n * `user`/`account` rows by hand — that keeps this in sync with whatever\n * Better Auth's internal adapter does (account.issuer, password hashing,\n * future schema changes) rather than re-deriving it and letting the two\n * drift apart.\n *\n * Serializes concurrent callers with a Postgres advisory lock: a plain\n * `WHERE NOT EXISTS` check on an empty `user` table lets two racing requests\n * both pass before either has inserted, minting two admins.\n *\n * Callers are expected to have already applied their own access gate (setup\n * token, allowed-emails list, etc.) — this function only enforces \"at most\n * one admin, ever\".\n */\nexport async function bootstrapFirstAdmin(\n auth: PlatformAuth,\n pool: BootstrapAdminPool,\n input: BootstrapFirstAdminInput,\n): Promise<BootstrapFirstAdminResult> {\n const client = await pool.connect()\n try {\n await client.query(\"SELECT pg_advisory_lock(hashtext($1))\", [\"admin-setup\"])\n\n const existing = await client.query(\n `SELECT 1 FROM \"user\" WHERE role = 'admin'`,\n )\n if (existing.rowCount && existing.rowCount > 0) {\n return { ok: false, error: \"already_completed\" }\n }\n\n const adminApi = (auth as unknown as AuthWithAdminCreateUser).api\n await adminApi.createUser({\n body: {\n email: input.email,\n password: input.password,\n name: input.name,\n role: \"admin\",\n data: { emailVerified: true },\n },\n })\n\n return { ok: true }\n } finally {\n await client.query(\"SELECT pg_advisory_unlock(hashtext($1))\", [\"admin-setup\"])\n client.release()\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts","../src/kratos-credentials.ts","../src/identity-provisioning.ts","../src/google-defaults.ts","../src/sso-endpoints.ts","../src/rate-limit.ts","../src/bootstrap-admin.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin, magicLink, twoFactor, genericOAuth } from \"better-auth/plugins\"\nimport type {\n PlatformAuthConfig,\n PlatformAuthMailerType,\n PlatformKratosPasswordConfig,\n PlatformSsoConfig,\n} from \"./types\"\nimport {\n KRATOS_SENTINEL_HASH,\n verifyAgainstKratos,\n type KratosOutcome,\n} from \"./kratos-credentials\"\nimport { provisionIdentity } from \"./identity-provisioning\"\nimport { withGoogleDefaults } from \"./google-defaults\"\nimport { mapSsoProfile, roleFromIdToken, type SsoProfile } from \"./sso-profile\"\nimport { ssoEndpoints } from \"./sso-endpoints\"\nimport { withSignUpName } from \"./signup-name\"\nimport { resolveRateLimit } from \"./rate-limit\"\n\nconst DEFAULT_EMAIL_SUBJECTS: Record<string, string> = {\n \"email-verification\": \"Verify your account\",\n \"forget-password\": \"Reset your password\",\n \"sign-in\": \"Your sign-in code\",\n}\n\nfunction defaultRenderOtpEmail(otp: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your verification code</h2>\n <p style=\"color:#555;margin-bottom:24px\">Use the code below to continue. It expires in 5 minutes.</p>\n <div style=\"background:#f5f5f5;border-radius:8px;padding:24px;text-align:center;letter-spacing:8px;font-size:32px;font-weight:700\">\n ${otp}\n </div>\n <p style=\"color:#999;font-size:12px;margin-top:24px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\nfunction defaultRenderMagicLinkEmail(url: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your sign-in link</h2>\n <p style=\"color:#555;margin-bottom:24px\">Click the button below to sign in. The link expires in 5 minutes and works once.</p>\n <a href=\"${url}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;border-radius:8px;padding:14px 28px;font-weight:600\">Sign in</a>\n <p style=\"color:#999;font-size:12px;margin-top:24px;word-break:break-all\">Or paste this address into your browser:<br>${url}</p>\n <p style=\"color:#999;font-size:12px;margin-top:16px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\n/**\n * Creates a Better Auth instance with platform defaults.\n * Each app calls this with its own config (DB, secret, providers, plugins).\n */\nexport function createPlatformAuth(\n config: PlatformAuthConfig,\n): Auth<BetterAuthOptions> {\n const {\n database,\n baseURL,\n secret,\n appName,\n mailer,\n google,\n github,\n plugins = [],\n databaseHooks,\n betaMode = false,\n isInvited,\n emailSubjects,\n renderOtpEmail,\n magicLink: magicLinkConfig,\n rateLimit,\n twoFactor: twoFactorConfig,\n trustedOrigins,\n sso,\n kratosPasswords,\n } = config\n const ssoProviderId = sso?.providerId ?? \"urbangate\"\n\n const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects }\n const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail\n\n // The concrete instance type (with email-otp/admin plugins) is widened to\n // the base Auth type so the published .d.ts stays portable (inferring the\n // full plugin type triggers TS2742 — it can't be named without a zod ref).\n // The admin() plugin's user.role field is re-exposed via module augmentation\n // below, so consumers (e.g. transcript-web me.ts) still see session.user.role.\n return betterAuth({\n database,\n baseURL,\n secret,\n ...(trustedOrigins ? { trustedOrigins } : {}),\n rateLimit: resolveRateLimit(rateLimit),\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n ...(kratosPasswords\n ? {\n password: {\n // Sign-in refuses before reaching the verifier when the account\n // carries no hash, so a sign-up must still write one. It is a\n // constant that validates nothing, never a hash of the password.\n hash: async () => KRATOS_SENTINEL_HASH,\n verify: kratosVerifier(kratosPasswords),\n },\n }\n : {}),\n },\n // Never auto-merge a social identity into an existing account by matching\n // email. Better Auth links by default (email-verified providers are trusted),\n // so signing in with Google/GitHub on an email already registered would fold\n // that identity into the existing account. We keep each sign-in method its\n // own account: a social login on a taken email is refused, not linked.\n // The suite's own identity provider is the one exception: it verifies\n // emails itself, and the same person must land on the same account\n // whether they signed in here before the SSO existed or not.\n account: {\n accountLinking: sso\n ? { enabled: true, trustedProviders: [ssoProviderId] }\n : { enabled: false },\n },\n ...(kratosPasswords\n ? {\n user: {\n additionalFields: {\n identityId: {\n type: \"string\",\n required: false,\n input: false,\n },\n },\n },\n }\n : {}),\n // Naming happens here rather than on /sign-up/email so that every way in\n // is covered: a magic link that signs up bypasses the endpoint entirely\n // and calls createUser straight, with `name: name || \"\"`.\n databaseHooks: {\n ...databaseHooks,\n account: withSsoRoleSync(databaseHooks?.account, sso, ssoProviderId),\n user: {\n ...databaseHooks?.user,\n create: {\n ...databaseHooks?.user?.create,\n after: withIdentityProvisioning(\n databaseHooks?.user?.create?.after,\n kratosPasswords,\n ),\n before: async (user: Record<string, unknown>, ctx: unknown) => {\n const named = withSignUpName(user)\n const appHook = databaseHooks?.user?.create?.before\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const applied = await appHook?.(named as any, ctx as any)\n if (applied === false) return false\n if (applied && typeof applied === \"object\" && \"data\" in applied) {\n return { data: withSignUpName(applied.data) }\n }\n return { data: named }\n },\n },\n },\n },\n hooks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n before: async (ctx: any) => {\n if (kratosPasswords && ctx.path === \"/sign-in/email\") {\n const body = ctx.body as { email?: string; password?: string } | undefined\n if (body?.email && body?.password) {\n rememberSignInIdentifier(body.password, body.email)\n }\n }\n if (!betaMode) return\n if (ctx.path !== \"/sign-up/email\") return\n const body = ctx.body as { email?: string; inviteToken?: string } | undefined\n const email = body?.email\n const inviteToken = body?.inviteToken\n if (email && inviteToken && isInvited) {\n const ok = await isInvited(email, inviteToken)\n if (ok) return\n }\n throw new APIError(\"FORBIDDEN\", {\n message: \"Registration is invite-only during the private beta.\",\n })\n },\n },\n plugins: [\n emailOTP({\n async sendVerificationOTP({ email, otp, type }) {\n const subject = subjects[type]\n ? `${subjects[type]} - ${appName}`\n : `Your ${appName} code`\n const html = renderEmail(otp, type as PlatformAuthMailerType)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: type as PlatformAuthMailerType,\n otp,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging OTP to stdout for ${email} (${type}): ${otp}`,\n )\n },\n otpLength: 6,\n expiresIn: 300,\n overrideDefaultEmailVerification: true,\n }),\n ...(magicLinkConfig\n ? [\n magicLink({\n expiresIn: magicLinkConfig.expiresIn ?? 300,\n // A magic link that signs up walks past both gates the platform\n // puts on the front door: requireEmailVerification, and the\n // invite-only hook, which only guards /sign-up/email.\n disableSignUp: !magicLinkConfig.allowSignUp,\n async sendMagicLink({ email, url }) {\n const subject =\n magicLinkConfig.subject ?? `Your sign-in link - ${appName}`\n const html = (\n magicLinkConfig.render ?? defaultRenderMagicLinkEmail\n )(url, email)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: \"magic-link\",\n url,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging magic link to stdout for ${email}: ${url}`,\n )\n },\n }),\n ]\n : []),\n admin(),\n ...(sso\n ? [\n genericOAuth({\n config: [\n {\n providerId: ssoProviderId,\n ...ssoEndpoints(sso.issuer),\n clientId: sso.clientId,\n clientSecret: sso.clientSecret,\n scopes: [\"openid\", \"email\", \"profile\", \"offline_access\"],\n pkce: true,\n overrideUserInfo: true,\n disableSignUp: sso.allowSignUp === false,\n mapProfileToUser: (profile) =>\n mapSsoProfile(profile as SsoProfile, sso.adminRole),\n },\n ],\n }),\n ]\n : []),\n ...(twoFactorConfig?.enabled\n ? [\n twoFactor({\n issuer: twoFactorConfig.issuer ?? appName,\n skipVerificationOnEnable:\n twoFactorConfig.skipVerificationOnEnable ?? false,\n }),\n ]\n : []),\n ...plugins, // app-specific plugins (e.g. tanstackStartCookies)\n ],\n socialProviders: {\n // Spread as given rather than rebuilt field by field: anything Better\n // Auth accepts belongs to the app, and a config silently dropped on the\n // way through is how an app ends up writing a plugin to put it back.\n //\n // The two defaults below are the platform's, not Google's: without\n // accessType 'offline' Google never mints a refresh token, and without\n // 'consent' it stops minting one for an account that already consented.\n // A NULL refreshToken means deleting an account can revoke the access\n // token but cannot remove the app from myaccount.google.com/permissions,\n // so the grant outlives the account it belonged to.\n ...(google ? { google: withGoogleDefaults(google) } : {}),\n ...(github ? { github } : {}),\n },\n }) as unknown as Auth<BetterAuthOptions>\n}\n\ntype UserHooks = NonNullable<NonNullable<BetterAuthOptions[\"databaseHooks\"]>[\"user\"]>\ntype UserAfterHook = NonNullable<NonNullable<UserHooks[\"create\"]>[\"after\"]>\n\n/**\n * Gives the new local user an identity at the provider and stores its id.\n *\n * This runs after the insert commits, so it cannot be atomic with the\n * sign-up: a provider that is down leaves `identityId` null and the person\n * registered all the same. The endpoint is idempotent on the address, so the\n * repair re-sends without risking a second identity.\n */\nfunction withIdentityProvisioning(\n own: UserAfterHook | undefined,\n config: PlatformKratosPasswordConfig | undefined,\n): UserAfterHook | undefined {\n if (!config) return own\n return async (user, ctx) => {\n await own?.(user, ctx)\n const record = user as { id?: string; email?: string; name?: string }\n if (!record.id || !record.email) return\n\n const outcome = await provisionIdentity(config, {\n email: record.email,\n name: record.name,\n })\n\n if (outcome.status === \"provisioned\" && ctx) {\n await ctx.context.internalAdapter.updateUser(record.id, {\n identityId: outcome.identityId,\n })\n return\n }\n\n await config.onProvisioningDeferred?.({ userId: record.id, email: record.email })\n }\n}\n\ntype AccountHooks = NonNullable<NonNullable<BetterAuthOptions[\"databaseHooks\"]>[\"account\"]>\ntype AccountAfterHook = NonNullable<NonNullable<AccountHooks[\"create\"]>[\"after\"]>\n\n// The admin plugin declares `role` as not settable from input, so the role\n// mapProfileToUser returns is dropped when the OAuth path creates the user.\n// The account row, created then refreshed on every sign-in, carries the ID\n// token: its roles claim is what sets the local role, each time.\nfunction withSsoRoleSync(\n hooks: AccountHooks | undefined,\n sso: PlatformSsoConfig | undefined,\n providerId: string,\n): AccountHooks | undefined {\n if (!sso) return hooks\n const sync: AccountAfterHook = async (account, ctx) => {\n if (account.providerId !== providerId || !ctx) return\n const role = roleFromIdToken(account.idToken, sso.adminRole)\n if (!role) return\n await ctx.context.internalAdapter.updateUser(account.userId, { role })\n }\n const chain =\n (own: AccountAfterHook | undefined): AccountAfterHook =>\n async (account, ctx) => {\n await own?.(account, ctx)\n await sync(account, ctx)\n }\n return {\n ...hooks,\n create: { ...hooks?.create, after: chain(hooks?.create?.after) },\n update: { ...hooks?.update, after: chain(hooks?.update?.after) },\n }\n}\n\n/**\n * Better Auth's verifier is handed the stored hash and the submitted password,\n * never the address, and the same verifier serves sign-in, password change and\n * account deletion. The address of the sign-in being processed is carried here\n * by the route hook, keyed by the submitted password so two concurrent\n * sign-ins cannot read each other's.\n */\nconst pendingIdentifiers = new Map<string, string>()\n\nexport function rememberSignInIdentifier(password: string, email: string): void {\n pendingIdentifiers.set(password, email.trim().toLowerCase())\n}\n\nfunction takeSignInIdentifier(password: string): string | undefined {\n const email = pendingIdentifiers.get(password)\n pendingIdentifiers.delete(password)\n return email\n}\n\nexport class KratosSignInError extends APIError {\n constructor(status: \"UNAUTHORIZED\" | \"FORBIDDEN\" | \"SERVICE_UNAVAILABLE\", code: string, message: string) {\n super(status, { code, message })\n }\n}\n\nfunction refusalFor(outcome: KratosOutcome): KratosSignInError | undefined {\n switch (outcome.status) {\n case \"no_credential\":\n return new KratosSignInError(\n \"FORBIDDEN\",\n \"IDENTITY_HAS_NO_PASSWORD\",\n \"This account has no password yet at the identity provider. Use the password recovery to set one.\",\n )\n case \"second_factor_required\":\n return new KratosSignInError(\n \"FORBIDDEN\",\n \"SECOND_FACTOR_REQUIRED\",\n \"A second factor is required to sign in.\",\n )\n case \"account_disabled\":\n return new KratosSignInError(\n \"FORBIDDEN\",\n \"ACCOUNT_DISABLED\",\n \"This account is deactivated.\",\n )\n case \"unavailable\":\n return new KratosSignInError(\n \"SERVICE_UNAVAILABLE\",\n \"IDENTITY_PROVIDER_UNAVAILABLE\",\n \"The identity service is unavailable. Your password has not been refused — try again shortly.\",\n )\n default:\n return undefined\n }\n}\n\n/**\n * Fails closed: anything other than an explicit success refuses the sign-in,\n * and only an explicit refusal by Kratos reads as a wrong password. An outage\n * answers 503, so nobody is told their password is wrong and rotates a\n * password that was right.\n */\nfunction kratosVerifier(config: PlatformKratosPasswordConfig) {\n return async ({ password }: { hash: string; password: string }): Promise<boolean> => {\n const email = takeSignInIdentifier(password)\n if (!email) return false\n const outcome = await verifyAgainstKratos(config.publicUrl, { email, password })\n if (outcome.status === \"valid\") return true\n const refusal = refusalFor(outcome)\n if (refusal) throw refusal\n return false\n }\n}\n\nexport type PlatformAuth = ReturnType<typeof createPlatformAuth>\n\n// Re-export the session contract from /server so consumers that import the\n// auth factory can type api.getSession() without a second import path.\nexport type {\n PlatformUser,\n PlatformSession,\n PlatformSessionData,\n PlatformRateLimitConfig,\n PlatformRateLimitRule,\n PlatformTwoFactorConfig,\n} from \"./types\"\n\n// Invitation claiming runs on the auth callback, where the session is\n// established — the one place every sign-up flow passes through.\nexport {\n claimInvitation,\n completesSignup,\n holdInviteTokenCookie,\n invitationOutcomeCookie,\n inviteTokenFrom,\n isInvitationFailure,\n pinInviteToken,\n releaseInviteTokenCookie,\n} from \"./invitation\"\nexport type { ClaimOutcome, ClaimInvitationOptions } from \"./invitation\"\n\nexport { mapSsoProfile } from \"./sso-profile\"\nexport type { SsoProfile, SsoMappedUser } from \"./sso-profile\"\n\nexport { KRATOS_SENTINEL_HASH, isKratosSentinel } from \"./kratos-credentials\"\nexport type { KratosOutcome } from \"./kratos-credentials\"\n\n// The repair path an app schedules for the sign-ups whose provisioning could\n// not reach the provider.\nexport { provisionIdentity } from \"./identity-provisioning\"\nexport type {\n IdentityProvisioningConfig,\n ProvisionOutcome,\n} from \"./identity-provisioning\"\n\nexport { bootstrapFirstAdmin } from \"./bootstrap-admin\"\nexport type {\n BootstrapAdminPool,\n BootstrapAdminClient,\n BootstrapFirstAdminInput,\n BootstrapFirstAdminResult,\n} from \"./bootstrap-admin\"\n","/**\n * The value written in place of a password hash once Kratos holds the\n * password. Better Auth's sign-in route refuses the request before reaching\n * the verifier when the credential account carries no hash, so a row must\n * exist and be non-empty for the Kratos check to run at all.\n *\n * It is not a hash and cannot become one: argon2/bcrypt/scrypt verification\n * of this string fails on its format, so a build that ever bypassed the\n * custom verifier refuses everyone instead of admitting anyone.\n */\nexport const KRATOS_SENTINEL_HASH = \"kratos:external-credential:no-local-hash\"\n\nexport function isKratosSentinel(hash: string | null | undefined): boolean {\n return hash === KRATOS_SENTINEL_HASH\n}\n\nexport type KratosOutcome =\n | { status: \"valid\"; identityId: string }\n | { status: \"invalid_credentials\" }\n | { status: \"no_credential\" }\n | { status: \"email_not_verified\" }\n | { status: \"second_factor_required\" }\n | { status: \"account_disabled\" }\n | { status: \"unavailable\" }\n\nexport interface KratosCredentialCheck {\n email: string\n password: string\n}\n\ninterface KratosLoginFlow {\n id: string\n ui?: { nodes?: Array<{ attributes?: { name?: string; value?: string } }> }\n}\n\ninterface KratosErrorBody {\n error?: { id?: string; code?: number; reason?: string; message?: string }\n ui?: { messages?: Array<{ id?: number; text?: string; type?: string }> }\n redirect_browser_to?: string\n}\n\ninterface KratosSuccessBody {\n session?: { identity?: { id?: string; state?: string } }\n session_token?: string\n}\n\nconst FLOW_TIMEOUT_MS = 5000\n\nfunction csrfTokenOf(flow: KratosLoginFlow): string | undefined {\n return flow.ui?.nodes?.find((n) => n.attributes?.name === \"csrf_token\")\n ?.attributes?.value\n}\n\n/**\n * Kratos reports a refusal through numbered UI messages rather than a status\n * code of its own; 4000006 is the generic credential refusal, 4000010 an\n * inactive account, 4000002 a missing field. The address-unverified and\n * second-factor cases arrive as a redirect or an aal2 requirement instead.\n */\nfunction outcomeFromMessages(body: KratosErrorBody): KratosOutcome | undefined {\n const ids = (body.ui?.messages ?? []).map((m) => m.id)\n if (ids.includes(4000010)) return { status: \"account_disabled\" }\n if (ids.includes(4000006) || ids.includes(4000002)) {\n return { status: \"invalid_credentials\" }\n }\n const errorId = body.error?.id\n if (errorId === \"session_aal2_required\") {\n return { status: \"second_factor_required\" }\n }\n if (errorId === \"session_verified_address_required\") {\n return { status: \"email_not_verified\" }\n }\n if (errorId === \"browser_location_change_required\") {\n return { status: \"second_factor_required\" }\n }\n return undefined\n}\n\n/**\n * Validates a password against Kratos through the native (API) login flow,\n * which returns the outcome as JSON instead of driving a browser.\n *\n * Every failure that is not an explicit refusal by Kratos answers\n * `unavailable`, never `invalid_credentials`: a person told their password is\n * wrong during an outage changes a password that was right.\n */\nexport async function verifyAgainstKratos(\n publicUrl: string,\n check: KratosCredentialCheck,\n fetchImpl: typeof fetch = fetch,\n): Promise<KratosOutcome> {\n const base = publicUrl.replace(/\\/$/, \"\")\n let flow: KratosLoginFlow\n try {\n const started = await fetchImpl(`${base}/self-service/login/api`, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n signal: AbortSignal.timeout(FLOW_TIMEOUT_MS),\n })\n if (!started.ok) return { status: \"unavailable\" }\n flow = (await started.json()) as KratosLoginFlow\n if (!flow?.id) return { status: \"unavailable\" }\n } catch {\n return { status: \"unavailable\" }\n }\n\n const csrf = csrfTokenOf(flow)\n try {\n const submitted = await fetchImpl(\n `${base}/self-service/login?flow=${encodeURIComponent(flow.id)}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n body: JSON.stringify({\n method: \"password\",\n identifier: check.email.trim().toLowerCase(),\n password: check.password,\n ...(csrf ? { csrf_token: csrf } : {}),\n }),\n signal: AbortSignal.timeout(FLOW_TIMEOUT_MS),\n },\n )\n\n if (submitted.ok) {\n const body = (await submitted.json()) as KratosSuccessBody\n const identity = body.session?.identity\n if (!identity?.id) return { status: \"unavailable\" }\n if (identity.state && identity.state !== \"active\") {\n return { status: \"account_disabled\" }\n }\n return { status: \"valid\", identityId: identity.id }\n }\n\n if (submitted.status >= 500) return { status: \"unavailable\" }\n\n const body = (await submitted.json().catch(() => ({}))) as KratosErrorBody\n const mapped = outcomeFromMessages(body)\n if (mapped) return mapped\n if (submitted.status === 400 || submitted.status === 401) {\n return { status: \"invalid_credentials\" }\n }\n if (submitted.status === 403) return { status: \"second_factor_required\" }\n return { status: \"unavailable\" }\n } catch {\n return { status: \"unavailable\" }\n }\n}\n","export interface IdentityProvisioningConfig {\n /** urbangate's issuer URL, e.g. https://id.urbangate.dev */\n issuer: string\n /** The product's provisioner client, e.g. \"spore-provisioner\". */\n clientId: string\n clientSecret: string\n /** The role granted on provisioning, e.g. \"spore:user\". */\n role: string\n /** The product id carried in the request, e.g. \"spore\". */\n product: string\n}\n\nexport type ProvisionOutcome =\n | { status: \"provisioned\"; identityId: string; created: boolean }\n | { status: \"rejected\"; reason: string }\n | { status: \"unavailable\" }\n\nexport interface ProvisionRequest {\n email: string\n name?: string\n}\n\ninterface TokenResponse {\n access_token?: string\n expires_in?: number\n}\n\ninterface IdentityResponse {\n identity_id?: string\n created?: boolean\n}\n\nconst REQUEST_TIMEOUT_MS = 5000\nconst TOKEN_EXPIRY_MARGIN_S = 30\n\ninterface CachedToken {\n value: string\n expiresAt: number\n}\n\nconst tokenCache = new Map<string, CachedToken>()\n\nexport function resetProvisioningTokenCache(): void {\n tokenCache.clear()\n}\n\nasync function accessToken(\n config: IdentityProvisioningConfig,\n fetchImpl: typeof fetch,\n): Promise<string | undefined> {\n const key = `${config.issuer}|${config.clientId}`\n const cached = tokenCache.get(key)\n const now = Date.now() / 1000\n if (cached && cached.expiresAt > now) return cached.value\n\n const base = config.issuer.replace(/\\/$/, \"\")\n const credentials = btoa(`${config.clientId}:${config.clientSecret}`)\n try {\n const response = await fetchImpl(`${base}/oauth2/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Basic ${credentials}`,\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n audience: \"urbangate\",\n scope: \"urbangate:identities:provision\",\n }).toString(),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n if (!response.ok) return undefined\n const body = (await response.json()) as TokenResponse\n if (!body.access_token) return undefined\n tokenCache.set(key, {\n value: body.access_token,\n expiresAt: now + (body.expires_in ?? 900) - TOKEN_EXPIRY_MARGIN_S,\n })\n return body.access_token\n } catch {\n return undefined\n }\n}\n\n/**\n * Creates or joins the person's identity at urbangate, returning the id the\n * local user row stores.\n *\n * The endpoint is idempotent on the address: a person who already has an\n * identity through another product of the suite gets that same one, with this\n * product's role added. The products therefore never own the identity, only\n * their role on it — a product deleting its local account must drop its role,\n * never deactivate the identity, or it would sign the person out of every\n * other product of the suite.\n */\nexport async function provisionIdentity(\n config: IdentityProvisioningConfig,\n request: ProvisionRequest,\n fetchImpl: typeof fetch = fetch,\n): Promise<ProvisionOutcome> {\n const token = await accessToken(config, fetchImpl)\n if (!token) return { status: \"unavailable\" }\n\n const base = config.issuer.replace(/\\/$/, \"\")\n try {\n const response = await fetchImpl(`${base}/api/machine/identities`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n email: request.email.trim().toLowerCase(),\n email_verified: true,\n name: request.name ?? \"\",\n role: config.role,\n product: config.product,\n }),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n\n if (response.ok) {\n const body = (await response.json()) as IdentityResponse\n if (!body.identity_id) return { status: \"unavailable\" }\n return {\n status: \"provisioned\",\n identityId: body.identity_id,\n created: body.created === true,\n }\n }\n\n // 503 is the endpoint's retryable answer, including the inconclusive\n // lookup that refuses to risk a duplicate identity.\n if (response.status === 503 || response.status >= 500) {\n return { status: \"unavailable\" }\n }\n if (response.status === 401) {\n tokenCache.delete(`${config.issuer}|${config.clientId}`)\n return { status: \"unavailable\" }\n }\n const body = (await response.json().catch(() => ({}))) as {\n error?: string\n message?: string\n }\n return {\n status: \"rejected\",\n reason: body.error ?? body.message ?? `http_${response.status}`,\n }\n } catch {\n return { status: \"unavailable\" }\n }\n}\n","import type { PlatformAuthConfig } from \"./types\"\n\n/**\n * Applies the platform's Google defaults without touching what the app set.\n *\n * Better Auth also accepts a function returning the options, which cannot be\n * amended without calling it — such a config is passed straight through and\n * owns its own defaults.\n */\nexport function withGoogleDefaults(\n google: NonNullable<PlatformAuthConfig[\"google\"]>,\n): NonNullable<PlatformAuthConfig[\"google\"]> {\n if (typeof google === \"function\") return google\n return {\n ...google,\n accessType: google.accessType ?? \"offline\",\n prompt: google.prompt ?? \"select_account consent\",\n }\n}\n\n","export interface SsoEndpoints {\n discoveryUrl: string\n accountIssuer: string\n authorizationUrl: string\n tokenUrl: string\n userInfoUrl: string\n}\n\n/**\n * Hydra's endpoints, derived from the issuer so that the provider comes up\n * without a discovery fetch: Better Auth refuses to initialise a provider\n * whose discovery failed unless it already knows the account issuer and the\n * endpoints, and that failure would take the whole app down at boot.\n */\nexport function ssoEndpoints(issuer: string): SsoEndpoints {\n const base = issuer.replace(/\\/$/, \"\")\n return {\n discoveryUrl: `${base}/.well-known/openid-configuration`,\n accountIssuer: base,\n authorizationUrl: `${base}/oauth2/auth`,\n tokenUrl: `${base}/oauth2/token`,\n userInfoUrl: `${base}/userinfo`,\n }\n}\n","import type { PlatformRateLimitConfig, PlatformRateLimitRule } from \"./types\"\n\n// Better Auth keys its own rate limiter on NODE_ENV === \"production\", so a\n// deployment that forgets the variable serves sign-in with no brute-force\n// protection and nothing reports it. These are the platform's rules, applied\n// regardless of the environment.\nconst PLATFORM_RULES: Record<string, PlatformRateLimitRule> = {\n \"/sign-in/email\": { window: 60, max: 5 },\n \"/sign-up/email\": { window: 60, max: 5 },\n \"/email-otp/send-verification-otp\": { window: 60, max: 3 },\n \"/email-otp/verify-email\": { window: 60, max: 5 },\n \"/email-otp/reset-password\": { window: 60, max: 5 },\n \"/forget-password\": { window: 60, max: 3 },\n \"/reset-password\": { window: 60, max: 5 },\n \"/sign-in/magic-link\": { window: 60, max: 3 },\n \"/two-factor/verify-totp\": { window: 60, max: 5 },\n \"/two-factor/verify-otp\": { window: 60, max: 5 },\n \"/two-factor/verify-backup-code\": { window: 60, max: 5 },\n \"/two-factor/send-otp\": { window: 60, max: 3 },\n}\n\nexport interface ResolvedRateLimit {\n enabled: boolean\n window: number\n max: number\n storage?: \"memory\" | \"database\" | \"secondary-storage\"\n modelName?: string\n customRules: Record<string, PlatformRateLimitRule>\n}\n\nexport function resolveRateLimit(\n config?: PlatformRateLimitConfig,\n): ResolvedRateLimit {\n return {\n enabled: config?.enabled ?? true,\n window: config?.window ?? 10,\n max: config?.max ?? 100,\n ...(config?.storage ? { storage: config.storage } : {}),\n ...(config?.modelName ? { modelName: config.modelName } : {}),\n customRules: { ...PLATFORM_RULES, ...config?.customRules },\n }\n}\n\nexport { PLATFORM_RULES }\n","import type { PlatformAuth } from \"./server\"\n\n/**\n * Minimal Postgres pool contract this module needs — kept structural so the\n * package doesn't pull in `pg` as a dependency; any `pg.Pool` satisfies it.\n */\nexport interface BootstrapAdminPool {\n connect(): Promise<BootstrapAdminClient>\n}\n\nexport interface BootstrapAdminClient {\n query(text: string, values?: unknown[]): Promise<{ rowCount: number | null }>\n release(): void\n}\n\nexport interface BootstrapFirstAdminInput {\n email: string\n password: string\n name: string\n}\n\nexport type BootstrapFirstAdminResult =\n | { ok: true }\n | { ok: false; error: \"already_completed\" }\n\n// The admin() plugin's createUser endpoint isn't in PlatformAuth's published\n// type (see the widening note on createPlatformAuth in ./server), even though\n// it's mounted at runtime by every app that enables admin().\ninterface AuthWithAdminCreateUser {\n api: {\n createUser: (input: {\n body: {\n email: string\n password: string\n name: string\n role: string\n data?: Record<string, unknown>\n }\n }) => Promise<unknown>\n }\n}\n\n/**\n * Creates the very first admin for an app, before any admin exists — the one\n * case the admin() plugin's own `createUser` endpoint can't cover, since it\n * requires an already-authenticated admin session to call.\n *\n * Delegates the actual user/account creation to `auth.api.createUser` (called\n * server-side, with no request/session, so the plugin's own auth check is\n * skipped the same way a trusted server script would be) instead of inserting\n * `user`/`account` rows by hand — that keeps this in sync with whatever\n * Better Auth's internal adapter does (account.issuer, password hashing,\n * future schema changes) rather than re-deriving it and letting the two\n * drift apart.\n *\n * Serializes concurrent callers with a Postgres advisory lock: a plain\n * `WHERE NOT EXISTS` check on an empty `user` table lets two racing requests\n * both pass before either has inserted, minting two admins.\n *\n * Callers are expected to have already applied their own access gate (setup\n * token, allowed-emails list, etc.) — this function only enforces \"at most\n * one admin, ever\".\n */\nexport async function bootstrapFirstAdmin(\n auth: PlatformAuth,\n pool: BootstrapAdminPool,\n input: BootstrapFirstAdminInput,\n): Promise<BootstrapFirstAdminResult> {\n const client = await pool.connect()\n try {\n await client.query(\"SELECT pg_advisory_lock(hashtext($1))\", [\"admin-setup\"])\n\n const existing = await client.query(\n `SELECT 1 FROM \"user\" WHERE role = 'admin'`,\n )\n if (existing.rowCount && existing.rowCount > 0) {\n return { ok: false, error: \"already_completed\" }\n }\n\n const adminApi = (auth as unknown as AuthWithAdminCreateUser).api\n await adminApi.createUser({\n body: {\n email: input.email,\n password: input.password,\n name: input.name,\n role: \"admin\",\n data: { emailVerified: true },\n },\n })\n\n return { ok: true }\n } finally {\n await client.query(\"SELECT pg_advisory_unlock(hashtext($1))\", [\"admin-setup\"])\n client.release()\n }\n}\n"],"mappings":"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","names":["body","body","body"]}
@@ -1,4 +1,4 @@
1
- import { j as InvitationFailure } from './types-3OHf736K.js';
1
+ import { j as InvitationFailure } from './types-COX3VaBw.js';
2
2
 
3
3
  /**
4
4
  * What became of a claim, in terms the invitee can be told.
@@ -141,6 +141,29 @@ interface PlatformSsoConfig {
141
141
  /** Lets a first visit create the local user. Defaults to true. */
142
142
  allowSignUp?: boolean;
143
143
  }
144
+ interface PlatformKratosPasswordConfig {
145
+ /** Kratos' public URL, e.g. https://id.urbangate.dev */
146
+ publicUrl: string;
147
+ /** urbangate's issuer URL for the provisioning credential. */
148
+ issuer: string;
149
+ /** The product's provisioner client, e.g. "spore-provisioner". */
150
+ clientId: string;
151
+ clientSecret: string;
152
+ /** The role granted to a customer of this product, e.g. "spore:user". */
153
+ role: string;
154
+ /** The product id, e.g. "spore". */
155
+ product: string;
156
+ /**
157
+ * Called when a sign-up could not reach the provider, so the app can queue
158
+ * the repair. The sign-up itself always succeeds: the hook runs after the
159
+ * insert commits, and a customer is never refused registration because the
160
+ * provider is down.
161
+ */
162
+ onProvisioningDeferred?: (input: {
163
+ userId: string;
164
+ email: string;
165
+ }) => void | Promise<void>;
166
+ }
144
167
  interface SsoClientSurface {
145
168
  signIn: {
146
169
  social(args: {
@@ -182,6 +205,21 @@ interface PlatformAuthConfig {
182
205
  * as admin, anyone else as a plain user. Omit to leave it off.
183
206
  */
184
207
  sso?: PlatformSsoConfig;
208
+ /**
209
+ * Moves the customers' passwords to the suite's identity provider while the
210
+ * app keeps its own login screen, its own domain and its own session. The
211
+ * form posts to this app as before; the password is checked against Kratos
212
+ * instead of a local hash, and the person is never redirected.
213
+ *
214
+ * Each app keeps its own accounts: the same person signing up on two
215
+ * products has two local users and two passwords. They share one identity
216
+ * at the provider, which is what an app key is issued against — so an app
217
+ * dropping a local account must drop its role, never deactivate the
218
+ * identity, or the person loses every other product of the suite.
219
+ *
220
+ * Omit to leave passwords local, as before.
221
+ */
222
+ kratosPasswords?: PlatformKratosPasswordConfig;
185
223
  /**
186
224
  * Override the OTP email subject line per verification type. Merged over
187
225
  * the platform defaults — provide only the keys you want to change. The
@@ -729,4 +767,4 @@ interface AuthLayoutProps extends AuthHeadingProps {
729
767
  footer?: React.ReactNode;
730
768
  }
731
769
 
732
- export type { AuthLayoutProps as A, ForgotPasswordFormProps as F, InvitationNoticeProps as I, LoginFormProps as L, MagicLinkFormProps as M, PlatformAuthClientConfig as P, RegisterFormProps as R, SsoClientSurface as S, TwoFactorClientSurface as T, VerifyEmailFormProps as V, ResetPasswordFormProps as a, AuthClientResult as b, LinkComponent as c, AdminClientSurface as d, AuthClientDataResult as e, AuthClientSurface as f, AuthInviteProps as g, AuthNavProps as h, AuthThemeProps as i, InvitationFailure as j, LoginFormLabels as k, MagicLinkClientSurface as l, MagicLinkConfig as m, MagicLinkFormLabels as n, PlatformAuthConfig as o, PlatformAuthMailer as p, PlatformAuthMailerArgs as q, PlatformAuthMailerType as r, PlatformRateLimitConfig as s, PlatformRateLimitRule as t, PlatformSession as u, PlatformSessionData as v, PlatformSsoConfig as w, PlatformTwoFactorConfig as x, PlatformUser as y, RegisterFormLabels as z };
770
+ export type { AuthLayoutProps as A, RegisterFormLabels as B, ForgotPasswordFormProps as F, InvitationNoticeProps as I, LoginFormProps as L, MagicLinkFormProps as M, PlatformAuthClientConfig as P, RegisterFormProps as R, SsoClientSurface as S, TwoFactorClientSurface as T, VerifyEmailFormProps as V, ResetPasswordFormProps as a, AuthClientResult as b, LinkComponent as c, AdminClientSurface as d, AuthClientDataResult as e, AuthClientSurface as f, AuthInviteProps as g, AuthNavProps as h, AuthThemeProps as i, InvitationFailure as j, LoginFormLabels as k, MagicLinkClientSurface as l, MagicLinkConfig as m, MagicLinkFormLabels as n, PlatformAuthConfig as o, PlatformAuthMailer as p, PlatformAuthMailerArgs as q, PlatformAuthMailerType as r, PlatformKratosPasswordConfig as s, PlatformRateLimitConfig as t, PlatformRateLimitRule as u, PlatformSession as v, PlatformSessionData as w, PlatformSsoConfig as x, PlatformTwoFactorConfig as y, PlatformUser as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalternative/auth",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "description": "Shared Better Auth wrapper for L'Alternative apps (server + React client + auth UI)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",