@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.cjs CHANGED
@@ -9,11 +9,11 @@ var __export = (target, all) => {
9
9
  for (var name in all)
10
10
  __defProp(target, name, { get: all[name], enumerable: true });
11
11
  };
12
- var __copyProps = (to, from, except, desc) => {
12
+ var __copyProps = (to, from, except, desc2) => {
13
13
  if (from && typeof from === "object" || typeof from === "function") {
14
14
  for (let key of __getOwnPropNames(from))
15
15
  if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
17
17
  }
18
18
  return to;
19
19
  };
@@ -35,6 +35,8 @@ __export(src_exports, {
35
35
  CloudflareEmailAdapter: () => CloudflareEmailAdapter,
36
36
  EmailService: () => EmailService,
37
37
  MAX_CHALLENGE_ATTEMPTS: () => MAX_CHALLENGE_ATTEMPTS,
38
+ PASSWORDLESS_MAX_ATTEMPTS: () => PASSWORDLESS_MAX_ATTEMPTS,
39
+ PASSWORDLESS_TTL_MS: () => PASSWORDLESS_TTL_MS,
38
40
  ProblemTypes: () => ProblemTypes,
39
41
  ResendAdapter: () => ResendAdapter,
40
42
  SCHEMA_VERSION: () => SCHEMA_VERSION,
@@ -46,6 +48,7 @@ __export(src_exports, {
46
48
  cancelEmailChange: () => cancelEmailChange,
47
49
  checkAccountLocked: () => checkAccountLocked,
48
50
  checkBreachedPassword: () => checkBreachedPassword,
51
+ cleanupExpiredPasswordlessChallenges: () => cleanupExpiredPasswordlessChallenges,
49
52
  clearAccountLockout: () => clearAccountLockout,
50
53
  clearChallengeCookie: () => clearChallengeCookie,
51
54
  clearSessionCookie: () => clearSessionCookie,
@@ -56,6 +59,7 @@ __export(src_exports, {
56
59
  createChallengeToken: () => createChallengeToken,
57
60
  createDeviceToken: () => createDeviceToken,
58
61
  createEmailAdapter: () => createEmailAdapter,
62
+ createPasswordlessChallenge: () => createPasswordlessChallenge,
59
63
  createProblemDetails: () => createProblemDetails,
60
64
  createSession: () => createSession,
61
65
  csrf: () => csrf,
@@ -72,6 +76,7 @@ __export(src_exports, {
72
76
  generate2faEnabledEmail: () => generate2faEnabledEmail,
73
77
  generateApiKey: () => generateApiKey,
74
78
  generateBackupCodes: () => generateBackupCodes,
79
+ generateCode: () => generateCode,
75
80
  generateCodeChallenge: () => generateCodeChallenge,
76
81
  generateCodeVerifier: () => generateCodeVerifier,
77
82
  generateEmailChangeConfirmation: () => generateEmailChangeConfirmation,
@@ -110,14 +115,17 @@ __export(src_exports, {
110
115
  optionalAuth: () => optionalAuth,
111
116
  parseDeviceName: () => parseDeviceName,
112
117
  parseDeviceType: () => parseDeviceType,
118
+ passwordlessEmail: () => passwordlessEmail,
113
119
  problemJson: () => problemJson,
114
120
  problems: () => problems,
115
121
  rateLimit: () => rateLimit,
122
+ recentPasswordlessFailures: () => recentPasswordlessFailures,
116
123
  refreshSession: () => refreshSession,
117
124
  requestEmailChange: () => requestEmailChange,
118
125
  requireAuth: () => requireAuth,
119
126
  requireVerifiedEmail: () => requireVerifiedEmail,
120
127
  retrieveChallengeToken: () => retrieveChallengeToken,
128
+ sessionRefreshDue: () => sessionRefreshDue,
121
129
  setChallengeCookie: () => setChallengeCookie,
122
130
  setSessionCookie: () => setSessionCookie,
123
131
  setTrustedDeviceCookie: () => setTrustedDeviceCookie,
@@ -135,6 +143,8 @@ __export(src_exports, {
135
143
  verifyCodeChallenge: () => verifyCodeChallenge,
136
144
  verifyPassword: () => verifyPassword,
137
145
  verifyPasswordWithRotation: () => verifyPasswordWithRotation,
146
+ verifyPasswordlessCode: () => verifyPasswordlessCode,
147
+ verifyPasswordlessLink: () => verifyPasswordlessLink,
138
148
  verifyTotpCode: () => verifyTotpCode,
139
149
  verifyTurnstileToken: () => verifyTurnstileToken,
140
150
  verifyWebhookSignature: () => verifyWebhookSignature
@@ -142,7 +152,7 @@ __export(src_exports, {
142
152
  module.exports = __toCommonJS(src_exports);
143
153
 
144
154
  // src/schema/definitions.ts
145
- var SCHEMA_VERSION = "2026.05.1";
155
+ var SCHEMA_VERSION = "2026.10.1";
146
156
  var enums = {
147
157
  identifierType: {
148
158
  name: "identifier_type_enum",
@@ -380,6 +390,23 @@ var tables = {
380
390
  { name: "oauth_accounts_user_provider_idx", columns: ["userId", "provider"], unique: true },
381
391
  { name: "oauth_accounts_provider_user_idx", columns: ["provider", "providerUserId"], unique: true }
382
392
  ]
393
+ },
394
+ passwordlessChallenges: {
395
+ columns: {
396
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
397
+ email: { type: "varchar", length: 255, notNull: true },
398
+ // The id of whatever account signs in, so no foreign key; null for a sign-up.
399
+ subjectId: { type: "uuid" },
400
+ linkTokenHash: { type: "varchar", length: 128, notNull: true, unique: true },
401
+ codeHash: { type: "varchar", length: 128, notNull: true },
402
+ browserHash: { type: "varchar", length: 128, notNull: true },
403
+ attempts: { type: "integer", notNull: true, default: 0 },
404
+ payload: { type: "jsonb" },
405
+ expiresAt: { type: "bigint", mode: "number", notNull: true },
406
+ consumedAt: { type: "bigint", mode: "number" },
407
+ createdAt: { type: "bigint", mode: "number", notNull: true }
408
+ },
409
+ indexes: [{ name: "idx_passwordless_email_created", columns: ["email", "createdAt"] }]
383
410
  }
384
411
  };
385
412
  var tableNames = {
@@ -394,7 +421,8 @@ var tableNames = {
394
421
  emailEvents: "email_events",
395
422
  failedLoginAttempts: "failed_login_attempts",
396
423
  securityAuditLog: "security_audit_log",
397
- oauthAccounts: "oauth_accounts"
424
+ oauthAccounts: "oauth_accounts",
425
+ passwordlessChallenges: "passwordless_challenges"
398
426
  };
399
427
 
400
428
  // src/core/password.ts
@@ -647,9 +675,14 @@ var AUTH_DEFAULTS = {
647
675
  // Absolute cap: a session dies this long after creation regardless of
648
676
  // sliding-window activity (limits the value of a stolen session token).
649
677
  SESSION_MAX_LIFETIME_DAYS: 30,
678
+ // The sliding window moves at most this often, so a busy session is not a write per request.
679
+ SESSION_REFRESH_INTERVAL_MINUTES: 15,
650
680
  LOCKOUT_DURATION_MINUTES: 30,
651
681
  LOCKOUT_MAX_ATTEMPTS: 5,
652
682
  PASSWORD_RESET_TTL_MINUTES: 15,
683
+ EMAIL_VERIFICATION_TTL_HOURS: 24,
684
+ EMAIL_CHANGE_TTL_HOURS: 24,
685
+ TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5,
653
686
  TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300,
654
687
  // 5 minutes
655
688
  APP_NAME: "App"
@@ -727,7 +760,7 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
727
760
  note: "SSR requests have different User-Agent, causing expected mismatch"
728
761
  });
729
762
  }
730
- await refreshSession(db, { sessions }, sessionId);
763
+ if (sessionRefreshDue(row.sessionLastActiveAt)) await refreshSession(db, { sessions }, sessionId);
731
764
  return {
732
765
  user: {
733
766
  id: row.userId,
@@ -746,6 +779,10 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
746
779
  }
747
780
  };
748
781
  }
782
+ function sessionRefreshDue(lastActiveAt, now = Date.now()) {
783
+ const interval = AUTH_DEFAULTS.SESSION_REFRESH_INTERVAL_MINUTES * 60 * 1e3;
784
+ return lastActiveAt === null || now - lastActiveAt >= interval;
785
+ }
749
786
  async function refreshSession(db, tables2, sessionId, sessionTtlMs = AUTH_DEFAULTS.SESSION_TTL_DAYS * 24 * 60 * 60 * 1e3) {
750
787
  const { sessions } = tables2;
751
788
  const newExpiration = Date.now() + sessionTtlMs;
@@ -913,16 +950,16 @@ function createDeviceToken() {
913
950
  };
914
951
  }
915
952
  async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
916
- const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
953
+ const { eq: eq34, and: and15, gt: gt7 } = await import("drizzle-orm");
917
954
  const [device] = await db.select().from(userTrustedDevicesTable).where(
918
- and14(
919
- eq33(userTrustedDevicesTable.userId, userId),
920
- eq33(userTrustedDevicesTable.tokenHash, tokenHash),
921
- gt6(userTrustedDevicesTable.expiresAt, Date.now())
955
+ and15(
956
+ eq34(userTrustedDevicesTable.userId, userId),
957
+ eq34(userTrustedDevicesTable.tokenHash, tokenHash),
958
+ gt7(userTrustedDevicesTable.expiresAt, Date.now())
922
959
  )
923
960
  ).limit(1);
924
961
  if (device) {
925
- await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
962
+ await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq34(userTrustedDevicesTable.id, device.id));
926
963
  return true;
927
964
  }
928
965
  return false;
@@ -1135,7 +1172,7 @@ async function verifyTurnstileToken(token, secretKey, remoteIp, environment) {
1135
1172
 
1136
1173
  // src/core/email-change.ts
1137
1174
  var import_drizzle_orm3 = require("drizzle-orm");
1138
- var TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1e3;
1175
+ var TOKEN_EXPIRY_MS = AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60 * 60 * 1e3;
1139
1176
  async function requestEmailChange(params) {
1140
1177
  const { userId, password, newEmail, pepper, db, tables: tables2 } = params;
1141
1178
  const { users, emailChangeTokens } = tables2;
@@ -1335,582 +1372,48 @@ function base64UrlEncode(data) {
1335
1372
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1336
1373
  }
1337
1374
 
1338
- // src/middleware/auth.ts
1339
- var import_factory = require("hono/factory");
1375
+ // src/core/passwordless.ts
1340
1376
  var import_drizzle_orm4 = require("drizzle-orm");
1341
1377
 
1342
- // src/factory.ts
1343
- function createAuth(config) {
1344
- const { db, schema, getEnv, emailBranding, invitations } = config;
1345
- const getContext = (c) => ({
1346
- db,
1347
- schema,
1348
- env: getEnv(c),
1349
- emailBranding,
1350
- invitations
1351
- });
1352
- return {
1353
- db,
1354
- schema,
1355
- getContext
1356
- };
1357
- }
1358
- function createAuthMiddleware(auth2) {
1359
- return async (c, next) => {
1360
- const ctx = auth2.getContext(c);
1361
- c.set("authContext", ctx);
1362
- c.set("db", auth2.db);
1363
- await next();
1364
- };
1365
- }
1366
- function getAuthContext(c) {
1367
- const ctx = c.get("authContext");
1368
- if (!ctx) {
1369
- throw new Error(
1370
- "AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
1371
- );
1378
+ // src/lib/email/security.ts
1379
+ function getAllowedDomains(appUrl) {
1380
+ const base = ["localhost", "127.0.0.1"];
1381
+ try {
1382
+ const url = new URL(appUrl);
1383
+ return [...base, url.hostname];
1384
+ } catch {
1385
+ return base;
1372
1386
  }
1373
- return ctx;
1374
- }
1375
-
1376
- // src/lib/problem-json.ts
1377
- var ProblemTypes = {
1378
- // 4xx Client Errors
1379
- BAD_REQUEST: "/errors/bad-request",
1380
- UNAUTHORIZED: "/errors/unauthorized",
1381
- FORBIDDEN: "/errors/forbidden",
1382
- NOT_FOUND: "/errors/not-found",
1383
- CONFLICT: "/errors/conflict",
1384
- GONE: "/errors/gone",
1385
- VALIDATION_ERROR: "/errors/validation-error",
1386
- RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
1387
- // 5xx Server Errors
1388
- INTERNAL_ERROR: "/errors/internal-error",
1389
- SERVICE_UNAVAILABLE: "/errors/service-unavailable"
1390
- };
1391
- function generateTraceId() {
1392
- const bytes = crypto.getRandomValues(new Uint8Array(16));
1393
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1394
1387
  }
1395
- function createProblemDetails(type, title, status, options) {
1396
- const problem = {
1397
- type,
1398
- title,
1399
- status
1400
- };
1401
- if (options?.detail) {
1402
- problem.detail = options.detail;
1403
- }
1404
- if (options?.instance) {
1405
- problem.instance = options.instance;
1388
+ function sanitizeEmailUrl(url, appUrl = "") {
1389
+ let parsed;
1390
+ try {
1391
+ parsed = new URL(url);
1392
+ } catch {
1393
+ throw new Error("Invalid URL format");
1406
1394
  }
1407
- problem.traceId = options?.traceId || generateTraceId();
1408
- if (options?.requestId) {
1409
- problem.requestId = options.requestId;
1395
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1396
+ throw new Error(`Disallowed protocol: ${parsed.protocol}`);
1410
1397
  }
1411
- if (options?.extensions) {
1412
- Object.assign(problem, options.extensions);
1398
+ const allowedDomains = getAllowedDomains(appUrl);
1399
+ const hostname = parsed.hostname.toLowerCase();
1400
+ const isAllowed = allowedDomains.some((domain) => {
1401
+ return hostname === domain || hostname.endsWith(`.${domain}`);
1402
+ });
1403
+ if (!isAllowed) {
1404
+ throw new Error(`Disallowed domain: ${hostname}`);
1413
1405
  }
1414
- return problem;
1406
+ return parsed.toString();
1415
1407
  }
1416
- function problemJson(c, problem) {
1417
- logger_default.warn("API error response", {
1418
- type: problem.type,
1419
- status: problem.status,
1420
- traceId: problem.traceId,
1421
- requestId: problem.requestId,
1422
- path: c.req.path,
1423
- method: c.req.method
1424
- });
1425
- return c.json(problem, problem.status, {
1426
- "Content-Type": "application/problem+json"
1427
- });
1428
- }
1429
- var problems = {
1430
- /**
1431
- * 400 Bad Request - Generic client error
1432
- */
1433
- badRequest(c, detail) {
1434
- return problemJson(
1435
- c,
1436
- createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
1437
- detail: detail || "The request could not be understood by the server",
1438
- instance: `${c.req.method} ${c.req.path}`,
1439
- requestId: c.get("requestId"),
1440
- traceId: c.get("traceId")
1441
- })
1442
- );
1443
- },
1444
- /**
1445
- * 400 Bad Request - Validation errors with field-specific details
1446
- */
1447
- validationError(c, detail, errors) {
1448
- return problemJson(
1449
- c,
1450
- createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
1451
- detail,
1452
- instance: `${c.req.method} ${c.req.path}`,
1453
- requestId: c.get("requestId"),
1454
- traceId: c.get("traceId"),
1455
- extensions: errors ? { errors } : void 0
1456
- })
1457
- );
1458
- },
1459
- /**
1460
- * 401 Unauthorized - Authentication required
1461
- */
1462
- unauthorized(c, detail) {
1463
- return problemJson(
1464
- c,
1465
- createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
1466
- detail: detail || "Authentication is required to access this resource",
1467
- instance: `${c.req.method} ${c.req.path}`,
1468
- requestId: c.get("requestId"),
1469
- traceId: c.get("traceId")
1470
- })
1471
- );
1472
- },
1473
- /**
1474
- * 403 Forbidden - Insufficient permissions
1475
- */
1476
- forbidden(c, detail) {
1477
- return problemJson(
1478
- c,
1479
- createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
1480
- detail: detail || "You do not have permission to access this resource",
1481
- instance: `${c.req.method} ${c.req.path}`,
1482
- requestId: c.get("requestId"),
1483
- traceId: c.get("traceId")
1484
- })
1485
- );
1486
- },
1487
- /**
1488
- * 404 Not Found
1489
- */
1490
- notFound(c, detail) {
1491
- return problemJson(
1492
- c,
1493
- createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
1494
- detail: detail || "The requested resource was not found",
1495
- instance: `${c.req.method} ${c.req.path}`,
1496
- requestId: c.get("requestId"),
1497
- traceId: c.get("traceId")
1498
- })
1499
- );
1500
- },
1501
- /**
1502
- * 409 Conflict - Resource conflict (e.g., duplicate email)
1503
- */
1504
- conflict(c, detail) {
1505
- return problemJson(
1506
- c,
1507
- createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
1508
- detail,
1509
- instance: `${c.req.method} ${c.req.path}`,
1510
- requestId: c.get("requestId"),
1511
- traceId: c.get("traceId")
1512
- })
1513
- );
1514
- },
1515
- /**
1516
- * 410 Gone - Resource is no longer available
1517
- */
1518
- gone(c, detail) {
1519
- return problemJson(
1520
- c,
1521
- createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
1522
- detail: detail || "The requested resource is no longer available",
1523
- instance: `${c.req.method} ${c.req.path}`,
1524
- requestId: c.get("requestId"),
1525
- traceId: c.get("traceId")
1526
- })
1527
- );
1528
- },
1529
- /**
1530
- * 429 Too Many Requests - Rate limit exceeded
1531
- */
1532
- rateLimitExceeded(c, retryAfter) {
1533
- const extensions = retryAfter ? { retryAfter } : void 0;
1534
- return problemJson(
1535
- c,
1536
- createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
1537
- detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
1538
- instance: `${c.req.method} ${c.req.path}`,
1539
- requestId: c.get("requestId"),
1540
- traceId: c.get("traceId"),
1541
- extensions
1542
- })
1543
- );
1544
- },
1545
- /**
1546
- * 500 Internal Server Error - Generic server error
1547
- * SECURITY: Never include error details or stack traces
1548
- */
1549
- internalError(c, error) {
1550
- const traceId = c.get("traceId") || generateTraceId();
1551
- if (error) {
1552
- logError(error, {
1553
- context: "internal_error",
1554
- path: c.req.path,
1555
- method: c.req.method,
1556
- traceId,
1557
- requestId: c.get("requestId")
1558
- });
1559
- }
1560
- return problemJson(
1561
- c,
1562
- createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
1563
- detail: "An unexpected error occurred. Please try again later",
1564
- instance: `${c.req.method} ${c.req.path}`,
1565
- traceId,
1566
- requestId: c.get("requestId")
1567
- })
1568
- );
1569
- },
1570
- /**
1571
- * 503 Service Unavailable - Service is temporarily unavailable
1572
- */
1573
- serviceUnavailable(c, detail) {
1574
- return problemJson(
1575
- c,
1576
- createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
1577
- detail: detail || "The service is temporarily unavailable. Please try again later",
1578
- instance: `${c.req.method} ${c.req.path}`,
1579
- requestId: c.get("requestId"),
1580
- traceId: c.get("traceId")
1581
- })
1582
- );
1583
- }
1584
- };
1585
-
1586
- // src/middleware/auth.ts
1587
- var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
1588
- async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
1589
- if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
1590
- logger_default.info("Session exceeded absolute lifetime - revoked", {
1591
- type: "security",
1592
- event: "session_max_lifetime_exceeded",
1593
- severity: "low",
1594
- sessionId: sessionId.slice(0, 8),
1595
- userId: session.userId
1596
- });
1597
- await deleteSession(db, { sessions }, sessionId);
1598
- return true;
1599
- }
1600
- async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
1601
- const cookieName = getSessionCookieName(env);
1602
- const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
1603
- const sessionIdMatch = cookieHeader.match(cookiePattern);
1604
- if (!sessionIdMatch) return null;
1605
- const sessionId = sessionIdMatch[1];
1606
- const foundSessions = await db.select({
1607
- userId: sessions.userId,
1608
- expiresAt: sessions.expiresAt,
1609
- createdAt: sessions.createdAt,
1610
- fingerprint: sessions.fingerprint
1611
- }).from(sessions).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(sessions.id, sessionId), (0, import_drizzle_orm4.gt)(sessions.expiresAt, Date.now()))).limit(1);
1612
- if (foundSessions.length === 0) return null;
1613
- const session = foundSessions[0];
1614
- if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1615
- await refreshSession(db, { sessions }, sessionId);
1616
- const userAgent = request.headers.get("user-agent") || "";
1617
- const cfWorker = request.headers.get("cf-worker") || "";
1618
- const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
1619
- const isLocalSSR = userAgent.toLowerCase().includes("node");
1620
- const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
1621
- const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
1622
- const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
1623
- if (isSSR) {
1624
- logger_default.info("SSR request detected, using IP validation", {
1625
- type: "security",
1626
- event: "ssr_session_validation",
1627
- sessionId: sessionId.slice(0, 8),
1628
- userId: session.userId,
1629
- clientIp,
1630
- cfWorker,
1631
- isLocalSSR,
1632
- isSvelteKitSSR,
1633
- isCloudflareSSR
1634
- });
1635
- } else {
1636
- if (session.fingerprint) {
1637
- const currentFingerprint = await generateFingerprint(request);
1638
- if (session.fingerprint !== currentFingerprint) {
1639
- logger_default.info("Session fingerprint changed (not enforced)", {
1640
- type: "security",
1641
- event: "fingerprint_changed",
1642
- sessionId: sessionId.slice(0, 8),
1643
- userId: session.userId,
1644
- clientIp
1645
- });
1646
- }
1647
- }
1648
- }
1649
- return {
1650
- userId: session.userId,
1651
- expiresAt: session.expiresAt
1652
- };
1653
- }
1654
- async function getSessionFromBearerToken(db, sessions, authHeader) {
1655
- if (!authHeader.startsWith("Bearer ")) return null;
1656
- const sessionId = authHeader.slice(7);
1657
- const foundSessions = await db.select({
1658
- userId: sessions.userId,
1659
- expiresAt: sessions.expiresAt,
1660
- createdAt: sessions.createdAt
1661
- }).from(sessions).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(sessions.id, sessionId), (0, import_drizzle_orm4.gt)(sessions.expiresAt, Date.now()))).limit(1);
1662
- if (foundSessions.length === 0) return null;
1663
- const session = foundSessions[0];
1664
- if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1665
- await refreshSession(db, { sessions }, sessionId);
1666
- return {
1667
- userId: session.userId,
1668
- expiresAt: session.expiresAt
1669
- };
1670
- }
1671
- var requireAuth = (0, import_factory.createMiddleware)(
1672
- async (c, next) => {
1673
- const { db, schema } = getAuthContext(c);
1674
- const authHeader = c.req.header("Authorization");
1675
- if (authHeader) {
1676
- const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1677
- if (session2) {
1678
- c.set("userId", session2.userId);
1679
- await next();
1680
- return;
1681
- }
1682
- }
1683
- const cookieHeader = c.req.header("cookie");
1684
- if (!cookieHeader) {
1685
- return problems.unauthorized(c, "Please log in to continue");
1686
- }
1687
- const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1688
- if (!session) {
1689
- c.header("Set-Cookie", clearSessionCookie(c.env));
1690
- return problems.unauthorized(c, "Your session has expired. Please log in again.");
1691
- }
1692
- c.set("userId", session.userId);
1693
- await next();
1694
- }
1695
- );
1696
- var optionalAuth = (0, import_factory.createMiddleware)(
1697
- async (c, next) => {
1698
- const { db, schema } = getAuthContext(c);
1699
- const authHeader = c.req.header("Authorization");
1700
- if (authHeader) {
1701
- const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1702
- if (session) {
1703
- c.set("userId", session.userId);
1704
- await next();
1705
- return;
1706
- }
1707
- }
1708
- const cookieHeader = c.req.header("cookie");
1709
- if (cookieHeader) {
1710
- const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1711
- if (session) {
1712
- c.set("userId", session.userId);
1713
- }
1714
- }
1715
- await next();
1716
- }
1717
- );
1718
-
1719
- // src/middleware/csrf.ts
1720
- var import_factory3 = require("hono/factory");
1721
- var csrf = (0, import_factory3.createMiddleware)(async (c, next) => {
1722
- const method = c.req.method;
1723
- const path = c.req.path;
1724
- const isWebhook = path.startsWith("/v1/webhooks/");
1725
- if (isWebhook) {
1726
- await next();
1727
- return;
1728
- }
1729
- const isMcpEndpoint = path === "/v1/mcp";
1730
- if (isMcpEndpoint) {
1731
- await next();
1732
- return;
1733
- }
1734
- const isOAuthMcpEndpoint = path.startsWith("/v1/oauth/");
1735
- if (isOAuthMcpEndpoint) {
1736
- await next();
1737
- return;
1738
- }
1739
- const isToolsApi = path.startsWith("/v1/tools/") && !path.startsWith("/v1/tools/catalog");
1740
- if (isToolsApi) {
1741
- await next();
1742
- return;
1743
- }
1744
- const isAdminApi = path.startsWith("/v1/admin/");
1745
- if (isAdminApi) {
1746
- await next();
1747
- return;
1748
- }
1749
- const isMobileAuthEndpoint = path === "/v1/auth/login" || path === "/v1/auth/signup" || path === "/v1/auth/refresh";
1750
- const authHeader = c.req.header("Authorization");
1751
- const hasBearerToken = authHeader?.startsWith("Bearer ");
1752
- const hasNoOrigin = !c.req.header("origin") && !c.req.header("referer");
1753
- if (isMobileAuthEndpoint && hasNoOrigin || hasBearerToken) {
1754
- await next();
1755
- return;
1756
- }
1757
- const isInternalSync = path.startsWith("/v1/internal/sync/");
1758
- if (isInternalSync) {
1759
- await next();
1760
- return;
1761
- }
1762
- if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
1763
- const appUrl = c.env.APP_URL || "http://localhost:5173";
1764
- const allowedOrigins = getAllowedOrigins(appUrl);
1765
- const request = c.req.raw;
1766
- const isTestEndpoint = c.req.path.startsWith("/test");
1767
- if (isTestEndpoint) {
1768
- const origin = request.headers.get("origin");
1769
- const referer = request.headers.get("referer");
1770
- logger_default.debug("CSRF validation for test endpoint", {
1771
- path: c.req.path,
1772
- method,
1773
- origin,
1774
- referer,
1775
- appUrl,
1776
- allowedOrigins
1777
- });
1778
- }
1779
- const pagesPattern = c.env.CORS_PAGES_PATTERN;
1780
- if (!validateOrigin(request, allowedOrigins, pagesPattern)) {
1781
- return problems.forbidden(c, "Request blocked for security reasons. Please try again.");
1782
- }
1783
- }
1784
- await next();
1785
- });
1786
-
1787
- // src/middleware/rateLimit.ts
1788
- var import_factory4 = require("hono/factory");
1789
-
1790
- // src/lib/rate-limit-kv.ts
1791
- async function checkRateLimitKV(kv, key, maxAttempts, windowSeconds) {
1792
- try {
1793
- const rateLimitKey = `rate_limit:${key}`;
1794
- const currentAttemptsStr = await kv.get(rateLimitKey);
1795
- const currentAttempts = currentAttemptsStr ? parseInt(currentAttemptsStr, 10) : 0;
1796
- if (currentAttempts >= maxAttempts) {
1797
- return {
1798
- allowed: false,
1799
- remaining: 0
1800
- };
1801
- }
1802
- const newAttempts = currentAttempts + 1;
1803
- await kv.put(rateLimitKey, newAttempts.toString(), {
1804
- expirationTtl: windowSeconds
1805
- });
1806
- return {
1807
- allowed: true,
1808
- remaining: maxAttempts - newAttempts
1809
- };
1810
- } catch (error) {
1811
- logger_default.error("Rate limit KV error, failing open", {
1812
- key,
1813
- error: error instanceof Error ? error.message : "Unknown error"
1814
- });
1815
- return {
1816
- allowed: true,
1817
- remaining: maxAttempts
1818
- };
1819
- }
1820
- }
1821
-
1822
- // src/middleware/rateLimit.ts
1823
- var rateLimit = (config) => (0, import_factory4.createMiddleware)(async (c, next) => {
1824
- const kv = c.env.RATE_LIMIT_KV;
1825
- const identifier = await config.identifier(c);
1826
- const key = `${config.action}:${identifier}`;
1827
- const windowSeconds = Math.ceil(config.windowMs / 1e3);
1828
- const { allowed, remaining } = await checkRateLimitKV(
1829
- kv,
1830
- key,
1831
- config.maxAttempts,
1832
- windowSeconds
1833
- );
1834
- const resetTimestamp = Math.ceil(Date.now() / 1e3) + windowSeconds;
1835
- c.header("X-RateLimit-Limit", config.maxAttempts.toString());
1836
- c.header("X-RateLimit-Remaining", Math.max(0, remaining).toString());
1837
- c.header("X-RateLimit-Reset", resetTimestamp.toString());
1838
- if (!allowed) {
1839
- c.header("Retry-After", windowSeconds.toString());
1840
- return problems.rateLimitExceeded(c, windowSeconds);
1841
- }
1842
- await next();
1843
- });
1844
-
1845
- // src/middleware/require-verified-email.ts
1846
- var import_factory5 = require("hono/factory");
1847
- var import_drizzle_orm5 = require("drizzle-orm");
1848
- var requireVerifiedEmail = (0, import_factory5.createMiddleware)(
1849
- async (c, next) => {
1850
- const userId = c.get("userId");
1851
- if (!userId) {
1852
- return problems.unauthorized(c, "Not authenticated");
1853
- }
1854
- const { db, schema } = getAuthContext(c);
1855
- const database = db;
1856
- const users = schema.users;
1857
- const [user] = await database.select({ emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm5.eq)(users.id, userId)).limit(1);
1858
- if (!user || !user.emailVerified) {
1859
- return problems.forbidden(c, "Please verify your email address to continue");
1860
- }
1861
- await next();
1862
- }
1863
- );
1864
-
1865
- // src/routes/index.ts
1866
- var import_zod_openapi42 = require("@hono/zod-openapi");
1867
-
1868
- // src/routes/signup.ts
1869
- var import_zod_openapi2 = require("@hono/zod-openapi");
1870
- var import_drizzle_orm9 = require("drizzle-orm");
1871
-
1872
- // src/lib/email/email-service.ts
1873
- var import_drizzle_orm6 = require("drizzle-orm");
1874
-
1875
- // src/lib/email/security.ts
1876
- function getAllowedDomains(appUrl) {
1877
- const base = ["localhost", "127.0.0.1"];
1878
- try {
1879
- const url = new URL(appUrl);
1880
- return [...base, url.hostname];
1881
- } catch {
1882
- return base;
1883
- }
1884
- }
1885
- function sanitizeEmailUrl(url, appUrl = "") {
1886
- let parsed;
1887
- try {
1888
- parsed = new URL(url);
1889
- } catch {
1890
- throw new Error("Invalid URL format");
1891
- }
1892
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1893
- throw new Error(`Disallowed protocol: ${parsed.protocol}`);
1894
- }
1895
- const allowedDomains = getAllowedDomains(appUrl);
1896
- const hostname = parsed.hostname.toLowerCase();
1897
- const isAllowed = allowedDomains.some((domain) => {
1898
- return hostname === domain || hostname.endsWith(`.${domain}`);
1899
- });
1900
- if (!isAllowed) {
1901
- throw new Error(`Disallowed domain: ${hostname}`);
1902
- }
1903
- return parsed.toString();
1904
- }
1905
- function escapeHtml(text) {
1906
- const htmlEscapeMap = {
1907
- "&": "&amp;",
1908
- "<": "&lt;",
1909
- ">": "&gt;",
1910
- '"': "&quot;",
1911
- "'": "&#39;"
1912
- };
1913
- return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char]);
1408
+ function escapeHtml(text) {
1409
+ const htmlEscapeMap = {
1410
+ "&": "&amp;",
1411
+ "<": "&lt;",
1412
+ ">": "&gt;",
1413
+ '"': "&quot;",
1414
+ "'": "&#39;"
1415
+ };
1416
+ return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char]);
1914
1417
  }
