@syncello/auth 3.6.1 → 4.1.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/index.d.ts CHANGED
@@ -66,7 +66,7 @@ declare function validatePasswordWithBreachCheck(password: string, checkBreaches
66
66
  warning?: string;
67
67
  }>;
68
68
 
69
- type DrizzleDB$2 = any;
69
+ type DrizzleDB$3 = any;
70
70
 
71
71
  type SessionsTable = PgTableWithColumns<any>;
72
72
  type UsersTable$4 = PgTableWithColumns<any>;
@@ -101,7 +101,7 @@ interface SessionData {
101
101
  * @param sessionTtlMs - Session TTL in milliseconds (optional)
102
102
  * @returns Session ID
103
103
  */
104
- declare function createSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, userId: string, fingerprint?: string, ipAddress?: string, sessionTtlMs?: number): Promise<string>;
104
+ declare function createSession(db: DrizzleDB$3, tables: Pick<SessionTables, 'sessions'>, userId: string, fingerprint?: string, ipAddress?: string, sessionTtlMs?: number): Promise<string>;
105
105
  /**
106
106
  * Validate a session and return user data
107
107
  * @param db - Drizzle database instance
@@ -111,7 +111,9 @@ declare function createSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'ses
111
111
  * @param sessionMaxLifetimeMs - Absolute session lifetime cap in milliseconds (optional)
112
112
  * @returns Session data or null if invalid
113
113
  */
114
- declare function validateSession(db: DrizzleDB$2, tables: SessionTables, sessionId: string, currentFingerprint?: string, sessionMaxLifetimeMs?: number): Promise<SessionData | null>;
114
+ declare function validateSession(db: DrizzleDB$3, tables: SessionTables, sessionId: string, currentFingerprint?: string, sessionMaxLifetimeMs?: number): Promise<SessionData | null>;
115
+ /** Whether the sliding window should move: never yet, or not in the last refresh interval. */
116
+ declare function sessionRefreshDue(lastActiveAt: number | null, now?: number): boolean;
115
117
  /**
116
118
  * Refresh session expiration (sliding window)
117
119
  * @param db - Drizzle database instance
@@ -119,28 +121,28 @@ declare function validateSession(db: DrizzleDB$2, tables: SessionTables, session
119
121
  * @param sessionId - Session ID
120
122
  * @param sessionTtlMs - Session TTL in milliseconds (optional)
121
123
  */
122
- declare function refreshSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, sessionId: string, sessionTtlMs?: number): Promise<void>;
124
+ declare function refreshSession(db: DrizzleDB$3, tables: Pick<SessionTables, 'sessions'>, sessionId: string, sessionTtlMs?: number): Promise<void>;
123
125
  /**
124
126
  * Delete a session (logout)
125
127
  * @param db - Drizzle database instance
126
128
  * @param tables - Session tables (sessions)
127
129
  * @param sessionId - Session ID
128
130
  */
129
- declare function deleteSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, sessionId: string): Promise<void>;
131
+ declare function deleteSession(db: DrizzleDB$3, tables: Pick<SessionTables, 'sessions'>, sessionId: string): Promise<void>;
130
132
  /**
131
133
  * Delete all sessions for a user (logout all devices)
132
134
  * @param db - Drizzle database instance
133
135
  * @param tables - Session tables (sessions)
134
136
  * @param userId - User ID
135
137
  */
136
- declare function deleteAllUserSessions(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, userId: string): Promise<void>;
138
+ declare function deleteAllUserSessions(db: DrizzleDB$3, tables: Pick<SessionTables, 'sessions'>, userId: string): Promise<void>;
137
139
  /**
138
140
  * Invalidate all sessions after password change
139
141
  * @param db - Drizzle database instance
140
142
  * @param tables - Session tables (sessions, users)
141
143
  * @param userId - User ID
142
144
  */
