@syncello/auth 3.6.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,6 +255,8 @@ 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,
@@ -335,7 +337,7 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
335
337
  note: "SSR requests have different User-Agent, causing expected mismatch"
336
338
  });
337
339
  }
338
- await refreshSession(db, { sessions }, sessionId);
340
+ if (sessionRefreshDue(row.sessionLastActiveAt)) await refreshSession(db, { sessions }, sessionId);
339
341
  return {
340
342
  user: {
341
343
  id: row.userId,
@@ -354,6 +356,10 @@ async function validateSession(db, tables2, sessionId, currentFingerprint, sessi
354
356
  }
355
357
  };
356
358
  }
359
+ function sessionRefreshDue(lastActiveAt, now = Date.now()) {
360
+ const interval = AUTH_DEFAULTS.SESSION_REFRESH_INTERVAL_MINUTES * 60 * 1e3;
361
+ return lastActiveAt === null || now - lastActiveAt >= interval;
362
+ }
357
363
  async function refreshSession(db, tables2, sessionId, sessionTtlMs = AUTH_DEFAULTS.SESSION_TTL_DAYS * 24 * 60 * 60 * 1e3) {
358
364
  const { sessions } = tables2;
359
365
  const newExpiration = Date.now() + sessionTtlMs;
@@ -521,16 +527,16 @@ function createDeviceToken() {
521
527
  };
522
528
  }
523
529
  async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
524
- const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
530
+ const { eq: eq34, and: and15, gt: gt7 } = await import("drizzle-orm");
525
531
  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())
532
+ and15(
533
+ eq34(userTrustedDevicesTable.userId, userId),
534
+ eq34(userTrustedDevicesTable.tokenHash, tokenHash),
535
+ gt7(userTrustedDevicesTable.expiresAt, Date.now())
530
536
  )
531
537
  ).limit(1);
532
538
  if (device) {
533
- await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
539
+ await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq34(userTrustedDevicesTable.id, device.id));
534
540
  return true;
535
541
  }
536
542
  return false;
@@ -943,582 +949,48 @@ function base64UrlEncode(data) {
943
949
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
944
950
  }
945
951
 
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";
952
+ // src/core/passwordless.ts
953
+ import { and as and2, desc, eq as eq4, gt as gt2, isNull, lt, sql as sql2 } from "drizzle-orm";
949
954
 
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
- );
955
+ // src/lib/email/security.ts
956
+ function getAllowedDomains(appUrl) {
957
+ const base = ["localhost", "127.0.0.1"];
958
+ try {
959
+ const url = new URL(appUrl);
960
+ return [...base, url.hostname];
961
+ } catch {
962
+ return base;
980
963
  }
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
964
  }
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;
965
+ function sanitizeEmailUrl(url, appUrl = "") {
966
+ let parsed;
967
+ try {
968
+ parsed = new URL(url);
969
+ } catch {
970
+ throw new Error("Invalid URL format");
1014
971
  }
1015
- problem.traceId = options?.traceId || generateTraceId();
1016
- if (options?.requestId) {
1017
- problem.requestId = options.requestId;
972
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
973
+ throw new Error(`Disallowed protocol: ${parsed.protocol}`);
1018
974
  }
1019
- if (options?.extensions) {
1020
- Object.assign(problem, options.extensions);
975
+ const allowedDomains = getAllowedDomains(appUrl);
976
+ const hostname = parsed.hostname.toLowerCase();
977
+ const isAllowed = allowedDomains.some((domain) => {
978
+ return hostname === domain || hostname.endsWith(`.${domain}`);
979
+ });
980
+ if (!isAllowed) {
981
+ throw new Error(`Disallowed domain: ${hostname}`);
1021
982
  }
1022
- return problem;
983
+ return parsed.toString();
1023
984
  }
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]);
985
+ function escapeHtml(text) {
986
+ const htmlEscapeMap = {
987
+ "&": "&amp;",
988
+ "<": "&lt;",
989
+ ">": "&gt;",
990
+ '"': "&quot;",
991
+ "'": "&#39;"
992
+ };
993
+ return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char]);
1522
994
  }
1523
995
 
1524
996
  // src/core/email-template.ts
@@ -1540,8 +1012,10 @@ function generateEmailTemplate(options) {
1540
1012
  buttonFontWeight = 500,
1541
1013
  buttonRadius = "6px",
1542
1014
  logoUrl,
1543
- footerColor = "#1e1b4b"
1015
+ footerColor = "#1e1b4b",
1016
+ headingColor = "#1e1b4b"
1544
1017
  } = options;
1018
+ const safeAppName = escapeHtml(appName);
1545
1019
  const safeButtonUrl = buttonUrl ? sanitizeEmailUrl(buttonUrl, appUrl) : "";
1546
1020
  const safeAppUrl = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
1547
1021
  const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, appUrl) : "";