1915
1418
 
1916
1419
  // src/core/email-template.ts
@@ -1924,19 +1427,20 @@ function generateEmailTemplate(options) {
1924
1427
  securityWarning,
1925
1428
  footerNote,
1926
1429
  appUrl,
1927
- marketingUrl = appUrl,
1928
- // Default to appUrl if no marketing site
1430
+ marketingUrl,
1929
1431
  appName = "App",
1930
1432
  buttonBgColor = "#00fe9a",
1931
1433
  buttonTextColor = "#000004",
1932
1434
  buttonFontWeight = 500,
1933
1435
  buttonRadius = "6px",
1934
1436
  logoUrl,
1935
- footerColor = "#1e1b4b"
1437
+ footerColor = "#1e1b4b",
1438
+ headingColor = "#1e1b4b"
1936
1439
  } = options;
1440
+ const safeAppName = escapeHtml(appName);
1937
1441
  const safeButtonUrl = buttonUrl ? sanitizeEmailUrl(buttonUrl, appUrl) : "";
1938
1442
  const safeAppUrl = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
1939
- const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, appUrl) : "";
1443
+ const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, marketingUrl).replace(/\/$/, "") : "";
1940
1444
  const safeLogoUrl = logoUrl ? sanitizeEmailUrl(logoUrl, appUrl) : `${safeAppUrl.replace(/\/$/, "")}/email-header-image.png`;
