@lalternative/auth 0.10.6 → 0.12.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/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createAuthClient } from 'better-auth/react';
2
- import { f as AuthClientSurface, l as MagicLinkClientSurface, d as AdminClientSurface, P as PlatformAuthClientConfig } from './types-BEd3aSYE.js';
2
+ import { f as AuthClientSurface, l as MagicLinkClientSurface, d as AdminClientSurface, T as TwoFactorClientSurface, P as PlatformAuthClientConfig } from './types-Ct0Kt36S.js';
3
3
  import 'better-auth';
4
4
 
5
5
  /**
@@ -21,7 +21,8 @@ import 'better-auth';
21
21
  type PlatformAuthClient = Omit<AuthClientSurface, "signIn" | "admin"> & {
22
22
  signIn: AuthClientSurface["signIn"] & MagicLinkClientSurface["signIn"];
23
23
  admin: AdminClientSurface;
24
- } & Omit<ReturnType<typeof createAuthClient>, keyof AuthClientSurface | keyof MagicLinkClientSurface>;
24
+ twoFactor: TwoFactorClientSurface;
25
+ } & Omit<ReturnType<typeof createAuthClient>, keyof AuthClientSurface | keyof MagicLinkClientSurface | "twoFactor">;
25
26
  /**
26
27
  * Creates a Better Auth client for React usage.
27
28
  * Provides useSession() and the email-OTP / magic-link / admin plugin methods.
package/dist/client.js CHANGED
@@ -3,7 +3,8 @@ import { createAuthClient } from "better-auth/react";
3
3
  import {
4
4
  emailOTPClient,
5
5
  adminClient,
6
- magicLinkClient
6
+ magicLinkClient,
7
+ twoFactorClient
7
8
  } from "better-auth/client/plugins";
8
9
  function createPlatformAuthClient(config) {
9
10
  return createAuthClient({
@@ -12,6 +13,7 @@ function createPlatformAuthClient(config) {
12
13
  emailOTPClient(),
13
14
  magicLinkClient(),
14
15
  adminClient(),
16
+ twoFactorClient(),
15
17
  ...config?.plugins ?? []
16
18
  ]
17
19
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createAuthClient } from \"better-auth/react\"\nimport {\n emailOTPClient,\n adminClient,\n magicLinkClient,\n} from \"better-auth/client/plugins\"\nimport type {\n AdminClientSurface,\n AuthClientSurface,\n MagicLinkClientSurface,\n PlatformAuthClientConfig,\n} from \"./types\"\n\n/**\n * A Better Auth React client carrying the platform plugins.\n *\n * The concrete inferred type cannot be named in a published .d.ts (TS2742 — it\n * reaches into zod's internals), so the surface the auth screens call is\n * declared by hand in AuthClientSurface and intersected with the rest of the\n * client. Keep it in sync with the plugins enabled below.\n *\n * signIn carries both halves: the client always mounts magicLinkClient, since\n * which methods exist client-side costs nothing — whether the route answers is\n * decided server-side by passing `magicLink` to createPlatformAuth.\n *\n * admin is optional on AuthClientSurface, whose job is to type the prop the\n * forms take, and required here: this client always mounts adminClient(), so a\n * back-office calling admin.listUsers() off it must not have to widen the type.\n */\nexport type PlatformAuthClient = Omit<AuthClientSurface, \"signIn\" | \"admin\"> & {\n signIn: AuthClientSurface[\"signIn\"] & MagicLinkClientSurface[\"signIn\"]\n admin: AdminClientSurface\n} & Omit<\n ReturnType<typeof createAuthClient>,\n keyof AuthClientSurface | keyof MagicLinkClientSurface\n >\n\n/**\n * Creates a Better Auth client for React usage.\n * Provides useSession() and the email-OTP / magic-link / admin plugin methods.\n */\nexport function createPlatformAuthClient(\n config?: PlatformAuthClientConfig,\n): PlatformAuthClient {\n return createAuthClient({\n baseURL:\n config?.baseURL ??\n (typeof window !== \"undefined\"\n ? window.location.origin\n : \"http://localhost:3000\"),\n plugins: [\n emailOTPClient(),\n magicLinkClient(),\n adminClient(),\n ...(config?.plugins ?? []),\n ],\n }) as unknown as PlatformAuthClient\n}\n"],"mappings":";AAAA,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAoCA,SAAS,yBACd,QACoB;AACpB,SAAO,iBAAiB;AAAA,IACtB,SACE,QAAQ,YACP,OAAO,WAAW,cACf,OAAO,SAAS,SAChB;AAAA,IACN,SAAS;AAAA,MACP,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,GAAI,QAAQ,WAAW,CAAC;AAAA,IAC1B;AAAA,EACF,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createAuthClient } from \"better-auth/react\"\nimport {\n emailOTPClient,\n adminClient,\n magicLinkClient,\n twoFactorClient,\n} from \"better-auth/client/plugins\"\nimport type {\n AdminClientSurface,\n AuthClientSurface,\n MagicLinkClientSurface,\n PlatformAuthClientConfig,\n TwoFactorClientSurface,\n} from \"./types\"\n\n/**\n * A Better Auth React client carrying the platform plugins.\n *\n * The concrete inferred type cannot be named in a published .d.ts (TS2742 — it\n * reaches into zod's internals), so the surface the auth screens call is\n * declared by hand in AuthClientSurface and intersected with the rest of the\n * client. Keep it in sync with the plugins enabled below.\n *\n * signIn carries both halves: the client always mounts magicLinkClient, since\n * which methods exist client-side costs nothing — whether the route answers is\n * decided server-side by passing `magicLink` to createPlatformAuth.\n *\n * admin is optional on AuthClientSurface, whose job is to type the prop the\n * forms take, and required here: this client always mounts adminClient(), so a\n * back-office calling admin.listUsers() off it must not have to widen the type.\n */\nexport type PlatformAuthClient = Omit<AuthClientSurface, \"signIn\" | \"admin\"> & {\n signIn: AuthClientSurface[\"signIn\"] & MagicLinkClientSurface[\"signIn\"]\n admin: AdminClientSurface\n twoFactor: TwoFactorClientSurface\n} & Omit<\n ReturnType<typeof createAuthClient>,\n | keyof AuthClientSurface\n | keyof MagicLinkClientSurface\n | \"twoFactor\"\n >\n\n/**\n * Creates a Better Auth client for React usage.\n * Provides useSession() and the email-OTP / magic-link / admin plugin methods.\n */\nexport function createPlatformAuthClient(\n config?: PlatformAuthClientConfig,\n): PlatformAuthClient {\n return createAuthClient({\n baseURL:\n config?.baseURL ??\n (typeof window !== \"undefined\"\n ? window.location.origin\n : \"http://localhost:3000\"),\n plugins: [\n emailOTPClient(),\n magicLinkClient(),\n adminClient(),\n twoFactorClient(),\n ...(config?.plugins ?? []),\n ],\n }) as unknown as PlatformAuthClient\n}\n"],"mappings":";AAAA,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAwCA,SAAS,yBACd,QACoB;AACpB,SAAO,iBAAiB;AAAA,IACtB,SACE,QAAQ,YACP,OAAO,WAAW,cACf,OAAO,SAAS,SAChB;AAAA,IACN,SAAS;AAAA,MACP,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,GAAI,QAAQ,WAAW,CAAC;AAAA,IAC1B;AAAA,EACF,CAAC;AACH;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { L as LoginFormProps, R as RegisterFormProps, V as VerifyEmailFormProps, F as ForgotPasswordFormProps, M as MagicLinkFormProps, a as ResetPasswordFormProps, A as AuthLayoutProps, I as InvitationNoticeProps, b as AuthClientResult, c as LinkComponent } from './types-BEd3aSYE.js';
2
- export { d as AdminClientSurface, e as AuthClientDataResult, f as AuthClientSurface, g as AuthInviteProps, h as AuthNavProps, i as AuthThemeProps, j as InvitationFailure, k as LoginFormLabels, l as MagicLinkClientSurface, m as MagicLinkConfig, n as MagicLinkFormLabels, P as PlatformAuthClientConfig, o as PlatformAuthConfig, p as PlatformAuthMailer, q as PlatformAuthMailerArgs, r as PlatformAuthMailerType, s as PlatformSession, t as PlatformSessionData, u as PlatformUser, v as RegisterFormLabels } from './types-BEd3aSYE.js';
1
+ import { L as LoginFormProps, R as RegisterFormProps, V as VerifyEmailFormProps, F as ForgotPasswordFormProps, M as MagicLinkFormProps, a as ResetPasswordFormProps, A as AuthLayoutProps, I as InvitationNoticeProps, b as AuthClientResult, c as LinkComponent } from './types-Ct0Kt36S.js';
2
+ export { d as AdminClientSurface, e as AuthClientDataResult, f as AuthClientSurface, g as AuthInviteProps, h as AuthNavProps, i as AuthThemeProps, j as InvitationFailure, k as LoginFormLabels, l as MagicLinkClientSurface, m as MagicLinkConfig, n as MagicLinkFormLabels, P as PlatformAuthClientConfig, o as PlatformAuthConfig, p as PlatformAuthMailer, q as PlatformAuthMailerArgs, r as PlatformAuthMailerType, s as PlatformRateLimitConfig, t as PlatformRateLimitRule, u as PlatformSession, v as PlatformSessionData, w as PlatformTwoFactorConfig, x as PlatformUser, y as RegisterFormLabels, T as TwoFactorClientSurface } from './types-Ct0Kt36S.js';
3
3
  import * as better_auth_react from 'better-auth/react';