143
- declare function invalidateAllUserSessions(db: DrizzleDB$2, tables: SessionTables, userId: string): Promise<void>;
145
+ declare function invalidateAllUserSessions(db: DrizzleDB$3, tables: SessionTables, userId: string): Promise<void>;
144
146
 
145
147
  /**
146
148
  * Generate a cryptographically secure random token
@@ -163,9 +165,13 @@ declare function hashToken(token: string): Promise<string>;
163
165
  declare const AUTH_DEFAULTS: {
164
166
  readonly SESSION_TTL_DAYS: 7;
165
167
  readonly SESSION_MAX_LIFETIME_DAYS: 30;
168
+ readonly SESSION_REFRESH_INTERVAL_MINUTES: 15;
166
169
  readonly LOCKOUT_DURATION_MINUTES: 30;
167
170
  readonly LOCKOUT_MAX_ATTEMPTS: 5;
168
171
  readonly PASSWORD_RESET_TTL_MINUTES: 15;
172
+ readonly EMAIL_VERIFICATION_TTL_HOURS: 24;
173
+ readonly EMAIL_CHANGE_TTL_HOURS: 24;
174
+ readonly TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5;
169
175
  readonly TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300;
170
176
  readonly APP_NAME: "App";
171
177
  };
@@ -255,6 +261,13 @@ interface EmailBranding {
255
261
  buttonRadius?: string;
256
262
  /** Footer band background color. Default: '#1e1b4b' */
257
263
  footerColor?: string;
264
+ /** Heading and welcome-line color. Default: '#1e1b4b' */
265
+ headingColor?: string;
266
+ /**
267
+ * Marketing site the footer's Privacy Policy (`/privacy`) and Get in Touch (`/contact`)
268
+ * links point at. Default: unset, and the footer has no links.
269
+ */
270
+ marketingUrl?: string;
258
271
  }
259
272
 
260
273
  /**
@@ -332,8 +345,14 @@ interface InvitationHookUser {
332
345
  * library calls `verify`. If it returns a matching email, the account is created
333
346
  * with `emailVerified: true` and NO verification email is sent — the token, which
334
347
  * was delivered to that address, is itself the proof of ownership (the same trust
335
- * model as OAuth-provider signup). `onAccepted` then runs so the app can link the
336
- * user (e.g. create a team membership) atomically with signup.
348
+ * model as OAuth-provider signup). `onAccepted` then runs right after the user is
349
+ * created so the app can link them (e.g. create a team membership); a failure is
350
+ * logged, not raised, since the account already exists. A user the OAuth callback
351
+ * creates from a verified invitation gets `onAccepted` too. A `verify` that
352
+ * throws is logged and treated as "not invited".
353
+ *
354
+ * With `signup: 'invitation'`, a verified invitation is the only way to create
355
+ * an account.
337
356
  */
338
357
  interface InvitationHooks<TEnv = Record<string, unknown>> {
339
358
  /**
@@ -358,6 +377,16 @@ interface InvitationHooks<TEnv = Record<string, unknown>> {
358
377
  env: TEnv;
359
378
  }): Promise<void>;
360
379
  }
380
+ /**
381
+ * Who may create an account.
382
+ *
383
+ * - `'open'`: anyone can sign up. An `invitationToken`, when given and verified,
384
+ * only skips email verification and links the user.
385
+ * - `'invitation'`: every new account needs an invitation that `invitations.verify`
386
+ * accepts for its email, on password signup, OAuth signup and the OAuth callback
387
+ * alike. Existing users sign in as usual. Requires `invitations`.
388
+ */
389
+ type SignupMode = 'open' | 'invitation';
361
390
  /**
362
391
  * Configuration for createAuth
363
392
  */
@@ -372,6 +401,8 @@ interface AuthConfig<TEnv = Record<string, unknown>> {
372
401
  emailBranding?: EmailBranding;
373
402
  /** Optional app-owned invitation hooks for token-verified signup. */
374
403
  invitations?: InvitationHooks<TEnv>;
404
+ /** Who may create an account; defaults to `'open'`. See {@link SignupMode}. */
405
+ signup?: SignupMode;
375
406
  }
376
407
  /**
377
408
  * Auth context available in all auth operations.
@@ -383,6 +414,8 @@ interface AuthContext<TEnv = Record<string, unknown>> {
383
414
  env: TEnv;
384
415
  emailBranding?: EmailBranding;
385
416
  invitations?: InvitationHooks<TEnv>;
417
+ /** Absent means `'open'`. */
418
+ signup?: SignupMode;
386
419
  }
