@nxgt/janus 0.13.0 → 0.14.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.
Files changed (66) hide show
  1. package/README.md +56 -15
  2. package/dist/auth/config/index.d.ts +1 -1
  3. package/dist/auth/config/index.d.ts.map +1 -1
  4. package/dist/auth/config/janus-config.d.ts +23 -0
  5. package/dist/auth/config/janus-config.d.ts.map +1 -1
  6. package/dist/auth/config/resolve-config.d.ts.map +1 -1
  7. package/dist/auth/config/resolve-sign-in.d.ts +5 -0
  8. package/dist/auth/config/resolve-sign-in.d.ts.map +1 -0
  9. package/dist/auth/config/resolved-config.d.ts +5 -0
  10. package/dist/auth/config/resolved-config.d.ts.map +1 -1
  11. package/dist/auth/derived-keys.d.ts +9 -0
  12. package/dist/auth/derived-keys.d.ts.map +1 -0
  13. package/dist/auth/events.d.ts +19 -4
  14. package/dist/auth/events.d.ts.map +1 -1
  15. package/dist/auth/index.d.ts +1 -1
  16. package/dist/auth/index.d.ts.map +1 -1
  17. package/dist/auth/one-time/index.d.ts +3 -1
  18. package/dist/auth/one-time/index.d.ts.map +1 -1
  19. package/dist/auth/one-time/window.d.ts +50 -0
  20. package/dist/auth/one-time/window.d.ts.map +1 -0
  21. package/dist/auth/password-written.d.ts +7 -0
  22. package/dist/auth/password-written.d.ts.map +1 -1
  23. package/dist/auth/second-factor/app-code-attempts.d.ts +5 -8
  24. package/dist/auth/second-factor/app-code-attempts.d.ts.map +1 -1
  25. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  26. package/dist/auth/second-factor/recovery-codes.d.ts.map +1 -1
  27. package/dist/auth/second-factor/recovery.d.ts.map +1 -1
  28. package/dist/auth/sign-in-attempts.d.ts +43 -0
  29. package/dist/auth/sign-in-attempts.d.ts.map +1 -0
  30. package/dist/auth/types/password.d.ts +8 -0
  31. package/dist/auth/types/password.d.ts.map +1 -1
  32. package/dist/auth/users/fields-patch.d.ts +8 -2
  33. package/dist/auth/users/fields-patch.d.ts.map +1 -1
  34. package/dist/auth/users/record-flows.d.ts.map +1 -1
  35. package/dist/auth/users/sign-in.d.ts +2 -2
  36. package/dist/auth/users/sign-in.d.ts.map +1 -1
  37. package/dist/auth/users/update.d.ts +12 -0
  38. package/dist/auth/users/update.d.ts.map +1 -0
  39. package/dist/chunks/{index-km7ffctx.js → index-6c9fbtv2.js} +2 -2
  40. package/dist/chunks/{index-rd2rddvv.js → index-baazjyvc.js} +2 -2
  41. package/dist/chunks/{index-j2gd8fry.js → index-hnwggyxs.js} +3 -2
  42. package/dist/chunks/{index-j2gd8fry.js.map → index-hnwggyxs.js.map} +3 -3
  43. package/dist/chunks/{index-jp2kka6d.js → index-kj3st57z.js} +2 -2
  44. package/dist/conformance/index.js +3 -3
  45. package/dist/errors/janus-error/base.d.ts +1 -0
  46. package/dist/errors/janus-error/base.d.ts.map +1 -1
  47. package/dist/errors/janus-error/codes.d.ts +3 -1
  48. package/dist/errors/janus-error/codes.d.ts.map +1 -1
  49. package/dist/errors/janus-error/options.d.ts +9 -2
  50. package/dist/errors/janus-error/options.d.ts.map +1 -1
  51. package/dist/index.js +226 -86
  52. package/dist/index.js.map +20 -15
  53. package/dist/permissions/index.js +3 -3
  54. package/docs/README.md +2 -2
  55. package/docs/guide/errors.md +10 -2
  56. package/docs/guide/events.md +73 -12
  57. package/docs/guide/passwords.md +115 -17
  58. package/docs/guide/second-factor.md +4 -2
  59. package/docs/guide/users.md +11 -6
  60. package/docs/guide/vocabulary.md +1 -1
  61. package/docs/roadmap.md +27 -18
  62. package/docs/troubleshooting.md +55 -2
  63. package/package.json +1 -1
  64. /package/dist/chunks/{index-km7ffctx.js.map → index-6c9fbtv2.js.map} +0 -0
  65. /package/dist/chunks/{index-rd2rddvv.js.map → index-baazjyvc.js.map} +0 -0
  66. /package/dist/chunks/{index-jp2kka6d.js.map → index-kj3st57z.js.map} +0 -0
