@syncello/auth 3.6.1 → 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>
@@ -2178,102 +1680,765 @@ function generateEmailTemplate(options) {
2178
1680
  <!-- Footer Note -->
2179
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;">
2180
1682
  <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>
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>
2184
1757
  </div>
2185
1758
  </td>
2186
1759
  </tr>
2187
1760
  </table>
2188
1761
 
2189
- </td>
2190
- </tr>
2191
- </tbody>
2192
- </table>
2193
- </td>
2194
- </tr>
2195
- </tbody>
2196
- </table>
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}
1893
+
1894
+ The code and the link expire in ${lifetime}.
1895
+
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
+ };
2151
+
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,18 +3763,18 @@ 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));
@@ -3677,7 +3808,7 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3677
3808
  const secret = await decryptTotpSecret(totpMethod.totpSecret, env.TOTP_ENCRYPTION_KEY);
3678
3809
  const result = verifyTotpCode(secret, code, totpMethod.lastTotpCounter);
3679
3810
  if (result.valid) {
3680
- await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where((0, import_drizzle_orm10.and)((0, import_drizzle_orm10.eq)(user2faMethodsTable.userId, userId), (0, import_drizzle_orm10.eq)(user2faMethodsTable.method, "totp")));
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")));
3681
3812
  return { valid: true };
3682
3813
  }
3683
3814
  return { valid: false };
@@ -3689,18 +3820,18 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3689
3820
  const kvKey = `email_2fa_challenge:${userId}`;
3690
3821
  const storedHash = await env.OAUTH_STATES.get(kvKey);
3691
3822
  if (!storedHash) return { valid: false };
3692
- const codeHash = await hashToken(code);
3693
- if (codeHash === storedHash) {
3823
+ const codeHash2 = await hashToken(code);
3824
+ if (codeHash2 === storedHash) {
3694
3825
  await env.OAUTH_STATES.delete(kvKey);
3695
3826
  return { valid: true };
3696
3827
  }
3697
3828
  return { valid: false };
3698
3829
  }
3699
3830
  if (method === "backup") {
3700
- const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where((0, import_drizzle_orm10.and)((0, import_drizzle_orm10.eq)(userBackupCodesTable.userId, userId), (0, import_drizzle_orm10.isNull)(userBackupCodesTable.usedAt)));
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)));
3701
3832
  for (const bc of backupCodes) {
3702
3833
  if (await verifyBackupCode(code, bc.codeHash)) {
3703
- await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm10.eq)(userBackupCodesTable.id, bc.id));
3834
+ await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm11.eq)(userBackupCodesTable.id, bc.id));
3704
3835
  logSecurityEvent("2fa_backup_code_used", "medium", { userId, codeId: bc.id });
3705
3836
  return { valid: true };
3706
3837
  }
@@ -3756,7 +3887,7 @@ var loginHandler = async (c) => {
3756
3887
  return problems.badRequest(c, "Invalid captcha");
3757
3888
  }
3758
3889
  const user = await database.query.users.findFirst({
3759
- where: (0, import_drizzle_orm11.eq)(schema.users.email, email.toLowerCase())
3890
+ where: (0, import_drizzle_orm12.eq)(schema.users.email, email.toLowerCase())
3760
3891
  });