1941
1445
  return `
1942
1446
  <!DOCTYPE html>
@@ -2077,7 +1581,7 @@ function generateEmailTemplate(options) {
2077
1581
  <td class="pad" style="padding-left:40px;padding-right:40px;width:100%;">
2078
1582
  <div class="alignment" align="center">
2079
1583
  <div class="fullWidth" style="max-width: 50%;">
2080
- <img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${appName}" title="${appName}" height="auto">
1584
+ <img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${safeAppName}" title="${safeAppName}" height="auto">
2081
1585
  </div>
2082
1586
  </div>
2083
1587
  </td>
@@ -2089,7 +1593,7 @@ function generateEmailTemplate(options) {
2089
1593
  <table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
2090
1594
  <tr>
2091
1595
  <td class="pad" style="padding-top:30px;padding-left:40px;padding-right:40px;">
2092
- <div style="color:#1e1b4b;font-family:'Montserrat',Arial,sans-serif;font-size:22px;font-weight:600;line-height:1.4;text-align:center;mso-line-height-alt:31px;">
1596
+ <div style="color:${headingColor};font-family:'Montserrat',Arial,sans-serif;font-size:22px;font-weight:600;line-height:1.4;text-align:center;mso-line-height-alt:31px;">
2093
1597
  <p style="margin: 0; word-break: break-word;">${welcomeMessage}</p>
2094
1598
  </div>
2095
1599
  </td>
@@ -2102,7 +1606,7 @@ function generateEmailTemplate(options) {
2102
1606
  <table class="paragraph_block block-4" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
2103
1607
  <tr>
2104
1608
  <td class="pad" style="padding-bottom:10px;padding-left:40px;padding-right:40px;padding-top:10px;">
2105
- <div style="color:#1e1b4b;font-family:'Montserrat',Arial,sans-serif;font-size:30px;font-weight:700;line-height:1.2;text-align:center;mso-line-height-alt:36px;">
1609
+ <div style="color:${headingColor};font-family:'Montserrat',Arial,sans-serif;font-size:30px;font-weight:700;line-height:1.2;text-align:center;mso-line-height-alt:36px;">
2106
1610
  <p style="margin: 0; word-break: break-word;">${heading}</p>
2107
1611
  </div>
2108
1612
  </td>
@@ -2178,104 +1682,778 @@ function generateEmailTemplate(options) {
2178
1682
  <!-- Footer Note -->
2179
1683
  <table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
2180
1684
  <tr>
2181
- <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
2182
- <div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
2183
- <p style="margin: 0; word-break: break-word;">${footerNote}</p>
1685
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1686
+ <div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
1687
+ <p style="margin: 0; word-break: break-word;">${footerNote}</p>
1688
+ </div>
1689
+ </td>
1690
+ </tr>
1691
+ </table>
1692
+
1693
+ </td>
1694
+ </tr>
1695
+ </tbody>
1696
+ </table>
1697
+ </td>
1698
+ </tr>
1699
+ </tbody>
1700
+ </table>
1701
+
1702
+ <!-- Spacer Row -->
1703
+ <table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
1704
+ <tbody>
1705
+ <tr>
1706
+ <td>
1707
+ <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f8f9fa; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
1708
+ <tbody>
1709
+ <tr>
1710
+ <td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; padding-bottom: 5px; vertical-align: top;">
1711
+ <table class="empty_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
1712
+ <tr>
1713
+ <td class="pad">
1714
+ <div></div>
1715
+ </td>
1716
+ </tr>
1717
+ </table>
1718
+ </td>
1719
+ </tr>
1720
+ </tbody>
1721
+ </table>
1722
+ </td>
1723
+ </tr>
1724
+ </tbody>
1725
+ </table>
1726
+
1727
+ <!-- Footer Row -->
1728
+ <table class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
1729
+ <tbody>
1730
+ <tr>
1731
+ <td>
1732
+ <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: ${footerColor}; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
1733
+ <tbody>
1734
+ <tr>
1735
+ <td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding: 20px;">
1736
+
1737
+ ${safeMarketingUrl ? `
1738
+ <!-- Policy Links -->
1739
+ <table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
1740
+ <tr>
1741
+ <td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
1742
+ <div style="color:#ffffff;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
1743
+ <p style="margin: 0; word-break: break-word;">
1744
+ <a href="${safeMarketingUrl}/privacy" style="color: #ffffff; text-decoration: underline;">Privacy Policy</a> | <a href="${safeMarketingUrl}/contact" style="color: #ffffff; text-decoration: underline;">Get in Touch</a>
1745
+ </p>
1746
+ </div>
1747
+ </td>
1748
+ </tr>
1749
+ </table>
1750
+ ` : ""}
1751
+
1752
+ <!-- Copyright & Disclaimer -->
1753
+ <table class="paragraph_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
1754
+ <tr>
1755
+ <td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
1756
+ <div style="color:#a3a3a3;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
1757
+ <p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
1758
+ <p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
1759
+ Please do not reply to this email as this address does not accept incoming messages. This service email contains essential information relating to your ${safeAppName} account.
1760
+ </p>
2184
1761
  </div>
2185
1762
  </td>
2186
1763
  </tr>
2187
1764
  </table>
2188
1765
 
2189
- </td>
2190
- </tr>
2191
- </tbody>
2192
- </table>
2193
- </td>
2194
- </tr>
2195
- </tbody>
2196
- </table>
1766
+ </td>
1767
+ </tr>
1768
+ </tbody>
1769
+ </table>
1770
+ </td>
1771
+ </tr>
1772
+ </tbody>
1773
+ </table>
1774
+
1775
+ </td>
1776
+ </tr>
1777
+ </tbody>
1778
+ </table>
1779
+ </body>
1780
+ </html>
1781
+ `;
1782
+ }
1783
+
1784
+ // src/core/passwordless.ts
1785
+ var PASSWORDLESS_TTL_MS = 10 * 60 * 1e3;
1786
+ var PASSWORDLESS_MAX_ATTEMPTS = 5;
1787
+ var CODE_SPACE = 1e6;
1788
+ var CODE_LIMIT = Math.floor(4294967296 / CODE_SPACE) * CODE_SPACE;
1789
+ var CLEANUP_GRACE_MS = 24 * 60 * 60 * 1e3;
1790
+ var normalizeEmail = (email) => email.trim().toLowerCase();
1791
+ function generateCode() {
1792
+ const buffer = new Uint32Array(1);
1793
+ for (; ; ) {
1794
+ crypto.getRandomValues(buffer);
1795
+ if (buffer[0] < CODE_LIMIT) return String(buffer[0] % CODE_SPACE).padStart(6, "0");
1796
+ }
1797
+ }
1798
+ var codeHash = (challengeId, code) => hashToken(`${challengeId}:${code}`);
1799
+ function constantTimeEqual(a, b) {
1800
+ if (a.length !== b.length) return false;
1801
+ let diff = 0;
1802
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1803
+ return diff === 0;
1804
+ }
1805
+ async function consumeOpen(db, t, email, now) {
1806
+ await db.update(t).set({ consumedAt: now }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.email, email), (0, import_drizzle_orm4.isNull)(t.consumedAt)));
1807
+ }
1808
+ async function createPasswordlessChallenge(db, tables2, input) {
1809
+ const t = tables2.passwordlessChallenges;
1810
+ const email = normalizeEmail(input.email);
1811
+ const now = Date.now();
1812
+ const challengeId = crypto.randomUUID();
1813
+ const linkToken = generateSecureToken(32);
1814
+ const browserSecret = generateSecureToken(32);
1815
+ const code = generateCode();
1816
+ await consumeOpen(db, t, email, now);
1817
+ await db.insert(t).values({
1818
+ id: challengeId,
1819
+ email,
1820
+ subjectId: input.subjectId ?? null,
1821
+ linkTokenHash: await hashToken(linkToken),
1822
+ codeHash: await codeHash(challengeId, code),
1823
+ browserHash: await hashToken(browserSecret),
1824
+ attempts: 0,
1825
+ payload: input.payload ?? null,
1826
+ expiresAt: now + (input.ttlMs ?? PASSWORDLESS_TTL_MS),
1827
+ createdAt: now
1828
+ });
1829
+ return { challengeId, linkToken, code, browserSecret };
1830
+ }
1831
+ async function verifyPasswordlessLink(db, tables2, input) {
1832
+ const t = tables2.passwordlessChallenges;
1833
+ const now = Date.now();
1834
+ const hash = await hashToken(input.linkToken);
1835
+ const [row] = await db.update(t).set({ consumedAt: now }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.linkTokenHash, hash), (0, import_drizzle_orm4.isNull)(t.consumedAt), (0, import_drizzle_orm4.gt)(t.expiresAt, now))).returning();
1836
+ if (!row) {
1837
+ const [stale] = await db.select({ expiresAt: t.expiresAt, consumedAt: t.consumedAt }).from(t).where((0, import_drizzle_orm4.eq)(t.linkTokenHash, hash)).limit(1);
1838
+ const expired = stale && stale.consumedAt === null && stale.expiresAt <= now;
1839
+ return { ok: false, reason: expired ? "expired" : "invalid" };
1840
+ }
1841
+ await consumeOpen(db, t, row.email, now);
1842
+ return { ok: true, email: row.email, subjectId: row.subjectId, payload: row.payload };
1843
+ }
1844
+ async function verifyPasswordlessCode(db, tables2, input) {
1845
+ const t = tables2.passwordlessChallenges;
1846
+ const max = input.maxAttempts ?? PASSWORDLESS_MAX_ATTEMPTS;
1847
+ const email = normalizeEmail(input.email);
1848
+ const now = Date.now();
1849
+ const [row] = await db.select().from(t).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.email, email), (0, import_drizzle_orm4.isNull)(t.consumedAt))).orderBy((0, import_drizzle_orm4.desc)(t.createdAt)).limit(1);
1850
+ if (!row) return { ok: false, reason: "invalid" };
1851
+ if (!constantTimeEqual(row.browserHash, await hashToken(input.browserSecret))) {
1852
+ return { ok: false, reason: "invalid" };
1853
+ }
1854
+ if (row.expiresAt <= now) return { ok: false, reason: "expired" };
1855
+ if (row.attempts >= max) return { ok: false, reason: "locked" };
1856
+ if (!constantTimeEqual(row.codeHash, await codeHash(row.id, input.code.trim()))) {
1857
+ const [counted] = await db.update(t).set({ attempts: import_drizzle_orm4.sql`${t.attempts} + 1` }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.id, row.id), (0, import_drizzle_orm4.lt)(t.attempts, max))).returning({ attempts: t.attempts });
1858
+ return { ok: false, reason: counted && counted.attempts < max ? "invalid" : "locked" };
1859
+ }
1860
+ const [won] = await db.update(t).set({ consumedAt: now }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.id, row.id), (0, import_drizzle_orm4.isNull)(t.consumedAt), (0, import_drizzle_orm4.lt)(t.attempts, max))).returning();
1861
+ if (!won) return { ok: false, reason: "invalid" };
1862
+ await consumeOpen(db, t, email, now);
1863
+ return { ok: true, email, subjectId: won.subjectId, payload: won.payload };
1864
+ }
1865
+ async function recentPasswordlessFailures(db, tables2, input) {
1866
+ const t = tables2.passwordlessChallenges;
1867
+ const [row] = await db.select({ total: import_drizzle_orm4.sql`coalesce(sum(${t.attempts}), 0)::int` }).from(t).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.email, normalizeEmail(input.email)), (0, import_drizzle_orm4.gt)(t.createdAt, Date.now() - input.windowMs)));
1868
+ return Number(row?.total ?? 0);
1869
+ }
1870
+ async function cleanupExpiredPasswordlessChallenges(db, tables2) {
1871
+ const t = tables2.passwordlessChallenges;
1872
+ await db.delete(t).where((0, import_drizzle_orm4.lt)(t.expiresAt, Date.now() - CLEANUP_GRACE_MS));
1873
+ }
1874
+ function passwordlessEmail(input) {
1875
+ const { appName, appUrl, linkUrl, code } = input;
1876
+ const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
1877
+ const lifetime2 = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1878
+ const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
1879
+ const html = generateEmailTemplate({
1880
+ heading: `Sign in to ${escapeHtml(appName)}`,
1881
+ body: `<p>Use this code, or the button below, to sign in:</p>
1882
+ <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
1883
+ <p>The code and the link expire in ${lifetime2}.</p>`,
1884
+ buttonText: "Sign in",
1885
+ buttonUrl: safeLink,
1886
+ footerNote: "If you didn't ask to sign in, you can ignore this email.",
1887
+ appUrl,
1888
+ appName
1889
+ });
1890
+ return {
1891
+ subject: `${code} is your sign-in code`,
1892
+ html,
1893
+ text: `Your ${appName} sign-in code is ${code}.
1894
+
1895
+ Or sign in with this link:
1896
+ ${safeLink}
1897
+
1898
+ The code and the link expire in ${lifetime2}.
1899
+
1900
+ If you didn't ask to sign in, you can ignore this email.`
1901
+ };
1902
+ }
1903
+
1904
+ // src/middleware/auth.ts
1905
+ var import_factory = require("hono/factory");
1906
+ var import_drizzle_orm5 = require("drizzle-orm");
1907
+
1908
+ // src/factory.ts
1909
+ function createAuth(config) {
1910
+ const { db, schema, getEnv, emailBranding, invitations, signup = "open" } = config;
1911
+ if (signup === "invitation" && !invitations) {
1912
+ throw new Error("createAuth: signup: 'invitation' requires the invitations hooks");
1913
+ }
1914
+ const getContext = (c) => ({
1915
+ db,
1916
+ schema,
1917
+ env: getEnv(c),
1918
+ emailBranding,
1919
+ invitations,
1920
+ signup
1921
+ });
1922
+ return {
1923
+ db,
1924
+ schema,
1925
+ getContext
1926
+ };
1927
+ }
1928
+ function createAuthMiddleware(auth2) {
1929
+ return async (c, next) => {
1930
+ const ctx = auth2.getContext(c);
1931
+ c.set("authContext", ctx);
1932
+ c.set("db", auth2.db);
1933
+ await next();
1934
+ };
1935
+ }
1936
+ function getAuthContext(c) {
1937
+ const ctx = c.get("authContext");
1938
+ if (!ctx) {
1939
+ throw new Error(
1940
+ "AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
1941
+ );
1942
+ }
1943
+ return ctx;
1944
+ }
1945
+
1946
+ // src/lib/problem-json.ts
1947
+ var ProblemTypes = {
1948
+ // 4xx Client Errors
1949
+ BAD_REQUEST: "/errors/bad-request",
1950
+ UNAUTHORIZED: "/errors/unauthorized",
1951
+ FORBIDDEN: "/errors/forbidden",
1952
+ NOT_FOUND: "/errors/not-found",
1953
+ CONFLICT: "/errors/conflict",
1954
+ GONE: "/errors/gone",
1955
+ VALIDATION_ERROR: "/errors/validation-error",
1956
+ RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
1957
+ // 5xx Server Errors
1958
+ INTERNAL_ERROR: "/errors/internal-error",
1959
+ SERVICE_UNAVAILABLE: "/errors/service-unavailable"
1960
+ };
1961
+ function generateTraceId() {
1962
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
1963
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1964
+ }
1965
+ function createProblemDetails(type, title, status, options) {
1966
+ const problem = {
1967
+ type,
1968
+ title,
1969
+ status
1970
+ };
1971
+ if (options?.detail) {
1972
+ problem.detail = options.detail;
1973
+ }
1974
+ if (options?.instance) {
1975
+ problem.instance = options.instance;
1976
+ }
1977
+ problem.traceId = options?.traceId || generateTraceId();
1978
+ if (options?.requestId) {
1979
+ problem.requestId = options.requestId;
1980
+ }
1981
+ if (options?.extensions) {
1982
+ Object.assign(problem, options.extensions);
1983
+ }
1984
+ return problem;
1985
+ }
1986
+ function problemJson(c, problem) {
1987
+ logger_default.warn("API error response", {
1988
+ type: problem.type,
1989
+ status: problem.status,
1990
+ traceId: problem.traceId,
1991
+ requestId: problem.requestId,
1992
+ path: c.req.path,
1993
+ method: c.req.method
1994
+ });
1995
+ return c.json(problem, problem.status, {
1996
+ "Content-Type": "application/problem+json"
1997
+ });
1998
+ }
1999
+ var problems = {
2000
+ /**
2001
+ * 400 Bad Request - Generic client error
2002
+ */
2003
+ badRequest(c, detail) {
2004
+ return problemJson(
2005
+ c,
2006
+ createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
2007
+ detail: detail || "The request could not be understood by the server",
2008
+ instance: `${c.req.method} ${c.req.path}`,
2009
+ requestId: c.get("requestId"),
2010
+ traceId: c.get("traceId")
2011
+ })
2012
+ );
2013
+ },
2014
+ /**
2015
+ * 400 Bad Request - Validation errors with field-specific details
2016
+ */
2017
+ validationError(c, detail, errors) {
2018
+ return problemJson(
2019
+ c,
2020
+ createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
2021
+ detail,
2022
+ instance: `${c.req.method} ${c.req.path}`,
2023
+ requestId: c.get("requestId"),
2024
+ traceId: c.get("traceId"),
2025
+ extensions: errors ? { errors } : void 0
2026
+ })
2027
+ );
2028
+ },
2029
+ /**
2030
+ * 401 Unauthorized - Authentication required
2031
+ */
2032
+ unauthorized(c, detail) {
2033
+ return problemJson(
2034
+ c,
2035
+ createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
2036
+ detail: detail || "Authentication is required to access this resource",
2037
+ instance: `${c.req.method} ${c.req.path}`,
2038
+ requestId: c.get("requestId"),
2039
+ traceId: c.get("traceId")
2040
+ })
2041
+ );
2042
+ },
2043
+ /**
2044
+ * 403 Forbidden - Insufficient permissions
2045
+ */
2046
+ forbidden(c, detail) {
2047
+ return problemJson(
2048
+ c,
2049
+ createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
2050
+ detail: detail || "You do not have permission to access this resource",
2051
+ instance: `${c.req.method} ${c.req.path}`,
2052
+ requestId: c.get("requestId"),
2053
+ traceId: c.get("traceId")
2054
+ })
2055
+ );
2056
+ },
2057
+ /**
2058
+ * 404 Not Found
2059
+ */
2060
+ notFound(c, detail) {
2061
+ return problemJson(
2062
+ c,
2063
+ createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
2064
+ detail: detail || "The requested resource was not found",
2065
+ instance: `${c.req.method} ${c.req.path}`,
2066
+ requestId: c.get("requestId"),
2067
+ traceId: c.get("traceId")
2068
+ })
2069
+ );
2070
+ },
2071
+ /**
2072
+ * 409 Conflict - Resource conflict (e.g., duplicate email)
2073
+ */
2074
+ conflict(c, detail) {
2075
+ return problemJson(
2076
+ c,
2077
+ createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
2078
+ detail,
2079
+ instance: `${c.req.method} ${c.req.path}`,
2080
+ requestId: c.get("requestId"),
2081
+ traceId: c.get("traceId")
2082
+ })
2083
+ );
2084
+ },
2085
+ /**
2086
+ * 410 Gone - Resource is no longer available
2087
+ */
2088
+ gone(c, detail) {
2089
+ return problemJson(
2090
+ c,
2091
+ createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
2092
+ detail: detail || "The requested resource is no longer available",
2093
+ instance: `${c.req.method} ${c.req.path}`,
2094
+ requestId: c.get("requestId"),
2095
+ traceId: c.get("traceId")
2096
+ })
2097
+ );
2098
+ },
2099
+ /**
2100
+ * 429 Too Many Requests - Rate limit exceeded
2101
+ */
2102
+ rateLimitExceeded(c, retryAfter) {
2103
+ const extensions = retryAfter ? { retryAfter } : void 0;
2104
+ return problemJson(
2105
+ c,
2106
+ createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
2107
+ detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
2108
+ instance: `${c.req.method} ${c.req.path}`,
2109
+ requestId: c.get("requestId"),
2110
+ traceId: c.get("traceId"),
2111
+ extensions
2112
+ })
2113
+ );
2114
+ },
2115
+ /**
2116
+ * 500 Internal Server Error - Generic server error
2117
+ * SECURITY: Never include error details or stack traces
2118
+ */
2119
+ internalError(c, error) {
2120
+ const traceId = c.get("traceId") || generateTraceId();
2121
+ if (error) {
2122
+ logError(error, {
2123
+ context: "internal_error",
2124
+ path: c.req.path,
2125
+ method: c.req.method,
2126
+ traceId,
2127
+ requestId: c.get("requestId")
2128
+ });
2129
+ }
2130
+ return problemJson(
2131
+ c,
2132
+ createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
2133
+ detail: "An unexpected error occurred. Please try again later",
2134
+ instance: `${c.req.method} ${c.req.path}`,
2135
+ traceId,
2136
+ requestId: c.get("requestId")
2137
+ })
2138
+ );
2139
+ },
2140
+ /**
2141
+ * 503 Service Unavailable - Service is temporarily unavailable
2142
+ */
2143
+ serviceUnavailable(c, detail) {
2144
+ return problemJson(
2145
+ c,
2146
+ createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
2147
+ detail: detail || "The service is temporarily unavailable. Please try again later",
2148
+ instance: `${c.req.method} ${c.req.path}`,
2149
+ requestId: c.get("requestId"),
2150
+ traceId: c.get("traceId")
2151
+ })
2152
+ );
2153
+ }
2154
+ };
2155
+
2156
+ // src/middleware/auth.ts
2157
+ var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
2158
+ async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
2159
+ if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
2160
+ logger_default.info("Session exceeded absolute lifetime - revoked", {
2161
+ type: "security",
2162
+ event: "session_max_lifetime_exceeded",
2163
+ severity: "low",
2164
+ sessionId: sessionId.slice(0, 8),
2165
+ userId: session.userId
2166
+ });
2167
+ await deleteSession(db, { sessions }, sessionId);
2168
+ return true;
2169
+ }
2170
+ async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
2171
+ const cookieName = getSessionCookieName(env);
2172
+ const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
2173
+ const sessionIdMatch = cookieHeader.match(cookiePattern);
2174
+ if (!sessionIdMatch) return null;
2175
+ const sessionId = sessionIdMatch[1];
2176
+ const foundSessions = await db.select({
2177
+ userId: sessions.userId,
2178
+ expiresAt: sessions.expiresAt,
2179
+ createdAt: sessions.createdAt,
2180
+ lastActiveAt: sessions.lastActiveAt,
2181
+ fingerprint: sessions.fingerprint
2182
+ }).from(sessions).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.eq)(sessions.id, sessionId), (0, import_drizzle_orm5.gt)(sessions.expiresAt, Date.now()))).limit(1);
2183
+ if (foundSessions.length === 0) return null;
2184
+ const session = foundSessions[0];
2185
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
2186
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
2187
+ const userAgent = request.headers.get("user-agent") || "";
2188
+ const cfWorker = request.headers.get("cf-worker") || "";
2189
+ const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
2190
+ const isLocalSSR = userAgent.toLowerCase().includes("node");
2191
+ const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
2192
+ const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
2193
+ const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
2194
+ if (isSSR) {
2195
+ logger_default.info("SSR request detected, using IP validation", {
2196
+ type: "security",
2197
+ event: "ssr_session_validation",
2198
+ sessionId: sessionId.slice(0, 8),
2199
+ userId: session.userId,
2200
+ clientIp,
2201
+ cfWorker,
2202
+ isLocalSSR,
2203
+ isSvelteKitSSR,
2204
+ isCloudflareSSR
2205
+ });
2206
+ } else {
2207
+ if (session.fingerprint) {
2208
+ const currentFingerprint = await generateFingerprint(request);
2209
+ if (session.fingerprint !== currentFingerprint) {
2210
+ logger_default.info("Session fingerprint changed (not enforced)", {
2211
+ type: "security",
2212
+ event: "fingerprint_changed",
2213
+ sessionId: sessionId.slice(0, 8),
2214
+ userId: session.userId,
2215
+ clientIp
2216
+ });
2217
+ }
2218
+ }
2219
+ }
2220
+ return {
2221
+ userId: session.userId,
2222
+ expiresAt: session.expiresAt
2223
+ };
2224
+ }
2225
+ async function getSessionFromBearerToken(db, sessions, authHeader) {
2226
+ if (!authHeader.startsWith("Bearer ")) return null;
2227
+ const sessionId = authHeader.slice(7);
2228
+ const foundSessions = await db.select({
2229
+ userId: sessions.userId,
2230
+ expiresAt: sessions.expiresAt,
2231
+ createdAt: sessions.createdAt,
2232
+ lastActiveAt: sessions.lastActiveAt
2233
+ }).from(sessions).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.eq)(sessions.id, sessionId), (0, import_drizzle_orm5.gt)(sessions.expiresAt, Date.now()))).limit(1);
2234
+ if (foundSessions.length === 0) return null;
2235
+ const session = foundSessions[0];
2236
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
2237
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
2238
+ return {
2239
+ userId: session.userId,
2240
+ expiresAt: session.expiresAt
2241
+ };
2242
+ }
2243
+ var requireAuth = (0, import_factory.createMiddleware)(
2244
+ async (c, next) => {
2245
+ const { db, schema } = getAuthContext(c);
2246
+ const authHeader = c.req.header("Authorization");
2247
+ if (authHeader) {
2248
+ const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
2249
+ if (session2) {
2250
+ c.set("userId", session2.userId);
2251
+ await next();
2252
+ return;
2253
+ }
2254
+ }
2255
+ const cookieHeader = c.req.header("cookie");
2256
+ if (!cookieHeader) {
2257
+ return problems.unauthorized(c, "Please log in to continue");
2258
+ }
2259
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
2260
+ if (!session) {
2261
+ c.header("Set-Cookie", clearSessionCookie(c.env));
2262
+ return problems.unauthorized(c, "Your session has expired. Please log in again.");
2263
+ }
2264
+ c.set("userId", session.userId);
2265
+ await next();
2266
+ }
2267
+ );
2268
+ var optionalAuth = (0, import_factory.createMiddleware)(
2269
+ async (c, next) => {
2270
+ const { db, schema } = getAuthContext(c);
2271
+ const authHeader = c.req.header("Authorization");
2272
+ if (authHeader) {
2273
+ const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
2274
+ if (session) {
2275
+ c.set("userId", session.userId);
2276
+ await next();
2277
+ return;
2278
+ }
2279
+ }
2280
+ const cookieHeader = c.req.header("cookie");
2281
+ if (cookieHeader) {
2282
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
2283
+ if (session) {
2284
+ c.set("userId", session.userId);
2285
+ }
2286
+ }
2287
+ await next();
2288
+ }
2289
+ );
2197
2290
 