4
4
  import * as better_auth from 'better-auth';
5
5
  import { PlatformAuthClient } from './client.js';
6
6
  import * as react from 'react';
7
7
  import { InputHTMLAttributes, ReactNode } from 'react';
8
- export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-Bcrd8t1_.js';
8
+ export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-miBKXoxz.js';
9
9
 
10
10
  /**
11
11
  * Returns a useSession hook bound to the given auth client.
@@ -1,4 +1,4 @@
1
- import { j as InvitationFailure } from './types-BEd3aSYE.js';
1
+ import { j as InvitationFailure } from './types-Ct0Kt36S.js';
2
2
 
3
3
  /**
4
4
  * What became of a claim, in terms the invitee can be told.
package/dist/server.d.ts CHANGED
@@ -1,7 +1,54 @@
1
1
  import { Auth, BetterAuthOptions } from 'better-auth';
2
- import { o as PlatformAuthConfig } from './types-BEd3aSYE.js';
3
- export { s as PlatformSession, t as PlatformSessionData, u as PlatformUser } from './types-BEd3aSYE.js';
4
- export { a as ClaimInvitationOptions, C as ClaimOutcome, c as claimInvitation, b as completesSignup, h as holdInviteTokenCookie, d as invitationOutcomeCookie, e as inviteTokenFrom, i as isInvitationFailure, p as pinInviteToken, r as releaseInviteTokenCookie } from './invitation-Bcrd8t1_.js';
2
+ import { o as PlatformAuthConfig } from './types-Ct0Kt36S.js';
3
+ export { s as PlatformRateLimitConfig, t as PlatformRateLimitRule, u as PlatformSession, v as PlatformSessionData, w as PlatformTwoFactorConfig, x as PlatformUser } from './types-Ct0Kt36S.js';
4
+ export { a as ClaimInvitationOptions, C as ClaimOutcome, c as claimInvitation, b as completesSignup, h as holdInviteTokenCookie, d as invitationOutcomeCookie, e as inviteTokenFrom, i as isInvitationFailure, p as pinInviteToken, r as releaseInviteTokenCookie } from './invitation-miBKXoxz.js';
5
+
6
+ /**
7
+ * Minimal Postgres pool contract this module needs — kept structural so the
8
+ * package doesn't pull in `pg` as a dependency; any `pg.Pool` satisfies it.
9
+ */
10
+ interface BootstrapAdminPool {
11
+ connect(): Promise<BootstrapAdminClient>;
12
+ }
13
+ interface BootstrapAdminClient {
14
+ query(text: string, values?: unknown[]): Promise<{
15
+ rowCount: number | null;
16
+ }>;
17
+ release(): void;
18
+ }
19
+ interface BootstrapFirstAdminInput {
20
+ email: string;
21
+ password: string;
22
+ name: string;
23
+ }
24
+ type BootstrapFirstAdminResult = {
25
+ ok: true;
26
+ } | {
27
+ ok: false;
28
+ error: "already_completed";
29
+ };
30
+ /**
31
+ * Creates the very first admin for an app, before any admin exists — the one
32
+ * case the admin() plugin's own `createUser` endpoint can't cover, since it
33
+ * requires an already-authenticated admin session to call.
34
+ *
35
+ * Delegates the actual user/account creation to `auth.api.createUser` (called
36
+ * server-side, with no request/session, so the plugin's own auth check is
37
+ * skipped the same way a trusted server script would be) instead of inserting
38
+ * `user`/`account` rows by hand — that keeps this in sync with whatever
39
+ * Better Auth's internal adapter does (account.issuer, password hashing,
40
+ * future schema changes) rather than re-deriving it and letting the two
41
+ * drift apart.
42
+ *
43
+ * Serializes concurrent callers with a Postgres advisory lock: a plain
44
+ * `WHERE NOT EXISTS` check on an empty `user` table lets two racing requests
45
+ * both pass before either has inserted, minting two admins.
46
+ *
47
+ * Callers are expected to have already applied their own access gate (setup
48
+ * token, allowed-emails list, etc.) — this function only enforces "at most
49
+ * one admin, ever".
50
+ */
51
+ declare function bootstrapFirstAdmin(auth: PlatformAuth, pool: BootstrapAdminPool, input: BootstrapFirstAdminInput): Promise<BootstrapFirstAdminResult>;
5
52
 