3761
3892
  if (!user) {
3762
3893
  return problems.unauthorized(c, "Invalid email or password");
@@ -3865,7 +3996,7 @@ var loginMiddleware = [
3865
3996
 
3866
3997
  // src/routes/logout.ts
3867
3998
  var import_zod_openapi4 = require("@hono/zod-openapi");
3868
- var import_drizzle_orm12 = require("drizzle-orm");
3999
+ var import_drizzle_orm13 = require("drizzle-orm");
3869
4000
  var logoutRoute = (0, import_zod_openapi4.createRoute)({
3870
4001
  method: "post",
3871
4002
  path: "/logout",
@@ -3888,7 +4019,7 @@ var logoutHandler = async (c) => {
3888
4019
  const sessionIdMatch = cookieHeader.match(cookiePattern);
3889
4020
  if (sessionIdMatch) {
3890
4021
  const sessionId = sessionIdMatch[1];
3891
- 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));
3892
4023
  }
3893
4024
  }
3894
4025
  c.header("Set-Cookie", clearSessionCookie(c.env), { append: true });
@@ -3897,7 +4028,7 @@ var logoutHandler = async (c) => {
3897
4028
 
3898
4029
  // src/routes/me.ts
3899
4030
  var import_zod_openapi5 = require("@hono/zod-openapi");
3900
- var import_drizzle_orm13 = require("drizzle-orm");
4031
+ var import_drizzle_orm14 = require("drizzle-orm");
3901
4032
  var meRoute = (0, import_zod_openapi5.createRoute)({
3902
4033
  method: "get",
3903
4034
  path: "/me",
@@ -3925,7 +4056,7 @@ var meHandler = async (c) => {
3925
4056
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3926
4057
  const { db: database, schema } = getAuthContext(c);
3927
4058
  const user = await database.query.users.findFirst({
3928
- where: (0, import_drizzle_orm13.eq)(schema.users.id, userId)
4059
+ where: (0, import_drizzle_orm14.eq)(schema.users.id, userId)
3929
4060
  });
3930
4061
  if (!user) {
3931
4062
  return problems.notFound(c, "User not found");
@@ -3951,7 +4082,7 @@ var meMiddleware = [requireAuth];
3951
4082
 
3952
4083
  // src/routes/verify-email.ts
3953
4084
  var import_zod_openapi6 = require("@hono/zod-openapi");
3954
- var import_drizzle_orm14 = require("drizzle-orm");
4085
+ var import_drizzle_orm15 = require("drizzle-orm");
3955
4086
  var verifyEmailRoute = (0, import_zod_openapi6.createRoute)({
3956
4087
  method: "post",
3957
4088
  path: "/verify-email",
@@ -3980,17 +4111,17 @@ var verifyEmailHandler = async (c) => {
3980
4111
  const { db: database, schema } = getAuthContext(c);
3981
4112
  const tokenHash = await hashToken(token);
3982
4113
  const verificationToken = await database.query.emailVerificationTokens.findFirst({
3983
- where: (0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
4114
+ where: (0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
3984
4115
  });
3985
4116
  if (!verificationToken) {
3986
4117
  return problems.badRequest(c, "Invalid or expired verification token");
3987
4118
  }
3988
4119
  if (verificationToken.expiresAt < Date.now()) {
3989
- 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));
3990
4121
  return problems.badRequest(c, "Verification token has expired");
3991
4122
  }
3992
- await database.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm14.eq)(schema.users.id, verificationToken.userId));
3993
- await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm14.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
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));
3994
4125
  logSecurityEvent("email_verified", "low", { userId: verificationToken.userId });
3995
4126
  return c.json({ success: true, redirect: "/" });
3996
4127
  };
@@ -3999,18 +4130,18 @@ var verifyEmailHandler = async (c) => {
3999
4130
  var import_zod_openapi7 = require("@hono/zod-openapi");
4000
4131
 
4001
4132
  // src/core/password-reset.ts
4002
- var import_drizzle_orm15 = require("drizzle-orm");
4133
+ var import_drizzle_orm16 = require("drizzle-orm");
4003
4134
  var TOKEN_EXPIRATION_MS = AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES * 60 * 1e3;
4004
4135
  async function createPasswordResetToken(db, tables2, email) {
4005
4136
  const { users, passwordResetTokens } = tables2;
4006
- const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm15.eq)(users.email, email.toLowerCase())).limit(1);
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);
4007
4138
  if (!user) {
4008
4139
  return null;
4009
4140
  }
4010
4141
  if (!user.emailVerified) {
4011
4142
  return null;
4012
4143
  }
4013
- 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));
4014
4145
  const token = generateSecureToken(64);
4015
4146
  const tokenHash = await hashToken(token);
4016
4147
  const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
@@ -4032,10 +4163,10 @@ async function validateResetToken(db, tables2, token) {
4032
4163
  used: passwordResetTokens.used,
4033
4164
  expiresAt: passwordResetTokens.expiresAt
4034
4165
  }).from(passwordResetTokens).where(
4035
- (0, import_drizzle_orm15.and)(
4036
- (0, import_drizzle_orm15.eq)(passwordResetTokens.tokenHash, tokenHash),
4037
- (0, import_drizzle_orm15.eq)(passwordResetTokens.used, false),
4038
- (0, import_drizzle_orm15.gt)(passwordResetTokens.expiresAt, Date.now())
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())
4039
4170
  )
4040
4171
  ).limit(1);
4041
4172
  if (!resetToken) {
@@ -4056,11 +4187,11 @@ async function resetPassword(db, tables2, token, newPassword, pepper, pepperKid
4056
4187
  await db.update(users).set({
4057
4188
  hashedPassword,
4058
4189
  pepperKid,
4059
- sessionVersion: import_drizzle_orm15.sql`${users.sessionVersion} + 1`
4060
- }).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));
4061
4192
  const tokenHash = await hashToken(token);
4062
- await db.update(passwordResetTokens).set({ used: true }).where((0, import_drizzle_orm15.eq)(passwordResetTokens.tokenHash, tokenHash));
4063
- await db.delete(sessions).where((0, import_drizzle_orm15.eq)(sessions.userId, userId));
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));
4064
4195
  logger_default.info("Password reset successful", { userId });
4065
4196
  return { success: true };
4066
4197
  }
