@syncello/auth 3.6.0 → 4.0.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,6 +675,8 @@ 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,
@@ -727,7 +757,7 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
727
757
  note: "SSR requests have different User-Agent, causing expected mismatch"
728
758
  });
729
759
  }
730
- await refreshSession(db, { sessions }, sessionId);
760
+ if (sessionRefreshDue(row.sessionLastActiveAt)) await refreshSession(db, { sessions }, sessionId);
731
761
  return {
732
762
  user: {
733
763
  id: row.userId,
@@ -746,6 +776,10 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
746
776
  }
747
777
  };
748
778
  }
779
+ function sessionRefreshDue(lastActiveAt, now = Date.now()) {
780
+ const interval = AUTH_DEFAULTS.SESSION_REFRESH_INTERVAL_MINUTES * 60 * 1e3;
781
+ return lastActiveAt === null || now - lastActiveAt >= interval;
782
+ }
749
783
  async function refreshSession(db, tables2, sessionId, sessionTtlMs = AUTH_DEFAULTS.SESSION_TTL_DAYS * 24 * 60 * 60 * 1e3) {
750
784
  const { sessions } = tables2;
751
785
  const newExpiration = Date.now() + sessionTtlMs;
@@ -913,16 +947,16 @@ function createDeviceToken() {
913
947
  };
914
948
  }
915
949
  async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
916
- const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
950
+ const { eq: eq34, and: and15, gt: gt7 } = await import("drizzle-orm");
917
951
  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())
952
+ and15(
953
+ eq34(userTrustedDevicesTable.userId, userId),
954
+ eq34(userTrustedDevicesTable.tokenHash, tokenHash),
955
+ gt7(userTrustedDevicesTable.expiresAt, Date.now())
922
956
  )
923
957
  ).limit(1);
924
958
  if (device) {
925
- await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
959
+ await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq34(userTrustedDevicesTable.id, device.id));
926
960
  return true;
927
961
  }
928
962
  return false;
@@ -1335,582 +1369,48 @@ function base64UrlEncode(data) {
1335
1369
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1336
1370
  }
1337
1371
 
1338
- // src/middleware/auth.ts
1339
- var import_factory = require("hono/factory");
1372
+ // src/core/passwordless.ts
1340
1373
  var import_drizzle_orm4 = require("drizzle-orm");
1341
1374
 
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
- );
1375
+ // src/lib/email/security.ts
1376
+ function getAllowedDomains(appUrl) {
1377
+ const base = ["localhost", "127.0.0.1"];
1378
+ try {
1379
+ const url = new URL(appUrl);
1380
+ return [...base, url.hostname];
1381
+ } catch {
1382
+ return base;
1372
1383
  }
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
1384
  }
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;
1385
+ function sanitizeEmailUrl(url, appUrl = "") {
1386
+ let parsed;
1387
+ try {
1388
+ parsed = new URL(url);
1389
+ } catch {
1390
+ throw new Error("Invalid URL format");
1406
1391
  }
1407
- problem.traceId = options?.traceId || generateTraceId();
1408
- if (options?.requestId) {
1409
- problem.requestId = options.requestId;
1392
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1393
+ throw new Error(`Disallowed protocol: ${parsed.protocol}`);
1410
1394
  }
1411
- if (options?.extensions) {
1412
- Object.assign(problem, options.extensions);
1395
+ const allowedDomains = getAllowedDomains(appUrl);
1396
+ const hostname = parsed.hostname.toLowerCase();
1397
+ const isAllowed = allowedDomains.some((domain) => {
1398
+ return hostname === domain || hostname.endsWith(`.${domain}`);
1399
+ });
1400
+ if (!isAllowed) {
1401
+ throw new Error(`Disallowed domain: ${hostname}`);
1413
1402
  }
1414
- return problem;
1403
+ return parsed.toString();
1415
1404
  }
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]);
1405
+ function escapeHtml(text) {
1406
+ const htmlEscapeMap = {
1407
+ "&": "&amp;",
1408
+ "<": "&lt;",
1409
+ ">": "&gt;",
1410
+ '"': "&quot;",
1411
+ "'": "&#39;"
1412
+ };
1413
+ return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char]);
1914
1414
  }
1915
1415
 
1916
1416
  // src/core/email-template.ts
@@ -1932,8 +1432,10 @@ function generateEmailTemplate(options) {
1932
1432
  buttonFontWeight = 500,
1933
1433
  buttonRadius = "6px",
1934
1434
  logoUrl,
1935
- footerColor = "#1e1b4b"
1435
+ footerColor = "#1e1b4b",
1436
+ headingColor = "#1e1b4b"
1936
1437
  } = options;
1438
+ const safeAppName = escapeHtml(appName);
1937
1439
  const safeButtonUrl = buttonUrl ? sanitizeEmailUrl(buttonUrl, appUrl) : "";
1938
1440
  const safeAppUrl = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
1939
1441
  const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, appUrl) : "";
