@syncello/auth 3.6.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  enums,
4
4
  tableNames,
5
5
  tables
6
- } from "./chunk-OZ26T3ZA.js";
6
+ } from "./chunk-W35EDOHW.js";
7
7
 
8
8
  // src/core/password.ts
9
9
  import bcrypt from "bcryptjs";
@@ -255,9 +255,14 @@ var AUTH_DEFAULTS = {
255
255
  // Absolute cap: a session dies this long after creation regardless of
256
256
  // sliding-window activity (limits the value of a stolen session token).
257
257
  SESSION_MAX_LIFETIME_DAYS: 30,
258
+ // The sliding window moves at most this often, so a busy session is not a write per request.
259
+ SESSION_REFRESH_INTERVAL_MINUTES: 15,
258
260
  LOCKOUT_DURATION_MINUTES: 30,
259
261
  LOCKOUT_MAX_ATTEMPTS: 5,
260
262
  PASSWORD_RESET_TTL_MINUTES: 15,
263
+ EMAIL_VERIFICATION_TTL_HOURS: 24,
264
+ EMAIL_CHANGE_TTL_HOURS: 24,
265
+ TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5,
261
266
  TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300,
262
267
  // 5 minutes
263
268
  APP_NAME: "App"
@@ -335,7 +340,7 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
335
340
  note: "SSR requests have different User-Agent, causing expected mismatch"
336
341
  });
337
342
  }
338
- await refreshSession(db, { sessions }, sessionId);
343
+ if (sessionRefreshDue(row.sessionLastActiveAt)) await refreshSession(db, { sessions }, sessionId);
339
344
  return {
340
345
  user: {
341
346
  id: row.userId,
@@ -354,6 +359,10 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
354
359
  }
355
360
  };
356
361
  }
362
+ function sessionRefreshDue(lastActiveAt, now = Date.now()) {
363
+ const interval = AUTH_DEFAULTS.SESSION_REFRESH_INTERVAL_MINUTES * 60 * 1e3;
364
+ return lastActiveAt === null || now - lastActiveAt >= interval;
365
+ }
357
366
  async function refreshSession(db, tables2, sessionId, sessionTtlMs = AUTH_DEFAULTS.SESSION_TTL_DAYS * 24 * 60 * 60 * 1e3) {
358
367
  const { sessions } = tables2;
359
368
  const newExpiration = Date.now() + sessionTtlMs;
@@ -521,16 +530,16 @@ function createDeviceToken() {
521
530
  };
522
531
  }
523
532
  async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
524
- const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
533
+ const { eq: eq34, and: and15, gt: gt7 } = await import("drizzle-orm");
525
534
  const [device] = await db.select().from(userTrustedDevicesTable).where(
526
- and14(
527
- eq33(userTrustedDevicesTable.userId, userId),
528
- eq33(userTrustedDevicesTable.tokenHash, tokenHash),
529
- gt6(userTrustedDevicesTable.expiresAt, Date.now())
535
+ and15(
536
+ eq34(userTrustedDevicesTable.userId, userId),
537
+ eq34(userTrustedDevicesTable.tokenHash, tokenHash),
538
+ gt7(userTrustedDevicesTable.expiresAt, Date.now())
530
539
  )
531
540
  ).limit(1);
532
541
  if (device) {
533
- await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
542
+ await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq34(userTrustedDevicesTable.id, device.id));
534
543
  return true;
535
544
  }
536
545
  return false;
@@ -743,7 +752,7 @@ async function verifyTurnstileToken(token, secretKey, remoteIp, environment) {
743
752
 
744
753
  // src/core/email-change.ts
745
754
  import { eq as eq3 } from "drizzle-orm";
746
- var TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1e3;
755
+ var TOKEN_EXPIRY_MS = AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60 * 60 * 1e3;
747
756
  async function requestEmailChange(params) {
748
757
  const { userId, password, newEmail, pepper, db, tables: tables2 } = params;
749
758
  const { users, emailChangeTokens } = tables2;
@@ -943,582 +952,48 @@ function base64UrlEncode(data) {
943
952
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
944
953
  }
945
954
 
946
- // src/middleware/auth.ts
947
- import { createMiddleware } from "hono/factory";
948
- import { eq as eq4, and as and2, gt as gt2 } from "drizzle-orm";
955
+ // src/core/passwordless.ts
956
+ import { and as and2, desc, eq as eq4, gt as gt2, isNull, lt, sql as sql2 } from "drizzle-orm";
949
957
 
950
- // src/factory.ts
951
- function createAuth(config) {
952
- const { db, schema, getEnv, emailBranding, invitations } = config;
953
- const getContext = (c) => ({
954
- db,
955
- schema,
956
- env: getEnv(c),
957
- emailBranding,
958
- invitations
959
- });
960
- return {
961
- db,
962
- schema,
963
- getContext
964
- };
965
- }
966
- function createAuthMiddleware(auth2) {
967
- return async (c, next) => {
968
- const ctx = auth2.getContext(c);
969
- c.set("authContext", ctx);
970
- c.set("db", auth2.db);
971
- await next();
972
- };
973
- }
974
- function getAuthContext(c) {
975
- const ctx = c.get("authContext");
976
- if (!ctx) {
977
- throw new Error(
978
- "AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
979
- );
958
+ // src/lib/email/security.ts
959
+ function getAllowedDomains(appUrl) {
960
+ const base = ["localhost", "127.0.0.1"];
961
+ try {
962
+ const url = new URL(appUrl);
963
+ return [...base, url.hostname];
964
+ } catch {
965
+ return base;
980
966
  }
981
- return ctx;
982
- }
983
-
984
- // src/lib/problem-json.ts
985
- var ProblemTypes = {
986
- // 4xx Client Errors
987
- BAD_REQUEST: "/errors/bad-request",
988
- UNAUTHORIZED: "/errors/unauthorized",
989
- FORBIDDEN: "/errors/forbidden",
990
- NOT_FOUND: "/errors/not-found",
991
- CONFLICT: "/errors/conflict",
992
- GONE: "/errors/gone",
993
- VALIDATION_ERROR: "/errors/validation-error",
994
- RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
995
- // 5xx Server Errors
996
- INTERNAL_ERROR: "/errors/internal-error",
997
- SERVICE_UNAVAILABLE: "/errors/service-unavailable"
998
- };
999
- function generateTraceId() {
1000
- const bytes = crypto.getRandomValues(new Uint8Array(16));
1001
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1002
967
  }
1003
- function createProblemDetails(type, title, status, options) {
1004
- const problem = {
1005
- type,
1006
- title,
1007
- status
1008
- };
1009
- if (options?.detail) {
1010
- problem.detail = options.detail;
1011
- }
1012
- if (options?.instance) {
1013
- problem.instance = options.instance;
968
+ function sanitizeEmailUrl(url, appUrl = "") {
969
+ let parsed;
970
+ try {
971
+ parsed = new URL(url);
972
+ } catch {
973
+ throw new Error("Invalid URL format");
1014
974
  }
1015
- problem.traceId = options?.traceId || generateTraceId();
1016
- if (options?.requestId) {
1017
- problem.requestId = options.requestId;
975
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
976
+ throw new Error(`Disallowed protocol: ${parsed.protocol}`);
1018
977
  }
1019
- if (options?.extensions) {
1020
- Object.assign(problem, options.extensions);
978
+ const allowedDomains = getAllowedDomains(appUrl);
979
+ const hostname = parsed.hostname.toLowerCase();
980
+ const isAllowed = allowedDomains.some((domain) => {
981
+ return hostname === domain || hostname.endsWith(`.${domain}`);
982
+ });
983
+ if (!isAllowed) {
984
+ throw new Error(`Disallowed domain: ${hostname}`);
1021
985
  }
1022
- return problem;
986
+ return parsed.toString();
1023
987
  }
1024
- function problemJson(c, problem) {
1025
- logger_default.warn("API error response", {
1026
- type: problem.type,
1027
- status: problem.status,
1028
- traceId: problem.traceId,
1029
- requestId: problem.requestId,
1030
- path: c.req.path,
1031
- method: c.req.method
1032
- });
1033
- return c.json(problem, problem.status, {
1034
- "Content-Type": "application/problem+json"
1035
- });
1036
- }
1037
- var problems = {
1038
- /**
1039
- * 400 Bad Request - Generic client error
1040
- */
1041
- badRequest(c, detail) {
1042
- return problemJson(
1043
- c,
1044
- createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
1045
- detail: detail || "The request could not be understood by the server",
1046
- instance: `${c.req.method} ${c.req.path}`,
1047
- requestId: c.get("requestId"),
1048
- traceId: c.get("traceId")
1049
- })
1050
- );
1051
- },
1052
- /**
1053
- * 400 Bad Request - Validation errors with field-specific details
1054
- */
1055
- validationError(c, detail, errors) {
1056
- return problemJson(
1057
- c,
1058
- createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
1059
- detail,
1060
- instance: `${c.req.method} ${c.req.path}`,
1061
- requestId: c.get("requestId"),
1062
- traceId: c.get("traceId"),
1063
- extensions: errors ? { errors } : void 0
1064
- })
1065
- );
1066
- },
1067
- /**
1068
- * 401 Unauthorized - Authentication required
1069
- */
1070
- unauthorized(c, detail) {
1071
- return problemJson(
1072
- c,
1073
- createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
1074
- detail: detail || "Authentication is required to access this resource",
1075
- instance: `${c.req.method} ${c.req.path}`,
1076
- requestId: c.get("requestId"),
1077
- traceId: c.get("traceId")
1078
- })
1079
- );
1080
- },
1081
- /**
1082
- * 403 Forbidden - Insufficient permissions
1083
- */
1084
- forbidden(c, detail) {
1085
- return problemJson(
1086
- c,
1087
- createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
1088
- detail: detail || "You do not have permission to access this resource",
1089
- instance: `${c.req.method} ${c.req.path}`,
1090
- requestId: c.get("requestId"),
1091
- traceId: c.get("traceId")
1092
- })
1093
- );
1094
- },
1095
- /**
1096
- * 404 Not Found
1097
- */
1098
- notFound(c, detail) {
1099
- return problemJson(
1100
- c,
1101
- createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
1102
- detail: detail || "The requested resource was not found",
1103
- instance: `${c.req.method} ${c.req.path}`,
1104
- requestId: c.get("requestId"),
1105
- traceId: c.get("traceId")
1106
- })
1107
- );
1108
- },
1109
- /**
1110
- * 409 Conflict - Resource conflict (e.g., duplicate email)
1111
- */
1112
- conflict(c, detail) {
1113
- return problemJson(
1114
- c,
1115
- createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
1116
- detail,
1117
- instance: `${c.req.method} ${c.req.path}`,
1118
- requestId: c.get("requestId"),
1119
- traceId: c.get("traceId")
1120
- })
1121
- );
1122
- },
1123
- /**
1124
- * 410 Gone - Resource is no longer available
1125
- */
1126
- gone(c, detail) {
1127
- return problemJson(
1128
- c,
1129
- createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
1130
- detail: detail || "The requested resource is no longer available",
1131
- instance: `${c.req.method} ${c.req.path}`,
1132
- requestId: c.get("requestId"),
1133
- traceId: c.get("traceId")
1134
- })
1135
- );
1136
- },
1137
- /**
1138
- * 429 Too Many Requests - Rate limit exceeded
1139
- */
1140
- rateLimitExceeded(c, retryAfter) {
1141
- const extensions = retryAfter ? { retryAfter } : void 0;
1142
- return problemJson(
1143
- c,
1144
- createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
1145
- detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
1146
- instance: `${c.req.method} ${c.req.path}`,
1147
- requestId: c.get("requestId"),
1148
- traceId: c.get("traceId"),
1149
- extensions
1150
- })
1151
- );
1152
- },
1153
- /**
1154
- * 500 Internal Server Error - Generic server error
1155
- * SECURITY: Never include error details or stack traces
1156
- */
1157
- internalError(c, error) {
1158
- const traceId = c.get("traceId") || generateTraceId();
1159
- if (error) {
1160
- logError(error, {
1161
- context: "internal_error",
1162
- path: c.req.path,
1163
- method: c.req.method,
1164
- traceId,
1165
- requestId: c.get("requestId")
1166
- });
1167
- }
1168
- return problemJson(
1169
- c,
1170
- createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
1171
- detail: "An unexpected error occurred. Please try again later",
1172
- instance: `${c.req.method} ${c.req.path}`,
1173
- traceId,
1174
- requestId: c.get("requestId")
1175
- })
1176
- );
1177
- },
1178
- /**
1179
- * 503 Service Unavailable - Service is temporarily unavailable
1180
- */
1181
- serviceUnavailable(c, detail) {
1182
- return problemJson(
1183
- c,
1184
- createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
1185
- detail: detail || "The service is temporarily unavailable. Please try again later",
1186
- instance: `${c.req.method} ${c.req.path}`,
1187
- requestId: c.get("requestId"),
1188
- traceId: c.get("traceId")
1189
- })
1190
- );
1191
- }
1192
- };
1193
-
1194
- // src/middleware/auth.ts
1195
- var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
1196
- async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
1197
- if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
1198
- logger_default.info("Session exceeded absolute lifetime - revoked", {
1199
- type: "security",
1200
- event: "session_max_lifetime_exceeded",
1201
- severity: "low",
1202
- sessionId: sessionId.slice(0, 8),
1203
- userId: session.userId
1204
- });
1205
- await deleteSession(db, { sessions }, sessionId);
1206
- return true;
1207
- }
1208
- async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
1209
- const cookieName = getSessionCookieName(env);
1210
- const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
1211
- const sessionIdMatch = cookieHeader.match(cookiePattern);
1212
- if (!sessionIdMatch) return null;
1213
- const sessionId = sessionIdMatch[1];
1214
- const foundSessions = await db.select({
1215
- userId: sessions.userId,
1216
- expiresAt: sessions.expiresAt,
1217
- createdAt: sessions.createdAt,
1218
- fingerprint: sessions.fingerprint
1219
- }).from(sessions).where(and2(eq4(sessions.id, sessionId), gt2(sessions.expiresAt, Date.now()))).limit(1);
1220
- if (foundSessions.length === 0) return null;
1221
- const session = foundSessions[0];
1222
- if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1223
- await refreshSession(db, { sessions }, sessionId);
1224
- const userAgent = request.headers.get("user-agent") || "";
1225
- const cfWorker = request.headers.get("cf-worker") || "";
1226
- const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
1227
- const isLocalSSR = userAgent.toLowerCase().includes("node");
1228
- const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
1229
- const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
1230
- const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
1231
- if (isSSR) {
1232
- logger_default.info("SSR request detected, using IP validation", {
1233
- type: "security",
1234
- event: "ssr_session_validation",
1235
- sessionId: sessionId.slice(0, 8),
1236
- userId: session.userId,
1237
- clientIp,
1238
- cfWorker,
1239
- isLocalSSR,
1240
- isSvelteKitSSR,
1241
- isCloudflareSSR
1242
- });
1243
- } else {
1244
- if (session.fingerprint) {
1245
- const currentFingerprint = await generateFingerprint(request);
1246
- if (session.fingerprint !== currentFingerprint) {
1247
- logger_default.info("Session fingerprint changed (not enforced)", {
1248
- type: "security",
1249
- event: "fingerprint_changed",
1250
- sessionId: sessionId.slice(0, 8),
1251
- userId: session.userId,
1252
- clientIp
1253
- });
1254
- }
1255
- }
1256
- }
1257
- return {
1258
- userId: session.userId,
1259
- expiresAt: session.expiresAt
1260
- };
1261
- }
1262
- async function getSessionFromBearerToken(db, sessions, authHeader) {
1263
- if (!authHeader.startsWith("Bearer ")) return null;
1264
- const sessionId = authHeader.slice(7);
1265
- const foundSessions = await db.select({
1266
- userId: sessions.userId,
1267
- expiresAt: sessions.expiresAt,
1268
- createdAt: sessions.createdAt
1269
- }).from(sessions).where(and2(eq4(sessions.id, sessionId), gt2(sessions.expiresAt, Date.now()))).limit(1);
1270
- if (foundSessions.length === 0) return null;
1271
- const session = foundSessions[0];
1272
- if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1273
- await refreshSession(db, { sessions }, sessionId);
1274
- return {
1275
- userId: session.userId,
1276
- expiresAt: session.expiresAt
1277
- };
1278
- }
1279
- var requireAuth = createMiddleware(
1280
- async (c, next) => {
1281
- const { db, schema } = getAuthContext(c);
1282
- const authHeader = c.req.header("Authorization");
1283
- if (authHeader) {
1284
- const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1285
- if (session2) {
1286
- c.set("userId", session2.userId);
1287
- await next();
1288
- return;
1289
- }
1290
- }
1291
- const cookieHeader = c.req.header("cookie");
1292
- if (!cookieHeader) {
1293
- return problems.unauthorized(c, "Please log in to continue");
1294
- }
1295
- const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1296
- if (!session) {
1297
- c.header("Set-Cookie", clearSessionCookie(c.env));
1298
- return problems.unauthorized(c, "Your session has expired. Please log in again.");
1299
- }
1300
- c.set("userId", session.userId);
1301
- await next();
1302
- }
1303
- );
1304
- var optionalAuth = createMiddleware(
1305
- async (c, next) => {
1306
- const { db, schema } = getAuthContext(c);
1307
- const authHeader = c.req.header("Authorization");
1308
- if (authHeader) {
1309
- const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1310
- if (session) {
1311
- c.set("userId", session.userId);
1312
- await next();
1313
- return;
1314
- }
1315
- }
1316
- const cookieHeader = c.req.header("cookie");
1317
- if (cookieHeader) {
1318
- const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1319
- if (session) {
1320
- c.set("userId", session.userId);
1321
- }
1322
- }
1323
- await next();
1324
- }
1325
- );
1326
-
1327
- // src/middleware/csrf.ts
1328
- import { createMiddleware as createMiddleware2 } from "hono/factory";
1329
- var csrf = createMiddleware2(async (c, next) => {
1330
- const method = c.req.method;
1331
- const path = c.req.path;
1332
- const isWebhook = path.startsWith("/v1/webhooks/");
1333
- if (isWebhook) {
1334
- await next();
1335
- return;
1336
- }
1337
- const isMcpEndpoint = path === "/v1/mcp";
1338
- if (isMcpEndpoint) {
1339
- await next();
1340
- return;
1341
- }
1342
- const isOAuthMcpEndpoint = path.startsWith("/v1/oauth/");
1343
- if (isOAuthMcpEndpoint) {
1344
- await next();
1345
- return;
1346
- }
1347
- const isToolsApi = path.startsWith("/v1/tools/") && !path.startsWith("/v1/tools/catalog");
1348
- if (isToolsApi) {
1349
- await next();
1350
- return;
1351
- }
1352
- const isAdminApi = path.startsWith("/v1/admin/");
1353
- if (isAdminApi) {
1354
- await next();
1355
- return;
1356
- }
1357
- const isMobileAuthEndpoint = path === "/v1/auth/login" || path === "/v1/auth/signup" || path === "/v1/auth/refresh";
1358
- const authHeader = c.req.header("Authorization");
1359
- const hasBearerToken = authHeader?.startsWith("Bearer ");
1360
- const hasNoOrigin = !c.req.header("origin") && !c.req.header("referer");
1361
- if (isMobileAuthEndpoint && hasNoOrigin || hasBearerToken) {
1362
- await next();
1363
- return;
1364
- }
1365
- const isInternalSync = path.startsWith("/v1/internal/sync/");
1366
- if (isInternalSync) {
1367
- await next();
1368
- return;
1369
- }
1370
- if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
1371
- const appUrl = c.env.APP_URL || "http://localhost:5173";
1372
- const allowedOrigins = getAllowedOrigins(appUrl);
1373
- const request = c.req.raw;
1374
- const isTestEndpoint = c.req.path.startsWith("/test");
1375
- if (isTestEndpoint) {
1376
- const origin = request.headers.get("origin");
1377
- const referer = request.headers.get("referer");
1378
- logger_default.debug("CSRF validation for test endpoint", {
1379
- path: c.req.path,
1380
- method,
1381
- origin,
1382
- referer,
1383
- appUrl,
1384
- allowedOrigins
1385
- });
1386
- }
1387
- const pagesPattern = c.env.CORS_PAGES_PATTERN;
1388
- if (!validateOrigin(request, allowedOrigins, pagesPattern)) {
1389
- return problems.forbidden(c, "Request blocked for security reasons. Please try again.");
1390
- }
1391
- }
1392
- await next();
1393
- });
1394
-
1395
- // src/middleware/rateLimit.ts
1396
- import { createMiddleware as createMiddleware3 } from "hono/factory";
1397
-
1398
- // src/lib/rate-limit-kv.ts
1399
- async function checkRateLimitKV(kv, key, maxAttempts, windowSeconds) {
1400
- try {
1401
- const rateLimitKey = `rate_limit:${key}`;
1402
- const currentAttemptsStr = await kv.get(rateLimitKey);
1403
- const currentAttempts = currentAttemptsStr ? parseInt(currentAttemptsStr, 10) : 0;
1404
- if (currentAttempts >= maxAttempts) {
1405
- return {
1406
- allowed: false,
1407
- remaining: 0
1408
- };
1409
- }
1410
- const newAttempts = currentAttempts + 1;
1411
- await kv.put(rateLimitKey, newAttempts.toString(), {
1412
- expirationTtl: windowSeconds
1413
- });
1414
- return {
1415
- allowed: true,
1416
- remaining: maxAttempts - newAttempts
1417
- };
1418
- } catch (error) {
1419
- logger_default.error("Rate limit KV error, failing open", {
1420
- key,
1421
- error: error instanceof Error ? error.message : "Unknown error"
1422
- });
1423
- return {
1424
- allowed: true,
1425
- remaining: maxAttempts
1426
- };
1427
- }
1428
- }
1429
-
1430
- // src/middleware/rateLimit.ts
1431
- var rateLimit = (config) => createMiddleware3(async (c, next) => {
1432
- const kv = c.env.RATE_LIMIT_KV;
1433
- const identifier = await config.identifier(c);
1434
- const key = `${config.action}:${identifier}`;
1435
- const windowSeconds = Math.ceil(config.windowMs / 1e3);
1436
- const { allowed, remaining } = await checkRateLimitKV(
1437
- kv,
1438
- key,
1439
- config.maxAttempts,
1440
- windowSeconds
1441
- );
1442
- const resetTimestamp = Math.ceil(Date.now() / 1e3) + windowSeconds;
1443
- c.header("X-RateLimit-Limit", config.maxAttempts.toString());
1444
- c.header("X-RateLimit-Remaining", Math.max(0, remaining).toString());
1445
- c.header("X-RateLimit-Reset", resetTimestamp.toString());
1446
- if (!allowed) {
1447
- c.header("Retry-After", windowSeconds.toString());
1448
- return problems.rateLimitExceeded(c, windowSeconds);
1449
- }
1450
- await next();
1451
- });
1452
-
1453
- // src/middleware/require-verified-email.ts
1454
- import { createMiddleware as createMiddleware4 } from "hono/factory";
1455
- import { eq as eq5 } from "drizzle-orm";
1456
- var requireVerifiedEmail = createMiddleware4(
1457
- async (c, next) => {
1458
- const userId = c.get("userId");
1459
- if (!userId) {
1460
- return problems.unauthorized(c, "Not authenticated");
1461
- }
1462
- const { db, schema } = getAuthContext(c);
1463
- const database = db;
1464
- const users = schema.users;
1465
- const [user] = await database.select({ emailVerified: users.emailVerified }).from(users).where(eq5(users.id, userId)).limit(1);
1466
- if (!user || !user.emailVerified) {
1467
- return problems.forbidden(c, "Please verify your email address to continue");
1468
- }
1469
- await next();
1470
- }
1471
- );
1472
-
1473
- // src/routes/index.ts
1474
- import { OpenAPIHono as OpenAPIHono3 } from "@hono/zod-openapi";
1475
-
1476
- // src/routes/signup.ts
1477
- import { createRoute } from "@hono/zod-openapi";
1478
- import { eq as eq8 } from "drizzle-orm";
1479
-
1480
- // src/lib/email/email-service.ts
1481
- import { eq as eq6 } from "drizzle-orm";
1482
-
1483
- // src/lib/email/security.ts
1484
- function getAllowedDomains(appUrl) {
1485
- const base = ["localhost", "127.0.0.1"];
1486
- try {
1487
- const url = new URL(appUrl);
1488
- return [...base, url.hostname];
1489
- } catch {
1490
- return base;
1491
- }
1492
- }
1493
- function sanitizeEmailUrl(url, appUrl = "") {
1494
- let parsed;
1495
- try {
1496
- parsed = new URL(url);
1497
- } catch {
1498
- throw new Error("Invalid URL format");
1499
- }
1500
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1501
- throw new Error(`Disallowed protocol: ${parsed.protocol}`);
1502
- }
1503
- const allowedDomains = getAllowedDomains(appUrl);
1504
- const hostname = parsed.hostname.toLowerCase();
1505
- const isAllowed = allowedDomains.some((domain) => {
1506
- return hostname === domain || hostname.endsWith(`.${domain}`);
1507
- });
1508
- if (!isAllowed) {
1509
- throw new Error(`Disallowed domain: ${hostname}`);
1510
- }
1511
- return parsed.toString();
1512
- }
1513
- function escapeHtml(text) {
1514
- const htmlEscapeMap = {
1515
- "&": "&amp;",
1516
- "<": "&lt;",
1517
- ">": "&gt;",
1518
- '"': "&quot;",
1519
- "'": "&#39;"
1520
- };
1521
- return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char]);
988
+ function escapeHtml(text) {
989
+ const htmlEscapeMap = {
990
+ "&": "&amp;",
991
+ "<": "&lt;",
992
+ ">": "&gt;",
993
+ '"': "&quot;",
994
+ "'": "&#39;"
995
+ };
996
+ return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char]);
1522
997
  }