6
53
  /**
7
54
  * Creates a Better Auth instance with platform defaults.
@@ -10,4 +57,4 @@ export { a as ClaimInvitationOptions, C as ClaimOutcome, c as claimInvitation, b
10
57
  declare function createPlatformAuth(config: PlatformAuthConfig): Auth<BetterAuthOptions>;
11
58
  type PlatformAuth = ReturnType<typeof createPlatformAuth>;
12
59
 
13
- export { type PlatformAuth, createPlatformAuth };
60
+ export { type BootstrapAdminClient, type BootstrapAdminPool, type BootstrapFirstAdminInput, type BootstrapFirstAdminResult, type PlatformAuth, bootstrapFirstAdmin, createPlatformAuth };
package/dist/server.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  // src/server.ts
14
14
  import { betterAuth, APIError } from "better-auth";
15
- import { emailOTP, admin, magicLink } from "better-auth/plugins";
15
+ import { emailOTP, admin, magicLink, twoFactor } from "better-auth/plugins";
16
16
 
17
17
  // src/google-defaults.ts
18
18
  function withGoogleDefaults(google) {
@@ -24,6 +24,60 @@ function withGoogleDefaults(google) {
24
24
  };
25
25
  }
26
26
 
27
+ // src/rate-limit.ts
28
+ var PLATFORM_RULES = {
29
+ "/sign-in/email": { window: 60, max: 5 },
30
+ "/sign-up/email": { window: 60, max: 5 },
31
+ "/email-otp/send-verification-otp": { window: 60, max: 3 },
32
+ "/email-otp/verify-email": { window: 60, max: 5 },
33
+ "/email-otp/reset-password": { window: 60, max: 5 },
34
+ "/forget-password": { window: 60, max: 3 },
35
+ "/reset-password": { window: 60, max: 5 },
36
+ "/sign-in/magic-link": { window: 60, max: 3 },
37
+ "/two-factor/verify-totp": { window: 60, max: 5 },
38
+ "/two-factor/verify-otp": { window: 60, max: 5 },
39
+ "/two-factor/verify-backup-code": { window: 60, max: 5 },
40
+ "/two-factor/send-otp": { window: 60, max: 3 }
41
+ };
42
+ function resolveRateLimit(config) {
43
+ return {
44
+ enabled: config?.enabled ?? true,
45
+ window: config?.window ?? 10,
46
+ max: config?.max ?? 100,
47
+ ...config?.storage ? { storage: config.storage } : {},
48
+ ...config?.modelName ? { modelName: config.modelName } : {},
49
+ customRules: { ...PLATFORM_RULES, ...config?.customRules }
50
+ };
51
+ }
52
+
53
+ // src/bootstrap-admin.ts
54
+ async function bootstrapFirstAdmin(auth, pool, input) {
55
+ const client = await pool.connect();
56
+ try {
57
+ await client.query("SELECT pg_advisory_lock(hashtext($1))", ["admin-setup"]);
58
+ const existing = await client.query(
59
+ `SELECT 1 FROM "user" WHERE role = 'admin'`
60
+ );
61
+ if (existing.rowCount && existing.rowCount > 0) {
62
+ return { ok: false, error: "already_completed" };
63
+ }
64
+ const adminApi = auth.api;
65
+ await adminApi.createUser({
66
+ body: {
67
+ email: input.email,
68
+ password: input.password,
69
+ name: input.name,
70
+ role: "admin",
71
+ data: { emailVerified: true }
72
+ }
73
+ });
74
+ return { ok: true };
75
+ } finally {
76
+ await client.query("SELECT pg_advisory_unlock(hashtext($1))", ["admin-setup"]);
77
+ client.release();
78
+ }
79
+ }
80
+
27
81
  // src/server.ts
28
82
  var DEFAULT_EMAIL_SUBJECTS = {
29
83
  "email-verification": "Verify your account",
@@ -68,7 +122,10 @@ function createPlatformAuth(config) {
68
122
  isInvited,
69
123
  emailSubjects,
70
124
  renderOtpEmail,
71
- magicLink: magicLinkConfig
125
+ magicLink: magicLinkConfig,
126
+ rateLimit,
127
+ twoFactor: twoFactorConfig,
128
+ trustedOrigins
72
129
  } = config;
73
130
  const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects };
74
131
  const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail;
@@ -76,6 +133,8 @@ function createPlatformAuth(config) {
76
133
  database,
77
134
  baseURL,
78
135
  secret,
136
+ ...trustedOrigins ? { trustedOrigins } : {},
137
+ rateLimit: resolveRateLimit(rateLimit),
79
138
  emailAndPassword: {
80
139
  enabled: true,
81
140
  requireEmailVerification: true
@@ -179,6 +238,12 @@ function createPlatformAuth(config) {
179
238
  })
180
239
  ] : [],
181
240
  admin(),
241
+ ...twoFactorConfig?.enabled ? [
242
+ twoFactor({
243
+ issuer: twoFactorConfig.issuer ?? appName,
244
+ skipVerificationOnEnable: twoFactorConfig.skipVerificationOnEnable ?? false
245
+ })
246
+ ] : [],
182
247
  ...plugins
183
248
  // app-specific plugins (e.g. tanstackStartCookies)
184
249
  ],
@@ -199,6 +264,7 @@ function createPlatformAuth(config) {
199
264
  });
200
265
  }
201
266
  export {
267
+ bootstrapFirstAdmin,
202
268
  claimInvitation,
203
269
  completesSignup,
204
270
  createPlatformAuth,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/google-defaults.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin, magicLink } from \"better-auth/plugins\"\nimport type { PlatformAuthConfig, PlatformAuthMailerType } from \"./types\"\nimport { withGoogleDefaults } from \"./google-defaults\"\nimport { withSignUpName } from \"./signup-name\"\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 } = config\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 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 account: {\n accountLinking: {\n enabled: false,\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 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 ...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\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} 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","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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts","../src/google-defaults.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 } from \"better-auth/plugins\"\nimport type { PlatformAuthConfig, PlatformAuthMailerType } from \"./types\"\nimport { withGoogleDefaults } from \"./google-defaults\"\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 } = config\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 account: {\n accountLinking: {\n enabled: false,\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 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 ...(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\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 { 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","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":[]}
@@ -25,6 +25,8 @@ interface PlatformUser {
25
25
  role?: string | null;
26
26
  /** From the admin() plugin. */