package/dist/index.js CHANGED
@@ -7,13 +7,13 @@ import {
7
7
  MAX_PAGE_SIZE2,
8
8
  pageLimit2,
9
9
  invalidCursor2
10
- } from "./chunks/index-jp2kka6d.js";
10
+ } from "./chunks/index-kj3st57z.js";
11
11
  import {
12
12
  mintId2,
13
13
  isId2,
14
14
  mintedAt2,
15
15
  createMemoryStores2
16
- } from "./chunks/index-rd2rddvv.js";
16
+ } from "./chunks/index-baazjyvc.js";
17
17
  import {
18
18
  JanusError2,
19
19
  UserInvalidError2,
@@ -38,7 +38,7 @@ import {
38
38
  formatTuple2,
39
39
  parseTuple2,
40
40
  parseSubject2
41
- } from "./chunks/index-j2gd8fry.js";
41
+ } from "./chunks/index-hnwggyxs.js";
42
42
 
43
43
  // src/auth/hashers.ts
44
44
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
@@ -296,6 +296,28 @@ function resolveSecondFactor(config, where) {
296
296
  };
297
297
  }
298
298
 
299
+ // src/auth/config/resolve-sign-in.ts
300
+ function resolveSignInThrottle(config, where) {
301
+ if (config !== undefined && (typeof config !== "object" || config === null)) {
302
+ throw new TypeError(`${where}: signIn must be an object — { throttle }`);
303
+ }
304
+ const throttle = config?.throttle ?? {};
305
+ if (throttle === false)
306
+ return null;
307
+ const at = `${where}: signIn.throttle`;
308
+ if (typeof throttle !== "object" || throttle === null) {
309
+ throw new TypeError(`${at} must be { attempts, window }, or false to count nothing`);
310
+ }
311
+ const attempts = throttle.attempts ?? 10;
312
+ if (!Number.isSafeInteger(attempts) || attempts < 1) {
313
+ throw new TypeError(`${at}.attempts must be a whole number above zero`);
314
+ }
315
+ return {
316
+ attempts,
317
+ windowMs: parseDuration(throttle.window ?? "15m", `${at}.window`)
318
+ };
319
+ }
320
+
299
321
  // src/auth/config/resolve-type.ts
300
322
  function resolveType(name, type, at, schemaKey) {
301
323
  if (typeof type?.schema !== "object" || type.schema === null || typeof type.schema["~standard"]?.validate !== "function") {
@@ -342,6 +364,7 @@ function resolveConfig(config, where) {
342
364
  types,
343
365
  tokenTtlMs: resolveTokenTtls(config, where),
344
366
  cookie: resolveCookie(config.cookie ?? {}, where),
367
+ signInThrottle: resolveSignInThrottle(config.signIn, where),
345
368
  secondFactor: resolveSecondFactor(config.secondFactor, where)
346
369
  };
347
370
  }
@@ -632,7 +655,7 @@ function resolveEvents(events, where) {
632
655
  }
633
656
  return events;
634
657
  }
635
- async function emit(context, type, user, occurredAt) {
658
+ async function emit(context, type, user, occurredAt, extras = {}) {
636
659
  const listener = context.events;
637
660
  if (listener === null)
638
661
  return;
@@ -641,7 +664,8 @@ async function emit(context, type, user, occurredAt) {
641
664
  type,
642
665
  occurredAt: new Date(occurredAt.getTime()),
643
666
  userId: user.id,
644
- userType: user.type
667
+ userType: user.type,
668
+ ...extras
645
669
  });
646
670
  try {
647
671
  await listener(event);
@@ -977,6 +1001,73 @@ async function insertOneTime(context, token, code) {
977
1001
  });
978
1002
  return { secret, expiresAt };
979
1003
  }