@@ -1685,7 +1159,7 @@ function generateEmailTemplate(options) {
1685
1159
  <td class="pad" style="padding-left:40px;padding-right:40px;width:100%;">
1686
1160
  <div class="alignment" align="center">
1687
1161
  <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">
1162
+ <img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${safeAppName}" title="${safeAppName}" height="auto">
1689
1163
  </div>
1690
1164
  </div>
1691
1165
  </td>
@@ -1697,7 +1171,7 @@ function generateEmailTemplate(options) {
1697
1171
  <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
1172
  <tr>
1699
1173
  <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;">
1174
+ <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
1175
  <p style="margin: 0; word-break: break-word;">${welcomeMessage}</p>
1702
1176
  </div>
1703
1177
  </td>
@@ -1710,7 +1184,7 @@ function generateEmailTemplate(options) {
1710
1184
  <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
1185
  <tr>
1712
1186
  <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;">
1187
+ <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
1188
  <p style="margin: 0; word-break: break-word;">${heading}</p>
1715
1189
  </div>
1716
1190
  </td>
@@ -1755,133 +1229,796 @@ function generateEmailTemplate(options) {
1755
1229
  </tr>
1756
1230
  </table>
1757
1231
 
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;">
1232
+ <!-- Copy Link -->
1233
+ <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
+ <tr>
1235
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1236
+ <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;">
1237
+ <p style="margin: 0 0 5px 0; word-break: break-word;">or copy and paste this link into your browser:</p>
1238
+ <a href="${safeButtonUrl}" style="color:#4a90d9;text-decoration:underline;word-break:break-all;">${safeButtonUrl}</a>
1239
+ </div>
1240
+ </td>
1241
+ </tr>
1242
+ </table>
1243
+ ` : ""}
1244
+
1245
+ ${securityWarning ? `
1246
+ <!-- Security Warning -->
1247
+ <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;">
1248
+ <tr>
1249
+ <td class="pad" style="padding-left:40px;padding-right:40px;padding-bottom:20px;">
1250
+ <div style="background-color: #a1f1ff; border-radius: 8px; padding: 16px; display: flex; align-items: center; justify-content: center;">
1251
+ <p style="margin: 0; color: #01001b; font-family: 'Montserrat', Arial, sans-serif; font-size: 13px; line-height: 1.5; text-align: center;">
1252
+ ${securityWarning}
1253
+ </p>
1254
+ </div>
1255
+ </td>
1256
+ </tr>
1257
+ </table>
1258
+ ` : ""}
1259
+
1260
+ <!-- Footer Note -->
1261
+ <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;">
1262
+ <tr>
1263
+ <td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
1264
+ <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;">
1265
+ <p style="margin: 0; word-break: break-word;">${footerNote}</p>
1266
+ </div>
1267
+ </td>
1268
+ </tr>
1269
+ </table>
1270
+
1271
+ </td>
1272
+ </tr>
1273
+ </tbody>
1274
+ </table>
1275
+ </td>
1276
+ </tr>
1277
+ </tbody>
1278
+ </table>
1279
+
1280
+ <!-- Spacer Row -->
1281
+ <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;">
1282
+ <tbody>
1283
+ <tr>
1284
+ <td>
1285
+ <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;">
1286
+ <tbody>
1287
+ <tr>
1288
+ <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;">
1289
+ <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;">
1290
+ <tr>
1291
+ <td class="pad">
1292
+ <div></div>
1293
+ </td>
1294
+ </tr>
1295
+ </table>
1296
+ </td>
1297
+ </tr>
1298
+ </tbody>
1299
+ </table>
1300
+ </td>
1301
+ </tr>
1302
+ </tbody>
1303
+ </table>
1304
+
1305
+ <!-- Footer Row -->
1306
+ <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;">
1307
+ <tbody>
1308
+ <tr>
1309
+ <td>
1310
+ <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;">
1311
+ <tbody>
1312
+ <tr>
1313
+ <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;">
1314
+
1315
+ <!-- Policy Links -->
1316
+ <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;">
1317
+ <tr>
1318
+ <td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
1319
+ <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;">
1320
+ <p style="margin: 0; word-break: break-word;">
1321
+ <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>
1322
+ </p>
1323
+ </div>
1324
+ </td>
1325
+ </tr>
1326
+ </table>
1327
+
1328
+ <!-- Copyright & Disclaimer -->
1329
+ <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
1330
  <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>
1331
+ <td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
1332
+ <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;">
1333
+ <p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
1334
+ <p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
1335
+ 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.
1336
+ </p>
1765
1337
  </div>
1766
1338
  </td>
1767
1339
  </tr>
1768
1340
  </table>
1769
- ` : ""}
1770
1341
 
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
- ` : ""}
1342
+ </td>
1343
+ </tr>
1344
+ </tbody>
1345
+ </table>
1346
+ </td>
1347
+ </tr>
1348
+ </tbody>
1349
+ </table>
1350
+
1351
+ </td>
1352
+ </tr>
1353
+ </tbody>
1354
+ </table>
1355
+ </body>
1356
+ </html>
1357
+ `;
1358
+ }
1359
+
1360
+ // src/core/passwordless.ts
1361
+ var PASSWORDLESS_TTL_MS = 10 * 60 * 1e3;
1362
+ var PASSWORDLESS_MAX_ATTEMPTS = 5;
1363
+ var CODE_SPACE = 1e6;
1364
+ var CODE_LIMIT = Math.floor(4294967296 / CODE_SPACE) * CODE_SPACE;
1365
+ var CLEANUP_GRACE_MS = 24 * 60 * 60 * 1e3;
1366
+ var normalizeEmail = (email) => email.trim().toLowerCase();
1367
+ function generateCode() {
1368
+ const buffer = new Uint32Array(1);
1369
+ for (; ; ) {
1370
+ crypto.getRandomValues(buffer);
1371
+ if (buffer[0] < CODE_LIMIT) return String(buffer[0] % CODE_SPACE).padStart(6, "0");
1372
+ }
1373
+ }
1374
+ var codeHash = (challengeId, code) => hashToken(`${challengeId}:${code}`);
1375
+ function constantTimeEqual(a, b) {
1376
+ if (a.length !== b.length) return false;
1377
+ let diff = 0;
1378
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1379
+ return diff === 0;
1380
+ }
1381
+ async function consumeOpen(db, t, email, now) {
1382
+ await db.update(t).set({ consumedAt: now }).where(and2(eq4(t.email, email), isNull(t.consumedAt)));
1383
+ }
1384
+ async function createPasswordlessChallenge(db, tables2, input) {
1385
+ const t = tables2.passwordlessChallenges;
1386
+ const email = normalizeEmail(input.email);
1387
+ const now = Date.now();
1388
+ const challengeId = crypto.randomUUID();
1389
+ const linkToken = generateSecureToken(32);
1390
+ const browserSecret = generateSecureToken(32);
1391
+ const code = generateCode();
1392
+ await consumeOpen(db, t, email, now);
1393
+ await db.insert(t).values({
1394
+ id: challengeId,
1395
+ email,
1396
+ subjectId: input.subjectId ?? null,
1397
+ linkTokenHash: await hashToken(linkToken),
1398
+ codeHash: await codeHash(challengeId, code),
1399
+ browserHash: await hashToken(browserSecret),
1400
+ attempts: 0,
1401
+ payload: input.payload ?? null,
1402
+ expiresAt: now + (input.ttlMs ?? PASSWORDLESS_TTL_MS),
1403
+ createdAt: now
1404
+ });
1405
+ return { challengeId, linkToken, code, browserSecret };
1406
+ }
1407
+ async function verifyPasswordlessLink(db, tables2, input) {
1408
+ const t = tables2.passwordlessChallenges;
1409
+ const now = Date.now();
1410
+ const hash = await hashToken(input.linkToken);
1411
+ const [row] = await db.update(t).set({ consumedAt: now }).where(and2(eq4(t.linkTokenHash, hash), isNull(t.consumedAt), gt2(t.expiresAt, now))).returning();
1412
+ if (!row) {
1413
+ const [stale] = await db.select({ expiresAt: t.expiresAt, consumedAt: t.consumedAt }).from(t).where(eq4(t.linkTokenHash, hash)).limit(1);
1414
+ const expired = stale && stale.consumedAt === null && stale.expiresAt <= now;
1415
+ return { ok: false, reason: expired ? "expired" : "invalid" };
1416
+ }
1417
+ await consumeOpen(db, t, row.email, now);
1418
+ return { ok: true, email: row.email, subjectId: row.subjectId, payload: row.payload };
1419
+ }
1420
+ async function verifyPasswordlessCode(db, tables2, input) {
1421
+ const t = tables2.passwordlessChallenges;
1422
+ const max = input.maxAttempts ?? PASSWORDLESS_MAX_ATTEMPTS;
1423
+ const email = normalizeEmail(input.email);
1424
+ const now = Date.now();
1425
+ const [row] = await db.select().from(t).where(and2(eq4(t.email, email), isNull(t.consumedAt))).orderBy(desc(t.createdAt)).limit(1);
1426
+ if (!row) return { ok: false, reason: "invalid" };
1427
+ if (!constantTimeEqual(row.browserHash, await hashToken(input.browserSecret))) {
1428
+ return { ok: false, reason: "invalid" };
1429
+ }
1430
+ if (row.expiresAt <= now) return { ok: false, reason: "expired" };
1431
+ if (row.attempts >= max) return { ok: false, reason: "locked" };
1432
+ if (!constantTimeEqual(row.codeHash, await codeHash(row.id, input.code.trim()))) {
1433
+ 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 });
1434
+ return { ok: false, reason: counted && counted.attempts < max ? "invalid" : "locked" };
1435
+ }
1436
+ const [won] = await db.update(t).set({ consumedAt: now }).where(and2(eq4(t.id, row.id), isNull(t.consumedAt), lt(t.attempts, max))).returning();
1437
+ if (!won) return { ok: false, reason: "invalid" };
1438
+ await consumeOpen(db, t, email, now);
1439
+ return { ok: true, email, subjectId: won.subjectId, payload: won.payload };
1440
+ }
1441
+ async function recentPasswordlessFailures(db, tables2, input) {
1442
+ const t = tables2.passwordlessChallenges;
1443
+ 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)));
1444
+ return Number(row?.total ?? 0);
1445
+ }
1446
+ async function cleanupExpiredPasswordlessChallenges(db, tables2) {
1447
+ const t = tables2.passwordlessChallenges;
1448
+ await db.delete(t).where(lt(t.expiresAt, Date.now() - CLEANUP_GRACE_MS));
1449
+ }
1450
+ function passwordlessEmail(input) {
1451
+ const { appName, appUrl, linkUrl, code } = input;
1452
+ const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
1453
+ const lifetime = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1454
+ const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
1455
+ const html = generateEmailTemplate({
1456
+ heading: `Sign in to ${escapeHtml(appName)}`,
1457
+ body: `<p>Use this code, or the button below, to sign in:</p>
1458
+ <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
1459
+ <p>The code and the link expire in ${lifetime}.</p>`,
1460
+ buttonText: "Sign in",
1461
+ buttonUrl: safeLink,
1462
+ footerNote: "If you didn't ask to sign in, you can ignore this email.",
1463
+ appUrl,
1464
+ appName
1465
+ });
1466
+ return {
1467
+ subject: `${code} is your sign-in code`,
1468
+ html,
1469
+ text: `Your ${appName} sign-in code is ${code}.
1470
+
1471
+ Or sign in with this link:
1472
+ ${safeLink}
1473
+
1474
+ The code and the link expire in ${lifetime}.
1475
+
1476
+ If you didn't ask to sign in, you can ignore this email.`
1477
+ };
1478
+ }
1479
+
1480
+ // src/middleware/auth.ts
1481
+ import { createMiddleware } from "hono/factory";
1482
+ import { eq as eq5, and as and3, gt as gt3 } from "drizzle-orm";
1483
+
1484
+ // src/factory.ts
1485
+ function createAuth(config) {
1486
+ const { db, schema, getEnv, emailBranding, invitations, signup = "open" } = config;
1487
+ if (signup === "invitation" && !invitations) {
1488
+ throw new Error("createAuth: signup: 'invitation' requires the invitations hooks");
1489
+ }
1490
+ const getContext = (c) => ({
1491
+ db,
1492
+ schema,
1493
+ env: getEnv(c),
1494
+ emailBranding,
1495
+ invitations,
1496
+ signup
1497
+ });
1498
+ return {
1499
+ db,
1500
+ schema,
1501
+ getContext
1502
+ };
1503
+ }
1504
+ function createAuthMiddleware(auth2) {
1505
+ return async (c, next) => {
1506
+ const ctx = auth2.getContext(c);
1507
+ c.set("authContext", ctx);
1508
+ c.set("db", auth2.db);
1509
+ await next();
1510
+ };
1511
+ }
1512
+ function getAuthContext(c) {
1513
+ const ctx = c.get("authContext");
1514
+ if (!ctx) {
1515
+ throw new Error(
1516
+ "AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
1517
+ );
1518
+ }
1519
+ return ctx;
1520
+ }
1521
+
1522
+ // src/lib/problem-json.ts
1523
+ var ProblemTypes = {
1524
+ // 4xx Client Errors
1525
+ BAD_REQUEST: "/errors/bad-request",
1526
+ UNAUTHORIZED: "/errors/unauthorized",
1527
+ FORBIDDEN: "/errors/forbidden",
1528
+ NOT_FOUND: "/errors/not-found",
1529
+ CONFLICT: "/errors/conflict",
1530
+ GONE: "/errors/gone",
1531
+ VALIDATION_ERROR: "/errors/validation-error",
1532
+ RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
1533
+ // 5xx Server Errors
1534
+ INTERNAL_ERROR: "/errors/internal-error",
1535
+ SERVICE_UNAVAILABLE: "/errors/service-unavailable"
1536
+ };
1537
+ function generateTraceId() {
1538
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
1539
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1540
+ }
1541
+ function createProblemDetails(type, title, status, options) {
1542
+ const problem = {
1543
+ type,
1544
+ title,
1545
+ status
1546
+ };
1547
+ if (options?.detail) {
1548
+ problem.detail = options.detail;
1549
+ }
1550
+ if (options?.instance) {
1551
+ problem.instance = options.instance;
1552
+ }
1553
+ problem.traceId = options?.traceId || generateTraceId();
1554
+ if (options?.requestId) {
1555
+ problem.requestId = options.requestId;
1556
+ }
1557
+ if (options?.extensions) {
1558
+ Object.assign(problem, options.extensions);
1559
+ }
1560
+ return problem;
1561
+ }
1562
+ function problemJson(c, problem) {
1563
+ logger_default.warn("API error response", {
1564
+ type: problem.type,
1565
+ status: problem.status,
1566
+ traceId: problem.traceId,
1567
+ requestId: problem.requestId,
1568
+ path: c.req.path,
1569
+ method: c.req.method
1570
+ });
1571
+ return c.json(problem, problem.status, {
1572
+ "Content-Type": "application/problem+json"
1573
+ });
1574
+ }
1575
+ var problems = {
1576
+ /**
1577
+ * 400 Bad Request - Generic client error
1578
+ */
1579
+ badRequest(c, detail) {
1580
+ return problemJson(
1581
+ c,
1582
+ createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
1583
+ detail: detail || "The request could not be understood by the server",
1584
+ instance: `${c.req.method} ${c.req.path}`,
1585
+ requestId: c.get("requestId"),
1586
+ traceId: c.get("traceId")
1587
+ })
1588
+ );
1589
+ },
1590
+ /**
1591
+ * 400 Bad Request - Validation errors with field-specific details
1592
+ */
1593
+ validationError(c, detail, errors) {
1594
+ return problemJson(
1595
+ c,
1596
+ createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
1597
+ detail,
1598
+ instance: `${c.req.method} ${c.req.path}`,
1599
+ requestId: c.get("requestId"),
1600
+ traceId: c.get("traceId"),
1601
+ extensions: errors ? { errors } : void 0
1602
+ })
1603
+ );
1604
+ },
1605
+ /**
1606
+ * 401 Unauthorized - Authentication required
1607
+ */
1608
+ unauthorized(c, detail) {
1609
+ return problemJson(
1610
+ c,
1611
+ createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
1612
+ detail: detail || "Authentication is required to access this resource",
1613
+ instance: `${c.req.method} ${c.req.path}`,
1614
+ requestId: c.get("requestId"),
1615
+ traceId: c.get("traceId")
1616
+ })
1617
+ );
1618
+ },
1619
+ /**
1620
+ * 403 Forbidden - Insufficient permissions
1621
+ */
1622
+ forbidden(c, detail) {
1623
+ return problemJson(
1624
+ c,
1625
+ createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
1626
+ detail: detail || "You do not have permission to access this resource",
1627
+ instance: `${c.req.method} ${c.req.path}`,
1628
+ requestId: c.get("requestId"),
1629
+ traceId: c.get("traceId")
1630
+ })
1631
+ );
1632
+ },
1633
+ /**
1634
+ * 404 Not Found
1635
+ */
1636
+ notFound(c, detail) {
1637
+ return problemJson(
1638
+ c,
1639
+ createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
1640
+ detail: detail || "The requested resource was not found",
1641
+ instance: `${c.req.method} ${c.req.path}`,
1642
+ requestId: c.get("requestId"),
1643
+ traceId: c.get("traceId")
1644
+ })
1645
+ );
1646
+ },
1647
+ /**
1648
+ * 409 Conflict - Resource conflict (e.g., duplicate email)
1649
+ */
1650
+ conflict(c, detail) {
1651
+ return problemJson(
1652
+ c,
1653
+ createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
1654
+ detail,
1655
+ instance: `${c.req.method} ${c.req.path}`,
1656
+ requestId: c.get("requestId"),
1657
+ traceId: c.get("traceId")
1658
+ })
1659
+ );
1660
+ },
1661
+ /**
1662
+ * 410 Gone - Resource is no longer available
1663
+ */
1664
+ gone(c, detail) {
1665
+ return problemJson(
1666
+ c,
1667
+ createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
1668
+ detail: detail || "The requested resource is no longer available",
1669
+ instance: `${c.req.method} ${c.req.path}`,
1670
+ requestId: c.get("requestId"),
1671
+ traceId: c.get("traceId")
1672
+ })
1673
+ );
1674
+ },
1675
+ /**
1676
+ * 429 Too Many Requests - Rate limit exceeded
1677
+ */
1678
+ rateLimitExceeded(c, retryAfter) {
1679
+ const extensions = retryAfter ? { retryAfter } : void 0;
1680
+ return problemJson(
1681
+ c,
1682
+ createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
1683
+ detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
1684
+ instance: `${c.req.method} ${c.req.path}`,
1685
+ requestId: c.get("requestId"),
1686
+ traceId: c.get("traceId"),
1687
+ extensions
1688
+ })
1689
+ );
1690
+ },
1691
+ /**
1692
+ * 500 Internal Server Error - Generic server error
1693
+ * SECURITY: Never include error details or stack traces
1694
+ */
1695
+ internalError(c, error) {
1696
+ const traceId = c.get("traceId") || generateTraceId();
1697
+ if (error) {
1698
+ logError(error, {
1699
+ context: "internal_error",
1700
+ path: c.req.path,
1701
+ method: c.req.method,
1702
+ traceId,
1703
+ requestId: c.get("requestId")
1704
+ });
1705
+ }
1706
+ return problemJson(
1707
+ c,
1708
+ createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
1709
+ detail: "An unexpected error occurred. Please try again later",
1710
+ instance: `${c.req.method} ${c.req.path}`,
1711
+ traceId,
1712
+ requestId: c.get("requestId")
1713
+ })
1714
+ );
1715
+ },
1716
+ /**
1717
+ * 503 Service Unavailable - Service is temporarily unavailable
1718
+ */
1719
+ serviceUnavailable(c, detail) {
1720
+ return problemJson(
1721
+ c,
1722
+ createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
1723
+ detail: detail || "The service is temporarily unavailable. Please try again later",
1724
+ instance: `${c.req.method} ${c.req.path}`,
1725
+ requestId: c.get("requestId"),
1726
+ traceId: c.get("traceId")
1727
+ })
1728
+ );
1729
+ }
1730
+ };
1731
+
1732
+ // src/middleware/auth.ts
1733
+ var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
1734
+ async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
1735
+ if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
1736
+ logger_default.info("Session exceeded absolute lifetime - revoked", {
1737
+ type: "security",
1738
+ event: "session_max_lifetime_exceeded",
1739
+ severity: "low",
1740
+ sessionId: sessionId.slice(0, 8),
1741
+ userId: session.userId
1742
+ });
1743
+ await deleteSession(db, { sessions }, sessionId);
1744
+ return true;
1745
+ }
1746
+ async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
1747
+ const cookieName = getSessionCookieName(env);
1748
+ const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
1749
+ const sessionIdMatch = cookieHeader.match(cookiePattern);
1750
+ if (!sessionIdMatch) return null;
1751
+ const sessionId = sessionIdMatch[1];
1752
+ const foundSessions = await db.select({
1753
+ userId: sessions.userId,
1754
+ expiresAt: sessions.expiresAt,
1755
+ createdAt: sessions.createdAt,
1756
+ lastActiveAt: sessions.lastActiveAt,
1757
+ fingerprint: sessions.fingerprint
1758
+ }).from(sessions).where(and3(eq5(sessions.id, sessionId), gt3(sessions.expiresAt, Date.now()))).limit(1);
1759
+ if (foundSessions.length === 0) return null;
1760
+ const session = foundSessions[0];
1761
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1762
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
1763
+ const userAgent = request.headers.get("user-agent") || "";
1764
+ const cfWorker = request.headers.get("cf-worker") || "";
1765
+ const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
1766
+ const isLocalSSR = userAgent.toLowerCase().includes("node");
1767
+ const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
1768
+ const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
1769
+ const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
1770
+ if (isSSR) {
1771
+ logger_default.info("SSR request detected, using IP validation", {
1772
+ type: "security",
1773
+ event: "ssr_session_validation",
1774
+ sessionId: sessionId.slice(0, 8),
1775
+ userId: session.userId,
1776
+ clientIp,
1777
+ cfWorker,
1778
+ isLocalSSR,
1779
+ isSvelteKitSSR,
1780
+ isCloudflareSSR
1781
+ });
1782
+ } else {
1783
+ if (session.fingerprint) {
1784
+ const currentFingerprint = await generateFingerprint(request);
1785
+ if (session.fingerprint !== currentFingerprint) {
1786
+ logger_default.info("Session fingerprint changed (not enforced)", {
1787
+ type: "security",
1788
+ event: "fingerprint_changed",
1789
+ sessionId: sessionId.slice(0, 8),
1790
+ userId: session.userId,
1791
+ clientIp
1792
+ });
1793
+ }
1794
+ }
1795
+ }
1796
+ return {
1797
+ userId: session.userId,
1798
+ expiresAt: session.expiresAt
1799
+ };
1800
+ }
1801
+ async function getSessionFromBearerToken(db, sessions, authHeader) {
1802
+ if (!authHeader.startsWith("Bearer ")) return null;
1803
+ const sessionId = authHeader.slice(7);
1804
+ const foundSessions = await db.select({
1805
+ userId: sessions.userId,
1806
+ expiresAt: sessions.expiresAt,
1807
+ createdAt: sessions.createdAt,
1808
+ lastActiveAt: sessions.lastActiveAt
1809
+ }).from(sessions).where(and3(eq5(sessions.id, sessionId), gt3(sessions.expiresAt, Date.now()))).limit(1);
1810
+ if (foundSessions.length === 0) return null;
1811
+ const session = foundSessions[0];
1812
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1813
+ if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
1814
+ return {
1815
+ userId: session.userId,
1816
+ expiresAt: session.expiresAt
1817
+ };
1818
+ }
1819
+ var requireAuth = createMiddleware(
1820
+ async (c, next) => {
1821
+ const { db, schema } = getAuthContext(c);
1822
+ const authHeader = c.req.header("Authorization");
1823
+ if (authHeader) {
1824
+ const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1825
+ if (session2) {
1826
+ c.set("userId", session2.userId);
1827
+ await next();
1828
+ return;
1829
+ }
1830
+ }
1831
+ const cookieHeader = c.req.header("cookie");
1832
+ if (!cookieHeader) {
1833
+ return problems.unauthorized(c, "Please log in to continue");
1834
+ }
1835
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1836
+ if (!session) {
1837
+ c.header("Set-Cookie", clearSessionCookie(c.env));
1838
+ return problems.unauthorized(c, "Your session has expired. Please log in again.");
1839
+ }
1840
+ c.set("userId", session.userId);
1841
+ await next();
1842
+ }
1843
+ );
1844
+ var optionalAuth = createMiddleware(
1845
+ async (c, next) => {
1846
+ const { db, schema } = getAuthContext(c);
1847
+ const authHeader = c.req.header("Authorization");
1848
+ if (authHeader) {
1849
+ const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
1850
+ if (session) {
1851
+ c.set("userId", session.userId);
1852
+ await next();
1853
+ return;
1854
+ }
1855
+ }
1856
+ const cookieHeader = c.req.header("cookie");
1857
+ if (cookieHeader) {
1858
+ const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
1859
+ if (session) {
1860
+ c.set("userId", session.userId);
1861
+ }
1862
+ }
1863
+ await next();
1864
+ }
1865
+ );
1785
1866
 
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>
1867
+ // src/middleware/csrf.ts
1868
+ import { createMiddleware as createMiddleware2 } from "hono/factory";
1869
+ var csrf = createMiddleware2(async (c, next) => {
1870
+ const method = c.req.method;
1871
+ const path = c.req.path;
1872
+ const isWebhook = path.startsWith("/v1/webhooks/");
1873
+ if (isWebhook) {
1874
+ await next();
1875
+ return;
1876
+ }
1877
+ const isMcpEndpoint = path === "/v1/mcp";
1878
+ if (isMcpEndpoint) {
1879
+ await next();
1880
+ return;
1881
+ }
1882
+ const isOAuthMcpEndpoint = path.startsWith("/v1/oauth/");
1883
+ if (isOAuthMcpEndpoint) {
1884
+ await next();
1885
+ return;
1886
+ }
1887
+ const isToolsApi = path.startsWith("/v1/tools/") && !path.startsWith("/v1/tools/catalog");
1888
+ if (isToolsApi) {
1889
+ await next();
1890
+ return;
1891
+ }
1892
+ const isAdminApi = path.startsWith("/v1/admin/");
1893
+ if (isAdminApi) {
1894
+ await next();
1895
+ return;
1896
+ }
1897
+ const isMobileAuthEndpoint = path === "/v1/auth/login" || path === "/v1/auth/signup" || path === "/v1/auth/refresh";
1898
+ const authHeader = c.req.header("Authorization");
1899
+ const hasBearerToken = authHeader?.startsWith("Bearer ");
1900
+ const hasNoOrigin = !c.req.header("origin") && !c.req.header("referer");
1901
+ if (isMobileAuthEndpoint && hasNoOrigin || hasBearerToken) {
1902
+ await next();
1903
+ return;
1904
+ }
1905
+ const isInternalSync = path.startsWith("/v1/internal/sync/");
1906
+ if (isInternalSync) {
1907
+ await next();
1908
+ return;
1909
+ }
1910
+ if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
1911
+ const appUrl = c.env.APP_URL || "http://localhost:5173";
1912
+ const allowedOrigins = getAllowedOrigins(appUrl);
1913
+ const request = c.req.raw;
1914
+ const isTestEndpoint = c.req.path.startsWith("/test");
1915
+ if (isTestEndpoint) {
1916
+ const origin = request.headers.get("origin");
1917
+ const referer = request.headers.get("referer");
1918
+ logger_default.debug("CSRF validation for test endpoint", {
1919
+ path: c.req.path,
1920
+ method,
1921
+ origin,
1922
+ referer,
1923
+ appUrl,
1924
+ allowedOrigins
1925
+ });
1926
+ }
1927
+ const pagesPattern = c.env.CORS_PAGES_PATTERN;
1928
+ if (!validateOrigin(request, allowedOrigins, pagesPattern)) {
1929
+ return problems.forbidden(c, "Request blocked for security reasons. Please try again.");
1930
+ }
1931
+ }
1932
+ await next();
1933
+ });
1796
1934
 
