@syncello/auth 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +42 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -108,9 +108,10 @@ declare function createSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'ses
|
|
|
108
108
|
* @param tables - Session tables (sessions, users)
|
|
109
109
|
* @param sessionId - Session ID from cookie
|
|
110
110
|
* @param currentFingerprint - Current browser fingerprint (optional)
|
|
111
|
+
* @param sessionMaxLifetimeMs - Absolute session lifetime cap in milliseconds (optional)
|
|
111
112
|
* @returns Session data or null if invalid
|
|
112
113
|
*/
|
|
113
|
-
declare function validateSession(db: DrizzleDB$2, tables: SessionTables, sessionId: string, currentFingerprint?: string): Promise<SessionData | null>;
|
|
114
|
+
declare function validateSession(db: DrizzleDB$2, tables: SessionTables, sessionId: string, currentFingerprint?: string, sessionMaxLifetimeMs?: number): Promise<SessionData | null>;
|
|
114
115
|
/**
|
|
115
116
|
* Refresh session expiration (sliding window)
|
|
116
117
|
* @param db - Drizzle database instance
|
|
@@ -161,6 +162,7 @@ declare function hashToken(token: string): Promise<string>;
|
|
|
161
162
|
*/
|
|
162
163
|
declare const AUTH_DEFAULTS: {
|
|
163
164
|
readonly SESSION_TTL_DAYS: 7;
|
|
165
|
+
readonly SESSION_MAX_LIFETIME_DAYS: 30;
|
|
164
166
|
readonly LOCKOUT_DURATION_MINUTES: 30;
|
|
165
167
|
readonly LOCKOUT_MAX_ATTEMPTS: 5;
|
|
166
168
|
readonly PASSWORD_RESET_TTL_MINUTES: 15;
|
package/dist/index.d.ts
CHANGED
|
@@ -108,9 +108,10 @@ declare function createSession(db: DrizzleDB$2, tables: Pick<SessionTables, 'ses
|
|
|
108
108
|
* @param tables - Session tables (sessions, users)
|
|
109
109
|
* @param sessionId - Session ID from cookie
|
|
110
110
|
* @param currentFingerprint - Current browser fingerprint (optional)
|
|
111
|
+
* @param sessionMaxLifetimeMs - Absolute session lifetime cap in milliseconds (optional)
|
|
111
112
|
* @returns Session data or null if invalid
|
|
112
113
|
*/
|
|
113
|
-
declare function validateSession(db: DrizzleDB$2, tables: SessionTables, sessionId: string, currentFingerprint?: string): Promise<SessionData | null>;
|
|
114
|
+
declare function validateSession(db: DrizzleDB$2, tables: SessionTables, sessionId: string, currentFingerprint?: string, sessionMaxLifetimeMs?: number): Promise<SessionData | null>;
|
|
114
115
|
/**
|
|
115
116
|
* Refresh session expiration (sliding window)
|
|
116
117
|
* @param db - Drizzle database instance
|
|
@@ -161,6 +162,7 @@ declare function hashToken(token: string): Promise<string>;
|
|
|
161
162
|
*/
|
|
162
163
|
declare const AUTH_DEFAULTS: {
|
|
163
164
|
readonly SESSION_TTL_DAYS: 7;
|
|
165
|
+
readonly SESSION_MAX_LIFETIME_DAYS: 30;
|
|
164
166
|
readonly LOCKOUT_DURATION_MINUTES: 30;
|
|
165
167
|
readonly LOCKOUT_MAX_ATTEMPTS: 5;
|
|
166
168
|
readonly PASSWORD_RESET_TTL_MINUTES: 15;
|
package/dist/index.js
CHANGED
|
@@ -252,6 +252,9 @@ async function hashToken(token) {
|
|
|
252
252
|
// src/core/config.ts
|
|
253
253
|
var AUTH_DEFAULTS = {
|
|
254
254
|
SESSION_TTL_DAYS: 7,
|
|
255
|
+
// Absolute cap: a session dies this long after creation regardless of
|
|
256
|
+
// sliding-window activity (limits the value of a stolen session token).
|
|
257
|
+
SESSION_MAX_LIFETIME_DAYS: 30,
|
|
255
258
|
LOCKOUT_DURATION_MINUTES: 30,
|
|
256
259
|
LOCKOUT_MAX_ATTEMPTS: 5,
|
|
257
260
|
PASSWORD_RESET_TTL_MINUTES: 15,
|
|
@@ -284,7 +287,7 @@ async function createSession(db, tables2, userId, fingerprint, ipAddress, sessio
|
|
|
284
287
|
});
|
|
285
288
|
return sessionId;
|
|
286
289
|
}
|
|
287
|
-
async function validateSession(db, tables2, sessionId, currentFingerprint) {
|
|
290
|
+
async function validateSession(db, tables2, sessionId, currentFingerprint, sessionMaxLifetimeMs = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3) {
|
|
288
291
|
const { sessions, users } = tables2;
|
|
289
292
|
logger_default.debug("Validating session", {
|
|
290
293
|
sessionId: sessionId?.slice(0, 8),
|
|
@@ -311,6 +314,17 @@ async function validateSession(db, tables2, sessionId, currentFingerprint) {
|
|
|
311
314
|
return null;
|
|
312
315
|
}
|
|
313
316
|
const row = result[0];
|
|
317
|
+
if (Date.now() - row.sessionCreatedAt > sessionMaxLifetimeMs) {
|
|
318
|
+
logger_default.info("Session exceeded absolute lifetime - revoked", {
|
|
319
|
+
type: "security",
|
|
320
|
+
event: "session_max_lifetime_exceeded",
|
|
321
|
+
severity: "low",
|
|
322
|
+
sessionId: sessionId.slice(0, 8),
|
|
323
|
+
userId: row.userId
|
|
324
|
+
});
|
|
325
|
+
await deleteSession(db, { sessions }, sessionId);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
314
328
|
if (currentFingerprint && row.sessionFingerprint && row.sessionFingerprint !== currentFingerprint) {
|
|
315
329
|
logger_default.info("Session fingerprint mismatch - likely SSR request", {
|
|
316
330
|
type: "security",
|
|
@@ -589,8 +603,7 @@ function getSessionCookieName(env) {
|
|
|
589
603
|
return COOKIE_NAMES[environment] || COOKIE_NAMES.development;
|
|
590
604
|
}
|
|
591
605
|
function isProduction(env) {
|
|
592
|
-
|
|
593
|
-
return environment === "production" || environment === "staging";
|
|
606
|
+
return env?.ENVIRONMENT !== "development";
|
|
594
607
|
}
|
|
595
608
|
function getCookieSecurityFlags(env) {
|
|
596
609
|
if (isProduction(env)) {
|
|
@@ -1177,6 +1190,19 @@ var problems = {
|
|
|
1177
1190
|
};
|
|
1178
1191
|
|
|
1179
1192
|
// src/middleware/auth.ts
|
|
1193
|
+
var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
|
|
1194
|
+
async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
|
|
1195
|
+
if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
|
|
1196
|
+
logger_default.info("Session exceeded absolute lifetime - revoked", {
|
|
1197
|
+
type: "security",
|
|
1198
|
+
event: "session_max_lifetime_exceeded",
|
|
1199
|
+
severity: "low",
|
|
1200
|
+
sessionId: sessionId.slice(0, 8),
|
|
1201
|
+
userId: session.userId
|
|
1202
|
+
});
|
|
1203
|
+
await deleteSession(db, { sessions }, sessionId);
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1180
1206
|
async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
|
|
1181
1207
|
const cookieName = getSessionCookieName(env);
|
|
1182
1208
|
const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
|
|
@@ -1186,10 +1212,13 @@ async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
|
|
|
1186
1212
|
const foundSessions = await db.select({
|
|
1187
1213
|
userId: sessions.userId,
|
|
1188
1214
|
expiresAt: sessions.expiresAt,
|
|
1215
|
+
createdAt: sessions.createdAt,
|
|
1189
1216
|
fingerprint: sessions.fingerprint
|
|
1190
1217
|
}).from(sessions).where(and2(eq4(sessions.id, sessionId), gt2(sessions.expiresAt, Date.now()))).limit(1);
|
|
1191
1218
|
if (foundSessions.length === 0) return null;
|
|
1192
1219
|
const session = foundSessions[0];
|
|
1220
|
+
if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
|
|
1221
|
+
await refreshSession(db, { sessions }, sessionId);
|
|
1193
1222
|
const userAgent = request.headers.get("user-agent") || "";
|
|
1194
1223
|
const cfWorker = request.headers.get("cf-worker") || "";
|
|
1195
1224
|
const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
|
|
@@ -1233,10 +1262,12 @@ async function getSessionFromBearerToken(db, sessions, authHeader) {
|
|
|
1233
1262
|
const sessionId = authHeader.slice(7);
|
|
1234
1263
|
const foundSessions = await db.select({
|
|
1235
1264
|
userId: sessions.userId,
|
|
1236
|
-
expiresAt: sessions.expiresAt
|
|
1265
|
+
expiresAt: sessions.expiresAt,
|
|
1266
|
+
createdAt: sessions.createdAt
|
|
1237
1267
|
}).from(sessions).where(and2(eq4(sessions.id, sessionId), gt2(sessions.expiresAt, Date.now()))).limit(1);
|
|
1238
1268
|
if (foundSessions.length === 0) return null;
|
|
1239
1269
|
const session = foundSessions[0];
|
|
1270
|
+
if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
|
|
1240
1271
|
await refreshSession(db, { sessions }, sessionId);
|
|
1241
1272
|
return {
|
|
1242
1273
|
userId: session.userId,
|
|
@@ -2235,7 +2266,7 @@ var CloudflareEmailAdapter = class {
|
|
|
2235
2266
|
logger_default.error("Cloudflare send error", {
|
|
2236
2267
|
provider: this.providerName,
|
|
2237
2268
|
error: errorText,
|
|
2238
|
-
|
|
2269
|
+
errorName: error instanceof Error ? error.name : typeof error,
|
|
2239
2270
|
to: options.to,
|
|
2240
2271
|
subject: options.subject
|
|
2241
2272
|
});
|
|
@@ -4399,10 +4430,10 @@ var statusRoute = createRoute16({
|
|
|
4399
4430
|
content: { "application/json": { schema: statusResponseSchema } },
|
|
4400
4431
|
headers: {
|
|
4401
4432
|
"Cache-Control": {
|
|
4402
|
-
description: "Cache for
|
|
4433
|
+
description: "Cache for 60 seconds",
|
|
4403
4434
|
schema: {
|
|
4404
4435
|
type: "string",
|
|
4405
|
-
example: "private, max-age=
|
|
4436
|
+
example: "private, max-age=60"
|
|
4406
4437
|
}
|
|
4407
4438
|
}
|
|
4408
4439
|
}
|
|
@@ -4419,7 +4450,7 @@ var statusHandler = async (c) => {
|
|
|
4419
4450
|
const { db, schema } = getAuthContext(c);
|
|
4420
4451
|
const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
|
|
4421
4452
|
const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where(and7(eq19(schema.userBackupCodes.userId, userId), isNull2(schema.userBackupCodes.usedAt)));
|
|
4422
|
-
c.header("Cache-Control", "private, max-age=
|
|
4453
|
+
c.header("Cache-Control", "private, max-age=60");
|
|
4423
4454
|
c.header("Vary", "Cookie");
|
|
4424
4455
|
return c.json({
|
|
4425
4456
|
enabled: methods.length > 0,
|
|
@@ -4910,10 +4941,10 @@ var trustedDevicesGetRoute = createRoute24({
|
|
|
4910
4941
|
content: { "application/json": { schema: trustedDevicesResponseSchema } },
|
|
4911
4942
|
headers: {
|
|
4912
4943
|
"Cache-Control": {
|
|
4913
|
-
description: "Cache for
|
|
4944
|
+
description: "Cache for 60 seconds",
|
|
4914
4945
|
schema: {
|
|
4915
4946
|
type: "string",
|
|
4916
|
-
example: "private, max-age=
|
|
4947
|
+
example: "private, max-age=60"
|
|
4917
4948
|
}
|
|
4918
4949
|
}
|
|
4919
4950
|
}
|
|
@@ -4937,7 +4968,7 @@ var trustedDevicesGetHandler = async (c) => {
|
|
|
4937
4968
|
lastUsedAt: schema.userTrustedDevices.lastUsedAt,
|
|
4938
4969
|
createdAt: schema.userTrustedDevices.createdAt
|
|
4939
4970
|
}).from(schema.userTrustedDevices).where(and13(eq27(schema.userTrustedDevices.userId, userId), gt5(schema.userTrustedDevices.expiresAt, now)));
|
|
4940
|
-
c.header("Cache-Control", "private, max-age=
|
|
4971
|
+
c.header("Cache-Control", "private, max-age=60");
|
|
4941
4972
|
c.header("Vary", "Cookie");
|
|
4942
4973
|
return c.json({ devices });
|
|
4943
4974
|
};
|