2198
- <!-- Spacer Row -->
2199
- <table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
2200
- <tbody>
2201
- <tr>
2202
- <td>
2203
- <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f8f9fa; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
2204
- <tbody>
2205
- <tr>
2206
- <td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; padding-bottom: 5px; vertical-align: top;">
2207
- <table class="empty_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
2208
- <tr>
2209
- <td class="pad">
2210
- <div></div>
2211
- </td>
2212
- </tr>
2213
- </table>
2214
- </td>
2215
- </tr>
2216
- </tbody>
2217
- </table>
2218
- </td>
2219
- </tr>
2220
- </tbody>
2221
- </table>
2291
+ // src/middleware/csrf.ts
2292
+ var import_factory3 = require("hono/factory");
2293
+ var csrf = (0, import_factory3.createMiddleware)(async (c, next) => {
2294
+ const method = c.req.method;
2295
+ const path = c.req.path;
2296
+ const isWebhook = path.startsWith("/v1/webhooks/");
2297
+ if (isWebhook) {
2298
+ await next();
2299
+ return;
2300
+ }
2301
+ const isMcpEndpoint = path === "/v1/mcp";
2302
+ if (isMcpEndpoint) {
2303
+ await next();
2304
+ return;
2305
+ }
2306
+ const isOAuthMcpEndpoint = path.startsWith("/v1/oauth/");
2307
+ if (isOAuthMcpEndpoint) {
2308
+ await next();
2309
+ return;
2310
+ }
2311
+ const isToolsApi = path.startsWith("/v1/tools/") && !path.startsWith("/v1/tools/catalog");
2312
+ if (isToolsApi) {
2313
+ await next();
2314
+ return;
2315
+ }
2316
+ const isAdminApi = path.startsWith("/v1/admin/");
2317
+ if (isAdminApi) {
2318
+ await next();
2319
+ return;
2320
+ }
2321
+ const isMobileAuthEndpoint = path === "/v1/auth/login" || path === "/v1/auth/signup" || path === "/v1/auth/refresh";
2322
+ const authHeader = c.req.header("Authorization");
2323
+ const hasBearerToken = authHeader?.startsWith("Bearer ");
2324
+ const hasNoOrigin = !c.req.header("origin") && !c.req.header("referer");
2325
+ if (isMobileAuthEndpoint && hasNoOrigin || hasBearerToken) {
2326
+ await next();
2327
+ return;
2328
+ }
2329
+ const isInternalSync = path.startsWith("/v1/internal/sync/");
2330
+ if (isInternalSync) {
2331
+ await next();
2332
+ return;
2333
+ }
2334
+ if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
2335
+ const appUrl = c.env.APP_URL || "http://localhost:5173";
2336
+ const allowedOrigins = getAllowedOrigins(appUrl);
2337
+ const request = c.req.raw;
2338
+ const isTestEndpoint = c.req.path.startsWith("/test");
2339
+ if (isTestEndpoint) {
2340
+ const origin = request.headers.get("origin");
2341
+ const referer = request.headers.get("referer");
2342
+ logger_default.debug("CSRF validation for test endpoint", {
2343
+ path: c.req.path,
2344
+ method,
2345
+ origin,
2346
+ referer,
2347
+ appUrl,
2348
+ allowedOrigins
2349
+ });
2350
+ }
2351
+ const pagesPattern = c.env.CORS_PAGES_PATTERN;
2352
+ if (!validateOrigin(request, allowedOrigins, pagesPattern)) {
2353
+ return problems.forbidden(c, "Request blocked for security reasons. Please try again.");
2354
+ }
2355
+ }
2356
+ await next();
2357
+ });
2222
2358
 
2223
- <!-- Footer Row -->
2224
- <table class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
2225
- <tbody>
2226
- <tr>
2227
- <td>
2228
- <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: ${footerColor}; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
2229
- <tbody>
2230
- <tr>
2231
- <td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding: 20px;">
2359
+ // src/middleware/rateLimit.ts
2360
+ var import_factory4 = require("hono/factory");
2232
2361
 
2233
- <!-- Policy Links -->
2234
- <table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
2235
- <tr>
2236
- <td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
2237
- <div style="color:#ffffff;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
2238
- <p style="margin: 0; word-break: break-word;">
2239
- <a href="${safeMarketingUrl}/privacy" style="color: #ffffff; text-decoration: underline;">Privacy Policy</a> | <a href="${safeMarketingUrl}/contact" style="color: #ffffff; text-decoration: underline;">Get in Touch</a>
2240
- </p>
2241
- </div>
2242
- </td>
2243
- </tr>
2244
- </table>
2362
+ // src/lib/rate-limit-kv.ts
2363
+ async function checkRateLimitKV(kv, key, maxAttempts, windowSeconds) {
2364
+ try {
2365
+ const rateLimitKey = `rate_limit:${key}`;
2366
+ const currentAttemptsStr = await kv.get(rateLimitKey);
2367
+ const currentAttempts = currentAttemptsStr ? parseInt(currentAttemptsStr, 10) : 0;
2368
+ if (currentAttempts >= maxAttempts) {
2369
+ return {
2370
+ allowed: false,
2371
+ remaining: 0
2372
+ };
2373
+ }
2374
+ const newAttempts = currentAttempts + 1;
2375
+ await kv.put(rateLimitKey, newAttempts.toString(), {
2376
+ expirationTtl: windowSeconds
2377
+ });
2378
+ return {
2379
+ allowed: true,
2380
+ remaining: maxAttempts - newAttempts
2381
+ };
2382
+ } catch (error) {
2383
+ logger_default.error("Rate limit KV error, failing open", {
2384
+ key,
2385
+ error: error instanceof Error ? error.message : "Unknown error"
2386
+ });
2387
+ return {
2388
+ allowed: true,
2389
+ remaining: maxAttempts
2390
+ };
2391
+ }
2392
+ }
2245
2393
 
2246
- <!-- Copyright & Disclaimer -->
2247
- <table class="paragraph_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
2248
- <tr>
2249
- <td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
2250
- <div style="color:#a3a3a3;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
2251
- <p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${appName}. All rights reserved.</p>
2252
- <p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
2253
- Please do not reply to this email as this address does not accept incoming messages. This service email contains essential information relating to your ${appName} account.
2254
- </p>
2255
- </div>
2256
- </td>
2257
- </tr>
2258
- </table>
2394
+ // src/middleware/rateLimit.ts
2395
+ var rateLimit = (config) => (0, import_factory4.createMiddleware)(async (c, next) => {
2396
+ const kv = c.env.RATE_LIMIT_KV;
2397
+ const identifier = await config.identifier(c);
2398
+ const key = `${config.action}:${identifier}`;
2399
+ const windowSeconds = Math.ceil(config.windowMs / 1e3);
2400
+ const { allowed, remaining } = await checkRateLimitKV(
2401
+ kv,
2402
+ key,
2403
+ config.maxAttempts,
2404
+ windowSeconds
2405
+ );
2406
+ const resetTimestamp = Math.ceil(Date.now() / 1e3) + windowSeconds;
2407
+ c.header("X-RateLimit-Limit", config.maxAttempts.toString());
2408
+ c.header("X-RateLimit-Remaining", Math.max(0, remaining).toString());
2409
+ c.header("X-RateLimit-Reset", resetTimestamp.toString());
2410
+ if (!allowed) {
2411
+ c.header("Retry-After", windowSeconds.toString());
2412
+ return problems.rateLimitExceeded(c, windowSeconds);
2413
+ }
2414
+ await next();
2415
+ });
2259
2416
 
