@fonderie/auth 7.16.0 → 7.17.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
@@ -3,8 +3,8 @@ export { IMfaChallenge, ISession } from './types.cjs';
3
3
  import { IFonderieModule, IAdminDescription, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
4
4
  import { IStoreAdapter } from '@fonderie/store';
5
5
  import { EventBus } from '@fonderie/events';
6
- import { I as IAuthConfig, a as IRequestLocation } from './session-auNb2L8m.cjs';
7
- export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, d as IAuthRateLimitConfig, e as IAuthRuntimeConfig, f as IAuthSecrets, g as IDataExportContributor, h as ILocationRequest, L as LocationResolver, M as MESSAGE_KEYS, i as buildAuthAccountLimiter, j as buildAuthIpLimiter, r as resolveLocation, s as sanitizeLocation, w as withSession } from './session-auNb2L8m.cjs';
6
+ import { I as IAuthConfig, a as IRequestLocation } from './session-C-ptRylj.cjs';
7
+ export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, d as IAuthRateLimitConfig, e as IAuthRuntimeConfig, f as IAuthSecrets, g as IDataExportContributor, h as ILocationRequest, L as LocationResolver, M as MESSAGE_KEYS, i as buildAuthAccountLimiter, j as buildAuthIpLimiter, r as resolveLocation, s as sanitizeLocation, w as withSession } from './session-C-ptRylj.cjs';
8
8
  import { IUserDTO } from './dtos/user.cjs';
9
9
  export { toUserDTO } from './dtos/user.cjs';
10
10
  export { requireAuth, validate } from '@fonderie/core/middlewares';
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export { IMfaChallenge, ISession } from './types.js';
3
3
  import { IFonderieModule, IAdminDescription, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
4
4
  import { IStoreAdapter } from '@fonderie/store';
5
5
  import { EventBus } from '@fonderie/events';
6
- import { I as IAuthConfig, a as IRequestLocation } from './session-auNb2L8m.js';
7
- export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, d as IAuthRateLimitConfig, e as IAuthRuntimeConfig, f as IAuthSecrets, g as IDataExportContributor, h as ILocationRequest, L as LocationResolver, M as MESSAGE_KEYS, i as buildAuthAccountLimiter, j as buildAuthIpLimiter, r as resolveLocation, s as sanitizeLocation, w as withSession } from './session-auNb2L8m.js';
6
+ import { I as IAuthConfig, a as IRequestLocation } from './session-C-ptRylj.js';
7
+ export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, d as IAuthRateLimitConfig, e as IAuthRuntimeConfig, f as IAuthSecrets, g as IDataExportContributor, h as ILocationRequest, L as LocationResolver, M as MESSAGE_KEYS, i as buildAuthAccountLimiter, j as buildAuthIpLimiter, r as resolveLocation, s as sanitizeLocation, w as withSession } from './session-C-ptRylj.js';
8
8
  import { IUserDTO } from './dtos/user.js';
9
9
  export { toUserDTO } from './dtos/user.js';
10
10
  export { requireAuth, validate } from '@fonderie/core/middlewares';
package/dist/index.js CHANGED
@@ -243,8 +243,11 @@ import { NOTIFICATION_EVENT } from "@fonderie/events";
243
243
 
244
244
  // src/config.ts
245
245
  var DEFAULT_VERIFICATION_COOLDOWN = 5 * 60 * 1e3;
246
+ var DEFAULT_SESSION_DURATION = "90d";
247
+ var DEFAULT_ACCESS_TOKEN_DURATION = "1h";
246
248
  var AUTH_CONFIG_KEYS = {
247
249
  sessionDuration: "auth.session.duration",
250
+ sessionMaxAge: "auth.session.max_age",
248
251
  verificationCooldown: "auth.verification.cooldown",
249
252
  mfa: "auth.mfa.enabled",
250
253
  requireVerification: "auth.verification.required"
@@ -309,13 +312,14 @@ function issueMfaPendingToken(userId, config, loginMethod) {
309
312
  );
310
313
  }
