@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/brain/signatures.md +1 -0
- package/dist/index.cjs +40 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +40 -10
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +8 -3
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +8 -3
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-auNb2L8m.d.cts → session-C-ptRylj.d.cts} +1 -0
- package/dist/{session-auNb2L8m.d.ts → session-C-ptRylj.d.ts} +1 -0
- package/package.json +1 -1
package/brain/signatures.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -288,8 +288,11 @@ var import_events = require("@fonderie/events");
|
|
|
288
288
|
|
|
289
289
|
// src/config.ts
|
|
290
290
|
var DEFAULT_VERIFICATION_COOLDOWN = 5 * 60 * 1e3;
|
|
291
|
+
var DEFAULT_SESSION_DURATION = "90d";
|
|
292
|
+
var DEFAULT_ACCESS_TOKEN_DURATION = "1h";
|
|
291
293
|
var AUTH_CONFIG_KEYS = {
|
|
292
294
|
sessionDuration: "auth.session.duration",
|
|
295
|
+
sessionMaxAge: "auth.session.max_age",
|
|
293
296
|
verificationCooldown: "auth.verification.cooldown",
|
|
294
297
|
mfa: "auth.mfa.enabled",
|
|
295
298
|
requireVerification: "auth.verification.required"
|
|
@@ -354,13 +357,14 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
354
357
|
);
|
|
355
358
|
}
|
|
356
359
|
function issueTokenPair(userId, config, options) {
|
|
357
|
-
const duration = config.sessionDuration
|
|
358
|
-
const accessDuration = config.accessTokenDuration
|
|
360
|
+
const duration = validDuration(config.sessionDuration, DEFAULT_SESSION_DURATION, "sessionDuration");
|
|
361
|
+
const accessDuration = validDuration(config.accessTokenDuration, DEFAULT_ACCESS_TOKEN_DURATION, "accessTokenDuration");
|
|
362
|
+
const authTime = options.authTime ?? Math.floor(Date.now() / 1e3);
|
|
359
363
|
const loginMethod = options.loginMethod;
|
|
360
364
|
const phoneVerified = options.phoneVerified ?? false;
|
|
361
365
|
const sid = options.sid ?? (0, import_node_crypto.randomUUID)();
|
|
362
366
|
const accessToken = import_jsonwebtoken.default.sign(
|
|
363
|
-
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
367
|
+
{ sub: userId, type: "access", loginMethod, phoneVerified, sid, auth_time: authTime },
|
|
364
368
|
config.jwtSecret,
|
|
365
369
|
// jwtid: every token is unique. With the session id kept across rotations,
|
|
366
370
|
// two tokens issued in the same second would otherwise be byte-identical —
|
|
@@ -368,7 +372,7 @@ function issueTokenPair(userId, config, options) {
|
|
|
368
372
|
{ expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret), jwtid: (0, import_node_crypto.randomUUID)() }
|
|
369
373
|
);
|
|
370
374
|
const refreshToken = import_jsonwebtoken.default.sign(
|
|
371
|
-
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
375
|
+
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid, auth_time: authTime },
|
|
372
376
|
config.jwtSecret,
|
|
373
377
|
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret), jwtid: (0, import_node_crypto.randomUUID)() }
|
|
374
378
|
);
|
|
@@ -390,6 +394,24 @@ function verifyToken(token, config) {
|
|
|
390
394
|
}
|
|
391
395
|
return null;
|
|
392
396
|
}
|
|
397
|
+
function durationMs(value) {
|
|
398
|
+
const m = /^\s*(\d+)\s*(ms|s|m|h|d|w|y)?\s*$/i.exec(value ?? "");
|
|
399
|
+
if (!m) return null;
|
|
400
|
+
const unit = (m[2] ?? "s").toLowerCase();
|
|
401
|
+
const scale = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5, y: 315576e5 };
|
|
402
|
+
return Number(m[1]) * (scale[unit] ?? 1e3);
|
|
403
|
+
}
|
|
404
|
+
var warnedDurations = /* @__PURE__ */ new Set();
|
|
405
|
+
function validDuration(value, fallback, name) {
|
|
406
|
+
if (value === void 0 || value === null || value === "") return fallback;
|
|
407
|
+
if (durationMs(String(value)) !== null) return String(value);
|
|
408
|
+
const key = `${name}=${String(value)}`;
|
|
409
|
+
if (!warnedDurations.has(key)) {
|
|
410
|
+
warnedDurations.add(key);
|
|
411
|
+
console.warn(`[auth] ${name} ${JSON.stringify(value)} is not a duration (e.g. '90d', '1h') \u2014 using ${fallback}`);
|
|
412
|
+
}
|
|
413
|
+
return fallback;
|
|
414
|
+
}
|
|
393
415
|
|
|
394
416
|
// src/services/mfa.ts
|
|
395
417
|
var import_node_crypto2 = require("crypto");
|
|
@@ -1039,12 +1061,12 @@ var SessionModel = class {
|
|
|
1039
1061
|
async match(token) {
|
|
1040
1062
|
const hash = hashRefreshToken(token);
|
|
1041
1063
|
const [current] = await this.store.query(
|
|
1042
|
-
`SELECT id, user_id, sid, token FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
|
|
1064
|
+
`SELECT id, user_id, sid, token, created_at FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
|
|
1043
1065
|
[hash, token]
|
|
1044
1066
|
);
|
|
1045
1067
|
if (current) return { kind: "current", row: toRow(current) };
|
|
1046
1068
|
const [previous] = await this.store.query(
|
|
1047
|
-
`SELECT id, user_id, sid, token, (previous_valid_until > now()) AS in_grace
|
|
1069
|
+
`SELECT id, user_id, sid, token, created_at, (previous_valid_until > now()) AS in_grace
|
|
1048
1070
|
FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
|
|
1049
1071
|
[hash]
|
|
1050
1072
|
);
|
|
@@ -1159,7 +1181,7 @@ var SessionModel = class {
|
|
|
1159
1181
|
}
|
|
1160
1182
|
};
|
|
1161
1183
|
function toRow(r) {
|
|
1162
|
-
return { id: r.id, userId: r.user_id, sid: r.sid, stored: r.token };
|
|
1184
|
+
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() };
|
|
1163
1185
|
}
|
|
1164
1186
|
|
|
1165
1187
|
// src/models/backup-code.model.ts
|
|
@@ -2003,6 +2025,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2003
2025
|
if (!payload || payload.type !== "refresh") {
|
|
2004
2026
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
2005
2027
|
}
|
|
2028
|
+
const resolvedRefresh = { ...config, ...config.resolve?.(ctx) };
|
|
2029
|
+
const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
|
|
2006
2030
|
let issued = null;
|
|
2007
2031
|
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
2008
2032
|
const found = await sessions.match(token);
|
|
@@ -2016,14 +2040,20 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2016
2040
|
);
|
|
2017
2041
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2018
2042
|
}
|
|
2043
|
+
if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
|
|
2044
|
+
await sessions.revokeById(found.row.id);
|
|
2045
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
|
|
2046
|
+
}
|
|
2019
2047
|
const user = await users.findById(found.row.userId);
|
|
2020
2048
|
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
2021
2049
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2022
2050
|
}
|
|
2023
|
-
const pair = issueTokenPair(user.id,
|
|
2051
|
+
const pair = issueTokenPair(user.id, resolvedRefresh, {
|
|
2024
2052
|
loginMethod: payload.loginMethod ?? "email",
|
|
2025
2053
|
phoneVerified: payload.phoneVerified ?? false,
|
|
2026
|
-
...found.row.sid ? { sid: found.row.sid } : {}
|
|
2054
|
+
...found.row.sid ? { sid: found.row.sid } : {},
|
|
2055
|
+
// A refresh is not a sign-in: keep when the user last signed in.
|
|
2056
|
+
...typeof payload.auth_time === "number" ? { authTime: payload.auth_time } : {}
|
|
2027
2057
|
});
|
|
2028
2058
|
const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
|
|
2029
2059
|
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
@@ -3655,7 +3685,7 @@ var AuthModule = class {
|
|
|
3655
3685
|
name = "@fonderie/auth";
|
|
3656
3686
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3657
3687
|
// say what is actually deployed rather than 'not reported'.
|
|
3658
|
-
version = "7.
|
|
3688
|
+
version = "7.17.1";
|
|
3659
3689
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3660
3690
|
describeAdmin() {
|
|
3661
3691
|
return { routes: describeAuthAdminRoutes(this.store) };
|