@@ -2077,7 +1579,7 @@ function generateEmailTemplate(options) {
2077
1579
  <td class="pad" style="padding-left:40px;padding-right:40px;width:100%;">
2078
1580
  <div class="alignment" align="center">
2079
1581
  <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">
1582
+ <img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${safeAppName}" title="${safeAppName}" height="auto">
2081
1583
  </div>
2082
1584
  </div>
2083
1585
  </td>
@@ -2089,7 +1591,7 @@ function generateEmailTemplate(options) {
2089
1591
  <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
1592
  <tr>
2091
1593
  <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;">
1594
+ <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
1595
  <p style="margin: 0; word-break: break-word;">${welcomeMessage}</p>
2094
1596
  </div>
2095
1597
  </td>
@@ -2102,7 +1604,7 @@ function generateEmailTemplate(options) {
2102
1604
  <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
1605
  <tr>
2104
1606
  <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;">
1607
+ <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
1608
  <p style="margin: 0; word-break: break-word;">${heading}</p>
2107
1609
  </div>
2108
1610
  </td>
@@ -2147,133 +1649,796 @@ function generateEmailTemplate(options) {
2147
1649
  </tr>
2148
1650
  </table>
2149
1651
 
2150
- <!-- Copy Link -->
2151
- <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;">
2152
- <tr>
2153
- <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
2154
- <div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
2155
- <p style="margin: 0 0 5px 0; word-break: break-word;">or copy and paste this link into your browser:</p>
2156
- <a href="${safeButtonUrl}" style="color:#4a90d9;text-decoration:underline;word-break:break-all;">${safeButtonUrl}</a>
2157
- </div>
2158
- </td>
2159
- </tr>
2160
- </table>
2161
- ` : ""}
1652
+ <!-- Copy Link -->
1653
+ <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;">
1654
+ <tr>
1655
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1656
+ <div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
1657
+ <p style="margin: 0 0 5px 0; word-break: break-word;">or copy and paste this link into your browser:</p>
1658
+ <a href="${safeButtonUrl}" style="color:#4a90d9;text-decoration:underline;word-break:break-all;">${safeButtonUrl}</a>
1659
+ </div>
1660
+ </td>
1661
+ </tr>
1662
+ </table>
1663
+ ` : ""}
1664
+
1665
+ ${securityWarning ? `
1666
+ <!-- Security Warning -->
1667
+ <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;">
1668
+ <tr>
1669
+ <td class="pad" style="padding-left:40px;padding-right:40px;padding-bottom:20px;">
1670
+ <div style="background-color: #a1f1ff; border-radius: 8px; padding: 16px; display: flex; align-items: center; justify-content: center;">
1671
+ <p style="margin: 0; color: #01001b; font-family: 'Montserrat', Arial, sans-serif; font-size: 13px; line-height: 1.5; text-align: center;">
1672
+ ${securityWarning}
1673
+ </p>
1674
+ </div>
1675
+ </td>
1676
+ </tr>
1677
+ </table>
1678
+ ` : ""}
1679
+
1680
+ <!-- Footer Note -->
1681
+ <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;">
1682
+ <tr>
1683
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1684
+ <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;">
1685
+ <p style="margin: 0; word-break: break-word;">${footerNote}</p>
1686
+ </div>
1687
+ </td>
1688
+ </tr>
1689
+ </table>
1690
+
1691
+ </td>
1692
+ </tr>
1693
+ </tbody>
1694
+ </table>
1695
+ </td>
1696
+ </tr>
1697
+ </tbody>
1698
+ </table>
1699
+
1700
+ <!-- Spacer Row -->
1701
+ <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;">
1702
+ <tbody>
1703
+ <tr>
1704
+ <td>
1705
+ <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;">
1706
+ <tbody>
1707
+ <tr>
1708
+ <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;">
1709
+ <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;">
1710
+ <tr>
1711
+ <td class="pad">
1712
+ <div></div>
1713
+ </td>
1714
+ </tr>
1715
+ </table>
1716
+ </td>
1717
+ </tr>
1718
+ </tbody>
1719
+ </table>
1720
+ </td>
1721
+ </tr>
1722
+ </tbody>
1723
+ </table>
1724
+
1725
+ <!-- Footer Row -->
1726
+ <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;">
1727
+ <tbody>
1728
+ <tr>
1729
+ <td>
1730
+ <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;">
1731
+ <tbody>
1732
+ <tr>
1733
+ <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;">
1734
+
1735
+ <!-- Policy Links -->
1736
+ <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;">
1737
+ <tr>
1738
+ <td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
1739
+ <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;">
1740
+ <p style="margin: 0; word-break: break-word;">
1741
+ <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>
1742
+ </p>
1743
+ </div>
1744
+ </td>
1745
+ </tr>
1746
+ </table>
1747
+
1748
+ <!-- Copyright & Disclaimer -->
1749
+ <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;">
1750
+ <tr>
1751
+ <td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
1752
+ <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;">
1753
+ <p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
1754
+ <p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
1755
+ 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.
1756
+ </p>
1757
+ </div>
1758
+ </td>
1759
+ </tr>
1760
+ </table>
1761
+
1762
+ </td>
1763
+ </tr>
1764
+ </tbody>
1765
+ </table>
1766
+ </td>
1767
+ </tr>
1768
+ </tbody>
1769
+ </table>
1770
+
1771
+ </td>
1772
+ </tr>
1773
+ </tbody>
1774
+ </table>
1775
+ </body>
1776
+ </html>
1777
+ `;
1778
+ }
1779
+
1780
+ // src/core/passwordless.ts
1781
+ var PASSWORDLESS_TTL_MS = 10 * 60 * 1e3;
1782
+ var PASSWORDLESS_MAX_ATTEMPTS = 5;
1783
+ var CODE_SPACE = 1e6;
1784
+ var CODE_LIMIT = Math.floor(4294967296 / CODE_SPACE) * CODE_SPACE;
1785
+ var CLEANUP_GRACE_MS = 24 * 60 * 60 * 1e3;
1786
+ var normalizeEmail = (email) => email.trim().toLowerCase();
1787
+ function generateCode() {
1788
+ const buffer = new Uint32Array(1);
1789
+ for (; ; ) {
1790
+ crypto.getRandomValues(buffer);
1791
+ if (buffer[0] < CODE_LIMIT) return String(buffer[0] % CODE_SPACE).padStart(6, "0");
1792
+ }
1793
+ }
1794
+ var codeHash = (challengeId, code) => hashToken(`${challengeId}:${code}`);
1795
+ function constantTimeEqual(a, b) {
1796
+ if (a.length !== b.length) return false;
1797
+ let diff = 0;
1798
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1799
+ return diff === 0;
1800
+ }
1801
+ async function consumeOpen(db, t, email, now) {
1802
+ 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)));
1803
+ }
1804
+ async function createPasswordlessChallenge(db, tables2, input) {
1805
+ const t = tables2.passwordlessChallenges;
1806
+ const email = normalizeEmail(input.email);
1807
+ const now = Date.now();
1808
+ const challengeId = crypto.randomUUID();
1809
+ const linkToken = generateSecureToken(32);
1810
+ const browserSecret = generateSecureToken(32);
1811
+ const code = generateCode();
1812
+ await consumeOpen(db, t, email, now);
1813
+ await db.insert(t).values({
1814
+ id: challengeId,
1815
+ email,
1816
+ subjectId: input.subjectId ?? null,
1817
+ linkTokenHash: await hashToken(linkToken),
1818
+ codeHash: await codeHash(challengeId, code),
1819
+ browserHash: await hashToken(browserSecret),
1820
+ attempts: 0,
1821
+ payload: input.payload ?? null,
1822
+ expiresAt: now + (input.ttlMs ?? PASSWORDLESS_TTL_MS),
1823
+ createdAt: now
1824
+ });
1825
+ return { challengeId, linkToken, code, browserSecret };
1826
+ }
1827
+ async function verifyPasswordlessLink(db, tables2, input) {
1828
+ const t = tables2.passwordlessChallenges;
1829
+ const now = Date.now();
1830
+ const hash = await hashToken(input.linkToken);
1831
+ 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();
1832
+ if (!row) {
1833
+ const [stale] = await db.select({ expiresAt: t.expiresAt, consumedAt: t.consumedAt }).from(t).where((0, import_drizzle_orm4.eq)(t.linkTokenHash, hash)).limit(1);
1834
+ const expired = stale && stale.consumedAt === null && stale.expiresAt <= now;
1835
+ return { ok: false, reason: expired ? "expired" : "invalid" };
1836
+ }
1837
+ await consumeOpen(db, t, row.email, now);
1838
+ return { ok: true, email: row.email, subjectId: row.subjectId, payload: row.payload };
1839
+ }
1840
+ async function verifyPasswordlessCode(db, tables2, input) {
1841
+ const t = tables2.passwordlessChallenges;
1842
+ const max = input.maxAttempts ?? PASSWORDLESS_MAX_ATTEMPTS;
1843
+ const email = normalizeEmail(input.email);
1844
+ const now = Date.now();
1845
+ 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);
1846
+ if (!row) return { ok: false, reason: "invalid" };
1847
+ if (!constantTimeEqual(row.browserHash, await hashToken(input.browserSecret))) {
1848
+ return { ok: false, reason: "invalid" };
1849
+ }
1850
+ if (row.expiresAt <= now) return { ok: false, reason: "expired" };
1851
+ if (row.attempts >= max) return { ok: false, reason: "locked" };
1852
+ if (!constantTimeEqual(row.codeHash, await codeHash(row.id, input.code.trim()))) {
1853
+ 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 });
1854
+ return { ok: false, reason: counted && counted.attempts < max ? "invalid" : "locked" };
1855
+ }
1856
+ 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();
1857
+ if (!won) return { ok: false, reason: "invalid" };
1858
+ await consumeOpen(db, t, email, now);
1859
+ return { ok: true, email, subjectId: won.subjectId, payload: won.payload };
1860
+ }
1861
+ async function recentPasswordlessFailures(db, tables2, input) {
1862
+ const t = tables2.passwordlessChallenges;
1863
+ 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)));
1864
+ return Number(row?.total ?? 0);
1865
+ }
1866
+ async function cleanupExpiredPasswordlessChallenges(db, tables2) {
1867
+ const t = tables2.passwordlessChallenges;
1868
+ await db.delete(t).where((0, import_drizzle_orm4.lt)(t.expiresAt, Date.now() - CLEANUP_GRACE_MS));
1869
+ }
1870
+ function passwordlessEmail(input) {
1871
+ const { appName, appUrl, linkUrl, code } = input;
1872
+ const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
1873
+ const lifetime = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1874
+ const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
1875
+ const html = generateEmailTemplate({
1876
+ heading: `Sign in to ${escapeHtml(appName)}`,
1877
+ body: `<p>Use this code, or the button below, to sign in:</p>
1878
+ <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
1879
+ <p>The code and the link expire in ${lifetime}.</p>`,
1880
+ buttonText: "Sign in",
1881
+ buttonUrl: safeLink,
1882
+ footerNote: "If you didn't ask to sign in, you can ignore this email.",
1883
+ appUrl,
1884
+ appName
1885
+ });
1886
+ return {
1887
+ subject: `${code} is your sign-in code`,
1888
+ html,
1889
+ text: `Your ${appName} sign-in code is ${code}.
1890
+
1891
+ Or sign in with this link:
1892
+ ${safeLink}
2162
1893
 
2163
- ${securityWarning ? `
2164
- <!-- Security Warning -->
2165
- <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;">
2166
- <tr>
2167
- <td class="pad" style="padding-left:40px;padding-right:40px;padding-bottom:20px;">
2168
- <div style="background-color: #a1f1ff; border-radius: 8px; padding: 16px; display: flex; align-items: center; justify-content: center;">
2169
- <p style="margin: 0; color: #01001b; font-family: 'Montserrat', Arial, sans-serif; font-size: 13px; line-height: 1.5; text-align: center;">
2170
- ${securityWarning}
2171
- </p>
2172
- </div>
2173
- </td>
2174
- </tr>
2175
- </table>
2176
- ` : ""}
1894
+ The code and the link expire in ${lifetime}.
2177
1895
 
2178
- <!-- Footer Note -->
2179
- <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
- <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>
2184
- </div>
2185
- </td>
2186
- </tr>
2187
- </table>
1896
+ If you didn't ask to sign in, you can ignore this email.`
1897
+ };
1898
+ }
1899
+
1900
+ // src/middleware/auth.ts
1901
+ var import_factory = require("hono/factory");
1902
+ var import_drizzle_orm5 = require("drizzle-orm");
1903
+
1904
+ // src/factory.ts
1905
+ function createAuth(config) {
1906
+ const { db, schema, getEnv, emailBranding, invitations, signup = "open" } = config;
1907
+ if (signup === "invitation" && !invitations) {
1908
+ throw new Error("createAuth: signup: 'invitation' requires the invitations hooks");
1909
+ }
1910
+ const getContext = (c) => ({
1911
+ db,
1912
+ schema,
1913
+ env: getEnv(c),
1914
+ emailBranding,
1915
+ invitations,
1916
+ signup
1917
+ });
1918
+ return {
1919
+ db,
1920
+ schema,
1921
+ getContext
1922
+ };
1923
+ }
1924
+ function createAuthMiddleware(auth2) {
1925
+ return async (c, next) => {
1926
+ const ctx = auth2.getContext(c);
1927
+ c.set("authContext", ctx);
1928
+ c.set("db", auth2.db);
1929
+ await next();
1930
+ };
1931
+ }
1932
+ function getAuthContext(c) {
1933
+ const ctx = c.get("authContext");
1934
+ if (!ctx) {
1935
+ throw new Error(
1936
+ "AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
1937
+ );
1938
+ }
1939
+ return ctx;
1940
+ }
1941
+
1942
+ // src/lib/problem-json.ts
1943
+ var ProblemTypes = {
1944
+ // 4xx Client Errors
1945
+ BAD_REQUEST: "/errors/bad-request",
1946
+ UNAUTHORIZED: "/errors/unauthorized",
1947
+ FORBIDDEN: "/errors/forbidden",
1948
+ NOT_FOUND: "/errors/not-found",
1949
+ CONFLICT: "/errors/conflict",
1950
+ GONE: "/errors/gone",
1951
+ VALIDATION_ERROR: "/errors/validation-error",
1952
+ RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
1953
+ // 5xx Server Errors
1954
+ INTERNAL_ERROR: "/errors/internal-error",
1955
+ SERVICE_UNAVAILABLE: "/errors/service-unavailable"
1956
+ };
1957
+ function generateTraceId() {
1958
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
1959
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1960
+ }
1961
+ function createProblemDetails(type, title, status, options) {
1962
+ const problem = {
1963
+ type,
1964
+ title,
1965
+ status
1966
+ };
1967
+ if (options?.detail) {
1968
+ problem.detail = options.detail;
1969
+ }
1970
+ if (options?.instance) {
1971
+ problem.instance = options.instance;
1972
+ }
1973
+ problem.traceId = options?.traceId || generateTraceId();
1974
+ if (options?.requestId) {
1975
+ problem.requestId = options.requestId;
1976
+ }
1977
+ if (options?.extensions) {
1978
+ Object.assign(problem, options.extensions);
1979
+ }
1980
+ return problem;
1981
+ }
1982
+ function problemJson(c, problem) {
1983
+ logger_default.warn("API error response", {
1984
+ type: problem.type,
1985
+ status: problem.status,
1986
+ traceId: problem.traceId,
1987
+ requestId: problem.requestId,
1988
+ path: c.req.path,
1989
+ method: c.req.method
1990
+ });
1991
+ return c.json(problem, problem.status, {
1992
+ "Content-Type": "application/problem+json"
1993
+ });
1994
+ }
1995
+ var problems = {
1996
+ /**
1997
+ * 400 Bad Request - Generic client error
1998
+ */
1999
+ badRequest(c, detail) {
2000
+ return problemJson(
2001
+ c,
2002
+ createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
2003
+ detail: detail || "The request could not be understood by the server",
2004
+ instance: `${c.req.method} ${c.req.path}`,
2005
+ requestId: c.get("requestId"),
2006
+ traceId: c.get("traceId")
2007
+ })
2008
+ );
2009
+ },
2010
+ /**
2011
+ * 400 Bad Request - Validation errors with field-specific details
2012
+ */
2013
+ validationError(c, detail, errors) {
2014
+ return problemJson(
2015
+ c,
2016
+ createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
2017
+ detail,
2018
+ instance: `${c.req.method} ${c.req.path}`,
2019
+ requestId: c.get("requestId"),
2020
+ traceId: c.get("traceId"),
2021
+ extensions: errors ? { errors } : void 0
2022
+ })
2023
+ );
2024
+ },
2025
+ /**
2026
+ * 401 Unauthorized - Authentication required
2027
+ */
2028
+ unauthorized(c, detail) {
2029
+ return problemJson(
2030
+ c,
2031
+ createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
2032
+ detail: detail || "Authentication is required to access this resource",
2033
+ instance: `${c.req.method} ${c.req.path}`,
2034
+ requestId: c.get("requestId"),
2035
+ traceId: c.get("traceId")
2036
+ })
2037
+ );
2038
+ },
2039
+ /**
2040
+ * 403 Forbidden - Insufficient permissions
2041
+ */
2042
+ forbidden(c, detail) {
2043
+ return problemJson(
2044
+ c,
2045
+ createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
2046
+ detail: detail || "You do not have permission to access this resource",
2047
+ instance: `${c.req.method} ${c.req.path}`,
2048
+ requestId: c.get("requestId"),
2049
+ traceId: c.get("traceId")
2050
+ })
2051
+ );
2052
+ },
2053
+ /**
2054
+ * 404 Not Found
2055
+ */
2056
+ notFound(c, detail) {
2057
+ return problemJson(
2058
+ c,
2059
+ createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
2060
+ detail: detail || "The requested resource was not found",
2061
+ instance: `${c.req.method} ${c.req.path}`,
2062
+ requestId: c.get("requestId"),
2063
+ traceId: c.get("traceId")
2064
+ })
2065
+ );
2066
+ },
2067
+ /**
2068
+ * 409 Conflict - Resource conflict (e.g., duplicate email)
2069
+ */
2070
+ conflict(c, detail) {
2071
+ return problemJson(
2072
+ c,
2073
+ createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
2074
+ detail,
2075
+ instance: `${c.req.method} ${c.req.path}`,
2076
+ requestId: c.get("requestId"),
2077
+ traceId: c.get("traceId")
2078
+ })
2079
+ );
2080
+ },
2081
+ /**
2082
+ * 410 Gone - Resource is no longer available
2083
+ */
2084
+ gone(c, detail) {
2085
+ return problemJson(
2086
+ c,
2087
+ createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
2088
+ detail: detail || "The requested resource is no longer available",
2089
+ instance: `${c.req.method} ${c.req.path}`,
2090
+ requestId: c.get("requestId"),
2091
+ traceId: c.get("traceId")
2092
+ })
2093
+ );
2094
+ },
2095
+ /**
2096
+ * 429 Too Many Requests - Rate limit exceeded
2097
+ */
2098
+ rateLimitExceeded(c, retryAfter) {
2099
+ const extensions = retryAfter ? { retryAfter } : void 0;
2100
+ return problemJson(
2101
+ c,
2102
+ createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
2103
+ detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
2104
+ instance: `${c.req.method} ${c.req.path}`,
2105
+ requestId: c.get("requestId"),
2106
+ traceId: c.get("traceId"),
2107
+ extensions
2108
+ })
2109
+ );
2110
+ },
2111
+ /**
2112
+ * 500 Internal Server Error - Generic server error
2113
+ * SECURITY: Never include error details or stack traces
2114
+ */
2115
+ internalError(c, error) {
2116
+ const traceId = c.get("traceId") || generateTraceId();
2117
+ if (error) {
2118
+ logError(error, {
2119
+ context: "internal_error",
2120
+ path: c.req.path,
2121
+ method: c.req.method,
2122
+ traceId,
2123
+ requestId: c.get("requestId")
2124
+ });
2125
+ }
2126
+ return problemJson(
2127
+ c,
2128
+ createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
2129
+ detail: "An unexpected error occurred. Please try again later",
2130
+ instance: `${c.req.method} ${c.req.path}`,
2131
+ traceId,
2132
+ requestId: c.get("requestId")
2133
+ })
2134
+ );
2135
+ },
2136
+ /**
2137
+ * 503 Service Unavailable - Service is temporarily unavailable
2138
+ */
2139
+ serviceUnavailable(c, detail) {
2140
+ return problemJson(
2141
+ c,
2142
+ createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
2143
+ detail: detail || "The service is temporarily unavailable. Please try again later",
2144
+ instance: `${c.req.method} ${c.req.path}`,
2145
+ requestId: c.get("requestId"),
2146
+ traceId: c.get("traceId")
2147
+ })
2148
+ );
2149
+ }
2150
+ };
2188
2151
 