1523
998
 
1524
999
  // src/core/email-template.ts
@@ -1532,19 +1007,20 @@ function generateEmailTemplate(options) {
1532
1007
  securityWarning,
1533
1008
  footerNote,
1534
1009
  appUrl,
1535
- marketingUrl = appUrl,
1536
- // Default to appUrl if no marketing site
1010
+ marketingUrl,
1537
1011
  appName = "App",
1538
1012
  buttonBgColor = "#00fe9a",
1539
1013
  buttonTextColor = "#000004",
1540
1014
  buttonFontWeight = 500,
1541
1015
  buttonRadius = "6px",
1542
1016
  logoUrl,
1543
- footerColor = "#1e1b4b"
1017
+ footerColor = "#1e1b4b",
1018
+ headingColor = "#1e1b4b"
1544
1019
  } = options;
1020
+ const safeAppName = escapeHtml(appName);
1545
1021
  const safeButtonUrl = buttonUrl ? sanitizeEmailUrl(buttonUrl, appUrl) : "";
1546
1022
  const safeAppUrl = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
1547
- const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, appUrl) : "";
1023
+ const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, marketingUrl).replace(/\/$/, "") : "";
1548
1024
  const safeLogoUrl = logoUrl ? sanitizeEmailUrl(logoUrl, appUrl) : `${safeAppUrl.replace(/\/$/, "")}/email-header-image.png`;
1549
1025
  return `
1550
1026
  <!DOCTYPE html>
@@ -1685,7 +1161,7 @@ function generateEmailTemplate(options) {
1685
1161
  <td class="pad" style="padding-left:40px;padding-right:40px;width:100%;">
1686
1162
  <div class="alignment" align="center">
1687
1163
  <div class="fullWidth" style="max-width: 50%;">
1688
- <img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${appName}" title="${appName}" height="auto">
1164
+ <img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${safeAppName}" title="${safeAppName}" height="auto">
1689
1165
  </div>
1690
1166
  </div>
1691
1167
  </td>
@@ -1697,7 +1173,7 @@ function generateEmailTemplate(options) {
1697
1173
  <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;">
1698
1174
  <tr>
1699
1175
  <td class="pad" style="padding-top:30px;padding-left:40px;padding-right:40px;">
1700
- <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;">
1176
+ <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;">
1701
1177
  <p style="margin: 0; word-break: break-word;">${welcomeMessage}</p>
1702
1178
  </div>
1703
1179
  </td>
@@ -1710,7 +1186,7 @@ function generateEmailTemplate(options) {
1710
1186
  <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;">
1711
1187
  <tr>
1712
1188
  <td class="pad" style="padding-bottom:10px;padding-left:40px;padding-right:40px;padding-top:10px;">
1713
- <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;">
1189
+ <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;">
1714
1190
  <p style="margin: 0; word-break: break-word;">${heading}</p>
1715
1191
  </div>
1716
1192
  </td>
@@ -1755,135 +1231,809 @@ function generateEmailTemplate(options) {
1755
1231
  </tr>
1756
1232
  </table>
1757
1233
 
1758
- <!-- Copy Link -->
1759
- <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;">
1234
+ <!-- Copy Link -->
1235
+ <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;">
1236
+ <tr>
1237
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1238
+ <div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
1239
+ <p style="margin: 0 0 5px 0; word-break: break-word;">or copy and paste this link into your browser:</p>
1240
+ <a href="${safeButtonUrl}" style="color:#4a90d9;text-decoration:underline;word-break:break-all;">${safeButtonUrl}</a>
1241
+ </div>
1242
+ </td>
1243
+ </tr>
1244
+ </table>
1245
+ ` : ""}
1246
+
1247
+ ${securityWarning ? `
1248
+ <!-- Security Warning -->
1249
+ <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;">
1250
+ <tr>
1251
+ <td class="pad" style="padding-left:40px;padding-right:40px;padding-bottom:20px;">
1252
+ <div style="background-color: #a1f1ff; border-radius: 8px; padding: 16px; display: flex; align-items: center; justify-content: center;">
1253
+ <p style="margin: 0; color: #01001b; font-family: 'Montserrat', Arial, sans-serif; font-size: 13px; line-height: 1.5; text-align: center;">
1254
+ ${securityWarning}
1255
+ </p>
1256
+ </div>
1257
+ </td>
1258
+ </tr>
1259
+ </table>
1260
+ ` : ""}
1261
+
1262
+ <!-- Footer Note -->
1263
+ <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;">
1264
+ <tr>
1265
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1266
+ <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;">
1267
+ <p style="margin: 0; word-break: break-word;">${footerNote}</p>
1268
+ </div>
1269
+ </td>
1270
+ </tr>
1271
+ </table>
1272
+
1273
+ </td>
1274
+ </tr>
1275
+ </tbody>
1276
+ </table>
1277
+ </td>
1278
+ </tr>
1279
+ </tbody>
1280
+ </table>
1281
+
1282
+ <!-- Spacer Row -->
1283
+ <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;">
1284
+ <tbody>
1285
+ <tr>
1286
+ <td>
1287
+ <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;">
1288
+ <tbody>
1289
+ <tr>
1290
+ <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;">
1291
+ <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;">
1292
+ <tr>
1293
+ <td class="pad">
1294
+ <div></div>
1295
+ </td>
1296
+ </tr>
1297
+ </table>
1298
+ </td>
1299
+ </tr>
1300
+ </tbody>
1301
+ </table>
1302
+ </td>
1303
+ </tr>
1304
+ </tbody>
1305
+ </table>
1306
+
1307
+ <!-- Footer Row -->
1308
+ <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;">
1309
+ <tbody>
1310
+ <tr>
1311
+ <td>
1312
+ <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;">
1313
+ <tbody>
1314
+ <tr>
1315
+ <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;">
1316
+
1317
+ ${safeMarketingUrl ? `
1318
+ <!-- Policy Links -->
1319
+ <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;">
1320
+ <tr>
1321
+ <td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
1322
+ <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;">
1323
+ <p style="margin: 0; word-break: break-word;">
1324
+ <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>
1325
+ </p>
1326
+ </div>
1327
+ </td>
1328
+ </tr>
1329
+ </table>
1330
+ ` : ""}
1331
+
1332
+ <!-- Copyright & Disclaimer -->
1333
+ <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;">
1760
1334
  <tr>