@@ -4208,14 +4339,14 @@ var resetPasswordMiddleware = [
4208
4339
  var import_zod_openapi9 = require("@hono/zod-openapi");
4209
4340
 
4210
4341
  // src/core/change-password.ts
4211
- var import_drizzle_orm16 = require("drizzle-orm");
4342
+ var import_drizzle_orm17 = require("drizzle-orm");
4212
4343
  async function changePassword(params) {
4213
4344
  const { userId, currentPassword, newPassword, currentSessionId, pepper, db, tables: tables2 } = params;
4214
4345
  const { users, sessions } = tables2;
4215
4346
  const [user] = await db.select({
4216
4347
  id: users.id,
4217
4348
  hashedPassword: users.hashedPassword
4218
- }).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);
4219
4350
  if (!user || !user.hashedPassword) {
4220
4351
  return { success: false, error: "User not found" };
4221
4352
  }
@@ -4231,8 +4362,8 @@ async function changePassword(params) {
4231
4362
  await db.update(users).set({
4232
4363
  hashedPassword: newHashedPassword,
4233
4364
  updatedAt: /* @__PURE__ */ new Date()
4234
- }).where((0, import_drizzle_orm16.eq)(users.id, userId));
4235
- await db.delete(sessions).where((0, import_drizzle_orm16.and)((0, import_drizzle_orm16.eq)(sessions.userId, userId), (0, import_drizzle_orm16.ne)(sessions.id, currentSessionId)));
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)));
4236
4367
  logSecurityEvent("password_changed", "low", {
4237
4368
  userId
4238
4369
  });
@@ -4505,7 +4636,7 @@ var cancelEmailChangeHandler = async (c) => {
4505
4636
  // src/routes/delete-account.ts
4506
4637
  var import_zod_openapi17 = require("@hono/zod-openapi");
4507
4638
  var import_zod_openapi18 = require("@hono/zod-openapi");
4508
- var import_drizzle_orm17 = require("drizzle-orm");
4639
+ var import_drizzle_orm18 = require("drizzle-orm");
4509
4640
  var deleteAccountRequestSchema = import_zod_openapi18.z.object({
4510
4641
  password: import_zod_openapi18.z.string().min(1).openapi({ example: "CurrentPass123!" })
4511
4642
  }).openapi("DeleteAccountRequest");
@@ -4553,7 +4684,7 @@ var deleteAccountHandler = async (c) => {
4553
4684
  const [user] = await database.select({
4554
4685
  id: schema.users.id,
4555
4686
  hashedPassword: schema.users.hashedPassword
4556
- }).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);
4557
4688
  if (!user || !user.hashedPassword) {
4558
4689
  return problems.unauthorized(c, "Invalid credentials");
4559
4690
  }
@@ -4568,8 +4699,8 @@ var deleteAccountHandler = async (c) => {
4568
4699
  }
4569
4700
  const now = /* @__PURE__ */ new Date();
4570
4701
  const purgeAt = new Date(now.getTime() + PURGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3);
4571
- await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where((0, import_drizzle_orm17.eq)(schema.users.id, userId));
4572
- await database.delete(schema.sessions).where((0, import_drizzle_orm17.eq)(schema.sessions.userId, userId));
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));
4573
4704
  logSecurityEvent("account_deletion_requested", "high", { userId });
4574
4705
  return c.json(
4575
4706
  {
@@ -4590,7 +4721,7 @@ var deleteAccountMiddleware = [requireAuth];
4590
4721
  // src/routes/refresh.ts
4591
4722
  var import_zod_openapi19 = require("@hono/zod-openapi");
4592
4723
  var import_zod_openapi20 = require("@hono/zod-openapi");
4593
- var import_drizzle_orm18 = require("drizzle-orm");
4724
+ var import_drizzle_orm19 = require("drizzle-orm");
4594
4725
  var refreshRequestSchema = import_zod_openapi20.z.object({
4595
4726
  refreshToken: import_zod_openapi20.z.string().min(1).openapi({ example: "abc123..." })
4596
4727
  }).openapi("RefreshRequest");
@@ -4630,7 +4761,7 @@ var refreshHandler = async (c) => {
4630
4761
  expiresAt: schema.sessions.expiresAt,
4631
4762
  fingerprint: schema.sessions.fingerprint,
4632
4763
  ipAddress: schema.sessions.ipAddress
4633
- }).from(schema.sessions).where((0, import_drizzle_orm18.and)((0, import_drizzle_orm18.eq)(schema.sessions.id, refreshToken), (0, import_drizzle_orm18.gt)(schema.sessions.expiresAt, Date.now()))).limit(1);
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);
4634
4765
  if (!session) {
4635
4766
  return problems.unauthorized(c, "Invalid or expired refresh token");
4636
4767
  }
@@ -4653,7 +4784,7 @@ var refreshMiddleware = [];
4653
4784
  // src/routes/resend-verification.ts