2189
- </td>
2190
- </tr>
2191
- </tbody>
2192
- </table>
2193
- </td>
2194
- </tr>
2195
- </tbody>
2196
- </table>
2152
+ // src/middleware/auth.ts
2153
+ var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
2154
+ async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
2155
+ if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
2156
+ logger_default.info("Session exceeded absolute lifetime - revoked", {
2157
+ type: "security",
2158
+ event: "session_max_lifetime_exceeded",
2159
+ severity: "low",
2160
+ sessionId: sessionId.slice(0, 8),
2161
+ userId: session.userId
2162
+ });
2163
+ await deleteSession(db, { sessions }, sessionId);
2164
+ return true;
2165
+ }
2166
+ async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
2167
+ const cookieName = getSessionCookieName(env);
2168
+ const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
2169
+ const sessionIdMatch = cookieHeader.match(cookiePattern);
2170
+ if (!sessionIdMatch) return null;
2171
+ const sessionId = sessionIdMatch[1];
2172
+ const foundSessions = await db.select({
2173
+ userId: sessions.userId,
2174
+ expiresAt: sessions.expiresAt,
2175
+ createdAt: sessions.createdAt,
2176
+ lastActiveAt: sessions.lastActiveAt,
2177
+ fingerprint: sessions.fingerprint
2178
+ }).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);
2179
+ if (foundSessions.length === 0) return null;
2180
+ const session = foundSessions[0];
2181
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
2182
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
2183
+ const userAgent = request.headers.get("user-agent") || "";
2184
+ const cfWorker = request.headers.get("cf-worker") || "";
2185
+ const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
2186
+ const isLocalSSR = userAgent.toLowerCase().includes("node");
2187
+ const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
2188
+ const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
2189
+ const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
2190
+ if (isSSR) {
2191
+ logger_default.info("SSR request detected, using IP validation", {
2192
+ type: "security",
2193
+ event: "ssr_session_validation",
2194
+ sessionId: sessionId.slice(0, 8),
2195
+ userId: session.userId,
2196
+ clientIp,
2197
+ cfWorker,
2198
+ isLocalSSR,
2199
+ isSvelteKitSSR,
2200
+ isCloudflareSSR
2201
+ });
2202
+ } else {
2203
+ if (session.fingerprint) {
2204
+ const currentFingerprint = await generateFingerprint(request);
2205
+ if (session.fingerprint !== currentFingerprint) {
2206
+ logger_default.info("Session fingerprint changed (not enforced)", {
2207
+ type: "security",
2208
+ event: "fingerprint_changed",
2209
+ sessionId: sessionId.slice(0, 8),
2210
+ userId: session.userId,
2211
+ clientIp
2212
+ });
2213
+ }
2214
+ }
2215
+ }
2216
+ return {
2217
+ userId: session.userId,
2218
+ expiresAt: session.expiresAt
2219
+ };
2220
+ }
2221
+ async function getSessionFromBearerToken(db, sessions, authHeader) {
2222
+ if (!authHeader.startsWith("Bearer ")) return null;
2223
+ const sessionId = authHeader.slice(7);
2224
+ const foundSessions = await db.select({
2225
+ userId: sessions.userId,
2226
+ expiresAt: sessions.expiresAt,
2227
+ createdAt: sessions.createdAt,
2228
+ lastActiveAt: sessions.lastActiveAt
2229
+ }).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);
2230
+ if (foundSessions.length === 0) return null;
2231
+ const session = foundSessions[0];
2232
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
2233
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
2234
+ return {
2235
+ userId: session.userId,
2236
+ expiresAt: session.expiresAt
2237
+ };
2238
+ }
2239
+ var requireAuth = (0, import_factory.createMiddleware)(
2240
+ async (c, next) => {
2241
+ const { db, schema } = getAuthContext(c);
2242
+ const authHeader = c.req.header("Authorization");
2243
+ if (authHeader) {
2244
+ const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
2245
+ if (session2) {
2246
+ c.set("userId", session2.userId);
2247
+ await next();
2248
+ return;
2249
+ }
2250
+ }
2251
+ const cookieHeader = c.req.header("cookie");
2252
+ if (!cookieHeader) {
2253
+ return problems.unauthorized(c, "Please log in to continue");
2254
+ }
2255
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
2256
+ if (!session) {
2257
+ c.header("Set-Cookie", clearSessionCookie(c.env));
2258
+ return problems.unauthorized(c, "Your session has expired. Please log in again.");
2259
+ }
2260
+ c.set("userId", session.userId);
2261
+ await next();
2262
+ }
2263
+ );
2264
+ var optionalAuth = (0, import_factory.createMiddleware)(
2265
+ async (c, next) => {
2266
+ const { db, schema } = getAuthContext(c);
2267
+ const authHeader = c.req.header("Authorization");
2268
+ if (authHeader) {
2269
+ const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
2270
+ if (session) {
2271
+ c.set("userId", session.userId);
2272
+ await next();
2273
+ return;
2274
+ }
2275
+ }
2276
+ const cookieHeader = c.req.header("cookie");
2277
+ if (cookieHeader) {
2278
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
2279
+ if (session) {
2280
+ c.set("userId", session.userId);
2281
+ }
2282
+ }
2283
+ await next();
2284
+ }
2285
+ );
2197
2286
 
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>
2287
+ // src/middleware/csrf.ts
2288
+ var import_factory3 = require("hono/factory");
2289
+ var csrf = (0, import_factory3.createMiddleware)(async (c, next) => {
2290
+ const method = c.req.method;
2291
+ const path = c.req.path;
2292
+ const isWebhook = path.startsWith("/v1/webhooks/");
2293
+ if (isWebhook) {
2294
+ await next();
2295
+ return;
2296
+ }
2297
+ const isMcpEndpoint = path === "/v1/mcp";
2298
+ if (isMcpEndpoint) {
2299
+ await next();
2300
+ return;
2301
+ }
2302
+ const isOAuthMcpEndpoint = path.startsWith("/v1/oauth/");
2303
+ if (isOAuthMcpEndpoint) {
2304
+ await next();
2305
+ return;
2306
+ }
2307
+ const isToolsApi = path.startsWith("/v1/tools/") && !path.startsWith("/v1/tools/catalog");
2308
+ if (isToolsApi) {
2309
+ await next();
2310
+ return;
2311
+ }
2312
+ const isAdminApi = path.startsWith("/v1/admin/");
2313
+ if (isAdminApi) {
2314
+ await next();
2315
+ return;
2316
+ }
2317
+ const isMobileAuthEndpoint = path === "/v1/auth/login" || path === "/v1/auth/signup" || path === "/v1/auth/refresh";
2318
+ const authHeader = c.req.header("Authorization");
2319
+ const hasBearerToken = authHeader?.startsWith("Bearer ");
2320
+ const hasNoOrigin = !c.req.header("origin") && !c.req.header("referer");
2321
+ if (isMobileAuthEndpoint && hasNoOrigin || hasBearerToken) {
2322
+ await next();
2323
+ return;
2324
+ }
2325
+ const isInternalSync = path.startsWith("/v1/internal/sync/");
2326
+ if (isInternalSync) {
2327
+ await next();
2328
+ return;
2329
+ }
2330
+ if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
2331
+ const appUrl = c.env.APP_URL || "http://localhost:5173";
2332
+ const allowedOrigins = getAllowedOrigins(appUrl);
2333
+ const request = c.req.raw;
2334
+ const isTestEndpoint = c.req.path.startsWith("/test");
2335
+ if (isTestEndpoint) {
2336
+ const origin = request.headers.get("origin");
2337
+ const referer = request.headers.get("referer");
2338
+ logger_default.debug("CSRF validation for test endpoint", {
2339
+ path: c.req.path,
2340
+ method,
2341
+ origin,
2342
+ referer,
2343
+ appUrl,
2344
+ allowedOrigins
2345
+ });
2346
+ }
2347
+ const pagesPattern = c.env.CORS_PAGES_PATTERN;
2348
+ if (!validateOrigin(request, allowedOrigins, pagesPattern)) {
2349
+ return problems.forbidden(c, "Request blocked for security reasons. Please try again.");
2350
+ }
2351
+ }
2352
+ await next();
2353
+ });
2222
2354
 
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;">
2355
+ // src/middleware/rateLimit.ts
2356
+ var import_factory4 = require("hono/factory");
2232
2357
 
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>
2358
+ // src/lib/rate-limit-kv.ts
2359
+ async function checkRateLimitKV(kv, key, maxAttempts, windowSeconds) {
2360
+ try {
2361
+ const rateLimitKey = `rate_limit:${key}`;
2362
+ const currentAttemptsStr = await kv.get(rateLimitKey);
2363
+ const currentAttempts = currentAttemptsStr ? parseInt(currentAttemptsStr, 10) : 0;
2364
+ if (currentAttempts >= maxAttempts) {
2365
+ return {
2366
+ allowed: false,
2367
+ remaining: 0
2368
+ };
2369
+ }
2370
+ const newAttempts = currentAttempts + 1;
2371
+ await kv.put(rateLimitKey, newAttempts.toString(), {
2372
+ expirationTtl: windowSeconds
2373
+ });
2374
+ return {
2375
+ allowed: true,
2376
+ remaining: maxAttempts - newAttempts
2377
+ };
2378
+ } catch (error) {
2379
+ logger_default.error("Rate limit KV error, failing open", {
2380
+ key,
2381
+ error: error instanceof Error ? error.message : "Unknown error"
2382
+ });
2383
+ return {
2384
+ allowed: true,
2385
+ remaining: maxAttempts
2386
+ };
2387
+ }
2388
+ }
2245
2389
 
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>
2390
+ // src/middleware/rateLimit.ts
2391
+ var rateLimit = (config) => (0, import_factory4.createMiddleware)(async (c, next) => {
2392
+ const kv = c.env.RATE_LIMIT_KV;
2393
+ const identifier = await config.identifier(c);
2394
+ const key = `${config.action}:${identifier}`;
2395
+ const windowSeconds = Math.ceil(config.windowMs / 1e3);
2396
+ const { allowed, remaining } = await checkRateLimitKV(
2397
+ kv,
2398
+ key,
2399
+ config.maxAttempts,
2400
+ windowSeconds
2401
+ );
2402
+ const resetTimestamp = Math.ceil(Date.now() / 1e3) + windowSeconds;
2403
+ c.header("X-RateLimit-Limit", config.maxAttempts.toString());
2404
+ c.header("X-RateLimit-Remaining", Math.max(0, remaining).toString());
2405
+ c.header("X-RateLimit-Reset", resetTimestamp.toString());
2406
+ if (!allowed) {
2407
+ c.header("Retry-After", windowSeconds.toString());
2408
+ return problems.rateLimitExceeded(c, windowSeconds);
2409
+ }
2410
+ await next();
2411
+ });
2259
2412
 