1004
+ // src/auth/one-time/window.ts
1005
+ var MAX_LINKS = 32;
1006
+ var MAX_PROBES = 64;
1007
+ async function countInWindow(context, counting) {
1008
+ const now = context.clock.now().getTime();
1009
+ const window = Math.floor(now / counting.windowMs);
1010
+ const expiresAt = (window + 2) * counting.windowMs;
1011
+ const links = { context, counting, window, now, expiresAt };
1012
+ const counted = counting.spent === "carry" ? await countCarrying(links) : await countRestarting(links);
1013
+ return { ...counted, endsAt: (window + 1) * counting.windowMs };
1014
+ }
1015
+ async function countCarrying(links) {
1016
+ let carried = 0;
1017
+ for (let link = 0;link < MAX_LINKS; link += 1) {
1018
+ const hash = links.counting.linkHash(links.window, link);
1019
+ const counted = await countLink(links, hash);
1020
+ const attempts = carried + counted.attempts;
1021
+ if (counted.spentAt === null)
1022
+ return { attempts, link: hash };
1023
+ carried = attempts;
1024
+ }
1025
+ return { attempts: Number.POSITIVE_INFINITY, link: null };
1026
+ }
1027
+ async function countRestarting(links) {
1028
+ const tokens = links.context.store.tokens;
1029
+ let spent = -1;
1030
+ let absent = Number.POSITIVE_INFINITY;
1031
+ for (let probes = 0;probes < MAX_PROBES; probes += 1) {
1032
+ const next = absent === spent + 1;
1033
+ const link = next ? absent : absent === Number.POSITIVE_INFINITY ? 2 * spent + 2 : Math.floor((spent + absent) / 2);
1034
+ const hash = links.counting.linkHash(links.window, link);
1035
+ const counted = next ? await insertLink(links, hash) : await tokens.countAttempt(hash, "secondFactor");
1036
+ if (counted === null)
1037
+ absent = link;
1038
+ else if (counted.spentAt === null) {
1039
+ return { attempts: counted.attempts, link: hash };
1040
+ } else {
1041
+ spent = link;
1042
+ if (absent <= spent)
1043
+ absent = Number.POSITIVE_INFINITY;
1044
+ }
1045
+ }
1046
+ throw links.counting.vanished();
1047
+ }
1048
+ async function countLink(links, tokenHash) {
1049
+ const tokens = links.context.store.tokens;
1050
+ const counted = await tokens.countAttempt(tokenHash, "secondFactor");
1051
+ return counted ?? insertLink(links, tokenHash);
1052
+ }
1053
+ async function insertLink(links, tokenHash) {
1054
+ const tokens = links.context.store.tokens;
1055
+ await tokens.insertToken({
1056
+ tokenHash,
1057
+ kind: "secondFactor",
1058
+ userId: links.counting.userId,
1059
+ address: "",
1060
+ codeHash: null,
1061
+ attempts: 0,
1062
+ expiresAt: new Date(links.expiresAt),
1063
+ spentAt: null,
1064
+ createdAt: new Date(links.now)
1065
+ });
1066
+ const inserted = await tokens.countAttempt(tokenHash, "secondFactor");
1067
+ if (inserted !== null)
1068
+ return inserted;
1069
+ throw links.counting.vanished();
1070
+ }
980
1071
  // src/auth/password-written.ts