2260
- </td>
2261
- </tr>
2262
- </tbody>
2263
- </table>
2264
- </td>
2265
- </tr>
2266
- </tbody>
2267
- </table>
2417
+ // src/middleware/require-verified-email.ts
2418
+ var import_factory5 = require("hono/factory");
2419
+ var import_drizzle_orm6 = require("drizzle-orm");
2420
+ var requireVerifiedEmail = (0, import_factory5.createMiddleware)(
2421
+ async (c, next) => {
2422
+ const userId = c.get("userId");
2423
+ if (!userId) {
2424
+ return problems.unauthorized(c, "Not authenticated");
2425
+ }
2426
+ const { db, schema } = getAuthContext(c);
2427
+ const database = db;
2428
+ const users = schema.users;
2429
+ const [user] = await database.select({ emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm6.eq)(users.id, userId)).limit(1);
2430
+ if (!user || !user.emailVerified) {
2431
+ return problems.forbidden(c, "Please verify your email address to continue");
2432
+ }
2433
+ await next();
2434
+ }
2435
+ );
2268
2436
 
2269
- </td>
2270
- </tr>
2271
- </tbody>
2272
- </table>
2273
- </body>
2274
- </html>
2275
- `;
2276
- }
2437
+ // src/routes/index.ts
2438
+ var import_zod_openapi42 = require("@hono/zod-openapi");
2439
+
2440
+ // src/routes/signup.ts
2441
+ var import_zod_openapi2 = require("@hono/zod-openapi");
2442
+ var import_drizzle_orm10 = require("drizzle-orm");
2443
+
2444
+ // src/lib/email/email-service.ts
2445
+ var import_drizzle_orm7 = require("drizzle-orm");
2277
2446
 
2278
2447
  // src/lib/email/templates.ts
2448
+ function lifetime(minutes) {
2449
+ if (minutes % 60 !== 0) return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
2450
+ const hours = minutes / 60;
2451
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
2452
+ }
2453
+ var VERIFICATION_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60);
2454
+ var PASSWORD_RESET_LIFETIME = lifetime(AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES);
2455
+ var EMAIL_CHANGE_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60);
2456
+ var TWO_FACTOR_CODE_LIFETIME = lifetime(AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES);
2279
2457
  function extractAppUrl(url) {
2280
2458
  try {
2281
2459
  const parsed = new URL(url);
@@ -2291,12 +2469,13 @@ function generateVerificationEmail(data, appName = "Your App", configuredAppUrl
2291
2469
  const safeFirstName = firstName ? escapeHtml(firstName) : null;
2292
2470
  const html = generateEmailTemplate({
2293
2471
  heading: "",
2294
- body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}.`,
2472
+ body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}. This link expires in ${VERIFICATION_LIFETIME}.`,
2295
2473
  buttonText: "Verify Email",
2296
2474
  buttonUrl: safeUrl,
2297
2475
  welcomeMessage: safeFirstName ? `Hi ${safeFirstName}, Welcome to ${escapeHtml(appName)}!` : void 0,
2298
2476
  footerNote: "",
2299
2477
  appUrl,
2478
+ appName,
2300
2479
  ...branding
2301
2480
  });
2302
2481
  return {
@@ -2309,7 +2488,7 @@ Thank you for signing up for ${appName}! Please verify your email address by cli
2309
2488
 
2310
2489
  ${safeUrl}
2311
2490
 
2312
- This link expires in 48 hours.
2491
+ This link expires in ${VERIFICATION_LIFETIME}.
2313
2492
 
2314
2493
  If you didn't create an account with ${appName}, you can safely ignore this email.`,
2315
2494
  tags: {
@@ -2327,11 +2506,12 @@ function generatePasswordResetEmail(data, appName = "Your App", configuredAppUrl
2327
2506
  buttonText: "Reset Password",
2328
2507
  buttonUrl: safeUrl,
2329
2508
  securityWarning: `
2330
- <strong>Link expires in 1 hour.</strong><br>
2509
+ <strong>Link expires in ${PASSWORD_RESET_LIFETIME}.</strong><br>
2331
2510
  For security, this link can only be used once.
2332
2511
  `,
2333
2512
  footerNote: "If you didn't request this change, no action is needed. Your password will remain unchanged.",
2334
2513
  appUrl,
2514
+ appName,
2335
2515
  ...branding
2336
2516
  });