1761
- <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1762
- <div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
1763
- <p style="margin: 0 0 5px 0; word-break: break-word;">or copy and paste this link into your browser:</p>
1764
- <a href="${safeButtonUrl}" style="color:#4a90d9;text-decoration:underline;word-break:break-all;">${safeButtonUrl}</a>
1335
+ <td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
1336
+ <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;">
1337
+ <p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
1338
+ <p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
1339
+ 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.
1340
+ </p>
1765
1341
  </div>
1766
1342
  </td>
1767
1343
  </tr>
1768
1344
  </table>
1769
- ` : ""}
1770
1345
 
1771
- ${securityWarning ? `
1772
- <!-- Security Warning -->
1773
- <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;">
1774
- <tr>
1775
- <td class="pad" style="padding-left:40px;padding-right:40px;padding-bottom:20px;">
1776
- <div style="background-color: #a1f1ff; border-radius: 8px; padding: 16px; display: flex; align-items: center; justify-content: center;">
1777
- <p style="margin: 0; color: #01001b; font-family: 'Montserrat', Arial, sans-serif; font-size: 13px; line-height: 1.5; text-align: center;">
1778
- ${securityWarning}
1779
- </p>
1780
- </div>
1781
- </td>
1782
- </tr>
1783
- </table>
1784
- ` : ""}
1346
+ </td>
1347
+ </tr>
1348
+ </tbody>
1349
+ </table>
1350
+ </td>
1351
+ </tr>
1352
+ </tbody>
1353
+ </table>
1354
+
1355
+ </td>
1356
+ </tr>
1357
+ </tbody>
1358
+ </table>
1359
+ </body>
1360
+ </html>
1361
+ `;
1362
+ }
1363
+
1364
+ // src/core/passwordless.ts
1365
+ var PASSWORDLESS_TTL_MS = 10 * 60 * 1e3;
1366
+ var PASSWORDLESS_MAX_ATTEMPTS = 5;
1367
+ var CODE_SPACE = 1e6;
1368
+ var CODE_LIMIT = Math.floor(4294967296 / CODE_SPACE) * CODE_SPACE;
1369
+ var CLEANUP_GRACE_MS = 24 * 60 * 60 * 1e3;
1370
+ var normalizeEmail = (email) => email.trim().toLowerCase();
1371
+ function generateCode() {
1372
+ const buffer = new Uint32Array(1);
1373
+ for (; ; ) {
1374
+ crypto.getRandomValues(buffer);
1375
+ if (buffer[0] < CODE_LIMIT) return String(buffer[0] % CODE_SPACE).padStart(6, "0");
1376
+ }
1377
+ }
1378
+ var codeHash = (challengeId, code) => hashToken(`${challengeId}:${code}`);
1379
+ function constantTimeEqual(a, b) {
1380
+ if (a.length !== b.length) return false;
1381
+ let diff = 0;
1382
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1383
+ return diff === 0;
1384
+ }
1385
+ async function consumeOpen(db, t, email, now) {
1386
+ await db.update(t).set({ consumedAt: now }).where(and2(eq4(t.email, email), isNull(t.consumedAt)));
1387
+ }
1388
+ async function createPasswordlessChallenge(db, tables2, input) {
1389
+ const t = tables2.passwordlessChallenges;
1390
+ const email = normalizeEmail(input.email);
1391
+ const now = Date.now();
1392
+ const challengeId = crypto.randomUUID();
1393
+ const linkToken = generateSecureToken(32);
1394
+ const browserSecret = generateSecureToken(32);
1395
+ const code = generateCode();
1396
+ await consumeOpen(db, t, email, now);
1397
+ await db.insert(t).values({
1398
+ id: challengeId,
1399
+ email,
1400
+ subjectId: input.subjectId ?? null,
1401
+ linkTokenHash: await hashToken(linkToken),
1402
+ codeHash: await codeHash(challengeId, code),
1403
+ browserHash: await hashToken(browserSecret),
1404
+ attempts: 0,
1405
+ payload: input.payload ?? null,
1406
+ expiresAt: now + (input.ttlMs ?? PASSWORDLESS_TTL_MS),
1407
+ createdAt: now
1408
+ });
1409
+ return { challengeId, linkToken, code, browserSecret };
1410
+ }
1411
+ async function verifyPasswordlessLink(db, tables2, input) {
1412
+ const t = tables2.passwordlessChallenges;
1413
+ const now = Date.now();
1414
+ const hash = await hashToken(input.linkToken);
1415
+ const [row] = await db.update(t).set({ consumedAt: now }).where(and2(eq4(t.linkTokenHash, hash), isNull(t.consumedAt), gt2(t.expiresAt, now))).returning();
1416
+ if (!row) {
1417
+ const [stale] = await db.select({ expiresAt: t.expiresAt, consumedAt: t.consumedAt }).from(t).where(eq4(t.linkTokenHash, hash)).limit(1);
1418
+ const expired = stale && stale.consumedAt === null && stale.expiresAt <= now;
1419
+ return { ok: false, reason: expired ? "expired" : "invalid" };
1420
+ }
1421
+ await consumeOpen(db, t, row.email, now);
1422
+ return { ok: true, email: row.email, subjectId: row.subjectId, payload: row.payload };
1423
+ }
1424
+ async function verifyPasswordlessCode(db, tables2, input) {
1425
+ const t = tables2.passwordlessChallenges;
1426
+ const max = input.maxAttempts ?? PASSWORDLESS_MAX_ATTEMPTS;
1427
+ const email = normalizeEmail(input.email);
1428
+ const now = Date.now();
1429
+ const [row] = await db.select().from(t).where(and2(eq4(t.email, email), isNull(t.consumedAt))).orderBy(desc(t.createdAt)).limit(1);
1430
+ if (!row) return { ok: false, reason: "invalid" };
1431
+ if (!constantTimeEqual(row.browserHash, await hashToken(input.browserSecret))) {
1432
+ return { ok: false, reason: "invalid" };
1433
+ }
1434
+ if (row.expiresAt <= now) return { ok: false, reason: "expired" };
1435
+ if (row.attempts >= max) return { ok: false, reason: "locked" };
1436
+ if (!constantTimeEqual(row.codeHash, await codeHash(row.id, input.code.trim()))) {
1437
+ const [counted] = await db.update(t).set({ attempts: sql2`${t.attempts} + 1` }).where(and2(eq4(t.id, row.id), lt(t.attempts, max))).returning({ attempts: t.attempts });
1438
+ return { ok: false, reason: counted && counted.attempts < max ? "invalid" : "locked" };
1439
+ }
1440
+ const [won] = await db.update(t).set({ consumedAt: now }).where(and2(eq4(t.id, row.id), isNull(t.consumedAt), lt(t.attempts, max))).returning();
1441
+ if (!won) return { ok: false, reason: "invalid" };
1442
+ await consumeOpen(db, t, email, now);
1443
+ return { ok: true, email, subjectId: won.subjectId, payload: won.payload };
1444
+ }
1445
+ async function recentPasswordlessFailures(db, tables2, input) {
1446
+ const t = tables2.passwordlessChallenges;
1447
+ const [row] = await db.select({ total: sql2`coalesce(sum(${t.attempts}), 0)::int` }).from(t).where(and2(eq4(t.email, normalizeEmail(input.email)), gt2(t.createdAt, Date.now() - input.windowMs)));
1448
+ return Number(row?.total ?? 0);
1449
+ }
1450
+ async function cleanupExpiredPasswordlessChallenges(db, tables2) {
1451
+ const t = tables2.passwordlessChallenges;
1452
+ await db.delete(t).where(lt(t.expiresAt, Date.now() - CLEANUP_GRACE_MS));
1453
+ }
1454
+ function passwordlessEmail(input) {
1455
+ const { appName, appUrl, linkUrl, code } = input;
1456
+ const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
1457
+ const lifetime2 = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1458
+ const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
1459
+ const html = generateEmailTemplate({
1460
+ heading: `Sign in to ${escapeHtml(appName)}`,
1461
+ body: `<p>Use this code, or the button below, to sign in:</p>
1462
+ <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
1463
+ <p>The code and the link expire in ${lifetime2}.</p>`,
1464
+ buttonText: "Sign in",
1465
+ buttonUrl: safeLink,
1466
+ footerNote: "If you didn't ask to sign in, you can ignore this email.",
1467
+ appUrl,
1468
+ appName
1469
+ });
1470
+ return {
1471
+ subject: `${code} is your sign-in code`,
1472
+ html,
1473
+ text: `Your ${appName} sign-in code is ${code}.
1474
+
1475
+ Or sign in with this link:
1476
+ ${safeLink}
1477
+
1478
+ The code and the link expire in ${lifetime2}.
1479
+
1480
+ If you didn't ask to sign in, you can ignore this email.`
1481
+ };
1482
+ }
1483
+
1484
+ // src/middleware/auth.ts
1485
+ import { createMiddleware } from "hono/factory";
1486
+ import { eq as eq5, and as and3, gt as gt3 } from "drizzle-orm";
1487
+
1488
+ // src/factory.ts
1489
+ function createAuth(config) {
1490
+ const { db, schema, getEnv, emailBranding, invitations, signup = "open" } = config;
1491
+ if (signup === "invitation" && !invitations) {
1492
+ throw new Error("createAuth: signup: 'invitation' requires the invitations hooks");
1493
+ }
1494
+ const getContext = (c) => ({
1495
+ db,
1496
+ schema,
1497
+ env: getEnv(c),
1498
+ emailBranding,
1499
+ invitations,
1500
+ signup
1501
+ });
1502
+ return {
1503
+ db,
1504
+ schema,
1505
+ getContext
1506
+ };
1507
+ }
1508
+ function createAuthMiddleware(auth2) {
1509
+ return async (c, next) => {
1510
+ const ctx = auth2.getContext(c);
1511
+ c.set("authContext", ctx);
1512
+ c.set("db", auth2.db);
1513
+ await next();
1514
+ };
1515
+ }
1516
+ function getAuthContext(c) {
1517
+ const ctx = c.get("authContext");
1518
+ if (!ctx) {
1519
+ throw new Error(
1520
+ "AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
1521
+ );
1522
+ }
1523
+ return ctx;
1524
+ }
1525
+
1526
+ // src/lib/problem-json.ts
1527
+ var ProblemTypes = {
1528
+ // 4xx Client Errors
1529
+ BAD_REQUEST: "/errors/bad-request",
1530
+ UNAUTHORIZED: "/errors/unauthorized",
1531
+ FORBIDDEN: "/errors/forbidden",
1532
+ NOT_FOUND: "/errors/not-found",
1533
+ CONFLICT: "/errors/conflict",
1534
+ GONE: "/errors/gone",
1535
+ VALIDATION_ERROR: "/errors/validation-error",
1536
+ RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
1537
+ // 5xx Server Errors
1538
+ INTERNAL_ERROR: "/errors/internal-error",
1539
+ SERVICE_UNAVAILABLE: "/errors/service-unavailable"
1540
+ };
1541
+ function generateTraceId() {
1542
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
1543
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1544
+ }
1545
+ function createProblemDetails(type, title, status, options) {
1546
+ const problem = {
1547
+ type,
1548
+ title,
1549
+ status
1550
+ };
1551
+ if (options?.detail) {
1552
+ problem.detail = options.detail;
1553
+ }
1554
+ if (options?.instance) {
1555
+ problem.instance = options.instance;
1556
+ }
1557
+ problem.traceId = options?.traceId || generateTraceId();
1558
+ if (options?.requestId) {
1559
+ problem.requestId = options.requestId;
1560
+ }
1561
+ if (options?.extensions) {
1562
+ Object.assign(problem, options.extensions);
1563
+ }
1564
+ return problem;
1565
+ }
1566
+ function problemJson(c, problem) {
1567
+ logger_default.warn("API error response", {
1568
+ type: problem.type,
1569
+ status: problem.status,
1570
+ traceId: problem.traceId,
1571
+ requestId: problem.requestId,
1572
+ path: c.req.path,
1573
+ method: c.req.method
1574
+ });
1575
+ return c.json(problem, problem.status, {
1576
+ "Content-Type": "application/problem+json"
1577
+ });
1578
+ }
1579
+ var problems = {
1580
+ /**
1581
+ * 400 Bad Request - Generic client error
1582
+ */
1583
+ badRequest(c, detail) {
1584
+ return problemJson(
1585
+ c,
1586
+ createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
1587
+ detail: detail || "The request could not be understood by the server",
1588
+ instance: `${c.req.method} ${c.req.path}`,
1589
+ requestId: c.get("requestId"),
1590
+ traceId: c.get("traceId")
1591
+ })
1592
+ );
1593
+ },
1594
+ /**
1595
+ * 400 Bad Request - Validation errors with field-specific details
1596
+ */
1597
+ validationError(c, detail, errors) {
1598
+ return problemJson(
1599
+ c,
1600
+ createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
1601
+ detail,
1602
+ instance: `${c.req.method} ${c.req.path}`,
1603
+ requestId: c.get("requestId"),
1604
+ traceId: c.get("traceId"),
1605
+ extensions: errors ? { errors } : void 0
1606
+ })
1607
+ );
1608
+ },
1609
+ /**
1610
+ * 401 Unauthorized - Authentication required
1611
+ */
1612
+ unauthorized(c, detail) {
1613
+ return problemJson(
1614
+ c,
1615
+ createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
1616
+ detail: detail || "Authentication is required to access this resource",
1617
+ instance: `${c.req.method} ${c.req.path}`,
1618
+ requestId: c.get("requestId"),
1619
+ traceId: c.get("traceId")
1620
+ })
1621
+ );
1622
+ },
1623
+ /**
1624
+ * 403 Forbidden - Insufficient permissions
1625
+ */
1626
+ forbidden(c, detail) {
1627
+ return problemJson(
1628
+ c,
1629
+ createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
1630
+ detail: detail || "You do not have permission to access this resource",
1631
+ instance: `${c.req.method} ${c.req.path}`,
1632
+ requestId: c.get("requestId"),
1633
+ traceId: c.get("traceId")
1634
+ })
1635
+ );
1636
+ },
1637
+ /**
1638
+ * 404 Not Found
1639
+ */
1640
+ notFound(c, detail) {
1641
+ return problemJson(
1642
+ c,
1643
+ createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
1644
+ detail: detail || "The requested resource was not found",
1645
+ instance: `${c.req.method} ${c.req.path}`,
1646
+ requestId: c.get("requestId"),
1647
+ traceId: c.get("traceId")
1648
+ })
1649
+ );
1650
+ },
1651
+ /**
1652
+ * 409 Conflict - Resource conflict (e.g., duplicate email)
1653
+ */
1654
+ conflict(c, detail) {
1655
+ return problemJson(
1656
+ c,
1657
+ createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
1658
+ detail,
1659
+ instance: `${c.req.method} ${c.req.path}`,
1660
+ requestId: c.get("requestId"),
1661
+ traceId: c.get("traceId")
1662
+ })
1663
+ );
1664
+ },
1665
+ /**
1666
+ * 410 Gone - Resource is no longer available
1667
+ */
1668
+ gone(c, detail) {
1669
+ return problemJson(
1670
+ c,
1671
+ createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
1672
+ detail: detail || "The requested resource is no longer available",
1673
+ instance: `${c.req.method} ${c.req.path}`,
1674
+ requestId: c.get("requestId"),
1675
+ traceId: c.get("traceId")
1676
+ })
1677
+ );
1678
+ },
1679
+ /**
1680
+ * 429 Too Many Requests - Rate limit exceeded
1681
+ */
1682
+ rateLimitExceeded(c, retryAfter) {
1683
+ const extensions = retryAfter ? { retryAfter } : void 0;
1684
+ return problemJson(
1685
+ c,
1686
+ createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
1687
+ detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
1688
+ instance: `${c.req.method} ${c.req.path}`,
1689
+ requestId: c.get("requestId"),
1690
+ traceId: c.get("traceId"),
1691
+ extensions
1692
+ })
1693
+ );
1694
+ },
1695
+ /**
1696
+ * 500 Internal Server Error - Generic server error
1697
+ * SECURITY: Never include error details or stack traces
1698
+ */
1699
+ internalError(c, error) {
1700
+ const traceId = c.get("traceId") || generateTraceId();
1701
+ if (error) {
1702
+ logError(error, {
1703
+ context: "internal_error",
1704
+ path: c.req.path,
1705
+ method: c.req.method,
1706
+ traceId,
1707
+ requestId: c.get("requestId")
1708
+ });
1709
+ }
1710
+ return problemJson(
1711
+ c,
1712
+ createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
1713
+ detail: "An unexpected error occurred. Please try again later",
1714
+ instance: `${c.req.method} ${c.req.path}`,
1715
+ traceId,
1716
+ requestId: c.get("requestId")
1717
+ })
1718
+ );
1719
+ },
1720
+ /**
1721
+ * 503 Service Unavailable - Service is temporarily unavailable
1722
+ */
1723
+ serviceUnavailable(c, detail) {
1724
+ return problemJson(
1725
+ c,
1726
+ createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
1727
+ detail: detail || "The service is temporarily unavailable. Please try again later",
1728
+ instance: `${c.req.method} ${c.req.path}`,
1729
+ requestId: c.get("requestId"),
1730
+ traceId: c.get("traceId")
1731
+ })
1732
+ );
1733
+ }
1734
+ };
1735
+
1736
+ // src/middleware/auth.ts
1737
+ var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
1738
+ async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
1739
+ if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
1740
+ logger_default.info("Session exceeded absolute lifetime - revoked", {
1741
+ type: "security",
1742
+ event: "session_max_lifetime_exceeded",
1743
+ severity: "low",
1744
+ sessionId: sessionId.slice(0, 8),
1745
+ userId: session.userId
1746
+ });
1747
+ await deleteSession(db, { sessions }, sessionId);
1748
+ return true;
1749
+ }
1750
+ async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
1751
+ const cookieName = getSessionCookieName(env);
1752
+ const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
1753
+ const sessionIdMatch = cookieHeader.match(cookiePattern);
1754
+ if (!sessionIdMatch) return null;
1755
+ const sessionId = sessionIdMatch[1];
1756
+ const foundSessions = await db.select({
1757
+ userId: sessions.userId,
1758
+ expiresAt: sessions.expiresAt,
1759
+ createdAt: sessions.createdAt,
1760
+ lastActiveAt: sessions.lastActiveAt,
1761
+ fingerprint: sessions.fingerprint
1762
+ }).from(sessions).where(and3(eq5(sessions.id, sessionId), gt3(sessions.expiresAt, Date.now()))).limit(1);
1763
+ if (foundSessions.length === 0) return null;
1764
+ const session = foundSessions[0];
1765
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1766
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
1767
+ const userAgent = request.headers.get("user-agent") || "";
1768
+ const cfWorker = request.headers.get("cf-worker") || "";
1769
+ const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
1770
+ const isLocalSSR = userAgent.toLowerCase().includes("node");
1771
+ const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
1772
+ const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
1773
+ const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
1774
+ if (isSSR) {
1775
+ logger_default.info("SSR request detected, using IP validation", {
1776
+ type: "security",
1777
+ event: "ssr_session_validation",
1778
+ sessionId: sessionId.slice(0, 8),
1779
+ userId: session.userId,
1780
+ clientIp,
1781
+ cfWorker,
1782
+ isLocalSSR,
1783
+ isSvelteKitSSR,
1784
+ isCloudflareSSR
1785
+ });
1786
+ } else {
1787
+ if (session.fingerprint) {
1788
+ const currentFingerprint = await generateFingerprint(request);
1789
+ if (session.fingerprint !== currentFingerprint) {
1790
+ logger_default.info("Session fingerprint changed (not enforced)", {
1791
+ type: "security",
1792
+ event: "fingerprint_changed",
1793
+ sessionId: sessionId.slice(0, 8),
1794
+ userId: session.userId,
1795
+ clientIp
1796
+ });
1797
+ }
1798
+ }
1799
+ }
1800
+ return {
1801
+ userId: session.userId,
1802
+ expiresAt: session.expiresAt
1803
+ };
1804
+ }
1805
+ async function getSessionFromBearerToken(db, sessions, authHeader) {
1806
+ if (!authHeader.startsWith("Bearer ")) return null;
1807
+ const sessionId = authHeader.slice(7);
1808
+ const foundSessions = await db.select({
1809
+ userId: sessions.userId,
1810
+ expiresAt: sessions.expiresAt,
1811
+ createdAt: sessions.createdAt,
1812
+ lastActiveAt: sessions.lastActiveAt
1813
+ }).from(sessions).where(and3(eq5(sessions.id, sessionId), gt3(sessions.expiresAt, Date.now()))).limit(1);
1814
+ if (foundSessions.length === 0) return null;
1815
+ const session = foundSessions[0];
1816
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1817
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
1818
+ return {
1819
+ userId: session.userId,
1820
+ expiresAt: session.expiresAt
1821
+ };
1822
+ }
1823
+ var requireAuth = createMiddleware(
1824
+ async (c, next) => {
1825
+ const { db, schema } = getAuthContext(c);
1826
+ const authHeader = c.req.header("Authorization");
1827
+ if (authHeader) {
1828
+ const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1829
+ if (session2) {
1830
+ c.set("userId", session2.userId);
1831
+ await next();
1832
+ return;
1833
+ }
1834
+ }
1835
+ const cookieHeader = c.req.header("cookie");
1836
+ if (!cookieHeader) {
1837
+ return problems.unauthorized(c, "Please log in to continue");
1838
+ }
1839
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1840
+ if (!session) {
1841
+ c.header("Set-Cookie", clearSessionCookie(c.env));
1842
+ return problems.unauthorized(c, "Your session has expired. Please log in again.");
1843
+ }
1844
+ c.set("userId", session.userId);
1845
+ await next();
1846
+ }
1847
+ );
1848
+ var optionalAuth = createMiddleware(
1849
+ async (c, next) => {
1850
+ const { db, schema } = getAuthContext(c);
1851
+ const authHeader = c.req.header("Authorization");
1852
+ if (authHeader) {
1853
+ const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1854
+ if (session) {
1855
+ c.set("userId", session.userId);
1856
+ await next();
1857
+ return;
1858
+ }
1859
+ }
1860
+ const cookieHeader = c.req.header("cookie");
1861
+ if (cookieHeader) {
1862
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1863
+ if (session) {
1864
+ c.set("userId", session.userId);
1865
+ }
1866
+ }
1867
+ await next();
1868
+ }
1869
+ );
1785
1870
 