2260
- </td>
2261
- </tr>
2262
- </tbody>
2263
- </table>
2264
- </td>
2265
- </tr>
2266
- </tbody>
2267
- </table>
2413
+ // src/middleware/require-verified-email.ts
2414
+ var import_factory5 = require("hono/factory");
2415
+ var import_drizzle_orm6 = require("drizzle-orm");
2416
+ var requireVerifiedEmail = (0, import_factory5.createMiddleware)(
2417
+ async (c, next) => {
2418
+ const userId = c.get("userId");
2419
+ if (!userId) {
2420
+ return problems.unauthorized(c, "Not authenticated");
2421
+ }
2422
+ const { db, schema } = getAuthContext(c);
2423
+ const database = db;
2424
+ const users = schema.users;
2425
+ const [user] = await database.select({ emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm6.eq)(users.id, userId)).limit(1);
2426
+ if (!user || !user.emailVerified) {
2427
+ return problems.forbidden(c, "Please verify your email address to continue");
2428
+ }
2429
+ await next();
2430
+ }
2431
+ );
2268
2432
 
2269
- </td>
2270
- </tr>
2271
- </tbody>
2272
- </table>
2273
- </body>
2274
- </html>
2275
- `;
2276
- }
2433
+ // src/routes/index.ts
2434
+ var import_zod_openapi42 = require("@hono/zod-openapi");
2435
+
2436
+ // src/routes/signup.ts
2437
+ var import_zod_openapi2 = require("@hono/zod-openapi");
2438
+ var import_drizzle_orm10 = require("drizzle-orm");
2439
+
2440
+ // src/lib/email/email-service.ts
2441
+ var import_drizzle_orm7 = require("drizzle-orm");
2277
2442
 
2278
2443
  // src/lib/email/templates.ts
2279
2444
  function extractAppUrl(url) {
@@ -2297,6 +2462,7 @@ function generateVerificationEmail(data, appName = "Your App", configuredAppUrl
2297
2462
  welcomeMessage: safeFirstName ? `Hi ${safeFirstName}, Welcome to ${escapeHtml(appName)}!` : void 0,
2298
2463
  footerNote: "",
2299
2464
  appUrl,
2465
+ appName,
2300
2466
  ...branding
2301
2467
  });
2302
2468
  return {
@@ -2332,6 +2498,7 @@ function generatePasswordResetEmail(data, appName = "Your App", configuredAppUrl
2332
2498
  `,
2333
2499
  footerNote: "If you didn't request this change, no action is needed. Your password will remain unchanged.",
2334
2500
  appUrl,
2501
+ appName,
2335
2502
  ...branding
2336
2503
  });
2337
2504
  return {
@@ -2367,6 +2534,7 @@ function generateEmailChangeConfirmation(data, appName = "Your App", configuredA
2367
2534
  `,
2368
2535
  footerNote: "If you didn't request this change, you can safely ignore this email.",
2369
2536
  appUrl,
2537
+ appName,
2370
2538
  ...branding
2371
2539
  });
2372
2540
  return {
@@ -2405,6 +2573,7 @@ function generateEmailChangeNotification(data, appName = "Your App", configuredA
2405
2573
  `,
2406
2574
  footerNote: "If you didn't request this change, click the button above to cancel it.",
2407
2575
  appUrl,
2576
+ appName,
2408
2577
  ...branding
2409
2578
  });
2410
2579
  return {
@@ -2441,6 +2610,7 @@ function generate2faCodeEmail(data, appName = "Your App", appUrl = "", branding)
2441
2610
  buttonUrl: "",
2442
2611
  footerNote: "",
2443
2612
  appUrl,
2613
+ appName,
2444
2614
  ...branding
2445
2615
  });
2446
2616
  return {
@@ -2476,6 +2646,7 @@ function generate2faEnabledEmail(data, appName = "Your App", appUrl = "", brandi
2476
2646
  buttonUrl: "",
2477
2647
  footerNote: "",
2478
2648
  appUrl,
2649
+ appName,
2479
2650
  ...branding
2480
2651
  });
2481
2652
  return {
@@ -2506,6 +2677,7 @@ function generate2faDisabledEmail(data, appName = "Your App", appUrl = "", brand
2506
2677
  buttonUrl: appUrl ? `${appUrl}/forgot-password` : "",
2507
2678
  footerNote: "",
2508
2679
  appUrl,
2680
+ appName,
2509
2681
  ...branding
2510
2682
  });
2511
2683
  return {
@@ -2722,7 +2894,7 @@ var EmailService = class {
2722
2894
  const [user] = await db.select({
2723
2895
  emailBounced: usersTable.emailBounced,
2724
2896
  emailComplained: usersTable.emailComplained
2725
- }).from(usersTable).where((0, import_drizzle_orm6.eq)(usersTable.email, emailAddress.toLowerCase())).limit(1);
2897
+ }).from(usersTable).where((0, import_drizzle_orm7.eq)(usersTable.email, emailAddress.toLowerCase())).limit(1);
2726
2898
  if (!user) {
2727
2899
  return { blocked: false };
2728
2900
  }
@@ -3171,7 +3343,7 @@ var EmailService = class {
3171
3343
  var import_resend2 = require("resend");
3172
3344
 
3173
3345
  // src/lib/email/webhook-handler.ts
3174
- var import_drizzle_orm7 = require("drizzle-orm");
3346
+ var import_drizzle_orm8 = require("drizzle-orm");
3175
3347
  async function handleWebhookEvent(payload, db, tables2) {
3176
3348
  const { type, data } = payload;
3177
3349
  const emailAddress = data.to[0];
@@ -3230,7 +3402,7 @@ async function handleBounce(emailAddress, emailId, bounce, db, usersTable) {
3230
3402
  await db.update(usersTable).set({
3231
3403
  emailBounced: true,
3232
3404
  emailBouncedAt: /* @__PURE__ */ new Date()
3233
- }).where((0, import_drizzle_orm7.eq)(usersTable.email, emailAddress.toLowerCase()));
3405
+ }).where((0, import_drizzle_orm8.eq)(usersTable.email, emailAddress.toLowerCase()));
3234
3406
  logError(new Error("Email bounced"), {
3235
3407
  context: "email_webhook",
3236
3408
  emailAddress,
@@ -3246,7 +3418,7 @@ async function handleComplaint(emailAddress, emailId, db, usersTable) {
3246
3418
  await db.update(usersTable).set({
3247
3419
  emailComplained: true,
3248
3420
  emailComplainedAt: /* @__PURE__ */ new Date()
3249
- }).where((0, import_drizzle_orm7.eq)(usersTable.email, emailAddress.toLowerCase()));
3421
+ }).where((0, import_drizzle_orm8.eq)(usersTable.email, emailAddress.toLowerCase()));
3250
3422
  logError(new Error("Email spam complaint received"), {
3251
3423
  context: "email_webhook",
3252
3424
  emailAddress,
@@ -3268,15 +3440,15 @@ async function handleFailure(emailAddress, emailId, failed) {
3268
3440
  }
3269
3441
 
3270
3442
  // src/lib/email/monitoring.ts
3271
- var import_drizzle_orm8 = require("drizzle-orm");
3443
+ var import_drizzle_orm9 = require("drizzle-orm");
3272
3444
  async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
3273
3445
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
3274
3446
  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(
3447
+ totalSent: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3448
+ totalBounced: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.bounced')`.as(
3277
3449
  "total_bounced"
3278
3450
  )
3279
- }).from(emailEventsTable).where(import_drizzle_orm8.sql`created_at > ${cutoff}`);
3451
+ }).from(emailEventsTable).where(import_drizzle_orm9.sql`created_at > ${cutoff}`);
3280
3452
  if (!stats || stats.totalSent === 0) {
3281
3453
  return 0;
3282
3454
  }
@@ -3295,11 +3467,11 @@ async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
3295
3467
  async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
3296
3468
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
3297
3469
  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(
3470
+ totalSent: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3471
+ totalComplained: import_drizzle_orm9.sql`count(*) filter (where event_type = 'email.complained')`.as(
3300
3472
  "total_complained"
3301
3473
  )
3302
- }).from(emailEventsTable).where(import_drizzle_orm8.sql`created_at > ${cutoff}`);
3474
+ }).from(emailEventsTable).where(import_drizzle_orm9.sql`created_at > ${cutoff}`);
3303
3475
  if (!stats || stats.totalSent === 0) {
3304
3476
  return 0;
3305
3477
  }
@@ -3316,16 +3488,34 @@ async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
3316
3488
  return complaintRate;
3317
3489
  }
3318
3490
 
3491
+ // src/core/invitations.ts
3492
+ async function verifyInvitation(invitations, args) {
3493
+ if (!invitations || !args.token) return null;
3494
+ const email = args.email.toLowerCase();
3495
+ try {
3496
+ const match = await invitations.verify({ token: args.token, email, db: args.db, env: args.env });
3497
+ return match && match.email.toLowerCase() === email ? args.token : null;
3498
+ } catch (err) {
3499
+ logError(err, { context: "invitation_verify_failed", email });
3500
+ return null;
3501
+ }
3502
+ }
3503
+ async function acceptInvitation(invitations, args) {
3504
+ if (!invitations?.onAccepted) return;
3505
+ try {
3506
+ await invitations.onAccepted({ token: args.token, user: args.user, db: args.db, env: args.env });
3507
+ } catch (err) {
3508
+ logError(err, { context: args.context, userId: args.user.id });
3509
+ }
3510
+ }
3511
+
3319
3512
  // src/routes/schemas.ts
3320
3513
  var import_zod_openapi = require("@hono/zod-openapi");
