@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/brain/outcomes.md
CHANGED
|
@@ -95,6 +95,9 @@ expires_at TIMESTAMPTZ NOT NULL
|
|
|
95
95
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
96
96
|
sid UUID
|
|
97
97
|
location JSONB
|
|
98
|
+
previous_token_hash TEXT
|
|
99
|
+
previous_valid_until TIMESTAMPTZ
|
|
100
|
+
last_used_at TIMESTAMPTZ
|
|
98
101
|
-- INDEX idx_fonderie_sessions_expires_at (expires_at)
|
|
99
102
|
-- INDEX idx_fonderie_sessions_sid (sid)
|
|
100
103
|
```
|
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"
|
|
@@ -325,7 +328,10 @@ var EVENT_KEYS = {
|
|
|
325
328
|
// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
|
|
326
329
|
userPurged: "fonderie.user.purged",
|
|
327
330
|
emailVerified: "fonderie.user.email_verified",
|
|
328
|
-
passwordChanged: "fonderie.user.password_changed"
|
|
331
|
+
passwordChanged: "fonderie.user.password_changed",
|
|
332
|
+
// A session the server revoked — today: a refresh token reused after its
|
|
333
|
+
// grace (theft signal). { userId, sessionId, reason }.
|
|
334
|
+
sessionRevoked: "fonderie.session.revoked"
|
|
329
335
|
};
|
|
330
336
|
|
|
331
337
|
// src/services/jwt.ts
|
|
@@ -351,20 +357,24 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
351
357
|
);
|
|
352
358
|
}
|
|
353
359
|
function issueTokenPair(userId, config, options) {
|
|
354
|
-
const duration = config.sessionDuration ??
|
|
355
|
-
const accessDuration = config.accessTokenDuration ??
|
|
360
|
+
const duration = config.sessionDuration ?? DEFAULT_SESSION_DURATION;
|
|
361
|
+
const accessDuration = config.accessTokenDuration ?? DEFAULT_ACCESS_TOKEN_DURATION;
|
|
362
|
+
const authTime = options.authTime ?? Math.floor(Date.now() / 1e3);
|
|
356
363
|
const loginMethod = options.loginMethod;
|
|
357
364
|
const phoneVerified = options.phoneVerified ?? false;
|
|
358
|
-
const sid = (0, import_node_crypto.randomUUID)();
|
|
365
|
+
const sid = options.sid ?? (0, import_node_crypto.randomUUID)();
|
|
359
366
|
const accessToken = import_jsonwebtoken.default.sign(
|
|
360
|
-
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
367
|
+
{ sub: userId, type: "access", loginMethod, phoneVerified, sid, auth_time: authTime },
|
|
361
368
|
config.jwtSecret,
|
|
362
|
-
|
|
369
|
+
// jwtid: every token is unique. With the session id kept across rotations,
|
|
370
|
+
// two tokens issued in the same second would otherwise be byte-identical —
|
|
371
|
+
// and a rotated-away token indistinguishable from the live one.
|
|
372
|
+
{ expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret), jwtid: (0, import_node_crypto.randomUUID)() }
|
|
363
373
|
);
|
|
364
374
|
const refreshToken = import_jsonwebtoken.default.sign(
|
|
365
|
-
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
375
|
+
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid, auth_time: authTime },
|
|
366
376
|
config.jwtSecret,
|
|
367
|
-
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret) }
|
|
377
|
+
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret), jwtid: (0, import_node_crypto.randomUUID)() }
|
|
368
378
|
);
|
|
369
379
|
return { accessToken, refreshToken, sid };
|
|
370
380
|
}
|
|
@@ -384,6 +394,13 @@ function verifyToken(token, config) {
|
|
|
384
394
|
}
|
|
385
395
|
return null;
|
|
386
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
|
+
}
|
|
387
404
|
|
|
388
405
|
// src/services/mfa.ts
|
|
389
406
|
var import_node_crypto2 = require("crypto");
|
|
@@ -908,6 +925,9 @@ var UserModel = class {
|
|
|
908
925
|
};
|
|
909
926
|
var MAX_LIST_LIMIT = 200;
|
|
910
927
|
|
|
928
|
+
// src/models/session.model.ts
|
|
929
|
+
var import_node_crypto4 = require("crypto");
|
|
930
|
+
|
|
911
931
|
// src/services/request-location.ts
|
|
912
932
|
var LOCATION_TIMEOUT_MS = 500;
|
|
913
933
|
var TEXT_MAX = 128;
|
|
@@ -990,6 +1010,10 @@ async function resolveOnce(resolver, req, timeoutMs) {
|
|
|
990
1010
|
}
|
|
991
1011
|
|
|
992
1012
|
// src/models/session.model.ts
|
|
1013
|
+
function hashRefreshToken(token) {
|
|
1014
|
+
return (0, import_node_crypto4.createHash)("sha256").update(token).digest("hex");
|
|
1015
|
+
}
|
|
1016
|
+
var REFRESH_GRACE_MS = 3e4;
|
|
993
1017
|
var SessionModel = class {
|
|
994
1018
|
// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.
|
|
995
1019
|
constructor(store, locate) {
|
|
@@ -1001,12 +1025,12 @@ var SessionModel = class {
|
|
|
1001
1025
|
async create(userId, token, expiresAt, sid, meta) {
|
|
1002
1026
|
const location = this.locate && meta?.headers ? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers }) : null;
|
|
1003
1027
|
await this.store.query(
|
|
1004
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)
|
|
1005
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1028
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location, last_used_at)
|
|
1029
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1006
1030
|
ON CONFLICT (token) DO NOTHING`,
|
|
1007
1031
|
[
|
|
1008
1032
|
userId,
|
|
1009
|
-
token,
|
|
1033
|
+
hashRefreshToken(token),
|
|
1010
1034
|
expiresAt,
|
|
1011
1035
|
sid ?? null,
|
|
1012
1036
|
meta?.userAgent ?? null,
|
|
@@ -1016,7 +1040,56 @@ var SessionModel = class {
|
|
|
1016
1040
|
);
|
|
1017
1041
|
}
|
|
1018
1042
|
async delete(token) {
|
|
1019
|
-
await this.store.query(`DELETE FROM fonderie_sessions WHERE token
|
|
1043
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE token IN ($1, $2)`, [hashRefreshToken(token), token]);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Where a presented refresh token stands: the session's current token, its
|
|
1047
|
+
* previous one within the grace (a retry or race), its previous one AFTER
|
|
1048
|
+
* the grace (reuse — a theft signal), or nothing known.
|
|
1049
|
+
*/
|
|
1050
|
+
async match(token) {
|
|
1051
|
+
const hash = hashRefreshToken(token);
|
|
1052
|
+
const [current] = await this.store.query(
|
|
1053
|
+
`SELECT id, user_id, sid, token, created_at FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
|
|
1054
|
+
[hash, token]
|
|
1055
|
+
);
|
|
1056
|
+
if (current) return { kind: "current", row: toRow(current) };
|
|
1057
|
+
const [previous] = await this.store.query(
|
|
1058
|
+
`SELECT id, user_id, sid, token, created_at, (previous_valid_until > now()) AS in_grace
|
|
1059
|
+
FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
|
|
1060
|
+
[hash]
|
|
1061
|
+
);
|
|
1062
|
+
if (!previous) return { kind: "unknown" };
|
|
1063
|
+
return { kind: previous.in_grace ? "grace" : "reused", row: toRow(previous) };
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Rotate a session in place to a new refresh token: same row, same device.
|
|
1067
|
+
* From the CURRENT token it is optimistic — only if the stored token is
|
|
1068
|
+
* still the one matched; a concurrent rotation makes it return false and the
|
|
1069
|
+
* caller re-matches (and now finds the token as the previous one). The old
|
|
1070
|
+
* token becomes the previous, valid for the grace. From the PREVIOUS token
|
|
1071
|
+
* (within the grace) it is authorised by that hash and deadline alone, so
|
|
1072
|
+
* racing retries never starve; the previous and its deadline are kept.
|
|
1073
|
+
*/
|
|
1074
|
+
async rotate(row, presented, next, from) {
|
|
1075
|
+
const rows = await this.store.query(
|
|
1076
|
+
`UPDATE fonderie_sessions
|
|
1077
|
+
SET previous_token_hash = CASE WHEN $5 THEN $6 ELSE previous_token_hash END,
|
|
1078
|
+
previous_valid_until = CASE WHEN $5 THEN now() + ($7 || ' milliseconds')::interval ELSE previous_valid_until END,
|
|
1079
|
+
token = $3,
|
|
1080
|
+
expires_at = $4,
|
|
1081
|
+
last_used_at = now()
|
|
1082
|
+
WHERE id = $1
|
|
1083
|
+
AND CASE WHEN $5 THEN token = $2
|
|
1084
|
+
ELSE previous_token_hash = $6 AND previous_valid_until > now() END
|
|
1085
|
+
RETURNING id`,
|
|
1086
|
+
[row.id, row.stored, hashRefreshToken(next.token), next.expiresAt, from === "current", hashRefreshToken(presented), String(REFRESH_GRACE_MS)]
|
|
1087
|
+
);
|
|
1088
|
+
return rows.length === 1;
|
|
1089
|
+
}
|
|
1090
|
+
/** Revoke one session by its row id (any user) — reuse detection, admin. */
|
|
1091
|
+
async revokeById(id) {
|
|
1092
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE id = $1`, [id]);
|
|
1020
1093
|
}
|
|
1021
1094
|
// Delete the session an access token is bound to (by its sid claim). Lets
|
|
1022
1095
|
// logout revoke the current session without the client resending the refresh
|
|
@@ -1082,8 +1155,8 @@ var SessionModel = class {
|
|
|
1082
1155
|
}
|
|
1083
1156
|
async exists(token) {
|
|
1084
1157
|
const rows = await this.store.query(
|
|
1085
|
-
`SELECT id FROM fonderie_sessions WHERE token
|
|
1086
|
-
[token]
|
|
1158
|
+
`SELECT id FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now()`,
|
|
1159
|
+
[hashRefreshToken(token), token]
|
|
1087
1160
|
);
|
|
1088
1161
|
return rows.length > 0;
|
|
1089
1162
|
}
|
|
@@ -1096,6 +1169,9 @@ var SessionModel = class {
|
|
|
1096
1169
|
return rows.length > 0;
|
|
1097
1170
|
}
|
|
1098
1171
|
};
|
|
1172
|
+
function toRow(r) {
|
|
1173
|
+
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() };
|
|
1174
|
+
}
|
|
1099
1175
|
|
|
1100
1176
|
// src/models/backup-code.model.ts
|
|
1101
1177
|
var BackupCodeModel = class {
|
|
@@ -1430,7 +1506,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1430
1506
|
}
|
|
1431
1507
|
|
|
1432
1508
|
// src/controllers/auth.controller.ts
|
|
1433
|
-
var
|
|
1509
|
+
var import_node_crypto6 = require("crypto");
|
|
1434
1510
|
var import_events2 = require("@fonderie/events");
|
|
1435
1511
|
var import_core6 = require("@fonderie/core");
|
|
1436
1512
|
|
|
@@ -1477,8 +1553,8 @@ function normalizeEmailSafe(email2) {
|
|
|
1477
1553
|
}
|
|
1478
1554
|
|
|
1479
1555
|
// src/models/password-reset.model.ts
|
|
1480
|
-
var
|
|
1481
|
-
var hashSecret = (value) => (0,
|
|
1556
|
+
var import_node_crypto5 = require("crypto");
|
|
1557
|
+
var hashSecret = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
|
|
1482
1558
|
var PasswordResetModel = class {
|
|
1483
1559
|
constructor(store) {
|
|
1484
1560
|
this.store = store;
|
|
@@ -1672,7 +1748,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1672
1748
|
if (!row) {
|
|
1673
1749
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1674
1750
|
}
|
|
1675
|
-
const pin = (0,
|
|
1751
|
+
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1676
1752
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1677
1753
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1678
1754
|
const user = await users.findById(row.id);
|
|
@@ -1743,7 +1819,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1743
1819
|
lastName ?? null,
|
|
1744
1820
|
locale
|
|
1745
1821
|
);
|
|
1746
|
-
const otp = (0,
|
|
1822
|
+
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1747
1823
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1748
1824
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1749
1825
|
const user = await users.findById(id);
|
|
@@ -1887,7 +1963,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1887
1963
|
"Account suspended. Please contact support."
|
|
1888
1964
|
);
|
|
1889
1965
|
}
|
|
1890
|
-
const otp = (0,
|
|
1966
|
+
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1891
1967
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1892
1968
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1893
1969
|
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1938,24 +2014,43 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1938
2014
|
if (!payload || payload.type !== "refresh") {
|
|
1939
2015
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1940
2016
|
}
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2017
|
+
const resolvedRefresh = { ...config, ...config.resolve?.(ctx) };
|
|
2018
|
+
const maxAgeMs = durationMs(resolvedRefresh.sessionMaxAge);
|
|
2019
|
+
let issued = null;
|
|
2020
|
+
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
2021
|
+
const found = await sessions.match(token);
|
|
2022
|
+
if (found.kind === "unknown") {
|
|
2023
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2024
|
+
}
|
|
2025
|
+
if (found.kind === "reused") {
|
|
2026
|
+
await sessions.revokeById(found.row.id);
|
|
2027
|
+
await (0, import_core6.background)(
|
|
2028
|
+
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sessionId: found.row.id, reason: "refresh-reuse" })
|
|
2029
|
+
);
|
|
2030
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2031
|
+
}
|
|
2032
|
+
if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
|
|
2033
|
+
await sessions.revokeById(found.row.id);
|
|
2034
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
|
|
2035
|
+
}
|
|
2036
|
+
const user = await users.findById(found.row.userId);
|
|
2037
|
+
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
2038
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2039
|
+
}
|
|
2040
|
+
const pair = issueTokenPair(user.id, resolvedRefresh, {
|
|
2041
|
+
loginMethod: payload.loginMethod ?? "email",
|
|
2042
|
+
phoneVerified: payload.phoneVerified ?? false,
|
|
2043
|
+
...found.row.sid ? { sid: found.row.sid } : {},
|
|
2044
|
+
// A refresh is not a sign-in: keep when the user last signed in.
|
|
2045
|
+
...typeof payload.auth_time === "number" ? { authTime: payload.auth_time } : {}
|
|
2046
|
+
});
|
|
2047
|
+
const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
|
|
2048
|
+
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
1948
2049
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2050
|
+
if (!issued) {
|
|
2051
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1952
2052
|
}
|
|
1953
|
-
|
|
1954
|
-
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1955
|
-
loginMethod: payload.loginMethod ?? "email",
|
|
1956
|
-
phoneVerified: payload.phoneVerified ?? false
|
|
1957
|
-
});
|
|
1958
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
2053
|
+
const { accessToken, refreshToken } = issued;
|
|
1959
2054
|
return Response.json(
|
|
1960
2055
|
{
|
|
1961
2056
|
reason: "TOKENS_REFRESHED",
|
|
@@ -1996,8 +2091,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1996
2091
|
"Password reset email sent (if account exists)."
|
|
1997
2092
|
);
|
|
1998
2093
|
}
|
|
1999
|
-
const pin = (0,
|
|
2000
|
-
const token = (0,
|
|
2094
|
+
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2095
|
+
const token = (0, import_node_crypto6.randomBytes)(32).toString("hex");
|
|
2001
2096
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60);
|
|
2002
2097
|
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
2003
2098
|
const base = resolved.passwordResetUrl;
|
|
@@ -2203,7 +2298,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2203
2298
|
}
|
|
2204
2299
|
);
|
|
2205
2300
|
}
|
|
2206
|
-
const otp = (0,
|
|
2301
|
+
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2207
2302
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
2208
2303
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
2209
2304
|
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -2242,7 +2337,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2242
2337
|
}
|
|
2243
2338
|
);
|
|
2244
2339
|
}
|
|
2245
|
-
const pin = (0,
|
|
2340
|
+
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2246
2341
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2247
2342
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2248
2343
|
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -2259,7 +2354,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2259
2354
|
}
|
|
2260
2355
|
|
|
2261
2356
|
// src/controllers/user.controller.ts
|
|
2262
|
-
var
|
|
2357
|
+
var import_node_crypto7 = require("crypto");
|
|
2263
2358
|
var import_core8 = require("@fonderie/core");
|
|
2264
2359
|
var import_events3 = require("@fonderie/events");
|
|
2265
2360
|
|
|
@@ -2453,7 +2548,7 @@ function userController(store, config, bus) {
|
|
|
2453
2548
|
if (existing) {
|
|
2454
2549
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2455
2550
|
}
|
|
2456
|
-
const pin = (0,
|
|
2551
|
+
const pin = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2457
2552
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2458
2553
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2459
2554
|
await users.updateEmail(ctx.user.id, normalised);
|
|
@@ -2495,7 +2590,7 @@ function userController(store, config, bus) {
|
|
|
2495
2590
|
if (existing) {
|
|
2496
2591
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2497
2592
|
}
|
|
2498
|
-
const otp = (0,
|
|
2593
|
+
const otp = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2499
2594
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2500
2595
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2501
2596
|
await users.updatePhone(ctx.user.id, normalised);
|
|
@@ -2645,7 +2740,7 @@ function userController(store, config, bus) {
|
|
|
2645
2740
|
}
|
|
2646
2741
|
|
|
2647
2742
|
// src/controllers/oauth.controller.ts
|
|
2648
|
-
var
|
|
2743
|
+
var import_node_crypto8 = require("crypto");
|
|
2649
2744
|
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
2650
2745
|
var import_core9 = require("@fonderie/core");
|
|
2651
2746
|
var import_events4 = require("@fonderie/events");
|
|
@@ -2720,7 +2815,7 @@ async function getApplePublicKey(kid) {
|
|
|
2720
2815
|
}
|
|
2721
2816
|
if (!jwk) return null;
|
|
2722
2817
|
try {
|
|
2723
|
-
return (0,
|
|
2818
|
+
return (0, import_node_crypto8.createPublicKey)({ key: jwk, format: "jwk" });
|
|
2724
2819
|
} catch {
|
|
2725
2820
|
return null;
|
|
2726
2821
|
}
|
|
@@ -2878,7 +2973,7 @@ function oauthController(store, config, bus) {
|
|
|
2878
2973
|
"Google OAuth not configured"
|
|
2879
2974
|
);
|
|
2880
2975
|
}
|
|
2881
|
-
const state = (0,
|
|
2976
|
+
const state = (0, import_node_crypto8.randomBytes)(16).toString("hex");
|
|
2882
2977
|
const params = new URLSearchParams({
|
|
2883
2978
|
client_id: google.clientId,
|
|
2884
2979
|
redirect_uri: google.redirectUri,
|
|
@@ -3026,7 +3121,7 @@ function oauthController(store, config, bus) {
|
|
|
3026
3121
|
if (!apple) {
|
|
3027
3122
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3028
3123
|
}
|
|
3029
|
-
const state = (0,
|
|
3124
|
+
const state = (0, import_node_crypto8.randomBytes)(16).toString("hex");
|
|
3030
3125
|
const params = new URLSearchParams({
|
|
3031
3126
|
client_id: apple.clientId,
|
|
3032
3127
|
redirect_uri: apple.redirectUri,
|
|
@@ -3122,7 +3217,7 @@ function oauthController(store, config, bus) {
|
|
|
3122
3217
|
});
|
|
3123
3218
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3124
3219
|
}
|
|
3125
|
-
const tokenHash = (0,
|
|
3220
|
+
const tokenHash = (0, import_node_crypto8.createHash)("sha256").update(identityToken).digest("hex");
|
|
3126
3221
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3127
3222
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
3128
3223
|
if (!firstUse) {
|
|
@@ -3579,7 +3674,7 @@ var AuthModule = class {
|
|
|
3579
3674
|
name = "@fonderie/auth";
|
|
3580
3675
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3581
3676
|
// say what is actually deployed rather than 'not reported'.
|
|
3582
|
-
version = "7.
|
|
3677
|
+
version = "7.17.0";
|
|
3583
3678
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3584
3679
|
describeAdmin() {
|
|
3585
3680
|
return { routes: describeAuthAdminRoutes(this.store) };
|