311
314
  function issueTokenPair(userId, config, options) {
312
- const duration = config.sessionDuration ?? "7d";
313
- const accessDuration = config.accessTokenDuration ?? "24h";
315
+ const duration = validDuration(config.sessionDuration, DEFAULT_SESSION_DURATION, "sessionDuration");
316
+ const accessDuration = validDuration(config.accessTokenDuration, DEFAULT_ACCESS_TOKEN_DURATION, "accessTokenDuration");
317
+ const authTime = options.authTime ?? Math.floor(Date.now() / 1e3);
314
318
  const loginMethod = options.loginMethod;
315
319
  const phoneVerified = options.phoneVerified ?? false;
316
320
  const sid = options.sid ?? randomUUID();
317
321
  const accessToken = jwt.sign(
318
- { sub: userId, type: "access", loginMethod, phoneVerified, sid },
322
+ { sub: userId, type: "access", loginMethod, phoneVerified, sid, auth_time: authTime },
319
323
  config.jwtSecret,
320
324
  // jwtid: every token is unique. With the session id kept across rotations,
321
325
  // two tokens issued in the same second would otherwise be byte-identical —
@@ -323,7 +327,7 @@ function issueTokenPair(userId, config, options) {
323
327
  { expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret), jwtid: randomUUID() }
324
328
  );
325
329
  const refreshToken = jwt.sign(
326
- { sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
330
+ { sub: userId, type: "refresh", loginMethod, phoneVerified, sid, auth_time: authTime },
327
331
  config.jwtSecret,
328
332
  { expiresIn: duration, keyid: keyIdOf(config.jwtSecret), jwtid: randomUUID() }
329
333
  );
@@ -345,6 +349,24 @@ function verifyToken(token, config) {
345
349
  }
346
350
  return null;
347
351
  }
352
+ function durationMs(value) {
353
+ const m = /^\s*(\d+)\s*(ms|s|m|h|d|w|y)?\s*$/i.exec(value ?? "");
354
+ if (!m) return null;
355
+ const unit = (m[2] ?? "s").toLowerCase();
356
+ const scale = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5, y: 315576e5 };
357
+ return Number(m[1]) * (scale[unit] ?? 1e3);
358
+ }
359
+ var warnedDurations = /* @__PURE__ */ new Set();
360
+ function validDuration(value, fallback, name) {
361
+ if (value === void 0 || value === null || value === "") return fallback;
362
+ if (durationMs(String(value)) !== null) return String(value);
363
+ const key = `${name}=${String(value)}`;
364
+ if (!warnedDurations.has(key)) {
365
+ warnedDurations.add(key);
366
+ console.warn(`[auth] ${name} ${JSON.stringify(value)} is not a duration (e.g. '90d', '1h') \u2014 using ${fallback}`);
367
+ }
368
+ return fallback;
369
+ }
348
370
 
349
371
  // src/services/mfa.ts
350
372
  import { createHmac, randomBytes } from "crypto";
@@ -994,12 +1016,12 @@ var SessionModel = class {
994
1016
  async match(token) {
995
1017
  const hash = hashRefreshToken(token);
996
1018
  const [current] = await this.store.query(
997
- `SELECT id, user_id, sid, token FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
1019
+ `SELECT id, user_id, sid, token, created_at FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
998
1020
  [hash, token]
999
1021
  );
1000
1022
  if (current) return { kind: "current", row: toRow(current) };
1001
1023
  const [previous] = await this.store.query(
1002
- `SELECT id, user_id, sid, token, (previous_valid_until > now()) AS in_grace
1024
+ `SELECT id, user_id, sid, token, created_at, (previous_valid_until > now()) AS in_grace
1003
1025
  FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
1004
1026
  [hash]
1005
1027
  );
@@ -1114,7 +1136,7 @@ var SessionModel = class {
1114
1136
  }
1115
1137
  };
1116
1138
  function toRow(r) {
1117
- return { id: r.id, userId: r.user_id, sid: r.sid, stored: r.token };
1139
+ return { id: r.id, userId: r.user_id, sid: r.sid, stored: r.token, createdAt: r.created_at ? new Date(r.created_at) : /* @__PURE__ */ new Date() };
1118
1140
  }
1119
1141
 
1120
1142
  // src/models/backup-code.model.ts
@@ -1958,6 +1980,8 @@ function authController(store, config, bus, locales = defineLocales()) {
1958
1980
  if (!payload || payload.type !== "refresh") {
1959
1981
  return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
1960
1982
  }
1983
+ const resolvedRefresh = { ...config, ...config.resolve?.(ctx) };
1984
+ const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
1961
1985
  let issued = null;
1962
1986
  for (let attempt = 0; attempt < 3 && !issued; attempt++) {
1963
1987
  const found = await sessions.match(token);
@@ -1971,14 +1995,20 @@ function authController(store, config, bus, locales = defineLocales()) {
1971
1995
  );
1972
1996
  return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
1973
1997
  }
1998
+ if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
1999
+ await sessions.revokeById(found.row.id);
2000
+ return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
2001
+ }
1974
2002
  const user = await users.findById(found.row.userId);
1975
2003
  if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
1976
2004
  return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
1977
2005
  }
1978
- const pair = issueTokenPair(user.id, config, {
2006
+ const pair = issueTokenPair(user.id, resolvedRefresh, {
1979
2007
  loginMethod: payload.loginMethod ?? "email",
1980
2008
  phoneVerified: payload.phoneVerified ?? false,
1981
- ...found.row.sid ? { sid: found.row.sid } : {}
2009
+ ...found.row.sid ? { sid: found.row.sid } : {},
2010
+ // A refresh is not a sign-in: keep when the user last signed in.
2011
+ ...typeof payload.auth_time === "number" ? { authTime: payload.auth_time } : {}
1982
2012
  });
1983
2013
  const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
1984
2014
  if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
@@ -3610,7 +3640,7 @@ var AuthModule = class {
3610
3640
  name = "@fonderie/auth";
3611
3641
  // Baked in at build time by tsup.base, so the operator's Modules page can
3612
3642
  // say what is actually deployed rather than 'not reported'.
3613
- version = "7.16.0";
3643
+ version = "7.17.1";
3614
3644
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3615
3645
  describeAdmin() {
3616
3646
  return { routes: describeAuthAdminRoutes(this.store) };