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