3321
3514
  var signupRequestSchema = import_zod_openapi.z.object({
3322
3515
  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!" }),
3516
+ password: import_zod_openapi.z.string().min(8).openapi({ example: "SecurePass123!" }),
3325
3517
  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-..." }),
3518
+ turnstileToken: import_zod_openapi.z.string().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
3329
3519
  // App invitation token (from /signup?token=...). When it validates for this
3330
3520
  // email, the account is created already-verified with no verification email.
3331
3521
  invitationToken: import_zod_openapi.z.string().optional().openapi({ example: "iIB8tDJJ..." })
@@ -3451,76 +3641,22 @@ var signupRoute = (0, import_zod_openapi2.createRoute)({
3451
3641
  description: "Invalid input",
3452
3642
  content: { "application/json": { schema: errorResponseSchema } }
3453
3643
  },
3644
+ 403: {
3645
+ description: "Sign-up is by invitation only and no valid invitation was given",
3646
+ content: { "application/json": { schema: errorResponseSchema } }
3647
+ },
3454
3648
  409: {
3455
3649
  description: "Email already registered",
3456
3650
  content: { "application/json": { schema: errorResponseSchema } }
3457
3651
  }
3458
3652
  }
3459
3653
  });
3654
+ var INVITATION_ONLY = "Sign-up is by invitation only";
3655
+ var INVITATION_INVALID = "This invitation is invalid, expired, or for a different email address";
3460
3656
  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);
3657
+ const { email, password, name, turnstileToken, invitationToken } = c.req.valid("json");
3658
+ const { db: database, schema, env: authEnv, emailBranding, invitations, signup } = getAuthContext(c);
3463
3659
  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
3660
  const turnstileValid = await verifyTurnstileToken(
3525
3661
  turnstileToken,
3526
3662
  env.TURNSTILE_SECRET_KEY,
@@ -3530,8 +3666,22 @@ var signupHandler = async (c) => {
3530
3666
  if (!turnstileValid) {
3531
3667
  return problems.badRequest(c, "Invalid captcha");
3532
3668
  }
3669
+ const invited = await verifyInvitation(invitations, {
3670
+ token: invitationToken,
3671
+ email,
3672
+ db: database,
3673
+ env: authEnv
3674
+ });
3675
+ if (invitationToken && invitations && !invited && signup !== "invitation") {
3676
+ return problems.badRequest(c, INVITATION_INVALID);
3677
+ }
3678
+ if (signup === "invitation" && !invited) {
3679
+ logSecurityEvent("signup_refused_no_invitation", "medium", { email: email.toLowerCase() });
3680
+ return problems.forbidden(c, invitationToken ? INVITATION_INVALID : INVITATION_ONLY);
3681
+ }
3682
+ const invitedVerified = invited !== null;
3533
3683
  const existingUser = await database.query.users.findFirst({
3534
- where: (0, import_drizzle_orm9.eq)(schema.users.email, email.toLowerCase())
3684
+ where: (0, import_drizzle_orm10.eq)(schema.users.email, email.toLowerCase())
3535
3685
  });
3536
3686
  if (existingUser) {
3537
3687
  return problems.conflict(c, "Email already registered");
@@ -3541,22 +3691,6 @@ var signupHandler = async (c) => {
3541
3691
  return problems.badRequest(c, passwordValidation.error || "Invalid password");
3542
3692
  }
3543
3693
  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
3694
  const [newUser] = await database.insert(schema.users).values({
3561
3695
  email: email.toLowerCase(),
3562
3696
  name: name || null,
@@ -3589,17 +3723,14 @@ var signupHandler = async (c) => {
3589
3723
  schema.users
3590
3724
  );
3591
3725
  }
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
- }
3726
+ if (invited) {
3727
+ await acceptInvitation(invitations, {
3728
+ token: invited,
3729
+ user: { id: newUser.id, email: newUser.email, name: newUser.name },
3730
+ db: database,
3731
+ env: authEnv,
3732
+ context: "signup_invitation_onAccepted_failed"
3733
+ });
3603
3734
  }
3604
3735
  const fingerprint = await generateFingerprint(c.req.raw);
3605
3736
  const ipAddress = getClientIp(c.req.raw);
@@ -3632,24 +3763,34 @@ var signupMiddleware = [
3632
3763
 
3633
3764
  // src/routes/login.ts
3634
3765
  var import_zod_openapi3 = require("@hono/zod-openapi");
3635
- var import_drizzle_orm11 = require("drizzle-orm");
3766
+ var import_drizzle_orm12 = require("drizzle-orm");
3636
3767
  var import_cookie2 = require("hono/cookie");
3637
3768
 
3638
3769
  // src/routes/2fa/helpers.ts
3639
- var import_drizzle_orm10 = require("drizzle-orm");
3770
+ var import_drizzle_orm11 = require("drizzle-orm");
3640
3771
  async function getUserEnabled2faMethods(database, userId, user2faMethodsTable) {
3641
3772
  return database.select({
3642
3773
  method: user2faMethodsTable.method,
3643
3774
  isPrimary: user2faMethodsTable.isPrimary,
3644
3775
  totpSecret: user2faMethodsTable.totpSecret,
3645
3776
  lastTotpCounter: user2faMethodsTable.lastTotpCounter
3646
- }).from(user2faMethodsTable).where((0, import_drizzle_orm10.eq)(user2faMethodsTable.userId, userId));
3777
+ }).from(user2faMethodsTable).where((0, import_drizzle_orm11.eq)(user2faMethodsTable.userId, userId));
3647
3778
  }
3648
3779
  function generateEmailOtp() {
3649
3780
  const bytes = crypto.getRandomValues(new Uint8Array(4));
3650
3781
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3651
3782
  return String(num % 1e6).padStart(6, "0");
3652
3783
  }
3784
+ var EMAIL_CODE_TTL_SECONDS = 300;
3785
+ async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3786
+ const code = generateEmailOtp();
3787
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
3788
+ expirationTtl: EMAIL_CODE_TTL_SECONDS
3789
+ });
3790
+ const emailService = new EmailService(env, void 0, emailBranding);
3791
+ const firstName = user.name?.split(" ")[0] || null;
3792
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, database, usersTable);
3793
+ }
3653
3794
  async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3654
3795
  const { user2faMethods: user2faMethodsTable, userBackupCodes: userBackupCodesTable } = tables2;
3655
3796
  if (method === "totp") {
@@ -3667,7 +3808,7 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3667
3808
  const secret = await decryptTotpSecret(totpMethod.totpSecret, env.TOTP_ENCRYPTION_KEY);
3668
3809
  const result = verifyTotpCode(secret, code, totpMethod.lastTotpCounter);
3669
3810
  if (result.valid) {
3670
- 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")));
3811
+ 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")));
3671
3812
  return { valid: true };
3672
3813
  }
3673
3814
  return { valid: false };
@@ -3679,18 +3820,18 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3679
3820
  const kvKey = `email_2fa_challenge:${userId}`;
3680
3821
  const storedHash = await env.OAUTH_STATES.get(kvKey);
3681
3822
  if (!storedHash) return { valid: false };
3682
- const codeHash = await hashToken(code);
3683
- if (codeHash === storedHash) {
3823
+ const codeHash2 = await hashToken(code);
3824
+ if (codeHash2 === storedHash) {
3684
3825
  await env.OAUTH_STATES.delete(kvKey);
3685
3826
  return { valid: true };
3686
3827
  }
3687
3828
  return { valid: false };
3688
3829
  }
3689
3830
  if (method === "backup") {
3690
- 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)));
3831
+ 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)));
3691
3832
  for (const bc of backupCodes) {
3692
3833
  if (await verifyBackupCode(code, bc.codeHash)) {
3693
- await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm10.eq)(userBackupCodesTable.id, bc.id));
3834
+ await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm11.eq)(userBackupCodesTable.id, bc.id));
3694
3835
  logSecurityEvent("2fa_backup_code_used", "medium", { userId, codeId: bc.id });
3695
3836
  return { valid: true };
3696
3837
  }
@@ -3734,7 +3875,7 @@ var loginRoute = (0, import_zod_openapi3.createRoute)({
3734
3875
  });
