@syncello/auth 3.6.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1027 -870
- 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 +1025 -896
- 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>
|
|
@@ -2178,102 +1680,765 @@ function generateEmailTemplate(options) {
|
|
|
2178
1680
|
<!-- Footer Note -->
|
|
2179
1681
|
<table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
|
|
2180
1682
|
<tr>
|
|
2181
|
-
<td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
|
|
2182
|
-
<div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
|
|
2183
|
-
<p style="margin: 0; word-break: break-word;">${footerNote}</p>
|
|
1683
|
+
<td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
|
|
1684
|
+
<div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
|
|
1685
|
+
<p style="margin: 0; word-break: break-word;">${footerNote}</p>
|
|
1686
|
+
</div>
|
|
1687
|
+
</td>
|
|
1688
|
+
</tr>
|
|
1689
|
+
</table>
|
|
1690
|
+
|
|
1691
|
+
</td>
|
|
1692
|
+
</tr>
|
|
1693
|
+
</tbody>
|
|
1694
|
+
</table>
|
|
1695
|
+
</td>
|
|
1696
|
+
</tr>
|
|
1697
|
+
</tbody>
|
|
1698
|
+
</table>
|
|
1699
|
+
|
|
1700
|
+
<!-- Spacer Row -->
|
|
1701
|
+
<table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1702
|
+
<tbody>
|
|
1703
|
+
<tr>
|
|
1704
|
+
<td>
|
|
1705
|
+
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f8f9fa; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
|
|
1706
|
+
<tbody>
|
|
1707
|
+
<tr>
|
|
1708
|
+
<td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; padding-bottom: 5px; vertical-align: top;">
|
|
1709
|
+
<table class="empty_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1710
|
+
<tr>
|
|
1711
|
+
<td class="pad">
|
|
1712
|
+
<div></div>
|
|
1713
|
+
</td>
|
|
1714
|
+
</tr>
|
|
1715
|
+
</table>
|
|
1716
|
+
</td>
|
|
1717
|
+
</tr>
|
|
1718
|
+
</tbody>
|
|
1719
|
+
</table>
|
|
1720
|
+
</td>
|
|
1721
|
+
</tr>
|
|
1722
|
+
</tbody>
|
|
1723
|
+
</table>
|
|
1724
|
+
|
|
1725
|
+
<!-- Footer Row -->
|
|
1726
|
+
<table class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1727
|
+
<tbody>
|
|
1728
|
+
<tr>
|
|
1729
|
+
<td>
|
|
1730
|
+
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: ${footerColor}; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
|
|
1731
|
+
<tbody>
|
|
1732
|
+
<tr>
|
|
1733
|
+
<td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding: 20px;">
|
|
1734
|
+
|
|
1735
|
+
<!-- Policy Links -->
|
|
1736
|
+
<table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
|
|
1737
|
+
<tr>
|
|
1738
|
+
<td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
|
|
1739
|
+
<div style="color:#ffffff;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
|
|
1740
|
+
<p style="margin: 0; word-break: break-word;">
|
|
1741
|
+
<a href="${safeMarketingUrl}/privacy" style="color: #ffffff; text-decoration: underline;">Privacy Policy</a> | <a href="${safeMarketingUrl}/contact" style="color: #ffffff; text-decoration: underline;">Get in Touch</a>
|
|
1742
|
+
</p>
|
|
1743
|
+
</div>
|
|
1744
|
+
</td>
|
|
1745
|
+
</tr>
|
|
1746
|
+
</table>
|
|
1747
|
+
|
|
1748
|
+
<!-- Copyright & Disclaimer -->
|
|
1749
|
+
<table class="paragraph_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
|
|
1750
|
+
<tr>
|
|
1751
|
+
<td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
|
|
1752
|
+
<div style="color:#a3a3a3;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
|
|
1753
|
+
<p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
|
|
1754
|
+
<p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
|
|
1755
|
+
Please do not reply to this email as this address does not accept incoming messages. This service email contains essential information relating to your ${safeAppName} account.
|
|
1756
|
+
</p>
|
|
2184
1757
|
</div>
|
|
2185
1758
|
</td>
|
|
2186
1759
|
</tr>
|
|
2187
1760
|
</table>
|
|
2188
1761
|
|
|
2189
|
-
</td>
|
|
2190
|
-
</tr>
|
|
2191
|
-
</tbody>
|
|
2192
|
-
</table>
|
|
2193
|
-
</td>
|
|
2194
|
-
</tr>
|
|
2195
|
-
</tbody>
|
|
2196
|
-
</table>
|
|
1762
|
+
</td>
|
|
1763
|
+
</tr>
|
|
1764
|
+
</tbody>
|
|
1765
|
+
</table>
|
|
1766
|
+
</td>
|
|
1767
|
+
</tr>
|
|
1768
|
+
</tbody>
|
|
1769
|
+
</table>
|
|
1770
|
+
|
|
1771
|
+
</td>
|
|
1772
|
+
</tr>
|
|
1773
|
+
</tbody>
|
|
1774
|
+
</table>
|
|
1775
|
+
</body>
|
|
1776
|
+
</html>
|
|
1777
|
+
`;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/core/passwordless.ts
|
|
1781
|
+
var PASSWORDLESS_TTL_MS = 10 * 60 * 1e3;
|
|
1782
|
+
var PASSWORDLESS_MAX_ATTEMPTS = 5;
|
|
1783
|
+
var CODE_SPACE = 1e6;
|
|
1784
|
+
var CODE_LIMIT = Math.floor(4294967296 / CODE_SPACE) * CODE_SPACE;
|
|
1785
|
+
var CLEANUP_GRACE_MS = 24 * 60 * 60 * 1e3;
|
|
1786
|
+
var normalizeEmail = (email) => email.trim().toLowerCase();
|
|
1787
|
+
function generateCode() {
|
|
1788
|
+
const buffer = new Uint32Array(1);
|
|
1789
|
+
for (; ; ) {
|
|
1790
|
+
crypto.getRandomValues(buffer);
|
|
1791
|
+
if (buffer[0] < CODE_LIMIT) return String(buffer[0] % CODE_SPACE).padStart(6, "0");
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
var codeHash = (challengeId, code) => hashToken(`${challengeId}:${code}`);
|
|
1795
|
+
function constantTimeEqual(a, b) {
|
|
1796
|
+
if (a.length !== b.length) return false;
|
|
1797
|
+
let diff = 0;
|
|
1798
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1799
|
+
return diff === 0;
|
|
1800
|
+
}
|
|
1801
|
+
async function consumeOpen(db, t, email, now) {
|
|
1802
|
+
await db.update(t).set({ consumedAt: now }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.email, email), (0, import_drizzle_orm4.isNull)(t.consumedAt)));
|
|
1803
|
+
}
|
|
1804
|
+
async function createPasswordlessChallenge(db, tables2, input) {
|
|
1805
|
+
const t = tables2.passwordlessChallenges;
|
|
1806
|
+
const email = normalizeEmail(input.email);
|
|
1807
|
+
const now = Date.now();
|
|
1808
|
+
const challengeId = crypto.randomUUID();
|
|
1809
|
+
const linkToken = generateSecureToken(32);
|
|
1810
|
+
const browserSecret = generateSecureToken(32);
|
|
1811
|
+
const code = generateCode();
|
|
1812
|
+
await consumeOpen(db, t, email, now);
|
|
1813
|
+
await db.insert(t).values({
|
|
1814
|
+
id: challengeId,
|
|
1815
|
+
email,
|
|
1816
|
+
subjectId: input.subjectId ?? null,
|
|
1817
|
+
linkTokenHash: await hashToken(linkToken),
|
|
1818
|
+
codeHash: await codeHash(challengeId, code),
|
|
1819
|
+
browserHash: await hashToken(browserSecret),
|
|
1820
|
+
attempts: 0,
|
|
1821
|
+
payload: input.payload ?? null,
|
|
1822
|
+
expiresAt: now + (input.ttlMs ?? PASSWORDLESS_TTL_MS),
|
|
1823
|
+
createdAt: now
|
|
1824
|
+
});
|
|
1825
|
+
return { challengeId, linkToken, code, browserSecret };
|
|
1826
|
+
}
|
|
1827
|
+
async function verifyPasswordlessLink(db, tables2, input) {
|
|
1828
|
+
const t = tables2.passwordlessChallenges;
|
|
1829
|
+
const now = Date.now();
|
|
1830
|
+
const hash = await hashToken(input.linkToken);
|
|
1831
|
+
const [row] = await db.update(t).set({ consumedAt: now }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.linkTokenHash, hash), (0, import_drizzle_orm4.isNull)(t.consumedAt), (0, import_drizzle_orm4.gt)(t.expiresAt, now))).returning();
|
|
1832
|
+
if (!row) {
|
|
1833
|
+
const [stale] = await db.select({ expiresAt: t.expiresAt, consumedAt: t.consumedAt }).from(t).where((0, import_drizzle_orm4.eq)(t.linkTokenHash, hash)).limit(1);
|
|
1834
|
+
const expired = stale && stale.consumedAt === null && stale.expiresAt <= now;
|
|
1835
|
+
return { ok: false, reason: expired ? "expired" : "invalid" };
|
|
1836
|
+
}
|
|
1837
|
+
await consumeOpen(db, t, row.email, now);
|
|
1838
|
+
return { ok: true, email: row.email, subjectId: row.subjectId, payload: row.payload };
|
|
1839
|
+
}
|
|
1840
|
+
async function verifyPasswordlessCode(db, tables2, input) {
|
|
1841
|
+
const t = tables2.passwordlessChallenges;
|
|
1842
|
+
const max = input.maxAttempts ?? PASSWORDLESS_MAX_ATTEMPTS;
|
|
1843
|
+
const email = normalizeEmail(input.email);
|
|
1844
|
+
const now = Date.now();
|
|
1845
|
+
const [row] = await db.select().from(t).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.email, email), (0, import_drizzle_orm4.isNull)(t.consumedAt))).orderBy((0, import_drizzle_orm4.desc)(t.createdAt)).limit(1);
|
|
1846
|
+
if (!row) return { ok: false, reason: "invalid" };
|
|
1847
|
+
if (!constantTimeEqual(row.browserHash, await hashToken(input.browserSecret))) {
|
|
1848
|
+
return { ok: false, reason: "invalid" };
|
|
1849
|
+
}
|
|
1850
|
+
if (row.expiresAt <= now) return { ok: false, reason: "expired" };
|
|
1851
|
+
if (row.attempts >= max) return { ok: false, reason: "locked" };
|
|
1852
|
+
if (!constantTimeEqual(row.codeHash, await codeHash(row.id, input.code.trim()))) {
|
|
1853
|
+
const [counted] = await db.update(t).set({ attempts: import_drizzle_orm4.sql`${t.attempts} + 1` }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.id, row.id), (0, import_drizzle_orm4.lt)(t.attempts, max))).returning({ attempts: t.attempts });
|
|
1854
|
+
return { ok: false, reason: counted && counted.attempts < max ? "invalid" : "locked" };
|
|
1855
|
+
}
|
|
1856
|
+
const [won] = await db.update(t).set({ consumedAt: now }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.id, row.id), (0, import_drizzle_orm4.isNull)(t.consumedAt), (0, import_drizzle_orm4.lt)(t.attempts, max))).returning();
|
|
1857
|
+
if (!won) return { ok: false, reason: "invalid" };
|
|
1858
|
+
await consumeOpen(db, t, email, now);
|
|
1859
|
+
return { ok: true, email, subjectId: won.subjectId, payload: won.payload };
|
|
1860
|
+
}
|
|
1861
|
+
async function recentPasswordlessFailures(db, tables2, input) {
|
|
1862
|
+
const t = tables2.passwordlessChallenges;
|
|
1863
|
+
const [row] = await db.select({ total: import_drizzle_orm4.sql`coalesce(sum(${t.attempts}), 0)::int` }).from(t).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(t.email, normalizeEmail(input.email)), (0, import_drizzle_orm4.gt)(t.createdAt, Date.now() - input.windowMs)));
|
|
1864
|
+
return Number(row?.total ?? 0);
|
|
1865
|
+
}
|
|
1866
|
+
async function cleanupExpiredPasswordlessChallenges(db, tables2) {
|
|
1867
|
+
const t = tables2.passwordlessChallenges;
|
|
1868
|
+
await db.delete(t).where((0, import_drizzle_orm4.lt)(t.expiresAt, Date.now() - CLEANUP_GRACE_MS));
|
|
1869
|
+
}
|
|
1870
|
+
function passwordlessEmail(input) {
|
|
1871
|
+
const { appName, appUrl, linkUrl, code } = input;
|
|
1872
|
+
const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
|
|
1873
|
+
const lifetime = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
1874
|
+
const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
|
|
1875
|
+
const html = generateEmailTemplate({
|
|
1876
|
+
heading: `Sign in to ${escapeHtml(appName)}`,
|
|
1877
|
+
body: `<p>Use this code, or the button below, to sign in:</p>
|
|
1878
|
+
<p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
|
|
1879
|
+
<p>The code and the link expire in ${lifetime}.</p>`,
|
|
1880
|
+
buttonText: "Sign in",
|
|
1881
|
+
buttonUrl: safeLink,
|
|
1882
|
+
footerNote: "If you didn't ask to sign in, you can ignore this email.",
|
|
1883
|
+
appUrl,
|
|
1884
|
+
appName
|
|
1885
|
+
});
|
|
1886
|
+
return {
|
|
1887
|
+
subject: `${code} is your sign-in code`,
|
|
1888
|
+
html,
|
|
1889
|
+
text: `Your ${appName} sign-in code is ${code}.
|
|
1890
|
+
|
|
1891
|
+
Or sign in with this link:
|
|
1892
|
+
${safeLink}
|
|
1893
|
+
|
|
1894
|
+
The code and the link expire in ${lifetime}.
|
|
1895
|
+
|
|
1896
|
+
If you didn't ask to sign in, you can ignore this email.`
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/middleware/auth.ts
|
|
1901
|
+
var import_factory = require("hono/factory");
|
|
1902
|
+
var import_drizzle_orm5 = require("drizzle-orm");
|
|
1903
|
+
|
|
1904
|
+
// src/factory.ts
|
|
1905
|
+
function createAuth(config) {
|
|
1906
|
+
const { db, schema, getEnv, emailBranding, invitations, signup = "open" } = config;
|
|
1907
|
+
if (signup === "invitation" && !invitations) {
|
|
1908
|
+
throw new Error("createAuth: signup: 'invitation' requires the invitations hooks");
|
|
1909
|
+
}
|
|
1910
|
+
const getContext = (c) => ({
|
|
1911
|
+
db,
|
|
1912
|
+
schema,
|
|
1913
|
+
env: getEnv(c),
|
|
1914
|
+
emailBranding,
|
|
1915
|
+
invitations,
|
|
1916
|
+
signup
|
|
1917
|
+
});
|
|
1918
|
+
return {
|
|
1919
|
+
db,
|
|
1920
|
+
schema,
|
|
1921
|
+
getContext
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
function createAuthMiddleware(auth2) {
|
|
1925
|
+
return async (c, next) => {
|
|
1926
|
+
const ctx = auth2.getContext(c);
|
|
1927
|
+
c.set("authContext", ctx);
|
|
1928
|
+
c.set("db", auth2.db);
|
|
1929
|
+
await next();
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function getAuthContext(c) {
|
|
1933
|
+
const ctx = c.get("authContext");
|
|
1934
|
+
if (!ctx) {
|
|
1935
|
+
throw new Error(
|
|
1936
|
+
"AuthContext not found. Did you apply createAuthMiddleware before the auth routes?"
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
return ctx;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/lib/problem-json.ts
|
|
1943
|
+
var ProblemTypes = {
|
|
1944
|
+
// 4xx Client Errors
|
|
1945
|
+
BAD_REQUEST: "/errors/bad-request",
|
|
1946
|
+
UNAUTHORIZED: "/errors/unauthorized",
|
|
1947
|
+
FORBIDDEN: "/errors/forbidden",
|
|
1948
|
+
NOT_FOUND: "/errors/not-found",
|
|
1949
|
+
CONFLICT: "/errors/conflict",
|
|
1950
|
+
GONE: "/errors/gone",
|
|
1951
|
+
VALIDATION_ERROR: "/errors/validation-error",
|
|
1952
|
+
RATE_LIMIT_EXCEEDED: "/errors/rate-limit-exceeded",
|
|
1953
|
+
// 5xx Server Errors
|
|
1954
|
+
INTERNAL_ERROR: "/errors/internal-error",
|
|
1955
|
+
SERVICE_UNAVAILABLE: "/errors/service-unavailable"
|
|
1956
|
+
};
|
|
1957
|
+
function generateTraceId() {
|
|
1958
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
1959
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1960
|
+
}
|
|
1961
|
+
function createProblemDetails(type, title, status, options) {
|
|
1962
|
+
const problem = {
|
|
1963
|
+
type,
|
|
1964
|
+
title,
|
|
1965
|
+
status
|
|
1966
|
+
};
|
|
1967
|
+
if (options?.detail) {
|
|
1968
|
+
problem.detail = options.detail;
|
|
1969
|
+
}
|
|
1970
|
+
if (options?.instance) {
|
|
1971
|
+
problem.instance = options.instance;
|
|
1972
|
+
}
|
|
1973
|
+
problem.traceId = options?.traceId || generateTraceId();
|
|
1974
|
+
if (options?.requestId) {
|
|
1975
|
+
problem.requestId = options.requestId;
|
|
1976
|
+
}
|
|
1977
|
+
if (options?.extensions) {
|
|
1978
|
+
Object.assign(problem, options.extensions);
|
|
1979
|
+
}
|
|
1980
|
+
return problem;
|
|
1981
|
+
}
|
|
1982
|
+
function problemJson(c, problem) {
|
|
1983
|
+
logger_default.warn("API error response", {
|
|
1984
|
+
type: problem.type,
|
|
1985
|
+
status: problem.status,
|
|
1986
|
+
traceId: problem.traceId,
|
|
1987
|
+
requestId: problem.requestId,
|
|
1988
|
+
path: c.req.path,
|
|
1989
|
+
method: c.req.method
|
|
1990
|
+
});
|
|
1991
|
+
return c.json(problem, problem.status, {
|
|
1992
|
+
"Content-Type": "application/problem+json"
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
var problems = {
|
|
1996
|
+
/**
|
|
1997
|
+
* 400 Bad Request - Generic client error
|
|
1998
|
+
*/
|
|
1999
|
+
badRequest(c, detail) {
|
|
2000
|
+
return problemJson(
|
|
2001
|
+
c,
|
|
2002
|
+
createProblemDetails(ProblemTypes.BAD_REQUEST, "Bad Request", 400, {
|
|
2003
|
+
detail: detail || "The request could not be understood by the server",
|
|
2004
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2005
|
+
requestId: c.get("requestId"),
|
|
2006
|
+
traceId: c.get("traceId")
|
|
2007
|
+
})
|
|
2008
|
+
);
|
|
2009
|
+
},
|
|
2010
|
+
/**
|
|
2011
|
+
* 400 Bad Request - Validation errors with field-specific details
|
|
2012
|
+
*/
|
|
2013
|
+
validationError(c, detail, errors) {
|
|
2014
|
+
return problemJson(
|
|
2015
|
+
c,
|
|
2016
|
+
createProblemDetails(ProblemTypes.VALIDATION_ERROR, "Validation Error", 400, {
|
|
2017
|
+
detail,
|
|
2018
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2019
|
+
requestId: c.get("requestId"),
|
|
2020
|
+
traceId: c.get("traceId"),
|
|
2021
|
+
extensions: errors ? { errors } : void 0
|
|
2022
|
+
})
|
|
2023
|
+
);
|
|
2024
|
+
},
|
|
2025
|
+
/**
|
|
2026
|
+
* 401 Unauthorized - Authentication required
|
|
2027
|
+
*/
|
|
2028
|
+
unauthorized(c, detail) {
|
|
2029
|
+
return problemJson(
|
|
2030
|
+
c,
|
|
2031
|
+
createProblemDetails(ProblemTypes.UNAUTHORIZED, "Unauthorized", 401, {
|
|
2032
|
+
detail: detail || "Authentication is required to access this resource",
|
|
2033
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2034
|
+
requestId: c.get("requestId"),
|
|
2035
|
+
traceId: c.get("traceId")
|
|
2036
|
+
})
|
|
2037
|
+
);
|
|
2038
|
+
},
|
|
2039
|
+
/**
|
|
2040
|
+
* 403 Forbidden - Insufficient permissions
|
|
2041
|
+
*/
|
|
2042
|
+
forbidden(c, detail) {
|
|
2043
|
+
return problemJson(
|
|
2044
|
+
c,
|
|
2045
|
+
createProblemDetails(ProblemTypes.FORBIDDEN, "Forbidden", 403, {
|
|
2046
|
+
detail: detail || "You do not have permission to access this resource",
|
|
2047
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2048
|
+
requestId: c.get("requestId"),
|
|
2049
|
+
traceId: c.get("traceId")
|
|
2050
|
+
})
|
|
2051
|
+
);
|
|
2052
|
+
},
|
|
2053
|
+
/**
|
|
2054
|
+
* 404 Not Found
|
|
2055
|
+
*/
|
|
2056
|
+
notFound(c, detail) {
|
|
2057
|
+
return problemJson(
|
|
2058
|
+
c,
|
|
2059
|
+
createProblemDetails(ProblemTypes.NOT_FOUND, "Not Found", 404, {
|
|
2060
|
+
detail: detail || "The requested resource was not found",
|
|
2061
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2062
|
+
requestId: c.get("requestId"),
|
|
2063
|
+
traceId: c.get("traceId")
|
|
2064
|
+
})
|
|
2065
|
+
);
|
|
2066
|
+
},
|
|
2067
|
+
/**
|
|
2068
|
+
* 409 Conflict - Resource conflict (e.g., duplicate email)
|
|
2069
|
+
*/
|
|
2070
|
+
conflict(c, detail) {
|
|
2071
|
+
return problemJson(
|
|
2072
|
+
c,
|
|
2073
|
+
createProblemDetails(ProblemTypes.CONFLICT, "Conflict", 409, {
|
|
2074
|
+
detail,
|
|
2075
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2076
|
+
requestId: c.get("requestId"),
|
|
2077
|
+
traceId: c.get("traceId")
|
|
2078
|
+
})
|
|
2079
|
+
);
|
|
2080
|
+
},
|
|
2081
|
+
/**
|
|
2082
|
+
* 410 Gone - Resource is no longer available
|
|
2083
|
+
*/
|
|
2084
|
+
gone(c, detail) {
|
|
2085
|
+
return problemJson(
|
|
2086
|
+
c,
|
|
2087
|
+
createProblemDetails(ProblemTypes.GONE, "Gone", 410, {
|
|
2088
|
+
detail: detail || "The requested resource is no longer available",
|
|
2089
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2090
|
+
requestId: c.get("requestId"),
|
|
2091
|
+
traceId: c.get("traceId")
|
|
2092
|
+
})
|
|
2093
|
+
);
|
|
2094
|
+
},
|
|
2095
|
+
/**
|
|
2096
|
+
* 429 Too Many Requests - Rate limit exceeded
|
|
2097
|
+
*/
|
|
2098
|
+
rateLimitExceeded(c, retryAfter) {
|
|
2099
|
+
const extensions = retryAfter ? { retryAfter } : void 0;
|
|
2100
|
+
return problemJson(
|
|
2101
|
+
c,
|
|
2102
|
+
createProblemDetails(ProblemTypes.RATE_LIMIT_EXCEEDED, "Rate Limit Exceeded", 429, {
|
|
2103
|
+
detail: retryAfter ? `Too many requests. Please retry after ${retryAfter} seconds` : "Too many requests. Please slow down",
|
|
2104
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2105
|
+
requestId: c.get("requestId"),
|
|
2106
|
+
traceId: c.get("traceId"),
|
|
2107
|
+
extensions
|
|
2108
|
+
})
|
|
2109
|
+
);
|
|
2110
|
+
},
|
|
2111
|
+
/**
|
|
2112
|
+
* 500 Internal Server Error - Generic server error
|
|
2113
|
+
* SECURITY: Never include error details or stack traces
|
|
2114
|
+
*/
|
|
2115
|
+
internalError(c, error) {
|
|
2116
|
+
const traceId = c.get("traceId") || generateTraceId();
|
|
2117
|
+
if (error) {
|
|
2118
|
+
logError(error, {
|
|
2119
|
+
context: "internal_error",
|
|
2120
|
+
path: c.req.path,
|
|
2121
|
+
method: c.req.method,
|
|
2122
|
+
traceId,
|
|
2123
|
+
requestId: c.get("requestId")
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
return problemJson(
|
|
2127
|
+
c,
|
|
2128
|
+
createProblemDetails(ProblemTypes.INTERNAL_ERROR, "Internal Server Error", 500, {
|
|
2129
|
+
detail: "An unexpected error occurred. Please try again later",
|
|
2130
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2131
|
+
traceId,
|
|
2132
|
+
requestId: c.get("requestId")
|
|
2133
|
+
})
|
|
2134
|
+
);
|
|
2135
|
+
},
|
|
2136
|
+
/**
|
|
2137
|
+
* 503 Service Unavailable - Service is temporarily unavailable
|
|
2138
|
+
*/
|
|
2139
|
+
serviceUnavailable(c, detail) {
|
|
2140
|
+
return problemJson(
|
|
2141
|
+
c,
|
|
2142
|
+
createProblemDetails(ProblemTypes.SERVICE_UNAVAILABLE, "Service Unavailable", 503, {
|
|
2143
|
+
detail: detail || "The service is temporarily unavailable. Please try again later",
|
|
2144
|
+
instance: `${c.req.method} ${c.req.path}`,
|
|
2145
|
+
requestId: c.get("requestId"),
|
|
2146
|
+
traceId: c.get("traceId")
|
|
2147
|
+
})
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// src/middleware/auth.ts
|
|
2153
|
+
var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
|
|
2154
|
+
async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
|
|
2155
|
+
if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
|
|
2156
|
+
logger_default.info("Session exceeded absolute lifetime - revoked", {
|
|
2157
|
+
type: "security",
|
|
2158
|
+
event: "session_max_lifetime_exceeded",
|
|
2159
|
+
severity: "low",
|
|
2160
|
+
sessionId: sessionId.slice(0, 8),
|
|
2161
|
+
userId: session.userId
|
|
2162
|
+
});
|
|
2163
|
+
await deleteSession(db, { sessions }, sessionId);
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
|
|
2167
|
+
const cookieName = getSessionCookieName(env);
|
|
2168
|
+
const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
|
|
2169
|
+
const sessionIdMatch = cookieHeader.match(cookiePattern);
|
|
2170
|
+
if (!sessionIdMatch) return null;
|
|
2171
|
+
const sessionId = sessionIdMatch[1];
|
|
2172
|
+
const foundSessions = await db.select({
|
|
2173
|
+
userId: sessions.userId,
|
|
2174
|
+
expiresAt: sessions.expiresAt,
|
|
2175
|
+
createdAt: sessions.createdAt,
|
|
2176
|
+
lastActiveAt: sessions.lastActiveAt,
|
|
2177
|
+
fingerprint: sessions.fingerprint
|
|
2178
|
+
}).from(sessions).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.eq)(sessions.id, sessionId), (0, import_drizzle_orm5.gt)(sessions.expiresAt, Date.now()))).limit(1);
|
|
2179
|
+
if (foundSessions.length === 0) return null;
|
|
2180
|
+
const session = foundSessions[0];
|
|
2181
|
+
if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
|
|
2182
|
+
if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
|
|
2183
|
+
const userAgent = request.headers.get("user-agent") || "";
|
|
2184
|
+
const cfWorker = request.headers.get("cf-worker") || "";
|
|
2185
|
+
const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
|
|
2186
|
+
const isLocalSSR = userAgent.toLowerCase().includes("node");
|
|
2187
|
+
const isSvelteKitSSR = userAgent.toLowerCase().includes("sveltekit");
|
|
2188
|
+
const isCloudflareSSR = !userAgent && cfWorker.includes(".pages.dev");
|
|
2189
|
+
const isSSR = isLocalSSR || isSvelteKitSSR || isCloudflareSSR;
|
|
2190
|
+
if (isSSR) {
|
|
2191
|
+
logger_default.info("SSR request detected, using IP validation", {
|
|
2192
|
+
type: "security",
|
|
2193
|
+
event: "ssr_session_validation",
|
|
2194
|
+
sessionId: sessionId.slice(0, 8),
|
|
2195
|
+
userId: session.userId,
|
|
2196
|
+
clientIp,
|
|
2197
|
+
cfWorker,
|
|
2198
|
+
isLocalSSR,
|
|
2199
|
+
isSvelteKitSSR,
|
|
2200
|
+
isCloudflareSSR
|
|
2201
|
+
});
|
|
2202
|
+
} else {
|
|
2203
|
+
if (session.fingerprint) {
|
|
2204
|
+
const currentFingerprint = await generateFingerprint(request);
|
|
2205
|
+
if (session.fingerprint !== currentFingerprint) {
|
|
2206
|
+
logger_default.info("Session fingerprint changed (not enforced)", {
|
|
2207
|
+
type: "security",
|
|
2208
|
+
event: "fingerprint_changed",
|
|
2209
|
+
sessionId: sessionId.slice(0, 8),
|
|
2210
|
+
userId: session.userId,
|
|
2211
|
+
clientIp
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
userId: session.userId,
|
|
2218
|
+
expiresAt: session.expiresAt
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
async function getSessionFromBearerToken(db, sessions, authHeader) {
|
|
2222
|
+
if (!authHeader.startsWith("Bearer ")) return null;
|
|
2223
|
+
const sessionId = authHeader.slice(7);
|
|
2224
|
+
const foundSessions = await db.select({
|
|
2225
|
+
userId: sessions.userId,
|
|
2226
|
+
expiresAt: sessions.expiresAt,
|
|
2227
|
+
createdAt: sessions.createdAt,
|
|
2228
|
+
lastActiveAt: sessions.lastActiveAt
|
|
2229
|
+
}).from(sessions).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.eq)(sessions.id, sessionId), (0, import_drizzle_orm5.gt)(sessions.expiresAt, Date.now()))).limit(1);
|
|
2230
|
+
if (foundSessions.length === 0) return null;
|
|
2231
|
+
const session = foundSessions[0];
|
|
2232
|
+
if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
|
|
2233
|
+
if (sessionRefreshDue(session.lastActiveAt)) await refreshSession(db, { sessions }, sessionId);
|
|
2234
|
+
return {
|
|
2235
|
+
userId: session.userId,
|
|
2236
|
+
expiresAt: session.expiresAt
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
var requireAuth = (0, import_factory.createMiddleware)(
|
|
2240
|
+
async (c, next) => {
|
|
2241
|
+
const { db, schema } = getAuthContext(c);
|
|
2242
|
+
const authHeader = c.req.header("Authorization");
|
|
2243
|
+
if (authHeader) {
|
|
2244
|
+
const session2 = await getSessionFromBearerToken(db, schema.sessions, authHeader);
|
|
2245
|
+
if (session2) {
|
|
2246
|
+
c.set("userId", session2.userId);
|
|
2247
|
+
await next();
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
const cookieHeader = c.req.header("cookie");
|
|
2252
|
+
if (!cookieHeader) {
|
|
2253
|
+
return problems.unauthorized(c, "Please log in to continue");
|
|
2254
|
+
}
|
|
2255
|
+
const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
|
|
2256
|
+
if (!session) {
|
|
2257
|
+
c.header("Set-Cookie", clearSessionCookie(c.env));
|
|
2258
|
+
return problems.unauthorized(c, "Your session has expired. Please log in again.");
|
|
2259
|
+
}
|
|
2260
|
+
c.set("userId", session.userId);
|
|
2261
|
+
await next();
|
|
2262
|
+
}
|
|
2263
|
+
);
|
|
2264
|
+
var optionalAuth = (0, import_factory.createMiddleware)(
|
|
2265
|
+
async (c, next) => {
|
|
2266
|
+
const { db, schema } = getAuthContext(c);
|
|
2267
|
+
const authHeader = c.req.header("Authorization");
|
|
2268
|
+
if (authHeader) {
|
|
2269
|
+
const session = await getSessionFromBearerToken(db, schema.sessions, authHeader);
|
|
2270
|
+
if (session) {
|
|
2271
|
+
c.set("userId", session.userId);
|
|
2272
|
+
await next();
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
const cookieHeader = c.req.header("cookie");
|
|
2277
|
+
if (cookieHeader) {
|
|
2278
|
+
const session = await getSessionFromCookie(db, schema.sessions, cookieHeader, c.req.raw, c.env);
|
|
2279
|
+
if (session) {
|
|
2280
|
+
c.set("userId", session.userId);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
await next();
|
|
2284
|
+
}
|
|
2285
|
+
);
|
|
2197
2286
|
|
|
2198
|
-
|
|
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,18 +3763,18 @@ var signupMiddleware = [
|
|
|
3632
3763
|
|
|
3633
3764
|
// src/routes/login.ts
|
|
3634
3765
|
var import_zod_openapi3 = require("@hono/zod-openapi");
|
|
3635
|
-
var
|
|
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));
|
|
@@ -3677,7 +3808,7 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
|
|
|
3677
3808
|
const secret = await decryptTotpSecret(totpMethod.totpSecret, env.TOTP_ENCRYPTION_KEY);
|
|
3678
3809
|
const result = verifyTotpCode(secret, code, totpMethod.lastTotpCounter);
|
|
3679
3810
|
if (result.valid) {
|
|
3680
|
-
await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where((0,
|
|
3811
|
+
await database.update(user2faMethodsTable).set({ lastTotpCounter: result.counter }).where((0, import_drizzle_orm11.and)((0, import_drizzle_orm11.eq)(user2faMethodsTable.userId, userId), (0, import_drizzle_orm11.eq)(user2faMethodsTable.method, "totp")));
|
|
3681
3812
|
return { valid: true };
|
|
3682
3813
|
}
|
|
3683
3814
|
return { valid: false };
|
|
@@ -3689,18 +3820,18 @@ async function verifyAny2faCode(database, env, userId, code, method, tables2) {
|
|
|
3689
3820
|
const kvKey = `email_2fa_challenge:${userId}`;
|
|
3690
3821
|
const storedHash = await env.OAUTH_STATES.get(kvKey);
|
|
3691
3822
|
if (!storedHash) return { valid: false };
|
|
3692
|
-
const
|
|
3693
|
-
if (
|
|
3823
|
+
const codeHash2 = await hashToken(code);
|
|
3824
|
+
if (codeHash2 === storedHash) {
|
|
3694
3825
|
await env.OAUTH_STATES.delete(kvKey);
|
|
3695
3826
|
return { valid: true };
|
|
3696
3827
|
}
|
|
3697
3828
|
return { valid: false };
|
|
3698
3829
|
}
|
|
3699
3830
|
if (method === "backup") {
|
|
3700
|
-
const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where((0,
|
|
3831
|
+
const backupCodes = await database.select({ id: userBackupCodesTable.id, codeHash: userBackupCodesTable.codeHash }).from(userBackupCodesTable).where((0, import_drizzle_orm11.and)((0, import_drizzle_orm11.eq)(userBackupCodesTable.userId, userId), (0, import_drizzle_orm11.isNull)(userBackupCodesTable.usedAt)));
|
|
3701
3832
|
for (const bc of backupCodes) {
|
|
3702
3833
|
if (await verifyBackupCode(code, bc.codeHash)) {
|
|
3703
|
-
await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0,
|
|
3834
|
+
await database.update(userBackupCodesTable).set({ usedAt: Date.now() }).where((0, import_drizzle_orm11.eq)(userBackupCodesTable.id, bc.id));
|
|
3704
3835
|
logSecurityEvent("2fa_backup_code_used", "medium", { userId, codeId: bc.id });
|
|
3705
3836
|
return { valid: true };
|
|
3706
3837
|
}
|
|
@@ -3756,7 +3887,7 @@ var loginHandler = async (c) => {
|
|
|
3756
3887
|
return problems.badRequest(c, "Invalid captcha");
|
|
3757
3888
|
}
|
|
3758
3889
|
const user = await database.query.users.findFirst({
|
|
3759
|
-
where: (0,
|
|
3890
|
+
where: (0, import_drizzle_orm12.eq)(schema.users.email, email.toLowerCase())
|
|
3760
3891
|
});
|
|
3761
3892
|
if (!user) {
|
|
3762
3893
|
return problems.unauthorized(c, "Invalid email or password");
|
|
@@ -3865,7 +3996,7 @@ var loginMiddleware = [
|
|
|
3865
3996
|
|
|
3866
3997
|
// src/routes/logout.ts
|
|
3867
3998
|
var import_zod_openapi4 = require("@hono/zod-openapi");
|
|
3868
|
-
var
|
|
3999
|
+
var import_drizzle_orm13 = require("drizzle-orm");
|
|
3869
4000
|
var logoutRoute = (0, import_zod_openapi4.createRoute)({
|
|
3870
4001
|
method: "post",
|
|
3871
4002
|
path: "/logout",
|
|
@@ -3888,7 +4019,7 @@ var logoutHandler = async (c) => {
|
|
|
3888
4019
|
const sessionIdMatch = cookieHeader.match(cookiePattern);
|
|
3889
4020
|
if (sessionIdMatch) {
|
|
3890
4021
|
const sessionId = sessionIdMatch[1];
|
|
3891
|
-
await database.delete(schema.sessions).where((0,
|
|
4022
|
+
await database.delete(schema.sessions).where((0, import_drizzle_orm13.eq)(schema.sessions.id, sessionId));
|
|
3892
4023
|
}
|
|
3893
4024
|
}
|
|
3894
4025
|
c.header("Set-Cookie", clearSessionCookie(c.env), { append: true });
|
|
@@ -3897,7 +4028,7 @@ var logoutHandler = async (c) => {
|
|
|
3897
4028
|
|
|
3898
4029
|
// src/routes/me.ts
|
|
3899
4030
|
var import_zod_openapi5 = require("@hono/zod-openapi");
|
|
3900
|
-
var
|
|
4031
|
+
var import_drizzle_orm14 = require("drizzle-orm");
|
|
3901
4032
|
var meRoute = (0, import_zod_openapi5.createRoute)({
|
|
3902
4033
|
method: "get",
|
|
3903
4034
|
path: "/me",
|
|
@@ -3925,7 +4056,7 @@ var meHandler = async (c) => {
|
|
|
3925
4056
|
if (!userId) return problems.unauthorized(c, "Not authenticated");
|
|
3926
4057
|
const { db: database, schema } = getAuthContext(c);
|
|
3927
4058
|
const user = await database.query.users.findFirst({
|
|
3928
|
-
where: (0,
|
|
4059
|
+
where: (0, import_drizzle_orm14.eq)(schema.users.id, userId)
|
|
3929
4060
|
});
|
|
3930
4061
|
if (!user) {
|
|
3931
4062
|
return problems.notFound(c, "User not found");
|
|
@@ -3951,7 +4082,7 @@ var meMiddleware = [requireAuth];
|
|
|
3951
4082
|
|
|
3952
4083
|
// src/routes/verify-email.ts
|
|
3953
4084
|
var import_zod_openapi6 = require("@hono/zod-openapi");
|
|
3954
|
-
var
|
|
4085
|
+
var import_drizzle_orm15 = require("drizzle-orm");
|
|
3955
4086
|
var verifyEmailRoute = (0, import_zod_openapi6.createRoute)({
|
|
3956
4087
|
method: "post",
|
|
3957
4088
|
path: "/verify-email",
|
|
@@ -3980,17 +4111,17 @@ var verifyEmailHandler = async (c) => {
|
|
|
3980
4111
|
const { db: database, schema } = getAuthContext(c);
|
|
3981
4112
|
const tokenHash = await hashToken(token);
|
|
3982
4113
|
const verificationToken = await database.query.emailVerificationTokens.findFirst({
|
|
3983
|
-
where: (0,
|
|
4114
|
+
where: (0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash)
|
|
3984
4115
|
});
|
|
3985
4116
|
if (!verificationToken) {
|
|
3986
4117
|
return problems.badRequest(c, "Invalid or expired verification token");
|
|
3987
4118
|
}
|
|
3988
4119
|
if (verificationToken.expiresAt < Date.now()) {
|
|
3989
|
-
await database.delete(schema.emailVerificationTokens).where((0,
|
|
4120
|
+
await database.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm15.eq)(schema.emailVerificationTokens.tokenHash, tokenHash));
|
|
3990
4121
|
return problems.badRequest(c, "Verification token has expired");
|
|
3991
4122
|
}
|
|
3992
|
-
await database.update(schema.users).set({ emailVerified: true }).where((0,
|
|
3993
|
-
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));
|
|
3994
4125
|
logSecurityEvent("email_verified", "low", { userId: verificationToken.userId });
|
|
3995
4126
|
return c.json({ success: true, redirect: "/" });
|
|
3996
4127
|
};
|
|
@@ -3999,18 +4130,18 @@ var verifyEmailHandler = async (c) => {
|
|
|
3999
4130
|
var import_zod_openapi7 = require("@hono/zod-openapi");
|
|
4000
4131
|
|
|
4001
4132
|
// src/core/password-reset.ts
|
|
4002
|
-
var
|
|
4133
|
+
var import_drizzle_orm16 = require("drizzle-orm");
|
|
4003
4134
|
var TOKEN_EXPIRATION_MS = AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES * 60 * 1e3;
|
|
4004
4135
|
async function createPasswordResetToken(db, tables2, email) {
|
|
4005
4136
|
const { users, passwordResetTokens } = tables2;
|
|
4006
|
-
const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where((0,
|
|
4137
|
+
const [user] = await db.select({ id: users.id, emailVerified: users.emailVerified }).from(users).where((0, import_drizzle_orm16.eq)(users.email, email.toLowerCase())).limit(1);
|
|
4007
4138
|
if (!user) {
|
|
4008
4139
|
return null;
|
|
4009
4140
|
}
|
|
4010
4141
|
if (!user.emailVerified) {
|
|
4011
4142
|
return null;
|
|
4012
4143
|
}
|
|
4013
|
-
await db.delete(passwordResetTokens).where((0,
|
|
4144
|
+
await db.delete(passwordResetTokens).where((0, import_drizzle_orm16.eq)(passwordResetTokens.userId, user.id));
|
|
4014
4145
|
const token = generateSecureToken(64);
|
|
4015
4146
|
const tokenHash = await hashToken(token);
|
|
4016
4147
|
const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
|
|
@@ -4032,10 +4163,10 @@ async function validateResetToken(db, tables2, token) {
|
|
|
4032
4163
|
used: passwordResetTokens.used,
|
|
4033
4164
|
expiresAt: passwordResetTokens.expiresAt
|
|
4034
4165
|
}).from(passwordResetTokens).where(
|
|
4035
|
-
(0,
|
|
4036
|
-
(0,
|
|
4037
|
-
(0,
|
|
4038
|
-
(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())
|
|
4039
4170
|
)
|
|
4040
4171
|
).limit(1);
|
|
4041
4172
|
if (!resetToken) {
|
|
@@ -4056,11 +4187,11 @@ async function resetPassword(db, tables2, token, newPassword, pepper, pepperKid
|
|
|
4056
4187
|
await db.update(users).set({
|
|
4057
4188
|
hashedPassword,
|
|
4058
4189
|
pepperKid,
|
|
4059
|
-
sessionVersion:
|
|
4060
|
-
}).where((0,
|
|
4190
|
+
sessionVersion: import_drizzle_orm16.sql`${users.sessionVersion} + 1`
|
|
4191
|
+
}).where((0, import_drizzle_orm16.eq)(users.id, userId));
|
|
4061
4192
|
const tokenHash = await hashToken(token);
|
|
4062
|
-
await db.update(passwordResetTokens).set({ used: true }).where((0,
|
|
4063
|
-
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));
|
|
4064
4195
|
logger_default.info("Password reset successful", { userId });
|
|
4065
4196
|
return { success: true };
|
|
4066
4197
|
}
|
|
@@ -4208,14 +4339,14 @@ var resetPasswordMiddleware = [
|
|
|
4208
4339
|
var import_zod_openapi9 = require("@hono/zod-openapi");
|
|
4209
4340
|
|
|
4210
4341
|
// src/core/change-password.ts
|
|
4211
|
-
var
|
|
4342
|
+
var import_drizzle_orm17 = require("drizzle-orm");
|
|
4212
4343
|
async function changePassword(params) {
|
|
4213
4344
|
const { userId, currentPassword, newPassword, currentSessionId, pepper, db, tables: tables2 } = params;
|
|
4214
4345
|
const { users, sessions } = tables2;
|
|
4215
4346
|
const [user] = await db.select({
|
|
4216
4347
|
id: users.id,
|
|
4217
4348
|
hashedPassword: users.hashedPassword
|
|
4218
|
-
}).from(users).where((0,
|
|
4349
|
+
}).from(users).where((0, import_drizzle_orm17.eq)(users.id, userId)).limit(1);
|
|
4219
4350
|
if (!user || !user.hashedPassword) {
|
|
4220
4351
|
return { success: false, error: "User not found" };
|
|
4221
4352
|
}
|
|
@@ -4231,8 +4362,8 @@ async function changePassword(params) {
|
|
|
4231
4362
|
await db.update(users).set({
|
|
4232
4363
|
hashedPassword: newHashedPassword,
|
|
4233
4364
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4234
|
-
}).where((0,
|
|
4235
|
-
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)));
|
|
4236
4367
|
logSecurityEvent("password_changed", "low", {
|
|
4237
4368
|
userId
|
|
4238
4369
|
});
|
|
@@ -4505,7 +4636,7 @@ var cancelEmailChangeHandler = async (c) => {
|
|
|
4505
4636
|
// src/routes/delete-account.ts
|
|
4506
4637
|
var import_zod_openapi17 = require("@hono/zod-openapi");
|
|
4507
4638
|
var import_zod_openapi18 = require("@hono/zod-openapi");
|
|
4508
|
-
var
|
|
4639
|
+
var import_drizzle_orm18 = require("drizzle-orm");
|
|
4509
4640
|
var deleteAccountRequestSchema = import_zod_openapi18.z.object({
|
|
4510
4641
|
password: import_zod_openapi18.z.string().min(1).openapi({ example: "CurrentPass123!" })
|
|
4511
4642
|
}).openapi("DeleteAccountRequest");
|
|
@@ -4553,7 +4684,7 @@ var deleteAccountHandler = async (c) => {
|
|
|
4553
4684
|
const [user] = await database.select({
|
|
4554
4685
|
id: schema.users.id,
|
|
4555
4686
|
hashedPassword: schema.users.hashedPassword
|
|
4556
|
-
}).from(schema.users).where((0,
|
|
4687
|
+
}).from(schema.users).where((0, import_drizzle_orm18.eq)(schema.users.id, userId)).limit(1);
|
|
4557
4688
|
if (!user || !user.hashedPassword) {
|
|
4558
4689
|
return problems.unauthorized(c, "Invalid credentials");
|
|
4559
4690
|
}
|
|
@@ -4568,8 +4699,8 @@ var deleteAccountHandler = async (c) => {
|
|
|
4568
4699
|
}
|
|
4569
4700
|
const now = /* @__PURE__ */ new Date();
|
|
4570
4701
|
const purgeAt = new Date(now.getTime() + PURGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3);
|
|
4571
|
-
await database.update(schema.users).set({ deletedAt: now, scheduledPurgeAt: purgeAt, updatedAt: now }).where((0,
|
|
4572
|
-
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));
|
|
4573
4704
|
logSecurityEvent("account_deletion_requested", "high", { userId });
|
|
4574
4705
|
return c.json(
|
|
4575
4706
|
{
|
|
@@ -4590,7 +4721,7 @@ var deleteAccountMiddleware = [requireAuth];
|
|
|
4590
4721
|
// src/routes/refresh.ts
|
|
4591
4722
|
var import_zod_openapi19 = require("@hono/zod-openapi");
|
|
4592
4723
|
var import_zod_openapi20 = require("@hono/zod-openapi");
|
|
4593
|
-
var
|
|
4724
|
+
var import_drizzle_orm19 = require("drizzle-orm");
|
|
4594
4725
|
var refreshRequestSchema = import_zod_openapi20.z.object({
|
|
4595
4726
|
refreshToken: import_zod_openapi20.z.string().min(1).openapi({ example: "abc123..." })
|
|
4596
4727
|
}).openapi("RefreshRequest");
|
|
@@ -4630,7 +4761,7 @@ var refreshHandler = async (c) => {
|
|
|
4630
4761
|
expiresAt: schema.sessions.expiresAt,
|
|
4631
4762
|
fingerprint: schema.sessions.fingerprint,
|
|
4632
4763
|
ipAddress: schema.sessions.ipAddress
|
|
4633
|
-
}).from(schema.sessions).where((0,
|
|
4764
|
+
}).from(schema.sessions).where((0, import_drizzle_orm19.and)((0, import_drizzle_orm19.eq)(schema.sessions.id, refreshToken), (0, import_drizzle_orm19.gt)(schema.sessions.expiresAt, Date.now()))).limit(1);
|
|
4634
4765
|
if (!session) {
|
|
4635
4766
|
return problems.unauthorized(c, "Invalid or expired refresh token");
|
|
4636
4767
|
}
|
|
@@ -4653,7 +4784,7 @@ var refreshMiddleware = [];
|
|
|
4653
4784
|
// src/routes/resend-verification.ts
|
|
4654
4785
|
var import_zod_openapi21 = require("@hono/zod-openapi");
|
|
4655
4786
|
var import_zod_openapi22 = require("@hono/zod-openapi");
|
|
4656
|
-
var
|
|
4787
|
+
var import_drizzle_orm20 = require("drizzle-orm");
|
|
4657
4788
|
var resendVerificationResponseSchema = import_zod_openapi22.z.object({
|
|
4658
4789
|
success: import_zod_openapi22.z.boolean().openapi({ example: true }),
|
|
4659
4790
|
message: import_zod_openapi22.z.string().openapi({ example: "Verification email sent" })
|
|
@@ -4698,14 +4829,14 @@ var resendVerificationHandler = async (c) => {
|
|
|
4698
4829
|
email: schema.users.email,
|
|
4699
4830
|
name: schema.users.name,
|
|
4700
4831
|
emailVerified: schema.users.emailVerified
|
|
4701
|
-
}).from(schema.users).where((0,
|
|
4832
|
+
}).from(schema.users).where((0, import_drizzle_orm20.eq)(schema.users.id, userId)).limit(1);
|
|
4702
4833
|
if (!user) {
|
|
4703
4834
|
return problems.unauthorized(c, "User not found");
|
|
4704
4835
|
}
|
|
4705
4836
|
if (user.emailVerified) {
|
|
4706
4837
|
return problems.badRequest(c, "Email is already verified");
|
|
4707
4838
|
}
|
|
4708
|
-
await db.delete(schema.emailVerificationTokens).where((0,
|
|
4839
|
+
await db.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm20.eq)(schema.emailVerificationTokens.userId, userId));
|
|
4709
4840
|
const emailToken = generateSecureToken(32);
|
|
4710
4841
|
const tokenHash = await hashToken(emailToken);
|
|
4711
4842
|
const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
|
|
@@ -4750,7 +4881,7 @@ var import_zod_openapi36 = require("@hono/zod-openapi");
|
|
|
4750
4881
|
|
|
4751
4882
|
// src/routes/2fa/status.ts
|
|
4752
4883
|
var import_zod_openapi24 = require("@hono/zod-openapi");
|
|
4753
|
-
var
|
|
4884
|
+
var import_drizzle_orm21 = require("drizzle-orm");
|
|
4754
4885
|
|
|
4755
4886
|
// src/routes/2fa/schemas.ts
|
|
4756
4887
|
var import_zod_openapi23 = require("@hono/zod-openapi");
|
|
@@ -4911,7 +5042,7 @@ var statusHandler = async (c) => {
|
|
|
4911
5042
|
if (!userId) return problems.unauthorized(c);
|
|
4912
5043
|
const { db, schema } = getAuthContext(c);
|
|
4913
5044
|
const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
|
|
4914
|
-
const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where((0,
|
|
5045
|
+
const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where((0, import_drizzle_orm21.and)((0, import_drizzle_orm21.eq)(schema.userBackupCodes.userId, userId), (0, import_drizzle_orm21.isNull)(schema.userBackupCodes.usedAt)));
|
|
4915
5046
|
c.header("Cache-Control", "private, max-age=60");
|
|
4916
5047
|
c.header("Vary", "Cookie");
|
|
4917
5048
|
return c.json({
|
|
@@ -4923,7 +5054,7 @@ var statusHandler = async (c) => {
|
|
|
4923
5054
|
|
|
4924
5055
|
// src/routes/2fa/totp-setup.ts
|
|
4925
5056
|
var import_zod_openapi25 = require("@hono/zod-openapi");
|
|
4926
|
-
var
|
|
5057
|
+
var import_drizzle_orm22 = require("drizzle-orm");
|
|
4927
5058
|
var totpSetupRoute = (0, import_zod_openapi25.createRoute)({
|
|
4928
5059
|
method: "post",
|
|
4929
5060
|
path: "/totp/setup",
|
|
@@ -4953,11 +5084,11 @@ var totpSetupHandler = async (c) => {
|
|
|
4953
5084
|
const userId = c.get("userId");
|
|
4954
5085
|
if (!userId) return problems.unauthorized(c);
|
|
4955
5086
|
const { db, schema, env } = getAuthContext(c);
|
|
4956
|
-
const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0,
|
|
5087
|
+
const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm22.and)((0, import_drizzle_orm22.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm22.eq)(schema.user2faMethods.method, "totp"))).limit(1);
|
|
4957
5088
|
if (existing.length > 0) {
|
|
4958
5089
|
return problems.badRequest(c, "TOTP already enabled");
|
|
4959
5090
|
}
|
|
4960
|
-
const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0,
|
|
5091
|
+
const [user] = await db.select({ email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm22.eq)(schema.users.id, userId)).limit(1);
|
|
4961
5092
|
if (!user) return problems.notFound(c, "User not found");
|
|
4962
5093
|
const secret = generateTotpSecret();
|
|
4963
5094
|
const qrCodeUri = generateQrCodeUri(secret, user.email);
|
|
@@ -4968,7 +5099,7 @@ var totpSetupHandler = async (c) => {
|
|
|
4968
5099
|
|
|
4969
5100
|
// src/routes/2fa/totp-verify.ts
|
|
4970
5101
|
var import_zod_openapi26 = require("@hono/zod-openapi");
|
|
4971
|
-
var
|
|
5102
|
+
var import_drizzle_orm23 = require("drizzle-orm");
|
|
4972
5103
|
var totpVerifyRoute = (0, import_zod_openapi26.createRoute)({
|
|
4973
5104
|
method: "post",
|
|
4974
5105
|
path: "/totp/verify",
|
|
@@ -5014,7 +5145,7 @@ var totpVerifyHandler = async (c) => {
|
|
|
5014
5145
|
logSecurityEvent("2fa_totp_setup_failed", "medium", { userId });
|
|
5015
5146
|
return problems.badRequest(c, "Invalid code");
|
|
5016
5147
|
}
|
|
5017
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5148
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm23.eq)(schema.users.id, userId)).limit(1);
|
|
5018
5149
|
const encryptionKey = env.TOTP_ENCRYPTION_KEY;
|
|
5019
5150
|
if (!encryptionKey) {
|
|
5020
5151
|
logger_default.error("TOTP_ENCRYPTION_KEY not configured");
|
|
@@ -5062,7 +5193,7 @@ var totpVerifyHandler = async (c) => {
|
|
|
5062
5193
|
|
|
5063
5194
|
// src/routes/2fa/totp-disable.ts
|
|
5064
5195
|
var import_zod_openapi27 = require("@hono/zod-openapi");
|
|
5065
|
-
var
|
|
5196
|
+
var import_drizzle_orm24 = require("drizzle-orm");
|
|
5066
5197
|
var totpDisableRoute = (0, import_zod_openapi27.createRoute)({
|
|
5067
5198
|
method: "post",
|
|
5068
5199
|
path: "/totp/disable",
|
|
@@ -5105,13 +5236,13 @@ var totpDisableHandler = async (c) => {
|
|
|
5105
5236
|
logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "totp" });
|
|
5106
5237
|
return problems.badRequest(c, "Invalid verification code");
|
|
5107
5238
|
}
|
|
5108
|
-
await db.delete(schema.user2faMethods).where((0,
|
|
5239
|
+
await db.delete(schema.user2faMethods).where((0, import_drizzle_orm24.and)((0, import_drizzle_orm24.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm24.eq)(schema.user2faMethods.method, "totp")));
|
|
5109
5240
|
const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
|
|
5110
5241
|
if (remaining.length === 0) {
|
|
5111
|
-
await db.delete(schema.userBackupCodes).where((0,
|
|
5112
|
-
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));
|
|
5113
5244
|
await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
|
|
5114
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5245
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm24.eq)(schema.users.id, userId)).limit(1);
|
|
5115
5246
|
if (user) {
|
|
5116
5247
|
const emailService = new EmailService(env, void 0, emailBranding);
|
|
5117
5248
|
const firstName = user.name?.split(" ")[0] || null;
|
|
@@ -5130,7 +5261,7 @@ var totpDisableHandler = async (c) => {
|
|
|
5130
5261
|
|
|
5131
5262
|
// src/routes/2fa/email-setup.ts
|
|
5132
5263
|
var import_zod_openapi28 = require("@hono/zod-openapi");
|
|
5133
|
-
var
|
|
5264
|
+
var import_drizzle_orm25 = require("drizzle-orm");
|
|
5134
5265
|
var emailSetupRoute = (0, import_zod_openapi28.createRoute)({
|
|
5135
5266
|
method: "post",
|
|
5136
5267
|
path: "/email/setup",
|
|
@@ -5160,15 +5291,15 @@ var emailSetupHandler = async (c) => {
|
|
|
5160
5291
|
const userId = c.get("userId");
|
|
5161
5292
|
if (!userId) return problems.unauthorized(c);
|
|
5162
5293
|
const { db, schema, env, emailBranding } = getAuthContext(c);
|
|
5163
|
-
const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0,
|
|
5294
|
+
const existing = await db.select({ id: schema.user2faMethods.id }).from(schema.user2faMethods).where((0, import_drizzle_orm25.and)((0, import_drizzle_orm25.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm25.eq)(schema.user2faMethods.method, "email"))).limit(1);
|
|
5164
5295
|
if (existing.length > 0) {
|
|
5165
5296
|
return problems.badRequest(c, "Email 2FA already enabled");
|
|
5166
5297
|
}
|
|
5167
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5298
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm25.eq)(schema.users.id, userId)).limit(1);
|
|
5168
5299
|
if (!user) return problems.notFound(c, "User not found");
|
|
5169
5300
|
const code = generateEmailOtp();
|
|
5170
|
-
const
|
|
5171
|
-
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 });
|
|
5172
5303
|
const emailService = new EmailService(env, void 0, emailBranding);
|
|
5173
5304
|
const firstName = user.name?.split(" ")[0] || null;
|
|
5174
5305
|
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
|
|
@@ -5178,7 +5309,7 @@ var emailSetupHandler = async (c) => {
|
|
|
5178
5309
|
|
|
5179
5310
|
// src/routes/2fa/email-verify.ts
|
|
5180
5311
|
var import_zod_openapi29 = require("@hono/zod-openapi");
|
|
5181
|
-
var
|
|
5312
|
+
var import_drizzle_orm26 = require("drizzle-orm");
|
|
5182
5313
|
var emailVerifyRoute = (0, import_zod_openapi29.createRoute)({
|
|
5183
5314
|
method: "post",
|
|
5184
5315
|
path: "/email/verify",
|
|
@@ -5215,12 +5346,12 @@ var emailVerifyHandler = async (c) => {
|
|
|
5215
5346
|
if (!storedHash) {
|
|
5216
5347
|
return problems.badRequest(c, "Code expired. Please request a new one.");
|
|
5217
5348
|
}
|
|
5218
|
-
const
|
|
5219
|
-
if (
|
|
5349
|
+
const codeHash2 = await hashToken(code);
|
|
5350
|
+
if (codeHash2 !== storedHash) {
|
|
5220
5351
|
logSecurityEvent("2fa_email_setup_failed", "medium", { userId });
|
|
5221
5352
|
return problems.badRequest(c, "Invalid code");
|
|
5222
5353
|
}
|
|
5223
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5354
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm26.eq)(schema.users.id, userId)).limit(1);
|
|
5224
5355
|
const now = Date.now();
|
|
5225
5356
|
const existingMethods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
|
|
5226
5357
|
const isFirstMethod = existingMethods.length === 0;
|
|
@@ -5262,7 +5393,7 @@ var emailVerifyHandler = async (c) => {
|
|
|
5262
5393
|
|
|
5263
5394
|
// src/routes/2fa/email-send-code.ts
|
|
5264
5395
|
var import_zod_openapi30 = require("@hono/zod-openapi");
|
|
5265
|
-
var
|
|
5396
|
+
var import_drizzle_orm27 = require("drizzle-orm");
|
|
5266
5397
|
var emailSendCodeRoute = (0, import_zod_openapi30.createRoute)({
|
|
5267
5398
|
method: "post",
|
|
5268
5399
|
path: "/email/send-code",
|
|
@@ -5292,15 +5423,15 @@ var emailSendCodeHandler = async (c) => {
|
|
|
5292
5423
|
const userId = c.get("userId");
|
|
5293
5424
|
if (!userId) return problems.unauthorized(c);
|
|
5294
5425
|
const { db, schema, env, emailBranding } = getAuthContext(c);
|
|
5295
|
-
const [emailMethod] = await db.select().from(schema.user2faMethods).where((0,
|
|
5426
|
+
const [emailMethod] = await db.select().from(schema.user2faMethods).where((0, import_drizzle_orm27.and)((0, import_drizzle_orm27.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm27.eq)(schema.user2faMethods.method, "email"))).limit(1);
|
|
5296
5427
|
if (!emailMethod) {
|
|
5297
5428
|
return problems.badRequest(c, "Email 2FA not enabled");
|
|
5298
5429
|
}
|
|
5299
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5430
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm27.eq)(schema.users.id, userId)).limit(1);
|
|
5300
5431
|
if (!user) return problems.notFound(c, "User not found");
|
|
5301
5432
|
const code = generateEmailOtp();
|
|
5302
|
-
const
|
|
5303
|
-
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, {
|
|
5304
5435
|
expirationTtl: 300
|
|
5305
5436
|
});
|
|
5306
5437
|
const emailService = new EmailService(env, void 0, emailBranding);
|
|
@@ -5322,7 +5453,7 @@ var emailSendCodeHandler = async (c) => {
|
|
|
5322
5453
|
|
|
5323
5454
|
// src/routes/2fa/email-disable.ts
|
|
5324
5455
|
var import_zod_openapi31 = require("@hono/zod-openapi");
|
|
5325
|
-
var
|
|
5456
|
+
var import_drizzle_orm28 = require("drizzle-orm");
|
|
5326
5457
|
var emailDisableRoute = (0, import_zod_openapi31.createRoute)({
|
|
5327
5458
|
method: "post",
|
|
5328
5459
|
path: "/email/disable",
|
|
@@ -5365,13 +5496,13 @@ var emailDisableHandler = async (c) => {
|
|
|
5365
5496
|
logSecurityEvent("2fa_disable_failed", "medium", { userId, method: "email" });
|
|
5366
5497
|
return problems.badRequest(c, "Invalid verification code");
|
|
5367
5498
|
}
|
|
5368
|
-
await db.delete(schema.user2faMethods).where((0,
|
|
5499
|
+
await db.delete(schema.user2faMethods).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.user2faMethods.userId, userId), (0, import_drizzle_orm28.eq)(schema.user2faMethods.method, "email")));
|
|
5369
5500
|
const remaining = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
|
|
5370
5501
|
if (remaining.length === 0) {
|
|
5371
|
-
await db.delete(schema.userBackupCodes).where((0,
|
|
5372
|
-
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));
|
|
5373
5504
|
await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
|
|
5374
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5505
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm28.eq)(schema.users.id, userId)).limit(1);
|
|
5375
5506
|
if (user) {
|
|
5376
5507
|
const emailService = new EmailService(env, void 0, emailBranding);
|
|
5377
5508
|
const firstName = user.name?.split(" ")[0] || null;
|
|
@@ -5390,7 +5521,7 @@ var emailDisableHandler = async (c) => {
|
|
|
5390
5521
|
|
|
5391
5522
|
// src/routes/2fa/trusted-devices.ts
|
|
5392
5523
|
var import_zod_openapi32 = require("@hono/zod-openapi");
|
|
5393
|
-
var
|
|
5524
|
+
var import_drizzle_orm29 = require("drizzle-orm");
|
|
5394
5525
|
var trustedDevicesGetRoute = (0, import_zod_openapi32.createRoute)({
|
|
5395
5526
|
method: "get",
|
|
5396
5527
|
path: "/trusted-devices",
|
|
@@ -5429,7 +5560,7 @@ var trustedDevicesGetHandler = async (c) => {
|
|
|
5429
5560
|
ipAddress: schema.userTrustedDevices.ipAddress,
|
|
5430
5561
|
lastUsedAt: schema.userTrustedDevices.lastUsedAt,
|
|
5431
5562
|
createdAt: schema.userTrustedDevices.createdAt
|
|
5432
|
-
}).from(schema.userTrustedDevices).where((0,
|
|
5563
|
+
}).from(schema.userTrustedDevices).where((0, import_drizzle_orm29.and)((0, import_drizzle_orm29.eq)(schema.userTrustedDevices.userId, userId), (0, import_drizzle_orm29.gt)(schema.userTrustedDevices.expiresAt, now)));
|
|
5433
5564
|
c.header("Cache-Control", "private, max-age=60");
|
|
5434
5565
|
c.header("Vary", "Cookie");
|
|
5435
5566
|
return c.json({ devices });
|
|
@@ -5465,7 +5596,7 @@ var trustedDevicesDeleteHandler = async (c) => {
|
|
|
5465
5596
|
if (!userId) return problems.unauthorized(c);
|
|
5466
5597
|
const deviceId = c.req.param("id");
|
|
5467
5598
|
const { db, schema } = getAuthContext(c);
|
|
5468
|
-
const result = await db.delete(schema.userTrustedDevices).where((0,
|
|
5599
|
+
const result = await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm29.and)((0, import_drizzle_orm29.eq)(schema.userTrustedDevices.id, deviceId), (0, import_drizzle_orm29.eq)(schema.userTrustedDevices.userId, userId))).returning({ id: schema.userTrustedDevices.id });
|
|
5469
5600
|
if (result.length === 0) {
|
|
5470
5601
|
return problems.notFound(c, "Device not found");
|
|
5471
5602
|
}
|
|
@@ -5475,7 +5606,7 @@ var trustedDevicesDeleteHandler = async (c) => {
|
|
|
5475
5606
|
|
|
5476
5607
|
// src/routes/2fa/backup-codes.ts
|
|
5477
5608
|
var import_zod_openapi33 = require("@hono/zod-openapi");
|
|
5478
|
-
var
|
|
5609
|
+
var import_drizzle_orm30 = require("drizzle-orm");
|
|
5479
5610
|
var backupCodesRegenerateRoute = (0, import_zod_openapi33.createRoute)({
|
|
5480
5611
|
method: "post",
|
|
5481
5612
|
path: "/backup-codes/regenerate",
|
|
@@ -5516,7 +5647,7 @@ var backupCodesRegenerateHandler = async (c) => {
|
|
|
5516
5647
|
logSecurityEvent("2fa_backup_regenerate_failed", "medium", { userId });
|
|
5517
5648
|
return problems.badRequest(c, "Invalid verification code");
|
|
5518
5649
|
}
|
|
5519
|
-
await db.delete(schema.userBackupCodes).where((0,
|
|
5650
|
+
await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm30.eq)(schema.userBackupCodes.userId, userId));
|
|
5520
5651
|
const codes = generateBackupCodes();
|
|
5521
5652
|
const now = Date.now();
|
|
5522
5653
|
const hashedCodes = await Promise.all(
|
|
@@ -5533,7 +5664,7 @@ var backupCodesRegenerateHandler = async (c) => {
|
|
|
5533
5664
|
|
|
5534
5665
|
// src/routes/2fa/challenge.ts
|
|
5535
5666
|
var import_zod_openapi34 = require("@hono/zod-openapi");
|
|
5536
|
-
var
|
|
5667
|
+
var import_drizzle_orm31 = require("drizzle-orm");
|
|
5537
5668
|
var import_cookie3 = require("hono/cookie");
|
|
5538
5669
|
var challengeRoute = (0, import_zod_openapi34.createRoute)({
|
|
5539
5670
|
method: "post",
|
|
@@ -5605,7 +5736,7 @@ var challengeHandler = async (c) => {
|
|
|
5605
5736
|
}
|
|
5606
5737
|
await deleteChallengeToken(c.env.OAUTH_STATES, challengeToken);
|
|
5607
5738
|
clearChallengeCookie(c, c.env);
|
|
5608
|
-
const [user] = await db.select().from(schema.users).where((0,
|
|
5739
|
+
const [user] = await db.select().from(schema.users).where((0, import_drizzle_orm31.eq)(schema.users.id, payload.userId)).limit(1);
|
|
5609
5740
|
if (!user) {
|
|
5610
5741
|
logger_default.error("User not found during 2FA challenge", { userId: payload.userId });
|
|
5611
5742
|
return problems.notFound(c, "User not found");
|
|
@@ -5646,7 +5777,7 @@ var challengeHandler = async (c) => {
|
|
|
5646
5777
|
|
|
5647
5778
|
// src/routes/2fa/challenge-resend.ts
|
|
5648
5779
|
var import_zod_openapi35 = require("@hono/zod-openapi");
|
|
5649
|
-
var
|
|
5780
|
+
var import_drizzle_orm32 = require("drizzle-orm");
|
|
5650
5781
|
var import_cookie4 = require("hono/cookie");
|
|
5651
5782
|
var challengeResendRoute = (0, import_zod_openapi35.createRoute)({
|
|
5652
5783
|
method: "post",
|
|
@@ -5684,7 +5815,7 @@ var challengeResendHandler = async (c) => {
|
|
|
5684
5815
|
if (!payload.methods.includes("email")) {
|
|
5685
5816
|
return problems.badRequest(c, "Email verification not available");
|
|
5686
5817
|
}
|
|
5687
|
-
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0,
|
|
5818
|
+
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.id, payload.userId)).limit(1);
|
|
5688
5819
|
if (!user) {
|
|
5689
5820
|
logger_default.error("User not found during 2FA resend", { userId: payload.userId });
|
|
5690
5821
|
return problems.notFound(c, "User not found");
|
|
@@ -5915,7 +6046,7 @@ var authorizeHandler = async (c) => {
|
|
|
5915
6046
|
|
|
5916
6047
|
// src/routes/oauth/callback.ts
|
|
5917
6048
|
var import_zod_openapi39 = require("@hono/zod-openapi");
|
|
5918
|
-
var
|
|
6049
|
+
var import_drizzle_orm33 = require("drizzle-orm");
|
|
5919
6050
|
var callbackRoute = (0, import_zod_openapi39.createRoute)({
|
|
5920
6051
|
method: "get",
|
|
5921
6052
|
path: "/{provider}/callback",
|
|
@@ -6041,10 +6172,24 @@ var callbackHandler = async (c) => {
|
|
|
6041
6172
|
logger_default.warn("No user ID returned from OAuth provider", { provider });
|
|
6042
6173
|
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
6043
6174
|
}
|
|
6044
|
-
const { db, schema } = getAuthContext(c);
|
|
6045
|
-
const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0,
|
|
6175
|
+
const { db, schema, env, invitations, signup } = getAuthContext(c);
|
|
6176
|
+
const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm33.eq)(schema.users.email, email)).limit(1);
|
|
6046
6177
|
let authUser = existingUser;
|
|
6047
6178
|
if (!authUser) {
|
|
6179
|
+
const invited = await verifyInvitation(invitations, {
|
|
6180
|
+
token: invitationToken,
|
|
6181
|
+
email,
|
|
6182
|
+
db,
|
|
6183
|
+
env
|
|
6184
|
+
});
|
|
6185
|
+
if (signup === "invitation" && !invited) {
|
|
6186
|
+
logSecurityEvent("oauth_signup_refused_no_invitation", "medium", {
|
|
6187
|
+
email,
|
|
6188
|
+
provider,
|
|
6189
|
+
hadInvitation: !!invitationToken
|
|
6190
|
+
});
|
|
6191
|
+
return c.redirect(`${appUrl}/login?error=invitation_required`);
|
|
6192
|
+
}
|
|
6048
6193
|
const [createdUser] = await db.insert(schema.users).values({
|
|
6049
6194
|
email,
|
|
6050
6195
|
name: name || null,
|
|
@@ -6055,6 +6200,15 @@ var callbackHandler = async (c) => {
|
|
|
6055
6200
|
activatedAt: /* @__PURE__ */ new Date()
|
|
6056
6201
|
}).returning({ id: schema.users.id, email: schema.users.email });
|
|
6057
6202
|
authUser = createdUser;
|
|
6203
|
+
if (invited) {
|
|
6204
|
+
await acceptInvitation(invitations, {
|
|
6205
|
+
token: invited,
|
|
6206
|
+
user: { id: createdUser.id, email: createdUser.email, name: name || null },
|
|
6207
|
+
db,
|
|
6208
|
+
env,
|
|
6209
|
+
context: "oauth_invitation_onAccepted_failed"
|
|
6210
|
+
});
|
|
6211
|
+
}
|
|
6058
6212
|
logSecurityEvent("user_signup_oauth", "low", {
|
|
6059
6213
|
userId: createdUser.id,
|
|
6060
6214
|
email: createdUser.email,
|
|
@@ -6094,7 +6248,7 @@ var callbackHandler = async (c) => {
|
|
|
6094
6248
|
|
|
6095
6249
|
// src/routes/oauth/complete.ts
|
|
6096
6250
|
var import_zod_openapi40 = require("@hono/zod-openapi");
|
|
6097
|
-
var
|
|
6251
|
+
var import_drizzle_orm34 = require("drizzle-orm");
|
|
6098
6252
|
var completeRoute = (0, import_zod_openapi40.createRoute)({
|
|
6099
6253
|
method: "post",
|
|
6100
6254
|
path: "/complete",
|
|
@@ -6142,7 +6296,7 @@ var completeHandler = async (c) => {
|
|
|
6142
6296
|
redirect: storedRedirect
|
|
6143
6297
|
} = JSON.parse(stored);
|
|
6144
6298
|
const { db, schema } = getAuthContext(c);
|
|
6145
|
-
await db.update(schema.users).set({ emailVerified: true }).where((0,
|
|
6299
|
+
await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm34.eq)(schema.users.id, userId));
|
|
6146
6300
|
await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
|
|
6147
6301
|
const fingerprint = await generateFingerprint(c.req.raw);
|
|
6148
6302
|
const ipAddress = getClientIp(c.req.raw);
|
|
@@ -6187,19 +6341,12 @@ var routes_default = authRoutes;
|
|
|
6187
6341
|
|
|
6188
6342
|
// src/lib/email/webhook-verifier.ts
|
|
6189
6343
|
var import_svix = require("svix");
|
|
6190
|
-
function verifyWebhookSignature(payload,
|
|
6344
|
+
function verifyWebhookSignature(payload, headers, secret) {
|
|
6191
6345
|
if (!secret || secret.trim() === "") {
|
|
6192
6346
|
throw new Error("Webhook secret is required");
|
|
6193
6347
|
}
|
|
6194
6348
|
try {
|
|
6195
|
-
|
|
6196
|
-
wh.verify(payload, {
|
|
6197
|
-
"svix-id": "",
|
|
6198
|
-
// Not used by Resend
|
|
6199
|
-
"svix-timestamp": "",
|
|
6200
|
-
// Not used by Resend
|
|
6201
|
-
"svix-signature": signature
|
|
6202
|
-
});
|
|
6349
|
+
new import_svix.Webhook(secret).verify(payload, { ...headers });
|
|
6203
6350
|
return true;
|
|
6204
6351
|
} catch (error) {
|
|
6205
6352
|
logger_default.warn("Webhook signature verification failed", {
|
|
@@ -6215,6 +6362,8 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
6215
6362
|
CloudflareEmailAdapter,
|
|
6216
6363
|
EmailService,
|
|
6217
6364
|
MAX_CHALLENGE_ATTEMPTS,
|
|
6365
|
+
PASSWORDLESS_MAX_ATTEMPTS,
|
|
6366
|
+
PASSWORDLESS_TTL_MS,
|
|
6218
6367
|
ProblemTypes,
|
|
6219
6368
|
ResendAdapter,
|
|
6220
6369
|
SCHEMA_VERSION,
|
|
@@ -6226,6 +6375,7 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
6226
6375
|
cancelEmailChange,
|
|
6227
6376
|
checkAccountLocked,
|
|
6228
6377
|
checkBreachedPassword,
|
|
6378
|
+
cleanupExpiredPasswordlessChallenges,
|
|
6229
6379
|
clearAccountLockout,
|
|
6230
6380
|
clearChallengeCookie,
|
|
6231
6381
|
clearSessionCookie,
|
|
@@ -6236,6 +6386,7 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
6236
6386
|
createChallengeToken,
|
|
6237
6387
|
createDeviceToken,
|
|
6238
6388
|
createEmailAdapter,
|
|
6389
|
+
createPasswordlessChallenge,
|
|
6239
6390
|
createProblemDetails,
|
|
6240
6391
|
createSession,
|
|
6241
6392
|
csrf,
|
|
@@ -6252,6 +6403,7 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
6252
6403
|
generate2faEnabledEmail,
|
|
6253
6404
|
generateApiKey,
|
|
6254
6405
|
generateBackupCodes,
|
|
6406
|
+
generateCode,
|
|
6255
6407
|
generateCodeChallenge,
|
|
6256
6408
|
generateCodeVerifier,
|
|
6257
6409
|
generateEmailChangeConfirmation,
|
|
@@ -6290,14 +6442,17 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
6290
6442
|
optionalAuth,
|
|
6291
6443
|
parseDeviceName,
|
|
6292
6444
|
parseDeviceType,
|
|
6445
|
+
passwordlessEmail,
|
|
6293
6446
|
problemJson,
|
|
6294
6447
|
problems,
|
|
6295
6448
|
rateLimit,
|
|
6449
|
+
recentPasswordlessFailures,
|
|
6296
6450
|
refreshSession,
|
|
6297
6451
|
requestEmailChange,
|
|
6298
6452
|
requireAuth,
|
|
6299
6453
|
requireVerifiedEmail,
|
|
6300
6454
|
retrieveChallengeToken,
|
|
6455
|
+
sessionRefreshDue,
|
|
6301
6456
|
setChallengeCookie,
|
|
6302
6457
|
setSessionCookie,
|
|
6303
6458
|
setTrustedDeviceCookie,
|
|
@@ -6315,6 +6470,8 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
6315
6470
|
verifyCodeChallenge,
|
|
6316
6471
|
verifyPassword,
|
|
6317
6472
|
verifyPasswordWithRotation,
|
|
6473
|
+
verifyPasswordlessCode,
|
|
6474
|
+
verifyPasswordlessLink,
|
|
6318
6475
|
verifyTotpCode,
|
|
6319
6476
|
verifyTurnstileToken,
|
|
6320
6477
|
verifyWebhookSignature
|