@fonderie/auth 7.16.0 → 7.17.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/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 = config.sessionDuration ?? DEFAULT_SESSION_DURATION;
316
+ const accessDuration = config.accessTokenDuration ?? DEFAULT_ACCESS_TOKEN_DURATION;
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,13 @@ 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
+ }
348
359
 
349
360
  // src/services/mfa.ts
350
361
  import { createHmac, randomBytes } from "crypto";
@@ -994,12 +1005,12 @@ var SessionModel = class {
994
1005
  async match(token) {
995
1006
  const hash = hashRefreshToken(token);
996
1007
  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`,
1008
+ `SELECT id, user_id, sid, token, created_at FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
998
1009
  [hash, token]
999
1010
  );
1000
1011
  if (current) return { kind: "current", row: toRow(current) };
1001
1012
  const [previous] = await this.store.query(
1002
- `SELECT id, user_id, sid, token, (previous_valid_until > now()) AS in_grace
1013
+ `SELECT id, user_id, sid, token, created_at, (previous_valid_until > now()) AS in_grace
1003
1014
  FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
1004
1015
  [hash]
1005
1016
  );
@@ -1114,7 +1125,7 @@ var SessionModel = class {
1114
1125
  }
1115
1126
  };
1116
1127
  function toRow(r) {
1117
- return { id: r.id, userId: r.user_id, sid: r.sid, stored: r.token };
1128
+ 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
1129
  }
1119
1130
 
1120
1131
  // src/models/backup-code.model.ts
@@ -1958,6 +1969,8 @@ function authController(store, config, bus, locales = defineLocales()) {
1958
1969
  if (!payload || payload.type !== "refresh") {
1959
1970
  return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
1960
1971
  }
1972
+ const resolvedRefresh = { ...config, ...config.resolve?.(ctx) };
1973
+ const maxAgeMs = durationMs(resolvedRefresh.sessionMaxAge);
1961
1974
  let issued = null;
1962
1975
  for (let attempt = 0; attempt < 3 && !issued; attempt++) {
1963
1976
  const found = await sessions.match(token);
@@ -1971,14 +1984,20 @@ function authController(store, config, bus, locales = defineLocales()) {
1971
1984
  );
1972
1985
  return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
1973
1986
  }
1987
+ if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
1988
+ await sessions.revokeById(found.row.id);
1989
+ return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
1990
+ }
1974
1991
  const user = await users.findById(found.row.userId);
1975
1992
  if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
1976
1993
  return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
1977
1994
  }
1978
- const pair = issueTokenPair(user.id, config, {
1995
+ const pair = issueTokenPair(user.id, resolvedRefresh, {
1979
1996
  loginMethod: payload.loginMethod ?? "email",
1980
1997
  phoneVerified: payload.phoneVerified ?? false,
1981
- ...found.row.sid ? { sid: found.row.sid } : {}
1998
+ ...found.row.sid ? { sid: found.row.sid } : {},
1999
+ // A refresh is not a sign-in: keep when the user last signed in.
2000
+ ...typeof payload.auth_time === "number" ? { authTime: payload.auth_time } : {}
1982
2001
  });
1983
2002
  const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
1984
2003
  if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
@@ -3610,7 +3629,7 @@ var AuthModule = class {
3610
3629
  name = "@fonderie/auth";
3611
3630
  // Baked in at build time by tsup.base, so the operator's Modules page can
3612
3631
  // say what is actually deployed rather than 'not reported'.
3613
- version = "7.16.0";
3632
+ version = "7.17.0";
3614
3633
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3615
3634
  describeAdmin() {
3616
3635
  return { routes: describeAuthAdminRoutes(this.store) };