1786
- <!-- Footer Note -->
1787
- <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;">
1788
- <tr>
1789
- <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1790
- <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;">
1791
- <p style="margin: 0; word-break: break-word;">${footerNote}</p>
1792
- </div>
1793
- </td>
1794
- </tr>
1795
- </table>
1871
+ // src/middleware/csrf.ts
1872
+ import { createMiddleware as createMiddleware2 } from "hono/factory";
1873
+ var csrf = createMiddleware2(async (c, next) => {
1874
+ const method = c.req.method;
1875
+ const path = c.req.path;
1876
+ const isWebhook = path.startsWith("/v1/webhooks/");
1877
+ if (isWebhook) {
1878
+ await next();
1879
+ return;
1880
+ }
1881
+ const isMcpEndpoint = path === "/v1/mcp";
1882
+ if (isMcpEndpoint) {
1883
+ await next();
1884
+ return;
1885
+ }
1886
+ const isOAuthMcpEndpoint = path.startsWith("/v1/oauth/");
1887
+ if (isOAuthMcpEndpoint) {
1888
+ await next();
1889
+ return;
1890
+ }
1891
+ const isToolsApi = path.startsWith("/v1/tools/") && !path.startsWith("/v1/tools/catalog");
1892
+ if (isToolsApi) {
1893
+ await next();
1894
+ return;
1895
+ }
1896
+ const isAdminApi = path.startsWith("/v1/admin/");
1897
+ if (isAdminApi) {
1898
+ await next();
1899
+ return;
1900
+ }
1901
+ const isMobileAuthEndpoint = path === "/v1/auth/login" || path === "/v1/auth/signup" || path === "/v1/auth/refresh";
1902
+ const authHeader = c.req.header("Authorization");
1903
+ const hasBearerToken = authHeader?.startsWith("Bearer ");
1904
+ const hasNoOrigin = !c.req.header("origin") && !c.req.header("referer");
1905
+ if (isMobileAuthEndpoint && hasNoOrigin || hasBearerToken) {
1906
+ await next();
1907
+ return;
1908
+ }
1909
+ const isInternalSync = path.startsWith("/v1/internal/sync/");
1910
+ if (isInternalSync) {
1911
+ await next();
1912
+ return;
1913
+ }
1914
+ if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
1915
+ const appUrl = c.env.APP_URL || "http://localhost:5173";
1916
+ const allowedOrigins = getAllowedOrigins(appUrl);
1917
+ const request = c.req.raw;
1918
+ const isTestEndpoint = c.req.path.startsWith("/test");
1919
+ if (isTestEndpoint) {
1920
+ const origin = request.headers.get("origin");
1921
+ const referer = request.headers.get("referer");
1922
+ logger_default.debug("CSRF validation for test endpoint", {
1923
+ path: c.req.path,
1924
+ method,
1925
+ origin,
1926
+ referer,
1927
+ appUrl,
1928
+ allowedOrigins
1929
+ });
1930
+ }
1931
+ const pagesPattern = c.env.CORS_PAGES_PATTERN;
1932
+ if (!validateOrigin(request, allowedOrigins, pagesPattern)) {
1933
+ return problems.forbidden(c, "Request blocked for security reasons. Please try again.");
1934
+ }
1935
+ }
1936
+ await next();
1937
+ });
1796
1938
 
1797
- </td>
1798
- </tr>
1799
- </tbody>
1800
- </table>
1801
- </td>
1802
- </tr>
1803
- </tbody>
1804
- </table>
1939
+ // src/middleware/rateLimit.ts
1940
+ import { createMiddleware as createMiddleware3 } from "hono/factory";
1805
1941
 
1806
- <!-- Spacer Row -->
1807
- <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;">
1808
- <tbody>
1809
- <tr>
1810
- <td>
1811
- <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;">
1812
- <tbody>
1813
- <tr>
1814
- <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;">
1815
- <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;">
1816
- <tr>
1817
- <td class="pad">
1818
- <div></div>
1819
- </td>
1820
- </tr>
1821
- </table>
1822
- </td>
1823
- </tr>
1824
- </tbody>
1825
- </table>
1826
- </td>
1827
- </tr>
1828
- </tbody>
1829
- </table>
1942
+ // src/lib/rate-limit-kv.ts
1943
+ async function checkRateLimitKV(kv, key, maxAttempts, windowSeconds) {
1944
+ try {
1945
+ const rateLimitKey = `rate_limit:${key}`;
1946
+ const currentAttemptsStr = await kv.get(rateLimitKey);
1947
+ const currentAttempts = currentAttemptsStr ? parseInt(currentAttemptsStr, 10) : 0;
1948
+ if (currentAttempts >= maxAttempts) {
1949
+ return {
1950
+ allowed: false,
1951
+ remaining: 0
1952
+ };
1953
+ }
1954
+ const newAttempts = currentAttempts + 1;
1955
+ await kv.put(rateLimitKey, newAttempts.toString(), {
1956
+ expirationTtl: windowSeconds
1957
+ });
1958
+ return {
1959
+ allowed: true,
1960
+ remaining: maxAttempts - newAttempts
1961
+ };
1962
+ } catch (error) {
1963
+ logger_default.error("Rate limit KV error, failing open", {
1964
+ key,
1965
+ error: error instanceof Error ? error.message : "Unknown error"
1966
+ });
1967
+ return {
1968
+ allowed: true,
1969
+ remaining: maxAttempts
1970
+ };
1971
+ }
1972
+ }
1830
1973
 
1831
- <!-- Footer Row -->
1832
- <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;">
1833
- <tbody>
1834
- <tr>
1835
- <td>
1836
- <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;">
1837
- <tbody>
1838
- <tr>
1839
- <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;">
1974
+ // src/middleware/rateLimit.ts
1975
+ var rateLimit = (config) => createMiddleware3(async (c, next) => {
1976
+ const kv = c.env.RATE_LIMIT_KV;
1977
+ const identifier = await config.identifier(c);
1978
+ const key = `${config.action}:${identifier}`;
1979
+ const windowSeconds = Math.ceil(config.windowMs / 1e3);
1980
+ const { allowed, remaining } = await checkRateLimitKV(
1981
+ kv,
1982
+ key,
1983
+ config.maxAttempts,
1984
+ windowSeconds
1985
+ );
1986
+ const resetTimestamp = Math.ceil(Date.now() / 1e3) + windowSeconds;
1987
+ c.header("X-RateLimit-Limit", config.maxAttempts.toString());
1988
+ c.header("X-RateLimit-Remaining", Math.max(0, remaining).toString());
1989
+ c.header("X-RateLimit-Reset", resetTimestamp.toString());
1990
+ if (!allowed) {
1991
+ c.header("Retry-After", windowSeconds.toString());
1992
+ return problems.rateLimitExceeded(c, windowSeconds);
1993
+ }
1994
+ await next();
1995
+ });
1840
1996
 
1841
- <!-- Policy Links -->
1842
- <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;">
1843
- <tr>
1844
- <td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
1845
- <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;">
1846
- <p style="margin: 0; word-break: break-word;">
1847
- <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>
1848
- </p>
1849
- </div>
1850
- </td>
1851
- </tr>
1852
- </table>
1997
+ // src/middleware/require-verified-email.ts
1998
+ import { createMiddleware as createMiddleware4 } from "hono/factory";
1999
+ import { eq as eq6 } from "drizzle-orm";
2000
+ var requireVerifiedEmail = createMiddleware4(
2001
+ async (c, next) => {
2002
+ const userId = c.get("userId");
2003
+ if (!userId) {
2004
+ return problems.unauthorized(c, "Not authenticated");
2005
+ }
2006
+ const { db, schema } = getAuthContext(c);
2007
+ const database = db;
2008
+ const users = schema.users;
2009
+ const [user] = await database.select({ emailVerified: users.emailVerified }).from(users).where(eq6(users.id, userId)).limit(1);
2010
+ if (!user || !user.emailVerified) {
2011
+ return problems.forbidden(c, "Please verify your email address to continue");
2012
+ }
2013
+ await next();
2014
+ }
2015
+ );
1853
2016
 
1854
- <!-- Copyright & Disclaimer -->
1855
- <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;">
1856
- <tr>
1857
- <td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
1858
- <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;">
1859
- <p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${appName}. All rights reserved.</p>
1860
- <p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
1861
- 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.
1862
- </p>
1863
- </div>
1864
- </td>
1865
- </tr>
1866
- </table>
2017
+ // src/routes/index.ts
2018
+ import { OpenAPIHono as OpenAPIHono3 } from "@hono/zod-openapi";
1867
2019
 
1868
- </td>
1869
- </tr>
1870
- </tbody>
1871
- </table>
1872
- </td>
1873
- </tr>
1874
- </tbody>
1875
- </table>
2020
+ // src/routes/signup.ts
2021
+ import { createRoute } from "@hono/zod-openapi";
2022
+ import { eq as eq9 } from "drizzle-orm";
1876
2023
 