3735
3876
  var loginHandler = async (c) => {
3736
3877
  const { email, password, turnstileToken } = c.req.valid("json");
3737
- const { db: database, schema } = getAuthContext(c);
3878
+ const { db: database, schema, emailBranding } = getAuthContext(c);
3738
3879
  const env = c.env;
3739
3880
  const turnstileValid = await verifyTurnstileToken(
3740
3881
  turnstileToken,
@@ -3746,7 +3887,7 @@ var loginHandler = async (c) => {
3746
3887
  return problems.badRequest(c, "Invalid captcha");
3747
3888
  }
3748
3889
  const user = await database.query.users.findFirst({
3749
- where: (0, import_drizzle_orm11.eq)(schema.users.email, email.toLowerCase())
3890
+ where: (0, import_drizzle_orm12.eq)(schema.users.email, email.toLowerCase())
3750
3891
  });
3751
3892
  if (!user) {
3752
3893
  return problems.unauthorized(c, "Invalid email or password");
@@ -3819,6 +3960,10 @@ var loginHandler = async (c) => {
3819
3960
  };
3820
3961
  await storeChallengeToken(env.OAUTH_STATES, challengeToken, challengePayload);
3821
3962
  setChallengeCookie(c, env, challengeToken);
3963
+ const emailMethod = enabled2faMethods.find((m) => m.method === "email");
3964
+ if (emailMethod && (emailMethod.isPrimary || enabled2faMethods.length === 1)) {
3965
+ await sendEmailChallengeCode(env, database, schema.users, user, emailBranding);
3966
+ }
3822
3967
  return c.json({
3823
3968
  requires2fa: true,
3824
3969
  methods: enabled2faMethods.map((m) => m.method)
@@ -3851,7 +3996,7 @@ var loginMiddleware = [
3851
3996
 
3852
3997
  // src/routes/logout.ts
3853
3998
  var import_zod_openapi4 = require("@hono/zod-openapi");
3854
- var import_drizzle_orm12 = require("drizzle-orm");
3999
+ var import_drizzle_orm13 = require("drizzle-orm");
3855
4000
  var logoutRoute = (0, import_zod_openapi4.createRoute)({
3856
4001
  method: "post",
3857
4002
  path: "/logout",
@@ -3874,7 +4019,7 @@ var logoutHandler = async (c) => {
3874
4019
  const sessionIdMatch = cookieHeader.match(cookiePattern);
3875
4020
  if (sessionIdMatch) {
3876
4021
  const sessionId = sessionIdMatch[1];
3877
- await database.delete(schema.sessions).where((0, import_drizzle_orm12.eq)(schema.sessions.id, sessionId));
4022
+ await database.delete(schema.sessions).where((0, import_drizzle_orm13.eq)(schema.sessions.id, sessionId));
3878
4023
  }
3879
4024
  }
3880
4025
  c.header("Set-Cookie", clearSessionCookie(c.env), { append: true });
@@ -3883,7 +4028,7 @@ var logoutHandler = async (c) => {
3883
4028
 
3884
4029
  // src/routes/me.ts
3885
4030
  var import_zod_openapi5 = require("@hono/zod-openapi");
3886
- var import_drizzle_orm13 = require("drizzle-orm");
4031
+ var import_drizzle_orm14 = require("drizzle-orm");
3887
4032
  var meRoute = (0, import_zod_openapi5.createRoute)({
3888
4033
  method: "get",
3889
4034
  path: "/me",
@@ -3911,7 +4056,7 @@ var meHandler = async (c) => {
3911
4056
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3912
4057
  const { db: database, schema } = getAuthContext(c);
3913
4058
  const user = await database.query.users.findFirst({
3914
- where: (0, import_drizzle_orm13.eq)(schema.users.id, userId)
4059
+ where: (0, import_drizzle_orm14.eq)(schema.users.id, userId)
3915
4060
  });
3916
4061
  if (!user) {
3917
4062
  return problems.notFound(c, "User not found");
@@ -3937,7 +4082,7 @@ var meMiddleware = [requireAuth];
3937
4082
 
3938
4083
  // src/routes/verify-email.ts
3939
4084
  var import_zod_openapi6 = require("@hono/zod-openapi");
3940
- var import_drizzle_orm14 = require("drizzle-orm");
4085
+ var import_drizzle_orm15 = require("drizzle-orm");
3941
4086
  var verifyEmailRoute = (0, import_zod_openapi6.createRoute)({
3942
4087
  method: "post",
3943
4088
  path: "/verify-email",
@@ -3966,17 +4111,17 @@ var verifyEmailHandler = async (c) => {
3966
4111
  const { db: database, schema } = getAuthContext(c);
3967
4112
  const tokenHash = await hashToken(token);
3968
4113
  const verificationToken = await database.query.emailVerificationTokens.findFirst({
3969
- where: (0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
4114
+ where: (0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
3970
4115
  });
3971
4116
  if (!verificationToken) {
3972
4117
  return problems.badRequest(c, "Invalid or expired verification token");
3973
4118
  }
3974
4119
  if (verificationToken.expiresAt < Date.now()) {
3975
- await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
4120
+ await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
3976
4121
  return problems.badRequest(c, "Verification token has expired");
3977
4122
  }
3978
- await database.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm14.eq)(schema.users.id, verificationToken.userId));
3979
- await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
4123
+ await database.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm15.eq)(schema.users.id, verificationToken.userId));
4124
+ await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
3980
4125
  logSecurityEvent("email_verified", "low", { userId: verificationToken.userId });
3981
4126
  return c.json({ success: true, redirect: "/" });
3982
4127
  };
@@ -3985,18 +4130,18 @@ var verifyEmailHandler = async (c) => {
3985
4130
  var import_zod_openapi7 = require("@hono/zod-openapi");
3986
4131
 
3987
4132
  // src/core/password-reset.ts
3988
- var import_drizzle_orm15 = require("drizzle-orm");
4133
+ var import_drizzle_orm16 = require("drizzle-orm");
3989
4134
  var TOKEN_EXPIRATION_MS = AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES * 60 * 1e3;
3990
4135
  async function createPasswordResetToken(db, tables2, email) {
3991
4136
  const { users, passwordResetTokens } = tables2;
3992
- 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);
4137
+ 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);
3993
4138
  if (!user) {
3994
4139
  return null;
3995
4140
  }
3996
4141
  if (!user.emailVerified) {
3997
4142
  return null;
3998
4143
  }
3999
- await db.delete(passwordResetTokens).where((0, import_drizzle_orm15.eq)(passwordResetTokens.userId, user.id));
4144
+ await db.delete(passwordResetTokens).where((0, import_drizzle_orm16.eq)(passwordResetTokens.userId, user.id));
4000
4145
  const token = generateSecureToken(64);
4001
4146
  const tokenHash = await hashToken(token);
4002
4147
  const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
@@ -4018,10 +4163,10 @@ async function validateResetToken(db, tables2, token) {
4018
4163
  used: passwordResetTokens.used,
4019
4164
  expiresAt: passwordResetTokens.expiresAt
4020
4165
  }).from(passwordResetTokens).where(
4021
- (0, import_drizzle_orm15.and)(
4022
- (0, import_drizzle_orm15.eq)(passwordResetTokens.tokenHash, tokenHash),
4023
- (0, import_drizzle_orm15.eq)(passwordResetTokens.used, false),
4024
- (0, import_drizzle_orm15.gt)(passwordResetTokens.expiresAt, Date.now())
4166
+ (0, import_drizzle_orm16.and)(
4167
+ (0, import_drizzle_orm16.eq)(passwordResetTokens.tokenHash, tokenHash),
4168
+ (0, import_drizzle_orm16.eq)(passwordResetTokens.used, false),
4169
+ (0, import_drizzle_orm16.gt)(passwordResetTokens.expiresAt, Date.now())
4025
4170
  )
4026
4171
  ).limit(1);
4027
4172
  if (!resetToken) {
@@ -4042,11 +4187,11 @@ async function resetPassword(db, tables2, token, newPassword, pepper, pepperKid
4042
4187
  await db.update(users).set({
4043
4188
  hashedPassword,
4044
4189
  pepperKid,
4045
- sessionVersion: import_drizzle_orm15.sql`${users.sessionVersion} + 1`
4046
- }).where((0, import_drizzle_orm15.eq)(users.id, userId));
4190
+ sessionVersion: import_drizzle_orm16.sql`${users.sessionVersion} + 1`
4191
+ }).where((0, import_drizzle_orm16.eq)(users.id, userId));
4047
4192
  const tokenHash = await hashToken(token);
4048
- await db.update(passwordResetTokens).set({ used: true }).where((0, import_drizzle_orm15.eq)(passwordResetTokens.tokenHash, tokenHash));
4049
- await db.delete(sessions).where((0, import_drizzle_orm15.eq)(sessions.userId, userId));
4193
+ await db.update(passwordResetTokens).set({ used: true }).where((0, import_drizzle_orm16.eq)(passwordResetTokens.tokenHash, tokenHash));
4194
+ await db.delete(sessions).where((0, import_drizzle_orm16.eq)(sessions.userId, userId));
4050
4195
  logger_default.info("Password reset successful", { userId });
4051
4196
  return { success: true };
4052
4197
  }
@@ -4194,14 +4339,14 @@ var resetPasswordMiddleware = [
4194
4339
  var import_zod_openapi9 = require("@hono/zod-openapi");
4195
4340
 
4196
4341
  // src/core/change-password.ts
4197
- var import_drizzle_orm16 = require("drizzle-orm");
4342
+ var import_drizzle_orm17 = require("drizzle-orm");
4198
4343
  async function changePassword(params) {
4199
4344
  const { userId, currentPassword, newPassword, currentSessionId, pepper, db, tables: tables2 } = params;
4200
4345
  const { users, sessions } = tables2;
4201
4346
  const [user] = await db.select({
4202
4347
  id: users.id,
4203
4348
  hashedPassword: users.hashedPassword
4204
- }).from(users).where((0, import_drizzle_orm16.eq)(users.id, userId)).limit(1);
4349
+ }).from(users).where((0, import_drizzle_orm17.eq)(users.id, userId)).limit(1);
4205
4350
  if (!user || !user.hashedPassword) {
4206
4351
  return { success: false, error: "User not found" };
4207
4352
  }
@@ -4217,8 +4362,8 @@ async function changePassword(params) {
4217
4362
  await db.update(users).set({
4218
4363
  hashedPassword: newHashedPassword,
4219
4364
  updatedAt: /* @__PURE__ */ new Date()
4220
- }).where((0, import_drizzle_orm16.eq)(users.id, userId));
4221
- 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)));
4365
+ }).where((0, import_drizzle_orm17.eq)(users.id, userId));
4366
+ 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)));
4222
4367
  logSecurityEvent("password_changed", "low", {
4223
4368
  userId
4224
4369
  });
@@ -4491,7 +4636,7 @@ var cancelEmailChangeHandler = async (c) => {
4491
4636
  // src/routes/delete-account.ts
4492
4637
  var import_zod_openapi17 = require("@hono/zod-openapi");
4493
4638
  var import_zod_openapi18 = require("@hono/zod-openapi");
4494
- var import_drizzle_orm17 = require("drizzle-orm");
4639
+ var import_drizzle_orm18 = require("drizzle-orm");
4495
4640
  var deleteAccountRequestSchema = import_zod_openapi18.z.object({
4496
4641
  password: import_zod_openapi18.z.string().min(1).openapi({ example: "CurrentPass123!" })
4497
4642
  }).openapi("DeleteAccountRequest");
@@ -4539,7 +4684,7 @@ var deleteAccountHandler = async (c) => {
4539
4684
  const [user] = await database.select({
4540
4685
  id: schema.users.id,
4541
4686
  hashedPassword: schema.users.hashedPassword
4542
- }).from(schema.users).where((0, import_drizzle_orm17.eq)(schema.users.id, userId)).limit(1);
4687
+ }).from(schema.users).where((0, import_drizzle_orm18.eq)(schema.users.id, userId)).limit(1);
4543
4688
  if (!user || !user.hashedPassword) {
4544
4689
  return problems.unauthorized(c, "Invalid credentials");
4545
4690
  }
@@ -4554,8 +4699,8 @@ var deleteAccountHandler = async (c) => {
4554
4699
  }
4555
4700
  const now = /* @__PURE__ */ new Date();
4556
4701
  const purgeAt = new Date(now.getTime() + PURGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3);
4557
- await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where((0, import_drizzle_orm17.eq)(schema.users.id, userId));
4558
- await database.delete(schema.sessions).where((0, import_drizzle_orm17.eq)(schema.sessions.userId, userId));
4702
+ await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where((0, import_drizzle_orm18.eq)(schema.users.id, userId));
4703
+ await database.delete(schema.sessions).where((0, import_drizzle_orm18.eq)(schema.sessions.userId, userId));
4559
4704
  logSecurityEvent("account_deletion_requested", "high", { userId });
4560
4705
  return c.json(
4561
4706
  {
@@ -4576,7 +4721,7 @@ var deleteAccountMiddleware = [requireAuth];
4576
4721
  // src/routes/refresh.ts
4577
4722
  var import_zod_openapi19 = require("@hono/zod-openapi");
4578
4723
  var import_zod_openapi20 = require("@hono/zod-openapi");
4579
- var import_drizzle_orm18 = require("drizzle-orm");
4724
+ var import_drizzle_orm19 = require("drizzle-orm");
4580
4725
  var refreshRequestSchema = import_zod_openapi20.z.object({
4581
4726
  refreshToken: import_zod_openapi20.z.string().min(1).openapi({ example: "abc123..." })
4582
4727
  }).openapi("RefreshRequest");
@@ -4616,7 +4761,7 @@ var refreshHandler = async (c) => {
4616
4761
  expiresAt: schema.sessions.expiresAt,
4617
4762
  fingerprint: schema.sessions.fingerprint,
4618
4763
  ipAddress: schema.sessions.ipAddress
4619
- }).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);
4764
+ }).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);
4620
4765
  if (!session) {
4621
4766
  return problems.unauthorized(c, "Invalid or expired refresh token");
4622
4767
  }
@@ -4639,7 +4784,7 @@ var refreshMiddleware = [];
4639
4784
  // src/routes/resend-verification.ts
4640
4785
  var import_zod_openapi21 = require("@hono/zod-openapi");
4641
4786
  var import_zod_openapi22 = require("@hono/zod-openapi");
4642
- var import_drizzle_orm19 = require("drizzle-orm");
4787
+ var import_drizzle_orm20 = require("drizzle-orm");
4643
4788
  var resendVerificationResponseSchema = import_zod_openapi22.z.object({
4644
4789
  success: import_zod_openapi22.z.boolean().openapi({ example: true }),
4645
4790
  message: import_zod_openapi22.z.string().openapi({ example: "Verification email sent" })
@@ -4684,14 +4829,14 @@ var resendVerificationHandler = async (c) => {
4684
4829
  email: schema.users.email,
4685
4830
  name: schema.users.name,
4686
4831
  emailVerified: schema.users.emailVerified
4687
- }).from(schema.users).where((0, import_drizzle_orm19.eq)(schema.users.id, userId)).limit(1);
4832
+ }).from(schema.users).where((0, import_drizzle_orm20.eq)(schema.users.id, userId)).limit(1);
4688
4833
  if (!user) {
4689
4834
  return problems.unauthorized(c, "User not found");
4690
4835
  }
4691
4836
  if (user.emailVerified) {
4692
4837
  return problems.badRequest(c, "Email is already verified");
4693
4838
  }
4694
- await db.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm19.eq)(schema.emailVerificationTokens.userId, userId));
4839
+ await db.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm20.eq)(schema.emailVerificationTokens.userId, userId));
4695
4840
  const emailToken = generateSecureToken(32);
4696
4841
  const tokenHash = await hashToken(emailToken);
