@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 CHANGED
@@ -644,6 +644,9 @@ async function hashToken(token) {
644
644
  // src/core/config.ts
645
645
  var AUTH_DEFAULTS = {
646
646
  SESSION_TTL_DAYS: 7,
647
+ // Absolute cap: a session dies this long after creation regardless of
648
+ // sliding-window activity (limits the value of a stolen session token).
649
+ SESSION_MAX_LIFETIME_DAYS: 30,
647
650
  LOCKOUT_DURATION_MINUTES: 30,
648
651
  LOCKOUT_MAX_ATTEMPTS: 5,
649
652
  PASSWORD_RESET_TTL_MINUTES: 15,
@@ -676,7 +679,7 @@ async function createSession(db, tables2, userId, fingerprint, ipAddress, sessio
676
679
  });
677
680
  return sessionId;
678
681
  }
679
- async function validateSession(db, tables2, sessionId, currentFingerprint) {
682
+ async function validateSession(db, tables2, sessionId, currentFingerprint, sessionMaxLifetimeMs = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3) {
680
683
  const { sessions, users } = tables2;
681
684
  logger_default.debug("Validating session", {
682
685
  sessionId: sessionId?.slice(0, 8),
@@ -703,6 +706,17 @@ async function validateSession(db, tables2, sessionId, currentFingerprint) {
703
706
  return null;
704
707
  }
705
708
  const row = result[0];
709
+ if (Date.now() - row.sessionCreatedAt > sessionMaxLifetimeMs) {
710
+ logger_default.info("Session exceeded absolute lifetime - revoked", {
711
+ type: "security",
712
+ event: "session_max_lifetime_exceeded",
713
+ severity: "low",
714
+ sessionId: sessionId.slice(0, 8),
715
+ userId: row.userId
716
+ });
717
+ await deleteSession(db, { sessions }, sessionId);
718
+ return null;
719
+ }
706
720
  if (currentFingerprint && row.sessionFingerprint && row.sessionFingerprint !== currentFingerprint) {
707
721
  logger_default.info("Session fingerprint mismatch - likely SSR request", {
708
722
  type: "security",
@@ -981,8 +995,7 @@ function getSessionCookieName(env) {
981
995
  return COOKIE_NAMES[environment] || COOKIE_NAMES.development;
982
996
  }
983
997
  function isProduction(env) {
984
- const environment = env?.ENVIRONMENT;
985
- return environment === "production" || environment === "staging";
998
+ return env?.ENVIRONMENT !== "development";
986
999
  }
987
1000
  function getCookieSecurityFlags(env) {
988
1001
  if (isProduction(env)) {
@@ -1569,6 +1582,19 @@ var problems = {
1569
1582
  };
1570
1583
 
1571
1584
  // src/middleware/auth.ts
1585
+ var SESSION_MAX_LIFETIME_MS = AUTH_DEFAULTS.SESSION_MAX_LIFETIME_DAYS * 24 * 60 * 60 * 1e3;
1586
+ async function revokeIfPastMaxLifetime(db, sessions, sessionId, session) {
1587
+ if (Date.now() - session.createdAt <= SESSION_MAX_LIFETIME_MS) return false;
1588
+ logger_default.info("Session exceeded absolute lifetime - revoked", {
1589
+ type: "security",
1590
+ event: "session_max_lifetime_exceeded",
1591
+ severity: "low",
1592
+ sessionId: sessionId.slice(0, 8),
1593
+ userId: session.userId
1594
+ });
1595
+ await deleteSession(db, { sessions }, sessionId);
1596
+ return true;
1597
+ }
1572
1598
  async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
1573
1599
  const cookieName = getSessionCookieName(env);
1574
1600
  const cookiePattern = new RegExp(`${cookieName}=([^;]+)`);
@@ -1578,10 +1604,13 @@ async function getSessionFromCookie(db, sessions, cookieHeader, request, env) {
1578
1604
  const foundSessions = await db.select({
1579
1605
  userId: sessions.userId,
1580
1606
  expiresAt: sessions.expiresAt,
1607
+ createdAt: sessions.createdAt,
1581
1608
  fingerprint: sessions.fingerprint
1582
1609
  }).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);
1583
1610
  if (foundSessions.length === 0) return null;
1584
1611
  const session = foundSessions[0];
1612
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1613
+ await refreshSession(db, { sessions }, sessionId);
1585
1614
  const userAgent = request.headers.get("user-agent") || "";
1586
1615
  const cfWorker = request.headers.get("cf-worker") || "";
1587
1616
  const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || "";
@@ -1625,10 +1654,12 @@ async function getSessionFromBearerToken(db, sessions, authHeader) {
1625
1654
  const sessionId = authHeader.slice(7);
1626
1655
  const foundSessions = await db.select({
1627
1656
  userId: sessions.userId,
1628
- expiresAt: sessions.expiresAt
1657
+ expiresAt: sessions.expiresAt,
1658
+ createdAt: sessions.createdAt
1629
1659
  }).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);
1630
1660
  if (foundSessions.length === 0) return null;
1631
1661
  const session = foundSessions[0];
1662
+ if (await revokeIfPastMaxLifetime(db, sessions, sessionId, session)) return null;
1632
1663
  await refreshSession(db, { sessions }, sessionId);
1633
1664
  return {
1634
1665
  userId: session.userId,
@@ -2627,7 +2658,7 @@ var CloudflareEmailAdapter = class {
2627
2658
  logger_default.error("Cloudflare send error", {
2628
2659
  provider: this.providerName,
2629
2660
  error: errorText,
2630
- rawError: error,
2661
+ errorName: error instanceof Error ? error.name : typeof error,
2631
2662
  to: options.to,
2632
2663
  subject: options.subject
2633
2664
  });
@@ -4791,10 +4822,10 @@ var statusRoute = (0, import_zod_openapi24.createRoute)({
4791
4822
  content: { "application/json": { schema: statusResponseSchema } },
4792
4823
  headers: {
4793
4824
  "Cache-Control": {
4794
- description: "Cache for 24 hours",
4825
+ description: "Cache for 60 seconds",
4795
4826
  schema: {
4796
4827
  type: "string",
4797
- example: "private, max-age=86400, stale-while-revalidate=3600"
4828
+ example: "private, max-age=60"
4798
4829
  }
4799
4830
  }
4800
4831
  }
@@ -4811,7 +4842,7 @@ var statusHandler = async (c) => {
4811
4842
  const { db, schema } = getAuthContext(c);
4812
4843
  const methods = await getUserEnabled2faMethods(db, userId, schema.user2faMethods);
4813
4844
  const backupCodes = await db.select({ id: schema.userBackupCodes.id }).from(schema.userBackupCodes).where((0, import_drizzle_orm20.and)((0, import_drizzle_orm20.eq)(schema.userBackupCodes.userId, userId), (0, import_drizzle_orm20.isNull)(schema.userBackupCodes.usedAt)));
4814
- c.header("Cache-Control", "private, max-age=86400, stale-while-revalidate=3600");
4845
+ c.header("Cache-Control", "private, max-age=60");
4815
4846
  c.header("Vary", "Cookie");
4816
4847
  return c.json({
4817
4848
  enabled: methods.length > 0,
@@ -5302,10 +5333,10 @@ var trustedDevicesGetRoute = (0, import_zod_openapi32.createRoute)({
5302
5333
  content: { "application/json": { schema: trustedDevicesResponseSchema } },
5303
5334
  headers: {
5304
5335
  "Cache-Control": {
5305
- description: "Cache for 24 hours",
5336
+ description: "Cache for 60 seconds",
5306
5337
  schema: {
5307
5338
  type: "string",
5308
- example: "private, max-age=86400, stale-while-revalidate=3600"
5339
+ example: "private, max-age=60"
5309
5340
  }
5310
5341
  }
5311
5342
  }
@@ -5329,7 +5360,7 @@ var trustedDevicesGetHandler = async (c) => {
5329
5360
  lastUsedAt: schema.userTrustedDevices.lastUsedAt,
5330
5361
  createdAt: schema.userTrustedDevices.createdAt
5331
5362
  }).from(schema.userTrustedDevices).where((0, import_drizzle_orm28.and)((0, import_drizzle_orm28.eq)(schema.userTrustedDevices.userId, userId), (0, import_drizzle_orm28.gt)(schema.userTrustedDevices.expiresAt, now)));
5332
- c.header("Cache-Control", "private, max-age=86400, stale-while-revalidate=3600");
5363
+ c.header("Cache-Control", "private, max-age=60");
5333
5364
  c.header("Vary", "Cookie");
5334
5365
  return c.json({ devices });
5335
5366
  };