1797
- </td>
1798
- </tr>
1799
- </tbody>
1800
- </table>
1801
- </td>
1802
- </tr>
1803
- </tbody>
1804
- </table>
1935
+ // src/middleware/rateLimit.ts
1936
+ import { createMiddleware as createMiddleware3 } from "hono/factory";
1805
1937
 
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>
1938
+ // src/lib/rate-limit-kv.ts
1939
+ async function checkRateLimitKV(kv, key, maxAttempts, windowSeconds) {
1940
+ try {
1941
+ const rateLimitKey = `rate_limit:${key}`;
1942
+ const currentAttemptsStr = await kv.get(rateLimitKey);
1943
+ const currentAttempts = currentAttemptsStr ? parseInt(currentAttemptsStr, 10) : 0;
1944
+ if (currentAttempts >= maxAttempts) {
1945
+ return {
1946
+ allowed: false,
1947
+ remaining: 0
1948
+ };
1949
+ }
1950
+ const newAttempts = currentAttempts + 1;
1951
+ await kv.put(rateLimitKey, newAttempts.toString(), {
1952
+ expirationTtl: windowSeconds
1953
+ });
1954
+ return {
1955
+ allowed: true,
1956
+ remaining: maxAttempts - newAttempts
1957
+ };
1958
+ } catch (error) {
1959
+ logger_default.error("Rate limit KV error, failing open", {
1960
+ key,
1961
+ error: error instanceof Error ? error.message : "Unknown error"
1962
+ });
1963
+ return {
1964
+ allowed: true,
1965
+ remaining: maxAttempts
1966
+ };
1967
+ }
1968
+ }
1830
1969
 
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;">
1970
+ // src/middleware/rateLimit.ts
1971
+ var rateLimit = (config) => createMiddleware3(async (c, next) => {
1972
+ const kv = c.env.RATE_LIMIT_KV;
1973
+ const identifier = await config.identifier(c);
1974
+ const key = `${config.action}:${identifier}`;
1975
+ const windowSeconds = Math.ceil(config.windowMs / 1e3);
1976
+ const { allowed, remaining } = await checkRateLimitKV(
1977
+ kv,
1978
+ key,
1979
+ config.maxAttempts,
1980
+ windowSeconds
1981
+ );
1982
+ const resetTimestamp = Math.ceil(Date.now() / 1e3) + windowSeconds;
1983
+ c.header("X-RateLimit-Limit", config.maxAttempts.toString());
1984
+ c.header("X-RateLimit-Remaining", Math.max(0, remaining).toString());
1985
+ c.header("X-RateLimit-Reset", resetTimestamp.toString());
1986
+ if (!allowed) {
1987
+ c.header("Retry-After", windowSeconds.toString());
1988
+ return problems.rateLimitExceeded(c, windowSeconds);
1989
+ }
1990
+ await next();
1991
+ });
1840
1992
 
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>
1993
+ // src/middleware/require-verified-email.ts
1994
+ import { createMiddleware as createMiddleware4 } from "hono/factory";
1995
+ import { eq as eq6 } from "drizzle-orm";
1996
+ var requireVerifiedEmail = createMiddleware4(
1997
+ async (c, next) => {
1998
+ const userId = c.get("userId");
1999
+ if (!userId) {
2000
+ return problems.unauthorized(c, "Not authenticated");
2001
+ }
2002
+ const { db, schema } = getAuthContext(c);
2003
+ const database = db;
2004
+ const users = schema.users;
2005
+ const [user] = await database.select({ emailVerified: users.emailVerified }).from(users).where(eq6(users.id, userId)).limit(1);
2006
+ if (!user || !user.emailVerified) {
2007
+ return problems.forbidden(c, "Please verify your email address to continue");
2008
+ }
2009
+ await next();
2010
+ }
2011
+ );
1853
2012
 
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>
2013
+ // src/routes/index.ts
2014
+ import { OpenAPIHono as OpenAPIHono3 } from "@hono/zod-openapi";
1867
2015
 