4697
4842
  const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
@@ -4736,7 +4881,7 @@ var import_zod_openapi36 = require("@hono/zod-openapi");
4736
4881
 
4737
4882
  // src/routes/2fa/status.ts
4738
4883
  var import_zod_openapi24 = require("@hono/zod-openapi");
4739
- var import_drizzle_orm20 = require("drizzle-orm");
4884
+ var import_drizzle_orm21 = require("drizzle-orm");
4740
4885
 
4741
4886
  // src/routes/2fa/schemas.ts
4742
4887
  var import_zod_openapi23 = require("@hono/zod-openapi");
@@ -4897,7 +5042,7 @@ var statusHandler = async (c) => {
4897
5042
  if (!userId) return problems.unauthorized(c);
4898
5043
  const { db, schema } = getAuthContext(c);
4899
5044
  const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4900
- 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)));
5045
+ 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)));
4901
5046
  c.header("Cache-Control", "private, max-age=60");
4902
5047
  c.header("Vary", "Cookie");
4903
5048
  return c.json({
@@ -4909,7 +5054,7 @@ var statusHandler = async (c) => {
4909
5054
 
4910
5055
  // src/routes/2fa/totp-setup.ts
4911
5056
  var import_zod_openapi25 = require("@hono/zod-openapi");
4912
- var import_drizzle_orm21 = require("drizzle-orm");
5057
+ var import_drizzle_orm22 = require("drizzle-orm");
4913
5058
  var totpSetupRoute = (0, import_zod_openapi25.createRoute)({
4914
5059
  method: "post",
4915
5060
  path: "/totp/setup",
@@ -4939,11 +5084,11 @@ var totpSetupHandler = async (c) => {
4939
5084
  const userId = c.get("userId");
4940
5085
  if (!userId) return problems.unauthorized(c);
4941
5086
  const { db, schema, env } = getAuthContext(c);
4942
- 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);
5087
+ 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);
4943
5088
  if (existing.length > 0) {
4944
5089
  return problems.badRequest(c, "TOTP already enabled");
4945
5090
  }
4946
- const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm21.eq)(schema.users.id, userId)).limit(1);
5091
+ const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm22.eq)(schema.users.id, userId)).limit(1);
4947
5092
  if (!user) return problems.notFound(c, "User not found");
4948
5093
  const secret = generateTotpSecret();
4949
5094
  const qrCodeUri = generateQrCodeUri(secret, user.email);
@@ -4954,7 +5099,7 @@ var totpSetupHandler = async (c) => {
4954
5099
 
4955
5100
  // src/routes/2fa/totp-verify.ts
4956
5101
  var import_zod_openapi26 = require("@hono/zod-openapi");
4957
- var import_drizzle_orm22 = require("drizzle-orm");
5102
+ var import_drizzle_orm23 = require("drizzle-orm");
4958
5103
  var totpVerifyRoute = (0, import_zod_openapi26.createRoute)({
4959
5104
  method: "post",
4960
5105
  path: "/totp/verify",
@@ -5000,7 +5145,7 @@ var totpVerifyHandler = async (c) => {
5000
5145
  logSecurityEvent("2fa_totp_setup_failed", "medium", { userId });
5001
5146
  return problems.badRequest(c, "Invalid code");
5002
5147
  }
5003
- 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);
5148
+ 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);
5004
5149
  const encryptionKey = env.TOTP_ENCRYPTION_KEY;
5005
5150
  if (!encryptionKey) {
5006
5151
  logger_default.error("TOTP_ENCRYPTION_KEY not configured");
@@ -5048,7 +5193,7 @@ var totpVerifyHandler = async (c) => {
5048
5193
 
5049
5194
  // src/routes/2fa/totp-disable.ts
5050
5195
  var import_zod_openapi27 = require("@hono/zod-openapi");
5051
- var import_drizzle_orm23 = require("drizzle-orm");
5196
+ var import_drizzle_orm24 = require("drizzle-orm");
5052
5197
  var totpDisableRoute = (0, import_zod_openapi27.createRoute)({
5053
5198
  method: "post",
5054
5199
  path: "/totp/disable",
@@ -5091,13 +5236,13 @@ var totpDisableHandler = async (c) => {
5091
5236
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "totp" });
5092
5237
  return problems.badRequest(c, "Invalid verification code");
5093
5238
  }
5094
- 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")));
5239
+ 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")));
5095
5240
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5096
5241
  if (remaining.length === 0) {
5097
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm23.eq)(schema.userBackupCodes.userId, userId));
5098
- await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm23.eq)(schema.userTrustedDevices.userId, userId));
5242
+ await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm24.eq)(schema.userBackupCodes.userId, userId));
5243
+ await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm24.eq)(schema.userTrustedDevices.userId, userId));
5099
5244
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5100
- 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);
5245
+ 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);
5101
5246
  if (user) {
5102
5247
  const emailService = new EmailService(env, void 0, emailBranding);
5103
5248
  const firstName = user.name?.split(" ")[0] || null;
@@ -5116,7 +5261,7 @@ var totpDisableHandler = async (c) => {
5116
5261
 
5117
5262
  // src/routes/2fa/email-setup.ts
5118
5263
  var import_zod_openapi28 = require("@hono/zod-openapi");
5119
- var import_drizzle_orm24 = require("drizzle-orm");
5264
+ var import_drizzle_orm25 = require("drizzle-orm");
5120
5265
  var emailSetupRoute = (0, import_zod_openapi28.createRoute)({
5121
5266
  method: "post",
5122
5267
  path: "/email/setup",
@@ -5146,15 +5291,15 @@ var emailSetupHandler = async (c) => {
5146
5291
  const userId = c.get("userId");
5147
5292
  if (!userId) return problems.unauthorized(c);
5148
5293
  const { db, schema, env, emailBranding } = getAuthContext(c);
5149
- 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);
5294
+ 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);
5150
5295
  if (existing.length > 0) {
5151
5296
  return problems.badRequest(c, "Email 2FA already enabled");
5152
5297
  }
5153
- 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);
5298
+ 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);
5154
5299
  if (!user) return problems.notFound(c, "User not found");
5155
5300
  const code = generateEmailOtp();
5156
- const codeHash = await hashToken(code);
5157
- await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
5301
+ const codeHash2 = await hashToken(code);
5302
+ await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash2, { expirationTtl: 300 });
5158
5303
  const emailService = new EmailService(env, void 0, emailBranding);
5159
5304
  const firstName = user.name?.split(" ")[0] || null;
5160
5305
  await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
@@ -5164,7 +5309,7 @@ var emailSetupHandler = async (c) => {
5164
5309
 
5165
5310
  // src/routes/2fa/email-verify.ts
5166
5311
  var import_zod_openapi29 = require("@hono/zod-openapi");
5167
- var import_drizzle_orm25 = require("drizzle-orm");
5312
+ var import_drizzle_orm26 = require("drizzle-orm");
5168
5313
  var emailVerifyRoute = (0, import_zod_openapi29.createRoute)({
5169
5314
  method: "post",
5170
5315
  path: "/email/verify",
@@ -5201,12 +5346,12 @@ var emailVerifyHandler = async (c) => {
5201
5346
  if (!storedHash) {
5202
5347
  return problems.badRequest(c, "Code expired. Please request a new one.");
5203
5348
  }
5204
- const codeHash = await hashToken(code);
5205
- if (codeHash !== storedHash) {
5349
+ const codeHash2 = await hashToken(code);
5350
+ if (codeHash2 !== storedHash) {
5206
5351
  logSecurityEvent("2fa_email_setup_failed", "medium", { userId });
5207
5352
  return problems.badRequest(c, "Invalid code");
5208
5353
  }
5209
- 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);
5354
+ 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);
5210
5355
  const now = Date.now();
5211
5356
  const existingMethods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5212
5357
  const isFirstMethod = existingMethods.length === 0;
@@ -5248,7 +5393,7 @@ var emailVerifyHandler = async (c) => {
5248
5393
 
5249
5394
  // src/routes/2fa/email-send-code.ts
5250
5395
  var import_zod_openapi30 = require("@hono/zod-openapi");
5251
- var import_drizzle_orm26 = require("drizzle-orm");
5396
+ var import_drizzle_orm27 = require("drizzle-orm");
5252
5397
  var emailSendCodeRoute = (0, import_zod_openapi30.createRoute)({
5253
5398
  method: "post",
5254
5399
  path: "/email/send-code",
@@ -5278,15 +5423,15 @@ var emailSendCodeHandler = async (c) => {
5278
5423
  const userId = c.get("userId");
5279
5424
  if (!userId) return problems.unauthorized(c);
5280
5425
  const { db, schema, env, emailBranding } = getAuthContext(c);
5281
- 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);
5426
+ 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);
5282
5427
  if (!emailMethod) {
5283
5428
  return problems.badRequest(c, "Email 2FA not enabled");
5284
5429
  }
5285
- 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);
5430
+ 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);
5286
5431
  if (!user) return problems.notFound(c, "User not found");
5287
5432
  const code = generateEmailOtp();
5288
- const codeHash = await hashToken(code);
5289
- await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash, {
5433
+ const codeHash2 = await hashToken(code);
5434
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash2, {
5290
5435
  expirationTtl: 300
5291
5436
  });
5292
5437
  const emailService = new EmailService(env, void 0, emailBranding);
@@ -5308,7 +5453,7 @@ var emailSendCodeHandler = async (c) => {
5308
5453
 
5309
5454
  // src/routes/2fa/email-disable.ts
5310
5455
  var import_zod_openapi31 = require("@hono/zod-openapi");
5311
- var import_drizzle_orm27 = require("drizzle-orm");
5456
+ var import_drizzle_orm28 = require("drizzle-orm");
5312
5457
  var emailDisableRoute = (0, import_zod_openapi31.createRoute)({
5313
5458
  method: "post",
5314
5459
  path: "/email/disable",
@@ -5351,13 +5496,13 @@ var emailDisableHandler = async (c) => {
5351
5496
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "email" });
5352
5497
  return problems.badRequest(c, "Invalid verification code");
5353
5498
  }
5354
- 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")));
5499
+ 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")));
5355
5500
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5356
5501
  if (remaining.length === 0) {
5357
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm27.eq)(schema.userBackupCodes.userId, userId));
5358
- await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm27.eq)(schema.userTrustedDevices.userId, userId));
5502
+ await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm28.eq)(schema.userBackupCodes.userId, userId));
5503
+ await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId));
5359
5504
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5360
- 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);
5505
+ 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);
5361
5506
  if (user) {
5362
5507
  const emailService = new EmailService(env, void 0, emailBranding);
5363
5508
  const firstName = user.name?.split(" ")[0] || null;
@@ -5376,7 +5521,7 @@ var emailDisableHandler = async (c) => {
5376
5521
 
5377
5522
  // src/routes/2fa/trusted-devices.ts
5378
5523
  var import_zod_openapi32 = require("@hono/zod-openapi");
5379
- var import_drizzle_orm28 = require("drizzle-orm");
5524
+ var import_drizzle_orm29 = require("drizzle-orm");
5380
5525
  var trustedDevicesGetRoute = (0, import_zod_openapi32.createRoute)({
5381
5526
  method: "get",
5382
5527
  path: "/trusted-devices",
@@ -5415,7 +5560,7 @@ var trustedDevicesGetHandler = async (c) => {
5415
5560
  ipAddress: schema.userTrustedDevices.ipAddress,
5416
5561
  lastUsedAt: schema.userTrustedDevices.lastUsedAt,
5417
5562
  createdAt: schema.userTrustedDevices.createdAt
5418
- }).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)));
5563
+ }).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)));
5419
5564
  c.header("Cache-Control", "private, max-age=60");