981
1072
  async function endWhatThePasswordOpened(context, userId) {
982
1073
  const now = context.clock.now();
@@ -985,6 +1076,13 @@ async function endWhatThePasswordOpened(context, userId) {
985
1076
  if (failed !== undefined)
986
1077
  throw failed.reason;
987
1078
  }
1079
+ async function passwordChanged(context, written) {
1080
+ try {
1081
+ await endWhatThePasswordOpened(context, written.id);
1082
+ } finally {
1083
+ await emit(context, "user.passwordChanged", written, written.updatedAt);
1084
+ }
1085
+ }
988
1086
  async function heldByPassword(context, type, verified, password, result, where) {
989
1087
  const now = await findRecord(context, verified.id, type.name);
990
1088
  const kept = now !== null && now.password !== null && (now.password.hash === verified.password?.hash || await passwordMatches(context, now, password, where));
@@ -1101,8 +1199,78 @@ function emailFlows(context, type, at) {
1101
1199
  resetPassword: resetPasswordFlows(context, type, at)
1102
1200
  };
1103
1201
  }
1202
+ // src/auth/derived-keys.ts
1203
+ import { createHmac, hkdfSync } from "node:crypto";
1204
+ var derived = new WeakMap;
1205
+ function derivedKey(sealer, purpose, keyId = sealer.sealWith) {
1206
+ let keys = derived.get(sealer);
1207
+ if (keys === undefined) {
1208
+ keys = new Map;
1209
+ derived.set(sealer, keys);
1210
+ }
1211
+ const name = `${purpose}\x00${keyId}`;
1212
+ let key = keys.get(name);
1213
+ if (key === undefined) {
1214
+ const secret = sealer.keys.get(keyId);
1215
+ if (secret === undefined)
1216
+ return;
1217
+ key = Buffer.from(hkdfSync("sha256", secret, Buffer.alloc(0), purpose, 32));
1218
+ keys.set(name, key);
1219
+ }
1220
+ return key;
1221
+ }
1222
+ function keyedHash(key, parts) {
1223
+ return createHmac("sha256", key).update(parts.join("\x00")).digest();
1224
+ }
1225
+
1226
+ // src/auth/sign-in-attempts.ts
1227
+ var PURPOSE = "janus/sign-in/attempts/v1";
1228
+ var UNSEALED = Buffer.from(PURPOSE);
1229
+ function counterKey(context) {
1230
+ const sealer = context.config.secondFactor?.sealer;
1231
+ return sealer === undefined ? UNSEALED : derivedKey(sealer, PURPOSE);
1232
+ }
1233
+ var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
1234
+ async function countLogin(context, type, login, where) {
1235
+ const throttle = context.config.signInThrottle;
1236
+ if (throttle === null)
1237
+ return null;
1238
+ const key = counterKey(context);
1239
+ const loginKey = keyed(key, [type.name, login]);
1240
+ return countInWindow(context, {
1241
+ windowMs: throttle.windowMs,
1242
+ spent: "restart",
1243
+ userId: loginKey,
1244
+ linkHash: (window, link) => hashSecret(keyed(key, [loginKey, String(window), String(link)])),
1245
+ vanished: () => new StoreFailure2(`${where}: the store dropped the attempts it had just stored`, { slot: "tokens", operation: "countAttempt" })
1246
+ });
1247
+ }
1248
+ async function countSignInAttempt(context, type, login, where) {
1249
+ const counted = await countLogin(context, type, login, where);
1250
+ const limit = context.config.signInThrottle?.attempts;
1251
+ if (counted === null || limit === undefined || counted.attempts <= limit) {
1252
+ return;
1253
+ }
1254
+ const now = context.clock.now().getTime();
1255
+ throw new CredentialError2("CREDENTIALS_INVALID", `${where}: too many passwords tried at this login — wait for the next window`, {
1256
+ reason: "throttled",
1257
+ userType: type.name,
1258
+ retryAfter: Math.max(1, Math.ceil((counted.endsAt - now) / 1000))
1259
+ });
1260
+ }
1261
+ async function restartSignInCount(context, type, record, where) {
1262
+ const rule = type.password;
1263
+ const login = rule === null ? undefined : record.fields[rule.login];
1264
+ if (rule === null || typeof login !== "string")
1265
+ return;
1266
+ const counted = await countLogin(context, type, rule.normalize(login), where);
1267
+ if (counted?.link == null)
1268
+ return;
1269
+ await context.store.tokens.consumeToken(counted.link, "secondFactor", context.clock.now());
1270
+ }
1271
+
1104
1272
  // src/auth/totp.ts
1105
- import { createHmac, randomBytes as randomBytes4 } from "node:crypto";
1273
+ import { createHmac as createHmac2, randomBytes as randomBytes4 } from "node:crypto";
1106
1274
  var TOTP = { digits: 6, stepMs: 30000, drift: 1 };
1107
1275
  var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1108
1276
  function mintTotpSecret() {
@@ -1145,7 +1313,7 @@ var stepAt = (now) => Math.floor(now.getTime() / TOTP.stepMs);
1145
1313
  function codeAt(secret, step) {
1146
1314
  const counter = Buffer.alloc(8);
1147
1315
  counter.writeBigUInt64BE(BigInt(step));
1148
- const digest = createHmac("sha1", secret).update(counter).digest();
1316
+ const digest = createHmac2("sha1", secret).update(counter).digest();
1149
1317
  const offset = digest[19] & 15;
1150
1318
  const binary = digest.readUInt32BE(offset) & 2147483647;
1151
1319
  return String(binary % 10 ** TOTP.digits).padStart(TOTP.digits, "0");
@@ -1272,6 +1440,7 @@ async function confirmChallenge(context, type, challenge, code, at) {
1272
1440
  return refuseCode(context, type, opened, where);
1273
1441
  const written = await context.store.users.updateUser(record.id, { secondFactor: accepted, updatedAt: now }, record.version);
1274
1442
  await spendChallenge(context, opened.secret, where);
1443
+ await restartSignInCount(context, type, written, where);
1275
1444
  return openSession(context, type, written);
1276
1445
  }
1277
1446
 
@@ -1283,12 +1452,12 @@ async function recoveryCodesLeftOf(context, type, user, where) {
1283
1452
  }
1284
1453
 
1285
1454
  // src/auth/second-factor/recovery-codes.ts
1286
- import { createHmac as createHmac2, hkdfSync, randomInt as randomInt2, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
1455
+ import { randomInt as randomInt2, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
1287
1456
  var RECOVERY_CODES = 10;
1288
1457
  var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
1289
1458
  var LENGTH = 10;
1290
1459
  var SHAPE = /^[0-9a-hjkmnp-tv-z]{10}$/;
1291
- var PURPOSE = "janus/second-factor/recovery-codes/v1";
1460
+ var PURPOSE2 = "janus/second-factor/recovery-codes/v1";
1292
1461
  function mintRecoveryCodes() {
1293
1462
  return Array.from({ length: RECOVERY_CODES }, () => {
1294
1463
  let code = "";
@@ -1302,24 +1471,8 @@ function readRecoveryCode(typed) {
1302
1471
  const code = typed.toLowerCase().replace(/[\s-]/g, "").replace(/o/g, "0").replace(/[il]/g, "1");
1303
1472
  return SHAPE.test(code) ? code : null;
1304
1473
  }
1305
- var derived = new WeakMap;
1306
- function macKey(sealer, keyId) {
1307
- let keys = derived.get(sealer);
1308
- if (keys === undefined) {
1309
- keys = new Map;
1310
- derived.set(sealer, keys);
1311
- }
1312
- let key = keys.get(keyId);
1313
- if (key === undefined) {
1314
- const secret = sealer.keys.get(keyId);
1315
- if (secret === undefined)
1316
- return;
1317
- key = Buffer.from(hkdfSync("sha256", secret, Buffer.alloc(0), PURPOSE, 32));
1318
- keys.set(keyId, key);
1319
- }
1320
- return key;
1321
- }
1322
- var mac = (key, userId, code) => createHmac2("sha256", key).update(`${userId}\x00${code}`).digest();
1474
+ var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE2, keyId);
1475
+ var mac = (key, userId, code) => keyedHash(key, [userId, code]);
1323
1476
  function hashRecoveryCode(sealer, userId, code) {
1324
1477
  const read = readRecoveryCode(code) ?? code;
1325
1478
  const key = macKey(sealer, sealer.sealWith);
@@ -1427,21 +1580,9 @@ function accountOf(type, record, where) {
1427
1580
  }
1428
1581
 
1429
1582
  // src/auth/second-factor/app-code-attempts.ts
1430
- import { createHmac as createHmac3, hkdfSync as hkdfSync2 } from "node:crypto";
1431
1583
  var APP_CODE_WINDOW_MS = 15 * 60000;
1432
1584
  var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
1433
- var PURPOSE2 = "janus/second-factor/regenerate-attempts/v1";
1434
- var MAX_LINKS = 32;
1435
- var derived2 = new WeakMap;
1436
- function counterKey(sealer) {
1437
- let key = derived2.get(sealer);
1438
- if (key === undefined) {
1439
- const secret = sealer.keys.get(sealer.sealWith);
1440
- key = Buffer.from(hkdfSync2("sha256", secret, Buffer.alloc(0), PURPOSE2, 32));
1441
- derived2.set(sealer, key);
1442
- }
1443
- return key;
1444
- }
1585
+ var PURPOSE3 = "janus/second-factor/regenerate-attempts/v1";
1445
1586
  function linkHash(sealer, record, window, link) {
1446
1587
  const parts = [
1447
1588
  record.id,
@@ -1449,45 +1590,21 @@ function linkHash(sealer, record, window, link) {
1449
1590
  String(record.secondFactor?.lastStep ?? ""),
1450
1591
  String(link)
1451
1592
  ];
1452
- return hashSecret(createHmac3("sha256", counterKey(sealer)).update(parts.join("\x00")).digest("base64url"));
1593
+ const key = derivedKey(sealer, PURPOSE3);
1594
+ return hashSecret(keyedHash(key, parts).toString("base64url"));
1453
1595
  }
1454
1596
  async function countAppCodeAttempt(context, sealer, record, where) {
1455
- const now = context.clock.now().getTime();
1456
- const window = Math.floor(now / APP_CODE_WINDOW_MS);
1457
- const times = { now, expiresAt: (window + 2) * APP_CODE_WINDOW_MS };
1458
- let spent = 0;
1459
- for (let link = 0;link < MAX_LINKS; link += 1) {
1460
- const hash = linkHash(sealer, record, window, link);
1461
- const counted = await countLink(context, hash, record, times, where);
1462
- const attempts = spent + counted.attempts;
1463
- if (counted.spentAt === null)
1464
- return attemptsLeft(attempts, record, where);
1465
- spent = attempts;
1466
- }
1467
- return attemptsLeft(Number.POSITIVE_INFINITY, record, where);
1468
- }
1469
- async function countLink(context, tokenHash, record, times, where) {
1470
- const tokens = context.store.tokens;
1471
- const counted = await tokens.countAttempt(tokenHash, "secondFactor");
1472
- if (counted !== null)
1473
- return counted;
1474
- await tokens.insertToken({
1475
- tokenHash,
1476
- kind: "secondFactor",
1597
+ const { attempts } = await countInWindow(context, {
1598
+ windowMs: APP_CODE_WINDOW_MS,
1599
+ spent: "carry",
1477
1600
  userId: record.id,
1478
- address: "",
1479
- codeHash: null,
1480
- attempts: 0,
1481
- expiresAt: new Date(times.expiresAt),
1482
- spentAt: null,
1483
- createdAt: new Date(times.now)
1601
+ linkHash: (window, link) => linkHash(sealer, record, window, link),
1602
+ vanished: () => new StoreConflict2("version", `${where}: the user changed while the code was checked — read it again and retry`, {
1603
+ userId: record.id,
1604
+ expectedVersion: record.version,
1605
+ operation: where
1606
+ })
1484
1607
  });
1485
- const inserted = await tokens.countAttempt(tokenHash, "secondFactor");
1486
- if (inserted !== null)
1487
- return inserted;
1488
- throw new StoreConflict2("version", `${where}: the user changed while the code was checked — read it again and retry`, { userId: record.id, expectedVersion: record.version, operation: where });
1489
- }
1490
- function attemptsLeft(attempts, record, where) {
1491
1608
  if (attempts > CODE_ATTEMPTS) {
1492
1609
  throw new TokenError2("CODE_INVALID", `${where}: too many codes tried — wait for the next ${WINDOW_MINUTES}-minute window`, {
1493
1610
  operation: where,
@@ -1558,6 +1675,7 @@ async function recoverWithCode(context, type, challenge, code, at) {
1558
1675
  }, record.version);
1559
1676
  try {
1560
1677
  await spendChallenge(context, opened.secret, where);
1678
+ await restartSignInCount(context, type, written, where);
1561
1679
  const signedIn = await openSession(context, type, written);
1562
1680
  return { ...signedIn, recoveryCodesLeft: recoveryCodes.length };
1563
1681
  } finally {
@@ -1798,7 +1916,7 @@ async function changePassword(context, type, user, change, options, where) {
1798
1916
  }
1799
1917
  };
1800
1918
  });
1801
- await endWhatThePasswordOpened(context, written.id);
1919
+ await passwordChanged(context, written);
1802
1920
  return written;
1803
1921
  }
1804
1922
 
@@ -1840,7 +1958,11 @@ async function signIn(context, type, finish, input, where) {
1840
1958
  const hasher = requireHasher(context, where);
1841
1959
  const login = input?.[rule.login];
1842
1960
  const password = input?.password;
1843
- const record = typeof login === "string" ? await byLogin(context, type, rule.normalize(login)) : null;
1961
+ const normalized = typeof login === "string" ? rule.normalize(login) : null;
1962
+ if (normalized !== null) {
1963
+ await countSignInAttempt(context, type, normalized, where);
1964
+ }
1965
+ const record = normalized === null ? null : await byLogin(context, type, normalized);
1844
1966
  const refuse = (reason) => new CredentialError2("CREDENTIALS_INVALID", `${where}: the login and the password do not match`, { reason, userType: type.name });
1845
1967
  if (record === null || record.password === null) {
1846
1968
  await hasher.verify(String(password), await context.dummyHash());
@@ -1855,6 +1977,9 @@ async function signIn(context, type, finish, input, where) {
1855
1977
  userType: type.name
1856
1978
  });
1857
1979
  }
1980
+ if (!isActive(record.secondFactor)) {
1981
+ await restartSignInCount(context, type, record, where);
1982
+ }
1858
1983
  const verified = await rehashed(context, record, String(password));
1859
1984
  const result = await finish(verified, where);
1860
1985
  if (!await heldByPassword(context, type, verified, String(password), result, where)) {
@@ -1889,7 +2014,7 @@ function passwordFlows(context, type, at, finish) {
1889
2014
  checkPassword(type, password, where);
1890
2015
  const hash = await requireHasher(context, where).hash(password);
1891
2016
  const written = await writeUser(context, user, type, options, where, (_, now) => ({ password: { hash, updatedAt: now } }));
1892
- await endWhatThePasswordOpened(context, written.id);
2017
+ await passwordChanged(context, written);
1893
2018
  return toUser(written);
1894
2019
  },
1895
2020
  async changePassword(user, change, options) {
@@ -1923,19 +2048,36 @@ async function deleteUser(context, type, user) {
1923
2048
  }
1924
2049
 
1925
2050
  // src/auth/users/fields-patch.ts
2051
+ var normalized = (email) => email === null ? null : normalizeEmail(email);
2052
+ function emailChanged(type, before, after) {
2053
+ return normalized(emailOf(type, before)) !== normalized(emailOf(type, after));
2054
+ }
1926
2055
  async function fieldsPatch(type, record, patch, where) {
1927
2056
  const fields = await validateFields(type, { ...record.fields, ...patch }, where);
1928
- const before = emailOf(type, record.fields);
1929
- const after = emailOf(type, fields);
1930
- const emailChanged = (before === null ? null : normalizeEmail(before)) !== (after === null ? null : normalizeEmail(after));
1931
2057
  return {
1932
2058
  fields,
1933
2059
  logins: loginsOf(type, fields, where),
1934
2060
  schemaVersion: type.schemaVersion,
1935
- ...emailChanged ? { emailVerifiedAt: null } : {}
2061
+ ...emailChanged(type, record.fields, fields) ? { emailVerifiedAt: null } : {}
1936
2062
  };
1937
2063
  }
1938
2064
 
2065
+ // src/auth/users/update.ts
2066
+ async function update(context, type, user, patch, options, where) {
2067
+ const seen = { replaced: null };
2068
+ const written = await writeUser(context, user, type, options, where, (record) => {
2069
+ seen.replaced = record;
2070
+ return fieldsPatch(type, record, patch, where);
2071
+ });
2072
+ const { replaced } = seen;
2073
+ if (replaced !== null && emailChanged(type, replaced.fields, written.fields)) {
2074
+ await emit(context, "user.emailChanged", written, written.updatedAt, {
2075
+ formerEmail: emailOf(type, replaced.fields)
2076
+ });
2077
+ }
2078
+ return written;
2079
+ }
2080
+
1939
2081
  // src/auth/users/record-flows.ts
1940
2082
  function recordFlows(context, type, at) {
1941
2083
  const { store } = context;
@@ -1963,9 +2105,7 @@ function recordFlows(context, type, at) {
1963
2105
  return { items: answer.items.map(toUser), nextCursor: answer.nextCursor };
1964
2106
  },
1965
2107
  async update(user, patch, options) {
1966
- const where = at("update");
1967
- const written = await writeUser(context, user, type, options, where, (record) => fieldsPatch(type, record, patch, where));
1968
- return toUser(written);
2108
+ return toUser(await update(context, type, user, patch, options, at("update")));
1969
2109
  },
1970
2110
  async setActive(user, active, options) {
1971
2111
  return toUser(await writeUser(context, user, type, options, at("setActive"), () => ({
@@ -2075,5 +2215,5 @@ export {
2075
2215
  systemClock
2076
2216
  };
2077
2217
 
2078
- //# debugId=D61F9F351D899C2964756E2164756E21
2218
+ //# debugId=7CE655305FF58FDF64756E2164756E21
2079
2219
  //# sourceMappingURL=index.js.map