1868
- </td>
1869
- </tr>
1870
- </tbody>
1871
- </table>
1872
- </td>
1873
- </tr>
1874
- </tbody>
1875
- </table>
2016
+ // src/routes/signup.ts
2017
+ import { createRoute } from "@hono/zod-openapi";
2018
+ import { eq as eq9 } from "drizzle-orm";
1876
2019
 
1877
- </td>
1878
- </tr>
1879
- </tbody>
1880
- </table>
1881
- </body>
1882
- </html>
1883
- `;
1884
- }
2020
+ // src/lib/email/email-service.ts
2021
+ import { eq as eq7 } from "drizzle-orm";
1885
2022
 
1886
2023
  // src/lib/email/templates.ts
1887
2024
  function extractAppUrl(url) {
@@ -1905,6 +2042,7 @@ function generateVerificationEmail(data, appName = "Your App", configuredAppUrl
1905
2042
  welcomeMessage: safeFirstName ? `Hi ${safeFirstName}, Welcome to ${escapeHtml(appName)}!` : void 0,
1906
2043
  footerNote: "",
1907
2044
  appUrl,
2045
+ appName,
1908
2046
  ...branding
1909
2047
  });
1910
2048
  return {
@@ -1940,6 +2078,7 @@ function generatePasswordResetEmail(data, appName = "Your App", configuredAppUrl
1940
2078
  `,
1941
2079
  footerNote: "If you didn't request this change, no action is needed. Your password will remain unchanged.",
1942
2080
  appUrl,
2081
+ appName,
1943
2082
  ...branding
1944
2083
  });
1945
2084
  return {
@@ -1975,6 +2114,7 @@ function generateEmailChangeConfirmation(data, appName = "Your App", configuredA
1975
2114
  `,
1976
2115
  footerNote: "If you didn't request this change, you can safely ignore this email.",
1977
2116
  appUrl,
2117
+ appName,
1978
2118
  ...branding
1979
2119
  });
1980
2120
  return {
@@ -2013,6 +2153,7 @@ function generateEmailChangeNotification(data, appName = "Your App", configuredA
2013
2153
  `,
2014
2154
  footerNote: "If you didn't request this change, click the button above to cancel it.",
2015
2155
  appUrl,
2156
+ appName,
2016
2157
  ...branding
2017
2158
  });
2018
2159
  return {
@@ -2049,6 +2190,7 @@ function generate2faCodeEmail(data, appName = "Your App", appUrl = "", branding)
2049
2190
  buttonUrl: "",
2050
2191
  footerNote: "",
2051
2192
  appUrl,
2193
+ appName,
2052
2194
  ...branding
2053
2195
  });
2054
2196
  return {
@@ -2084,6 +2226,7 @@ function generate2faEnabledEmail(data, appName = "Your App", appUrl = "", brandi
2084
2226
  buttonUrl: "",
2085
2227
  footerNote: "",
2086
2228
  appUrl,
2229
+ appName,
2087
2230
  ...branding
2088
2231
  });
2089
2232
  return {
@@ -2114,6 +2257,7 @@ function generate2faDisabledEmail(data, appName = "Your App", appUrl = "", brand
2114
2257
  buttonUrl: appUrl ? `${appUrl}/forgot-password` : "",
2115
2258
  footerNote: "",
2116
2259
  appUrl,
2260
+ appName,
2117
2261
  ...branding
2118
2262
  });
2119
2263
  return {
@@ -2330,7 +2474,7 @@ var EmailService = class {
2330
2474
  const [user] = await db.select({
2331
2475
  emailBounced: usersTable.emailBounced,
2332
2476
  emailComplained: usersTable.emailComplained
2333
- }).from(usersTable).where(eq6(usersTable.email, emailAddress.toLowerCase())).limit(1);
2477
+ }).from(usersTable).where(eq7(usersTable.email, emailAddress.toLowerCase())).limit(1);
2334
2478
  if (!user) {
2335
2479
  return { blocked: false };
2336
2480
  }