2337
2517
  return {
@@ -2344,7 +2524,7 @@ We received a request to reset your password. Click the link below to create a n
2344
2524
 
2345
2525
  ${safeUrl}
2346
2526
 
2347
- This link will expire in 1 hour.
2527
+ This link expires in ${PASSWORD_RESET_LIFETIME}.
2348
2528
 
2349
2529
  If you didn't request a password reset, you can safely ignore this email.`,
2350
2530
  tags: {
@@ -2362,11 +2542,12 @@ function generateEmailChangeConfirmation(data, appName = "Your App", configuredA
2362
2542
  buttonText: "Confirm Email Change",
2363
2543
  buttonUrl: safeUrl,
2364
2544
  securityWarning: `
2365
- <strong>This link expires in 24 hours.</strong><br>
2545
+ <strong>This link expires in ${EMAIL_CHANGE_LIFETIME}.</strong><br>
2366
2546
  For security, this link can only be used once.
2367
2547
  `,
2368
2548
  footerNote: "If you didn't request this change, you can safely ignore this email.",
2369
2549
  appUrl,
2550
+ appName,
2370
2551
  ...branding
2371
2552
  });
2372
2553
  return {
@@ -2381,7 +2562,7 @@ Click the link below to confirm this change:
2381
2562
 
2382
2563
  ${safeUrl}
2383
2564
 
2384
- This link expires in 24 hours.
2565
+ This link expires in ${EMAIL_CHANGE_LIFETIME}.
2385
2566
 
2386
2567
  If you didn't request this change, you can safely ignore this email.`,
2387
2568
  tags: {
@@ -2405,6 +2586,7 @@ function generateEmailChangeNotification(data, appName = "Your App", configuredA
2405
2586
  `,
2406
2587
  footerNote: "If you didn't request this change, click the button above to cancel it.",
2407
2588
  appUrl,
2589
+ appName,
2408
2590
  ...branding
2409
2591
  });
2410
2592
  return {
@@ -2434,13 +2616,14 @@ function generate2faCodeEmail(data, appName = "Your App", appUrl = "", branding)
2434
2616
  <p>Hi ${safeFirstName},</p>
2435
2617
  <p>Your verification code is:</p>
2436
2618
  <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
2437
- <p>This code expires in 5 minutes.</p>
2619
+ <p>This code expires in ${TWO_FACTOR_CODE_LIFETIME}.</p>
2438
2620
  <p>If you didn't request this code, you can safely ignore this email.</p>
2439
2621
  `,
2440
2622
  buttonText: "",
2441
2623
  buttonUrl: "",
2442
2624
  footerNote: "",
2443
2625
  appUrl,
2626
+ appName,
2444
2627
  ...branding
2445
2628
  });
2446
2629
  return {
@@ -2453,7 +2636,7 @@ Your verification code is:
2453
2636
 
2454
2637
  ${code}
2455
2638
 
2456
- This code expires in 5 minutes.
2639
+ This code expires in ${TWO_FACTOR_CODE_LIFETIME}.
2457
2640
 
2458
2641
  If you didn't request this code, you can safely ignore this email.
2459
2642
 
@@ -2476,6 +2659,7 @@ function generate2faEnabledEmail(data, appName = "Your App", appUrl = "", brandi
2476
2659
  buttonUrl: "",
2477
2660
  footerNote: "",
2478
2661
  appUrl,
2662
+ appName,
2479
2663
  ...branding
2480
2664
  });
2481
2665
  return {
@@ -2506,6 +2690,7 @@ function generate2faDisabledEmail(data, appName = "Your App", appUrl = "", brand
2506
2690
  buttonUrl: appUrl ? `${appUrl}/forgot-password` : "",
2507
2691
  footerNote: "",
2508
2692
  appUrl,
2693
+ appName,
2509
2694
  ...branding
2510
2695
  });
2511
2696
  return {
@@ -2722,7 +2907,7 @@ var EmailService = class {
2722
2907
  const [user] = await db.select({
2723
2908
  emailBounced: usersTable.emailBounced,
2724
2909
  emailComplained: usersTable.emailComplained
2725
- }).from(usersTable).where((0, import_drizzle_orm6.eq)(usersTable.email, emailAddress.toLowerCase())).limit(1);
2910
+ }).from(usersTable).where((0, import_drizzle_orm7.eq)(usersTable.email, emailAddress.toLowerCase())).limit(1);
2726
2911
  if (!user) {
2727
2912
  return { blocked: false };
2728
2913
  }
@@ -3171,7 +3356,7 @@ var EmailService = class {
3171
3356
  var import_resend2 = require("resend");
3172
3357
 
3173
3358
  // src/lib/email/webhook-handler.ts
3174
- var import_drizzle_orm7 = require("drizzle-orm");
3359
+ var import_drizzle_orm8 = require("drizzle-orm");
3175
3360
  async function handleWebhookEvent(payload, db, tables2) {
3176
3361
  const { type, data } = payload;
3177
3362
  const emailAddress = data.to[0];
@@ -3230,7 +3415,7 @@ async function handleBounce(emailAddress, emailId, bounce, db, usersTable) {
3230
3415
  await db.update(usersTable).set({
3231
3416
  emailBounced: true,
3232
3417
  emailBouncedAt: /* @__PURE__ */ new Date()
3233
- }).where((0, import_drizzle_orm7.eq)(usersTable.email, emailAddress.toLowerCase()));
3418
+ }).where((0, import_drizzle_orm8.eq)(usersTable.email, emailAddress.toLowerCase()));
3234
3419
  logError(new Error("Email bounced"), {
3235
3420
  context: "email_webhook",
3236
3421
  emailAddress,
@@ -3246,7 +3431,7 @@ async function handleComplaint(emailAddress, emailId, db, usersTable) {
3246
3431
  await db.update(usersTable).set({
3247
3432
  emailComplained: true,
3248
3433
  emailComplainedAt: /* @__PURE__ */ new Date()
3249
- }).where((0, import_drizzle_orm7.eq)(usersTable.email, emailAddress.toLowerCase()));
3434
+ }).where((0, import_drizzle_orm8.eq)(usersTable.email, emailAddress.toLowerCase()));
3250
3435
  logError(new Error("Email spam complaint received"), {
3251
3436
  context: "email_webhook",
3252
3437
  emailAddress,
@@ -3268,15 +3453,15 @@ async function handleFailure(emailAddress, emailId, failed) {
3268
3453
  }
3269
3454
 
3270
3455
  // src/lib/email/monitoring.ts
3271
- var import_drizzle_orm8 = require("drizzle-orm");
3456
+ var import_drizzle_orm9 = require("drizzle-orm");
3272
3457
  async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
3273
3458
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
3274
3459
  const [stats] = await db.select({
3275
- totalSent: import_drizzle_orm8.sql`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3276
- totalBounced: import_drizzle_orm8.sql`count(*) filter (where event_type = 'email.bounced')`.as(
3460
+ totalSent: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3461
+ totalBounced: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.bounced')`.as(
3277
3462
  "total_bounced"
3278
3463
  )
3279
- }).from(emailEventsTable).where(import_drizzle_orm8.sql`created_at > ${cutoff}`);
3464
+ }).from(emailEventsTable).where(import_drizzle_orm9.sql`created_at > ${cutoff}`);
3280
3465
  if (!stats || stats.totalSent === 0) {
3281
3466
  return 0;
3282
3467
  }
@@ -3295,11 +3480,11 @@ async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
3295
3480
  async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
3296
3481
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
3297
3482
  const [stats] = await db.select({
3298
- totalSent: import_drizzle_orm8.sql`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3299
- totalComplained: import_drizzle_orm8.sql`count(*) filter (where event_type = 'email.complained')`.as(
3483
+ totalSent: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3484
+ totalComplained: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.complained')`.as(
3300
3485
  "total_complained"
3301
3486
  )
3302
- }).from(emailEventsTable).where(import_drizzle_orm8.sql`created_at > ${cutoff}`);
3487
+ }).from(emailEventsTable).where(import_drizzle_orm9.sql`created_at > ${cutoff}`);
3303
3488
  if (!stats || stats.totalSent === 0) {
3304
3489
  return 0;
3305
3490
  }
@@ -3316,16 +3501,34 @@ async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
3316
3501
  return complaintRate;
3317
3502
  }
3318
3503
 
3504
+ // src/core/invitations.ts
3505
+ async function verifyInvitation(invitations, args) {
3506
+ if (!invitations || !args.token) return null;
3507
+ const email = args.email.toLowerCase();
3508
+ try {
3509
+ const match = await invitations.verify({ token: args.token, email, db: args.db, env: args.env });
3510
+ return match && match.email.toLowerCase() === email ? args.token : null;
3511
+ } catch (err) {
3512
+ logError(err, { context: "invitation_verify_failed", email });
3513
+ return null;
3514
+ }
3515
+ }
3516
+ async function acceptInvitation(invitations, args) {
3517
+ if (!invitations?.onAccepted) return;
3518
+ try {
3519
+ await invitations.onAccepted({ token: args.token, user: args.user, db: args.db, env: args.env });
3520
+ } catch (err) {
3521
+ logError(err, { context: args.context, userId: args.user.id });
3522
+ }
3523
+ }
3524
+
3319
3525
  // src/routes/schemas.ts
3320
3526
  var import_zod_openapi = require("@hono/zod-openapi");
3321
3527
  var signupRequestSchema = import_zod_openapi.z.object({
3322
3528
  email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
3323
- // Password and turnstileToken are required for standard signup; optional when oauthToken is provided
3324
- password: import_zod_openapi.z.string().min(8).optional().openapi({ example: "SecurePass123!" }),
3529
+ password: import_zod_openapi.z.string().min(8).openapi({ example: "SecurePass123!" }),
3325
3530
  name: import_zod_openapi.z.string().min(1).optional().openapi({ example: "John Doe" }),
3326
- turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
3327
- // OAuth signup completion token (from /signup?oauth_token=...)
3328
- oauthToken: import_zod_openapi.z.string().optional().openapi({ example: "a1b2c3d4-..." }),
3531
+ turnstileToken: import_zod_openapi.z.string().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
3329
3532
  // App invitation token (from /signup?token=...). When it validates for this
3330
3533
  // email, the account is created already-verified with no verification email.
3331
3534
  invitationToken: import_zod_openapi.z.string().optional().openapi({ example: "iIB8tDJJ..." })
@@ -3451,76 +3654,22 @@ var signupRoute = (0, import_zod_openapi2.createRoute)({
3451
3654
  description: "Invalid input",
3452
3655
  content: { "application/json": { schema: errorResponseSchema } }
3453
3656
  },
3657
+ 403: {
3658
+ description: "Sign-up is by invitation only and no valid invitation was given",
3659
+ content: { "application/json": { schema: errorResponseSchema } }
3660
+ },
3454
3661
  409: {
3455
3662
  description: "Email already registered",
3456
3663
  content: { "application/json": { schema: errorResponseSchema } }
3457
3664
  }
3458
3665
  }
3459
3666
  });
3667
+ var INVITATION_ONLY = "Sign-up is by invitation only";
3668
+ var INVITATION_INVALID = "This invitation is invalid, expired, or for a different email address";
3460
3669
  var signupHandler = async (c) => {
3461
- const { email, password, name, turnstileToken, oauthToken, invitationToken } = c.req.valid("json");
3462
- const { db: database, schema, emailBranding, invitations } = getAuthContext(c);
3670
+ const { email, password, name, turnstileToken, invitationToken } = c.req.valid("json");
3671
+ const { db: database, schema, env: authEnv, emailBranding, invitations, signup } = getAuthContext(c);
3463
3672
  const env = c.env;
3464
- if (oauthToken) {
3465
- const stored = await env.OAUTH_STATES.get(`oauth:signup:${oauthToken}`);
3466
- if (!stored) {
3467
- return problems.badRequest(c, "Invalid or expired OAuth token");
3468
- }
3469
- const {
3470
- email: tokenEmail,
3471
- provider,
3472
- providerUserId
3473
- } = JSON.parse(stored);
3474
- if (email.toLowerCase() !== tokenEmail.toLowerCase()) {
3475
- return problems.badRequest(c, "Email does not match OAuth token");
3476
- }
3477
- await env.OAUTH_STATES.delete(`oauth:signup:${oauthToken}`);
3478
- const existingUser2 = await database.query.users.findFirst({
3479
- where: (0, import_drizzle_orm9.eq)(schema.users.email, tokenEmail.toLowerCase())
3480
- });
3481
- if (existingUser2) {
3482
- return problems.conflict(c, "Email already registered");
3483
- }
3484
- let hashedPassword2 = null;
3485
- if (password) {
3486
- const passwordValidation2 = await validatePasswordWithBreachCheck(password);
3487
- if (!passwordValidation2.valid) {
3488
- return problems.badRequest(c, passwordValidation2.error || "Invalid password");
3489
- }
3490
- hashedPassword2 = await hashPassword(password, env.PASSWORD_PEPPER_V1);
3491
- }
3492
- const [newUser2] = await database.insert(schema.users).values({
3493
- email: tokenEmail.toLowerCase(),
3494
- name: name || null,
3495
- hashedPassword: hashedPassword2,
3496
- pepperKid: hashedPassword2 ? "v1" : null,
3497
- emailVerified: true,
3498
- // Provider verified the email
3499
- activatedAt: /* @__PURE__ */ new Date()
3500
- }).returning();
3501
- await database.insert(schema.oauthAccounts).values({ userId: newUser2.id, provider, providerUserId, email: tokenEmail.toLowerCase() }).onConflictDoNothing();
3502
- const fingerprint2 = await generateFingerprint(c.req.raw);
3503
- const ipAddress2 = getClientIp(c.req.raw);
3504
- const sessionId2 = await createSession(database, { sessions: schema.sessions }, newUser2.id, fingerprint2, ipAddress2);
3505
- logSecurityEvent("user_signup_oauth", "low", {
3506
- userId: newUser2.id,
3507
- email: newUser2.email,
3508
- provider
3509
- });
3510
- c.header("Set-Cookie", setSessionCookie(sessionId2, env), { append: true });
3511
- return c.json({
3512
- user: {
3513
- id: newUser2.id,
3514
- email: newUser2.email,
3515
- name: newUser2.name,
3516
- emailVerified: true
3517
- },
3518
- redirect: "/"
3519
- });
3520
- }
3521
- if (!password || !turnstileToken) {
3522
- return problems.badRequest(c, "Password and captcha are required");
3523
- }
3524
3673
  const turnstileValid = await verifyTurnstileToken(
3525
3674
  turnstileToken,
3526
3675
  env.TURNSTILE_SECRET_KEY,
@@ -3530,8 +3679,22 @@ var signupHandler = async (c) => {
3530
3679
  if (!turnstileValid) {
3531
3680
  return problems.badRequest(c, "Invalid captcha");
3532
3681
  }
3682
+ const invited = await verifyInvitation(invitations, {
3683
+ token: invitationToken,
3684
+ email,
3685
+ db: database,
3686
+ env: authEnv
3687
+ });
3688
+ if (invitationToken && invitations && !invited && signup !== "invitation") {
3689
+ return problems.badRequest(c, INVITATION_INVALID);
3690
+ }
3691
+ if (signup === "invitation" && !invited) {
3692
+ logSecurityEvent("signup_refused_no_invitation", "medium", { email: email.toLowerCase() });
3693
+ return problems.forbidden(c, invitationToken ? INVITATION_INVALID : INVITATION_ONLY);
3694
+ }
3695
+ const invitedVerified = invited !== null;
3533
3696
  const existingUser = await database.query.users.findFirst({
3534
- where: (0, import_drizzle_orm9.eq)(schema.users.email, email.toLowerCase())
3697
+ where: (0, import_drizzle_orm10.eq)(schema.users.email, email.toLowerCase())
3535
3698
  });
3536
3699
  if (existingUser) {
3537
3700
  return problems.conflict(c, "Email already registered");
@@ -3541,22 +3704,6 @@ var signupHandler = async (c) => {
3541
3704
  return problems.badRequest(c, passwordValidation.error || "Invalid password");
3542
3705
  }
3543
3706
  const hashedPassword = await hashPassword(password, env.PASSWORD_PEPPER_V1);
3544
- let invitedVerified = false;
3545
- if (invitationToken && invitations) {
3546
- const match = await invitations.verify({
3547
- token: invitationToken,
3548
- email: email.toLowerCase(),
3549
- db: database,
3550
- env
3551
- });
3552
- if (!match || match.email.toLowerCase() !== email.toLowerCase()) {
3553
- return problems.badRequest(
3554
- c,
3555
- "This invitation is invalid, expired, or for a different email address"
3556
- );
3557
- }
3558
- invitedVerified = true;
3559
- }
3560
3707
  const [newUser] = await database.insert(schema.users).values({
3561
3708
  email: email.toLowerCase(),
3562
3709
  name: name || null,
@@ -3568,7 +3715,7 @@ var signupHandler = async (c) => {
3568
3715
  if (!invitedVerified) {
3569
3716
  const emailToken = generateSecureToken(32);
3570
3717
  const tokenHash = await hashToken(emailToken);
3571
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3718
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
3572
3719
  await database.insert(schema.emailVerificationTokens).values({
3573
3720
  tokenHash,
3574
3721
  userId: newUser.id,
@@ -3589,17 +3736,14 @@ var signupHandler = async (c) => {
3589
3736
  schema.users
3590
3737
  );
3591
3738
  }
3592
- if (invitedVerified && invitationToken && invitations?.onAccepted) {
3593
- try {
3594
- await invitations.onAccepted({
3595
- token: invitationToken,
3596
- user: { id: newUser.id, email: newUser.email, name: newUser.name },
3597
- db: database,
3598
- env
3599
- });
3600
- } catch (err) {
3601
- logError(err, { context: "signup_invitation_onAccepted_failed", userId: newUser.id });
3602
- }
3739
+ if (invited) {
3740
+ await acceptInvitation(invitations, {
3741
+ token: invited,
3742
+ user: { id: newUser.id, email: newUser.email, name: newUser.name },
3743
+ db: database,
3744
+ env: authEnv,
3745
+ context: "signup_invitation_onAccepted_failed"
3746
+ });
3603
3747
  }
3604
3748
  const fingerprint = await generateFingerprint(c.req.raw);
3605
3749
  const ipAddress = getClientIp(c.req.raw);
@@ -3632,25 +3776,25 @@ var signupMiddleware = [
3632
3776
 
3633
3777
  // src/routes/login.ts
3634
3778
  var import_zod_openapi3 = require("@hono/zod-openapi");
3635
- var import_drizzle_orm11 = require("drizzle-orm");
3779
+ var import_drizzle_orm12 = require("drizzle-orm");
3636
3780
  var import_cookie2 = require("hono/cookie");
3637
3781
 
3638
3782
  // src/routes/2fa/helpers.ts
3639
- var import_drizzle_orm10 = require("drizzle-orm");
3783
+ var import_drizzle_orm11 = require("drizzle-orm");
3640
3784
  async function getUserEnabled2faMethods(database, userId, user2faMethodsTable) {
3641
3785
  return database.select({
3642
3786
  method: user2faMethodsTable.method,
3643
3787
  isPrimary: user2faMethodsTable.isPrimary,
3644
3788
  totpSecret: user2faMethodsTable.totpSecret,
3645
3789
  lastTotpCounter: user2faMethodsTable.lastTotpCounter
3646
- }).from(user2faMethodsTable).where((0, import_drizzle_orm10.eq)(user2faMethodsTable.userId, userId));
3790
+ }).from(user2faMethodsTable).where((0, import_drizzle_orm11.eq)(user2faMethodsTable.userId, userId));
3647
3791
  }
3648
3792
  function generateEmailOtp() {
3649
3793
  const bytes = crypto.getRandomValues(new Uint8Array(4));
3650
3794
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3651
3795
  return String(num % 1e6).padStart(6, "0");
3652
3796
  }
3653
- var EMAIL_CODE_TTL_SECONDS = 300;
3797
+ var EMAIL_CODE_TTL_SECONDS = AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES * 60;
3654
3798
  async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3655
3799
  const code = generateEmailOtp();
3656
3800
  await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
@@ -3677,7 +3821,7 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3677
3821
  const secret = await decryptTotpSecret(totpMethod.totpSecret, env.TOTP_ENCRYPTION_KEY);
3678
3822
  const result = verifyTotpCode(secret, code, totpMethod.lastTotpCounter);
3679
3823
  if (result.valid) {
3680
- await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where((0, import_drizzle_orm10.and)((0, import_drizzle_orm10.eq)(user2faMethodsTable.userId, userId), (0, import_drizzle_orm10.eq)(user2faMethodsTable.method, "totp")));
3824
+ await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where((0, import_drizzle_orm11.and)((0, import_drizzle_orm11.eq)(user2faMethodsTable.userId, userId), (0, import_drizzle_orm11.eq)(user2faMethodsTable.method, "totp")));
3681
3825
  return { valid: true };
3682
3826
  }
3683
3827
  return { valid: false };
@@ -3689,18 +3833,18 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3689
3833
  const kvKey = `email_2fa_challenge:${userId}`;
3690
3834
  const storedHash = await env.OAUTH_STATES.get(kvKey);
3691
3835
  if (!storedHash) return { valid: false };
3692
- const codeHash = await hashToken(code);
3693
- if (codeHash === storedHash) {
3836
+ const codeHash2 = await hashToken(code);
3837
+ if (codeHash2 === storedHash) {
3694
3838
  await env.OAUTH_STATES.delete(kvKey);
3695
3839
  return { valid: true };
3696
3840
  }
3697
3841
  return { valid: false };
3698
3842
  }
3699
3843
  if (method === "backup") {
3700
- const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where((0, import_drizzle_orm10.and)((0, import_drizzle_orm10.eq)(userBackupCodesTable.userId, userId), (0, import_drizzle_orm10.isNull)(userBackupCodesTable.usedAt)));
3844
+ const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where((0, import_drizzle_orm11.and)((0, import_drizzle_orm11.eq)(userBackupCodesTable.userId, userId), (0, import_drizzle_orm11.isNull)(userBackupCodesTable.usedAt)));
3701
3845
  for (const bc of backupCodes) {
3702
3846
  if (await verifyBackupCode(code, bc.codeHash)) {
3703
- await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm10.eq)(userBackupCodesTable.id, bc.id));
3847
+ await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm11.eq)(userBackupCodesTable.id, bc.id));
3704
3848
  logSecurityEvent("2fa_backup_code_used", "medium", { userId, codeId: bc.id });
3705
3849
  return { valid: true };
3706
3850
  }
@@ -3756,7 +3900,7 @@ var loginHandler = async (c) => {
3756
3900
  return problems.badRequest(c, "Invalid captcha");
3757
3901
  }
3758
3902
  const user = await database.query.users.findFirst({
3759
- where: (0, import_drizzle_orm11.eq)(schema.users.email, email.toLowerCase())
3903
+ where: (0, import_drizzle_orm12.eq)(schema.users.email, email.toLowerCase())
3760
3904
  });
3761
3905
  if (!user) {
3762
3906
  return problems.unauthorized(c, "Invalid email or password");
@@ -3865,7 +4009,7 @@ var loginMiddleware = [
3865
4009
 
3866
4010
  // src/routes/logout.ts
3867
4011
  var import_zod_openapi4 = require("@hono/zod-openapi");
3868
- var import_drizzle_orm12 = require("drizzle-orm");
4012
+ var import_drizzle_orm13 = require("drizzle-orm");
3869
4013
  var logoutRoute = (0, import_zod_openapi4.createRoute)({
3870
4014
  method: "post",
3871
4015
  path: "/logout",
@@ -3888,7 +4032,7 @@ var logoutHandler = async (c) => {
3888
4032
  const sessionIdMatch = cookieHeader.match(cookiePattern);
3889
4033
  if (sessionIdMatch) {
3890
4034
  const sessionId = sessionIdMatch[1];
3891
- await database.delete(schema.sessions).where((0, import_drizzle_orm12.eq)(schema.sessions.id, sessionId));
4035
+ await database.delete(schema.sessions).where((0, import_drizzle_orm13.eq)(schema.sessions.id, sessionId));
3892
4036
  }
3893
4037
  }
3894
4038
  c.header("Set-Cookie", clearSessionCookie(c.env), { append: true });
@@ -3897,7 +4041,7 @@ var logoutHandler = async (c) => {
3897
4041
 
3898
4042
  // src/routes/me.ts
3899
4043
  var import_zod_openapi5 = require("@hono/zod-openapi");
3900
- var import_drizzle_orm13 = require("drizzle-orm");
4044
+ var import_drizzle_orm14 = require("drizzle-orm");
3901
4045
  var meRoute = (0, import_zod_openapi5.createRoute)({
3902
4046
  method: "get",
3903
4047
  path: "/me",
@@ -3925,7 +4069,7 @@ var meHandler = async (c) => {
3925
4069
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3926
4070
  const { db: database, schema } = getAuthContext(c);
3927
4071
  const user = await database.query.users.findFirst({
3928
- where: (0, import_drizzle_orm13.eq)(schema.users.id, userId)
4072
+ where: (0, import_drizzle_orm14.eq)(schema.users.id, userId)
3929
4073
  });
3930
4074
  if (!user) {
3931
4075
  return problems.notFound(c, "User not found");
@@ -3951,7 +4095,7 @@ var meMiddleware = [requireAuth];
3951
4095
 
3952
4096
  // src/routes/verify-email.ts
3953
4097
  var import_zod_openapi6 = require("@hono/zod-openapi");
3954
- var import_drizzle_orm14 = require("drizzle-orm");
4098
+ var import_drizzle_orm15 = require("drizzle-orm");
3955
4099
  var verifyEmailRoute = (0, import_zod_openapi6.createRoute)({
3956
4100
  method: "post",
3957
4101
  path: "/verify-email",
@@ -3980,17 +4124,17 @@ var verifyEmailHandler = async (c) => {
3980
4124
  const { db: database, schema } = getAuthContext(c);
3981
4125
  const tokenHash = await hashToken(token);
3982
4126
  const verificationToken = await database.query.emailVerificationTokens.findFirst({
3983
- where: (0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
4127
+ where: (0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
3984
4128
  });
3985
4129
  if (!verificationToken) {
3986
4130
  return problems.badRequest(c, "Invalid or expired verification token");
3987
4131
  }
3988
4132
  if (verificationToken.expiresAt < Date.now()) {
3989
- await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
4133
+ await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
3990
4134
  return problems.badRequest(c, "Verification token has expired");
3991
4135
  }
3992
- await database.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm14.eq)(schema.users.id, verificationToken.userId));
3993
- await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
4136
+ await database.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm15.eq)(schema.users.id, verificationToken.userId));
4137
+ await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
3994
4138
  logSecurityEvent("email_verified", "low", { userId: verificationToken.userId });
3995
4139
  return c.json({ success: true, redirect: "/" });
3996
4140
  };
@@ -3999,18 +4143,18 @@ var verifyEmailHandler = async (c) => {
3999
4143
  var import_zod_openapi7 = require("@hono/zod-openapi");
4000
4144
 
4001
4145
  // src/core/password-reset.ts
4002
- var import_drizzle_orm15 = require("drizzle-orm");
4146
+ var import_drizzle_orm16 = require("drizzle-orm");
4003
4147
  var TOKEN_EXPIRATION_MS = AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES * 60 * 1e3;
4004
4148
  async function createPasswordResetToken(db, tables2, email) {
4005
4149
  const { users, passwordResetTokens } = tables2;
4006
- const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm15.eq)(users.email, email.toLowerCase())).limit(1);
4150
+ const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm16.eq)(users.email, email.toLowerCase())).limit(1);
4007
4151
  if (!user) {
4008
4152
  return null;
4009
4153
  }
4010
4154
  if (!user.emailVerified) {
4011
4155
  return null;
4012
4156
  }
4013
- await db.delete(passwordResetTokens).where((0, import_drizzle_orm15.eq)(passwordResetTokens.userId, user.id));
4157
+ await db.delete(passwordResetTokens).where((0, import_drizzle_orm16.eq)(passwordResetTokens.userId, user.id));
4014
4158
  const token = generateSecureToken(64);
4015
4159
  const tokenHash = await hashToken(token);
4016
4160
  const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
@@ -4032,10 +4176,10 @@ async function validateResetToken(db, tables2, token) {
4032
4176
  used: passwordResetTokens.used,
4033
4177
  expiresAt: passwordResetTokens.expiresAt
4034
4178
  }).from(passwordResetTokens).where(
4035
- (0, import_drizzle_orm15.and)(
4036
- (0, import_drizzle_orm15.eq)(passwordResetTokens.tokenHash, tokenHash),
4037
- (0, import_drizzle_orm15.eq)(passwordResetTokens.used, false),
4038
- (0, import_drizzle_orm15.gt)(passwordResetTokens.expiresAt, Date.now())
4179
+ (0, import_drizzle_orm16.and)(
4180
+ (0, import_drizzle_orm16.eq)(passwordResetTokens.tokenHash, tokenHash),
4181
+ (0, import_drizzle_orm16.eq)(passwordResetTokens.used, false),
4182
+ (0, import_drizzle_orm16.gt)(passwordResetTokens.expiresAt, Date.now())
4039
4183
  )
4040
4184
  ).limit(1);
4041
4185
  if (!resetToken) {
@@ -4056,11 +4200,11 @@ async function resetPassword(db, tables2, token, newPassword, pepper, pepperKid
4056
4200
  await db.update(users).set({
4057
4201
  hashedPassword,
4058
4202
  pepperKid,
4059
- sessionVersion: import_drizzle_orm15.sql`${users.sessionVersion} + 1`
4060
- }).where((0, import_drizzle_orm15.eq)(users.id, userId));
4203
+ sessionVersion: import_drizzle_orm16.sql`${users.sessionVersion} + 1`
4204
+ }).where((0, import_drizzle_orm16.eq)(users.id, userId));
4061
4205
  const tokenHash = await hashToken(token);
4062
- await db.update(passwordResetTokens).set({ used: true }).where((0, import_drizzle_orm15.eq)(passwordResetTokens.tokenHash, tokenHash));
4063
- await db.delete(sessions).where((0, import_drizzle_orm15.eq)(sessions.userId, userId));
4206
+ await db.update(passwordResetTokens).set({ used: true }).where((0, import_drizzle_orm16.eq)(passwordResetTokens.tokenHash, tokenHash));
4207
+ await db.delete(sessions).where((0, import_drizzle_orm16.eq)(sessions.userId, userId));
4064
4208
  logger_default.info("Password reset successful", { userId });
4065
4209
  return { success: true };
4066
4210
  }
@@ -4208,14 +4352,14 @@ var resetPasswordMiddleware = [
4208
4352
  var import_zod_openapi9 = require("@hono/zod-openapi");
4209
4353
 
4210
4354
  // src/core/change-password.ts
4211
- var import_drizzle_orm16 = require("drizzle-orm");
4355
+ var import_drizzle_orm17 = require("drizzle-orm");
4212
4356
  async function changePassword(params) {
4213
4357
  const { userId, currentPassword, newPassword, currentSessionId, pepper, db, tables: tables2 } = params;
4214
4358
  const { users, sessions } = tables2;
4215
4359
  const [user] = await db.select({
4216
4360
  id: users.id,
4217
4361
  hashedPassword: users.hashedPassword
4218
- }).from(users).where((0, import_drizzle_orm16.eq)(users.id, userId)).limit(1);
4362
+ }).from(users).where((0, import_drizzle_orm17.eq)(users.id, userId)).limit(1);
4219
4363
  if (!user || !user.hashedPassword) {
4220
4364
  return { success: false, error: "User not found" };
4221
4365
  }
@@ -4231,8 +4375,8 @@ async function changePassword(params) {
4231
4375
  await db.update(users).set({
4232
4376
  hashedPassword: newHashedPassword,
4233
4377
  updatedAt: /* @__PURE__ */ new Date()
4234
- }).where((0, import_drizzle_orm16.eq)(users.id, userId));
4235
- await db.delete(sessions).where((0, import_drizzle_orm16.and)((0, import_drizzle_orm16.eq)(sessions.userId, userId), (0, import_drizzle_orm16.ne)(sessions.id, currentSessionId)));
4378
+ }).where((0, import_drizzle_orm17.eq)(users.id, userId));
4379
+ await db.delete(sessions).where((0, import_drizzle_orm17.and)((0, import_drizzle_orm17.eq)(sessions.userId, userId), (0, import_drizzle_orm17.ne)(sessions.id, currentSessionId)));
4236
4380
  logSecurityEvent("password_changed", "low", {
4237
4381
  userId
4238
4382
  });
@@ -4505,7 +4649,7 @@ var cancelEmailChangeHandler = async (c) => {
4505
4649
  // src/routes/delete-account.ts
4506
4650
  var import_zod_openapi17 = require("@hono/zod-openapi");
4507
4651
  var import_zod_openapi18 = require("@hono/zod-openapi");
4508
- var import_drizzle_orm17 = require("drizzle-orm");
4652
+ var import_drizzle_orm18 = require("drizzle-orm");
4509
4653
  var deleteAccountRequestSchema = import_zod_openapi18.z.object({
4510
4654
  password: import_zod_openapi18.z.string().min(1).openapi({ example: "CurrentPass123!" })
4511
4655
  }).openapi("DeleteAccountRequest");
@@ -4553,7 +4697,7 @@ var deleteAccountHandler = async (c) => {
4553
4697
  const [user] = await database.select({
4554
4698
  id: schema.users.id,
4555
4699
  hashedPassword: schema.users.hashedPassword
4556
- }).from(schema.users).where((0, import_drizzle_orm17.eq)(schema.users.id, userId)).limit(1);
4700
+ }).from(schema.users).where((0, import_drizzle_orm18.eq)(schema.users.id, userId)).limit(1);
4557
4701
  if (!user || !user.hashedPassword) {
4558
4702
  return problems.unauthorized(c, "Invalid credentials");
4559
4703
  }
@@ -4568,8 +4712,8 @@ var deleteAccountHandler = async (c) => {
4568
4712
  }
4569
4713
  const now = /* @__PURE__ */ new Date();
4570
4714
  const purgeAt = new Date(now.getTime() + PURGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3);
4571
- await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where((0, import_drizzle_orm17.eq)(schema.users.id, userId));
4572
- await database.delete(schema.sessions).where((0, import_drizzle_orm17.eq)(schema.sessions.userId, userId));
4715
+ await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where((0, import_drizzle_orm18.eq)(schema.users.id, userId));
4716
+ await database.delete(schema.sessions).where((0, import_drizzle_orm18.eq)(schema.sessions.userId, userId));
4573
4717
  logSecurityEvent("account_deletion_requested", "high", { userId });
4574
4718
  return c.json(
4575
4719
  {
@@ -4590,7 +4734,7 @@ var deleteAccountMiddleware = [requireAuth];
4590
4734
  // src/routes/refresh.ts
4591
4735
  var import_zod_openapi19 = require("@hono/zod-openapi");
4592
4736
  var import_zod_openapi20 = require("@hono/zod-openapi");
4593
- var import_drizzle_orm18 = require("drizzle-orm");
4737
+ var import_drizzle_orm19 = require("drizzle-orm");
4594
4738
  var refreshRequestSchema = import_zod_openapi20.z.object({
4595
4739
  refreshToken: import_zod_openapi20.z.string().min(1).openapi({ example: "abc123..." })
4596
4740
  }).openapi("RefreshRequest");
@@ -4630,7 +4774,7 @@ var refreshHandler = async (c) => {
4630
4774
  expiresAt: schema.sessions.expiresAt,
4631
4775
  fingerprint: schema.sessions.fingerprint,
4632
4776
  ipAddress: schema.sessions.ipAddress
4633
- }).from(schema.sessions).where((0, import_drizzle_orm18.and)((0, import_drizzle_orm18.eq)(schema.sessions.id, refreshToken), (0, import_drizzle_orm18.gt)(schema.sessions.expiresAt, Date.now()))).limit(1);
4777
+ }).from(schema.sessions).where((0, import_drizzle_orm19.and)((0, import_drizzle_orm19.eq)(schema.sessions.id, refreshToken), (0, import_drizzle_orm19.gt)(schema.sessions.expiresAt, Date.now()))).limit(1);
4634
4778
  if (!session) {
4635
4779
  return problems.unauthorized(c, "Invalid or expired refresh token");
4636
4780
  }
@@ -4653,7 +4797,7 @@ var refreshMiddleware = [];
4653
4797
  // src/routes/resend-verification.ts
4654
4798
  var import_zod_openapi21 = require("@hono/zod-openapi");
4655
4799
  var import_zod_openapi22 = require("@hono/zod-openapi");
4656
- var import_drizzle_orm19 = require("drizzle-orm");
4800
+ var import_drizzle_orm20 = require("drizzle-orm");
4657
4801
  var resendVerificationResponseSchema = import_zod_openapi22.z.object({
4658
4802
  success: import_zod_openapi22.z.boolean().openapi({ example: true }),
4659
4803
  message: import_zod_openapi22.z.string().openapi({ example: "Verification email sent" })
@@ -4698,17 +4842,17 @@ var resendVerificationHandler = async (c) => {
4698
4842
  email: schema.users.email,
4699
4843
  name: schema.users.name,
4700
4844
  emailVerified: schema.users.emailVerified
4701
- }).from(schema.users).where((0, import_drizzle_orm19.eq)(schema.users.id, userId)).limit(1);
4845
+ }).from(schema.users).where((0, import_drizzle_orm20.eq)(schema.users.id, userId)).limit(1);
4702
4846
  if (!user) {
4703
4847
  return problems.unauthorized(c, "User not found");
4704
4848
  }
4705
4849
  if (user.emailVerified) {
4706
4850
  return problems.badRequest(c, "Email is already verified");
4707
4851
  }
4708
- await db.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm19.eq)(schema.emailVerificationTokens.userId, userId));
4852
+ await db.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm20.eq)(schema.emailVerificationTokens.userId, userId));
4709
4853
  const emailToken = generateSecureToken(32);
4710
4854
  const tokenHash = await hashToken(emailToken);
4711
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
4855
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
4712
4856
  await db.insert(schema.emailVerificationTokens).values({
4713
4857
  tokenHash,
4714
4858
  userId: user.id,
@@ -4750,7 +4894,7 @@ var import_zod_openapi36 = require("@hono/zod-openapi");
4750
4894
 
4751
4895
  // src/routes/2fa/status.ts
4752
4896
  var import_zod_openapi24 = require("@hono/zod-openapi");
4753
- var import_drizzle_orm20 = require("drizzle-orm");
4897
+ var import_drizzle_orm21 = require("drizzle-orm");
4754
4898
 
4755
4899
  // src/routes/2fa/schemas.ts
4756
4900
  var import_zod_openapi23 = require("@hono/zod-openapi");
@@ -4911,7 +5055,7 @@ var statusHandler = async (c) => {
4911
5055
  if (!userId) return problems.unauthorized(c);
4912
5056
  const { db, schema } = getAuthContext(c);
4913
5057
  const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4914
- const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where((0, import_drizzle_orm20.and)((0, import_drizzle_orm20.eq)(schema.userBackupCodes.userId, userId), (0, import_drizzle_orm20.isNull)(schema.userBackupCodes.usedAt)));
5058
+ const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where((0, import_drizzle_orm21.and)((0, import_drizzle_orm21.eq)(schema.userBackupCodes.userId, userId), (0, import_drizzle_orm21.isNull)(schema.userBackupCodes.usedAt)));
4915
5059
  c.header("Cache-Control", "private, max-age=60");
4916
5060
  c.header("Vary", "Cookie");
4917
5061
  return c.json({
@@ -4923,7 +5067,7 @@ var statusHandler = async (c) => {
4923
5067
 
4924
5068
  // src/routes/2fa/totp-setup.ts
4925
5069
  var import_zod_openapi25 = require("@hono/zod-openapi");
4926
- var import_drizzle_orm21 = require("drizzle-orm");
5070
+ var import_drizzle_orm22 = require("drizzle-orm");
4927
5071
  var totpSetupRoute = (0, import_zod_openapi25.createRoute)({
4928
5072
  method: "post",
4929
5073
  path: "/totp/setup",
@@ -4953,11 +5097,11 @@ var totpSetupHandler = async (c) => {
4953
5097
  const userId = c.get("userId");
4954
5098
  if (!userId) return problems.unauthorized(c);
4955
5099
  const { db, schema, env } = getAuthContext(c);
4956
- const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm21.and)((0, import_drizzle_orm21.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm21.eq)(schema.user2faMethods.method, "totp"))).limit(1);
5100
+ const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm22.and)((0, import_drizzle_orm22.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm22.eq)(schema.user2faMethods.method, "totp"))).limit(1);
4957
5101
  if (existing.length > 0) {
4958
5102
  return problems.badRequest(c, "TOTP already enabled");
4959
5103
  }
4960
- const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm21.eq)(schema.users.id, userId)).limit(1);
5104
+ const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm22.eq)(schema.users.id, userId)).limit(1);
4961
5105
  if (!user) return problems.notFound(c, "User not found");
4962
5106
  const secret = generateTotpSecret();
4963
5107
  const qrCodeUri = generateQrCodeUri(secret, user.email);
@@ -4968,7 +5112,7 @@ var totpSetupHandler = async (c) => {
4968
5112
 
4969
5113
  // src/routes/2fa/totp-verify.ts
4970
5114
  var import_zod_openapi26 = require("@hono/zod-openapi");
4971
- var import_drizzle_orm22 = require("drizzle-orm");
5115
+ var import_drizzle_orm23 = require("drizzle-orm");
4972
5116
  var totpVerifyRoute = (0, import_zod_openapi26.createRoute)({
4973
5117
  method: "post",
4974
5118
  path: "/totp/verify",
@@ -5014,7 +5158,7 @@ var totpVerifyHandler = async (c) => {
5014
5158
  logSecurityEvent("2fa_totp_setup_failed", "medium", { userId });
5015
5159
  return problems.badRequest(c, "Invalid code");
5016
5160
  }
5017
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm22.eq)(schema.users.id, userId)).limit(1);
5161
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm23.eq)(schema.users.id, userId)).limit(1);
5018
5162
  const encryptionKey = env.TOTP_ENCRYPTION_KEY;
5019
5163
  if (!encryptionKey) {
5020
5164
  logger_default.error("TOTP_ENCRYPTION_KEY not configured");
@@ -5062,7 +5206,7 @@ var totpVerifyHandler = async (c) => {
5062
5206
 
5063
5207
  // src/routes/2fa/totp-disable.ts
5064
5208
  var import_zod_openapi27 = require("@hono/zod-openapi");
5065
- var import_drizzle_orm23 = require("drizzle-orm");
5209
+ var import_drizzle_orm24 = require("drizzle-orm");
5066
5210
  var totpDisableRoute = (0, import_zod_openapi27.createRoute)({
5067
5211
  method: "post",
5068
5212
  path: "/totp/disable",
@@ -5105,13 +5249,13 @@ var totpDisableHandler = async (c) => {
5105
5249
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "totp" });
5106
5250
  return problems.badRequest(c, "Invalid verification code");
5107
5251
  }
5108
- await db.delete(schema.user2faMethods).where((0, import_drizzle_orm23.and)((0, import_drizzle_orm23.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm23.eq)(schema.user2faMethods.method, "totp")));
5252
+ await db.delete(schema.user2faMethods).where((0, import_drizzle_orm24.and)((0, import_drizzle_orm24.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm24.eq)(schema.user2faMethods.method, "totp")));
5109
5253
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5110
5254
  if (remaining.length === 0) {
5111
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm23.eq)(schema.userBackupCodes.userId, userId));
5112
- await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm23.eq)(schema.userTrustedDevices.userId, userId));
5255
+ await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm24.eq)(schema.userBackupCodes.userId, userId));
5256
+ await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm24.eq)(schema.userTrustedDevices.userId, userId));
5113
5257
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5114
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm23.eq)(schema.users.id, userId)).limit(1);
5258
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm24.eq)(schema.users.id, userId)).limit(1);
5115
5259
  if (user) {
5116
5260
  const emailService = new EmailService(env, void 0, emailBranding);
5117
5261
  const firstName = user.name?.split(" ")[0] || null;
@@ -5130,7 +5274,7 @@ var totpDisableHandler = async (c) => {
5130
5274
 
5131
5275
  // src/routes/2fa/email-setup.ts
5132
5276
  var import_zod_openapi28 = require("@hono/zod-openapi");
5133
- var import_drizzle_orm24 = require("drizzle-orm");
5277
+ var import_drizzle_orm25 = require("drizzle-orm");
5134
5278
  var emailSetupRoute = (0, import_zod_openapi28.createRoute)({
5135
5279
  method: "post",
5136
5280
  path: "/email/setup",
@@ -5160,15 +5304,15 @@ var emailSetupHandler = async (c) => {
5160
5304
  const userId = c.get("userId");
5161
5305
  if (!userId) return problems.unauthorized(c);
5162
5306
  const { db, schema, env, emailBranding } = getAuthContext(c);
5163
- const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm24.and)((0, import_drizzle_orm24.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm24.eq)(schema.user2faMethods.method, "email"))).limit(1);
5307
+ const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm25.and)((0, import_drizzle_orm25.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm25.eq)(schema.user2faMethods.method, "email"))).limit(1);
5164
5308
  if (existing.length > 0) {
5165
5309
  return problems.badRequest(c, "Email 2FA already enabled");
5166
5310
  }
5167
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm24.eq)(schema.users.id, userId)).limit(1);
5311
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm25.eq)(schema.users.id, userId)).limit(1);
5168
5312
  if (!user) return problems.notFound(c, "User not found");
