@syncello/auth 2.5.1

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.
@@ -0,0 +1,1339 @@
1
+ export { ColumnDefinition, ColumnType, EnumDefinition, EnumName, IndexDefinition, SCHEMA_VERSION, TableDefinition, TableName, enums, tableNames, tables } from './schema/index.cjs';
2
+ import { PgTableWithColumns, PgTable } from 'drizzle-orm/pg-core';
3
+ import * as hono from 'hono';
4
+ import { Context } from 'hono';
5
+ import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
6
+ import { OpenAPIHono } from '@hono/zod-openapi';
7
+ import * as hono_utils_types from 'hono/utils/types';
8
+ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
9
+
10
+ /**
11
+ * Hash a password using pepper-hardened bcrypt
12
+ * Flow: Normalize (NFKC) → HMAC-SHA256 (pepper) → Base64 → bcrypt
13
+ * @param password - Plain text password
14
+ * @param pepper - Secret pepper key (from env.PASSWORD_PEPPER_V1)
15
+ * @returns Hashed password
16
+ */
17
+ declare function hashPassword(password: string, pepper: string): Promise<string>;
18
+ /**
19
+ * Verify a password against its hash using pepper-hardened bcrypt
20
+ * @param password - Plain text password to verify
21
+ * @param hash - Stored bcrypt hash
22
+ * @param pepper - Secret pepper key (from env.PASSWORD_PEPPER_V1 or V2)
23
+ * @returns True if password matches
24
+ */
25
+ declare function verifyPassword(password: string, hash: string, pepper: string): Promise<boolean>;
26
+ /**
27
+ * Verify password with pepper rotation support
28
+ * Tries current pepper first, then falls back to previous pepper if provided
29
+ * @param password - Plain text password to verify
30
+ * @param hash - Stored bcrypt hash
31
+ * @param currentPepper - Current pepper (v1)
32
+ * @param previousPepper - Previous pepper (v2) for rotation support (optional)
33
+ * @returns Object with verification result and which pepper succeeded
34
+ */
35
+ declare function verifyPasswordWithRotation(password: string, hash: string, currentPepper: string, previousPepper?: string): Promise<{
36
+ verified: boolean;
37
+ usedPreviousPepper: boolean;
38
+ }>;
39
+ /**
40
+ * Validate password meets security requirements
41
+ * OWASP 2024: Focus on length over complexity
42
+ * @param password - Password to validate
43
+ * @returns Validation result
44
+ */
45
+ declare function validatePassword(password: string): {
46
+ valid: boolean;
47
+ error?: string;
48
+ };
49
+ /**
50
+ * Check if password has been compromised using Have I Been Pwned API
51
+ * Uses k-anonymity model - only first 5 chars of SHA-1 hash are sent
52
+ * @param password - Password to check
53
+ * @returns True if password found in breach database
54
+ */
55
+ declare function checkBreachedPassword(password: string): Promise<boolean>;
56
+ /**
57
+ * Validate password with optional breach check
58
+ * @param password - Password to validate
59
+ * @param checkBreaches - Whether to check against HIBP database (default: true)
60
+ * @returns Validation result with optional breach warning
61
+ */
62
+ declare function validatePasswordWithBreachCheck(password: string, checkBreaches?: boolean): Promise<{
63
+ valid: boolean;
64
+ error?: string;
65
+ warning?: string;
66
+ }>;
67
+
68
+ type DrizzleDB$2 = any;
69
+
70
+ type SessionsTable = PgTableWithColumns<{
71
+ name: string;
72
+ schema: undefined;
73
+ columns: {
74
+ id: any;
75
+ userId: any;
76
+ expiresAt: any;
77
+ createdAt: any;
78
+ lastActiveAt: any;
79
+ fingerprint: any;
80
+ ipAddress: any;
81
+ };
82
+ dialect: 'pg';
83
+ }>;
84
+ type UsersTable$4 = PgTableWithColumns<{
85
+ name: string;
86
+ schema: undefined;
87
+ columns: {
88
+ id: any;
89
+ email: any;
90
+ emailVerified: any;
91
+ sessionVersion: any;
92
+ createdAt: any;
93
+ updatedAt: any;
94
+ };
95
+ dialect: 'pg';
96
+ }>;
97
+ interface SessionTables {
98
+ sessions: SessionsTable;
99
+ users: UsersTable$4;
100
+ }
101
+ interface SessionData {
102
+ user: {
103
+ id: string;
104
+ email: string;
105
+ emailVerified: boolean;
106
+ sessionVersion: number;
107
+ createdAt: Date;
108
+ updatedAt: Date;
109
+ };
110
+ session: {
111
+ id: string;
112
+ userId: string;
113
+ expiresAt: Date;
114
+ createdAt: Date;
115
+ lastActiveAt: Date | null;
116
+ };
117
+ }
118
+ /**
119
+ * Create a new session for a user
120
+ * @param db - Drizzle database instance
121
+ * @param tables - Session tables (sessions)
122
+ * @param userId - User ID
123
+ * @param fingerprint - Browser fingerprint (optional)
124
+ * @param ipAddress - Client IP address (optional)
125
+ * @param sessionTtlMs - Session TTL in milliseconds (optional)
126
+ * @returns Session ID
127
+ */
128
+ declare function createSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, userId: string, fingerprint?: string, ipAddress?: string, sessionTtlMs?: number): Promise<string>;
129
+ /**
130
+ * Validate a session and return user data
131
+ * @param db - Drizzle database instance
132
+ * @param tables - Session tables (sessions, users)
133
+ * @param sessionId - Session ID from cookie
134
+ * @param currentFingerprint - Current browser fingerprint (optional)
135
+ * @returns Session data or null if invalid
136
+ */
137
+ declare function validateSession(db: DrizzleDB$2, tables: SessionTables, sessionId: string, currentFingerprint?: string): Promise<SessionData | null>;
138
+ /**
139
+ * Refresh session expiration (sliding window)
140
+ * @param db - Drizzle database instance
141
+ * @param tables - Session tables (sessions)
142
+ * @param sessionId - Session ID
143
+ * @param sessionTtlMs - Session TTL in milliseconds (optional)
144
+ */
145
+ declare function refreshSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, sessionId: string, sessionTtlMs?: number): Promise<void>;
146
+ /**
147
+ * Delete a session (logout)
148
+ * @param db - Drizzle database instance
149
+ * @param tables - Session tables (sessions)
150
+ * @param sessionId - Session ID
151
+ */
152
+ declare function deleteSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, sessionId: string): Promise<void>;
153
+ /**
154
+ * Delete all sessions for a user (logout all devices)
155
+ * @param db - Drizzle database instance
156
+ * @param tables - Session tables (sessions)
157
+ * @param userId - User ID
158
+ */
159
+ declare function deleteAllUserSessions(db: DrizzleDB$2, tables: Pick<SessionTables, 'sessions'>, userId: string): Promise<void>;
160
+ /**
161
+ * Invalidate all sessions after password change
162
+ * @param db - Drizzle database instance
163
+ * @param tables - Session tables (sessions, users)
164
+ * @param userId - User ID
165
+ */
166
+ declare function invalidateAllUserSessions(db: DrizzleDB$2, tables: SessionTables, userId: string): Promise<void>;
167
+
168
+ /**
169
+ * Generate a cryptographically secure random token
170
+ * @param bytes - Number of bytes (default: 64 = 128 hex chars)
171
+ * @returns Hex-encoded token
172
+ */
173
+ declare function generateSecureToken(bytes?: number): string;
174
+ /**
175
+ * Hash a token using SHA-256 for storage
176
+ * NEVER store tokens in plain text - always hash first
177
+ * @param token - Token to hash
178
+ * @returns SHA-256 hash as hex string
179
+ */
180
+ declare function hashToken(token: string): Promise<string>;
181
+
182
+ /**
183
+ * Centralized auth configuration
184
+ * All hardcoded auth values should be defined here with env var overrides
185
+ */
186
+ declare const AUTH_DEFAULTS: {
187
+ readonly SESSION_TTL_DAYS: 7;
188
+ readonly LOCKOUT_DURATION_MINUTES: 30;
189
+ readonly LOCKOUT_MAX_ATTEMPTS: 5;
190
+ readonly PASSWORD_RESET_TTL_MINUTES: 15;
191
+ readonly TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300;
192
+ readonly APP_NAME: "App";
193
+ };
194
+
195
+ /**
196
+ * Base type for auth tables - any Postgres table
197
+ */
198
+ type AuthTable = PgTable;
199
+ /**
200
+ * Schema interface that consumers must provide.
201
+ * Each property must be a Drizzle PgTable with the expected columns.
202
+ * The CLI generates these tables in the consumer's project.
203
+ */
204
+ interface AuthSchema {
205
+ users: AuthTable;
206
+ sessions: AuthTable;
207
+ user2faMethods: AuthTable;
208
+ userBackupCodes: AuthTable;
209
+ userTrustedDevices: AuthTable;
210
+ emailVerificationTokens: AuthTable;
211
+ passwordResetTokens: AuthTable;
212
+ emailChangeTokens: AuthTable;
213
+ emailEvents: AuthTable;
214
+ failedLoginAttempts: AuthTable;
215
+ securityAuditLog: AuthTable;
216
+ oauthAccounts: AuthTable;
217
+ }
218
+ /**
219
+ * Infer model types from a consumer's schema.
220
+ * These utilities let consumers extract types from their specific tables.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * import { createAuth, type InferUser } from '@syncello/auth';
225
+ *
226
+ * const auth = createAuth({ db, schema, getEnv });
227
+ * type User = InferUser<typeof auth.schema>;
228
+ * ```
229
+ */
230
+ type InferUser<TSchema extends AuthSchema> = InferSelectModel<TSchema['users']>;
231
+ type InferNewUser<TSchema extends AuthSchema> = InferInsertModel<TSchema['users']>;
232
+ type InferSession<TSchema extends AuthSchema> = InferSelectModel<TSchema['sessions']>;
233
+ type InferNewSession<TSchema extends AuthSchema> = InferInsertModel<TSchema['sessions']>;
234
+ /**
235
+ * Database interface - accepts any Drizzle Postgres database.
236
+ * Uses duck typing for flexibility across different Drizzle configurations.
237
+ */
238
+ interface AuthDatabase {
239
+ select: <T extends PgTable>(from?: T) => unknown;
240
+ insert: <T extends PgTable>(into: T) => unknown;
241
+ update: <T extends PgTable>(table: T) => unknown;
242
+ delete: <T extends PgTable>(from: T) => unknown;
243
+ query: Record<string, unknown>;
244
+ }
245
+ /**
246
+ * Configuration for createAuth
247
+ */
248
+ interface AuthConfig<TEnv = Record<string, unknown>> {
249
+ /** Drizzle database instance */
250
+ db: AuthDatabase;
251
+ /** Schema tables from generated auth.ts */
252
+ schema: AuthSchema;
253
+ /** Environment variables accessor */
254
+ getEnv: (c: Context) => TEnv;
255
+ }
256
+ /**
257
+ * Auth context available in all auth operations.
258
+ * Preserves the TEnv generic for type-safe environment access.
259
+ */
260
+ interface AuthContext<TEnv = Record<string, unknown>> {
261
+ db: AuthDatabase;
262
+ schema: AuthSchema;
263
+ env: TEnv;
264
+ }
265
+ /**
266
+ * Creates an auth instance with injected database and schema.
267
+ * This follows the adapter pattern - consumers pass their db/schema TO the library.
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * import { createAuth } from '@syncello/auth';
272
+ * import { db } from './db';
273
+ * import * as schema from './db/schema';
274
+ *
275
+ * const auth = createAuth({
276
+ * db,
277
+ * schema: {
278
+ * users: schema.users,
279
+ * sessions: schema.sessions,
280
+ * user2faMethods: schema.user2faMethods,
281
+ * userBackupCodes: schema.userBackupCodes,
282
+ * userTrustedDevices: schema.userTrustedDevices,
283
+ * emailVerificationTokens: schema.emailVerificationTokens,
284
+ * passwordResetTokens: schema.passwordResetTokens,
285
+ * emailChangeTokens: schema.emailChangeTokens,
286
+ * emailEvents: schema.emailEvents,
287
+ * failedLoginAttempts: schema.failedLoginAttempts,
288
+ * securityAuditLog: schema.securityAuditLog,
289
+ * oauthAccounts: schema.oauthAccounts,
290
+ * },
291
+ * getEnv: (c) => c.env,
292
+ * });
293
+ *
294
+ * // Use context in middleware/handlers
295
+ * const ctx = auth.getContext(c);
296
+ * const user = await ctx.db.query.users.findFirst(...);
297
+ * ```
298
+ */
299
+ declare function createAuth<TEnv = Record<string, unknown>>(config: AuthConfig<TEnv>): {
300
+ db: AuthDatabase;
301
+ schema: AuthSchema;
302
+ getContext: (c: Context) => AuthContext<TEnv>;
303
+ };
304
+ type Auth<TEnv = Record<string, unknown>> = ReturnType<typeof createAuth<TEnv>>;
305
+ /**
306
+ * Creates a middleware that injects AuthContext into the Hono context.
307
+ * Apply this middleware before mounting auth routes.
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * import { createAuth, createAuthMiddleware } from '@syncello/auth';
312
+ *
313
+ * const auth = createAuth({ db, schema, getEnv: (c) => c.env });
314
+ * const authMiddleware = createAuthMiddleware(auth);
315
+ *
316
+ * app.use('/api/auth/*', authMiddleware);
317
+ * app.route('/api/auth', authRoutes);
318
+ * ```
319
+ */
320
+ declare function createAuthMiddleware<TEnv = Record<string, unknown>>(auth: Auth<TEnv>): (c: Context, next: () => Promise<void>) => Promise<void>;
321
+ /**
322
+ * Helper to get AuthContext from Hono context.
323
+ * Use this in route handlers to access db, schema, and env.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * import { getAuthContext } from '@syncello/auth';
328
+ *
329
+ * const handler = async (c) => {
330
+ * const { db, schema, env } = getAuthContext(c);
331
+ * const user = await db.select().from(schema.users).where(...);
332
+ * };
333
+ * ```
334
+ */
335
+ declare function getAuthContext<TEnv = Record<string, unknown>>(c: Context): AuthContext<TEnv>;
336
+
337
+ /**
338
+ * Environment variables available in Cloudflare Workers
339
+ */
340
+ type Env = {
341
+ APP_URL: string;
342
+ APP_NAME?: string;
343
+ SESSION_SECRET: string;
344
+ PASSWORD_PEPPER_V1: string;
345
+ PASSWORD_PEPPER_V2?: string;
346
+ RESEND_API_KEY?: string;
347
+ RESEND_WEBHOOK_SECRET?: string;
348
+ DB: {
349
+ connectionString: string;
350
+ };
351
+ ENVIRONMENT?: 'development' | 'staging' | 'production';
352
+ CORS_ORIGINS?: string;
353
+ CORS_PAGES_PATTERN?: string;
354
+ COOKIE_DOMAIN?: string;
355
+ SESSION_TTL_DAYS?: string;
356
+ LOCKOUT_DURATION_MINUTES?: string;
357
+ LOCKOUT_MAX_ATTEMPTS?: string;
358
+ PASSWORD_RESET_TTL_MINUTES?: string;
359
+ TWO_FACTOR_CHALLENGE_TTL_SECONDS?: string;
360
+ SENTRY_DSN?: string;
361
+ CF_VERSION_METADATA?: {
362
+ id: string;
363
+ tag: string;
364
+ timestamp: string;
365
+ };
366
+ ENCRYPTION_KEY_V1: string;
367
+ ENCRYPTION_KEY_V2?: string;
368
+ ENCRYPTION_ACTIVE_KEY_VERSION: string;
369
+ TOTP_ENCRYPTION_KEY: string;
370
+ GOOGLE_CLIENT_ID?: string;
371
+ GOOGLE_CLIENT_SECRET?: string;
372
+ MICROSOFT_CLIENT_ID?: string;
373
+ MICROSOFT_CLIENT_SECRET?: string;
374
+ STRIPE_SECRET_KEY?: string;
375
+ STRIPE_WEBHOOK_SECRET?: string;
376
+ STRIPE_PRICE_BASIC?: string;
377
+ STRIPE_PRICE_PRO?: string;
378
+ RATE_LIMIT_KV: KVNamespace;
379
+ OAUTH_STATES: KVNamespace;
380
+ AVATARS: R2Bucket;
381
+ AVATARS_PUBLIC_URL?: string;
382
+ CACHE_KV?: KVNamespace;
383
+ TURNSTILE_SECRET_KEY: string;
384
+ };
385
+ /**
386
+ * Variables set by middleware and available in route handlers
387
+ */
388
+ type Variables = {
389
+ db: AuthDatabase;
390
+ userId?: string;
391
+ requestId: string;
392
+ traceId: string;
393
+ spanId: string;
394
+ authContext?: AuthContext;
395
+ };
396
+
397
+ declare function getSessionCookieName(env?: {
398
+ ENVIRONMENT?: string;
399
+ }): string;
400
+ declare function setSessionCookie(sessionId: string, env?: {
401
+ ENVIRONMENT?: string;
402
+ }): string;
403
+ declare function clearSessionCookie(env?: {
404
+ ENVIRONMENT?: string;
405
+ }): string;
406
+ declare function getTrustedDeviceCookieName(env: Env): string;
407
+ declare function setTrustedDeviceCookie(c: Context, env: Env, token: string): void;
408
+ declare function clearTrustedDeviceCookie(c: Context, env: Env): void;
409
+ declare function getChallengeCookieName(env: Env): string;
410
+ declare function setChallengeCookie(c: Context, env: Env, token: string): void;
411
+ declare function clearChallengeCookie(c: Context, env: Env): void;
412
+
413
+ /**
414
+ * Generate a browser fingerprint from request headers
415
+ * Uses User-Agent and Accept-Language to create a stable identifier
416
+ * @param request - The incoming request
417
+ * @returns SHA-256 hash (first 32 characters)
418
+ */
419
+ declare function generateFingerprint(request: Request): Promise<string>;
420
+ /**
421
+ * Get client IP address from request headers
422
+ * Prioritizes Cloudflare's cf-connecting-ip, falls back to x-forwarded-for
423
+ * @param request - The incoming request
424
+ * @returns Client IP address or 'unknown'
425
+ */
426
+ declare function getClientIp(request: Request): string;
427
+
428
+ type DrizzleDB$1 = any;
429
+ type UsersTable$3 = PgTableWithColumns<{
430
+ name: string;
431
+ schema: undefined;
432
+ columns: {
433
+ id: any;
434
+ lockedUntil: any;
435
+ failedLoginCount: any;
436
+ };
437
+ dialect: 'pg';
438
+ }>;
439
+ interface AccountLockoutTables {
440
+ users: UsersTable$3;
441
+ }
442
+ /**
443
+ * Check if an account is currently locked
444
+ * @param db - Drizzle database instance
445
+ * @param tables - Account lockout tables (users)
446
+ * @param userId - User ID to check
447
+ * @returns Lock status and unlock time if locked
448
+ */
449
+ declare function checkAccountLocked(db: DrizzleDB$1, tables: AccountLockoutTables, userId: string): Promise<{
450
+ isLocked: boolean;
451
+ unlockAt?: number;
452
+ }>;
453
+ /**
454
+ * Increment failed login attempts for a user
455
+ * Locks account after maxAttempts
456
+ * @param db - Drizzle database instance
457
+ * @param tables - Account lockout tables (users)
458
+ * @param userId - User ID
459
+ * @param maxAttempts - Maximum failed attempts before lockout
460
+ * @param lockoutDurationMs - Duration of lockout in milliseconds
461
+ */
462
+ declare function incrementFailedAttempts(db: DrizzleDB$1, tables: AccountLockoutTables, userId: string, maxAttempts?: number, lockoutDurationMs?: number): Promise<void>;
463
+ /**
464
+ * Clear account lockout and failed attempt counter
465
+ * Called after successful login
466
+ * @param db - Drizzle database instance
467
+ * @param tables - Account lockout tables (users)
468
+ * @param userId - User ID
469
+ */
470
+ declare function clearAccountLockout(db: DrizzleDB$1, tables: AccountLockoutTables, userId: string): Promise<void>;
471
+ /**
472
+ * Get minutes remaining until account unlock
473
+ * @param unlockAt - Unlock timestamp
474
+ * @returns Minutes remaining (rounded up)
475
+ */
476
+ declare function getMinutesUntilUnlock(unlockAt: number): number;
477
+
478
+ /**
479
+ * Verifies a Cloudflare Turnstile token server-side.
480
+ * Returns true if the token is valid, false otherwise.
481
+ *
482
+ * In staging/test environments, uses Cloudflare's test secret key that always passes.
483
+ * @see https://developers.cloudflare.com/turnstile/tutorials/excluding-turnstile-from-e2e-tests/
484
+ */
485
+ declare function verifyTurnstileToken(token: string, secretKey: string, remoteIp?: string, environment?: 'development' | 'staging' | 'production'): Promise<boolean>;
486
+
487
+ type DrizzleDB = any;
488
+ type UsersTable$2 = PgTableWithColumns<{
489
+ name: string;
490
+ schema: undefined;
491
+ columns: {
492
+ id: any;
493
+ email: any;
494
+ hashedPassword: any;
495
+ updatedAt: any;
496
+ };
497
+ dialect: 'pg';
498
+ }>;
499
+ type EmailChangeTokensTable = PgTableWithColumns<{
500
+ name: string;
501
+ schema: undefined;
502
+ columns: {
503
+ id: any;
504
+ userId: any;
505
+ newEmail: any;
506
+ tokenHash: any;
507
+ cancelTokenHash: any;
508
+ expiresAt: any;
509
+ createdAt: any;
510
+ };
511
+ dialect: 'pg';
512
+ }>;
513
+ interface EmailChangeTables {
514
+ users: UsersTable$2;
515
+ emailChangeTokens: EmailChangeTokensTable;
516
+ }
517
+ interface RequestEmailChangeParams {
518
+ userId: string;
519
+ password: string;
520
+ newEmail: string;
521
+ pepper: string;
522
+ db: DrizzleDB;
523
+ tables: EmailChangeTables;
524
+ }
525
+ interface RequestEmailChangeResult {
526
+ success: boolean;
527
+ error?: string;
528
+ confirmToken?: string;
529
+ cancelToken?: string;
530
+ oldEmail?: string;
531
+ }
532
+ /**
533
+ * Request an email change - validates password and creates tokens
534
+ */
535
+ declare function requestEmailChange(params: RequestEmailChangeParams): Promise<RequestEmailChangeResult>;
536
+ interface ConfirmEmailChangeParams {
537
+ token: string;
538
+ db: DrizzleDB;
539
+ tables: EmailChangeTables;
540
+ }
541
+ interface ConfirmEmailChangeResult {
542
+ success: boolean;
543
+ error?: string;
544
+ }
545
+ /**
546
+ * Confirm email change - validates token and updates email
547
+ */
548
+ declare function confirmEmailChange(params: ConfirmEmailChangeParams): Promise<ConfirmEmailChangeResult>;
549
+ interface CancelEmailChangeParams {
550
+ token: string;
551
+ db: DrizzleDB;
552
+ tables: Pick<EmailChangeTables, 'emailChangeTokens'>;
553
+ }
554
+ interface CancelEmailChangeResult {
555
+ success: boolean;
556
+ error?: string;
557
+ }
558
+ /**
559
+ * Cancel email change - validates cancel token and deletes pending change
560
+ */
561
+ declare function cancelEmailChange(params: CancelEmailChangeParams): Promise<CancelEmailChangeResult>;
562
+
563
+ /**
564
+ * Unified API Key utilities
565
+ *
566
+ * Keys use `syn_` prefix + 32 hex chars (36 total).
567
+ * Keys are hashed with SHA-256 for storage — never stored in retrievable form.
568
+ */
569
+ /**
570
+ * Generate a new API key: syn_ + 32 random hex characters
571
+ */
572
+ declare function generateApiKey(): string;
573
+ /**
574
+ * Hash an API key using SHA-256 for secure storage/lookup
575
+ */
576
+ declare function hashApiKey(key: string): Promise<string>;
577
+ /**
578
+ * Extract display prefix from a key (first 12 chars: syn_ + 8 hex)
579
+ */
580
+ declare function getKeyPrefix(key: string): string;
581
+ /**
582
+ * Validate API key format: syn_ + 32 hex chars
583
+ */
584
+ declare function isValidApiKeyFormat(key: string): boolean;
585
+
586
+ /**
587
+ * Validate Origin header for state-changing requests
588
+ * Prevents CSRF attacks by ensuring requests come from same origin
589
+ *
590
+ * @param request - The incoming request
591
+ * @param allowedOrigins - List of allowed origins (e.g., ['https://app.example.com'])
592
+ * @returns True if valid, false if suspicious
593
+ */
594
+ declare function validateOrigin(request: Request, allowedOrigins: string[], pagesPattern?: string): boolean;
595
+ /**
596
+ * Get allowed origins from environment
597
+ */
598
+ declare function getAllowedOrigins(appUrl: string): string[];
599
+ /**
600
+ * Check if origin is a Cloudflare Pages preview URL for this project.
601
+ * Uses CORS_PAGES_PATTERN env var (e.g., "your-app.pages.dev") to match preview deployments.
602
+ * If no pattern is configured, returns false (no preview URLs allowed by default).
603
+ */
604
+ declare function isCloudflarePreviewUrl(origin: string, pagesPattern?: string): boolean;
605
+ /**
606
+ * Check if origin is a Capacitor mobile app
607
+ * Allows capacitor:// and ionic:// protocols used by iOS/Android apps
608
+ */
609
+ declare function isCapacitorApp(origin: string): boolean;
610
+
611
+ /**
612
+ * Mobile authentication utilities
613
+ * Implements PKCE (RFC 7636) for secure OAuth on native apps
614
+ */
615
+ /**
616
+ * Generate a cryptographically random code verifier for PKCE
617
+ * @returns Code verifier string (43-128 chars, URL-safe)
618
+ */
619
+ declare function generateCodeVerifier(): string;
620
+ /**
621
+ * Generate S256 code challenge from verifier
622
+ * @param verifier - Code verifier string
623
+ * @returns Base64URL-encoded SHA256 hash
624
+ */
625
+ declare function generateCodeChallenge(verifier: string): Promise<string>;
626
+ /**
627
+ * Verify that a code verifier matches a code challenge
628
+ * @param verifier - The code verifier from the token request
629
+ * @param challenge - The code challenge from the authorization request
630
+ * @returns True if verifier hashes to challenge
631
+ */
632
+ declare function verifyCodeChallenge(verifier: string, challenge: string): Promise<boolean>;
633
+ /**
634
+ * Check if a URI is a deep link (custom scheme)
635
+ * Deep links are used by native mobile apps for OAuth callbacks
636
+ * @param uri - The redirect URI to check
637
+ * @returns True if URI uses a custom scheme (not http/https)
638
+ */
639
+ declare function isDeepLinkUri(uri: string): boolean;
640
+
641
+ /**
642
+ * TOTP (Time-based One-Time Password) utilities
643
+ * RFC 6238 compliant with replay prevention
644
+ */
645
+ /**
646
+ * Get current TOTP counter (time step)
647
+ */
648
+ declare function getTotpCounter(): number;
649
+ /**
650
+ * Generate a cryptographically secure TOTP secret
651
+ * @returns Base32-encoded 20-byte secret (32 characters)
652
+ */
653
+ declare function generateTotpSecret(): string;
654
+ /**
655
+ * Generate the otpauth:// URI for QR code scanning
656
+ * @param secret - Base32-encoded TOTP secret
657
+ * @param email - User's email address
658
+ * @param issuer - Application name shown in authenticator apps (default: AUTH_DEFAULTS.APP_NAME)
659
+ */
660
+ declare function generateQrCodeUri(secret: string, email: string, issuer?: string): string;
661
+ type TotpVerifyResult = {
662
+ valid: true;
663
+ counter: number;
664
+ } | {
665
+ valid: false;
666
+ counter?: undefined;
667
+ };
668
+ /**
669
+ * Verify a TOTP code with replay prevention
670
+ * @param secret - Base32-encoded TOTP secret
671
+ * @param code - 6-digit TOTP code from user
672
+ * @param lastCounter - Last accepted counter (null if first verification)
673
+ * @returns Verification result with counter if valid
674
+ */
675
+ declare function verifyTotpCode(secret: string, code: string, lastCounter: number | null): TotpVerifyResult;
676
+ /**
677
+ * Encrypt a TOTP secret using AES-256-GCM
678
+ */
679
+ declare function encryptTotpSecret(secret: string, keyHex: string): Promise<string>;
680
+ /**
681
+ * Decrypt a TOTP secret using AES-256-GCM
682
+ */
683
+ declare function decryptTotpSecret(encrypted: string, keyHex: string): Promise<string>;
684
+
685
+ /**
686
+ * Backup codes for 2FA recovery
687
+ * Uses bcrypt for NIST SP 800-63B compliance (secrets < 112 bits)
688
+ */
689
+ /**
690
+ * Generate backup codes
691
+ * @param count - Number of codes to generate (default: 10)
692
+ * @returns Array of 8-character backup codes
693
+ */
694
+ declare function generateBackupCodes(count?: number): string[];
695
+ /**
696
+ * Format a backup code for display (XXXX-XXXX)
697
+ */
698
+ declare function formatBackupCode(code: string): string;
699
+ /**
700
+ * Normalize user input for comparison
701
+ */
702
+ declare function normalizeBackupCode(input: string): string;
703
+ /**
704
+ * Hash a backup code using bcrypt (NIST compliant)
705
+ */
706
+ declare function hashBackupCode(code: string): Promise<string>;
707
+ /**
708
+ * Verify a backup code against a stored bcrypt hash
709
+ */
710
+ declare function verifyBackupCode(code: string, storedHash: string): Promise<boolean>;
711
+
712
+ /**
713
+ * Trusted device management for 2FA "Remember this device" feature
714
+ */
715
+ /** Trusted device cookie TTL: 30 days in milliseconds */
716
+ declare const TRUSTED_DEVICE_TTL_MS: number;
717
+ /** Trusted device cookie base name */
718
+ declare const TRUSTED_DEVICE_COOKIE_BASE = "trusted_device";
719
+ type DeviceType = 'desktop' | 'mobile' | 'tablet';
720
+ /**
721
+ * Parse User-Agent to get a human-readable device name
722
+ */
723
+ declare function parseDeviceName(userAgent: string): string;
724
+ /**
725
+ * Parse User-Agent to determine device type (desktop, mobile, or tablet)
726
+ */
727
+ declare function parseDeviceType(userAgent: string): DeviceType;
728
+ /**
729
+ * Create a new trusted device token
730
+ */
731
+ declare function createDeviceToken(): {
732
+ token: string;
733
+ expiresAt: number;
734
+ };
735
+ /**
736
+ * Validate a trusted device token for a user
737
+ * Returns true if the token is valid and not expired
738
+ */
739
+ declare function validateTrustedDevice(db: any, userTrustedDevicesTable: any, userId: string, tokenHash: string): Promise<boolean>;
740
+
741
+ /**
742
+ * 2FA Challenge token management with KV helpers
743
+ */
744
+ /** Challenge token TTL: 5 minutes */
745
+ declare const CHALLENGE_TTL_MS: number;
746
+ /** Max attempts before challenge is invalidated */
747
+ declare const MAX_CHALLENGE_ATTEMPTS = 5;
748
+ interface ChallengePayload {
749
+ userId: string;
750
+ methods: ('totp' | 'email')[];
751
+ invitationToken: string | null;
752
+ expiresAt: number;
753
+ attempts: number;
754
+ createdAt: number;
755
+ }
756
+ type ValidationResult = {
757
+ valid: true;
758
+ } | {
759
+ valid: false;
760
+ reason: 'expired' | 'max_attempts' | 'invalid';
761
+ };
762
+ /**
763
+ * Create a new challenge token with full context
764
+ */
765
+ declare function createChallengeToken(userId: string, methods: ('totp' | 'email')[], invitationToken: string | null): {
766
+ token: string;
767
+ payload: ChallengePayload;
768
+ };
769
+ /**
770
+ * Validate a challenge payload
771
+ */
772
+ declare function validateChallengePayload(payload: ChallengePayload): ValidationResult;
773
+ /**
774
+ * Store a challenge token in KV
775
+ */
776
+ declare function storeChallengeToken(kv: KVNamespace, token: string, payload: ChallengePayload): Promise<void>;
777
+ /**
778
+ * Retrieve a challenge token from KV
779
+ */
780
+ declare function retrieveChallengeToken(kv: KVNamespace, token: string): Promise<ChallengePayload | null>;
781
+ /**
782
+ * Update challenge attempts in KV
783
+ */
784
+ declare function updateChallengeAttempts(kv: KVNamespace, token: string, payload: ChallengePayload): Promise<void>;
785
+ /**
786
+ * Delete a challenge token from KV
787
+ */
788
+ declare function deleteChallengeToken(kv: KVNamespace, token: string): Promise<void>;
789
+
790
+ /**
791
+ * Auth middleware - ensures user is authenticated
792
+ * Sets userId in context if session is valid
793
+ */
794
+ declare const requireAuth: hono.MiddlewareHandler<{
795
+ Bindings: Env;
796
+ Variables: Variables;
797
+ }, string, {}, Response>;
798
+ /**
799
+ * Optional auth middleware - sets userId if session exists, continues either way
800
+ */
801
+ declare const optionalAuth: hono.MiddlewareHandler<{
802
+ Bindings: Env;
803
+ Variables: Variables;
804
+ }, string, {}, Response>;
805
+
806
+ /**
807
+ * CSRF middleware - validates Origin header for state-changing requests
808
+ * Prevents CSRF attacks by ensuring requests come from allowed origins
809
+ */
810
+ declare const csrf: hono.MiddlewareHandler<{
811
+ Bindings: Env;
812
+ Variables: Variables;
813
+ }, string, {}, Response>;
814
+
815
+ type RateLimitConfig = {
816
+ identifier: (c: Context<{
817
+ Bindings: Env;
818
+ Variables: Variables;
819
+ }>) => Promise<string> | string;
820
+ action: string;
821
+ maxAttempts: number;
822
+ windowMs: number;
823
+ };
824
+ /**
825
+ * Rate limit middleware factory
826
+ * Uses KV-backed rate limiting (check + increment) to avoid database hits.
827
+ *
828
+ * Sets standard rate limit headers on all responses:
829
+ * - X-RateLimit-Limit: Maximum requests allowed in window
830
+ * - X-RateLimit-Remaining: Requests remaining in current window
831
+ * - X-RateLimit-Reset: Unix timestamp when the window resets
832
+ *
833
+ * On 429 responses, also sets:
834
+ * - Retry-After: Seconds until the rate limit resets
835
+ *
836
+ * @param config - Rate limit configuration
837
+ * @returns Middleware that enforces rate limits
838
+ */
839
+ declare const rateLimit: (config: RateLimitConfig) => hono.MiddlewareHandler<{
840
+ Bindings: Env;
841
+ Variables: Variables;
842
+ }, string, {}, Response>;
843
+
844
+ /**
845
+ * Middleware that requires the authenticated user to have a verified email.
846
+ * Must be used AFTER requireAuth middleware.
847
+ */
848
+ declare const requireVerifiedEmail: hono.MiddlewareHandler<{
849
+ Bindings: Env;
850
+ Variables: Variables;
851
+ }, string, {}, Response>;
852
+
853
+ declare const auth: OpenAPIHono<{
854
+ Bindings: Env;
855
+ Variables: Variables;
856
+ }, {}, "/">;
857
+
858
+ /**
859
+ * RFC 9457 Problem Details interface
860
+ * All fields are optional per spec, but we require type/title/status for consistency
861
+ */
862
+ interface ProblemDetails {
863
+ /** URI identifying the problem type (defaults to about:blank) */
864
+ type: string;
865
+ /** Short, human-readable summary of the problem type */
866
+ title: string;
867
+ /** HTTP status code */
868
+ status: number;
869
+ /** Human-readable explanation specific to this occurrence */
870
+ detail?: string;
871
+ /** URI identifying the specific occurrence of the problem */
872
+ instance?: string;
873
+ /** OpenTelemetry trace ID (32-char hex) for distributed tracing */
874
+ traceId?: string;
875
+ /** Request ID from X-Request-ID header for correlation */
876
+ requestId?: string;
877
+ /** Additional extension members (validation errors, etc.) */
878
+ [key: string]: unknown;
879
+ }
880
+ /**
881
+ * Common problem types for the API
882
+ * Using relative URIs - update these to point to your documentation
883
+ */
884
+ declare const ProblemTypes: {
885
+ readonly BAD_REQUEST: "/errors/bad-request";
886
+ readonly UNAUTHORIZED: "/errors/unauthorized";
887
+ readonly FORBIDDEN: "/errors/forbidden";
888
+ readonly NOT_FOUND: "/errors/not-found";
889
+ readonly CONFLICT: "/errors/conflict";
890
+ readonly GONE: "/errors/gone";
891
+ readonly VALIDATION_ERROR: "/errors/validation-error";
892
+ readonly RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded";
893
+ readonly INTERNAL_ERROR: "/errors/internal-error";
894
+ readonly SERVICE_UNAVAILABLE: "/errors/service-unavailable";
895
+ };
896
+ /**
897
+ * Generate a trace ID in OpenTelemetry format (32-character hex string)
898
+ * Uses Web Crypto API for cryptographically secure randomness
899
+ */
900
+ declare function generateTraceId(): string;
901
+ /**
902
+ * Create a Problem Details object with automatic traceId and requestId
903
+ */
904
+ declare function createProblemDetails(type: string, title: string, status: number, options?: {
905
+ detail?: string;
906
+ instance?: string;
907
+ traceId?: string;
908
+ requestId?: string;
909
+ extensions?: Record<string, unknown>;
910
+ }): ProblemDetails;
911
+ /**
912
+ * Return a Problem Details JSON response from a Hono context
913
+ * Sets the correct Content-Type header (application/problem+json)
914
+ */
915
+ declare function problemJson(c: Context, problem: ProblemDetails): Response & hono.TypedResponse<{
916
+ [x: string]: hono_utils_types.JSONValue;
917
+ type: string;
918
+ title: string;
919
+ status: number;
920
+ detail?: string | undefined;
921
+ instance?: string | undefined;
922
+ traceId?: string | undefined;
923
+ requestId?: string | undefined;
924
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
925
+ /**
926
+ * Helper function to return common problem types
927
+ */
928
+ declare const problems: {
929
+ /**
930
+ * 400 Bad Request - Generic client error
931
+ */
932
+ badRequest(c: Context, detail?: string): Response & hono.TypedResponse<{
933
+ [x: string]: hono_utils_types.JSONValue;
934
+ type: string;
935
+ title: string;
936
+ status: number;
937
+ detail?: string | undefined;
938
+ instance?: string | undefined;
939
+ traceId?: string | undefined;
940
+ requestId?: string | undefined;
941
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
942
+ /**
943
+ * 400 Bad Request - Validation errors with field-specific details
944
+ */
945
+ validationError(c: Context, detail: string, errors?: Array<{
946
+ field: string;
947
+ message: string;
948
+ }>): Response & hono.TypedResponse<{
949
+ [x: string]: hono_utils_types.JSONValue;
950
+ type: string;
951
+ title: string;
952
+ status: number;
953
+ detail?: string | undefined;
954
+ instance?: string | undefined;
955
+ traceId?: string | undefined;
956
+ requestId?: string | undefined;
957
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
958
+ /**
959
+ * 401 Unauthorized - Authentication required
960
+ */
961
+ unauthorized(c: Context, detail?: string): Response & hono.TypedResponse<{
962
+ [x: string]: hono_utils_types.JSONValue;
963
+ type: string;
964
+ title: string;
965
+ status: number;
966
+ detail?: string | undefined;
967
+ instance?: string | undefined;
968
+ traceId?: string | undefined;
969
+ requestId?: string | undefined;
970
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
971
+ /**
972
+ * 403 Forbidden - Insufficient permissions
973
+ */
974
+ forbidden(c: Context, detail?: string): Response & hono.TypedResponse<{
975
+ [x: string]: hono_utils_types.JSONValue;
976
+ type: string;
977
+ title: string;
978
+ status: number;
979
+ detail?: string | undefined;
980
+ instance?: string | undefined;
981
+ traceId?: string | undefined;
982
+ requestId?: string | undefined;
983
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
984
+ /**
985
+ * 404 Not Found
986
+ */
987
+ notFound(c: Context, detail?: string): Response & hono.TypedResponse<{
988
+ [x: string]: hono_utils_types.JSONValue;
989
+ type: string;
990
+ title: string;
991
+ status: number;
992
+ detail?: string | undefined;
993
+ instance?: string | undefined;
994
+ traceId?: string | undefined;
995
+ requestId?: string | undefined;
996
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
997
+ /**
998
+ * 409 Conflict - Resource conflict (e.g., duplicate email)
999
+ */
1000
+ conflict(c: Context, detail: string): Response & hono.TypedResponse<{
1001
+ [x: string]: hono_utils_types.JSONValue;
1002
+ type: string;
1003
+ title: string;
1004
+ status: number;
1005
+ detail?: string | undefined;
1006
+ instance?: string | undefined;
1007
+ traceId?: string | undefined;
1008
+ requestId?: string | undefined;
1009
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
1010
+ /**
1011
+ * 410 Gone - Resource is no longer available
1012
+ */
1013
+ gone(c: Context, detail?: string): Response & hono.TypedResponse<{
1014
+ [x: string]: hono_utils_types.JSONValue;
1015
+ type: string;
1016
+ title: string;
1017
+ status: number;
1018
+ detail?: string | undefined;
1019
+ instance?: string | undefined;
1020
+ traceId?: string | undefined;
1021
+ requestId?: string | undefined;
1022
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
1023
+ /**
1024
+ * 429 Too Many Requests - Rate limit exceeded
1025
+ */
1026
+ rateLimitExceeded(c: Context, retryAfter?: number): Response & hono.TypedResponse<{
1027
+ [x: string]: hono_utils_types.JSONValue;
1028
+ type: string;
1029
+ title: string;
1030
+ status: number;
1031
+ detail?: string | undefined;
1032
+ instance?: string | undefined;
1033
+ traceId?: string | undefined;
1034
+ requestId?: string | undefined;
1035
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
1036
+ /**
1037
+ * 500 Internal Server Error - Generic server error
1038
+ * SECURITY: Never include error details or stack traces
1039
+ */
1040
+ internalError(c: Context, error?: Error): Response & hono.TypedResponse<{
1041
+ [x: string]: hono_utils_types.JSONValue;
1042
+ type: string;
1043
+ title: string;
1044
+ status: number;
1045
+ detail?: string | undefined;
1046
+ instance?: string | undefined;
1047
+ traceId?: string | undefined;
1048
+ requestId?: string | undefined;
1049
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
1050
+ /**
1051
+ * 503 Service Unavailable - Service is temporarily unavailable
1052
+ */
1053
+ serviceUnavailable(c: Context, detail?: string): Response & hono.TypedResponse<{
1054
+ [x: string]: hono_utils_types.JSONValue;
1055
+ type: string;
1056
+ title: string;
1057
+ status: number;
1058
+ detail?: string | undefined;
1059
+ instance?: string | undefined;
1060
+ traceId?: string | undefined;
1061
+ requestId?: string | undefined;
1062
+ }, 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503, "json">;
1063
+ };
1064
+
1065
+ /**
1066
+ * Lightweight structured logger for Cloudflare Workers
1067
+ *
1068
+ * Passes objects directly to console methods for proper field indexing.
1069
+ * Workers Logs automatically extracts and indexes object properties.
1070
+ *
1071
+ * @see https://developers.cloudflare.com/workers/observability/logs/
1072
+ */
1073
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
1074
+ interface LogMetadata {
1075
+ [key: string]: unknown;
1076
+ }
1077
+ /**
1078
+ * Base logger that outputs structured JSON
1079
+ */
1080
+ declare class Logger {
1081
+ private minLevel;
1082
+ private isSilent;
1083
+ constructor(minLevel?: LogLevel);
1084
+ private shouldLog;
1085
+ private log;
1086
+ debug(message: string, meta?: LogMetadata): void;
1087
+ info(message: string, meta?: LogMetadata): void;
1088
+ warn(message: string, meta?: LogMetadata): void;
1089
+ error(message: string, meta?: LogMetadata): void;
1090
+ /**
1091
+ * Set minimum log level
1092
+ */
1093
+ setLevel(level: LogLevel): void;
1094
+ /**
1095
+ * Enable/disable silent mode
1096
+ * Silent mode suppresses all log output (useful for tests)
1097
+ */
1098
+ setSilent(silent: boolean): void;
1099
+ }
1100
+ declare const logger: Logger;
1101
+ /**
1102
+ * Log error with full context
1103
+ * Fields are automatically indexed in Workers Logs
1104
+ */
1105
+ declare function logError(error: Error | unknown, context?: LogMetadata): void;
1106
+ /**
1107
+ * Log security event
1108
+ */
1109
+ declare function logSecurityEvent(event: string, severity: 'low' | 'medium' | 'high' | 'critical', metadata?: LogMetadata): void;
1110
+
1111
+ /**
1112
+ * Email service types for Resend email provider integration
1113
+ */
1114
+ interface EmailTemplate {
1115
+ to: string;
1116
+ subject: string;
1117
+ html: string;
1118
+ text?: string;
1119
+ tags?: Record<string, string>;
1120
+ }
1121
+ interface VerificationEmailData {
1122
+ email: string;
1123
+ token: string;
1124
+ verificationUrl: string;
1125
+ firstName?: string;
1126
+ }
1127
+ interface PasswordResetEmailData {
1128
+ email: string;
1129
+ token: string;
1130
+ resetUrl: string;
1131
+ }
1132
+ interface EmailChangeConfirmationData {
1133
+ newEmail: string;
1134
+ confirmUrl: string;
1135
+ }
1136
+ interface EmailChangeNotificationData {
1137
+ oldEmail: string;
1138
+ newEmail: string;
1139
+ cancelUrl: string;
1140
+ }
1141
+ interface TwoFactorCodeEmailData {
1142
+ email: string;
1143
+ firstName: string | null;
1144
+ code: string;
1145
+ }
1146
+ interface TwoFactorEnabledEmailData {
1147
+ email: string;
1148
+ firstName: string | null;
1149
+ method: 'totp' | 'email';
1150
+ }
1151
+ interface TwoFactorDisabledEmailData {
1152
+ email: string;
1153
+ firstName: string | null;
1154
+ }
1155
+ type EmailType = 'verification' | 'password_reset' | 'email_change_confirmation' | 'email_change_notification' | '2fa_code' | '2fa_enabled' | '2fa_disabled';
1156
+ interface SendEmailResult {
1157
+ success: boolean;
1158
+ emailId?: string;
1159
+ error?: string;
1160
+ }
1161
+ type ResendEventType = 'email.sent' | 'email.delivered' | 'email.delivery_delayed' | 'email.complained' | 'email.bounced' | 'email.opened' | 'email.clicked' | 'email.failed';
1162
+ interface ResendWebhookPayload {
1163
+ type: ResendEventType;
1164
+ created_at: string;
1165
+ data: {
1166
+ email_id: string;
1167
+ from: string;
1168
+ to: string[];
1169
+ subject: string;
1170
+ created_at: string;
1171
+ tags?: Record<string, string>;
1172
+ bounce?: {
1173
+ message: string;
1174
+ subType: string;
1175
+ type: 'Permanent' | 'Transient';
1176
+ };
1177
+ failed?: {
1178
+ reason: string;
1179
+ };
1180
+ };
1181
+ }
1182
+
1183
+ /**
1184
+ * Email adapter interface for provider abstraction
1185
+ * Allows for provider abstraction (currently Resend)
1186
+ */
1187
+ interface EmailSendOptions {
1188
+ from: string;
1189
+ to: string;
1190
+ subject: string;
1191
+ html: string;
1192
+ text?: string;
1193
+ tags?: Record<string, string>;
1194
+ }
1195
+ interface EmailSendResult {
1196
+ success: boolean;
1197
+ emailId?: string;
1198
+ error?: string;
1199
+ }
1200
+ interface EmailAdapter {
1201
+ /**
1202
+ * Send an email through the provider
1203
+ */
1204
+ send(options: EmailSendOptions): Promise<EmailSendResult>;
1205
+ /**
1206
+ * Provider name for logging
1207
+ */
1208
+ readonly providerName: string;
1209
+ }
1210
+
1211
+ /**
1212
+ * Create the Resend email adapter
1213
+ *
1214
+ * @param env - Environment variables
1215
+ * @returns ResendAdapter instance
1216
+ * @throws Error if RESEND_API_KEY is missing
1217
+ */
1218
+ declare function createEmailAdapter(env: Env): EmailAdapter;
1219
+
1220
+ /**
1221
+ * Resend email adapter
1222
+ * Wraps the Resend SDK for sending transactional emails
1223
+ */
1224
+ declare class ResendAdapter implements EmailAdapter {
1225
+ private client;
1226
+ readonly providerName = "resend";
1227
+ constructor(apiKey: string);
1228
+ send(options: EmailSendOptions): Promise<EmailSendResult>;
1229
+ }
1230
+
1231
+ type UsersTable$1 = PgTableWithColumns<any>;
1232
+ declare class EmailService {
1233
+ private adapter;
1234
+ private env;
1235
+ private get fromAddress();
1236
+ constructor(env: Env, adapter?: EmailAdapter);
1237
+ /**
1238
+ * Check if email address has bounced or complained
1239
+ */
1240
+ private shouldBlockEmail;
1241
+ /**
1242
+ * Get recipient email based on environment
1243
+ * - Production & Staging: Use real email (verified domain on Resend)
1244
+ * - Local/Dev/Test: Use Resend test address to avoid sending real emails
1245
+ */
1246
+ getRecipientEmail(userEmail: string, emailType: EmailType): string;
1247
+ /**
1248
+ * Send verification email
1249
+ */
1250
+ sendVerificationEmail(data: VerificationEmailData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1251
+ /**
1252
+ * Send password reset email
1253
+ */
1254
+ sendPasswordResetEmail(data: PasswordResetEmailData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1255
+ /**
1256
+ * Send email change confirmation email (to new email address)
1257
+ */
1258
+ sendEmailChangeConfirmation(data: EmailChangeConfirmationData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1259
+ /**
1260
+ * Send email change notification email (to old email address)
1261
+ */
1262
+ sendEmailChangeNotification(data: EmailChangeNotificationData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1263
+ /**
1264
+ * Send 2FA verification code email
1265
+ */
1266
+ send2faCodeEmail(data: TwoFactorCodeEmailData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1267
+ /**
1268
+ * Send 2FA enabled confirmation email
1269
+ */
1270
+ send2faEnabledEmail(data: TwoFactorEnabledEmailData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1271
+ /**
1272
+ * Send 2FA disabled notification email
1273
+ */
1274
+ send2faDisabledEmail(data: TwoFactorDisabledEmailData, db: PostgresJsDatabase<Record<string, unknown>>, usersTable: UsersTable$1): Promise<SendEmailResult>;
1275
+ }
1276
+
1277
+ /**
1278
+ * Extract base URL from a full URL (e.g., https://flow.example.com/verify?token=xxx → https://flow.example.com)
1279
+ */
1280
+ declare function extractAppUrl(url: string): string;
1281
+ /**
1282
+ * Generate email verification email
1283
+ */
1284
+ declare function generateVerificationEmail(data: VerificationEmailData, appName?: string, configuredAppUrl?: string): EmailTemplate;
1285
+ /**
1286
+ * Generate password reset email
1287
+ */
1288
+ declare function generatePasswordResetEmail(data: PasswordResetEmailData, appName?: string, configuredAppUrl?: string): EmailTemplate;
1289
+ /**
1290
+ * Generate email change confirmation email (sent to NEW email)
1291
+ */
1292
+ declare function generateEmailChangeConfirmation(data: EmailChangeConfirmationData, appName?: string, configuredAppUrl?: string): EmailTemplate;
1293
+ /**
1294
+ * Generate email change notification email (sent to OLD email)
1295
+ */
1296
+ declare function generateEmailChangeNotification(data: EmailChangeNotificationData, appName?: string, configuredAppUrl?: string): EmailTemplate;
1297
+ /**
1298
+ * Generate 2FA verification code email
1299
+ */
1300
+ declare function generate2faCodeEmail(data: TwoFactorCodeEmailData, appName?: string, appUrl?: string): EmailTemplate;
1301
+ /**
1302
+ * Generate 2FA enabled confirmation email
1303
+ */
1304
+ declare function generate2faEnabledEmail(data: TwoFactorEnabledEmailData, appName?: string, appUrl?: string): EmailTemplate;
1305
+ /**
1306
+ * Generate 2FA disabled notification email
1307
+ */
1308
+ declare function generate2faDisabledEmail(data: TwoFactorDisabledEmailData, appName?: string, appUrl?: string): EmailTemplate;
1309
+
1310
+ type EmailEventsTable$1 = PgTableWithColumns<any>;
1311
+ type UsersTable = PgTableWithColumns<any>;
1312
+ /**
1313
+ * Handle Resend webhook event
1314
+ * Processes email delivery events and updates database accordingly
1315
+ */
1316
+ declare function handleWebhookEvent(payload: ResendWebhookPayload, db: PostgresJsDatabase<Record<string, unknown>>, tables: {
1317
+ emailEvents: EmailEventsTable$1;
1318
+ users: UsersTable;
1319
+ }): Promise<void>;
1320
+
1321
+ /**
1322
+ * Verify Resend webhook signature using svix
1323
+ * Resend uses svix for webhook signing
1324
+ */
1325
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
1326
+
1327
+ type EmailEventsTable = PgTableWithColumns<any>;
1328
+ /**
1329
+ * Calculate bounce rate for monitoring
1330
+ * Returns percentage of emails that bounced out of total sent
1331
+ */
1332
+ declare function calculateBounceRate(db: PostgresJsDatabase<Record<string, unknown>>, emailEventsTable: EmailEventsTable, hoursAgo?: number): Promise<number>;
1333
+ /**
1334
+ * Calculate complaint rate for monitoring
1335
+ * Returns percentage of emails marked as spam out of total sent
1336
+ */
1337
+ declare function calculateComplaintRate(db: PostgresJsDatabase<Record<string, unknown>>, emailEventsTable: EmailEventsTable, hoursAgo?: number): Promise<number>;
1338
+
1339
+ export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, type DeviceType, type EmailAdapter, 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, MAX_CHALLENGE_ATTEMPTS, type PasswordResetEmailData, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, 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, auth as 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 };