@fonderie/auth 7.15.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/brain/outcomes.md +3 -0
- package/brain/signatures.md +1 -0
- package/dist/index.cjs +144 -49
- 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 +130 -35
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +70 -6
- 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 +70 -6
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/020_session_rotation.sql +22 -0
- 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/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-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-
|
|
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-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"
|
|
@@ -280,7 +283,10 @@ var EVENT_KEYS = {
|
|
|
280
283
|
// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
|
|
281
284
|
userPurged: "fonderie.user.purged",
|
|
282
285
|
emailVerified: "fonderie.user.email_verified",
|
|
283
|
-
passwordChanged: "fonderie.user.password_changed"
|
|
286
|
+
passwordChanged: "fonderie.user.password_changed",
|
|
287
|
+
// A session the server revoked — today: a refresh token reused after its
|
|
288
|
+
// grace (theft signal). { userId, sessionId, reason }.
|
|
289
|
+
sessionRevoked: "fonderie.session.revoked"
|
|
284
290
|
};
|
|
285
291
|
|
|
286
292
|
// src/services/jwt.ts
|
|
@@ -306,20 +312,24 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
306
312
|
);
|
|
307
313
|
}
|
|
308
314
|
function issueTokenPair(userId, config, options) {
|
|
309
|
-
const duration = config.sessionDuration ??
|
|
310
|
-
const accessDuration = config.accessTokenDuration ??
|
|
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);
|
|
311
318
|
const loginMethod = options.loginMethod;
|
|
312
319
|
const phoneVerified = options.phoneVerified ?? false;
|
|
313
|
-
const sid = randomUUID();
|
|
320
|
+
const sid = options.sid ?? randomUUID();
|
|
314
321
|
const accessToken = jwt.sign(
|
|
315
|
-
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
322
|
+
{ sub: userId, type: "access", loginMethod, phoneVerified, sid, auth_time: authTime },
|
|
316
323
|
config.jwtSecret,
|
|
317
|
-
|
|
324
|
+
// jwtid: every token is unique. With the session id kept across rotations,
|
|
325
|
+
// two tokens issued in the same second would otherwise be byte-identical —
|
|
326
|
+
// and a rotated-away token indistinguishable from the live one.
|
|
327
|
+
{ expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret), jwtid: randomUUID() }
|
|
318
328
|
);
|
|
319
329
|
const refreshToken = jwt.sign(
|
|
320
|
-
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
330
|
+
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid, auth_time: authTime },
|
|
321
331
|
config.jwtSecret,
|
|
322
|
-
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret) }
|
|
332
|
+
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret), jwtid: randomUUID() }
|
|
323
333
|
);
|
|
324
334
|
return { accessToken, refreshToken, sid };
|
|
325
335
|
}
|
|
@@ -339,6 +349,13 @@ function verifyToken(token, config) {
|
|
|
339
349
|
}
|
|
340
350
|
return null;
|
|
341
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
|
+
}
|
|
342
359
|
|
|
343
360
|
// src/services/mfa.ts
|
|
344
361
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -863,6 +880,9 @@ var UserModel = class {
|
|
|
863
880
|
};
|
|
864
881
|
var MAX_LIST_LIMIT = 200;
|
|
865
882
|
|
|
883
|
+
// src/models/session.model.ts
|
|
884
|
+
import { createHash as createHash2 } from "crypto";
|
|
885
|
+
|
|
866
886
|
// src/services/request-location.ts
|
|
867
887
|
var LOCATION_TIMEOUT_MS = 500;
|
|
868
888
|
var TEXT_MAX = 128;
|
|
@@ -945,6 +965,10 @@ async function resolveOnce(resolver, req, timeoutMs) {
|
|
|
945
965
|
}
|
|
946
966
|
|
|
947
967
|
// src/models/session.model.ts
|
|
968
|
+
function hashRefreshToken(token) {
|
|
969
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
970
|
+
}
|
|
971
|
+
var REFRESH_GRACE_MS = 3e4;
|
|
948
972
|
var SessionModel = class {
|
|
949
973
|
// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.
|
|
950
974
|
constructor(store, locate) {
|
|
@@ -956,12 +980,12 @@ var SessionModel = class {
|
|
|
956
980
|
async create(userId, token, expiresAt, sid, meta) {
|
|
957
981
|
const location = this.locate && meta?.headers ? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers }) : null;
|
|
958
982
|
await this.store.query(
|
|
959
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)
|
|
960
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
983
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location, last_used_at)
|
|
984
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
961
985
|
ON CONFLICT (token) DO NOTHING`,
|
|
962
986
|
[
|
|
963
987
|
userId,
|
|
964
|
-
token,
|
|
988
|
+
hashRefreshToken(token),
|
|
965
989
|
expiresAt,
|
|
966
990
|
sid ?? null,
|
|
967
991
|
meta?.userAgent ?? null,
|
|
@@ -971,7 +995,56 @@ var SessionModel = class {
|
|
|
971
995
|
);
|
|
972
996
|
}
|
|
973
997
|
async delete(token) {
|
|
974
|
-
await this.store.query(`DELETE FROM fonderie_sessions WHERE token
|
|
998
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE token IN ($1, $2)`, [hashRefreshToken(token), token]);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Where a presented refresh token stands: the session's current token, its
|
|
1002
|
+
* previous one within the grace (a retry or race), its previous one AFTER
|
|
1003
|
+
* the grace (reuse — a theft signal), or nothing known.
|
|
1004
|
+
*/
|
|
1005
|
+
async match(token) {
|
|
1006
|
+
const hash = hashRefreshToken(token);
|
|
1007
|
+
const [current] = await this.store.query(
|
|
1008
|
+
`SELECT id, user_id, sid, token, created_at FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
|
|
1009
|
+
[hash, token]
|
|
1010
|
+
);
|
|
1011
|
+
if (current) return { kind: "current", row: toRow(current) };
|
|
1012
|
+
const [previous] = await this.store.query(
|
|
1013
|
+
`SELECT id, user_id, sid, token, created_at, (previous_valid_until > now()) AS in_grace
|
|
1014
|
+
FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
|
|
1015
|
+
[hash]
|
|
1016
|
+
);
|
|
1017
|
+
if (!previous) return { kind: "unknown" };
|
|
1018
|
+
return { kind: previous.in_grace ? "grace" : "reused", row: toRow(previous) };
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Rotate a session in place to a new refresh token: same row, same device.
|
|
1022
|
+
* From the CURRENT token it is optimistic — only if the stored token is
|
|
1023
|
+
* still the one matched; a concurrent rotation makes it return false and the
|
|
1024
|
+
* caller re-matches (and now finds the token as the previous one). The old
|
|
1025
|
+
* token becomes the previous, valid for the grace. From the PREVIOUS token
|
|
1026
|
+
* (within the grace) it is authorised by that hash and deadline alone, so
|
|
1027
|
+
* racing retries never starve; the previous and its deadline are kept.
|
|
1028
|
+
*/
|
|
1029
|
+
async rotate(row, presented, next, from) {
|
|
1030
|
+
const rows = await this.store.query(
|
|
1031
|
+
`UPDATE fonderie_sessions
|
|
1032
|
+
SET previous_token_hash = CASE WHEN $5 THEN $6 ELSE previous_token_hash END,
|
|
1033
|
+
previous_valid_until = CASE WHEN $5 THEN now() + ($7 || ' milliseconds')::interval ELSE previous_valid_until END,
|
|
1034
|
+
token = $3,
|
|
1035
|
+
expires_at = $4,
|
|
1036
|
+
last_used_at = now()
|
|
1037
|
+
WHERE id = $1
|
|
1038
|
+
AND CASE WHEN $5 THEN token = $2
|
|
1039
|
+
ELSE previous_token_hash = $6 AND previous_valid_until > now() END
|
|
1040
|
+
RETURNING id`,
|
|
1041
|
+
[row.id, row.stored, hashRefreshToken(next.token), next.expiresAt, from === "current", hashRefreshToken(presented), String(REFRESH_GRACE_MS)]
|
|
1042
|
+
);
|
|
1043
|
+
return rows.length === 1;
|
|
1044
|
+
}
|
|
1045
|
+
/** Revoke one session by its row id (any user) — reuse detection, admin. */
|
|
1046
|
+
async revokeById(id) {
|
|
1047
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE id = $1`, [id]);
|
|
975
1048
|
}
|
|
976
1049
|
// Delete the session an access token is bound to (by its sid claim). Lets
|
|
977
1050
|
// logout revoke the current session without the client resending the refresh
|
|
@@ -1037,8 +1110,8 @@ var SessionModel = class {
|
|
|
1037
1110
|
}
|
|
1038
1111
|
async exists(token) {
|
|
1039
1112
|
const rows = await this.store.query(
|
|
1040
|
-
`SELECT id FROM fonderie_sessions WHERE token
|
|
1041
|
-
[token]
|
|
1113
|
+
`SELECT id FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now()`,
|
|
1114
|
+
[hashRefreshToken(token), token]
|
|
1042
1115
|
);
|
|
1043
1116
|
return rows.length > 0;
|
|
1044
1117
|
}
|
|
@@ -1051,6 +1124,9 @@ var SessionModel = class {
|
|
|
1051
1124
|
return rows.length > 0;
|
|
1052
1125
|
}
|
|
1053
1126
|
};
|
|
1127
|
+
function toRow(r) {
|
|
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() };
|
|
1129
|
+
}
|
|
1054
1130
|
|
|
1055
1131
|
// src/models/backup-code.model.ts
|
|
1056
1132
|
var BackupCodeModel = class {
|
|
@@ -1432,8 +1508,8 @@ function normalizeEmailSafe(email2) {
|
|
|
1432
1508
|
}
|
|
1433
1509
|
|
|
1434
1510
|
// src/models/password-reset.model.ts
|
|
1435
|
-
import { createHash as
|
|
1436
|
-
var hashSecret = (value) =>
|
|
1511
|
+
import { createHash as createHash3 } from "crypto";
|
|
1512
|
+
var hashSecret = (value) => createHash3("sha256").update(value).digest("hex");
|
|
1437
1513
|
var PasswordResetModel = class {
|
|
1438
1514
|
constructor(store) {
|
|
1439
1515
|
this.store = store;
|
|
@@ -1893,24 +1969,43 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
1893
1969
|
if (!payload || payload.type !== "refresh") {
|
|
1894
1970
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1895
1971
|
}
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1972
|
+
const resolvedRefresh = { ...config, ...config.resolve?.(ctx) };
|
|
1973
|
+
const maxAgeMs = durationMs(resolvedRefresh.sessionMaxAge);
|
|
1974
|
+
let issued = null;
|
|
1975
|
+
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
1976
|
+
const found = await sessions.match(token);
|
|
1977
|
+
if (found.kind === "unknown") {
|
|
1978
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1979
|
+
}
|
|
1980
|
+
if (found.kind === "reused") {
|
|
1981
|
+
await sessions.revokeById(found.row.id);
|
|
1982
|
+
await background3(
|
|
1983
|
+
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sessionId: found.row.id, reason: "refresh-reuse" })
|
|
1984
|
+
);
|
|
1985
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
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
|
+
}
|
|
1991
|
+
const user = await users.findById(found.row.userId);
|
|
1992
|
+
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
1993
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1994
|
+
}
|
|
1995
|
+
const pair = issueTokenPair(user.id, resolvedRefresh, {
|
|
1996
|
+
loginMethod: payload.loginMethod ?? "email",
|
|
1997
|
+
phoneVerified: payload.phoneVerified ?? false,
|
|
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 } : {}
|
|
2001
|
+
});
|
|
2002
|
+
const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
|
|
2003
|
+
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
1903
2004
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2005
|
+
if (!issued) {
|
|
2006
|
+
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1907
2007
|
}
|
|
1908
|
-
|
|
1909
|
-
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1910
|
-
loginMethod: payload.loginMethod ?? "email",
|
|
1911
|
-
phoneVerified: payload.phoneVerified ?? false
|
|
1912
|
-
});
|
|
1913
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
2008
|
+
const { accessToken, refreshToken } = issued;
|
|
1914
2009
|
return Response.json(
|
|
1915
2010
|
{
|
|
1916
2011
|
reason: "TOKENS_REFRESHED",
|
|
@@ -2600,7 +2695,7 @@ function userController(store, config, bus) {
|
|
|
2600
2695
|
}
|
|
2601
2696
|
|
|
2602
2697
|
// src/controllers/oauth.controller.ts
|
|
2603
|
-
import { randomBytes as randomBytes4, createPublicKey, createHash as
|
|
2698
|
+
import { randomBytes as randomBytes4, createPublicKey, createHash as createHash4 } from "crypto";
|
|
2604
2699
|
import jwt2 from "jsonwebtoken";
|
|
2605
2700
|
import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
|
|
2606
2701
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
|
|
@@ -3077,7 +3172,7 @@ function oauthController(store, config, bus) {
|
|
|
3077
3172
|
});
|
|
3078
3173
|
return setApiResponse5(HTTP5.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3079
3174
|
}
|
|
3080
|
-
const tokenHash =
|
|
3175
|
+
const tokenHash = createHash4("sha256").update(identityToken).digest("hex");
|
|
3081
3176
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3082
3177
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
3083
3178
|
if (!firstUse) {
|
|
@@ -3534,7 +3629,7 @@ var AuthModule = class {
|
|
|
3534
3629
|
name = "@fonderie/auth";
|
|
3535
3630
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3536
3631
|
// say what is actually deployed rather than 'not reported'.
|
|
3537
|
-
version = "7.
|
|
3632
|
+
version = "7.17.0";
|
|
3538
3633
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3539
3634
|
describeAdmin() {
|
|
3540
3635
|
return { routes: describeAuthAdminRoutes(this.store) };
|