5169
5313
  const code = generateEmailOtp();
5170
- const codeHash = await hashToken(code);
5171
- await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
5314
+ const codeHash2 = await hashToken(code);
5315
+ await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash2, { expirationTtl: 300 });
5172
5316
  const emailService = new EmailService(env, void 0, emailBranding);
5173
5317
  const firstName = user.name?.split(" ")[0] || null;
5174
5318
  await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
@@ -5178,7 +5322,7 @@ var emailSetupHandler = async (c) => {
5178
5322
 
5179
5323
  // src/routes/2fa/email-verify.ts
5180
5324
  var import_zod_openapi29 = require("@hono/zod-openapi");
5181
- var import_drizzle_orm25 = require("drizzle-orm");
5325
+ var import_drizzle_orm26 = require("drizzle-orm");
5182
5326
  var emailVerifyRoute = (0, import_zod_openapi29.createRoute)({
5183
5327
  method: "post",
5184
5328
  path: "/email/verify",
@@ -5215,12 +5359,12 @@ var emailVerifyHandler = async (c) => {
5215
5359
  if (!storedHash) {
5216
5360
  return problems.badRequest(c, "Code expired. Please request a new one.");
5217
5361
  }
5218
- const codeHash = await hashToken(code);
5219
- if (codeHash !== storedHash) {
5362
+ const codeHash2 = await hashToken(code);
5363
+ if (codeHash2 !== storedHash) {
5220
5364
  logSecurityEvent("2fa_email_setup_failed", "medium", { userId });
5221
5365
  return problems.badRequest(c, "Invalid code");
5222
5366
  }
5223
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm25.eq)(schema.users.id, userId)).limit(1);
5367
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm26.eq)(schema.users.id, userId)).limit(1);
5224
5368
  const now = Date.now();
5225
5369
  const existingMethods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5226
5370
  const isFirstMethod = existingMethods.length === 0;
@@ -5262,7 +5406,7 @@ var emailVerifyHandler = async (c) => {
5262
5406
 
5263
5407
  // src/routes/2fa/email-send-code.ts
5264
5408
  var import_zod_openapi30 = require("@hono/zod-openapi");
5265
- var import_drizzle_orm26 = require("drizzle-orm");
5409
+ var import_drizzle_orm27 = require("drizzle-orm");
5266
5410
  var emailSendCodeRoute = (0, import_zod_openapi30.createRoute)({
5267
5411
  method: "post",
5268
5412
  path: "/email/send-code",
@@ -5292,15 +5436,15 @@ var emailSendCodeHandler = async (c) => {
5292
5436
  const userId = c.get("userId");
5293
5437
  if (!userId) return problems.unauthorized(c);
5294
5438
  const { db, schema, env, emailBranding } = getAuthContext(c);
5295
- const [emailMethod] = await db.select().from(schema.user2faMethods).where((0, import_drizzle_orm26.and)((0, import_drizzle_orm26.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm26.eq)(schema.user2faMethods.method, "email"))).limit(1);
5439
+ const [emailMethod] = await db.select().from(schema.user2faMethods).where((0, import_drizzle_orm27.and)((0, import_drizzle_orm27.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm27.eq)(schema.user2faMethods.method, "email"))).limit(1);
5296
5440
  if (!emailMethod) {
5297
5441
  return problems.badRequest(c, "Email 2FA not enabled");
5298
5442
  }
5299
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm26.eq)(schema.users.id, userId)).limit(1);
5443
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm27.eq)(schema.users.id, userId)).limit(1);
5300
5444
  if (!user) return problems.notFound(c, "User not found");
5301
5445
  const code = generateEmailOtp();
5302
- const codeHash = await hashToken(code);
5303
- await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash, {
5446
+ const codeHash2 = await hashToken(code);
5447
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash2, {
5304
5448
  expirationTtl: 300
5305
5449
  });
5306
5450
  const emailService = new EmailService(env, void 0, emailBranding);
@@ -5322,7 +5466,7 @@ var emailSendCodeHandler = async (c) => {
5322
5466
 
5323
5467
  // src/routes/2fa/email-disable.ts
5324
5468
  var import_zod_openapi31 = require("@hono/zod-openapi");
5325
- var import_drizzle_orm27 = require("drizzle-orm");
5469
+ var import_drizzle_orm28 = require("drizzle-orm");
5326
5470
  var emailDisableRoute = (0, import_zod_openapi31.createRoute)({
5327
5471
  method: "post",
5328
5472
  path: "/email/disable",
@@ -5365,13 +5509,13 @@ var emailDisableHandler = async (c) => {
5365
5509
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "email" });
5366
5510
  return problems.badRequest(c, "Invalid verification code");
5367
5511
  }
5368
- await db.delete(schema.user2faMethods).where((0, import_drizzle_orm27.and)((0, import_drizzle_orm27.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm27.eq)(schema.user2faMethods.method, "email")));
5512
+ await db.delete(schema.user2faMethods).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm28.eq)(schema.user2faMethods.method, "email")));
5369
5513
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5370
5514
  if (remaining.length === 0) {
5371
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm27.eq)(schema.userBackupCodes.userId, userId));
5372
- await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm27.eq)(schema.userTrustedDevices.userId, userId));
5515
+ await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm28.eq)(schema.userBackupCodes.userId, userId));
5516
+ await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId));
5373
5517
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5374
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm27.eq)(schema.users.id, userId)).limit(1);
5518
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm28.eq)(schema.users.id, userId)).limit(1);
5375
5519
  if (user) {
5376
5520
  const emailService = new EmailService(env, void 0, emailBranding);
5377
5521
  const firstName = user.name?.split(" ")[0] || null;
@@ -5390,7 +5534,7 @@ var emailDisableHandler = async (c) => {
5390
5534
 
5391
5535
  // src/routes/2fa/trusted-devices.ts
5392
5536
  var import_zod_openapi32 = require("@hono/zod-openapi");
5393
- var import_drizzle_orm28 = require("drizzle-orm");
5537
+ var import_drizzle_orm29 = require("drizzle-orm");
5394
5538
  var trustedDevicesGetRoute = (0, import_zod_openapi32.createRoute)({
5395
5539
  method: "get",
5396
5540
  path: "/trusted-devices",
@@ -5429,7 +5573,7 @@ var trustedDevicesGetHandler = async (c) => {
5429
5573
  ipAddress: schema.userTrustedDevices.ipAddress,
5430
5574
  lastUsedAt: schema.userTrustedDevices.lastUsedAt,
5431
5575
  createdAt: schema.userTrustedDevices.createdAt
5432
- }).from(schema.userTrustedDevices).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId), (0, import_drizzle_orm28.gt)(schema.userTrustedDevices.expiresAt, now)));
5576
+ }).from(schema.userTrustedDevices).where((0, import_drizzle_orm29.and)((0, import_drizzle_orm29.eq)(schema.userTrustedDevices.userId, userId), (0, import_drizzle_orm29.gt)(schema.userTrustedDevices.expiresAt, now)));
5433
5577
  c.header("Cache-Control", "private, max-age=60");