27
27
  banned?: boolean | null;
28
+ /** From the twoFactor() plugin, when enabled. */
29
+ twoFactorEnabled?: boolean | null;
28
30
  }
29
31
  interface PlatformSessionData {
30
32
  id: string;
@@ -82,6 +84,51 @@ interface MagicLinkConfig {
82
84
  */
83
85
  render?: (url: string, email: string) => string;
84
86
  }
87
+ interface PlatformRateLimitRule {
88
+ /** Length of the window, in seconds. */
89
+ window: number;
90
+ /** Requests allowed per window, per client. */
91
+ max: number;
92
+ }
93
+ interface PlatformRateLimitConfig {
94
+ /**
95
+ * Off only when an app is certain something in front of it already limits
96
+ * the auth endpoints. Defaults to true, deliberately: Better Auth keys its
97
+ * own default on NODE_ENV, so a deployment missing the variable would serve
98
+ * sign-in with no brute-force protection and nothing would say so.
99
+ */
100
+ enabled?: boolean;
101
+ /** Global window in seconds for paths without a rule. Defaults to 10. */
102
+ window?: number;
103
+ /** Global max per window for paths without a rule. Defaults to 100. */
104
+ max?: number;
105
+ /**
106
+ * Where counters live. "memory" (the default) is per-process, so it does not
107
+ * hold across replicas — an app running more than one must pass "database"
108
+ * or wire secondaryStorage.
109
+ */
110
+ storage?: "memory" | "database" | "secondary-storage";
111
+ /** Table name when storage is "database". */
112
+ modelName?: string;
113
+ /**
114
+ * Per-path overrides, merged over the platform's. The platform already
115
+ * tightens sign-in, sign-up, OTP send/verify, password reset, magic link
116
+ * and two-factor verification; use this to go further, not to loosen.
117
+ */
118
+ customRules?: Record<string, PlatformRateLimitRule>;
119
+ }
120
+ interface PlatformTwoFactorConfig {
121
+ /** Mounts the two-factor endpoints. Off unless set. */
122
+ enabled: boolean;
123
+ /** Label shown in the authenticator app. Defaults to appName. */
124
+ issuer?: string;
125
+ /**
126
+ * Trust an enrolment before a first code is verified. Leave false: enabling
127
+ * on an unverified secret locks the account out when the authenticator was
128
+ * mis-scanned.
129
+ */
130
+ skipVerificationOnEnable?: boolean;
131
+ }
85
132
  interface PlatformAuthConfig {
86
133
  /** PostgreSQL connection pool or connection string */
87
134
  database: BetterAuthOptions["database"];
@@ -143,6 +190,22 @@ interface PlatformAuthConfig {
143
190
  betaMode?: boolean;
144
191
  /** Check if an email+token pair has been invited (required when betaMode is true) */
145
192
  isInvited?: (email: string, inviteToken: string) => Promise<boolean>;
193
+ /**
194
+ * Brute-force limits on the auth endpoints. The platform enables them with
195
+ * tightened per-path rules; pass this only to adjust.
196
+ */
197
+ rateLimit?: PlatformRateLimitConfig;
198
+ /**
199
+ * TOTP second factor with backup codes. Requires the `twoFactor` table and
200
+ * the `user.twoFactorEnabled` column.
201
+ */
202
+ twoFactor?: PlatformTwoFactorConfig;
203
+ /**
204
+ * Origins allowed to drive the auth endpoints, beyond baseURL. Better Auth
205
+ * checks Origin against this list, so it is what stands between a session
206
+ * cookie and a cross-site request that spends it.
207
+ */
208
+ trustedOrigins?: string[];
146
209
  }
147
210
  interface PlatformAuthClientConfig {
148
211
  /** Base URL override (defaults to window.location.origin in browser) */
@@ -359,6 +422,45 @@ interface MagicLinkClientSurface {
359
422
  }): Promise<AuthClientResult>;
360
423
  };
361
424
  }
