@nxgt/janus 0.16.1 → 0.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.
Files changed (65) hide show
  1. package/README.md +99 -11
  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 +24 -3
  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-devices.d.ts +5 -0
  8. package/dist/auth/config/resolve-devices.d.ts.map +1 -0
  9. package/dist/auth/config/resolved-config.d.ts +4 -0
  10. package/dist/auth/config/resolved-config.d.ts.map +1 -1
  11. package/dist/auth/devices/hint.d.ts +30 -0
  12. package/dist/auth/devices/hint.d.ts.map +1 -0
  13. package/dist/auth/devices/index.d.ts +10 -0
  14. package/dist/auth/devices/index.d.ts.map +1 -0
  15. package/dist/auth/devices/report.d.ts +11 -0
  16. package/dist/auth/devices/report.d.ts.map +1 -0
  17. package/dist/auth/devices/token.d.ts +13 -0
  18. package/dist/auth/devices/token.d.ts.map +1 -0
  19. package/dist/auth/email-sign-in.d.ts +4 -2
  20. package/dist/auth/email-sign-in.d.ts.map +1 -1
  21. package/dist/auth/events.d.ts +18 -5
  22. package/dist/auth/events.d.ts.map +1 -1
  23. package/dist/auth/index.d.ts +2 -2
  24. package/dist/auth/index.d.ts.map +1 -1
  25. package/dist/auth/magic-link.d.ts.map +1 -1
  26. package/dist/auth/second-factor/challenge.d.ts +2 -2
  27. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  28. package/dist/auth/second-factor/flows.d.ts +6 -4
  29. package/dist/auth/second-factor/flows.d.ts.map +1 -1
  30. package/dist/auth/second-factor/recovery.d.ts +6 -2
  31. package/dist/auth/second-factor/recovery.d.ts.map +1 -1
  32. package/dist/auth/sessions/open-session.d.ts +7 -1
  33. package/dist/auth/sessions/open-session.d.ts.map +1 -1
  34. package/dist/auth/sign-in-code.d.ts.map +1 -1
  35. package/dist/auth/types/email-flows.d.ts +2 -2
  36. package/dist/auth/types/email-flows.d.ts.map +1 -1
  37. package/dist/auth/types/index.d.ts +1 -1
  38. package/dist/auth/types/index.d.ts.map +1 -1
  39. package/dist/auth/types/magic-link.d.ts +2 -2
  40. package/dist/auth/types/magic-link.d.ts.map +1 -1
  41. package/dist/auth/types/password.d.ts +10 -3
  42. package/dist/auth/types/password.d.ts.map +1 -1
  43. package/dist/auth/types/second-factor.d.ts +6 -3
  44. package/dist/auth/types/second-factor.d.ts.map +1 -1
  45. package/dist/auth/types/sign-in.d.ts +32 -0
  46. package/dist/auth/types/sign-in.d.ts.map +1 -1
  47. package/dist/auth/users/flow-types.d.ts +2 -1
  48. package/dist/auth/users/flow-types.d.ts.map +1 -1
  49. package/dist/auth/users/password-flows.d.ts.map +1 -1
  50. package/dist/auth/users/sign-in.d.ts +5 -1
  51. package/dist/auth/users/sign-in.d.ts.map +1 -1
  52. package/dist/index.js +172 -73
  53. package/dist/index.js.map +19 -15
  54. package/docs/README.md +2 -1
  55. package/docs/guide/devices.md +428 -0
  56. package/docs/guide/events.md +53 -8
  57. package/docs/guide/magic-link.md +25 -3
  58. package/docs/guide/second-factor.md +30 -5
  59. package/docs/guide/sessions.md +8 -0
  60. package/docs/guide/sign-in-code.md +22 -3
  61. package/docs/guide/users.md +3 -2
  62. package/docs/guide/vocabulary.md +4 -3
  63. package/docs/roadmap.md +12 -8
  64. package/docs/troubleshooting.md +210 -4
  65. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -281,6 +281,13 @@ function unseal(sealer, sealed, boundTo, where) {
281
281
  }
282
282
  }
283
283
 