4654
4785
  var import_zod_openapi21 = require("@hono/zod-openapi");
4655
4786
  var import_zod_openapi22 = require("@hono/zod-openapi");
4656
- var import_drizzle_orm19 = require("drizzle-orm");
4787
+ var import_drizzle_orm20 = require("drizzle-orm");
4657
4788
  var resendVerificationResponseSchema = import_zod_openapi22.z.object({
4658
4789
  success: import_zod_openapi22.z.boolean().openapi({ example: true }),
4659
4790
  message: import_zod_openapi22.z.string().openapi({ example: "Verification email sent" })
@@ -4698,14 +4829,14 @@ var resendVerificationHandler = async (c) => {
4698
4829
  email: schema.users.email,
4699
4830
  name: schema.users.name,
4700
4831
  emailVerified: schema.users.emailVerified
4701
- }).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);
4702
4833
  if (!user) {
4703
4834
  return problems.unauthorized(c, "User not found");
4704
4835
  }
4705
4836
  if (user.emailVerified) {
4706
4837
  return problems.badRequest(c, "Email is already verified");
4707
4838
  }
4708
- 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));
4709
4840
  const emailToken = generateSecureToken(32);
4710
4841
  const tokenHash = await hashToken(emailToken);
4711
4842
  const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
@@ -4750,7 +4881,7 @@ var import_zod_openapi36 = require("@hono/zod-openapi");
4750
4881
 
4751
4882
  // src/routes/2fa/status.ts
4752
4883
  var import_zod_openapi24 = require("@hono/zod-openapi");
4753
- var import_drizzle_orm20 = require("drizzle-orm");
4884
+ var import_drizzle_orm21 = require("drizzle-orm");
4754
4885
 
4755
4886
  // src/routes/2fa/schemas.ts
4756
4887
  var import_zod_openapi23 = require("@hono/zod-openapi");
@@ -4911,7 +5042,7 @@ var statusHandler = async (c) => {
4911
5042
  if (!userId) return problems.unauthorized(c);
4912
5043
  const { db, schema } = getAuthContext(c);
4913
5044
  const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4914
- const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where((0, import_drizzle_orm20.and)((0, import_drizzle_orm20.eq)(schema.userBackupCodes.userId, userId), (0, import_drizzle_orm20.isNull)(schema.userBackupCodes.usedAt)));
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)));
4915
5046
  c.header("Cache-Control", "private, max-age=60");
4916
5047
  c.header("Vary", "Cookie");
4917
5048
  return c.json({
@@ -4923,7 +5054,7 @@ var statusHandler = async (c) => {
4923
5054
 
4924
5055
  // src/routes/2fa/totp-setup.ts
4925
5056
  var import_zod_openapi25 = require("@hono/zod-openapi");
4926
- var import_drizzle_orm21 = require("drizzle-orm");
5057
+ var import_drizzle_orm22 = require("drizzle-orm");
4927
5058
  var totpSetupRoute = (0, import_zod_openapi25.createRoute)({
4928
5059
  method: "post",
4929
5060
  path: "/totp/setup",
@@ -4953,11 +5084,11 @@ var totpSetupHandler = async (c) => {
4953
5084
  const userId = c.get("userId");
4954
5085
  if (!userId) return problems.unauthorized(c);
4955
5086
  const { db, schema, env } = getAuthContext(c);
4956
- const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm21.and)((0, import_drizzle_orm21.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm21.eq)(schema.user2faMethods.method, "totp"))).limit(1);
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);
4957
5088
  if (existing.length > 0) {
4958
5089
  return problems.badRequest(c, "TOTP already enabled");
4959
5090
  }
4960
- const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm21.eq)(schema.users.id, userId)).limit(1);
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);
4961
5092
  if (!user) return problems.notFound(c, "User not found");
4962
5093
  const secret = generateTotpSecret();
4963
5094
  const qrCodeUri = generateQrCodeUri(secret, user.email);
@@ -4968,7 +5099,7 @@ var totpSetupHandler = async (c) => {
4968
5099
 
4969
5100
  // src/routes/2fa/totp-verify.ts
4970
5101
  var import_zod_openapi26 = require("@hono/zod-openapi");
4971
- var import_drizzle_orm22 = require("drizzle-orm");
5102
+ var import_drizzle_orm23 = require("drizzle-orm");
4972
5103
  var totpVerifyRoute = (0, import_zod_openapi26.createRoute)({
4973
5104
  method: "post",
4974
5105
  path: "/totp/verify",
@@ -5014,7 +5145,7 @@ var totpVerifyHandler = async (c) => {
5014
5145
  logSecurityEvent("2fa_totp_setup_failed", "medium", { userId });
5015
5146
  return problems.badRequest(c, "Invalid code");
5016
5147
  }
5017
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm22.eq)(schema.users.id, userId)).limit(1);
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);
5018
5149
  const encryptionKey = env.TOTP_ENCRYPTION_KEY;