425
+ /**
426
+ * The twoFactor() plugin's client half. Always mounted by
427
+ * createPlatformAuthClient — which methods exist client-side costs nothing;
428
+ * whether the routes answer is decided server-side by passing `twoFactor` to
429
+ * createPlatformAuth.
430
+ *
431
+ * A sign-in against an account with 2FA on returns `twoFactorRedirect: true`
432
+ * instead of a session, and the second factor is what completes it.
433
+ */
434
+ interface TwoFactorClientSurface {
435
+ enable(input: {
436
+ password: string;
437
+ issuer?: string;
438
+ }): Promise<AuthClientDataResult<{
439
+ totpURI: string;
440
+ backupCodes: string[];
441
+ }>>;
442
+ disable(input: {
443
+ password: string;
444
+ }): Promise<AuthClientResult>;
445
+ getTotpUri(input: {
446
+ password: string;
447
+ }): Promise<AuthClientDataResult<{
448
+ totpURI: string;
449
+ }>>;
450
+ verifyTotp(input: {
451
+ code: string;
452
+ trustDevice?: boolean;
453
+ }): Promise<AuthClientResult>;
454
+ verifyBackupCode(input: {
455
+ code: string;
456
+ trustDevice?: boolean;
457
+ }): Promise<AuthClientResult>;
458
+ generateBackupCodes(input: {
459
+ password: string;
460
+ }): Promise<AuthClientDataResult<{
461
+ backupCodes: string[];
462
+ }>>;
463
+ }
362
464
  interface AuthThemeProps {
363
465
  /** Replaces the submit button's `bg-primary text-primary-foreground` */
364
466
  submitClassName?: string;
@@ -600,4 +702,4 @@ interface AuthLayoutProps extends AuthHeadingProps {
600
702
  footer?: React.ReactNode;
601
703
  }
602
704
 
603
- export type { AuthLayoutProps as A, ForgotPasswordFormProps as F, InvitationNoticeProps as I, LoginFormProps as L, MagicLinkFormProps as M, PlatformAuthClientConfig as P, RegisterFormProps as R, 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, PlatformSession as s, PlatformSessionData as t, PlatformUser as u, RegisterFormLabels as v };
705
+ export type { AuthLayoutProps as A, ForgotPasswordFormProps as F, InvitationNoticeProps as I, LoginFormProps as L, MagicLinkFormProps as M, PlatformAuthClientConfig as P, RegisterFormProps as R, 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, PlatformTwoFactorConfig as w, PlatformUser as x, RegisterFormLabels as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalternative/auth",
3
- "version": "0.10.6",
3
+ "version": "0.12.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",