5420
5565
  c.header("Vary", "Cookie");
5421
5566
  return c.json({ devices });
@@ -5451,7 +5596,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5451
5596
  if (!userId) return problems.unauthorized(c);
5452
5597
  const deviceId = c.req.param("id");
5453
5598
  const { db, schema } = getAuthContext(c);
5454
- 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 });
5599
+ 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 });
5455
5600
  if (result.length === 0) {
5456
5601
  return problems.notFound(c, "Device not found");
5457
5602
  }
@@ -5461,7 +5606,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5461
5606
 
5462
5607
  // src/routes/2fa/backup-codes.ts
5463
5608
  var import_zod_openapi33 = require("@hono/zod-openapi");
5464
- var import_drizzle_orm29 = require("drizzle-orm");
5609
+ var import_drizzle_orm30 = require("drizzle-orm");
5465
5610
  var backupCodesRegenerateRoute = (0, import_zod_openapi33.createRoute)({
5466
5611
  method: "post",
5467
5612
  path: "/backup-codes/regenerate",
@@ -5502,7 +5647,7 @@ var backupCodesRegenerateHandler = async (c) => {
5502
5647
  logSecurityEvent("2fa_backup_regenerate_failed", "medium", { userId });
5503
5648
  return problems.badRequest(c, "Invalid verification code");
5504
5649
  }
5505
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm29.eq)(schema.userBackupCodes.userId, userId));
5650
+ await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm30.eq)(schema.userBackupCodes.userId, userId));
5506
5651
  const codes = generateBackupCodes();
5507
5652
  const now = Date.now();
5508
5653
  const hashedCodes = await Promise.all(
@@ -5519,7 +5664,7 @@ var backupCodesRegenerateHandler = async (c) => {
5519
5664
 
5520
5665
  // src/routes/2fa/challenge.ts
5521
5666
  var import_zod_openapi34 = require("@hono/zod-openapi");
5522
- var import_drizzle_orm30 = require("drizzle-orm");
5667
+ var import_drizzle_orm31 = require("drizzle-orm");
5523
5668
  var import_cookie3 = require("hono/cookie");
5524
5669
  var challengeRoute = (0, import_zod_openapi34.createRoute)({
5525
5670
  method: "post",
@@ -5591,7 +5736,7 @@ var challengeHandler = async (c) => {
5591
5736
  }
5592
5737
  await deleteChallengeToken(c.env.OAUTH_STATES, challengeToken);
5593
5738
  clearChallengeCookie(c, c.env);
5594
- const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm30.eq)(schema.users.id, payload.userId)).limit(1);
5739
+ const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm31.eq)(schema.users.id, payload.userId)).limit(1);
5595
5740
  if (!user) {
5596
5741
  logger_default.error("User not found during 2FA challenge", { userId: payload.userId });
5597
5742
  return problems.notFound(c, "User not found");
@@ -5632,7 +5777,7 @@ var challengeHandler = async (c) => {
5632
5777
 
5633
5778
  // src/routes/2fa/challenge-resend.ts
5634
5779
  var import_zod_openapi35 = require("@hono/zod-openapi");
5635
- var import_drizzle_orm31 = require("drizzle-orm");
5780
+ var import_drizzle_orm32 = require("drizzle-orm");
5636
5781
  var import_cookie4 = require("hono/cookie");
5637
5782
  var challengeResendRoute = (0, import_zod_openapi35.createRoute)({
5638
5783
  method: "post",
@@ -5670,19 +5815,12 @@ var challengeResendHandler = async (c) => {
5670
5815
  if (!payload.methods.includes("email")) {
5671
5816
  return problems.badRequest(c, "Email verification not available");
5672
5817
  }
5673
- 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);
5818
+ 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);
5674
5819
  if (!user) {
5675
5820
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5676
5821
  return problems.notFound(c, "User not found");
5677
5822
  }
5678
- const code = generateEmailOtp();
5679
- const codeHash = await hashToken(code);
5680
- await c.env.OAUTH_STATES.put(`email_2fa_challenge:${payload.userId}`, codeHash, {
5681
- expirationTtl: 300
5682
- });
5683
- const emailService = new EmailService(c.env, void 0, emailBranding);
5684
- const firstName = user.name?.split(" ")[0] || null;
5685
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
5823
+ await sendEmailChallengeCode(c.env, db, schema.users, { id: payload.userId, ...user }, emailBranding);
5686
5824
  logger_default.info("Email 2FA code resent", { userId: payload.userId });
5687
5825
  return c.json({ success: true });
5688
5826
  };
@@ -5908,7 +6046,7 @@ var authorizeHandler = async (c) => {
5908
6046
 
5909
6047
  // src/routes/oauth/callback.ts
5910
6048
  var import_zod_openapi39 = require("@hono/zod-openapi");
5911
- var import_drizzle_orm32 = require("drizzle-orm");
6049
+ var import_drizzle_orm33 = require("drizzle-orm");
5912
6050
  var callbackRoute = (0, import_zod_openapi39.createRoute)({
5913
6051
  method: "get",
5914
6052
  path: "/{provider}/callback",
@@ -6034,10 +6172,24 @@ var callbackHandler = async (c) => {
6034
6172
  logger_default.warn("No user ID returned from OAuth provider", { provider });
6035
6173
  return c.redirect(`${appUrl}/login?error=oauth_failed`);
6036
6174
  }
6037
- const { db, schema } = getAuthContext(c);
6038
- 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);
6175
+ const { db, schema, env, invitations, signup } = getAuthContext(c);
6176
+ 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);
6039
6177
  let authUser = existingUser;
6040
6178
  if (!authUser) {
6179
+ const invited = await verifyInvitation(invitations, {
6180
+ token: invitationToken,
6181
+ email,
6182
+ db,
6183
+ env
6184
+ });
6185
+ if (signup === "invitation" && !invited) {
6186
+ logSecurityEvent("oauth_signup_refused_no_invitation", "medium", {
6187
+ email,
6188
+ provider,
6189
+ hadInvitation: !!invitationToken
6190
+ });
6191
+ return c.redirect(`${appUrl}/login?error=invitation_required`);
6192
+ }
6041
6193
  const [createdUser] = await db.insert(schema.users).values({
6042
6194
  email,
6043
6195
  name: name || null,
@@ -6048,6 +6200,15 @@ var callbackHandler = async (c) => {
6048
6200
  activatedAt: /* @__PURE__ */ new Date()
6049
6201
  }).returning({ id: schema.users.id, email: schema.users.email });
6050
6202
  authUser = createdUser;
6203
+ if (invited) {
6204
+ await acceptInvitation(invitations, {
6205
+ token: invited,
6206
+ user: { id: createdUser.id, email: createdUser.email, name: name || null },
6207
+ db,
6208
+ env,
6209
+ context: "oauth_invitation_onAccepted_failed"
6210
+ });
6211
+ }
6051
6212
  logSecurityEvent("user_signup_oauth", "low", {
6052
6213
  userId: createdUser.id,
6053
6214
  email: createdUser.email,
@@ -6087,7 +6248,7 @@ var callbackHandler = async (c) => {
6087
6248
 
6088
6249
  // src/routes/oauth/complete.ts
6089
6250
  var import_zod_openapi40 = require("@hono/zod-openapi");
6090
- var import_drizzle_orm33 = require("drizzle-orm");
6251
+ var import_drizzle_orm34 = require("drizzle-orm");
6091
6252
  var completeRoute = (0, import_zod_openapi40.createRoute)({
6092
6253
  method: "post",
6093
6254
  path: "/complete",
@@ -6135,7 +6296,7 @@ var completeHandler = async (c) => {
6135
6296
  redirect: storedRedirect
6136
6297
  } = JSON.parse(stored);
6137
6298
  const { db, schema } = getAuthContext(c);
6138
- await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm33.eq)(schema.users.id, userId));
6299
+ await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm34.eq)(schema.users.id, userId));
6139
6300
  await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
6140
6301
  const fingerprint = await generateFingerprint(c.req.raw);
6141
6302
  const ipAddress = getClientIp(c.req.raw);
@@ -6180,19 +6341,12 @@ var routes_default = authRoutes;
6180
6341
 
6181
6342
  // src/lib/email/webhook-verifier.ts
6182
6343
  var import_svix = require("svix");
6183
- function verifyWebhookSignature(payload, signature, secret) {
6344
+ function verifyWebhookSignature(payload, headers, secret) {
6184
6345
  if (!secret || secret.trim() === "") {
6185
6346
  throw new Error("Webhook secret is required");
6186
6347
  }
6187
6348
  try {
6188
- const wh = new import_svix.Webhook(secret);
6189
- wh.verify(payload, {
6190
- "svix-id": "",
6191
- // Not used by Resend
6192
- "svix-timestamp": "",
6193
- // Not used by Resend
6194
- "svix-signature": signature
6195
- });
6349
+ new import_svix.Webhook(secret).verify(payload, { ...headers });
6196
6350
  return true;
6197
6351
  } catch (error) {
6198
6352
  logger_default.warn("Webhook signature verification failed", {
@@ -6208,6 +6362,8 @@ function verifyWebhookSignature(payload, signature, secret) {
6208
6362
  CloudflareEmailAdapter,
6209
6363
  EmailService,
6210
6364
  MAX_CHALLENGE_ATTEMPTS,
6365
+ PASSWORDLESS_MAX_ATTEMPTS,
6366
+ PASSWORDLESS_TTL_MS,
6211
6367
  ProblemTypes,
6212
6368
  ResendAdapter,
6213
6369
  SCHEMA_VERSION,
@@ -6219,6 +6375,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6219
6375
  cancelEmailChange,
6220
6376
  checkAccountLocked,
6221
6377
  checkBreachedPassword,
6378
+ cleanupExpiredPasswordlessChallenges,
6222
6379
  clearAccountLockout,
6223
6380
  clearChallengeCookie,
6224
6381
  clearSessionCookie,
@@ -6229,6 +6386,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6229
6386
  createChallengeToken,
6230
6387
  createDeviceToken,
6231
6388
  createEmailAdapter,
6389
+ createPasswordlessChallenge,
6232
6390
  createProblemDetails,
6233
6391
  createSession,
6234
6392
  csrf,
@@ -6245,6 +6403,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6245
6403
  generate2faEnabledEmail,
6246
6404
  generateApiKey,
6247
6405
  generateBackupCodes,
6406
+ generateCode,
6248
6407
  generateCodeChallenge,
6249
6408
  generateCodeVerifier,
6250
6409
  generateEmailChangeConfirmation,
@@ -6283,14 +6442,17 @@ function verifyWebhookSignature(payload, signature, secret) {
6283
6442
  optionalAuth,
6284
6443
  parseDeviceName,
6285
6444
  parseDeviceType,
6445
+ passwordlessEmail,
6286
6446
  problemJson,
6287
6447
  problems,
6288
6448
  rateLimit,
6449
+ recentPasswordlessFailures,
6289
6450
  refreshSession,
6290
6451
  requestEmailChange,
6291
6452
  requireAuth,
6292
6453
  requireVerifiedEmail,
6293
6454
  retrieveChallengeToken,
6455
+ sessionRefreshDue,
6294
6456
  setChallengeCookie,
6295
6457
  setSessionCookie,
6296
6458
  setTrustedDeviceCookie,
@@ -6308,6 +6470,8 @@ function verifyWebhookSignature(payload, signature, secret) {
6308
6470
  verifyCodeChallenge,
6309
6471
  verifyPassword,
6310
6472
  verifyPasswordWithRotation,
6473
+ verifyPasswordlessCode,
6474
+ verifyPasswordlessLink,
6311
6475
  verifyTotpCode,
6312
6476
  verifyTurnstileToken,
6313
6477
  verifyWebhookSignature