5434
5578
  c.header("Vary", "Cookie");
5435
5579
  return c.json({ devices });
@@ -5465,7 +5609,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5465
5609
  if (!userId) return problems.unauthorized(c);
5466
5610
  const deviceId = c.req.param("id");
5467
5611
  const { db, schema } = getAuthContext(c);
5468
- const result = await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.id, deviceId), (0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId))).returning({ id: schema.userTrustedDevices.id });
5612
+ const result = await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm29.and)((0, import_drizzle_orm29.eq)(schema.userTrustedDevices.id, deviceId), (0, import_drizzle_orm29.eq)(schema.userTrustedDevices.userId, userId))).returning({ id: schema.userTrustedDevices.id });
5469
5613
  if (result.length === 0) {
5470
5614
  return problems.notFound(c, "Device not found");
5471
5615
  }
@@ -5475,7 +5619,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5475
5619
 
5476
5620
  // src/routes/2fa/backup-codes.ts
5477
5621
  var import_zod_openapi33 = require("@hono/zod-openapi");
5478
- var import_drizzle_orm29 = require("drizzle-orm");
5622
+ var import_drizzle_orm30 = require("drizzle-orm");
5479
5623
  var backupCodesRegenerateRoute = (0, import_zod_openapi33.createRoute)({
5480
5624
  method: "post",
5481
5625
  path: "/backup-codes/regenerate",
@@ -5516,7 +5660,7 @@ var backupCodesRegenerateHandler = async (c) => {
5516
5660
  logSecurityEvent("2fa_backup_regenerate_failed", "medium", { userId });
5517
5661
  return problems.badRequest(c, "Invalid verification code");
5518
5662
  }
5519
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm29.eq)(schema.userBackupCodes.userId, userId));
5663
+ await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm30.eq)(schema.userBackupCodes.userId, userId));
5520
5664
  const codes = generateBackupCodes();
5521
5665
  const now = Date.now();
5522
5666
  const hashedCodes = await Promise.all(
@@ -5533,7 +5677,7 @@ var backupCodesRegenerateHandler = async (c) => {
5533
5677
 
5534
5678
  // src/routes/2fa/challenge.ts
5535
5679
  var import_zod_openapi34 = require("@hono/zod-openapi");
5536
- var import_drizzle_orm30 = require("drizzle-orm");
5680
+ var import_drizzle_orm31 = require("drizzle-orm");
5537
5681
  var import_cookie3 = require("hono/cookie");
5538
5682
  var challengeRoute = (0, import_zod_openapi34.createRoute)({
5539
5683
  method: "post",
@@ -5605,7 +5749,7 @@ var challengeHandler = async (c) => {
5605
5749
  }
5606
5750
  await deleteChallengeToken(c.env.OAUTH_STATES, challengeToken);
5607
5751
  clearChallengeCookie(c, c.env);
5608
- const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm30.eq)(schema.users.id, payload.userId)).limit(1);
5752
+ const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm31.eq)(schema.users.id, payload.userId)).limit(1);
5609
5753
  if (!user) {
5610
5754
  logger_default.error("User not found during 2FA challenge", { userId: payload.userId });
5611
5755
  return problems.notFound(c, "User not found");
@@ -5646,7 +5790,7 @@ var challengeHandler = async (c) => {
5646
5790
 
5647
5791
  // src/routes/2fa/challenge-resend.ts
5648
5792
  var import_zod_openapi35 = require("@hono/zod-openapi");
5649
- var import_drizzle_orm31 = require("drizzle-orm");
5793
+ var import_drizzle_orm32 = require("drizzle-orm");
5650
5794
  var import_cookie4 = require("hono/cookie");
5651
5795
  var challengeResendRoute = (0, import_zod_openapi35.createRoute)({
5652
5796
  method: "post",
@@ -5684,7 +5828,7 @@ var challengeResendHandler = async (c) => {
5684
5828
  if (!payload.methods.includes("email")) {
5685
5829
  return problems.badRequest(c, "Email verification not available");
5686
5830
  }
5687
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm31.eq)(schema.users.id, payload.userId)).limit(1);
5831
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.id, payload.userId)).limit(1);
5688
5832
  if (!user) {
5689
5833
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5690
5834
  return problems.notFound(c, "User not found");
@@ -5915,7 +6059,7 @@ var authorizeHandler = async (c) => {
5915
6059
 
5916
6060
  // src/routes/oauth/callback.ts
5917
6061
  var import_zod_openapi39 = require("@hono/zod-openapi");
5918
- var import_drizzle_orm32 = require("drizzle-orm");
6062
+ var import_drizzle_orm33 = require("drizzle-orm");
5919
6063
  var callbackRoute = (0, import_zod_openapi39.createRoute)({
5920
6064
  method: "get",
5921
6065
  path: "/{provider}/callback",
@@ -6041,10 +6185,24 @@ var callbackHandler = async (c) => {
6041
6185
  logger_default.warn("No user ID returned from OAuth provider", { provider });
6042
6186
  return c.redirect(`${appUrl}/login?error=oauth_failed`);
6043
6187
  }
6044
- const { db, schema } = getAuthContext(c);
6045
- const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.email, email)).limit(1);
6188
+ const { db, schema, env, invitations, signup } = getAuthContext(c);
6189
+ const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm33.eq)(schema.users.email, email)).limit(1);
6046
6190
  let authUser = existingUser;
6047
6191
  if (!authUser) {
6192
+ const invited = await verifyInvitation(invitations, {
6193
+ token: invitationToken,
6194
+ email,
6195
+ db,
6196
+ env
6197
+ });
6198
+ if (signup === "invitation" && !invited) {
6199
+ logSecurityEvent("oauth_signup_refused_no_invitation", "medium", {
6200
+ email,
6201
+ provider,
6202
+ hadInvitation: !!invitationToken
6203
+ });
6204
+ return c.redirect(`${appUrl}/login?error=invitation_required`);
6205
+ }
6048
6206
  const [createdUser] = await db.insert(schema.users).values({
6049
6207
  email,
6050
6208
  name: name || null,
@@ -6055,6 +6213,15 @@ var callbackHandler = async (c) => {
6055
6213
  activatedAt: /* @__PURE__ */ new Date()
6056
6214
  }).returning({ id: schema.users.id, email: schema.users.email });
6057
6215
  authUser = createdUser;
6216
+ if (invited) {
6217
+ await acceptInvitation(invitations, {
6218
+ token: invited,
6219
+ user: { id: createdUser.id, email: createdUser.email, name: name || null },
6220
+ db,
6221
+ env,
6222
+ context: "oauth_invitation_onAccepted_failed"
6223
+ });
6224
+ }
6058
6225
  logSecurityEvent("user_signup_oauth", "low", {
6059
6226
  userId: createdUser.id,
6060
6227
  email: createdUser.email,
@@ -6094,7 +6261,7 @@ var callbackHandler = async (c) => {
6094
6261
 
6095
6262
  // src/routes/oauth/complete.ts
6096
6263
  var import_zod_openapi40 = require("@hono/zod-openapi");
6097
- var import_drizzle_orm33 = require("drizzle-orm");
6264
+ var import_drizzle_orm34 = require("drizzle-orm");
6098
6265
  var completeRoute = (0, import_zod_openapi40.createRoute)({
6099
6266
  method: "post",
6100
6267
  path: "/complete",
@@ -6142,7 +6309,7 @@ var completeHandler = async (c) => {
6142
6309
  redirect: storedRedirect
6143
6310
  } = JSON.parse(stored);
6144
6311
  const { db, schema } = getAuthContext(c);
6145
- await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm33.eq)(schema.users.id, userId));
6312
+ await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm34.eq)(schema.users.id, userId));
6146
6313
  await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
6147
6314
  const fingerprint = await generateFingerprint(c.req.raw);
6148
6315
  const ipAddress = getClientIp(c.req.raw);
@@ -6187,19 +6354,12 @@ var routes_default = authRoutes;
6187
6354
 
6188
6355
  // src/lib/email/webhook-verifier.ts
6189
6356
  var import_svix = require("svix");
6190
- function verifyWebhookSignature(payload, signature, secret) {
6357
+ function verifyWebhookSignature(payload, headers, secret) {
6191
6358
  if (!secret || secret.trim() === "") {
6192
6359
  throw new Error("Webhook secret is required");
6193
6360
  }
6194
6361
  try {
6195
- const wh = new import_svix.Webhook(secret);
6196
- wh.verify(payload, {
6197
- "svix-id": "",
6198
- // Not used by Resend
6199
- "svix-timestamp": "",
6200
- // Not used by Resend
6201
- "svix-signature": signature
6202
- });
6362
+ new import_svix.Webhook(secret).verify(payload, { ...headers });
6203
6363
  return true;
6204
6364
  } catch (error) {
6205
6365
  logger_default.warn("Webhook signature verification failed", {
@@ -6215,6 +6375,8 @@ function verifyWebhookSignature(payload, signature, secret) {
6215
6375
  CloudflareEmailAdapter,
6216
6376
  EmailService,
6217
6377
  MAX_CHALLENGE_ATTEMPTS,
6378
+ PASSWORDLESS_MAX_ATTEMPTS,
6379
+ PASSWORDLESS_TTL_MS,
6218
6380
  ProblemTypes,
6219
6381
  ResendAdapter,
6220
6382
  SCHEMA_VERSION,
@@ -6226,6 +6388,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6226
6388
  cancelEmailChange,
6227
6389
  checkAccountLocked,
6228
6390
  checkBreachedPassword,
6391
+ cleanupExpiredPasswordlessChallenges,
6229
6392
  clearAccountLockout,
6230
6393
  clearChallengeCookie,
6231
6394
  clearSessionCookie,
@@ -6236,6 +6399,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6236
6399
  createChallengeToken,
6237
6400
  createDeviceToken,
6238
6401
  createEmailAdapter,
6402
+ createPasswordlessChallenge,
6239
6403
  createProblemDetails,
6240
6404
  createSession,
6241
6405
  csrf,
@@ -6252,6 +6416,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6252
6416
  generate2faEnabledEmail,
6253
6417
  generateApiKey,
6254
6418
  generateBackupCodes,
6419
+ generateCode,
6255
6420
  generateCodeChallenge,
6256
6421
  generateCodeVerifier,
6257
6422
  generateEmailChangeConfirmation,
@@ -6290,14 +6455,17 @@ function verifyWebhookSignature(payload, signature, secret) {
6290
6455
  optionalAuth,
6291
6456
  parseDeviceName,
6292
6457
  parseDeviceType,
6458
+ passwordlessEmail,
6293
6459
  problemJson,
6294
6460
  problems,
6295
6461
  rateLimit,
6462
+ recentPasswordlessFailures,
6296
6463
  refreshSession,
6297
6464
  requestEmailChange,
6298
6465
  requireAuth,
6299
6466
  requireVerifiedEmail,
6300
6467
  retrieveChallengeToken,
6468
+ sessionRefreshDue,
6301
6469
  setChallengeCookie,
6302
6470
  setSessionCookie,
6303
6471
  setTrustedDeviceCookie,
@@ -6315,6 +6483,8 @@ function verifyWebhookSignature(payload, signature, secret) {
6315
6483
  verifyCodeChallenge,
6316
6484
  verifyPassword,
6317
6485
  verifyPasswordWithRotation,
6486
+ verifyPasswordlessCode,
6487
+ verifyPasswordlessLink,
6318
6488
  verifyTotpCode,
6319
6489
  verifyTurnstileToken,
6320
6490
  verifyWebhookSignature