5019
5150
  if (!encryptionKey) {
5020
5151
  logger_default.error("TOTP_ENCRYPTION_KEY not configured");
@@ -5062,7 +5193,7 @@ var totpVerifyHandler = async (c) => {
5062
5193
 
5063
5194
  // src/routes/2fa/totp-disable.ts
5064
5195
  var import_zod_openapi27 = require("@hono/zod-openapi");
5065
- var import_drizzle_orm23 = require("drizzle-orm");
5196
+ var import_drizzle_orm24 = require("drizzle-orm");
5066
5197
  var totpDisableRoute = (0, import_zod_openapi27.createRoute)({
5067
5198
  method: "post",
5068
5199
  path: "/totp/disable",
@@ -5105,13 +5236,13 @@ var totpDisableHandler = async (c) => {
5105
5236
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "totp" });
5106
5237
  return problems.badRequest(c, "Invalid verification code");
5107
5238
  }
5108
- await db.delete(schema.user2faMethods).where((0, import_drizzle_orm23.and)((0, import_drizzle_orm23.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm23.eq)(schema.user2faMethods.method, "totp")));
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")));
5109
5240
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5110
5241
  if (remaining.length === 0) {
5111
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm23.eq)(schema.userBackupCodes.userId, userId));
5112
- await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm23.eq)(schema.userTrustedDevices.userId, userId));
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));
5113
5244
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5114
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm23.eq)(schema.users.id, userId)).limit(1);
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);
5115
5246
  if (user) {
5116
5247
  const emailService = new EmailService(env, void 0, emailBranding);
5117
5248
  const firstName = user.name?.split(" ")[0] || null;
@@ -5130,7 +5261,7 @@ var totpDisableHandler = async (c) => {
5130
5261
 
5131
5262
  // src/routes/2fa/email-setup.ts
5132
5263
  var import_zod_openapi28 = require("@hono/zod-openapi");
5133
- var import_drizzle_orm24 = require("drizzle-orm");
5264
+ var import_drizzle_orm25 = require("drizzle-orm");
5134
5265
  var emailSetupRoute = (0, import_zod_openapi28.createRoute)({
5135
5266
  method: "post",
5136
5267
  path: "/email/setup",
@@ -5160,15 +5291,15 @@ var emailSetupHandler = async (c) => {
5160
5291
  const userId = c.get("userId");
5161
5292
  if (!userId) return problems.unauthorized(c);
5162
5293
  const { db, schema, env, emailBranding } = getAuthContext(c);
5163
- const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm24.and)((0, import_drizzle_orm24.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm24.eq)(schema.user2faMethods.method, "email"))).limit(1);
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);
5164
5295
  if (existing.length > 0) {
5165
5296
  return problems.badRequest(c, "Email 2FA already enabled");
5166
5297
  }
5167
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm24.eq)(schema.users.id, userId)).limit(1);
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);
5168
5299
  if (!user) return problems.notFound(c, "User not found");
5169
5300
  const code = generateEmailOtp();
5170
- const codeHash = await hashToken(code);
5171
- 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 });
5172
5303
  const emailService = new EmailService(env, void 0, emailBranding);
5173
5304
  const firstName = user.name?.split(" ")[0] || null;
5174
5305
  await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