387
420
  /**
388
421
  * Creates an auth instance with injected database and schema.
@@ -595,7 +628,7 @@ declare function generateFingerprint(request: Request): Promise<string>;
595
628
  */
596
629
  declare function getClientIp(request: Request): string;
597
630
 
598
- type DrizzleDB$1 = any;
631
+ type DrizzleDB$2 = any;
599
632
  type UsersTable$3 = PgTableWithColumns<any>;
600
633
  interface AccountLockoutTables {
601
634
  users: UsersTable$3;
@@ -607,7 +640,7 @@ interface AccountLockoutTables {
607
640
  * @param userId - User ID to check
608
641
  * @returns Lock status and unlock time if locked
609
642
  */
610
- declare function checkAccountLocked(db: DrizzleDB$1, tables: AccountLockoutTables, userId: string): Promise<{
643
+ declare function checkAccountLocked(db: DrizzleDB$2, tables: AccountLockoutTables, userId: string): Promise<{
611
644
  isLocked: boolean;
612
645
  unlockAt?: number;
613
646
  }>;
@@ -620,7 +653,7 @@ declare function checkAccountLocked(db: DrizzleDB$1, tables: AccountLockoutTable
620
653
  * @param maxAttempts - Maximum failed attempts before lockout
621
654
  * @param lockoutDurationMs - Duration of lockout in milliseconds
622
655
  */
623
- declare function incrementFailedAttempts(db: DrizzleDB$1, tables: AccountLockoutTables, userId: string, maxAttempts?: number, lockoutDurationMs?: number): Promise<void>;
656
+ declare function incrementFailedAttempts(db: DrizzleDB$2, tables: AccountLockoutTables, userId: string, maxAttempts?: number, lockoutDurationMs?: number): Promise<void>;
624
657
  /**
625
658
  * Clear account lockout and failed attempt counter
626
659
  * Called after successful login
@@ -628,7 +661,7 @@ declare function incrementFailedAttempts(db: DrizzleDB$1, tables: AccountLockout
628
661
  * @param tables - Account lockout tables (users)
629
662
  * @param userId - User ID
630
663
  */
631
- declare function clearAccountLockout(db: DrizzleDB$1, tables: AccountLockoutTables, userId: string): Promise<void>;
664
+ declare function clearAccountLockout(db: DrizzleDB$2, tables: AccountLockoutTables, userId: string): Promise<void>;
632
665
  /**
633
666
  * Get minutes remaining until account unlock
634
667
  * @param unlockAt - Unlock timestamp
@@ -645,7 +678,7 @@ declare function getMinutesUntilUnlock(unlockAt: number): number;
645
678
  */
646
679
  declare function verifyTurnstileToken(token: string, secretKey: string, remoteIp?: string, environment?: 'development' | 'staging' | 'production'): Promise<boolean>;
647
680
 
648
- type DrizzleDB = any;
681
+ type DrizzleDB$1 = any;
649
682
  type UsersTable$2 = PgTableWithColumns<any>;
650
683
  type EmailChangeTokensTable = PgTableWithColumns<any>;
651
684
  interface EmailChangeTables {
@@ -657,7 +690,7 @@ interface RequestEmailChangeParams {
657
690
  password: string;
658
691
  newEmail: string;
659
692
  pepper: string;
660
- db: DrizzleDB;
693
+ db: DrizzleDB$1;
661
694
  tables: EmailChangeTables;
662
695
  }
663
696
  interface RequestEmailChangeResult {
@@ -673,7 +706,7 @@ interface RequestEmailChangeResult {
673
706
  declare function requestEmailChange(params: RequestEmailChangeParams): Promise<RequestEmailChangeResult>;
674
707
  interface ConfirmEmailChangeParams {
675
708
  token: string;
676
- db: DrizzleDB;
709
+ db: DrizzleDB$1;
677
710
  tables: EmailChangeTables;
678
711
  }
679
712
  interface ConfirmEmailChangeResult {
@@ -686,7 +719,7 @@ interface ConfirmEmailChangeResult {
686
719
  declare function confirmEmailChange(params: ConfirmEmailChangeParams): Promise<ConfirmEmailChangeResult>;
687
720
  interface CancelEmailChangeParams {
688
721
  token: string;
689
- db: DrizzleDB;
722
+ db: DrizzleDB$1;
690
723
  tables: Pick<EmailChangeTables, 'emailChangeTokens'>;
691
724
  }
692
725
  interface CancelEmailChangeResult {
@@ -776,6 +809,93 @@ declare function verifyCodeChallenge(verifier: string, challenge: string): Promi
776
809
  */
777
810
  declare function isDeepLinkUri(uri: string): boolean;
778
811
 
812
+ type DrizzleDB = any;
813
+
814
+ type ChallengesTable = PgTableWithColumns<any>;
815
+ interface PasswordlessTables {
816
+ passwordlessChallenges: ChallengesTable;
817
+ }
818
+ declare const PASSWORDLESS_TTL_MS: number;
819
+ declare const PASSWORDLESS_MAX_ATTEMPTS = 5;
820
+ type PasswordlessSuccess = {
821
+ ok: true;
822
+ email: string;
823
+ subjectId: string | null;
824
+ payload: unknown;
825
+ };
826
+ type PasswordlessLinkResult = PasswordlessSuccess | {
827
+ ok: false;
828
+ reason: 'invalid' | 'expired';
829
+ };
830
+ type PasswordlessCodeResult = PasswordlessSuccess | {
831
+ ok: false;
832
+ reason: 'invalid' | 'expired' | 'locked';
833
+ };
834
+ /** A uniformly distributed 6-digit code. */
835
+ declare function generateCode(): string;
836
+ /**
837
+ * Start a sign-in for an email, replacing any open one. Email the link token and code; keep the
838
+ * browser secret in an HttpOnly cookie, since the code only works alongside it.
839
+ * @param input.subjectId - The account signing in, or null for a sign-up
840
+ * @param input.payload - JSON handed back on success (e.g. sign-up details such as a name)
841
+ */
842
+ declare function createPasswordlessChallenge(db: DrizzleDB, tables: PasswordlessTables, input: {
843
+ email: string;
844
+ subjectId?: string | null;
845
+ payload?: unknown;
846
+ ttlMs?: number;
847
+ }): Promise<{
848
+ challengeId: string;
849
+ linkToken: string;
850
+ code: string;
851
+ browserSecret: string;
852
+ }>;
853
+ /** Redeem an emailed link, from any browser. Success consumes every open challenge for the email. */
854
+ declare function verifyPasswordlessLink(db: DrizzleDB, tables: PasswordlessTables, input: {
855
+ linkToken: string;
856
+ }): Promise<PasswordlessLinkResult>;
857
+ /**
858
+ * Redeem a code against the email's newest open challenge, in the browser that started it. Any
859
+ * other browser is answered `invalid`, uncounted, so a stranger who knows the email can neither
860
+ * lock the challenge nor learn its state. A wrong code counts an attempt; at `maxAttempts` the
861
+ * challenge is locked. Success consumes every open challenge for the email.
862
+ */
863
+ declare function verifyPasswordlessCode(db: DrizzleDB, tables: PasswordlessTables, input: {
864
+ email: string;
865
+ code: string;
866
+ browserSecret: string;
867
+ maxAttempts?: number;
868
+ }): Promise<PasswordlessCodeResult>;
869
+ /**
870
+ * Failed code attempts for an email across every challenge created within the window, consumed
871
+ * ones included, so asking for a new code never resets the count. Failures are dated by their
872
+ * challenge's creation, so one can leave the window up to a TTL early. Anyone can fail codes for
873
+ * an email they know only from their own browser, so pair this with a per-browser or per-IP limit.
874
+ */
875
+ declare function recentPasswordlessFailures(db: DrizzleDB, tables: PasswordlessTables, input: {
876
+ email: string;
877
+ windowMs: number;
878
+ }): Promise<number>;
879
+ /** Delete challenges that expired more than a day ago. Run from a scheduled job. */
880
+ declare function cleanupExpiredPasswordlessChallenges(db: DrizzleDB, tables: PasswordlessTables): Promise<void>;
881
+ /**
882
+ * The sign-in email: the code in the subject and body, and the link as a button.
883
+ * @param input.appUrl - Base for the logo and footer links; the link must be on its host or a subdomain
884
+ * @param input.ttlMs - The challenge's lifetime, for the stated expiry (default PASSWORDLESS_TTL_MS)
885
+ * @throws If `linkUrl` is not http(s) on `appUrl`'s host
886
+ */
887
+ declare function passwordlessEmail(input: {
888
+ appName: string;
889
+ appUrl: string;
890
+ linkUrl: string;
891
+ code: string;
892
+ ttlMs?: number;
893
+ }): {
894
+ subject: string;
895
+ html: string;
896
+ text: string;
897
+ };
898
+
779
899
  /**
780
900
  * TOTP (Time-based One-Time Password) utilities
781
901
  * RFC 6238 compliant with replay prevention
@@ -1701,10 +1821,9 @@ declare const authRoutes: OpenAPIHono<{
1701
1821
  input: {
1702
1822
  json: {
1703
1823
  email: string;
1704
- password?: string | undefined;
1824
+ password: string;
1825
+ turnstileToken: string;
1705
1826
  name?: string | undefined;
1706
- turnstileToken?: string | undefined;
1707
- oauthToken?: string | undefined;
1708
1827
  invitationToken?: string | undefined;
1709
1828
  };
1710
1829
  };
@@ -1723,10 +1842,30 @@ declare const authRoutes: OpenAPIHono<{
1723
1842
  input: {
1724
1843
  json: {
1725
1844
  email: string;
1726
- password?: string | undefined;
1845
+ password: string;
1846
+ turnstileToken: string;
1847
+ name?: string | undefined;
1848
+ invitationToken?: string | undefined;
1849
+ };
1850
+ };
1851
+ output: {
1852
+ type: string;
1853
+ title: string;
1854
+ status: number;
1855
+ detail?: string | undefined;
1856
+ instance?: string | undefined;
1857
+ traceId?: string | undefined;
1858
+ requestId?: string | undefined;
1859
+ };
1860
+ outputFormat: "json";
1861
+ status: 403;
1862
+ } | {
1863
+ input: {
1864
+ json: {
1865
+ email: string;
1866
+ password: string;
1867
+ turnstileToken: string;
1727
1868
  name?: string | undefined;
1728
- turnstileToken?: string | undefined;
1729
- oauthToken?: string | undefined;
1730
1869
  invitationToken?: string | undefined;
1731
1870
  };
1732
1871
  };
@@ -1745,10 +1884,9 @@ declare const authRoutes: OpenAPIHono<{
1745
1884
  input: {
1746
1885
  json: {
1747
1886
  email: string;
1748
- password?: string | undefined;
1887
+ password: string;
1888
+ turnstileToken: string;
1749
1889
  name?: string | undefined;
1750
- turnstileToken?: string | undefined;
1751
- oauthToken?: string | undefined;
1752
1890
  invitationToken?: string | undefined;
1753
1891
  };
1754
1892
  };
@@ -2406,7 +2544,7 @@ declare function problemJson(c: Context, problem: ProblemDetails): Response & ho
2406
2544
  instance?: string | undefined;
2407
2545
  traceId?: string | undefined;
2408
2546
  requestId?: string | undefined;
2409
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2547
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2410
2548
  /**
2411
2549
  * Helper function to return common problem types
2412
2550
  */
@@ -2423,7 +2561,7 @@ declare const problems: {
2423
2561
  instance?: string | undefined;
2424
2562
  traceId?: string | undefined;
2425
2563
  requestId?: string | undefined;
2426
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2564
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2427
2565
  /**
2428
2566
  * 400 Bad Request - Validation errors with field-specific details
2429
2567
  */
@@ -2439,7 +2577,7 @@ declare const problems: {
2439
2577
  instance?: string | undefined;
2440
2578
  traceId?: string | undefined;
2441
2579
  requestId?: string | undefined;
2442
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2580
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2443
2581
  /**
2444
2582
  * 401 Unauthorized - Authentication required
2445
2583
  */
@@ -2452,7 +2590,7 @@ declare const problems: {
2452
2590
  instance?: string | undefined;
2453
2591
  traceId?: string | undefined;
2454
2592
  requestId?: string | undefined;
2455
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2593
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2456
2594
  /**
2457
2595
  * 403 Forbidden - Insufficient permissions
2458
2596
  */
@@ -2465,7 +2603,7 @@ declare const problems: {
2465
2603
  instance?: string | undefined;
2466
2604
  traceId?: string | undefined;
2467
2605
  requestId?: string | undefined;
2468
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2606
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2469
2607
  /**
2470
2608
  * 404 Not Found
2471
2609
  */
@@ -2478,7 +2616,7 @@ declare const problems: {
2478
2616
  instance?: string | undefined;
2479
2617
  traceId?: string | undefined;
2480
2618
  requestId?: string | undefined;
2481
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2619
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2482
2620
  /**
2483
2621
  * 409 Conflict - Resource conflict (e.g., duplicate email)
2484
2622
  */
@@ -2491,7 +2629,7 @@ declare const problems: {
2491
2629
  instance?: string | undefined;
2492
2630
  traceId?: string | undefined;
2493
2631
  requestId?: string | undefined;
2494
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2632
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2495
2633
  /**
2496
2634
  * 410 Gone - Resource is no longer available
2497
2635
  */
@@ -2504,7 +2642,7 @@ declare const problems: {
2504
2642
  instance?: string | undefined;
2505
2643
  traceId?: string | undefined;
2506
2644
  requestId?: string | undefined;
2507
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2645
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2508
2646
  /**
2509
2647
  * 429 Too Many Requests - Rate limit exceeded
2510
2648
  */
@@ -2517,7 +2655,7 @@ declare const problems: {
2517
2655
  instance?: string | undefined;
2518
2656
  traceId?: string | undefined;
2519
2657
  requestId?: string | undefined;
2520
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2658
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2521
2659
  /**
2522
2660
  * 500 Internal Server Error - Generic server error
2523
2661
  * SECURITY: Never include error details or stack traces
@@ -2531,7 +2669,7 @@ declare const problems: {
2531
2669
  instance?: string | undefined;
2532
2670
  traceId?: string | undefined;
2533
2671
  requestId?: string | undefined;
2534
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2672
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2535
2673
  /**
2536
2674
  * 503 Service Unavailable - Service is temporarily unavailable
2537
2675
  */
@@ -2544,7 +2682,7 @@ declare const problems: {
2544
2682
  instance?: string | undefined;
2545
2683
  traceId?: string | undefined;
2546
2684
  requestId?: string | undefined;
2547
- }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
2685
+ }, 500 | 400 | 401 | 403 | 404 | 409 | 410 | 429 | 503, "json">;
2548
2686
  };
2549
2687
 
2550
2688
  /**
@@ -2728,11 +2866,17 @@ declare function handleWebhookEvent(payload: ResendWebhookPayload, db: PostgresJ
2728
2866
  users: UsersTable;
2729
2867
  }): Promise<void>;
2730
2868
 
2869
+ /** The svix headers Resend sends with every webhook delivery. */
2870
+ interface SvixHeaders {
2871
+ 'svix-id': string;
2872
+ 'svix-timestamp': string;
2873
+ 'svix-signature': string;
2874
+ }
2731
2875
  /**
2732
- * Verify Resend webhook signature using svix
2733
- * Resend uses svix for webhook signing
2876
+ * Whether a Resend webhook is genuine. Svix signs `<id>.<timestamp>.<body>` and refuses a
2877
+ * timestamp more than five minutes off, so all three headers are needed.
2734
2878
  */
2735
- declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
2879
+ declare function verifyWebhookSignature(payload: string, headers: SvixHeaders, secret: string): boolean;
2736
2880
 
2737
2881
  type EmailEventsTable = PgTableWithColumns<any>;
2738
2882
  /**
@@ -2746,4 +2890,4 @@ declare function calculateBounceRate(db: PostgresJsDatabase<Record<string, unkno
2746
2890
  */
2747
2891
  declare function calculateComplaintRate(db: PostgresJsDatabase<Record<string, unknown>>, emailEventsTable: EmailEventsTable, hoursAgo?: number): Promise<number>;
2748
2892
 
2749
- export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, CloudflareEmailAdapter, type DeviceType, type EmailAdapter, type EmailBranding, type EmailChangeConfirmationData, type EmailChangeNotificationData, type EmailChangeTables, type EmailSendOptions, type EmailSendResult, EmailService, type EmailTemplate, type EmailType, type Env, type InferNewSession, type InferNewUser, type InferSession, type InferUser, type InvitationHookUser, type InvitationHooks, MAX_CHALLENGE_ATTEMPTS, type PasswordResetEmailData, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, type SendEmailBinding, type SendEmailResult, type SessionData, type SessionTables, TRUSTED_DEVICE_COOKIE_BASE, TRUSTED_DEVICE_TTL_MS, type TotpVerifyResult, type TwoFactorCodeEmailData, type TwoFactorDisabledEmailData, type TwoFactorEnabledEmailData, type ValidationResult, type Variables, type VerificationEmailData, authRoutes, calculateBounceRate, calculateComplaintRate, cancelEmailChange, checkAccountLocked, checkBreachedPassword, clearAccountLockout, clearChallengeCookie, clearSessionCookie, clearTrustedDeviceCookie, confirmEmailChange, createAuth, createAuthMiddleware, createChallengeToken, createDeviceToken, createEmailAdapter, createProblemDetails, createSession, csrf, decryptTotpSecret, deleteAllUserSessions, deleteChallengeToken, deleteSession, encryptTotpSecret, extractAppUrl, formatBackupCode, generate2faCodeEmail, generate2faDisabledEmail, generate2faEnabledEmail, generateApiKey, generateBackupCodes, generateCodeChallenge, generateCodeVerifier, generateEmailChangeConfirmation, generateEmailChangeNotification, generateFingerprint, generatePasswordResetEmail, generateQrCodeUri, generateSecureToken, generateTotpSecret, generateTraceId, generateVerificationEmail, getAllowedOrigins, getAuthContext, getChallengeCookieName, getClientIp, getKeyPrefix, getMinutesUntilUnlock, getSessionCookieName, getTotpCounter, getTrustedDeviceCookieName, handleWebhookEvent, hashApiKey, hashBackupCode, hashPassword, hashToken, incrementFailedAttempts, invalidateAllUserSessions, isCapacitorApp, isCloudflarePreviewUrl, isDeepLinkUri, isValidApiKeyFormat, logError, logSecurityEvent, logger, normalizeBackupCode, optionalAuth, parseDeviceName, parseDeviceType, problemJson, problems, rateLimit, refreshSession, requestEmailChange, requireAuth, requireVerifiedEmail, retrieveChallengeToken, setChallengeCookie, setSessionCookie, setTrustedDeviceCookie, storeChallengeToken, updateChallengeAttempts, validateChallengePayload, validateOrigin, validatePassword, validatePasswordWithBreachCheck, validateSession, validateTrustedDevice, verifyBackupCode, verifyCodeChallenge, verifyPassword, verifyPasswordWithRotation, verifyTotpCode, verifyTurnstileToken, verifyWebhookSignature };
2893
+ export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, CloudflareEmailAdapter, type DeviceType, type EmailAdapter, type EmailBranding, type EmailChangeConfirmationData, type EmailChangeNotificationData, type EmailChangeTables, type EmailSendOptions, type EmailSendResult, EmailService, type EmailTemplate, type EmailType, type Env, type InferNewSession, type InferNewUser, type InferSession, type InferUser, type InvitationHookUser, type InvitationHooks, MAX_CHALLENGE_ATTEMPTS, PASSWORDLESS_MAX_ATTEMPTS, PASSWORDLESS_TTL_MS, type PasswordResetEmailData, type PasswordlessCodeResult, type PasswordlessLinkResult, type PasswordlessSuccess, type PasswordlessTables, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, type SendEmailBinding, type SendEmailResult, type SessionData, type SessionTables, type SignupMode, type SvixHeaders, TRUSTED_DEVICE_COOKIE_BASE, TRUSTED_DEVICE_TTL_MS, type TotpVerifyResult, type TwoFactorCodeEmailData, type TwoFactorDisabledEmailData, type TwoFactorEnabledEmailData, type ValidationResult, type Variables, type VerificationEmailData, authRoutes, calculateBounceRate, calculateComplaintRate, cancelEmailChange, checkAccountLocked, checkBreachedPassword, cleanupExpiredPasswordlessChallenges, clearAccountLockout, clearChallengeCookie, clearSessionCookie, clearTrustedDeviceCookie, confirmEmailChange, createAuth, createAuthMiddleware, createChallengeToken, createDeviceToken, createEmailAdapter, createPasswordlessChallenge, createProblemDetails, createSession, csrf, decryptTotpSecret, deleteAllUserSessions, deleteChallengeToken, deleteSession, encryptTotpSecret, extractAppUrl, formatBackupCode, generate2faCodeEmail, generate2faDisabledEmail, generate2faEnabledEmail, generateApiKey, generateBackupCodes, generateCode, generateCodeChallenge, generateCodeVerifier, generateEmailChangeConfirmation, generateEmailChangeNotification, generateFingerprint, generatePasswordResetEmail, generateQrCodeUri, generateSecureToken, generateTotpSecret, generateTraceId, generateVerificationEmail, getAllowedOrigins, getAuthContext, getChallengeCookieName, getClientIp, getKeyPrefix, getMinutesUntilUnlock, getSessionCookieName, getTotpCounter, getTrustedDeviceCookieName, handleWebhookEvent, hashApiKey, hashBackupCode, hashPassword, hashToken, incrementFailedAttempts, invalidateAllUserSessions, isCapacitorApp, isCloudflarePreviewUrl, isDeepLinkUri, isValidApiKeyFormat, logError, logSecurityEvent, logger, normalizeBackupCode, optionalAuth, parseDeviceName, parseDeviceType, passwordlessEmail, problemJson, problems, rateLimit, recentPasswordlessFailures, refreshSession, requestEmailChange, requireAuth, requireVerifiedEmail, retrieveChallengeToken, sessionRefreshDue, setChallengeCookie, setSessionCookie, setTrustedDeviceCookie, storeChallengeToken, updateChallengeAttempts, validateChallengePayload, validateOrigin, validatePassword, validatePasswordWithBreachCheck, validateSession, validateTrustedDevice, verifyBackupCode, verifyCodeChallenge, verifyPassword, verifyPasswordWithRotation, verifyPasswordlessCode, verifyPasswordlessLink, verifyTotpCode, verifyTurnstileToken, verifyWebhookSignature };