@@ -2779,7 +2923,7 @@ var EmailService = class {
2779
2923
  import { Resend as Resend2 } from "resend";
2780
2924
 
2781
2925
  // src/lib/email/webhook-handler.ts
2782
- import { eq as eq7 } from "drizzle-orm";
2926
+ import { eq as eq8 } from "drizzle-orm";
2783
2927
  async function handleWebhookEvent(payload, db, tables2) {
2784
2928
  const { type, data } = payload;
2785
2929
  const emailAddress = data.to[0];
@@ -2838,7 +2982,7 @@ async function handleBounce(emailAddress, emailId, bounce, db, usersTable) {
2838
2982
  await db.update(usersTable).set({
2839
2983
  emailBounced: true,
2840
2984
  emailBouncedAt: /* @__PURE__ */ new Date()
2841
- }).where(eq7(usersTable.email, emailAddress.toLowerCase()));
2985
+ }).where(eq8(usersTable.email, emailAddress.toLowerCase()));
2842
2986
  logError(new Error("Email bounced"), {
2843
2987
  context: "email_webhook",
2844
2988
  emailAddress,
@@ -2854,7 +2998,7 @@ async function handleComplaint(emailAddress, emailId, db, usersTable) {
2854
2998
  await db.update(usersTable).set({
2855
2999
  emailComplained: true,
2856
3000
  emailComplainedAt: /* @__PURE__ */ new Date()
2857
- }).where(eq7(usersTable.email, emailAddress.toLowerCase()));
3001
+ }).where(eq8(usersTable.email, emailAddress.toLowerCase()));
2858
3002
  logError(new Error("Email spam complaint received"), {
2859
3003
  context: "email_webhook",
2860
3004
  emailAddress,
@@ -2876,15 +3020,15 @@ async function handleFailure(emailAddress, emailId, failed) {
2876
3020
  }
2877
3021
 
2878
3022
  // src/lib/email/monitoring.ts
2879
- import { sql as sql2 } from "drizzle-orm";
3023
+ import { sql as sql3 } from "drizzle-orm";
2880
3024
  async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
2881
3025
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
2882
3026
  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(
3027
+ totalSent: sql3`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3028
+ totalBounced: sql3`count(*) filter (where event_type = 'email.bounced')`.as(
2885
3029
  "total_bounced"
2886
3030
  )
2887
- }).from(emailEventsTable).where(sql2`created_at > ${cutoff}`);
3031
+ }).from(emailEventsTable).where(sql3`created_at > ${cutoff}`);
2888
3032
  if (!stats || stats.totalSent === 0) {
2889
3033
  return 0;
2890
3034
  }
@@ -2903,11 +3047,11 @@ async function calculateBounceRate(db, emailEventsTable, hoursAgo = 24) {
2903
3047
  async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
2904
3048
  const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1e3);
2905
3049
  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(
3050
+ totalSent: sql3`count(*) filter (where event_type = 'email.sent')`.as("total_sent"),
3051
+ totalComplained: sql3`count(*) filter (where event_type = 'email.complained')`.as(
2908
3052
  "total_complained"
2909
3053
  )
2910
- }).from(emailEventsTable).where(sql2`created_at > ${cutoff}`);
3054
+ }).from(emailEventsTable).where(sql3`created_at > ${cutoff}`);
2911
3055
  if (!stats || stats.totalSent === 0) {
2912
3056
  return 0;
2913
3057
  }
@@ -2924,16 +3068,34 @@ async function calculateComplaintRate(db, emailEventsTable, hoursAgo = 24) {
2924
3068
  return complaintRate;
2925
3069
  }
2926
3070
 
3071
+ // src/core/invitations.ts
3072
+ async function verifyInvitation(invitations, args) {
3073
+ if (!invitations || !args.token) return null;
3074
+ const email = args.email.toLowerCase();
3075
+ try {
3076
+ const match = await invitations.verify({ token: args.token, email, db: args.db, env: args.env });
3077
+ return match && match.email.toLowerCase() === email ? args.token : null;
3078
+ } catch (err) {
3079
+ logError(err, { context: "invitation_verify_failed", email });
3080
+ return null;
3081
+ }
3082
+ }
3083
+ async function acceptInvitation(invitations, args) {
3084
+ if (!invitations?.onAccepted) return;
3085
+ try {
3086
+ await invitations.onAccepted({ token: args.token, user: args.user, db: args.db, env: args.env });
3087
+ } catch (err) {
3088
+ logError(err, { context: args.context, userId: args.user.id });
3089
+ }
3090
+ }
3091
+
2927
3092
  // src/routes/schemas.ts
2928
3093
  import { z } from "@hono/zod-openapi";
2929
3094
  var signupRequestSchema = z.object({
2930
3095
  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!" }),
3096
+ password: z.string().min(8).openapi({ example: "SecurePass123!" }),
2933
3097
  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-..." }),
3098
+ turnstileToken: z.string().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
2937
3099
  // App invitation token (from /signup?token=...). When it validates for this
2938
3100
  // email, the account is created already-verified with no verification email.
2939
3101
  invitationToken: z.string().optional().openapi({ example: "iIB8tDJJ..." })
@@ -3059,76 +3221,22 @@ var signupRoute = createRoute({
3059
3221
  description: "Invalid input",
3060
3222
  content: { "application/json": { schema: errorResponseSchema } }
3061
3223
  },
3224
+ 403: {
3225
+ description: "Sign-up is by invitation only and no valid invitation was given",
3226
+ content: { "application/json": { schema: errorResponseSchema } }
3227
+ },
3062
3228
  409: {
3063
3229
  description: "Email already registered",
3064
3230
  content: { "application/json": { schema: errorResponseSchema } }
3065
3231
  }
3066
3232
  }
3067
3233
  });
3234
+ var INVITATION_ONLY = "Sign-up is by invitation only";
3235
+ var INVITATION_INVALID = "This invitation is invalid, expired, or for a different email address";
3068
3236
  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);
3237
+ const { email, password, name, turnstileToken, invitationToken } = c.req.valid("json");
3238
+ const { db: database, schema, env: authEnv, emailBranding, invitations, signup } = getAuthContext(c);
3071
3239
  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
3240
  const turnstileValid = await verifyTurnstileToken(
3133
3241
  turnstileToken,
3134
3242
  env.TURNSTILE_SECRET_KEY,
@@ -3138,8 +3246,22 @@ var signupHandler = async (c) => {
3138
3246
  if (!turnstileValid) {
3139
3247
  return problems.badRequest(c, "Invalid captcha");
3140
3248
  }
3249
+ const invited = await verifyInvitation(invitations, {
3250
+ token: invitationToken,
3251
+ email,
3252
+ db: database,
3253
+ env: authEnv
3254
+ });
3255
+ if (invitationToken && invitations && !invited && signup !== "invitation") {
3256
+ return problems.badRequest(c, INVITATION_INVALID);
3257
+ }
3258
+ if (signup === "invitation" && !invited) {
3259
+ logSecurityEvent("signup_refused_no_invitation", "medium", { email: email.toLowerCase() });
3260
+ return problems.forbidden(c, invitationToken ? INVITATION_INVALID : INVITATION_ONLY);
3261
+ }
3262
+ const invitedVerified = invited !== null;
3141
3263
  const existingUser = await database.query.users.findFirst({
3142
- where: eq8(schema.users.email, email.toLowerCase())
3264
+ where: eq9(schema.users.email, email.toLowerCase())
3143
3265
  });
3144
3266
  if (existingUser) {
3145
3267
  return problems.conflict(c, "Email already registered");
@@ -3149,22 +3271,6 @@ var signupHandler = async (c) => {
3149
3271
  return problems.badRequest(c, passwordValidation.error || "Invalid password");
3150
3272
  }
3151
3273
  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
3274
  const [newUser] = await database.insert(schema.users).values({
3169
3275
  email: email.toLowerCase(),
3170
3276
  name: name || null,
@@ -3197,17 +3303,14 @@ var signupHandler = async (c) => {
3197
3303
  schema.users
3198
3304
  );
3199
3305
  }
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
- }
3306
+ if (invited) {
3307
+ await acceptInvitation(invitations, {
3308
+ token: invited,
3309
+ user: { id: newUser.id, email: newUser.email, name: newUser.name },
3310
+ db: database,
3311
+ env: authEnv,
3312
+ context: "signup_invitation_onAccepted_failed"
3313
+ });
3211
3314
  }
3212
3315
  const fingerprint = await generateFingerprint(c.req.raw);
3213
3316
  const ipAddress = getClientIp(c.req.raw);
@@ -3240,18 +3343,18 @@ var signupMiddleware = [
3240
3343
 
3241
3344
  // src/routes/login.ts
3242
3345
  import { createRoute as createRoute2 } from "@hono/zod-openapi";
3243
- import { eq as eq10 } from "drizzle-orm";
3346
+ import { eq as eq11 } from "drizzle-orm";
3244
3347
  import { getCookie } from "hono/cookie";
3245
3348
 
3246
3349
  // src/routes/2fa/helpers.ts
3247
- import { eq as eq9, and as and3, isNull } from "drizzle-orm";
3350
+ import { eq as eq10, and as and4, isNull as isNull2 } from "drizzle-orm";
3248
3351
  async function getUserEnabled2faMethods(database, userId, user2faMethodsTable) {
3249
3352
  return database.select({
3250
3353
  method: user2faMethodsTable.method,
3251
3354
  isPrimary: user2faMethodsTable.isPrimary,
3252
3355
  totpSecret: user2faMethodsTable.totpSecret,
3253
3356
  lastTotpCounter: user2faMethodsTable.lastTotpCounter
3254
- }).from(user2faMethodsTable).where(eq9(user2faMethodsTable.userId, userId));
3357
+ }).from(user2faMethodsTable).where(eq10(user2faMethodsTable.userId, userId));
3255
3358
  }
3256
3359
  function generateEmailOtp() {
3257
3360
  const bytes = crypto.getRandomValues(new Uint8Array(4));
@@ -3285,7 +3388,7 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3285
3388
  const secret = await decryptTotpSecret(totpMethod.totpSecret, env.TOTP_ENCRYPTION_KEY);
3286
3389
  const result = verifyTotpCode(secret, code, totpMethod.lastTotpCounter);
3287
3390
  if (result.valid) {
3288
- await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where(and3(eq9(user2faMethodsTable.userId, userId), eq9(user2faMethodsTable.method, "totp")));
3391
+ await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where(and4(eq10(user2faMethodsTable.userId, userId), eq10(user2faMethodsTable.method, "totp")));
3289
3392
  return { valid: true };
3290
3393
  }
3291
3394
  return { valid: false };
@@ -3297,18 +3400,18 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3297
3400
  const kvKey = `email_2fa_challenge:${userId}`;
3298
3401
  const storedHash = await env.OAUTH_STATES.get(kvKey);
3299
3402
  if (!storedHash) return { valid: false };
3300
- const codeHash = await hashToken(code);
3301
- if (codeHash === storedHash) {
3403
+ const codeHash2 = await hashToken(code);
3404
+ if (codeHash2 === storedHash) {
3302
3405
  await env.OAUTH_STATES.delete(kvKey);
3303
3406
  return { valid: true };
3304
3407
  }
3305
3408
  return { valid: false };
3306
3409
  }
3307
3410
  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)));
3411
+ const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where(and4(eq10(userBackupCodesTable.userId, userId), isNull2(userBackupCodesTable.usedAt)));
3309
3412
  for (const bc of backupCodes) {
3310
3413
  if (await verifyBackupCode(code, bc.codeHash)) {
3311
- await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where(eq9(userBackupCodesTable.id, bc.id));
3414
+ await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where(eq10(userBackupCodesTable.id, bc.id));
3312
3415
  logSecurityEvent("2fa_backup_code_used", "medium", { userId, codeId: bc.id });
3313
3416
  return { valid: true };
3314
3417
  }
@@ -3364,7 +3467,7 @@ var loginHandler = async (c) => {
3364
3467
  return problems.badRequest(c, "Invalid captcha");
3365
3468
  }
3366
3469
  const user = await database.query.users.findFirst({
3367
- where: eq10(schema.users.email, email.toLowerCase())
3470
+ where: eq11(schema.users.email, email.toLowerCase())
3368
3471
  });
3369
3472
  if (!user) {
3370
3473
  return problems.unauthorized(c, "Invalid email or password");
@@ -3473,7 +3576,7 @@ var loginMiddleware = [
3473
3576
 
3474
3577
  // src/routes/logout.ts
3475
3578
  import { createRoute as createRoute3 } from "@hono/zod-openapi";
3476
- import { eq as eq11 } from "drizzle-orm";
3579
+ import { eq as eq12 } from "drizzle-orm";
3477
3580
  var logoutRoute = createRoute3({
3478
3581
  method: "post",
3479
3582
  path: "/logout",
@@ -3496,7 +3599,7 @@ var logoutHandler = async (c) => {
3496
3599
  const sessionIdMatch = cookieHeader.match(cookiePattern);
3497
3600
  if (sessionIdMatch) {
3498
3601
  const sessionId = sessionIdMatch[1];
3499
- await database.delete(schema.sessions).where(eq11(schema.sessions.id, sessionId));
3602
+ await database.delete(schema.sessions).where(eq12(schema.sessions.id, sessionId));
3500
3603
  }
3501
3604
  }
3502
3605
  c.header("Set-Cookie", clearSessionCookie(c.env), { append: true });
@@ -3505,7 +3608,7 @@ var logoutHandler = async (c) => {
3505
3608
 
3506
3609
  // src/routes/me.ts
3507
3610
  import { createRoute as createRoute4 } from "@hono/zod-openapi";
3508
- import { eq as eq12 } from "drizzle-orm";
3611
+ import { eq as eq13 } from "drizzle-orm";
3509
3612
  var meRoute = createRoute4({
3510
3613
  method: "get",
3511
3614
  path: "/me",
@@ -3533,7 +3636,7 @@ var meHandler = async (c) => {
3533
3636
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3534
3637
  const { db: database, schema } = getAuthContext(c);
3535
3638
  const user = await database.query.users.findFirst({
3536
- where: eq12(schema.users.id, userId)
3639
+ where: eq13(schema.users.id, userId)
3537
3640
  });
3538
3641
  if (!user) {
3539
3642
  return problems.notFound(c, "User not found");
@@ -3559,7 +3662,7 @@ var meMiddleware = [requireAuth];
3559
3662
 
3560
3663
  // src/routes/verify-email.ts
3561
3664
  import { createRoute as createRoute5 } from "@hono/zod-openapi";
3562
- import { eq as eq13 } from "drizzle-orm";
3665
+ import { eq as eq14 } from "drizzle-orm";
3563
3666
  var verifyEmailRoute = createRoute5({
3564
3667
  method: "post",
3565
3668
  path: "/verify-email",
@@ -3588,17 +3691,17 @@ var verifyEmailHandler = async (c) => {
3588
3691
  const { db: database, schema } = getAuthContext(c);
3589
3692
  const tokenHash = await hashToken(token);
3590
3693
  const verificationToken = await database.query.emailVerificationTokens.findFirst({
3591
- where: eq13(schema.emailVerificationTokens.tokenHash, tokenHash)
3694
+ where: eq14(schema.emailVerificationTokens.tokenHash, tokenHash)
3592
3695
  });
3593
3696
  if (!verificationToken) {
3594
3697
  return problems.badRequest(c, "Invalid or expired verification token");
3595
3698
  }
3596
3699
  if (verificationToken.expiresAt < Date.now()) {
3597
- await database.delete(schema.emailVerificationTokens).where(eq13(schema.emailVerificationTokens.tokenHash, tokenHash));
3700
+ await database.delete(schema.emailVerificationTokens).where(eq14(schema.emailVerificationTokens.tokenHash, tokenHash));
3598
3701
  return problems.badRequest(c, "Verification token has expired");
3599
3702
  }
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));
3703
+ await database.update(schema.users).set({ emailVerified: true }).where(eq14(schema.users.id, verificationToken.userId));
3704
+ await database.delete(schema.emailVerificationTokens).where(eq14(schema.emailVerificationTokens.tokenHash, tokenHash));
3602
3705
  logSecurityEvent("email_verified", "low", { userId: verificationToken.userId });
3603
3706
  return c.json({ success: true, redirect: "/" });
3604
3707
  };
@@ -3607,18 +3710,18 @@ var verifyEmailHandler = async (c) => {
3607
3710
  import { createRoute as createRoute6 } from "@hono/zod-openapi";
3608
3711
 
3609
3712
  // src/core/password-reset.ts
3610
- import { eq as eq14, and as and4, gt as gt3, lt, sql as sql3 } from "drizzle-orm";
3713
+ import { eq as eq15, and as and5, gt as gt4, lt as lt2, sql as sql4 } from "drizzle-orm";
3611
3714
  var TOKEN_EXPIRATION_MS = AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES * 60 * 1e3;
3612
3715
  async function createPasswordResetToken(db, tables2, email) {
3613
3716
  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);
3717
+ const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where(eq15(users.email, email.toLowerCase())).limit(1);
3615
3718
  if (!user) {
3616
3719
  return null;
3617
3720
  }
3618
3721
  if (!user.emailVerified) {
3619
3722
  return null;
3620
3723
  }
3621
- await db.delete(passwordResetTokens).where(eq14(passwordResetTokens.userId, user.id));
3724
+ await db.delete(passwordResetTokens).where(eq15(passwordResetTokens.userId, user.id));
3622
3725
  const token = generateSecureToken(64);
3623
3726
  const tokenHash = await hashToken(token);
3624
3727
  const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
@@ -3640,10 +3743,10 @@ async function validateResetToken(db, tables2, token) {
3640
3743
  used: passwordResetTokens.used,
3641
3744
  expiresAt: passwordResetTokens.expiresAt
3642
3745
  }).from(passwordResetTokens).where(
3643
- and4(
3644
- eq14(passwordResetTokens.tokenHash, tokenHash),
3645
- eq14(passwordResetTokens.used, false),
3646
- gt3(passwordResetTokens.expiresAt, Date.now())
3746
+ and5(
3747
+ eq15(passwordResetTokens.tokenHash, tokenHash),
3748
+ eq15(passwordResetTokens.used, false),
3749
+ gt4(passwordResetTokens.expiresAt, Date.now())
3647
3750
  )
3648
3751
  ).limit(1);
3649
3752
  if (!resetToken) {
@@ -3664,11 +3767,11 @@ async function resetPassword(db, tables2, token, newPassword, pepper, pepperKid
3664
3767
  await db.update(users).set({
3665
3768
  hashedPassword,
3666
3769
  pepperKid,
3667
- sessionVersion: sql3`${users.sessionVersion} + 1`
3668
- }).where(eq14(users.id, userId));
3770
+ sessionVersion: sql4`${users.sessionVersion} + 1`
3771
+ }).where(eq15(users.id, userId));
3669
3772
  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));
3773
+ await db.update(passwordResetTokens).set({ used: true }).where(eq15(passwordResetTokens.tokenHash, tokenHash));
3774
+ await db.delete(sessions).where(eq15(sessions.userId, userId));
3672
3775
  logger_default.info("Password reset successful", { userId });
3673
3776
  return { success: true };
3674
3777
  }
@@ -3816,14 +3919,14 @@ var resetPasswordMiddleware = [
3816
3919
  import { createRoute as createRoute8 } from "@hono/zod-openapi";
3817
3920
 
3818
3921
  // src/core/change-password.ts
3819
- import { eq as eq15, and as and5, ne } from "drizzle-orm";
3922
+ import { eq as eq16, and as and6, ne } from "drizzle-orm";
3820
3923
  async function changePassword(params) {
3821
3924
  const { userId, currentPassword, newPassword, currentSessionId, pepper, db, tables: tables2 } = params;
3822
3925
  const { users, sessions } = tables2;
3823
3926
  const [user] = await db.select({
3824
3927
  id: users.id,
3825
3928
  hashedPassword: users.hashedPassword
3826
- }).from(users).where(eq15(users.id, userId)).limit(1);
3929
+ }).from(users).where(eq16(users.id, userId)).limit(1);
3827
3930
  if (!user || !user.hashedPassword) {
3828
3931
  return { success: false, error: "User not found" };
3829
3932
  }
@@ -3839,8 +3942,8 @@ async function changePassword(params) {
3839
3942
  await db.update(users).set({
3840
3943
  hashedPassword: newHashedPassword,
3841
3944
  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)));
3945
+ }).where(eq16(users.id, userId));
3946
+ await db.delete(sessions).where(and6(eq16(sessions.userId, userId), ne(sessions.id, currentSessionId)));
3844
3947
  logSecurityEvent("password_changed", "low", {
3845
3948
  userId
3846
3949
  });
@@ -4113,7 +4216,7 @@ var cancelEmailChangeHandler = async (c) => {
4113
4216
  // src/routes/delete-account.ts
4114
4217
  import { createRoute as createRoute13 } from "@hono/zod-openapi";
4115
4218
  import { z as z5 } from "@hono/zod-openapi";
4116
- import { eq as eq16 } from "drizzle-orm";
4219
+ import { eq as eq17 } from "drizzle-orm";
4117
4220
  var deleteAccountRequestSchema = z5.object({
4118
4221
  password: z5.string().min(1).openapi({ example: "CurrentPass123!" })
4119
4222
  }).openapi("DeleteAccountRequest");
@@ -4161,7 +4264,7 @@ var deleteAccountHandler = async (c) => {
4161
4264
  const [user] = await database.select({
4162
4265
  id: schema.users.id,
4163
4266
  hashedPassword: schema.users.hashedPassword
4164
- }).from(schema.users).where(eq16(schema.users.id, userId)).limit(1);
4267
+ }).from(schema.users).where(eq17(schema.users.id, userId)).limit(1);
4165
4268
  if (!user || !user.hashedPassword) {
4166
4269
  return problems.unauthorized(c, "Invalid credentials");
4167
4270
  }
@@ -4176,8 +4279,8 @@ var deleteAccountHandler = async (c) => {
4176
4279
  }
4177
4280
  const now = /* @__PURE__ */ new Date();
4178
4281
  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));
4282
+ await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where(eq17(schema.users.id, userId));
4283
+ await database.delete(schema.sessions).where(eq17(schema.sessions.userId, userId));
4181
4284
  logSecurityEvent("account_deletion_requested", "high", { userId });
4182
4285
  return c.json(
4183
4286
  {
@@ -4198,7 +4301,7 @@ var deleteAccountMiddleware = [requireAuth];
4198
4301
  // src/routes/refresh.ts
4199
4302
  import { createRoute as createRoute14 } from "@hono/zod-openapi";
4200
4303
  import { z as z6 } from "@hono/zod-openapi";
4201
- import { eq as eq17, and as and6, gt as gt4 } from "drizzle-orm";
4304
+ import { eq as eq18, and as and7, gt as gt5 } from "drizzle-orm";
4202
4305
  var refreshRequestSchema = z6.object({
4203
4306
  refreshToken: z6.string().min(1).openapi({ example: "abc123..." })
4204
4307
  }).openapi("RefreshRequest");
@@ -4238,7 +4341,7 @@ var refreshHandler = async (c) => {
4238
4341
  expiresAt: schema.sessions.expiresAt,
4239
4342
  fingerprint: schema.sessions.fingerprint,
4240
4343
  ipAddress: schema.sessions.ipAddress
4241
- }).from(schema.sessions).where(and6(eq17(schema.sessions.id, refreshToken), gt4(schema.sessions.expiresAt, Date.now()))).limit(1);
4344
+ }).from(schema.sessions).where(and7(eq18(schema.sessions.id, refreshToken), gt5(schema.sessions.expiresAt, Date.now()))).limit(1);
4242
4345
  if (!session) {
4243
4346
  return problems.unauthorized(c, "Invalid or expired refresh token");
4244
4347
  }
@@ -4261,7 +4364,7 @@ var refreshMiddleware = [];
4261
4364
  // src/routes/resend-verification.ts
4262
4365
  import { createRoute as createRoute15 } from "@hono/zod-openapi";
4263
4366
  import { z as z7 } from "@hono/zod-openapi";
4264
- import { eq as eq18 } from "drizzle-orm";
4367
+ import { eq as eq19 } from "drizzle-orm";
4265
4368
  var resendVerificationResponseSchema = z7.object({
4266
4369
  success: z7.boolean().openapi({ example: true }),
4267
4370
  message: z7.string().openapi({ example: "Verification email sent" })
@@ -4306,14 +4409,14 @@ var resendVerificationHandler = async (c) => {
4306
4409
  email: schema.users.email,
4307
4410
  name: schema.users.name,
4308
4411
  emailVerified: schema.users.emailVerified
4309
- }).from(schema.users).where(eq18(schema.users.id, userId)).limit(1);
4412
+ }).from(schema.users).where(eq19(schema.users.id, userId)).limit(1);
4310
4413
  if (!user) {
4311
4414
  return problems.unauthorized(c, "User not found");
4312
4415
  }
4313
4416
  if (user.emailVerified) {
4314
4417
  return problems.badRequest(c, "Email is already verified");
4315
4418
  }
4316
- await db.delete(schema.emailVerificationTokens).where(eq18(schema.emailVerificationTokens.userId, userId));
4419
+ await db.delete(schema.emailVerificationTokens).where(eq19(schema.emailVerificationTokens.userId, userId));
4317
4420
  const emailToken = generateSecureToken(32);
4318
4421
  const tokenHash = await hashToken(emailToken);
4319
4422
  const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
@@ -4358,7 +4461,7 @@ import { OpenAPIHono } from "@hono/zod-openapi";
4358
4461
 
4359
4462
  // src/routes/2fa/status.ts
4360
4463
  import { createRoute as createRoute16 } from "@hono/zod-openapi";
4361
- import { eq as eq19, and as and7, isNull as isNull2 } from "drizzle-orm";
4464
+ import { eq as eq20, and as and8, isNull as isNull3 } from "drizzle-orm";
4362
4465
 
4363
4466
  // src/routes/2fa/schemas.ts
4364
4467
  import { z as z8 } from "@hono/zod-openapi";
@@ -4519,7 +4622,7 @@ var statusHandler = async (c) => {
4519
4622
  if (!userId) return problems.unauthorized(c);
4520
4623
  const { db, schema } = getAuthContext(c);
4521
4624
  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)));
4625
+ const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where(and8(eq20(schema.userBackupCodes.userId, userId), isNull3(schema.userBackupCodes.usedAt)));
4523
4626
  c.header("Cache-Control", "private, max-age=60");
4524
4627
  c.header("Vary", "Cookie");
4525
4628
  return c.json({
@@ -4531,7 +4634,7 @@ var statusHandler = async (c) => {
4531
4634
 
4532
4635
  // src/routes/2fa/totp-setup.ts
4533
4636
  import { createRoute as createRoute17 } from "@hono/zod-openapi";
4534
- import { eq as eq20, and as and8 } from "drizzle-orm";
4637
+ import { eq as eq21, and as and9 } from "drizzle-orm";
4535
4638
  var totpSetupRoute = createRoute17({
4536
4639
  method: "post",
4537
4640
  path: "/totp/setup",
@@ -4561,11 +4664,11 @@ var totpSetupHandler = async (c) => {
4561
4664
  const userId = c.get("userId");
4562
4665
  if (!userId) return problems.unauthorized(c);
4563
4666
  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);
4667
+ 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
4668
  if (existing.length > 0) {
4566
4669
  return problems.badRequest(c, "TOTP already enabled");
4567
4670
  }
4568
- const [user] = await db.select({ email: schema.users.email }).from(schema.users).where(eq20(schema.users.id, userId)).limit(1);
4671
+ const [user] = await db.select({ email: schema.users.email }).from(schema.users).where(eq21(schema.users.id, userId)).limit(1);
4569
4672
  if (!user) return problems.notFound(c, "User not found");
4570
4673
  const secret = generateTotpSecret();
4571
4674
  const qrCodeUri = generateQrCodeUri(secret, user.email);
@@ -4576,7 +4679,7 @@ var totpSetupHandler = async (c) => {
4576
4679
 
4577
4680
  // src/routes/2fa/totp-verify.ts
4578
4681
  import { createRoute as createRoute18 } from "@hono/zod-openapi";
4579
- import { eq as eq21 } from "drizzle-orm";
4682
+ import { eq as eq22 } from "drizzle-orm";
4580
4683
  var totpVerifyRoute = createRoute18({
4581
4684
  method: "post",
4582
4685
  path: "/totp/verify",
@@ -4622,7 +4725,7 @@ var totpVerifyHandler = async (c) => {
4622
4725
  logSecurityEvent("2fa_totp_setup_failed", "medium", { userId });
4623
4726
  return problems.badRequest(c, "Invalid code");
4624
4727
  }
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);
4728
+ 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
4729
  const encryptionKey = env.TOTP_ENCRYPTION_KEY;
4627
4730
  if (!encryptionKey) {
4628
4731
  logger_default.error("TOTP_ENCRYPTION_KEY not configured");
@@ -4670,7 +4773,7 @@ var totpVerifyHandler = async (c) => {
4670
4773
 
4671
4774
  // src/routes/2fa/totp-disable.ts
4672
4775
  import { createRoute as createRoute19 } from "@hono/zod-openapi";
4673
- import { eq as eq22, and as and9 } from "drizzle-orm";
4776
+ import { eq as eq23, and as and10 } from "drizzle-orm";
4674
4777
  var totpDisableRoute = createRoute19({
4675
4778
  method: "post",
4676
4779
  path: "/totp/disable",
@@ -4713,13 +4816,13 @@ var totpDisableHandler = async (c) => {
4713
4816
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "totp" });
4714
4817
  return problems.badRequest(c, "Invalid verification code");
4715
4818
  }
4716
- await db.delete(schema.user2faMethods).where(and9(eq22(schema.user2faMethods.userId, userId), eq22(schema.user2faMethods.method, "totp")));
4819
+ await db.delete(schema.user2faMethods).where(and10(eq23(schema.user2faMethods.userId, userId), eq23(schema.user2faMethods.method, "totp")));
4717
4820
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4718
4821
  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));
4822
+ await db.delete(schema.userBackupCodes).where(eq23(schema.userBackupCodes.userId, userId));
4823
+ await db.delete(schema.userTrustedDevices).where(eq23(schema.userTrustedDevices.userId, userId));
4721
4824
  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);
4825
+ 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
4826
  if (user) {
4724
4827
  const emailService = new EmailService(env, void 0, emailBranding);
4725
4828
  const firstName = user.name?.split(" ")[0] || null;
@@ -4738,7 +4841,7 @@ var totpDisableHandler = async (c) => {
4738
4841
 
4739
4842
  // src/routes/2fa/email-setup.ts
4740
4843
  import { createRoute as createRoute20 } from "@hono/zod-openapi";
4741
- import { eq as eq23, and as and10 } from "drizzle-orm";
4844
+ import { eq as eq24, and as and11 } from "drizzle-orm";
4742
4845
  var emailSetupRoute = createRoute20({
4743
4846
  method: "post",
4744
4847
  path: "/email/setup",
@@ -4768,15 +4871,15 @@ var emailSetupHandler = async (c) => {
4768
4871
  const userId = c.get("userId");
4769
4872
  if (!userId) return problems.unauthorized(c);
4770
4873
  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);
4874
+ 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
4875
  if (existing.length > 0) {
4773
4876
  return problems.badRequest(c, "Email 2FA already enabled");
4774
4877
  }
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);
4878
+ 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
4879
  if (!user) return problems.notFound(c, "User not found");
4777
4880
  const code = generateEmailOtp();
4778
- const codeHash = await hashToken(code);
4779
- await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
4881
+ const codeHash2 = await hashToken(code);
4882
+ await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash2, { expirationTtl: 300 });
4780
4883
  const emailService = new EmailService(env, void 0, emailBranding);
4781
4884
  const firstName = user.name?.split(" ")[0] || null;
4782
4885
  await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
@@ -4786,7 +4889,7 @@ var emailSetupHandler = async (c) => {
4786
4889
 
4787
4890
  // src/routes/2fa/email-verify.ts
4788
4891
  import { createRoute as createRoute21 } from "@hono/zod-openapi";
4789
- import { eq as eq24 } from "drizzle-orm";
4892
+ import { eq as eq25 } from "drizzle-orm";
4790
4893
  var emailVerifyRoute = createRoute21({
4791
4894
  method: "post",
4792
4895
  path: "/email/verify",
@@ -4823,12 +4926,12 @@ var emailVerifyHandler = async (c) => {
4823
4926
  if (!storedHash) {
4824
4927
  return problems.badRequest(c, "Code expired. Please request a new one.");
4825
4928
  }
4826
- const codeHash = await hashToken(code);
4827
- if (codeHash !== storedHash) {
4929
+ const codeHash2 = await hashToken(code);
4930
+ if (codeHash2 !== storedHash) {
4828
4931
  logSecurityEvent("2fa_email_setup_failed", "medium", { userId });
4829
4932
  return problems.badRequest(c, "Invalid code");
4830
4933
  }
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);
4934
+ 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
4935
  const now = Date.now();
4833
4936
  const existingMethods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4834
4937
  const isFirstMethod = existingMethods.length === 0;
@@ -4870,7 +4973,7 @@ var emailVerifyHandler = async (c) => {
4870
4973
 
4871
4974
  // src/routes/2fa/email-send-code.ts
4872
4975
  import { createRoute as createRoute22 } from "@hono/zod-openapi";
4873
- import { eq as eq25, and as and11 } from "drizzle-orm";
4976
+ import { eq as eq26, and as and12 } from "drizzle-orm";
4874
4977
  var emailSendCodeRoute = createRoute22({
4875
4978
  method: "post",
4876
4979
  path: "/email/send-code",
@@ -4900,15 +5003,15 @@ var emailSendCodeHandler = async (c) => {
4900
5003
  const userId = c.get("userId");
4901
5004
  if (!userId) return problems.unauthorized(c);
4902
5005
  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);
5006
+ const [emailMethod] = await db.select().from(schema.user2faMethods).where(and12(eq26(schema.user2faMethods.userId, userId), eq26(schema.user2faMethods.method, "email"))).limit(1);
4904
5007
  if (!emailMethod) {
4905
5008
  return problems.badRequest(c, "Email 2FA not enabled");
4906
5009
  }
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);
5010
+ 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
5011
  if (!user) return problems.notFound(c, "User not found");
4909
5012
  const code = generateEmailOtp();
4910
- const codeHash = await hashToken(code);
4911
- await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash, {
5013
+ const codeHash2 = await hashToken(code);
5014
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${userId}`, codeHash2, {
4912
5015
  expirationTtl: 300
4913
5016
  });
4914
5017
  const emailService = new EmailService(env, void 0, emailBranding);
@@ -4930,7 +5033,7 @@ var emailSendCodeHandler = async (c) => {
4930
5033
 
4931
5034
  // src/routes/2fa/email-disable.ts
4932
5035
  import { createRoute as createRoute23 } from "@hono/zod-openapi";
4933
- import { eq as eq26, and as and12 } from "drizzle-orm";
5036
+ import { eq as eq27, and as and13 } from "drizzle-orm";
4934
5037
  var emailDisableRoute = createRoute23({
4935
5038
  method: "post",
4936
5039
  path: "/email/disable",
@@ -4973,13 +5076,13 @@ var emailDisableHandler = async (c) => {
4973
5076
  logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "email" });
4974
5077
  return problems.badRequest(c, "Invalid verification code");
4975
5078
  }
4976
- await db.delete(schema.user2faMethods).where(and12(eq26(schema.user2faMethods.userId, userId), eq26(schema.user2faMethods.method, "email")));
5079
+ await db.delete(schema.user2faMethods).where(and13(eq27(schema.user2faMethods.userId, userId), eq27(schema.user2faMethods.method, "email")));
4977
5080
  const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4978
5081
  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));
5082
+ await db.delete(schema.userBackupCodes).where(eq27(schema.userBackupCodes.userId, userId));
5083
+ await db.delete(schema.userTrustedDevices).where(eq27(schema.userTrustedDevices.userId, userId));
4981
5084
  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);
5085
+ 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
5086
  if (user) {
4984
5087
  const emailService = new EmailService(env, void 0, emailBranding);
4985
5088
  const firstName = user.name?.split(" ")[0] || null;
@@ -4998,7 +5101,7 @@ var emailDisableHandler = async (c) => {
4998
5101
 
4999
5102
  // src/routes/2fa/trusted-devices.ts
5000
5103
  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";
5104
+ import { eq as eq28, and as and14, gt as gt6 } from "drizzle-orm";
5002
5105
  var trustedDevicesGetRoute = createRoute24({
5003
5106
  method: "get",
5004
5107
  path: "/trusted-devices",
@@ -5037,7 +5140,7 @@ var trustedDevicesGetHandler = async (c) => {
5037
5140
  ipAddress: schema.userTrustedDevices.ipAddress,
5038
5141
  lastUsedAt: schema.userTrustedDevices.lastUsedAt,
5039
5142
  createdAt: schema.userTrustedDevices.createdAt
5040
- }).from(schema.userTrustedDevices).where(and13(eq27(schema.userTrustedDevices.userId, userId), gt5(schema.userTrustedDevices.expiresAt, now)));
5143
+ }).from(schema.userTrustedDevices).where(and14(eq28(schema.userTrustedDevices.userId, userId), gt6(schema.userTrustedDevices.expiresAt, now)));
5041
5144
  c.header("Cache-Control", "private, max-age=60");
5042
5145
  c.header("Vary", "Cookie");
5043
5146
  return c.json({ devices });
@@ -5073,7 +5176,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5073
5176
  if (!userId) return problems.unauthorized(c);
5074
5177
  const deviceId = c.req.param("id");
5075
5178
  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 });
5179
+ 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
5180
  if (result.length === 0) {
5078
5181
  return problems.notFound(c, "Device not found");
5079
5182
  }
@@ -5083,7 +5186,7 @@ var trustedDevicesDeleteHandler = async (c) => {
5083
5186
 
5084
5187
  // src/routes/2fa/backup-codes.ts
5085
5188
  import { createRoute as createRoute25 } from "@hono/zod-openapi";
5086
- import { eq as eq28 } from "drizzle-orm";
5189
+ import { eq as eq29 } from "drizzle-orm";
5087
5190
  var backupCodesRegenerateRoute = createRoute25({
5088
5191
  method: "post",
5089
5192
  path: "/backup-codes/regenerate",
@@ -5124,7 +5227,7 @@ var backupCodesRegenerateHandler = async (c) => {
5124
5227
  logSecurityEvent("2fa_backup_regenerate_failed", "medium", { userId });
5125
5228
  return problems.badRequest(c, "Invalid verification code");
5126
5229
  }
5127
- await db.delete(schema.userBackupCodes).where(eq28(schema.userBackupCodes.userId, userId));
5230
+ await db.delete(schema.userBackupCodes).where(eq29(schema.userBackupCodes.userId, userId));
5128
5231
  const codes = generateBackupCodes();
5129
5232
  const now = Date.now();
5130
5233
  const hashedCodes = await Promise.all(
@@ -5141,7 +5244,7 @@ var backupCodesRegenerateHandler = async (c) => {
5141
5244
 
5142
5245
  // src/routes/2fa/challenge.ts
5143
5246
  import { createRoute as createRoute26 } from "@hono/zod-openapi";
5144
- import { eq as eq29 } from "drizzle-orm";
5247
+ import { eq as eq30 } from "drizzle-orm";
5145
5248
  import { getCookie as getCookie2 } from "hono/cookie";
5146
5249
  var challengeRoute = createRoute26({
5147
5250
  method: "post",
@@ -5213,7 +5316,7 @@ var challengeHandler = async (c) => {
5213
5316
  }
5214
5317
  await deleteChallengeToken(c.env.OAUTH_STATES, challengeToken);
5215
5318
  clearChallengeCookie(c, c.env);
5216
- const [user] = await db.select().from(schema.users).where(eq29(schema.users.id, payload.userId)).limit(1);
5319
+ const [user] = await db.select().from(schema.users).where(eq30(schema.users.id, payload.userId)).limit(1);
5217
5320
  if (!user) {
5218
5321
  logger_default.error("User not found during 2FA challenge", { userId: payload.userId });
5219
5322
  return problems.notFound(c, "User not found");
@@ -5254,7 +5357,7 @@ var challengeHandler = async (c) => {
5254
5357
 
5255
5358
  // src/routes/2fa/challenge-resend.ts
5256
5359
  import { createRoute as createRoute27 } from "@hono/zod-openapi";
5257
- import { eq as eq30 } from "drizzle-orm";
5360
+ import { eq as eq31 } from "drizzle-orm";
5258
5361
  import { getCookie as getCookie3 } from "hono/cookie";
5259
5362
  var challengeResendRoute = createRoute27({
5260
5363
  method: "post",
@@ -5292,7 +5395,7 @@ var challengeResendHandler = async (c) => {
5292
5395
  if (!payload.methods.includes("email")) {
5293
5396
  return problems.badRequest(c, "Email verification not available");
5294
5397
  }
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);
5398
+ 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
5399
  if (!user) {
5297
5400
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5298
5401
  return problems.notFound(c, "User not found");
@@ -5523,7 +5626,7 @@ var authorizeHandler = async (c) => {
5523
5626
 
5524
5627
  // src/routes/oauth/callback.ts
5525
5628
  import { createRoute as createRoute29 } from "@hono/zod-openapi";
5526
- import { eq as eq31 } from "drizzle-orm";
5629
+ import { eq as eq32 } from "drizzle-orm";
5527
5630
  var callbackRoute = createRoute29({
5528
5631
  method: "get",
5529
5632
  path: "/{provider}/callback",
@@ -5649,10 +5752,24 @@ var callbackHandler = async (c) => {
5649
5752
  logger_default.warn("No user ID returned from OAuth provider", { provider });
5650
5753
  return c.redirect(`${appUrl}/login?error=oauth_failed`);
5651
5754
  }
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);
5755
+ const { db, schema, env, invitations, signup } = getAuthContext(c);
5756
+ 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
5757
  let authUser = existingUser;
5655
5758
  if (!authUser) {
5759
+ const invited = await verifyInvitation(invitations, {
5760
+ token: invitationToken,
5761
+ email,
5762
+ db,
5763
+ env
5764
+ });
5765
+ if (signup === "invitation" && !invited) {
5766
+ logSecurityEvent("oauth_signup_refused_no_invitation", "medium", {
5767
+ email,
5768
+ provider,
5769
+ hadInvitation: !!invitationToken
5770
+ });
5771
+ return c.redirect(`${appUrl}/login?error=invitation_required`);
5772
+ }
5656
5773
  const [createdUser] = await db.insert(schema.users).values({
5657
5774
  email,
5658
5775
  name: name || null,
@@ -5663,6 +5780,15 @@ var callbackHandler = async (c) => {
5663
5780
  activatedAt: /* @__PURE__ */ new Date()
5664
5781
  }).returning({ id: schema.users.id, email: schema.users.email });
5665
5782
  authUser = createdUser;
5783
+ if (invited) {
5784
+ await acceptInvitation(invitations, {
5785
+ token: invited,
5786
+ user: { id: createdUser.id, email: createdUser.email, name: name || null },
5787
+ db,
5788
+ env,
5789
+ context: "oauth_invitation_onAccepted_failed"
5790
+ });
5791
+ }
5666
5792
  logSecurityEvent("user_signup_oauth", "low", {
5667
5793
  userId: createdUser.id,
5668
5794
  email: createdUser.email,
@@ -5702,7 +5828,7 @@ var callbackHandler = async (c) => {
5702
5828
 
5703
5829
  // src/routes/oauth/complete.ts
5704
5830
  import { createRoute as createRoute30 } from "@hono/zod-openapi";
5705
- import { eq as eq32 } from "drizzle-orm";
5831
+ import { eq as eq33 } from "drizzle-orm";
5706
5832
  var completeRoute = createRoute30({
5707
5833
  method: "post",
5708
5834
  path: "/complete",
@@ -5750,7 +5876,7 @@ var completeHandler = async (c) => {
5750
5876
  redirect: storedRedirect
5751
5877
  } = JSON.parse(stored);
5752
5878
  const { db, schema } = getAuthContext(c);
5753
- await db.update(schema.users).set({ emailVerified: true }).where(eq32(schema.users.id, userId));
5879
+ await db.update(schema.users).set({ emailVerified: true }).where(eq33(schema.users.id, userId));
5754
5880
  await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
5755
5881
  const fingerprint = await generateFingerprint(c.req.raw);
5756
5882
  const ipAddress = getClientIp(c.req.raw);
@@ -5795,19 +5921,12 @@ var routes_default = authRoutes;
5795
5921
 
5796
5922
  // src/lib/email/webhook-verifier.ts
5797
5923
  import { Webhook } from "svix";
5798
- function verifyWebhookSignature(payload, signature, secret) {
5924
+ function verifyWebhookSignature(payload, headers, secret) {
5799
5925
  if (!secret || secret.trim() === "") {
5800
5926
  throw new Error("Webhook secret is required");
5801
5927
  }
5802
5928
  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
- });
5929
+ new Webhook(secret).verify(payload, { ...headers });
5811
5930
  return true;
5812
5931
  } catch (error) {
5813
5932
  logger_default.warn("Webhook signature verification failed", {
@@ -5822,6 +5941,8 @@ export {
5822
5941
  CloudflareEmailAdapter,
5823
5942
  EmailService,
5824
5943
  MAX_CHALLENGE_ATTEMPTS,
5944
+ PASSWORDLESS_MAX_ATTEMPTS,
5945
+ PASSWORDLESS_TTL_MS,
5825
5946
  ProblemTypes,
5826
5947
  ResendAdapter,
5827
5948
  SCHEMA_VERSION,
@@ -5833,6 +5954,7 @@ export {
5833
5954
  cancelEmailChange,
5834
5955
  checkAccountLocked,
5835
5956
  checkBreachedPassword,
5957
+ cleanupExpiredPasswordlessChallenges,
5836
5958
  clearAccountLockout,
5837
5959
  clearChallengeCookie,
5838
5960
  clearSessionCookie,
@@ -5843,6 +5965,7 @@ export {
5843
5965
  createChallengeToken,
5844
5966
  createDeviceToken,
5845
5967
  createEmailAdapter,
5968
+ createPasswordlessChallenge,
5846
5969
  createProblemDetails,
5847
5970
  createSession,
5848
5971
  csrf,
@@ -5859,6 +5982,7 @@ export {
5859
5982
  generate2faEnabledEmail,
5860
5983
  generateApiKey,
5861
5984
  generateBackupCodes,
5985
+ generateCode,
5862
5986
  generateCodeChallenge,
5863
5987
  generateCodeVerifier,
5864
5988
  generateEmailChangeConfirmation,
@@ -5897,14 +6021,17 @@ export {
5897
6021
  optionalAuth,
5898
6022
  parseDeviceName,
5899
6023
  parseDeviceType,
6024
+ passwordlessEmail,
5900
6025
  problemJson,
5901
6026
  problems,
5902
6027
  rateLimit,
6028
+ recentPasswordlessFailures,
5903
6029
  refreshSession,
5904
6030
  requestEmailChange,
5905
6031
  requireAuth,
5906
6032
  requireVerifiedEmail,
5907
6033
  retrieveChallengeToken,
6034
+ sessionRefreshDue,
5908
6035
  setChallengeCookie,
5909
6036
  setSessionCookie,
5910
6037
  setTrustedDeviceCookie,
@@ -5922,6 +6049,8 @@ export {
5922
6049
  verifyCodeChallenge,
5923
6050
  verifyPassword,
5924
6051
  verifyPasswordWithRotation,
6052
+ verifyPasswordlessCode,
6053
+ verifyPasswordlessLink,
5925
6054
  verifyTotpCode,
5926
6055
  verifyTurnstileToken,
5927
6056
  verifyWebhookSignature