@@ -5178,7 +5309,7 @@ var emailSetupHandler = async (c) => {
5178
5309
 
5179
5310
  // src/routes/2fa/email-verify.ts
5180
5311
  var import_zod_openapi29 = require("@hono/zod-openapi");
5181
- var import_drizzle_orm25 = require("drizzle-orm");
5312
+ var import_drizzle_orm26 = require("drizzle-orm");
5182
5313
  var emailVerifyRoute = (0, import_zod_openapi29.createRoute)({
5183
5314
  method: "post",
5184
5315
  path: "/email/verify",
@@ -5215,12 +5346,12 @@ var emailVerifyHandler = async (c) => {
5215
5346
  if (!storedHash) {
5216
5347
  return problems.badRequest(c, "Code expired. Please request a new one.");
5217
5348
  }
5218
- const codeHash = await hashToken(code);
5219
- if (codeHash !== storedHash) {
5349
+ const codeHash2 = await hashToken(code);
5350
+ if (codeHash2 !== storedHash) {
5220
5351
  logSecurityEvent("2fa_email_setup_failed", "medium", { userId });
5221
5352
  return problems.badRequest(c, "Invalid code");
5222
5353
  }
5223
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm25.eq)(schema.users.id, userId)).limit(1);
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);
5224
5355
  const now = Date.now();
5225
5356
  const existingMethods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5226
5357
  const isFirstMethod = existingMethods.length === 0;
@@ -5262,7 +5393,7 @@ var emailVerifyHandler = async (c) => {
5262
5393
 
5263
5394
  // src/routes/2fa/email-send-code.ts
5264
5395
  var import_zod_openapi30 = require("@hono/zod-openapi");
5265
- var import_drizzle_orm26 = require("drizzle-orm");
5396
+ var import_drizzle_orm27 = require("drizzle-orm");
5266
5397
  var emailSendCodeRoute = (0, import_zod_openapi30.createRoute)({
5267
5398
  method: "post",
5268
5399
  path: "/email/send-code",
@@ -5292,15 +5423,15 @@ var emailSendCodeHandler = async (c) => {
5292
5423
  const userId = c.get("userId");
5293
5424
  if (!userId) return problems.unauthorized(c);
5294
5425
  const { db, schema, env, emailBranding } = getAuthContext(c);
5295
- const [emailMethod] = await db.select().from(schema.user2faMethods).where((0, import_drizzle_orm26.and)((0, import_drizzle_orm26.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm26.eq)(schema.user2faMethods.method, "email"))).limit(1);
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);
5296
5427
  if (!emailMethod) {
5297
5428
  return problems.badRequest(c, "Email 2FA not enabled");
5298
5429
  }
5299
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm26.eq)(schema.users.id, userId)).limit(1);
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);
5300
5431
  if (!user) return problems.notFound(c, "User not found");
5301
5432
  const code = generateEmailOtp();
5302
- const codeHash = await hashToken(code);
5303
- 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, {
5304
5435
  expirationTtl: 300
5305
5436
  });
5306
5437
  const emailService = new EmailService(env, void 0, emailBranding);
@@ -5322,7 +5453,7 @@ var emailSendCodeHandler = async (c) => {
5322
5453
 
5323
5454
  // src/routes/2fa/email-disable.ts
5324
5455
  var import_zod_openapi31 = require("@hono/zod-openapi");
5325
- var import_drizzle_orm27 = require("drizzle-orm");
5456
+ var import_drizzle_orm28 = require("drizzle-orm");
5326
5457
  var emailDisableRoute = (0, import_zod_openapi31.createRoute)({
5327
5458
  method: "post",
5328
5459
  path: "/email/disable",
@@ -5365,13 +5496,13 @@ var emailDisableHandler = async (c) => {
5365
5496
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "email" });
5366
5497
  return problems.badRequest(c, "Invalid verification code");
5367
5498
  }
5368
- await db.delete(schema.user2faMethods).where((0, import_drizzle_orm27.and)((0, import_drizzle_orm27.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm27.eq)(schema.user2faMethods.method, "email")));
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")));
5369
5500
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
5370
5501
  if (remaining.length === 0) {
5371
- await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm27.eq)(schema.userBackupCodes.userId, userId));
5372
- await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm27.eq)(schema.userTrustedDevices.userId, userId));
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));
5373
5504
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5374
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm27.eq)(schema.users.id, userId)).limit(1);
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);
5375
5506
  if (user) {
5376
5507
  const emailService = new EmailService(env, void 0, emailBranding);
5377
5508
  const firstName = user.name?.split(" ")[0] || null;
@@ -5390,7 +5521,7 @@ var emailDisableHandler = async (c) => {
5390
5521
 
5391
5522
  // src/routes/2fa/trusted-devices.ts
5392
5523
  var import_zod_openapi32 = require("@hono/zod-openapi");
5393
- var import_drizzle_orm28 = require("drizzle-orm");
5524
+ var import_drizzle_orm29 = require("drizzle-orm");
5394
5525
  var trustedDevicesGetRoute = (0, import_zod_openapi32.createRoute)({
5395
5526
  method: "get",
5396
5527
  path: "/trusted-devices",
@@ -5429,7 +5560,7 @@ var trustedDevicesGetHandler = async (c) => {
5429
5560
  ipAddress: schema.userTrustedDevices.ipAddress,
5430
5561
  lastUsedAt: schema.userTrustedDevices.lastUsedAt,
5431
5562
  createdAt: schema.userTrustedDevices.createdAt
5432
- }).from(schema.userTrustedDevices).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId), (0, import_drizzle_orm28.gt)(schema.userTrustedDevices.expiresAt, now)));
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)));
5433
5564
  c.header("Cache-Control", "private, max-age=60");
5434
5565
  c.header("Vary", "Cookie");
5435
5566
  return c.json({ devices });
@@ -5465,7 +5596,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5465
5596
  if (!userId) return problems.unauthorized(c);
5466
5597
  const deviceId = c.req.param("id");
5467
5598
  const { db, schema } = getAuthContext(c);
5468
- const result = await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.id, deviceId), (0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId))).returning({ id: schema.userTrustedDevices.id });
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 });
5469
5600
  if (result.length === 0) {
5470
5601
  return problems.notFound(c, "Device not found");
5471
5602
  }