1877
- </td>
1878
- </tr>
1879
- </tbody>
1880
- </table>
1881
- </body>
1882
- </html>
1883
- `;
1884
- }
2024
+ // src/lib/email/email-service.ts
2025
+ import { eq as eq7 } from "drizzle-orm";
1885
2026
 
1886
2027
  // src/lib/email/templates.ts
2028
+ function lifetime(minutes) {
2029
+ if (minutes % 60 !== 0) return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
2030
+ const hours = minutes / 60;
2031
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
2032
+ }
2033
+ var VERIFICATION_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60);
2034
+ var PASSWORD_RESET_LIFETIME = lifetime(AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES);
2035
+ var EMAIL_CHANGE_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60);
2036
+ var TWO_FACTOR_CODE_LIFETIME = lifetime(AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES);
1887
2037
  function extractAppUrl(url) {
1888
2038
  try {
1889
2039
  const parsed = new URL(url);
@@ -1899,12 +2049,13 @@ function generateVerificationEmail(data, appName = "Your App", configuredAppUrl
1899
2049
  const safeFirstName = firstName ? escapeHtml(firstName) : null;
1900
2050
  const html = generateEmailTemplate({
1901
2051
  heading: "",
1902
- body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}.`,
2052
+ body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}. This link expires in ${VERIFICATION_LIFETIME}.`,
1903
2053
  buttonText: "Verify Email",
1904
2054
  buttonUrl: safeUrl,
1905
2055
  welcomeMessage: safeFirstName ? `Hi ${safeFirstName}, Welcome to ${escapeHtml(appName)}!` : void 0,
1906
2056
  footerNote: "",
1907
2057
  appUrl,
2058
+ appName,
1908
2059
  ...branding
1909
2060
  });
1910
2061
  return {
@@ -1917,7 +2068,7 @@ Thank you for signing up for ${appName}! Please verify your email address by cli
1917
2068
 
1918
2069
  ${safeUrl}
1919
2070
 
1920
- This link expires in 48 hours.
2071
+ This link expires in ${VERIFICATION_LIFETIME}.
1921
2072
 
1922
2073
  If you didn't create an account with ${appName}, you can safely ignore this email.`,
1923
2074
  tags: {
@@ -1935,11 +2086,12 @@ function generatePasswordResetEmail(data, appName = "Your App", configuredAppUrl
1935
2086
  buttonText: "Reset Password",
1936
2087
  buttonUrl: safeUrl,
1937
2088
  securityWarning: `
1938
- <strong>Link expires in 1 hour.</strong><br>
2089
+ <strong>Link expires in ${PASSWORD_RESET_LIFETIME}.</strong><br>
1939
2090
  For security, this link can only be used once.
1940
2091
  `,
1941
2092
  footerNote: "If you didn't request this change, no action is needed. Your password will remain unchanged.",
1942
2093
  appUrl,
2094
+ appName,
1943
2095
  ...branding
1944
2096
  });
