@fonderie/auth 7.14.4 → 7.16.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/README.md +20 -0
- package/brain/outcomes.md +3 -0
- package/brain/signatures.md +1 -0
- package/dist/index.cjs +149 -50
- 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 +136 -37
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +80 -10
- 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 +81 -11
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/020_session_rotation.sql +22 -0
- package/dist/{session-BBF5t-3Y.d.cts → session-auNb2L8m.d.cts} +1 -0
- package/dist/{session-BBF5t-3Y.d.ts → session-auNb2L8m.d.ts} +1 -0
- package/env.json +10 -0
- package/package.json +1 -1
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-
|
|
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-
|
|
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';
|
|
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-
|
|
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-
|
|
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';
|
|
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
|
@@ -280,12 +280,21 @@ var EVENT_KEYS = {
|
|
|
280
280
|
// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
|
|
281
281
|
userPurged: "fonderie.user.purged",
|
|
282
282
|
emailVerified: "fonderie.user.email_verified",
|
|
283
|
-
passwordChanged: "fonderie.user.password_changed"
|
|
283
|
+
passwordChanged: "fonderie.user.password_changed",
|
|
284
|
+
// A session the server revoked — today: a refresh token reused after its
|
|
285
|
+
// grace (theft signal). { userId, sessionId, reason }.
|
|
286
|
+
sessionRevoked: "fonderie.session.revoked"
|
|
284
287
|
};
|
|
285
288
|
|
|
286
289
|
// src/services/jwt.ts
|
|
287
|
-
import { randomUUID } from "crypto";
|
|
290
|
+
import { createHash, randomUUID } from "crypto";
|
|
288
291
|
import jwt from "jsonwebtoken";
|
|
292
|
+
function keyIdOf(secret) {
|
|
293
|
+
return createHash("sha256").update(secret).digest("hex").slice(0, 16);
|
|
294
|
+
}
|
|
295
|
+
function keyRing(config) {
|
|
296
|
+
return [config.jwtSecret, ...config.jwtPreviousSecrets ?? []].filter((s) => typeof s === "string" && s.length > 0);
|
|
297
|
+
}
|
|
289
298
|
function issueMfaPendingToken(userId, config, loginMethod) {
|
|
290
299
|
return jwt.sign(
|
|
291
300
|
{
|
|
@@ -296,7 +305,7 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
296
305
|
mfaPending: true
|
|
297
306
|
},
|
|
298
307
|
config.jwtSecret,
|
|
299
|
-
{ expiresIn: "5m" }
|
|
308
|
+
{ expiresIn: "5m", keyid: keyIdOf(config.jwtSecret) }
|
|
300
309
|
);
|
|
301
310
|
}
|
|
302
311
|
function issueTokenPair(userId, config, options) {
|
|
@@ -304,16 +313,19 @@ function issueTokenPair(userId, config, options) {
|
|
|
304
313
|
const accessDuration = config.accessTokenDuration ?? "24h";
|
|
305
314
|
const loginMethod = options.loginMethod;
|
|
306
315
|
const phoneVerified = options.phoneVerified ?? false;
|
|
307
|
-
const sid = randomUUID();
|
|
316
|
+
const sid = options.sid ?? randomUUID();
|
|
308
317
|
const accessToken = jwt.sign(
|
|
309
318
|
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
310
319
|
config.jwtSecret,
|
|
311
|
-
|
|
320
|
+
// jwtid: every token is unique. With the session id kept across rotations,
|
|
321
|
+
// two tokens issued in the same second would otherwise be byte-identical —
|
|
322
|
+
// and a rotated-away token indistinguishable from the live one.
|
|
323
|
+
{ expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret), jwtid: randomUUID() }
|
|
312
324
|
);
|
|
313
325
|
const refreshToken = jwt.sign(
|
|
314
326
|
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
315
327
|
config.jwtSecret,
|
|
316
|
-
{ expiresIn: duration }
|
|
328
|
+
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret), jwtid: randomUUID() }
|
|
317
329
|
);
|
|
318
330
|
return { accessToken, refreshToken, sid };
|
|
319
331
|
}
|
|
@@ -322,11 +334,16 @@ function refreshTokenExpiry(token) {
|
|
|
322
334
|
return decoded?.exp ? new Date(decoded.exp * 1e3) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
323
335
|
}
|
|
324
336
|
function verifyToken(token, config) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
const ring = keyRing(config);
|
|
338
|
+
const kid = jwt.decode(token, { complete: true })?.header?.kid;
|
|
339
|
+
const candidates = typeof kid === "string" ? ring.filter((s) => keyIdOf(s) === kid) : ring;
|
|
340
|
+
for (const secret of candidates) {
|
|
341
|
+
try {
|
|
342
|
+
return jwt.verify(token, secret);
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
329
345
|
}
|
|
346
|
+
return null;
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
// src/services/mfa.ts
|
|
@@ -852,6 +869,9 @@ var UserModel = class {
|
|
|
852
869
|
};
|
|
853
870
|
var MAX_LIST_LIMIT = 200;
|
|
854
871
|
|
|
872
|
+
// src/models/session.model.ts
|
|
873
|
+
import { createHash as createHash2 } from "crypto";
|
|
874
|
+
|
|
855
875
|
// src/services/request-location.ts
|
|
856
876
|
var LOCATION_TIMEOUT_MS = 500;
|
|
857
877
|
var TEXT_MAX = 128;
|
|
@@ -934,6 +954,10 @@ async function resolveOnce(resolver, req, timeoutMs) {
|
|
|
934
954
|
}
|
|
935
955
|
|
|
936
956
|
// src/models/session.model.ts
|
|
957
|
+
function hashRefreshToken(token) {
|
|
958
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
959
|
+
}
|
|
960
|
+
var REFRESH_GRACE_MS = 3e4;
|
|
937
961
|
var SessionModel = class {
|
|
938
962
|
// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.
|
|
939
963
|
constructor(store, locate) {
|
|
@@ -945,12 +969,12 @@ var SessionModel = class {
|
|
|
945
969
|
async create(userId, token, expiresAt, sid, meta) {
|
|
946
970
|
const location = this.locate && meta?.headers ? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers }) : null;
|
|
947
971
|
await this.store.query(
|
|
948
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)
|
|
949
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
972
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location, last_used_at)
|
|
973
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
950
974
|
ON CONFLICT (token) DO NOTHING`,
|
|
951
975
|
[
|
|
952
976
|
userId,
|
|
953
|
-
token,
|
|
977
|
+
hashRefreshToken(token),
|
|
954
978
|
expiresAt,
|
|
955
979
|
sid ?? null,
|
|
956
980
|
meta?.userAgent ?? null,
|
|
@@ -960,7 +984,56 @@ var SessionModel = class {
|
|
|
960
984
|
);
|
|
961
985
|
}
|
|
962
986
|
async delete(token) {
|
|
963
|
-
await this.store.query(`DELETE FROM fonderie_sessions WHERE token
|
|
987
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE token IN ($1, $2)`, [hashRefreshToken(token), token]);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Where a presented refresh token stands: the session's current token, its
|
|
991
|
+
* previous one within the grace (a retry or race), its previous one AFTER
|
|
992
|
+
* the grace (reuse — a theft signal), or nothing known.
|
|
993
|
+
*/
|
|
994
|
+
async match(token) {
|
|
995
|
+
const hash = hashRefreshToken(token);
|
|
996
|
+
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`,
|
|
998
|
+
[hash, token]
|
|
999
|
+
);
|
|
1000
|
+
if (current) return { kind: "current", row: toRow(current) };
|
|
1001
|
+
const [previous] = await this.store.query(
|
|
1002
|
+
`SELECT id, user_id, sid, token, (previous_valid_until > now()) AS in_grace
|
|
1003
|
+
FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
|
|
1004
|
+
[hash]
|
|
1005
|
+
);
|
|
1006
|
+
if (!previous) return { kind: "unknown" };
|
|
1007
|
+
return { kind: previous.in_grace ? "grace" : "reused", row: toRow(previous) };
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Rotate a session in place to a new refresh token: same row, same device.
|
|
1011
|
+
* From the CURRENT token it is optimistic — only if the stored token is
|
|
1012
|
+
* still the one matched; a concurrent rotation makes it return false and the
|
|
1013
|
+
* caller re-matches (and now finds the token as the previous one). The old
|
|
1014
|
+
* token becomes the previous, valid for the grace. From the PREVIOUS token
|
|
1015
|
+
* (within the grace) it is authorised by that hash and deadline alone, so
|
|
1016
|
+
* racing retries never starve; the previous and its deadline are kept.
|
|
1017
|
+
*/
|
|
1018
|
+
async rotate(row, presented, next, from) {
|
|
1019
|
+
const rows = await this.store.query(
|
|
1020
|
+
`UPDATE fonderie_sessions
|
|
1021
|
+
SET previous_token_hash = CASE WHEN $5 THEN $6 ELSE previous_token_hash END,
|
|
1022
|
+
previous_valid_until = CASE WHEN $5 THEN now() + ($7 || ' milliseconds')::interval ELSE previous_valid_until END,
|
|
1023
|
+
token = $3,
|
|
1024
|
+
expires_at = $4,
|
|
1025
|
+
last_used_at = now()
|
|
1026
|
+
WHERE id = $1
|
|
1027
|
+
AND CASE WHEN $5 THEN token = $2
|
|
1028
|
+
ELSE previous_token_hash = $6 AND previous_valid_until > now() END
|
|
1029
|
+
RETURNING id`,
|
|
1030
|
+
[row.id, row.stored, hashRefreshToken(next.token), next.expiresAt, from === "current", hashRefreshToken(presented), String(REFRESH_GRACE_MS)]
|
|
1031
|
+
);
|
|
1032
|
+
return rows.length === 1;
|
|
1033
|
+
}
|
|
1034
|
+
/** Revoke one session by its row id (any user) — reuse detection, admin. */
|
|
1035
|
+
async revokeById(id) {
|
|
1036
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE id = $1`, [id]);
|
|
964
1037
|
}
|
|
965
1038
|
// Delete the session an access token is bound to (by its sid claim). Lets
|
|
966
1039
|
// logout revoke the current session without the client resending the refresh
|
|
@@ -1026,8 +1099,8 @@ var SessionModel = class {
|
|
|
1026
1099
|
}
|
|
1027
1100
|
async exists(token) {
|
|
1028
1101
|
const rows = await this.store.query(
|
|
1029
|
-
`SELECT id FROM fonderie_sessions WHERE token
|
|
1030
|
-
[token]
|
|
1102
|
+
`SELECT id FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now()`,
|
|
1103
|
+
[hashRefreshToken(token), token]
|
|
1031
1104
|
);
|
|
1032
1105
|
return rows.length > 0;
|
|
1033
1106
|
}
|
|
@@ -1040,6 +1113,9 @@ var SessionModel = class {
|
|
|
1040
1113
|
return rows.length > 0;
|
|
1041
1114
|
}
|
|
1042
1115
|
};
|
|
1116
|
+
function toRow(r) {
|
|
1117
|
+
return { id: r.id, userId: r.user_id, sid: r.sid, stored: r.token };
|
|
1118
|
+
}
|
|
1043
1119
|
|
|
1044
1120
|
// src/models/backup-code.model.ts
|
|
1045
1121
|
var BackupCodeModel = class {
|
|
@@ -1421,8 +1497,8 @@ function normalizeEmailSafe(email2) {
|
|
|
1421
1497
|
}
|
|
1422
1498
|
|
|
1423
1499
|
// src/models/password-reset.model.ts
|
|
1424
|
-
import { createHash } from "crypto";
|
|
1425
|
-
var hashSecret = (value) =>
|
|
1500
|
+
import { createHash as createHash3 } from "crypto";
|
|
1501
|
+
var hashSecret = (value) => createHash3("sha256").update(value).digest("hex");
|
|
1426
1502
|
var PasswordResetModel = class {
|
|
1427
1503
|
constructor(store) {
|
|
1428
1504
|
this.store = store;
|
|
@@ -1882,24 +1958,35 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1882
1958
|
if (!payload || payload.type !== "refresh") {
|
|
1883
1959
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1884
1960
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
"TOKEN_REFRESH_FAILED",
|
|
1890
|
-
|
|
1891
|
-
)
|
|
1961
|
+
let issued = null;
|
|
1962
|
+
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
1963
|
+
const found = await sessions.match(token);
|
|
1964
|
+
if (found.kind === "unknown") {
|
|
1965
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1966
|
+
}
|
|
1967
|
+
if (found.kind === "reused") {
|
|
1968
|
+
await sessions.revokeById(found.row.id);
|
|
1969
|
+
await background3(
|
|
1970
|
+
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sessionId: found.row.id, reason: "refresh-reuse" })
|
|
1971
|
+
);
|
|
1972
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1973
|
+
}
|
|
1974
|
+
const user = await users.findById(found.row.userId);
|
|
1975
|
+
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
1976
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1977
|
+
}
|
|
1978
|
+
const pair = issueTokenPair(user.id, config, {
|
|
1979
|
+
loginMethod: payload.loginMethod ?? "email",
|
|
1980
|
+
phoneVerified: payload.phoneVerified ?? false,
|
|
1981
|
+
...found.row.sid ? { sid: found.row.sid } : {}
|
|
1982
|
+
});
|
|
1983
|
+
const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
|
|
1984
|
+
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
1892
1985
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1986
|
+
if (!issued) {
|
|
1987
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1896
1988
|
}
|
|
1897
|
-
|
|
1898
|
-
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1899
|
-
loginMethod: payload.loginMethod ?? "email",
|
|
1900
|
-
phoneVerified: payload.phoneVerified ?? false
|
|
1901
|
-
});
|
|
1902
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
1989
|
+
const { accessToken, refreshToken } = issued;
|
|
1903
1990
|
return Response.json(
|
|
1904
1991
|
{
|
|
1905
1992
|
reason: "TOKENS_REFRESHED",
|
|
@@ -2589,7 +2676,7 @@ function userController(store, config, bus) {
|
|
|
2589
2676
|
}
|
|
2590
2677
|
|
|
2591
2678
|
// src/controllers/oauth.controller.ts
|
|
2592
|
-
import { randomBytes as randomBytes4, createPublicKey, createHash as
|
|
2679
|
+
import { randomBytes as randomBytes4, createPublicKey, createHash as createHash4 } from "crypto";
|
|
2593
2680
|
import jwt2 from "jsonwebtoken";
|
|
2594
2681
|
import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
|
|
2595
2682
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
|
|
@@ -3066,7 +3153,7 @@ function oauthController(store, config, bus) {
|
|
|
3066
3153
|
});
|
|
3067
3154
|
return setApiResponse5(HTTP5.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3068
3155
|
}
|
|
3069
|
-
const tokenHash =
|
|
3156
|
+
const tokenHash = createHash4("sha256").update(identityToken).digest("hex");
|
|
3070
3157
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3071
3158
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
3072
3159
|
if (!firstUse) {
|
|
@@ -3226,6 +3313,18 @@ function collectAuthConfigProblems(config) {
|
|
|
3226
3313
|
reason: "JWT_SECRET_PLACEHOLDER"
|
|
3227
3314
|
});
|
|
3228
3315
|
}
|
|
3316
|
+
(config.jwtPreviousSecrets ?? []).forEach((previous, index) => {
|
|
3317
|
+
if (secretStrengthProblem(previous ?? "")) {
|
|
3318
|
+
problems.push({
|
|
3319
|
+
module: MODULE,
|
|
3320
|
+
severity: "error",
|
|
3321
|
+
message: `jwtPreviousSecrets[${index}] is too short or a placeholder \u2014 it still verifies tokens`,
|
|
3322
|
+
domain: "auth",
|
|
3323
|
+
reason: "JWT_PREVIOUS_SECRET_WEAK",
|
|
3324
|
+
metadata: { index }
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
});
|
|
3229
3328
|
if (config.secureCookies === false) {
|
|
3230
3329
|
problems.push({
|
|
3231
3330
|
module: MODULE,
|
|
@@ -3511,7 +3610,7 @@ var AuthModule = class {
|
|
|
3511
3610
|
name = "@fonderie/auth";
|
|
3512
3611
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3513
3612
|
// say what is actually deployed rather than 'not reported'.
|
|
3514
|
-
version = "7.
|
|
3613
|
+
version = "7.16.0";
|
|
3515
3614
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3516
3615
|
describeAdmin() {
|
|
3517
3616
|
return { routes: describeAuthAdminRoutes(this.store) };
|