@@ -5475,7 +5606,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5475
5606
 
5476
5607
  // src/routes/2fa/backup-codes.ts
5477
5608
  var import_zod_openapi33 = require("@hono/zod-openapi");
5478
- var import_drizzle_orm29 = require("drizzle-orm");
5609
+ var import_drizzle_orm30 = require("drizzle-orm");
5479
5610
  var backupCodesRegenerateRoute = (0, import_zod_openapi33.createRoute)({
5480
5611
  method: "post",
5481
5612
  path: "/backup-codes/regenerate",
@@ -5516,7 +5647,7 @@ var backupCodesRegenerateHandler = async (c) => {
5516
5647
  logSecurityEvent("2fa_backup_regenerate_failed", "medium", { userId });
5517
5648
  return problems.badRequest(c, "Invalid verification code");
5518
5649
  }
5519
- 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));
5520
5651
  const codes = generateBackupCodes();
5521
5652
  const now = Date.now();
5522
5653
  const hashedCodes = await Promise.all(
@@ -5533,7 +5664,7 @@ var backupCodesRegenerateHandler = async (c) => {
5533
5664
 
5534
5665
  // src/routes/2fa/challenge.ts
5535
5666
  var import_zod_openapi34 = require("@hono/zod-openapi");
5536
- var import_drizzle_orm30 = require("drizzle-orm");
5667
+ var import_drizzle_orm31 = require("drizzle-orm");
5537
5668
  var import_cookie3 = require("hono/cookie");
5538
5669
  var challengeRoute = (0, import_zod_openapi34.createRoute)({
5539
5670
  method: "post",
@@ -5605,7 +5736,7 @@ var challengeHandler = async (c) => {
5605
5736
  }
5606
5737
  await deleteChallengeToken(c.env.OAUTH_STATES, challengeToken);
5607
5738
  clearChallengeCookie(c, c.env);
5608
- const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm30.eq)(schema.users.id, payload.userId)).limit(1);
5739
+ const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm31.eq)(schema.users.id, payload.userId)).limit(1);
5609
5740
  if (!user) {
5610
5741
  logger_default.error("User not found during 2FA challenge", { userId: payload.userId });
5611
5742
  return problems.notFound(c, "User not found");
@@ -5646,7 +5777,7 @@ var challengeHandler = async (c) => {
5646
5777
 
5647
5778
  // src/routes/2fa/challenge-resend.ts
5648
5779
  var import_zod_openapi35 = require("@hono/zod-openapi");
5649
- var import_drizzle_orm31 = require("drizzle-orm");
5780
+ var import_drizzle_orm32 = require("drizzle-orm");
5650
5781
  var import_cookie4 = require("hono/cookie");
5651
5782
  var challengeResendRoute = (0, import_zod_openapi35.createRoute)({
5652
5783
  method: "post",
@@ -5684,7 +5815,7 @@ var challengeResendHandler = async (c) => {
5684
5815
  if (!payload.methods.includes("email")) {
5685
5816
  return problems.badRequest(c, "Email verification not available");
5686
5817
  }
5687
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm31.eq)(schema.users.id, payload.userId)).limit(1);
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);
5688
5819
  if (!user) {
5689
5820
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5690
5821
  return problems.notFound(c, "User not found");
@@ -5915,7 +6046,7 @@ var authorizeHandler = async (c) => {
5915
6046
 
5916
6047
  // src/routes/oauth/callback.ts
5917
6048
  var import_zod_openapi39 = require("@hono/zod-openapi");
5918
- var import_drizzle_orm32 = require("drizzle-orm");
6049
+ var import_drizzle_orm33 = require("drizzle-orm");
5919
6050
  var callbackRoute = (0, import_zod_openapi39.createRoute)({
5920
6051
  method: "get",
5921
6052
  path: "/{provider}/callback",
@@ -6041,10 +6172,24 @@ var callbackHandler = async (c) => {
6041
6172
  logger_default.warn("No user ID returned from OAuth provider", { provider });
6042
6173
  return c.redirect(`${appUrl}/login?error=oauth_failed`);
6043
6174
  }
6044
- const { db, schema } = getAuthContext(c);
6045
- const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.email, email)).limit(1);
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);
6046
6177
  let authUser = existingUser;
6047
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
+ }
6048
6193
  const [createdUser] = await db.insert(schema.users).values({
6049
6194
  email,
6050
6195
  name: name || null,
@@ -6055,6 +6200,15 @@ var callbackHandler = async (c) => {
6055
6200
  activatedAt: /* @__PURE__ */ new Date()
6056
6201
  }).returning({ id: schema.users.id, email: schema.users.email });
6057
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
+ }
6058
6212
  logSecurityEvent("user_signup_oauth", "low", {
6059
6213
  userId: createdUser.id,
6060
6214
  email: createdUser.email,
@@ -6094,7 +6248,7 @@ var callbackHandler = async (c) => {
6094
6248
 
6095
6249
  // src/routes/oauth/complete.ts
6096
6250
  var import_zod_openapi40 = require("@hono/zod-openapi");
6097
- var import_drizzle_orm33 = require("drizzle-orm");
6251
+ var import_drizzle_orm34 = require("drizzle-orm");
6098
6252
  var completeRoute = (0, import_zod_openapi40.createRoute)({
6099
6253
  method: "post",
6100
6254
  path: "/complete",
@@ -6142,7 +6296,7 @@ var completeHandler = async (c) => {
6142
6296
  redirect: storedRedirect
6143
6297
  } = JSON.parse(stored);
6144
6298
  const { db, schema } = getAuthContext(c);
6145
- 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));
6146
6300
  await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
6147
6301
  const fingerprint = await generateFingerprint(c.req.raw);
6148
6302
  const ipAddress = getClientIp(c.req.raw);
@@ -6187,19 +6341,12 @@ var routes_default = authRoutes;
6187
6341
 
6188
6342
  // src/lib/email/webhook-verifier.ts
6189
6343
  var import_svix = require("svix");
6190
- function verifyWebhookSignature(payload, signature, secret) {
6344
+ function verifyWebhookSignature(payload, headers, secret) {
6191
6345
  if (!secret || secret.trim() === "") {
6192
6346
  throw new Error("Webhook secret is required");
6193
6347
  }
6194
6348
  try {
6195
- const wh = new import_svix.Webhook(secret);
6196
- wh.verify(payload, {
6197
- "svix-id": "",
6198
- // Not used by Resend
6199
- "svix-timestamp": "",
6200
- // Not used by Resend
6201
- "svix-signature": signature
6202
- });
6349
+ new import_svix.Webhook(secret).verify(payload, { ...headers });
6203
6350
  return true;
6204
6351
  } catch (error) {
6205
6352
  logger_default.warn("Webhook signature verification failed", {
@@ -6215,6 +6362,8 @@ function verifyWebhookSignature(payload, signature, secret) {
6215
6362
  CloudflareEmailAdapter,
6216
6363
  EmailService,
6217
6364
  MAX_CHALLENGE_ATTEMPTS,
6365
+ PASSWORDLESS_MAX_ATTEMPTS,
6366
+ PASSWORDLESS_TTL_MS,
6218
6367
  ProblemTypes,
6219
6368
  ResendAdapter,
6220
6369
  SCHEMA_VERSION,
@@ -6226,6 +6375,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6226
6375
  cancelEmailChange,
6227
6376
  checkAccountLocked,
6228
6377
  checkBreachedPassword,
6378
+ cleanupExpiredPasswordlessChallenges,
6229
6379
  clearAccountLockout,
6230
6380
  clearChallengeCookie,
6231
6381
  clearSessionCookie,
@@ -6236,6 +6386,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6236
6386
  createChallengeToken,
6237
6387
  createDeviceToken,
6238
6388
  createEmailAdapter,
6389
+ createPasswordlessChallenge,
6239
6390
  createProblemDetails,
6240
6391
  createSession,
6241
6392
  csrf,
@@ -6252,6 +6403,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6252
6403
  generate2faEnabledEmail,
6253
6404
  generateApiKey,
6254
6405
  generateBackupCodes,
6406
+ generateCode,
6255
6407
  generateCodeChallenge,
6256
6408
  generateCodeVerifier,
6257
6409
  generateEmailChangeConfirmation,
@@ -6290,14 +6442,17 @@ function verifyWebhookSignature(payload, signature, secret) {
6290
6442
  optionalAuth,
6291
6443
  parseDeviceName,
6292
6444
  parseDeviceType,
6445
+ passwordlessEmail,
6293
6446
  problemJson,
6294
6447
  problems,
6295
6448
  rateLimit,
6449
+ recentPasswordlessFailures,
6296
6450
  refreshSession,
6297
6451
  requestEmailChange,
6298
6452
  requireAuth,
6299
6453
  requireVerifiedEmail,
6300
6454
  retrieveChallengeToken,
6455
+ sessionRefreshDue,
6301
6456
  setChallengeCookie,
6302
6457
  setSessionCookie,
6303
6458
  setTrustedDeviceCookie,
@@ -6315,6 +6470,8 @@ function verifyWebhookSignature(payload, signature, secret) {
6315
6470
  verifyCodeChallenge,
6316
6471
  verifyPassword,
6317
6472
  verifyPasswordWithRotation,
6473
+ verifyPasswordlessCode,
6474
+ verifyPasswordlessLink,
6318
6475
  verifyTotpCode,
6319
6476
  verifyTurnstileToken,
6320
6477
  verifyWebhookSignature