284
+ // src/auth/config/resolve-devices.ts
285
+ function resolveDevices(config, where) {
286
+ if (config === undefined)
287
+ return null;
288
+ return { sealer: resolveSealer(config?.keys, `${where}: devices.keys`) };
289
+ }
290
+
284
291
  // src/auth/config/resolve-second-factor.ts
285
292
  function resolveSecondFactor(config, where) {
286
293
  if (config === undefined)
@@ -365,7 +372,8 @@ function resolveConfig(config, where) {
365
372
  tokenTtlMs: resolveTokenTtls(config, where),
366
373
  cookie: resolveCookie(config.cookie ?? {}, where),
367
374
  signInThrottle: resolveSignInThrottle(config.signIn, where),
368
- secondFactor: resolveSecondFactor(config.secondFactor, where)
375
+ secondFactor: resolveSecondFactor(config.secondFactor, where),
376
+ devices: resolveDevices(config.devices, where)
369
377
  };
370
378
  }
371
379
  function entriesOf(config, hasUsers) {
@@ -725,10 +733,105 @@ function assertStores(stores, where) {
725
733
  return { collectExpired: collect !== undefined };
726
734
  }
727
735
 
736
+ // src/auth/devices/token.ts
737
+ import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
738
+
739
+ // src/auth/derived-keys.ts
740
+ import { createHmac, hkdfSync } from "node:crypto";
741
+ var derived = new WeakMap;
742
+ function derivedKey(sealer, purpose, keyId = sealer.sealWith) {
743
+ let keys = derived.get(sealer);
744
+ if (keys === undefined) {
745
+ keys = new Map;
746
+ derived.set(sealer, keys);
747
+ }
748
+ const name = `${purpose}\x00${keyId}`;
749
+ let key = keys.get(name);
750
+ if (key === undefined) {
751
+ const secret = sealer.keys.get(keyId);
752
+ if (secret === undefined)
753
+ return;
754
+ key = Buffer.from(hkdfSync("sha256", secret, Buffer.alloc(0), purpose, 32));
755
+ keys.set(name, key);
756
+ }
757
+ return key;
758
+ }
759
+ function keyedHash(key, parts) {
760
+ return createHmac("sha256", key).update(parts.join("\x00")).digest();
761
+ }
762
+
763
+ // src/auth/devices/token.ts
764
+ var PURPOSE = "janus/devices/v1";
765
+ var TOKEN = /^d1\.([A-Za-z0-9_-]{1,64})\.([A-Za-z0-9_-]{22})\.([A-Za-z0-9_-]{43})$/;
766
+ function macOf(key, userId, deviceId) {
767
+ return keyedHash(key, [userId, deviceId]);
768
+ }
769
+ function signed(sealer, userId, deviceId) {
770
+ const key = derivedKey(sealer, PURPOSE);
771
+ const mac = macOf(key, userId, deviceId).toString("base64url");
772
+ return `d1.${sealer.sealWith}.${deviceId}.${mac}`;
773
+ }
774
+ function mintDeviceToken(sealer, userId) {
775
+ return signed(sealer, userId, randomBytes3(16).toString("base64url"));
776
+ }
777
+ function knownDeviceToken(sealer, userId, presented) {
778
+ const [, keyId, deviceId, mac] = TOKEN.exec(presented) ?? [];
779
+ if (keyId === undefined || deviceId === undefined || mac === undefined) {
780
+ return null;
781
+ }
782
+ const key = derivedKey(sealer, PURPOSE, keyId);
783
+ if (key === undefined)
784
+ return null;
785
+ const expected = macOf(key, userId, deviceId);
786
+ const given = Buffer.from(mac, "base64url");
787
+ if (given.length !== expected.length || !timingSafeEqual2(given, expected)) {
788
+ return null;
789
+ }
790
+ return keyId === sealer.sealWith ? presented : signed(sealer, userId, deviceId);
791
+ }
792
+
793
+ // src/auth/devices/hint.ts
794
+ var UNTRACKED = Object.freeze({
795
+ newDevice: false,
796
+ deviceToken: null
797
+ });
798
+ function deviceHint(context, options, where) {
799
+ if (options !== undefined && (typeof options !== "object" || options === null)) {
800
+ throw new TypeError(`${where}: options must be { device }, or left out — the token goes under device`);
801
+ }
802
+ const device = options?.device;
803
+ if (device === undefined)
804
+ return;
805
+ if (device !== null && typeof device !== "string") {
806
+ throw new TypeError(`${where}: options.device must be the device token the client holds, or null when it holds none`);
807
+ }
808
+ if (context.config.devices === null) {
809
+ throw new TypeError(`${where}: a device was given, but janus() has no devices — pass devices: { keys }`);
810
+ }
811
+ return device;
812
+ }
813
+ function deviceOutcome(context, userId, hint, signUp) {
814
+ const sealer = context.config.devices?.sealer;
815
+ if (hint === undefined || sealer === undefined)
816
+ return UNTRACKED;
817
+ const known = hint === null || signUp ? null : knownDeviceToken(sealer, userId, hint);
818
+ if (known !== null)
819
+ return { newDevice: false, deviceToken: known };
820
+ return {
821
+ newDevice: !signUp,
822
+ deviceToken: mintDeviceToken(sealer, userId)
823
+ };
824
+ }
825
+ // src/auth/devices/report.ts
826
+ async function reportNewDevice(context, result) {
827
+ if (result.status !== "signedIn" || !result.newDevice)
828
+ return;
829
+ await emit(context, "user.newDeviceSignedIn", result.user, result.session.createdAt, { sessionId: result.session.id });
830
+ }
728
831
  // src/auth/secrets.ts
729
- import { createHash, randomBytes as randomBytes3 } from "node:crypto";
832
+ import { createHash, randomBytes as randomBytes4 } from "node:crypto";
730
833
  function mintSecret() {
731
- return randomBytes3(32).toString("base64url");
834
+ return randomBytes4(32).toString("base64url");
732
835
  }
733
836
  function hashSecret(secret) {
734
837
  return createHash("sha256").update(secret).digest("hex");
@@ -739,7 +842,7 @@ function toSession(record) {
739
842
  const { tokenHash: _, ...session } = record;
740
843
  return session;
741
844
  }
742
- async function openSession(context, type, user) {
845
+ async function openSession(context, type, user, device, signUp = false) {
743
846
  const now = context.clock.now();
744
847
  const token = mintSecret();
745
848
  const record = {
@@ -756,7 +859,8 @@ async function openSession(context, type, user) {
756
859
  status: "signedIn",
757
860
  user: toUser(user),
758
861
  session: toSession(record),
759
- token
862
+ token,
863
+ ...deviceOutcome(context, user.id, device, signUp)
760
864
  };
761
865
  }
762
866
  // src/auth/sessions/presented-token.ts
@@ -905,7 +1009,7 @@ function sharedApi(context) {
905
1009
  };
906
1010
  }
907
1011
  // src/auth/one-time/codes.ts
908
- import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
1012
+ import { timingSafeEqual as timingSafeEqual3 } from "node:crypto";
909
1013
 
910
1014
  // src/auth/one-time/refusals.ts
911
1015
  function refuseStale(type, user, token, where, noun) {
@@ -974,7 +1078,7 @@ function codeMatches(token, challenge, code) {
974
1078
  return false;
975
1079
  const given = Buffer.from(hashCode(challenge, code));
976
1080
  const held = Buffer.from(token.codeHash);
977
- return given.length === held.length && timingSafeEqual2(given, held);
1081
+ return given.length === held.length && timingSafeEqual3(given, held);
978
1082
  }
979
1083
  // src/auth/one-time/issue.ts
980
1084
  import { randomInt } from "node:crypto";
@@ -1201,11 +1305,11 @@ function emailFlows(context, type, at) {
1201
1305
  };
1202
1306
  }
1203
1307
  // src/auth/totp.ts
1204
- import { createHmac, randomBytes as randomBytes4 } from "node:crypto";
1308
+ import { createHmac as createHmac2, randomBytes as randomBytes5 } from "node:crypto";
1205
1309
  var TOTP = { digits: 6, stepMs: 30000, drift: 1 };
1206
1310
  var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1207
1311
  function mintTotpSecret() {
1208
- return toBase32(randomBytes4(20));
1312
+ return toBase32(randomBytes5(20));
1209
1313
  }
1210
1314
  function toBase32(bytes) {
1211
1315
  let bits = 0;
@@ -1244,7 +1348,7 @@ var stepAt = (now) => Math.floor(now.getTime() / TOTP.stepMs);
1244
1348
  function codeAt(secret, step) {
1245
1349
  const counter = Buffer.alloc(8);
1246
1350
  counter.writeBigUInt64BE(BigInt(step));
1247
- const digest = createHmac("sha1", secret).update(counter).digest();
1351
+ const digest = createHmac2("sha1", secret).update(counter).digest();
1248
1352
  const offset = digest[19] & 15;
1249
1353
  const binary = digest.readUInt32BE(offset) & 2147483647;
1250
1354
  return String(binary % 10 ** TOTP.digits).padStart(TOTP.digits, "0");
@@ -1343,7 +1447,7 @@ async function keepOnlyLatest(context, userId, kind, secret) {
1343
1447
  await context.store.tokens.spendUserTokens(userId, kind, context.clock.now(), hashSecret(secret));
1344
1448
  }
1345
1449
  async function finishEmailSignIn(context, type, finish, signIn) {
1346
- const { user, token, where, noun } = signIn;
1450
+ const { user, token, where, noun, device } = signIn;
1347
1451
  refuseStale(type, user, token, where, noun);
1348
1452
  if (!user.active) {
1349
1453
  throw new UserInactiveError2(`${where}: the user is inactive`, {
@@ -1351,9 +1455,10 @@ async function finishEmailSignIn(context, type, finish, signIn) {
1351
1455
  userType: type.name
1352
1456
  });
1353
1457
  }
1354
- if (user.emailVerifiedAt !== null)
1355
- return finish(user, where);
1356
- return finish(await proveFirstEmail(context, type, user, where), where);
1458
+ const proved = user.emailVerifiedAt !== null ? user : await proveFirstEmail(context, type, user, where);
1459
+ const result = await finish(proved, where, device);
1460
+ await reportNewDevice(context, result);
1461
+ return result;
1357
1462
  }
1358
1463
 
1359
1464
  // src/auth/magic-link.ts
@@ -1362,8 +1467,10 @@ function magicLinkFlows(context, type, at, finish) {
1362
1467
  async request(email) {
1363
1468
  return requestLink(context, type, String(email));
1364
1469
  },
1365
- async confirm(token) {
1366
- return confirmLink(context, type, finish, String(token), at("magicLink.confirm"));
1470
+ async confirm(token, options) {
1471
+ const where = at("magicLink.confirm");
1472
+ const device = deviceHint(context, options, where);
1473
+ return confirmLink(context, type, finish, String(token), where, device);
1367
1474
  }
1368
1475
  };
1369
1476
  }
@@ -1381,46 +1488,23 @@ async function requestLink(context, type, email) {
1381
1488
  await keepOnlyLatest(context, record.id, "magicLink", secret);
1382
1489
  return { token: secret, email: address, expiresAt, user: toUser(record) };
1383
1490
  }
1384
- async function confirmLink(context, type, finish, secret, where) {
1491
+ async function confirmLink(context, type, finish, secret, where, device) {
1385
1492
  const { token, user } = await redeemEmailToken(context, type, "magicLink", secret, where);
1386
1493
  return finishEmailSignIn(context, type, finish, {
1387
1494
  user,
1388
1495
  token,
1389
1496
  where,
1390
- noun: "token"
1497
+ noun: "token",
1498
+ device
1391
1499
  });
1392
1500
  }
1393
1501
 
1394
- // src/auth/derived-keys.ts
1395
- import { createHmac as createHmac2, hkdfSync } from "node:crypto";
1396
- var derived = new WeakMap;
1397
- function derivedKey(sealer, purpose, keyId = sealer.sealWith) {
1398
- let keys = derived.get(sealer);
1399
- if (keys === undefined) {
1400
- keys = new Map;
1401
- derived.set(sealer, keys);
1402
- }
1403
- const name = `${purpose}\x00${keyId}`;
1404
- let key = keys.get(name);
1405
- if (key === undefined) {
1406
- const secret = sealer.keys.get(keyId);
1407
- if (secret === undefined)
1408
- return;
1409
- key = Buffer.from(hkdfSync("sha256", secret, Buffer.alloc(0), purpose, 32));
1410
- keys.set(name, key);
1411
- }
1412
- return key;
1413
- }
1414
- function keyedHash(key, parts) {
1415
- return createHmac2("sha256", key).update(parts.join("\x00")).digest();
1416
- }
1417
-
1418
1502
  // src/auth/sign-in-attempts.ts
1419
- var PURPOSE = "janus/sign-in/attempts/v1";
1420
- var UNSEALED = Buffer.from(PURPOSE);
1503
+ var PURPOSE2 = "janus/sign-in/attempts/v1";
1504
+ var UNSEALED = Buffer.from(PURPOSE2);
1421
1505
  function counterKey(context) {
1422
1506
  const sealer = context.config.secondFactor?.sealer;
1423
- return sealer === undefined ? UNSEALED : derivedKey(sealer, PURPOSE);
1507
+ return sealer === undefined ? UNSEALED : derivedKey(sealer, PURPOSE2);
1424
1508
  }
1425
1509
  var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
1426
1510
  async function countLogin(context, type, login, where) {
@@ -1515,13 +1599,14 @@ function challengeFlows(context, type, at) {
1515
1599
  userId: record.id
1516
1600
  };
1517
1601
  },
1518
- async confirm(challenge, code) {
1519
- return confirmChallenge(context, type, challenge, code, at);
1602
+ async confirm(challenge, code, options) {
1603
+ return confirmChallenge(context, type, challenge, code, at, options);
1520
1604
  }
1521
1605
  };
1522
1606
  }
1523
- async function confirmChallenge(context, type, challenge, code, at) {
1607
+ async function confirmChallenge(context, type, challenge, code, at, options) {
1524
1608
  const where = at("secondFactor.confirm");
1609
+ const device = deviceHint(context, options, where);
1525
1610
  const opened = await openChallenge(context, type, challenge, where);
1526
1611
  const { record } = opened;
1527
1612
  const now = context.clock.now();
@@ -1531,7 +1616,9 @@ async function confirmChallenge(context, type, challenge, code, at) {
1531
1616
  const written = await context.store.users.updateUser(record.id, { secondFactor: accepted, updatedAt: now }, record.version);
1532
1617
  await spendChallenge(context, opened.secret, where);
1533
1618
  await restartSignInCount(context, type, written, where);
1534
- return openSession(context, type, written);
1619
+ const signedIn = await openSession(context, type, written, device);
1620
+ await reportNewDevice(context, signedIn);
1621
+ return signedIn;
1535
1622
  }
1536
1623
 
1537
1624
  // src/auth/second-factor/codes-left.ts
@@ -1542,12 +1629,12 @@ async function recoveryCodesLeftOf(context, type, user, where) {
1542
1629
  }
1543
1630
 
1544
1631
  // src/auth/second-factor/recovery-codes.ts
1545
- import { randomInt as randomInt2, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
1632
+ import { randomInt as randomInt2, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
1546
1633
  var RECOVERY_CODES = 10;
1547
1634
  var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
1548
1635
  var LENGTH = 10;
1549
1636
  var SHAPE = /^[0-9a-hjkmnp-tv-z]{10}$/;
1550
- var PURPOSE2 = "janus/second-factor/recovery-codes/v1";
1637
+ var PURPOSE3 = "janus/second-factor/recovery-codes/v1";
1551
1638
  function mintRecoveryCodes() {
1552
1639
  return Array.from({ length: RECOVERY_CODES }, () => {
1553
1640
  let code = "";
@@ -1561,7 +1648,7 @@ function readRecoveryCode(typed) {
1561
1648
  const code = typed.toLowerCase().replace(/[\s-]/g, "").replace(/o/g, "0").replace(/[il]/g, "1");
1562
1649
  return SHAPE.test(code) ? code : null;
1563
1650
  }
1564
- var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE2, keyId);
1651
+ var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE3, keyId);
1565
1652
  var mac = (key, userId, code) => keyedHash(key, [userId, code]);
1566
1653
  function hashRecoveryCode(sealer, userId, code) {
1567
1654
  const read = readRecoveryCode(code) ?? code;
@@ -1584,7 +1671,7 @@ function findRecoveryCode(sealer, userId, hashes, typed, where) {
1584
1671
  return;
1585
1672
  const held = Buffer.from(body, "base64url");
1586
1673
  const given = mac(key, userId, code);
1587
- if (held.length === given.length && timingSafeEqual3(held, given)) {
1674
+ if (held.length === given.length && timingSafeEqual4(held, given)) {
1588
1675
  found = found === -1 ? index : found;
1589
1676
  }
1590
1677
  });
@@ -1672,7 +1759,7 @@ function accountOf(type, record, where) {
1672
1759
  // src/auth/second-factor/app-code-attempts.ts
1673
1760
  var APP_CODE_WINDOW_MS = 15 * 60000;
1674
1761
  var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
1675
- var PURPOSE3 = "janus/second-factor/regenerate-attempts/v1";
1762
+ var PURPOSE4 = "janus/second-factor/regenerate-attempts/v1";
1676
1763
  function linkHash(sealer, record, window, link) {
1677
1764
  const parts = [
1678
1765
  record.id,
@@ -1680,7 +1767,7 @@ function linkHash(sealer, record, window, link) {
1680
1767
  String(record.secondFactor?.lastStep ?? ""),
1681
1768
  String(link)
1682
1769
  ];
1683
- const key = derivedKey(sealer, PURPOSE3);
1770
+ const key = derivedKey(sealer, PURPOSE4);
1684
1771
  return hashSecret(keyedHash(key, parts).toString("base64url"));
1685
1772
  }
1686
1773
  async function countAppCodeAttempt(context, sealer, record, where) {
@@ -1750,9 +1837,11 @@ function lifecycleFlows(context, type, at) {
1750
1837
  }
1751
1838
 
1752
1839
  // src/auth/second-factor/recovery.ts
1753
- async function recoverWithCode(context, type, challenge, code, at) {
1840
+ async function recoverWithCode(context, type, given, at) {
1754
1841
  const where = at("secondFactor.recover");
1755
- const opened = await openChallenge(context, type, challenge, where);
1842
+ const device = deviceHint(context, given.options, where);
1843
+ const { code } = given;
1844
+ const opened = await openChallenge(context, type, given.challenge, where);
1756
1845
  const { record } = opened;
1757
1846
  const factor = record.secondFactor;
1758
1847
  const found = findRecoveryCode(opened.configured.sealer, record.id, factor.recoveryCodes, String(code), where);
@@ -1763,14 +1852,16 @@ async function recoverWithCode(context, type, challenge, code, at) {
1763
1852
  secondFactor: { ...factor, recoveryCodes },
1764
1853
  updatedAt: context.clock.now()
1765
1854
  }, record.version);
1855
+ let signedIn;
1766
1856
  try {
1767
1857
  await spendChallenge(context, opened.secret, where);
1768
1858
  await restartSignInCount(context, type, written, where);
1769
- const signedIn = await openSession(context, type, written);
1770
- return { ...signedIn, recoveryCodesLeft: recoveryCodes.length };
1859
+ signedIn = await openSession(context, type, written, device);
1771
1860
  } finally {
1772
1861
  await emit(context, "user.recoveryCodeUsed", written, written.updatedAt);
1773
1862
  }
1863
+ await reportNewDevice(context, signedIn);
1864
+ return { ...signedIn, recoveryCodesLeft: recoveryCodes.length };
1774
1865
  }
1775
1866
 
1776
1867
  // src/auth/second-factor/flows.ts
@@ -1779,12 +1870,12 @@ function secondFactorFlows(context, type, at) {
1779
1870
  const api = {
1780
1871
  ...lifecycleFlows(context, type, at),
1781
1872
  confirm: challenges.confirm,
1782
- recover: (challenge, code) => recoverWithCode(context, type, challenge, code, at),
1873
+ recover: (challenge, code, options) => recoverWithCode(context, type, { challenge, code, options }, at),
1783
1874
  recoveryCodesLeft: (user) => recoveryCodesLeftOf(context, type, user, at("secondFactor.recoveryCodesLeft"))
1784
1875
  };
1785
1876
  return {
1786
1877
  api,
1787
- finish: (record, where) => isActive(record.secondFactor) ? challenges.issue(record, where) : openSession(context, type, record)
1878
+ finish: (record, where, device) => isActive(record.secondFactor) ? challenges.issue(record, where) : openSession(context, type, record, device)
1788
1879
  };
1789
1880
  }
1790
1881
 
@@ -1794,8 +1885,8 @@ function signInCodeFlows(context, type, at, finish) {
1794
1885
  async request(email) {
1795
1886
  return requestCode(context, type, String(email));
1796
1887
  },
1797
- async confirm(challenge, code) {
1798
- return confirmCode(context, type, finish, challenge, code, at);
1888
+ async confirm(challenge, code, options) {
1889
+ return confirmCode(context, type, finish, { challenge, code, options }, at);
1799
1890
  }
1800
1891
  };
1801
1892
  }
@@ -1818,9 +1909,11 @@ async function requestCode(context, type, email) {
1818
1909
  user: toUser(record)
1819
1910
  };
1820
1911
  }
1821
- async function confirmCode(context, type, finish, challenge, code, at) {
1912
+ async function confirmCode(context, type, finish, given, at) {
1822
1913
  const where = at("signInCode.confirm");
1823
- const secret = String(challenge);
1914
+ const device = deviceHint(context, given.options, where);
1915
+ const secret = String(given.challenge);
1916
+ const code = given.code;
1824
1917
  const { token, attemptsLeft } = await countCodeAttempt(context, secret, "signInCode", where, type.name);
1825
1918
  const user = await findRecord(context, token.userId, type.name);
1826
1919
  if (user === null) {
@@ -1837,7 +1930,8 @@ async function confirmCode(context, type, finish, challenge, code, at) {
1837
1930
  user,
1838
1931
  token,
1839
1932
  where,
1840
- noun: "code"
1933
+ noun: "code",
1934
+ device
1841
1935
  });
1842
1936
  }
1843
1937
 
@@ -2037,7 +2131,8 @@ async function insert(context, type, input, where) {
2037
2131
  }
2038
2132
 
2039
2133
  // src/auth/users/sign-in.ts
2040
- async function signIn(context, type, finish, input, where) {
2134
+ async function signIn(context, type, finish, input, call) {
2135
+ const { where, device } = call;
2041
2136
  const rule = passwordRule(type, where);
2042
2137
  const hasher = requireHasher(context, where);
2043
2138
  const login = input?.[rule.login];
@@ -2065,27 +2160,31 @@ async function signIn(context, type, finish, input, where) {
2065
2160
  await restartSignInCount(context, type, record, where);
2066
2161
  }
2067
2162
  const verified = await rehashed(context, record, String(password));
2068
- const result = await finish(verified, where);
2163
+ const result = await finish(verified, where, device);
2069
2164
  if (!await heldByPassword(context, type, verified, String(password), result, where)) {
2070
2165
  throw refuse("wrongPassword");
2071
2166
  }
2167
+ await reportNewDevice(context, result);
2072
2168
  return result;
2073
2169
  }
2074
2170
 
2075
2171
  // src/auth/users/password-flows.ts
2076
2172
  function passwordFlows(context, type, at, finish) {
2077
2173
  return {
2078
- async signUp(input) {
2174
+ async signUp(input, options) {
2079
2175
  const where = at("signUp");
2176
+ const device = deviceHint(context, options, where);
2080
2177
  passwordRule(type, where);
2081
2178
  if (input?.password === undefined) {
2082
2179
  checkPassword(type, undefined, where);
2083
2180
  }
2084
2181
  const record = await insert(context, type, input, where);
2085
- return openSession(context, type, record);
2182
+ return openSession(context, type, record, device, true);
2086
2183
  },
2087
- async signIn(input) {
2088
- return signIn(context, type, finish, input, at("signIn"));
2184
+ async signIn(input, options) {
2185
+ const where = at("signIn");
2186
+ const device = deviceHint(context, options, where);
2187
+ return signIn(context, type, finish, input, { where, device });
2089
2188
  },
2090
2189
  async findByLogin(login) {
2091
2190
  const rule = passwordRule(type, at("findByLogin"));
@@ -2300,5 +2399,5 @@ export {
2300
2399
  systemClock
2301
2400
  };
2302
2401
 
2303
- //# debugId=BC1254710D035B0264756E2164756E21
2402
+ //# debugId=735288C311F315A564756E2164756E21
2304
2403
  //# sourceMappingURL=index.js.map