@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.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
- const environment = env?.ENVIRONMENT;
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
- rawError: error,
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 24 hours",
4433
+ description: "Cache for 60 seconds",
4403
4434
  schema: {
4404
4435
  type: "string",
4405
- example: "private, max-age=86400, stale-while-revalidate=3600"
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=86400, stale-while-revalidate=3600");
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 24 hours",
4944
+ description: "Cache for 60 seconds",
4914
4945
  schema: {
4915
4946
  type: "string",
4916
- example: "private, max-age=86400, stale-while-revalidate=3600"
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=86400, stale-while-revalidate=3600");
4971
+ c.header("Cache-Control", "private, max-age=60");
4941
4972
  c.header("Vary", "Cookie");
4942
4973
  return c.json({ devices });
4943
4974
  };