1945
2097
  return {
@@ -1952,7 +2104,7 @@ We received a request to reset your password. Click the link below to create a n
1952
2104
 
1953
2105
  ${safeUrl}
1954
2106
 
1955
- This link will expire in 1 hour.
2107
+ This link expires in ${PASSWORD_RESET_LIFETIME}.
1956
2108
 
1957
2109
  If you didn't request a password reset, you can safely ignore this email.`,
1958
2110
  tags: {
@@ -1970,11 +2122,12 @@ function generateEmailChangeConfirmation(data, appName = "Your App", configuredA
1970
2122
  buttonText: "Confirm Email Change",
1971
2123
  buttonUrl: safeUrl,
1972
2124
  securityWarning: `
1973
- <strong>This link expires in 24 hours.</strong><br>
2125
+ <strong>This link expires in ${EMAIL_CHANGE_LIFETIME}.</strong><br>
1974
2126
  For security, this link can only be used once.
1975
2127
  `,
1976
2128
  footerNote: "If you didn't request this change, you can safely ignore this email.",
1977
2129
  appUrl,
2130
+ appName,
1978
2131
  ...branding
1979
2132
  });
1980
2133
  return {
@@ -1989,7 +2142,7 @@ Click the link below to confirm this change:
1989
2142
 
1990
2143
  ${safeUrl}
1991
2144
 
1992
- This link expires in 24 hours.
2145
+ This link expires in ${EMAIL_CHANGE_LIFETIME}.
1993
2146
 
1994
2147
  If you didn't request this change, you can safely ignore this email.`,
1995
2148
  tags: {
@@ -2013,6 +2166,7 @@ function generateEmailChangeNotification(data, appName = "Your App", configuredA
2013
2166
  `,
2014
2167
  footerNote: "If you didn't request this change, click the button above to cancel it.",
2015
2168
  appUrl,
2169
+ appName,
2016
2170
  ...branding
2017
2171
  });
2018
2172
  return {
@@ -2042,13 +2196,14 @@ function generate2faCodeEmail(data, appName = "Your App", appUrl = "", branding)
2042
2196
  <p>Hi ${safeFirstName},</p>
2043
2197
  <p>Your verification code is:</p>
2044
2198
  <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
2045
- <p>This code expires in 5 minutes.</p>
2199
+ <p>This code expires in ${TWO_FACTOR_CODE_LIFETIME}.</p>
2046
2200
  <p>If you didn't request this code, you can safely ignore this email.</p>
2047
2201
  `,
2048
2202
  buttonText: "",
2049
2203
  buttonUrl: "",
2050
2204
  footerNote: "",
2051
2205
  appUrl,
2206
+ appName,
2052
2207
  ...branding
2053
2208
  });
2054
2209
  return {
@@ -2061,7 +2216,7 @@ Your verification code is:
2061
2216
 
2062
2217
  ${code}
2063
2218
 
2064
- This code expires in 5 minutes.
2219
+ This code expires in ${TWO_FACTOR_CODE_LIFETIME}.
2065
2220
 
2066
2221
  If you didn't request this code, you can safely ignore this email.
2067
2222
 
@@ -2084,6 +2239,7 @@ function generate2faEnabledEmail(data, appName = "Your App", appUrl = "", brandi
2084
2239
  buttonUrl: "",
2085
2240
  footerNote: "",
2086
2241
  appUrl,
2242
+ appName,
2087
2243
  ...branding
2088
2244
  });
2089
2245
  return {
@@ -2114,6 +2270,7 @@ function generate2faDisabledEmail(data, appName = "Your App", appUrl = "", brand
2114
2270
  buttonUrl: appUrl ? `${appUrl}/forgot-password` : "",
2115
2271
  footerNote: "",
2116
2272
  appUrl,
2273
+ appName,
2117
2274
  ...branding
2118
2275
  });
2119
2276
  return {
@@ -2330,7 +2487,7 @@ var EmailService = class {
2330
2487
  const [user] = await db.select({
2331
2488
  emailBounced: usersTable.emailBounced,
2332
2489
  emailComplained: usersTable.emailComplained
2333
- }).from(usersTable).where(eq6(usersTable.email, emailAddress.toLowerCase())).limit(1);
2490
+ }).from(usersTable).where(eq7(usersTable.email, emailAddress.toLowerCase())).limit(1);
2334
2491
  if (!user) {
2335
2492
  return { blocked: false };
2336
2493
  }
@@ -2779,7 +2936,7 @@ var EmailService = class {
2779
2936
  import { Resend as Resend2 } from "resend";
2780
2937
 
2781
2938
  // src/lib/email/webhook-handler.ts
2782
- import { eq as eq7 } from "drizzle-orm";
2939
+ import { eq as eq8 } from "drizzle-orm";
2783
2940
  async function handleWebhookEvent(payload, db, tables2) {
2784
2941
  const { type, data } = payload;
2785
2942
  const emailAddress = data.to[0];
@@ -2838,7 +2995,7 @@ async function handleBounce(emailAddress, emailId, bounce, db, usersTable) {
2838
2995
  await db.update(usersTable).set({
2839
2996
  emailBounced: true,
2840
2997
  emailBouncedAt: /* @__PURE__ */ new Date()
2841
- }).where(eq7(usersTable.email, emailAddress.toLowerCase()));
2998
+ }).where(eq8(usersTable.email, emailAddress.toLowerCase()));
2842
2999
  logError(new Error("Email bounced"), {
2843
3000
  context: "email_webhook",
2844
3001
  emailAddress,
@@ -2854,7 +3011,7 @@ async function handleComplaint(emailAddress, emailId, db, usersTable) {
2854
3011
  await db.update(usersTable).set({
2855
3012
  emailComplained: true,
2856
3013
  emailComplainedAt: /* @__PURE__ */ new Date()
2857
- }).where(eq7(usersTable.email, emailAddress.toLowerCase()));
3014
+ }).where(eq8(usersTable.email, emailAddress.toLowerCase()));
2858
3015
  logError(new Error("Email spam complaint received"), {
2859
3016
  context: "email_webhook",
2860
3017
  emailAddress,
@@ -2876,15 +3033,15 @@ async function handleFailure(emailAddress, emailId, failed) {
2876
3033
  }
2877
3034
 
2878
3035
  // src/lib/email/monitoring.ts
2879
- import { sql as sql2 } from "drizzle-orm";
3036
+ import { sql as sql3 } from "drizzle-orm";
2880
3037
  async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
2881
3038
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
2882
3039
  const [stats] = await db.select({
2883
- totalSent: sql2`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
2884
- totalBounced: sql2`count(*) filter (where event_type = 'email.bounced')`.as(
3040
+ totalSent: sql3`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3041
+ totalBounced: sql3`count(*) filter (where event_type = 'email.bounced')`.as(
2885
3042
  "total_bounced"
2886
3043
  )
2887
- }).from(emailEventsTable).where(sql2`created_at > ${cutoff}`);
3044
+ }).from(emailEventsTable).where(sql3`created_at > ${cutoff}`);
2888
3045
  if (!stats || stats.totalSent === 0) {
2889
3046
  return 0;
2890
3047
  }
@@ -2903,11 +3060,11 @@ async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
2903
3060
  async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
2904
3061
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
2905
3062
  const [stats] = await db.select({
2906
- totalSent: sql2`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
2907
- totalComplained: sql2`count(*) filter (where event_type = 'email.complained')`.as(
3063
+ totalSent: sql3`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3064
+ totalComplained: sql3`count(*) filter (where event_type = 'email.complained')`.as(
2908
3065
  "total_complained"
2909
3066
  )
2910
- }).from(emailEventsTable).where(sql2`created_at > ${cutoff}`);
3067
+ }).from(emailEventsTable).where(sql3`created_at > ${cutoff}`);
2911
3068
  if (!stats || stats.totalSent === 0) {
2912
3069
  return 0;
2913
3070
  }
@@ -2924,16 +3081,34 @@ async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
2924
3081
  return complaintRate;
2925
3082
  }
2926
3083
 
3084
+ // src/core/invitations.ts
3085
+ async function verifyInvitation(invitations, args) {
3086
+ if (!invitations || !args.token) return null;
3087
+ const email = args.email.toLowerCase();
3088
+ try {
3089
+ const match = await invitations.verify({ token: args.token, email, db: args.db, env: args.env });
3090
+ return match && match.email.toLowerCase() === email ? args.token : null;
3091
+ } catch (err) {
3092
+ logError(err, { context: "invitation_verify_failed", email });
3093
+ return null;
3094
+ }
3095
+ }
3096
+ async function acceptInvitation(invitations, args) {
3097
+ if (!invitations?.onAccepted) return;
3098
+ try {
3099
+ await invitations.onAccepted({ token: args.token, user: args.user, db: args.db, env: args.env });
3100
+ } catch (err) {
3101
+ logError(err, { context: args.context, userId: args.user.id });
3102
+ }
3103
+ }
3104
+
2927
3105
  // src/routes/schemas.ts
2928
3106
  import { z } from "@hono/zod-openapi";
2929
3107
  var signupRequestSchema = z.object({
2930
3108
  email: z.string().email().openapi({ example: "user@example.com" }),
2931
- // Password and turnstileToken are required for standard signup; optional when oauthToken is provided
2932
- password: z.string().min(8).optional().openapi({ example: "SecurePass123!" }),
3109
+ password: z.string().min(8).openapi({ example: "SecurePass123!" }),
2933
3110
  name: z.string().min(1).optional().openapi({ example: "John Doe" }),
2934
- turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
2935
- // OAuth signup completion token (from /signup?oauth_token=...)
2936
- oauthToken: z.string().optional().openapi({ example: "a1b2c3d4-..." }),
3111
+ turnstileToken: z.string().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
2937
3112
  // App invitation token (from /signup?token=...). When it validates for this
2938
3113
  // email, the account is created already-verified with no verification email.
2939
3114
  invitationToken: z.string().optional().openapi({ example: "iIB8tDJJ..." })
@@ -3059,76 +3234,22 @@ var signupRoute = createRoute({
3059
3234
  description: "Invalid input",
3060
3235
  content: { "application/json": { schema: errorResponseSchema } }
3061
3236
  },
3237
+ 403: {
3238
+ description: "Sign-up is by invitation only and no valid invitation was given",
3239
+ content: { "application/json": { schema: errorResponseSchema } }
3240
+ },
3062
3241
  409: {
3063
3242
  description: "Email already registered",
3064
3243
  content: { "application/json": { schema: errorResponseSchema } }
3065
3244
  }
3066
3245
  }
3067
3246
  });
3247
+ var INVITATION_ONLY = "Sign-up is by invitation only";
3248
+ var INVITATION_INVALID = "This invitation is invalid, expired, or for a different email address";
3068
3249
  var signupHandler = async (c) => {
3069
- const { email, password, name, turnstileToken, oauthToken, invitationToken } = c.req.valid("json");
3070
- const { db: database, schema, emailBranding, invitations } = getAuthContext(c);
3250
+ const { email, password, name, turnstileToken, invitationToken } = c.req.valid("json");
3251
+ const { db: database, schema, env: authEnv, emailBranding, invitations, signup } = getAuthContext(c);
3071
3252
  const env = c.env;
3072
- if (oauthToken) {
3073
- const stored = await env.OAUTH_STATES.get(`oauth:signup:${oauthToken}`);
3074
- if (!stored) {
3075
- return problems.badRequest(c, "Invalid or expired OAuth token");
3076
- }
3077
- const {
3078
- email: tokenEmail,
3079
- provider,
3080
- providerUserId
3081
- } = JSON.parse(stored);
3082
- if (email.toLowerCase() !== tokenEmail.toLowerCase()) {
3083
- return problems.badRequest(c, "Email does not match OAuth token");
3084
- }
3085
- await env.OAUTH_STATES.delete(`oauth:signup:${oauthToken}`);
3086
- const existingUser2 = await database.query.users.findFirst({
3087
- where: eq8(schema.users.email, tokenEmail.toLowerCase())
3088
- });
3089
- if (existingUser2) {
3090
- return problems.conflict(c, "Email already registered");
3091
- }
3092
- let hashedPassword2 = null;
3093
- if (password) {
3094
- const passwordValidation2 = await validatePasswordWithBreachCheck(password);
3095
- if (!passwordValidation2.valid) {
3096
- return problems.badRequest(c, passwordValidation2.error || "Invalid password");
3097
- }
3098
- hashedPassword2 = await hashPassword(password, env.PASSWORD_PEPPER_V1);
3099
- }
3100
- const [newUser2] = await database.insert(schema.users).values({
3101
- email: tokenEmail.toLowerCase(),
3102
- name: name || null,
3103
- hashedPassword: hashedPassword2,
3104
- pepperKid: hashedPassword2 ? "v1" : null,
3105
- emailVerified: true,
3106
- // Provider verified the email
3107
- activatedAt: /* @__PURE__ */ new Date()
3108
- }).returning();
3109
- await database.insert(schema.oauthAccounts).values({ userId: newUser2.id, provider, providerUserId, email: tokenEmail.toLowerCase() }).onConflictDoNothing();
3110
- const fingerprint2 = await generateFingerprint(c.req.raw);
3111
- const ipAddress2 = getClientIp(c.req.raw);
3112
- const sessionId2 = await createSession(database, { sessions: schema.sessions }, newUser2.id, fingerprint2, ipAddress2);
3113
- logSecurityEvent("user_signup_oauth", "low", {
3114
- userId: newUser2.id,
3115
- email: newUser2.email,
3116
- provider
3117
- });
3118
- c.header("Set-Cookie", setSessionCookie(sessionId2, env), { append: true });
3119
- return c.json({
3120
- user: {
3121
- id: newUser2.id,
3122
- email: newUser2.email,
3123
- name: newUser2.name,
3124
- emailVerified: true
3125
- },
3126
- redirect: "/"
3127
- });
3128
- }
3129
- if (!password || !turnstileToken) {
3130
- return problems.badRequest(c, "Password and captcha are required");
3131
- }
3132
3253
  const turnstileValid = await verifyTurnstileToken(
3133
3254
  turnstileToken,
3134
3255
  env.TURNSTILE_SECRET_KEY,
@@ -3138,8 +3259,22 @@ var signupHandler = async (c) => {
3138
3259
  if (!turnstileValid) {
3139
3260
  return problems.badRequest(c, "Invalid captcha");
3140
3261
  }
3262
+ const invited = await verifyInvitation(invitations, {
3263
+ token: invitationToken,
3264
+ email,
3265
+ db: database,
3266
+ env: authEnv
3267
+ });
3268
+ if (invitationToken && invitations && !invited && signup !== "invitation") {
3269
+ return problems.badRequest(c, INVITATION_INVALID);
3270
+ }
3271
+ if (signup === "invitation" && !invited) {
3272
+ logSecurityEvent("signup_refused_no_invitation", "medium", { email: email.toLowerCase() });
3273
+ return problems.forbidden(c, invitationToken ? INVITATION_INVALID : INVITATION_ONLY);
3274
+ }
3275
+ const invitedVerified = invited !== null;
3141
3276
  const existingUser = await database.query.users.findFirst({
3142
- where: eq8(schema.users.email, email.toLowerCase())
3277
+ where: eq9(schema.users.email, email.toLowerCase())
3143
3278
  });
3144
3279
  if (existingUser) {
3145
3280
  return problems.conflict(c, "Email already registered");
@@ -3149,22 +3284,6 @@ var signupHandler = async (c) => {
3149
3284
  return problems.badRequest(c, passwordValidation.error || "Invalid password");
3150
3285
  }
3151
3286
  const hashedPassword = await hashPassword(password, env.PASSWORD_PEPPER_V1);
3152
- let invitedVerified = false;
3153
- if (invitationToken && invitations) {
3154
- const match = await invitations.verify({
3155
- token: invitationToken,
3156
- email: email.toLowerCase(),
3157
- db: database,
3158
- env
3159
- });
3160
- if (!match || match.email.toLowerCase() !== email.toLowerCase()) {
3161
- return problems.badRequest(
3162
- c,
3163
- "This invitation is invalid, expired, or for a different email address"
3164
- );
3165
- }
3166
- invitedVerified = true;
3167
- }
3168
3287
  const [newUser] = await database.insert(schema.users).values({
3169
3288
  email: email.toLowerCase(),
3170
3289
  name: name || null,
@@ -3176,7 +3295,7 @@ var signupHandler = async (c) => {
3176
3295
  if (!invitedVerified) {
3177
3296
  const emailToken = generateSecureToken(32);
3178
3297
  const tokenHash = await hashToken(emailToken);
3179
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3298
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
3180
3299
  await database.insert(schema.emailVerificationTokens).values({
3181
3300
  tokenHash,
3182
3301
  userId: newUser.id,
@@ -3197,17 +3316,14 @@ var signupHandler = async (c) => {
3197
3316
  schema.users
3198
3317
  );
3199
3318
  }
3200
- if (invitedVerified && invitationToken && invitations?.onAccepted) {
3201
- try {
3202
- await invitations.onAccepted({
3203
- token: invitationToken,
3204
- user: { id: newUser.id, email: newUser.email, name: newUser.name },
3205
- db: database,
3206
- env
3207
- });
3208
- } catch (err) {
3209
- logError(err, { context: "signup_invitation_onAccepted_failed", userId: newUser.id });
3210
- }
3319
+ if (invited) {
3320
+ await acceptInvitation(invitations, {
3321
+ token: invited,
3322
+ user: { id: newUser.id, email: newUser.email, name: newUser.name },
3323
+ db: database,
3324
+ env: authEnv,
3325
+ context: "signup_invitation_onAccepted_failed"
3326
+ });
3211
3327
  }
3212
3328
  const fingerprint = await generateFingerprint(c.req.raw);
3213
3329
  const ipAddress = getClientIp(c.req.raw);
@@ -3240,25 +3356,25 @@ var signupMiddleware = [
3240
3356
 
3241
3357
  // src/routes/login.ts
3242
3358
  import { createRoute as createRoute2 } from "@hono/zod-openapi";
3243
- import { eq as eq10 } from "drizzle-orm";
3359
+ import { eq as eq11 } from "drizzle-orm";
3244
3360
  import { getCookie } from "hono/cookie";
3245
3361
 
3246
3362
  // src/routes/2fa/helpers.ts
3247
- import { eq as eq9, and as and3, isNull } from "drizzle-orm";
3363
+ import { eq as eq10, and as and4, isNull as isNull2 } from "drizzle-orm";
3248
3364
  async function getUserEnabled2faMethods(database, userId, user2faMethodsTable) {
3249
3365
  return database.select({
3250
3366
  method: user2faMethodsTable.method,
3251
3367
  isPrimary: user2faMethodsTable.isPrimary,
3252
3368
  totpSecret: user2faMethodsTable.totpSecret,
3253
3369
  lastTotpCounter: user2faMethodsTable.lastTotpCounter
3254
- }).from(user2faMethodsTable).where(eq9(user2faMethodsTable.userId, userId));
3370
+ }).from(user2faMethodsTable).where(eq10(user2faMethodsTable.userId, userId));
3255
3371
  }
3256
3372
  function generateEmailOtp() {
3257
3373
  const bytes = crypto.getRandomValues(new Uint8Array(4));
3258
3374
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3259
3375
  return String(num % 1e6).padStart(6, "0");
3260
3376
  }
3261
- var EMAIL_CODE_TTL_SECONDS = 300;
3377
+ var EMAIL_CODE_TTL_SECONDS = AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES * 60;
3262
3378
  async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3263
3379
  const code = generateEmailOtp();
3264
3380
  await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
@@ -3285,7 +3401,7 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3285
3401
  const secret = await decryptTotpSecret(totpMethod.totpSecret, env.TOTP_ENCRYPTION_KEY);
3286
3402
  const result = verifyTotpCode(secret, code, totpMethod.lastTotpCounter);
3287
3403
  if (result.valid) {
3288
- await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where(and3(eq9(user2faMethodsTable.userId, userId), eq9(user2faMethodsTable.method, "totp")));
3404
+ await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where(and4(eq10(user2faMethodsTable.userId, userId), eq10(user2faMethodsTable.method, "totp")));
3289
3405
  return { valid: true };
3290
3406
  }
3291
3407
  return { valid: false };
@@ -3297,18 +3413,18 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3297
3413
  const kvKey = `email_2fa_challenge:${userId}`;
3298
3414
  const storedHash = await env.OAUTH_STATES.get(kvKey);
3299
3415
  if (!storedHash) return { valid: false };
3300
- const codeHash = await hashToken(code);
3301
- if (codeHash === storedHash) {
3416
+ const codeHash2 = await hashToken(code);
3417
+ if (codeHash2 === storedHash) {
3302
3418
  await env.OAUTH_STATES.delete(kvKey);
3303
3419
  return { valid: true };
3304
3420
  }
3305
3421
  return { valid: false };
3306
3422
  }
3307
3423
  if (method === "backup") {
3308
- const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where(and3(eq9(userBackupCodesTable.userId, userId), isNull(userBackupCodesTable.usedAt)));
3424
+ const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where(and4(eq10(userBackupCodesTable.userId, userId), isNull2(userBackupCodesTable.usedAt)));
3309
3425
  for (const bc of backupCodes) {
3310
3426
  if (await verifyBackupCode(code, bc.codeHash)) {
3311
- await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where(eq9(userBackupCodesTable.id, bc.id));
3427
+ await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where(eq10(userBackupCodesTable.id, bc.id));
3312
3428
  logSecurityEvent("2fa_backup_code_used", "medium", { userId, codeId: bc.id });
3313
3429
  return { valid: true };
3314
3430
  }
@@ -3364,7 +3480,7 @@ var loginHandler = async (c) => {
3364
3480
  return problems.badRequest(c, "Invalid captcha");
3365
3481
  }
3366
3482
  const user = await database.query.users.findFirst({
3367
- where: eq10(schema.users.email, email.toLowerCase())
3483
+ where: eq11(schema.users.email, email.toLowerCase())
3368
3484
  });
3369
3485
  if (!user) {
3370
3486
  return problems.unauthorized(c, "Invalid email or password");
@@ -3473,7 +3589,7 @@ var loginMiddleware = [
3473
3589
 
3474
3590
  // src/routes/logout.ts
3475
3591
  import { createRoute as createRoute3 } from "@hono/zod-openapi";
3476
- import { eq as eq11 } from "drizzle-orm";
3592
+ import { eq as eq12 } from "drizzle-orm";
3477
3593
  var logoutRoute = createRoute3({
3478
3594
  method: "post",
3479
3595
  path: "/logout",
@@ -3496,7 +3612,7 @@ var logoutHandler = async (c) => {
3496
3612
  const sessionIdMatch = cookieHeader.match(cookiePattern);
3497
3613
  if (sessionIdMatch) {
3498
3614
  const sessionId = sessionIdMatch[1];
3499
- await database.delete(schema.sessions).where(eq11(schema.sessions.id, sessionId));
3615
+ await database.delete(schema.sessions).where(eq12(schema.sessions.id, sessionId));
3500
3616
  }
3501
3617
  }
3502
3618
  c.header("Set-Cookie", clearSessionCookie(c.env), { append: true });
@@ -3505,7 +3621,7 @@ var logoutHandler = async (c) => {
3505
3621
 
3506
3622
  // src/routes/me.ts
3507
3623
  import { createRoute as createRoute4 } from "@hono/zod-openapi";
3508
- import { eq as eq12 } from "drizzle-orm";
3624
+ import { eq as eq13 } from "drizzle-orm";
3509
3625
  var meRoute = createRoute4({
3510
3626
  method: "get",
3511
3627
  path: "/me",
@@ -3533,7 +3649,7 @@ var meHandler = async (c) => {
3533
3649
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3534
3650
  const { db: database, schema } = getAuthContext(c);
3535
3651
  const user = await database.query.users.findFirst({
3536
- where: eq12(schema.users.id, userId)
3652
+ where: eq13(schema.users.id, userId)
3537
3653
  });
3538
3654
  if (!user) {
3539
3655
  return problems.notFound(c, "User not found");
@@ -3559,7 +3675,7 @@ var meMiddleware = [requireAuth];
3559
3675
 
3560
3676
  // src/routes/verify-email.ts
3561
3677
  import { createRoute as createRoute5 } from "@hono/zod-openapi";
3562
- import { eq as eq13 } from "drizzle-orm";
3678
+ import { eq as eq14 } from "drizzle-orm";
3563
3679
  var verifyEmailRoute = createRoute5({
3564
3680
  method: "post",
3565
3681
  path: "/verify-email",
@@ -3588,17 +3704,17 @@ var verifyEmailHandler = async (c) => {
3588
3704
  const { db: database, schema } = getAuthContext(c);
3589
3705
  const tokenHash = await hashToken(token);
3590
3706
  const verificationToken = await database.query.emailVerificationTokens.findFirst({
3591
- where: eq13(schema.emailVerificationTokens.tokenHash, tokenHash)
3707
+ where: eq14(schema.emailVerificationTokens.tokenHash, tokenHash)
3592
3708
  });
3593
3709
  if (!verificationToken) {
3594
3710
  return problems.badRequest(c, "Invalid or expired verification token");
3595
3711
  }
3596
3712
  if (verificationToken.expiresAt < Date.now()) {
3597
- await database.delete(schema.emailVerificationTokens).where(eq13(schema.emailVerificationTokens.tokenHash, tokenHash));
3713
+ await database.delete(schema.emailVerificationTokens).where(eq14(schema.emailVerificationTokens.tokenHash, tokenHash));
3598
3714
  return problems.badRequest(c, "Verification token has expired");
3599
3715
  }
3600
- await database.update(schema.users).set({ emailVerified: true }).where(eq13(schema.users.id, verificationToken.userId));
3601
- await database.delete(schema.emailVerificationTokens).where(eq13(schema.emailVerificationTokens.tokenHash, tokenHash));
3716
+ await database.update(schema.users).set({ emailVerified: true }).where(eq14(schema.users.id, verificationToken.userId));
3717
+ await database.delete(schema.emailVerificationTokens).where(eq14(schema.emailVerificationTokens.tokenHash, tokenHash));
3602
3718
  logSecurityEvent("email_verified", "low", { userId: verificationToken.userId });
3603
3719
  return c.json({ success: true, redirect: "/" });
3604
3720
  };
@@ -3607,18 +3723,18 @@ var verifyEmailHandler = async (c) => {
3607
3723
  import { createRoute as createRoute6 } from "@hono/zod-openapi";
3608
3724
 
3609
3725
  // src/core/password-reset.ts
3610
- import { eq as eq14, and as and4, gt as gt3, lt, sql as sql3 } from "drizzle-orm";
3726
+ import { eq as eq15, and as and5, gt as gt4, lt as lt2, sql as sql4 } from "drizzle-orm";
3611
3727
  var TOKEN_EXPIRATION_MS = AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES * 60 * 1e3;
3612
3728
  async function createPasswordResetToken(db, tables2, email) {
3613
3729
  const { users, passwordResetTokens } = tables2;
3614
- const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where(eq14(users.email, email.toLowerCase())).limit(1);
3730
+ const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where(eq15(users.email, email.toLowerCase())).limit(1);
3615
3731
  if (!user) {
3616
3732
  return null;
3617
3733
  }
3618
3734
  if (!user.emailVerified) {
3619
3735
  return null;
3620
3736
  }
3621
- await db.delete(passwordResetTokens).where(eq14(passwordResetTokens.userId, user.id));
3737
+ await db.delete(passwordResetTokens).where(eq15(passwordResetTokens.userId, user.id));
3622
3738
  const token = generateSecureToken(64);
3623
3739
  const tokenHash = await hashToken(token);
3624
3740
  const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
@@ -3640,10 +3756,10 @@ async function validateResetToken(db, tables2, token) {
3640
3756
  used: passwordResetTokens.used,
3641
3757
  expiresAt: passwordResetTokens.expiresAt
3642
3758
  }).from(passwordResetTokens).where(
3643
- and4(
3644
- eq14(passwordResetTokens.tokenHash, tokenHash),
3645
- eq14(passwordResetTokens.used, false),
3646
- gt3(passwordResetTokens.expiresAt, Date.now())
3759
+ and5(
3760
+ eq15(passwordResetTokens.tokenHash, tokenHash),
3761
+ eq15(passwordResetTokens.used, false),
3762
+ gt4(passwordResetTokens.expiresAt, Date.now())
3647
3763
  )
3648
3764
  ).limit(1);
3649
3765
  if (!resetToken) {
@@ -3664,11 +3780,11 @@ async function resetPassword(db, tables2, token, newPassword, pepper, pepperKid
3664
3780
  await db.update(users).set({
3665
3781
  hashedPassword,
3666
3782
  pepperKid,
3667
- sessionVersion: sql3`${users.sessionVersion} + 1`
3668
- }).where(eq14(users.id, userId));
3783
+ sessionVersion: sql4`${users.sessionVersion} + 1`
3784
+ }).where(eq15(users.id, userId));
3669
3785
  const tokenHash = await hashToken(token);
3670
- await db.update(passwordResetTokens).set({ used: true }).where(eq14(passwordResetTokens.tokenHash, tokenHash));
3671
- await db.delete(sessions).where(eq14(sessions.userId, userId));
3786
+ await db.update(passwordResetTokens).set({ used: true }).where(eq15(passwordResetTokens.tokenHash, tokenHash));
3787
+ await db.delete(sessions).where(eq15(sessions.userId, userId));
3672
3788
  logger_default.info("Password reset successful", { userId });
3673
3789
  return { success: true };
3674
3790
  }
@@ -3816,14 +3932,14 @@ var resetPasswordMiddleware = [
3816
3932
  import { createRoute as createRoute8 } from "@hono/zod-openapi";
3817
3933
 
3818
3934
  // src/core/change-password.ts
3819
- import { eq as eq15, and as and5, ne } from "drizzle-orm";
3935
+ import { eq as eq16, and as and6, ne } from "drizzle-orm";
3820
3936
  async function changePassword(params) {
3821
3937
  const { userId, currentPassword, newPassword, currentSessionId, pepper, db, tables: tables2 } = params;
3822
3938
  const { users, sessions } = tables2;
3823
3939
  const [user] = await db.select({
3824
3940
  id: users.id,
3825
3941
  hashedPassword: users.hashedPassword
3826
- }).from(users).where(eq15(users.id, userId)).limit(1);
3942
+ }).from(users).where(eq16(users.id, userId)).limit(1);
3827
3943
  if (!user || !user.hashedPassword) {
3828
3944
  return { success: false, error: "User not found" };
3829
3945
  }
@@ -3839,8 +3955,8 @@ async function changePassword(params) {
3839
3955
  await db.update(users).set({
3840
3956
  hashedPassword: newHashedPassword,
3841
3957
  updatedAt: /* @__PURE__ */ new Date()
3842
- }).where(eq15(users.id, userId));
3843
- await db.delete(sessions).where(and5(eq15(sessions.userId, userId), ne(sessions.id, currentSessionId)));
3958
+ }).where(eq16(users.id, userId));
3959
+ await db.delete(sessions).where(and6(eq16(sessions.userId, userId), ne(sessions.id, currentSessionId)));
3844
3960
  logSecurityEvent("password_changed", "low", {
3845
3961
  userId
3846
3962
  });
@@ -4113,7 +4229,7 @@ var cancelEmailChangeHandler = async (c) => {
4113
4229
  // src/routes/delete-account.ts
4114
4230
  import { createRoute as createRoute13 } from "@hono/zod-openapi";
4115
4231
  import { z as z5 } from "@hono/zod-openapi";
4116
- import { eq as eq16 } from "drizzle-orm";
4232
+ import { eq as eq17 } from "drizzle-orm";
4117
4233
  var deleteAccountRequestSchema = z5.object({
4118
4234
  password: z5.string().min(1).openapi({ example: "CurrentPass123!" })
4119
4235
  }).openapi("DeleteAccountRequest");
@@ -4161,7 +4277,7 @@ var deleteAccountHandler = async (c) => {
4161
4277
  const [user] = await database.select({
4162
4278
  id: schema.users.id,
4163
4279
  hashedPassword: schema.users.hashedPassword
4164
- }).from(schema.users).where(eq16(schema.users.id, userId)).limit(1);
4280
+ }).from(schema.users).where(eq17(schema.users.id, userId)).limit(1);
4165
4281
  if (!user || !user.hashedPassword) {
4166
4282
  return problems.unauthorized(c, "Invalid credentials");
4167
4283
  }
@@ -4176,8 +4292,8 @@ var deleteAccountHandler = async (c) => {
4176
4292
  }
4177
4293
  const now = /* @__PURE__ */ new Date();
4178
4294
  const purgeAt = new Date(now.getTime() + PURGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3);
4179
- await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where(eq16(schema.users.id, userId));
4180
- await database.delete(schema.sessions).where(eq16(schema.sessions.userId, userId));
4295
+ await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where(eq17(schema.users.id, userId));
4296
+ await database.delete(schema.sessions).where(eq17(schema.sessions.userId, userId));
4181
4297
  logSecurityEvent("account_deletion_requested", "high", { userId });
4182
4298
  return c.json(
4183
4299
  {
@@ -4198,7 +4314,7 @@ var deleteAccountMiddleware = [requireAuth];
4198
4314
  // src/routes/refresh.ts
4199
4315
  import { createRoute as createRoute14 } from "@hono/zod-openapi";
4200
4316
  import { z as z6 } from "@hono/zod-openapi";
4201
- import { eq as eq17, and as and6, gt as gt4 } from "drizzle-orm";
4317
+ import { eq as eq18, and as and7, gt as gt5 } from "drizzle-orm";
4202
4318
  var refreshRequestSchema = z6.object({
4203
4319
  refreshToken: z6.string().min(1).openapi({ example: "abc123..." })
4204
4320
  }).openapi("RefreshRequest");
@@ -4238,7 +4354,7 @@ var refreshHandler = async (c) => {
4238
4354
  expiresAt: schema.sessions.expiresAt,
4239
4355
  fingerprint: schema.sessions.fingerprint,
4240
4356
  ipAddress: schema.sessions.ipAddress
4241
- }).from(schema.sessions).where(and6(eq17(schema.sessions.id, refreshToken), gt4(schema.sessions.expiresAt, Date.now()))).limit(1);
4357
+ }).from(schema.sessions).where(and7(eq18(schema.sessions.id, refreshToken), gt5(schema.sessions.expiresAt, Date.now()))).limit(1);
4242
4358
  if (!session) {
4243
4359
  return problems.unauthorized(c, "Invalid or expired refresh token");
4244
4360
  }
@@ -4261,7 +4377,7 @@ var refreshMiddleware = [];
4261
4377
  // src/routes/resend-verification.ts
4262
4378
  import { createRoute as createRoute15 } from "@hono/zod-openapi";
4263
4379
  import { z as z7 } from "@hono/zod-openapi";
4264
- import { eq as eq18 } from "drizzle-orm";
4380
+ import { eq as eq19 } from "drizzle-orm";
4265
4381
  var resendVerificationResponseSchema = z7.object({
4266
4382
  success: z7.boolean().openapi({ example: true }),
4267
4383
  message: z7.string().openapi({ example: "Verification email sent" })
@@ -4306,17 +4422,17 @@ var resendVerificationHandler = async (c) => {
4306
4422
  email: schema.users.email,
4307
4423
  name: schema.users.name,
4308
4424
  emailVerified: schema.users.emailVerified
4309
- }).from(schema.users).where(eq18(schema.users.id, userId)).limit(1);
4425
+ }).from(schema.users).where(eq19(schema.users.id, userId)).limit(1);
4310
4426
  if (!user) {
4311
4427
  return problems.unauthorized(c, "User not found");
4312
4428
  }
4313
4429
  if (user.emailVerified) {
4314
4430
  return problems.badRequest(c, "Email is already verified");
4315
4431
  }
4316
- await db.delete(schema.emailVerificationTokens).where(eq18(schema.emailVerificationTokens.userId, userId));
4432
+ await db.delete(schema.emailVerificationTokens).where(eq19(schema.emailVerificationTokens.userId, userId));
4317
4433
  const emailToken = generateSecureToken(32);
4318
4434
  const tokenHash = await hashToken(emailToken);
4319
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
4435
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
4320
4436
  await db.insert(schema.emailVerificationTokens).values({
4321
4437
  tokenHash,
4322
4438
  userId: user.id,
@@ -4358,7 +4474,7 @@ import { OpenAPIHono } from "@hono/zod-openapi";
4358
4474
 
4359
4475
  // src/routes/2fa/status.ts
4360
4476
  import { createRoute as createRoute16 } from "@hono/zod-openapi";
4361
- import { eq as eq19, and as and7, isNull as isNull2 } from "drizzle-orm";
4477
+ import { eq as eq20, and as and8, isNull as isNull3 } from "drizzle-orm";
4362
4478
 
4363
4479
  // src/routes/2fa/schemas.ts
4364
4480
  import { z as z8 } from "@hono/zod-openapi";
@@ -4519,7 +4635,7 @@ var statusHandler = async (c) => {
4519
4635
  if (!userId) return problems.unauthorized(c);
4520
4636
  const { db, schema } = getAuthContext(c);
4521
4637
  const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4522
- const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where(and7(eq19(schema.userBackupCodes.userId, userId), isNull2(schema.userBackupCodes.usedAt)));
4638
+ const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where(and8(eq20(schema.userBackupCodes.userId, userId), isNull3(schema.userBackupCodes.usedAt)));
4523
4639
  c.header("Cache-Control", "private, max-age=60");
4524
4640
  c.header("Vary", "Cookie");
4525
4641
  return c.json({
@@ -4531,7 +4647,7 @@ var statusHandler = async (c) => {
4531
4647
 
4532
4648
  // src/routes/2fa/totp-setup.ts
4533
4649
  import { createRoute as createRoute17 } from "@hono/zod-openapi";
4534
- import { eq as eq20, and as and8 } from "drizzle-orm";
4650
+ import { eq as eq21, and as and9 } from "drizzle-orm";
4535
4651
  var totpSetupRoute = createRoute17({
4536
4652
  method: "post",
4537
4653
  path: "/totp/setup",
@@ -4561,11 +4677,11 @@ var totpSetupHandler = async (c) => {
4561
4677
  const userId = c.get("userId");
4562
4678
  if (!userId) return problems.unauthorized(c);
4563
4679
  const { db, schema, env } = getAuthContext(c);
4564
- const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where(and8(eq20(schema.user2faMethods.userId, userId), eq20(schema.user2faMethods.method, "totp"))).limit(1);
4680
+ const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where(and9(eq21(schema.user2faMethods.userId, userId), eq21(schema.user2faMethods.method, "totp"))).limit(1);
4565
4681
  if (existing.length > 0) {
4566
4682
  return problems.badRequest(c, "TOTP already enabled");
4567
4683
  }
4568
- const [user] = await db.select({ email: schema.users.email }).from(schema.users).where(eq20(schema.users.id, userId)).limit(1);
4684
+ const [user] = await db.select({ email: schema.users.email }).from(schema.users).where(eq21(schema.users.id, userId)).limit(1);
4569
4685
  if (!user) return problems.notFound(c, "User not found");
4570
4686
  const secret = generateTotpSecret();
4571
4687
  const qrCodeUri = generateQrCodeUri(secret, user.email);
@@ -4576,7 +4692,7 @@ var totpSetupHandler = async (c) => {
4576
4692
 
4577
4693
  // src/routes/2fa/totp-verify.ts
4578
4694
  import { createRoute as createRoute18 } from "@hono/zod-openapi";
4579
- import { eq as eq21 } from "drizzle-orm";
4695
+ import { eq as eq22 } from "drizzle-orm";
4580
4696
  var totpVerifyRoute = createRoute18({
4581
4697
  method: "post",
4582
4698
  path: "/totp/verify",
@@ -4622,7 +4738,7 @@ var totpVerifyHandler = async (c) => {
4622
4738
  logSecurityEvent("2fa_totp_setup_failed", "medium", { userId });
4623
4739
  return problems.badRequest(c, "Invalid code");
4624
4740
  }
4625
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq21(schema.users.id, userId)).limit(1);
4741
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq22(schema.users.id, userId)).limit(1);
4626
4742
  const encryptionKey = env.TOTP_ENCRYPTION_KEY;
4627
4743
  if (!encryptionKey) {
4628
4744
  logger_default.error("TOTP_ENCRYPTION_KEY not configured");
@@ -4670,7 +4786,7 @@ var totpVerifyHandler = async (c) => {
4670
4786
 
4671
4787
  // src/routes/2fa/totp-disable.ts
4672
4788
  import { createRoute as createRoute19 } from "@hono/zod-openapi";
4673
- import { eq as eq22, and as and9 } from "drizzle-orm";
4789
+ import { eq as eq23, and as and10 } from "drizzle-orm";
4674
4790
  var totpDisableRoute = createRoute19({
4675
4791
  method: "post",
4676
4792
  path: "/totp/disable",
@@ -4713,13 +4829,13 @@ var totpDisableHandler = async (c) => {
4713
4829
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "totp" });
4714
4830
  return problems.badRequest(c, "Invalid verification code");
4715
4831
  }
4716
- await db.delete(schema.user2faMethods).where(and9(eq22(schema.user2faMethods.userId, userId), eq22(schema.user2faMethods.method, "totp")));
4832
+ await db.delete(schema.user2faMethods).where(and10(eq23(schema.user2faMethods.userId, userId), eq23(schema.user2faMethods.method, "totp")));
4717
4833
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4718
4834
  if (remaining.length === 0) {
4719
- await db.delete(schema.userBackupCodes).where(eq22(schema.userBackupCodes.userId, userId));
4720
- await db.delete(schema.userTrustedDevices).where(eq22(schema.userTrustedDevices.userId, userId));
4835
+ await db.delete(schema.userBackupCodes).where(eq23(schema.userBackupCodes.userId, userId));
4836
+ await db.delete(schema.userTrustedDevices).where(eq23(schema.userTrustedDevices.userId, userId));
4721
4837
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
4722
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq22(schema.users.id, userId)).limit(1);
4838
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq23(schema.users.id, userId)).limit(1);
4723
4839
  if (user) {
4724
4840
  const emailService = new EmailService(env, void 0, emailBranding);
4725
4841
  const firstName = user.name?.split(" ")[0] || null;
@@ -4738,7 +4854,7 @@ var totpDisableHandler = async (c) => {
4738
4854
 
4739
4855
  // src/routes/2fa/email-setup.ts
4740
4856
  import { createRoute as createRoute20 } from "@hono/zod-openapi";
4741
- import { eq as eq23, and as and10 } from "drizzle-orm";
4857
+ import { eq as eq24, and as and11 } from "drizzle-orm";
4742
4858
  var emailSetupRoute = createRoute20({
4743
4859
  method: "post",
4744
4860
  path: "/email/setup",
@@ -4768,15 +4884,15 @@ var emailSetupHandler = async (c) => {
4768
4884
  const userId = c.get("userId");
4769
4885
  if (!userId) return problems.unauthorized(c);
4770
4886
  const { db, schema, env, emailBranding } = getAuthContext(c);
4771
- const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where(and10(eq23(schema.user2faMethods.userId, userId), eq23(schema.user2faMethods.method, "email"))).limit(1);
4887
+ const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where(and11(eq24(schema.user2faMethods.userId, userId), eq24(schema.user2faMethods.method, "email"))).limit(1);
4772
4888
  if (existing.length > 0) {
4773
4889
  return problems.badRequest(c, "Email 2FA already enabled");
4774
4890
  }
4775
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq23(schema.users.id, userId)).limit(1);
4891
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq24(schema.users.id, userId)).limit(1);
4776
4892
  if (!user) return problems.notFound(c, "User not found");
4777
4893
  const code = generateEmailOtp();
4778
- const codeHash = await hashToken(code);
4779
- await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
4894
+ const codeHash2 = await hashToken(code);
4895
+ await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash2, { expirationTtl: 300 });
4780
4896
  const emailService = new EmailService(env, void 0, emailBranding);
4781
4897
  const firstName = user.name?.split(" ")[0] || null;
4782
4898
  await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
@@ -4786,7 +4902,7 @@ var emailSetupHandler = async (c) => {
4786
4902
 
4787
4903
  // src/routes/2fa/email-verify.ts
4788
4904
  import { createRoute as createRoute21 } from "@hono/zod-openapi";
4789
- import { eq as eq24 } from "drizzle-orm";
4905
+ import { eq as eq25 } from "drizzle-orm";
4790
4906
  var emailVerifyRoute = createRoute21({
4791
4907
  method: "post",
4792
4908
  path: "/email/verify",
@@ -4823,12 +4939,12 @@ var emailVerifyHandler = async (c) => {
4823
4939
  if (!storedHash) {
4824
4940
  return problems.badRequest(c, "Code expired. Please request a new one.");
4825
4941
  }
4826
- const codeHash = await hashToken(code);
4827
- if (codeHash !== storedHash) {
4942
+ const codeHash2 = await hashToken(code);
4943
+ if (codeHash2 !== storedHash) {
4828
4944
  logSecurityEvent("2fa_email_setup_failed", "medium", { userId });
4829
4945
  return problems.badRequest(c, "Invalid code");
4830
4946
  }
4831
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq24(schema.users.id, userId)).limit(1);
4947
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq25(schema.users.id, userId)).limit(1);
4832
4948
  const now = Date.now();
4833
4949
  const existingMethods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4834
4950
  const isFirstMethod = existingMethods.length === 0;
@@ -4870,7 +4986,7 @@ var emailVerifyHandler = async (c) => {
4870
4986
 
4871
4987
  // src/routes/2fa/email-send-code.ts
4872
4988
  import { createRoute as createRoute22 } from "@hono/zod-openapi";
4873
- import { eq as eq25, and as and11 } from "drizzle-orm";
4989
+ import { eq as eq26, and as and12 } from "drizzle-orm";
4874
4990
  var emailSendCodeRoute = createRoute22({
4875
4991
  method: "post",
4876
4992
  path: "/email/send-code",
@@ -4900,15 +5016,15 @@ var emailSendCodeHandler = async (c) => {
4900
5016
  const userId = c.get("userId");
4901
5017
  if (!userId) return problems.unauthorized(c);
4902
5018
  const { db, schema, env, emailBranding } = getAuthContext(c);
4903
- const [emailMethod] = await db.select().from(schema.user2faMethods).where(and11(eq25(schema.user2faMethods.userId, userId), eq25(schema.user2faMethods.method, "email"))).limit(1);
5019
+ const [emailMethod] = await db.select().from(schema.user2faMethods).where(and12(eq26(schema.user2faMethods.userId, userId), eq26(schema.user2faMethods.method, "email"))).limit(1);
4904
5020
  if (!emailMethod) {
4905
5021
  return problems.badRequest(c, "Email 2FA not enabled");
4906
5022
  }
4907
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq25(schema.users.id, userId)).limit(1);
5023
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq26(schema.users.id, userId)).limit(1);
4908
5024
  if (!user) return problems.notFound(c, "User not found");
4909
5025
  const code = generateEmailOtp();
4910
- const codeHash = await hashToken(code);
4911
- await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash, {
5026
+ const codeHash2 = await hashToken(code);
5027
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash2, {
4912
5028
  expirationTtl: 300
4913
5029
  });
4914
5030
  const emailService = new EmailService(env, void 0, emailBranding);
@@ -4930,7 +5046,7 @@ var emailSendCodeHandler = async (c) => {
4930
5046
 
4931
5047
  // src/routes/2fa/email-disable.ts
4932
5048
  import { createRoute as createRoute23 } from "@hono/zod-openapi";
4933
- import { eq as eq26, and as and12 } from "drizzle-orm";
5049
+ import { eq as eq27, and as and13 } from "drizzle-orm";
4934
5050
  var emailDisableRoute = createRoute23({
4935
5051
  method: "post",
4936
5052
  path: "/email/disable",
@@ -4973,13 +5089,13 @@ var emailDisableHandler = async (c) => {
4973
5089
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "email" });
4974
5090
  return problems.badRequest(c, "Invalid verification code");
4975
5091
  }
4976
- await db.delete(schema.user2faMethods).where(and12(eq26(schema.user2faMethods.userId, userId), eq26(schema.user2faMethods.method, "email")));
5092
+ await db.delete(schema.user2faMethods).where(and13(eq27(schema.user2faMethods.userId, userId), eq27(schema.user2faMethods.method, "email")));
4977
5093
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4978
5094
  if (remaining.length === 0) {
4979
- await db.delete(schema.userBackupCodes).where(eq26(schema.userBackupCodes.userId, userId));
4980
- await db.delete(schema.userTrustedDevices).where(eq26(schema.userTrustedDevices.userId, userId));
5095
+ await db.delete(schema.userBackupCodes).where(eq27(schema.userBackupCodes.userId, userId));
5096
+ await db.delete(schema.userTrustedDevices).where(eq27(schema.userTrustedDevices.userId, userId));
4981
5097
  await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
4982
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq26(schema.users.id, userId)).limit(1);
5098
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq27(schema.users.id, userId)).limit(1);
4983
5099
  if (user) {
4984
5100
  const emailService = new EmailService(env, void 0, emailBranding);
4985
5101
  const firstName = user.name?.split(" ")[0] || null;
@@ -4998,7 +5114,7 @@ var emailDisableHandler = async (c) => {
4998
5114
 
4999
5115
  // src/routes/2fa/trusted-devices.ts
5000
5116
  import { createRoute as createRoute24, z as z9 } from "@hono/zod-openapi";
5001
- import { eq as eq27, and as and13, gt as gt5 } from "drizzle-orm";
5117
+ import { eq as eq28, and as and14, gt as gt6 } from "drizzle-orm";
5002
5118
  var trustedDevicesGetRoute = createRoute24({
5003
5119
  method: "get",
5004
5120
  path: "/trusted-devices",
@@ -5037,7 +5153,7 @@ var trustedDevicesGetHandler = async (c) => {
5037
5153
  ipAddress: schema.userTrustedDevices.ipAddress,
5038
5154
  lastUsedAt: schema.userTrustedDevices.lastUsedAt,
5039
5155
  createdAt: schema.userTrustedDevices.createdAt
5040
- }).from(schema.userTrustedDevices).where(and13(eq27(schema.userTrustedDevices.userId, userId), gt5(schema.userTrustedDevices.expiresAt, now)));
5156
+ }).from(schema.userTrustedDevices).where(and14(eq28(schema.userTrustedDevices.userId, userId), gt6(schema.userTrustedDevices.expiresAt, now)));
5041
5157
  c.header("Cache-Control", "private, max-age=60");
5042
5158
  c.header("Vary", "Cookie");
5043
5159
  return c.json({ devices });
@@ -5073,7 +5189,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5073
5189
  if (!userId) return problems.unauthorized(c);
5074
5190
  const deviceId = c.req.param("id");
5075
5191
  const { db, schema } = getAuthContext(c);
5076
- const result = await db.delete(schema.userTrustedDevices).where(and13(eq27(schema.userTrustedDevices.id, deviceId), eq27(schema.userTrustedDevices.userId, userId))).returning({ id: schema.userTrustedDevices.id });
5192
+ const result = await db.delete(schema.userTrustedDevices).where(and14(eq28(schema.userTrustedDevices.id, deviceId), eq28(schema.userTrustedDevices.userId, userId))).returning({ id: schema.userTrustedDevices.id });
5077
5193
  if (result.length === 0) {
5078
5194
  return problems.notFound(c, "Device not found");
5079
5195
  }
@@ -5083,7 +5199,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5083
5199
 
5084
5200
  // src/routes/2fa/backup-codes.ts
5085
5201
  import { createRoute as createRoute25 } from "@hono/zod-openapi";
5086
- import { eq as eq28 } from "drizzle-orm";
5202
+ import { eq as eq29 } from "drizzle-orm";
5087
5203
  var backupCodesRegenerateRoute = createRoute25({
5088
5204
  method: "post",
5089
5205
  path: "/backup-codes/regenerate",
@@ -5124,7 +5240,7 @@ var backupCodesRegenerateHandler = async (c) => {
5124
5240
  logSecurityEvent("2fa_backup_regenerate_failed", "medium", { userId });
5125
5241
  return problems.badRequest(c, "Invalid verification code");
5126
5242
  }
5127
- await db.delete(schema.userBackupCodes).where(eq28(schema.userBackupCodes.userId, userId));
5243
+ await db.delete(schema.userBackupCodes).where(eq29(schema.userBackupCodes.userId, userId));
5128
5244
  const codes = generateBackupCodes();
5129
5245
  const now = Date.now();
5130
5246
  const hashedCodes = await Promise.all(
@@ -5141,7 +5257,7 @@ var backupCodesRegenerateHandler = async (c) => {
5141
5257
 
5142
5258
  // src/routes/2fa/challenge.ts
5143
5259
  import { createRoute as createRoute26 } from "@hono/zod-openapi";
5144
- import { eq as eq29 } from "drizzle-orm";
5260
+ import { eq as eq30 } from "drizzle-orm";
5145
5261
  import { getCookie as getCookie2 } from "hono/cookie";
5146
5262
  var challengeRoute = createRoute26({
5147
5263
  method: "post",
@@ -5213,7 +5329,7 @@ var challengeHandler = async (c) => {
5213
5329
  }
5214
5330
  await deleteChallengeToken(c.env.OAUTH_STATES, challengeToken);
5215
5331
  clearChallengeCookie(c, c.env);
5216
- const [user] = await db.select().from(schema.users).where(eq29(schema.users.id, payload.userId)).limit(1);
5332
+ const [user] = await db.select().from(schema.users).where(eq30(schema.users.id, payload.userId)).limit(1);
5217
5333
  if (!user) {
5218
5334
  logger_default.error("User not found during 2FA challenge", { userId: payload.userId });
5219
5335
  return problems.notFound(c, "User not found");
@@ -5254,7 +5370,7 @@ var challengeHandler = async (c) => {
5254
5370
 
5255
5371
  // src/routes/2fa/challenge-resend.ts
5256
5372
  import { createRoute as createRoute27 } from "@hono/zod-openapi";
5257
- import { eq as eq30 } from "drizzle-orm";
5373
+ import { eq as eq31 } from "drizzle-orm";
5258
5374
  import { getCookie as getCookie3 } from "hono/cookie";
5259
5375
  var challengeResendRoute = createRoute27({
5260
5376
  method: "post",
@@ -5292,7 +5408,7 @@ var challengeResendHandler = async (c) => {
5292
5408
  if (!payload.methods.includes("email")) {
5293
5409
  return problems.badRequest(c, "Email verification not available");
5294
5410
  }
5295
- const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq30(schema.users.id, payload.userId)).limit(1);
5411
+ const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq31(schema.users.id, payload.userId)).limit(1);
5296
5412
  if (!user) {
5297
5413
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5298
5414
  return problems.notFound(c, "User not found");
@@ -5523,7 +5639,7 @@ var authorizeHandler = async (c) => {
5523
5639
 
5524
5640
  // src/routes/oauth/callback.ts
5525
5641
  import { createRoute as createRoute29 } from "@hono/zod-openapi";
5526
- import { eq as eq31 } from "drizzle-orm";
5642
+ import { eq as eq32 } from "drizzle-orm";
5527
5643
  var callbackRoute = createRoute29({
5528
5644
  method: "get",
5529
5645
  path: "/{provider}/callback",
@@ -5649,10 +5765,24 @@ var callbackHandler = async (c) => {
5649
5765
  logger_default.warn("No user ID returned from OAuth provider", { provider });
5650
5766
  return c.redirect(`${appUrl}/login?error=oauth_failed`);
5651
5767
  }
5652
- const { db, schema } = getAuthContext(c);
5653
- const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where(eq31(schema.users.email, email)).limit(1);
5768
+ const { db, schema, env, invitations, signup } = getAuthContext(c);
5769
+ const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where(eq32(schema.users.email, email)).limit(1);
5654
5770
  let authUser = existingUser;
5655
5771
  if (!authUser) {
5772
+ const invited = await verifyInvitation(invitations, {
5773
+ token: invitationToken,
5774
+ email,
5775
+ db,
5776
+ env
5777
+ });
5778
+ if (signup === "invitation" && !invited) {
5779
+ logSecurityEvent("oauth_signup_refused_no_invitation", "medium", {
5780
+ email,
5781
+ provider,
5782
+ hadInvitation: !!invitationToken
5783
+ });
5784
+ return c.redirect(`${appUrl}/login?error=invitation_required`);
5785
+ }
5656
5786
  const [createdUser] = await db.insert(schema.users).values({
5657
5787
  email,
5658
5788
  name: name || null,
@@ -5663,6 +5793,15 @@ var callbackHandler = async (c) => {
5663
5793
  activatedAt: /* @__PURE__ */ new Date()
5664
5794
  }).returning({ id: schema.users.id, email: schema.users.email });
5665
5795
  authUser = createdUser;
5796
+ if (invited) {
5797
+ await acceptInvitation(invitations, {
5798
+ token: invited,
5799
+ user: { id: createdUser.id, email: createdUser.email, name: name || null },
5800
+ db,
5801
+ env,
5802
+ context: "oauth_invitation_onAccepted_failed"
5803
+ });
5804
+ }
5666
5805
  logSecurityEvent("user_signup_oauth", "low", {
5667
5806
  userId: createdUser.id,
5668
5807
  email: createdUser.email,
@@ -5702,7 +5841,7 @@ var callbackHandler = async (c) => {
5702
5841
 
5703
5842
  // src/routes/oauth/complete.ts
5704
5843
  import { createRoute as createRoute30 } from "@hono/zod-openapi";
5705
- import { eq as eq32 } from "drizzle-orm";
5844
+ import { eq as eq33 } from "drizzle-orm";
5706
5845
  var completeRoute = createRoute30({
5707
5846
  method: "post",
5708
5847
  path: "/complete",
@@ -5750,7 +5889,7 @@ var completeHandler = async (c) => {
5750
5889
  redirect: storedRedirect
5751
5890
  } = JSON.parse(stored);
5752
5891
  const { db, schema } = getAuthContext(c);
5753
- await db.update(schema.users).set({ emailVerified: true }).where(eq32(schema.users.id, userId));
5892
+ await db.update(schema.users).set({ emailVerified: true }).where(eq33(schema.users.id, userId));
5754
5893
  await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
5755
5894
  const fingerprint = await generateFingerprint(c.req.raw);
5756
5895
  const ipAddress = getClientIp(c.req.raw);
@@ -5795,19 +5934,12 @@ var routes_default = authRoutes;
5795
5934
 
5796
5935
  // src/lib/email/webhook-verifier.ts
5797
5936
  import { Webhook } from "svix";
5798
- function verifyWebhookSignature(payload, signature, secret) {
5937
+ function verifyWebhookSignature(payload, headers, secret) {
5799
5938
  if (!secret || secret.trim() === "") {
5800
5939
  throw new Error("Webhook secret is required");
5801
5940
  }
5802
5941
  try {
5803
- const wh = new Webhook(secret);
5804
- wh.verify(payload, {
5805
- "svix-id": "",
5806
- // Not used by Resend
5807
- "svix-timestamp": "",
5808
- // Not used by Resend
5809
- "svix-signature": signature
5810
- });
5942
+ new Webhook(secret).verify(payload, { ...headers });
5811
5943
  return true;
5812
5944
  } catch (error) {
5813
5945
  logger_default.warn("Webhook signature verification failed", {
@@ -5822,6 +5954,8 @@ export {
5822
5954
  CloudflareEmailAdapter,
5823
5955
  EmailService,
5824
5956
  MAX_CHALLENGE_ATTEMPTS,
5957
+ PASSWORDLESS_MAX_ATTEMPTS,
5958
+ PASSWORDLESS_TTL_MS,
5825
5959
  ProblemTypes,
5826
5960
  ResendAdapter,
5827
5961
  SCHEMA_VERSION,
@@ -5833,6 +5967,7 @@ export {
5833
5967
  cancelEmailChange,
5834
5968
  checkAccountLocked,
5835
5969
  checkBreachedPassword,
5970
+ cleanupExpiredPasswordlessChallenges,
5836
5971
  clearAccountLockout,
5837
5972
  clearChallengeCookie,
5838
5973
  clearSessionCookie,
@@ -5843,6 +5978,7 @@ export {
5843
5978
  createChallengeToken,
5844
5979
  createDeviceToken,
5845
5980
  createEmailAdapter,
5981
+ createPasswordlessChallenge,
5846
5982
  createProblemDetails,
5847
5983
  createSession,
5848
5984
  csrf,
@@ -5859,6 +5995,7 @@ export {
5859
5995
  generate2faEnabledEmail,
5860
5996
  generateApiKey,
5861
5997
  generateBackupCodes,
5998
+ generateCode,
5862
5999
  generateCodeChallenge,
5863
6000
  generateCodeVerifier,
5864
6001
  generateEmailChangeConfirmation,
@@ -5897,14 +6034,17 @@ export {
5897
6034
  optionalAuth,
5898
6035
  parseDeviceName,
5899
6036
  parseDeviceType,
6037
+ passwordlessEmail,
5900
6038
  problemJson,
5901
6039
  problems,
5902
6040
  rateLimit,
6041
+ recentPasswordlessFailures,
5903
6042
  refreshSession,
5904
6043
  requestEmailChange,
5905
6044
  requireAuth,
5906
6045
  requireVerifiedEmail,
5907
6046
  retrieveChallengeToken,
6047
+ sessionRefreshDue,
5908
6048
  setChallengeCookie,
5909
6049
  setSessionCookie,
5910
6050
  setTrustedDeviceCookie,
@@ -5922,6 +6062,8 @@ export {
5922
6062
  verifyCodeChallenge,
5923
6063
  verifyPassword,
5924
6064
  verifyPasswordWithRotation,
6065
+ verifyPasswordlessCode,
6066
+ verifyPasswordlessLink,
5925
6067
  verifyTotpCode,
5926
6068
  verifyTurnstileToken,
5927
6069
  verifyWebhookSignature