@nxgt/janus 0.17.2 → 0.19.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 (93) hide show
  1. package/README.md +107 -27
  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 +31 -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-throttle.d.ts +30 -0
  8. package/dist/auth/config/resolve-throttle.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/config/warn-throttle-window.d.ts +14 -0
  12. package/dist/auth/config/warn-throttle-window.d.ts.map +1 -0
  13. package/dist/auth/counter-key.d.ts +19 -0
  14. package/dist/auth/counter-key.d.ts.map +1 -0
  15. package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
  16. package/dist/auth/email-flows/verify-email.d.ts.map +1 -1
  17. package/dist/auth/index.d.ts +1 -1
  18. package/dist/auth/index.d.ts.map +1 -1
  19. package/dist/auth/janus.d.ts.map +1 -1
  20. package/dist/auth/magic-link.d.ts.map +1 -1
  21. package/dist/auth/mail-requests.d.ts +51 -0
  22. package/dist/auth/mail-requests.d.ts.map +1 -0
  23. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  24. package/dist/auth/port/memory/users.d.ts.map +1 -1
  25. package/dist/auth/port/types/stores.d.ts +5 -0
  26. package/dist/auth/port/types/stores.d.ts.map +1 -1
  27. package/dist/auth/port/types/users.d.ts +17 -0
  28. package/dist/auth/port/types/users.d.ts.map +1 -1
  29. package/dist/auth/sign-in-attempts.d.ts +3 -6
  30. package/dist/auth/sign-in-attempts.d.ts.map +1 -1
  31. package/dist/auth/sign-in-code.d.ts +3 -2
  32. package/dist/auth/sign-in-code.d.ts.map +1 -1
  33. package/dist/auth/step-up/request.d.ts.map +1 -1
  34. package/dist/auth/types/email-flows.d.ts +16 -6
  35. package/dist/auth/types/email-flows.d.ts.map +1 -1
  36. package/dist/auth/types/magic-link.d.ts +4 -2
  37. package/dist/auth/types/magic-link.d.ts.map +1 -1
  38. package/dist/auth/types/step-up.d.ts +4 -1
  39. package/dist/auth/types/step-up.d.ts.map +1 -1
  40. package/dist/auth/types/user-type.d.ts +15 -0
  41. package/dist/auth/types/user-type.d.ts.map +1 -1
  42. package/dist/auth/users/find-many.d.ts +29 -0
  43. package/dist/auth/users/find-many.d.ts.map +1 -0
  44. package/dist/auth/users/record-flows.d.ts.map +1 -1
  45. package/dist/chunks/{index-6fxq0kzf.js → index-4khsm7mw.js} +2 -2
  46. package/dist/chunks/{index-7xe5x3eg.js → index-6bjfv4xv.js} +9 -3
  47. package/dist/chunks/{index-7xe5x3eg.js.map → index-6bjfv4xv.js.map} +3 -3
  48. package/dist/chunks/{index-k136102s.js → index-cntabs9d.js} +2 -2
  49. package/dist/chunks/{index-ztng6018.js → index-kppkcbn5.js} +10 -3
  50. package/dist/chunks/{index-ztng6018.js.map → index-kppkcbn5.js.map} +4 -4
  51. package/dist/conformance/cases/outage.d.ts.map +1 -1
  52. package/dist/conformance/cases/users/batch.d.ts +8 -0
  53. package/dist/conformance/cases/users/batch.d.ts.map +1 -0
  54. package/dist/conformance/cases/users/index.d.ts.map +1 -1
  55. package/dist/conformance/describe.d.ts +2 -1
  56. package/dist/conformance/describe.d.ts.map +1 -1
  57. package/dist/conformance/index.d.ts +1 -1
  58. package/dist/conformance/index.d.ts.map +1 -1
  59. package/dist/conformance/index.js +106 -46
  60. package/dist/conformance/index.js.map +7 -6
  61. package/dist/conformance/types.d.ts +10 -2
  62. package/dist/conformance/types.d.ts.map +1 -1
  63. package/dist/errors/janus-error/codes.d.ts +10 -0
  64. package/dist/errors/janus-error/codes.d.ts.map +1 -1
  65. package/dist/errors/janus-error/index.d.ts +1 -1
  66. package/dist/errors/janus-error/index.d.ts.map +1 -1
  67. package/dist/errors/janus-error/options.d.ts +4 -3
  68. package/dist/errors/janus-error/options.d.ts.map +1 -1
  69. package/dist/errors/janus-error/refusals.d.ts +8 -0
  70. package/dist/errors/janus-error/refusals.d.ts.map +1 -1
  71. package/dist/errors/janus-error/status.d.ts +1 -1
  72. package/dist/errors/janus-error/status.d.ts.map +1 -1
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +171 -30
  76. package/dist/index.js.map +18 -14
  77. package/dist/permissions/index.js +3 -3
  78. package/docs/README.md +1 -1
  79. package/docs/guide/adapters.md +44 -4
  80. package/docs/guide/email-flows.md +164 -4
  81. package/docs/guide/errors.md +12 -3
  82. package/docs/guide/magic-link.md +41 -5
  83. package/docs/guide/passwords.md +2 -1
  84. package/docs/guide/sign-in-code.md +46 -7
  85. package/docs/guide/step-up.md +38 -7
  86. package/docs/guide/users.md +49 -0
  87. package/docs/roadmap.md +26 -15
  88. package/docs/troubleshooting.md +127 -5
  89. package/package.json +1 -1
  90. package/dist/auth/config/resolve-sign-in.d.ts +0 -5
  91. package/dist/auth/config/resolve-sign-in.d.ts.map +0 -1
  92. /package/dist/chunks/{index-6fxq0kzf.js.map → index-4khsm7mw.js.map} +0 -0
  93. /package/dist/chunks/{index-k136102s.js.map → index-cntabs9d.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * `@nxgt/janus/conformance` fails an adapter that breaks it.
22
22
  */
23
23
  export * from './auth/index';
24
- export { CredentialError, type CredentialRefusal, InvalidCursorError, type Issue, JanusError, type JanusErrorCode, type JanusErrorOptions, type JanusErrorStatus, NotFoundError, PermissionDepthError, SecondFactorError, StepUpRequiredError, StoreConflict, StoreFailure, statusOf, TokenError, UnsupportedError, UserInactiveError, UserInvalidError, } from './errors/janus-error';
24
+ export { CredentialError, type CredentialRefusal, InvalidCursorError, type Issue, JanusError, type JanusErrorCode, type JanusErrorOptions, type JanusErrorStatus, MailThrottledError, NotFoundError, PermissionDepthError, SecondFactorError, StepUpRequiredError, StoreConflict, StoreFailure, statusOf, TokenError, UnsupportedError, UserInactiveError, UserInvalidError, } from './errors/janus-error';
25
25
  export { type Id, isId, mintedAt, mintId, } from './ids/id';
26
26
  export { type CursorPage, DEFAULT_PAGE_SIZE, invalidCursor, MAX_PAGE_SIZE, pageLimit, } from './pagination/cursor-page';
27
27
  export { formatEntity, formatSubject, formatTuple, parseSubject, parseTuple, } from './subjects/notation';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,cAAc,cAAc,CAAC;AAC7B,OAAO,EACN,eAAe,EACf,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,KAAK,EACV,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,KAAK,EAAE,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,GACN,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,KAAK,UAAU,EACf,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,SAAS,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,KAAK,MAAM,EACX,OAAO,EACP,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,EACL,SAAS,GACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,KAAK,QAAQ,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,cAAc,cAAc,CAAC;AAC7B,OAAO,EACN,eAAe,EACf,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,KAAK,EACV,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,KAAK,EAAE,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,GACN,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,KAAK,UAAU,EACf,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,SAAS,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,KAAK,MAAM,EACX,OAAO,EACP,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,EACL,SAAS,GACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,KAAK,QAAQ,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
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-6fxq0kzf.js";
10
+ } from "./chunks/index-4khsm7mw.js";
11
11
  import {
12
12
  mintId2,
13
13
  isId2,
14
14
  mintedAt2,
15
15
  createMemoryStores2
16
- } from "./chunks/index-7xe5x3eg.js";
16
+ } from "./chunks/index-6bjfv4xv.js";
17
17
  import {
18
18
  JanusError2,
19
19
  UserInvalidError2,
@@ -22,6 +22,7 @@ import {
22
22
  TokenError2,
23
23
  SecondFactorError2,
24
24
  StepUpRequiredError2,
25
+ MailThrottledError2,
25
26
  InvalidCursorError2,
26
27
  UnsupportedError2,
27
28
  PermissionDepthError2,
@@ -38,7 +39,7 @@ import {
38
39
  formatTuple2,
39
40
  parseTuple2,
40
41
  parseSubject2
41
- } from "./chunks/index-ztng6018.js";
42
+ } from "./chunks/index-kppkcbn5.js";
42
43
 
43
44
  // src/auth/hashers.ts
44
45
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
@@ -303,25 +304,32 @@ function resolveSecondFactor(config, where) {
303
304
  };
304
305
  }
305
306
 
306
- // src/auth/config/resolve-sign-in.ts
307
+ // src/auth/config/resolve-throttle.ts
307
308
  function resolveSignInThrottle(config, where) {
309
+ return resolveThrottle(config, { option: "signIn", attempts: 10, window: "15m" }, where);
310
+ }
311
+ function resolveMailThrottle(config, where) {
312
+ return resolveThrottle(config, { option: "mail", attempts: 5, window: "10m" }, where);
313
+ }
314
+ function resolveThrottle(config, defaults, where) {
315
+ const { option } = defaults;
308
316
  if (config !== undefined && (typeof config !== "object" || config === null)) {
309
- throw new TypeError(`${where}: signIn must be an object — { throttle }`);
317
+ throw new TypeError(`${where}: ${option} must be an object — { throttle }`);
310
318
  }
311
319
  const throttle = config?.throttle ?? {};
312
320
  if (throttle === false)
313
321
  return null;
314
- const at = `${where}: signIn.throttle`;
322
+ const at = `${where}: ${option}.throttle`;
315
323
  if (typeof throttle !== "object" || throttle === null) {
316
324
  throw new TypeError(`${at} must be { attempts, window }, or false to count nothing`);
317
325
  }
318
- const attempts = throttle.attempts ?? 10;
326
+ const attempts = throttle.attempts ?? defaults.attempts;
319
327
  if (!Number.isSafeInteger(attempts) || attempts < 1) {
320
328
  throw new TypeError(`${at}.attempts must be a whole number above zero`);
321
329
  }
322
330
  return {
323
331
  attempts,
324
- windowMs: parseDuration(throttle.window ?? "15m", `${at}.window`)
332
+ windowMs: parseDuration(throttle.window ?? defaults.window, `${at}.window`)
325
333
  };
326
334
  }
327
335
 
@@ -372,6 +380,7 @@ function resolveConfig(config, where) {
372
380
  tokenTtlMs: resolveTokenTtls(config, where),
373
381
  cookie: resolveCookie(config.cookie ?? {}, where),
374
382
  signInThrottle: resolveSignInThrottle(config.signIn, where),
383
+ mailThrottle: resolveMailThrottle(config.mail, where),
375
384
  secondFactor: resolveSecondFactor(config.secondFactor, where),
376
385
  devices: resolveDevices(config.devices, where)
377
386
  };
@@ -419,6 +428,46 @@ function resolveTokenTtls(config, where) {
419
428
  stepUp: parseDuration(config.tokens?.stepUp ?? "10m", `${where}: tokens.stepUp`)
420
429
  };
421
430
  }
431
+ // src/auth/config/warn-throttle-window.ts
432
+ var FLOWS = [
433
+ "magicLink",
434
+ "signInCode",
435
+ "resetPassword",
436
+ "verifyEmail",
437
+ "stepUp"
438
+ ];
439
+ var UNITS2 = [
440
+ ["d", 86400000],
441
+ ["h", 3600000],
442
+ ["m", 60000],
443
+ ["s", 1000]
444
+ ];
445
+ function show(ms) {
446
+ const unit = UNITS2.find(([, size]) => ms % size === 0);
447
+ return unit === undefined ? `${ms}ms` : `${ms / unit[1]}${unit[0]}`;
448
+ }
449
+ function throttleWindowMessage(resolved, where) {
450
+ const throttle = resolved.mailThrottle;
451
+ if (throttle === null)
452
+ return null;
453
+ const window = show(throttle.windowMs);
454
+ const shorter = FLOWS.filter((flow) => resolved.tokenTtlMs[flow] < throttle.windowMs);
455
+ if (shorter.length === 0)
456
+ return null;
457
+ const shortest = Math.min(...shorter.map((flow) => resolved.tokenTtlMs[flow]));
458
+ const parts = shorter.map((flow) => `tokens.${flow} is ${show(resolved.tokenTtlMs[flow])}, shorter than mail.throttle.window, ${window}`);
459
+ return `${where}: ${parts.join("; ")} — past the limit the last ${shorter.length === 1 ? "token" : "tokens"} sent can expire before the refusal ends. Set mail.throttle.window to ${show(shortest)} or less, or raise ${shorter.map((flow) => `tokens.${flow}`).join(", ")} to ${window} or more.`;
460
+ }
461
+ function warnThrottleWindow(resolved, where) {
462
+ const message = throttleWindowMessage(resolved, where);
463
+ if (message === null)
464
+ return;
465
+ if (typeof process === "undefined" || typeof process.emitWarning !== "function") {
466
+ return;
467
+ }
468
+ process.emitWarning(message, { code: "JANUS_THROTTLE_WINDOW" });
469
+ }
470
+
422
471
  // src/auth/context/create-context.ts
423
472
  function createContext(config, store, relations, capabilities, clock, hasher, verifiers, events = null) {
424
473
  let dummy = null;
@@ -725,12 +774,18 @@ function assertStores(stores, where) {
725
774
  }
726
775
  }
727
776
  }
728
- const sessions = stores.sessions;
729
- const collect = sessions["deleteExpiredSessions"];
730
- if (collect !== undefined && typeof collect !== "function") {
731
- throw new TypeError(`${where}: store.sessions.deleteExpiredSessions must be a function or absent`);
777
+ const slots = stores;
778
+ return {
779
+ collectExpired: optional(slots, "sessions", "deleteExpiredSessions", where),
780
+ findUsers: optional(slots, "users", "findUsers", where)
781
+ };
782
+ }
783
+ function optional(stores, slot, method, where) {
784
+ const found = stores[slot][method];
785
+ if (found !== undefined && typeof found !== "function") {
786
+ throw new TypeError(`${where}: store.${slot}.${method} must be a function or absent`);
732
787
  }
733
- return { collectExpired: collect !== undefined };
788
+ return found !== undefined;
734
789
  }
735
790
 
736
791
  // src/auth/devices/token.ts
@@ -1008,6 +1063,13 @@ function sharedApi(context) {
1008
1063
  types: [...config.types.keys()]
1009
1064
  };
1010
1065
  }
1066
+ // src/auth/counter-key.ts
1067
+ function counterKey(context, purpose) {
1068
+ const sealer = context.config.secondFactor?.sealer;
1069
+ return sealer === undefined ? Buffer.from(purpose) : derivedKey(sealer, purpose);
1070
+ }
1071
+ var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
1072
+
1011
1073
  // src/auth/one-time/codes.ts
1012
1074
  import { timingSafeEqual as timingSafeEqual3 } from "node:crypto";
1013
1075
 
@@ -1173,6 +1235,38 @@ async function insertLink(links, tokenHash) {
1173
1235
  return inserted;
1174
1236
  throw links.counting.vanished();
1175
1237
  }
1238
+ // src/auth/mail-requests.ts
1239
+ var PURPOSE2 = "janus/mail/requests/v1";
1240
+ async function countMailByAddress(context, type, flow, email, where) {
1241
+ await countMail(context, type, flow, { address: normalizeEmail(email) }, where);
1242
+ }
1243
+ async function countMailByUser(context, type, flow, userId, where) {
1244
+ await countMail(context, type, flow, { userId }, where);
1245
+ }
1246
+ async function countMail(context, type, flow, counted, where) {
1247
+ const throttle = context.config.mailThrottle;
1248
+ if (throttle === null)
1249
+ return;
1250
+ const key = counterKey(context, PURPOSE2);
1251
+ const subject = "address" in counted ? keyed(key, [flow, "address", counted.address]) : keyed(key, [flow, "user", counted.userId]);
1252
+ const { attempts, endsAt } = await countInWindow(context, {
1253
+ windowMs: throttle.windowMs,
1254
+ spent: "carry",
1255
+ userId: subject,
1256
+ linkHash: (window, link) => hashSecret(keyed(key, [subject, String(window), String(link)])),
1257
+ vanished: () => new StoreFailure2(`${where}: the store dropped the requests it had just counted`, { slot: "tokens", operation: "countAttempt" })
1258
+ });
1259
+ if (attempts <= throttle.attempts)
1260
+ return;
1261
+ const now = context.clock.now().getTime();
1262
+ const byUser = "userId" in counted;
1263
+ throw new MailThrottledError2(`${where}: too many e-mails asked for this ${byUser ? "user" : "address"} — the last one sent still works; use it, or wait for the next window`, {
1264
+ userType: type.name,
1265
+ ...byUser ? { userId: counted.userId } : {},
1266
+ retryAfter: Math.max(1, Math.ceil((endsAt - now) / 1000))
1267
+ });
1268
+ }
1269
+
1176
1270
  // src/auth/password-written.ts
1177
1271
  async function endWhatThePasswordOpened(context, userId) {
1178
1272
  const now = context.clock.now();
@@ -1234,6 +1328,7 @@ function resetPasswordFlows(context, type, at) {
1234
1328
  async request(email) {
1235
1329
  const where = at("resetPassword.request");
1236
1330
  passwordRule(type, where);
1331
+ await countMailByAddress(context, type, "resetPassword", String(email), where);
1237
1332
  const record = await holderOfEmail(context, type, String(email));
1238
1333
  if (record === null)
1239
1334
  return null;
@@ -1278,6 +1373,7 @@ function verifyEmailFlows(context, type, at) {
1278
1373
  async send(user) {
1279
1374
  const where = at("verifyEmail.send");
1280
1375
  const record = await getRecord(context, idOf(user), type.name, where);
1376
+ await countMailByUser(context, type, "verifyEmail", record.id, where);
1281
1377
  return issueEmailToken(context, type, "verifyEmail", record, where);
1282
1378
  },
1283
1379
  async confirm(secret) {
@@ -1465,7 +1561,7 @@ async function finishEmailSignIn(context, type, finish, signIn) {
1465
1561
  function magicLinkFlows(context, type, at, finish) {
1466
1562
  return {
1467
1563
  async request(email) {
1468
- return requestLink(context, type, String(email));
1564
+ return requestLink(context, type, String(email), at("magicLink.request"));
1469
1565
  },
1470
1566
  async confirm(token, options) {
1471
1567
  const where = at("magicLink.confirm");
@@ -1474,7 +1570,8 @@ function magicLinkFlows(context, type, at, finish) {
1474
1570
  }
1475
1571
  };
1476
1572
  }
1477
- async function requestLink(context, type, email) {
1573
+ async function requestLink(context, type, email, where) {
1574
+ await countMailByAddress(context, type, "magicLink", email, where);
1478
1575
  const record = await signInHolder(context, type, email);
1479
1576
  if (record === null)
1480
1577
  return null;
@@ -1500,18 +1597,12 @@ async function confirmLink(context, type, finish, secret, where, device) {
1500
1597
  }
1501
1598
 
1502
1599
  // src/auth/sign-in-attempts.ts
1503
- var PURPOSE2 = "janus/sign-in/attempts/v1";
1504
- var UNSEALED = Buffer.from(PURPOSE2);
1505
- function counterKey(context) {
1506
- const sealer = context.config.secondFactor?.sealer;
1507
- return sealer === undefined ? UNSEALED : derivedKey(sealer, PURPOSE2);
1508
- }
1509
- var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
1600
+ var PURPOSE3 = "janus/sign-in/attempts/v1";
1510
1601
  async function countLogin(context, type, login, where) {
1511
1602
  const throttle = context.config.signInThrottle;
1512
1603
  if (throttle === null)
1513
1604
  return null;
1514
- const key = counterKey(context);
1605
+ const key = counterKey(context, PURPOSE3);
1515
1606
  const loginKey = keyed(key, [type.name, login]);
1516
1607
  return countInWindow(context, {
1517
1608
  windowMs: throttle.windowMs,
@@ -1634,7 +1725,7 @@ var RECOVERY_CODES = 10;
1634
1725
  var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
1635
1726
  var LENGTH = 10;
1636
1727
  var SHAPE = /^[0-9a-hjkmnp-tv-z]{10}$/;
1637
- var PURPOSE3 = "janus/second-factor/recovery-codes/v1";
1728
+ var PURPOSE4 = "janus/second-factor/recovery-codes/v1";
1638
1729
  function mintRecoveryCodes() {
1639
1730
  return Array.from({ length: RECOVERY_CODES }, () => {
1640
1731
  let code = "";
@@ -1648,7 +1739,7 @@ function readRecoveryCode(typed) {
1648
1739
  const code = typed.toLowerCase().replace(/[\s-]/g, "").replace(/o/g, "0").replace(/[il]/g, "1");
1649
1740
  return SHAPE.test(code) ? code : null;
1650
1741
  }
1651
- var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE3, keyId);
1742
+ var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE4, keyId);
1652
1743
  var mac = (key, userId, code) => keyedHash(key, [userId, code]);
1653
1744
  function hashRecoveryCode(sealer, userId, code) {
1654
1745
  const read = readRecoveryCode(code) ?? code;
@@ -1759,7 +1850,7 @@ function accountOf(type, record, where) {
1759
1850
  // src/auth/second-factor/app-code-attempts.ts
1760
1851
  var APP_CODE_WINDOW_MS = 15 * 60000;
1761
1852
  var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
1762
- var PURPOSE4 = "janus/second-factor/regenerate-attempts/v1";
1853
+ var PURPOSE5 = "janus/second-factor/regenerate-attempts/v1";
1763
1854
  function linkHash(sealer, record, window, link) {
1764
1855
  const parts = [
1765
1856
  record.id,
@@ -1767,7 +1858,7 @@ function linkHash(sealer, record, window, link) {
1767
1858
  String(record.secondFactor?.lastStep ?? ""),
1768
1859
  String(link)
1769
1860
  ];
1770
- const key = derivedKey(sealer, PURPOSE4);
1861
+ const key = derivedKey(sealer, PURPOSE5);
1771
1862
  return hashSecret(keyedHash(key, parts).toString("base64url"));
1772
1863
  }
1773
1864
  async function countAppCodeAttempt(context, sealer, record, where) {
@@ -1883,14 +1974,15 @@ function secondFactorFlows(context, type, at) {
1883
1974
  function signInCodeFlows(context, type, at, finish) {
1884
1975
  return {
1885
1976
  async request(email) {
1886
- return requestCode(context, type, String(email));
1977
+ return requestCode(context, type, String(email), at("signInCode.request"));
1887
1978
  },
1888
1979
  async confirm(challenge, code, options) {
1889
1980
  return confirmCode(context, type, finish, { challenge, code, options }, at);
1890
1981
  }
1891
1982
  };
1892
1983
  }
1893
- async function requestCode(context, type, email) {
1984
+ async function requestCode(context, type, email, where) {
1985
+ await countMailByAddress(context, type, "signInCode", email, where);
1894
1986
  const record = await signInHolder(context, type, email);
1895
1987
  if (record === null)
1896
1988
  return null;
@@ -2028,6 +2120,7 @@ async function byEmail(context, type, record, where) {
2028
2120
  operation: where
2029
2121
  });
2030
2122
  }
2123
+ await countMailByUser(context, type, "stepUp", record.id, where);
2031
2124
  const { secret, code, expiresAt } = await issueCode(context, {
2032
2125
  kind: "stepUp",
2033
2126
  userId: record.id,
@@ -2230,6 +2323,48 @@ async function deleteUser(context, type, user) {
2230
2323
  return deleted;
2231
2324
  }
2232
2325
 
2326
+ // src/auth/users/find-many.ts
2327
+ var FIND_MANY_BATCH = 100;
2328
+ var FIND_MANY_CONCURRENCY = 10;
2329
+ async function findManyRecords(context, ids, type, where) {
2330
+ if (!Array.isArray(ids)) {
2331
+ throw new TypeError(`${where}: ids must be an array of user ids`);
2332
+ }
2333
+ const wanted = [
2334
+ ...new Set(ids.filter((id) => typeof id === "string" && isId2(id)))
2335
+ ];
2336
+ const found = new Map;
2337
+ for (let start = 0;start < wanted.length; start += FIND_MANY_BATCH) {
2338
+ const batch = wanted.slice(start, start + FIND_MANY_BATCH);
2339
+ for (const record of await readBatch(context, batch)) {
2340
+ if (record.type === type)
2341
+ found.set(record.id, record);
2342
+ }
2343
+ }
2344
+ return wanted.flatMap((id) => {
2345
+ const record = found.get(id);
2346
+ return record === undefined ? [] : [record];
2347
+ });
2348
+ }
2349
+ async function readBatch(context, ids) {
2350
+ const { users } = context.store;
2351
+ if (context.capabilities.findUsers && users.findUsers !== undefined) {
2352
+ const answer = await users.findUsers(ids);
2353
+ if (!Array.isArray(answer)) {
2354
+ throw new StoreFailure2("users.findUsers answered no list: an absence is a shorter list, so this store forgot to answer", { slot: "users", operation: "findUsers" });
2355
+ }
2356
+ return answer;
2357
+ }
2358
+ const records = [];
2359
+ for (let start = 0;start < ids.length; start += FIND_MANY_CONCURRENCY) {
2360
+ const answers = await Promise.all(ids.slice(start, start + FIND_MANY_CONCURRENCY).map((id) => users.findUser(id)));
2361
+ for (const record of answers)
2362
+ if (record !== null)
2363
+ records.push(record);
2364
+ }
2365
+ return records;
2366
+ }
2367
+
2233
2368
  // src/auth/users/fields-patch.ts
2234
2369
  var normalized = (email) => email === null ? null : normalizeEmail(email);
2235
2370
  function emailChanged(type, before, after) {
@@ -2272,6 +2407,10 @@ function recordFlows(context, type, at) {
2272
2407
  const record = await findRecord(context, id, type.name);
2273
2408
  return record === null ? null : toUser(record);
2274
2409
  },
2410
+ async findMany(ids) {
2411
+ const records = await findManyRecords(context, ids, type.name, at("findMany"));
2412
+ return records.map(toUser);
2413
+ },
2275
2414
  async get(id) {
2276
2415
  return toUser(await getRecord(context, id, type.name, at("get")));
2277
2416
  },
@@ -2320,6 +2459,7 @@ function typeApi(context, type) {
2320
2459
  function janus(config) {
2321
2460
  const where = "janus";
2322
2461
  const resolved = resolveConfig(config, where);
2462
+ warnThrottleWindow(resolved, where);
2323
2463
  const capabilities = assertStores(config.store, where);
2324
2464
  const hasher = config.hasher ?? null;
2325
2465
  const verifiers = [
@@ -2363,6 +2503,7 @@ export {
2363
2503
  InvalidCursorError2 as InvalidCursorError,
2364
2504
  JanusError2 as JanusError,
2365
2505
  MAX_PAGE_SIZE2 as MAX_PAGE_SIZE,
2506
+ MailThrottledError2 as MailThrottledError,
2366
2507
  NotFoundError2 as NotFoundError,
2367
2508
  PermissionDepthError2 as PermissionDepthError,
2368
2509
  SecondFactorError2 as SecondFactorError,
@@ -2399,5 +2540,5 @@ export {
2399
2540
  systemClock
2400
2541
  };
2401
2542
 
2402
- //# debugId=AA27B9CD21C48F3A64756E2164756E21
2543
+ //# debugId=22ACDB89AAF4B56564756E2164756E21
2403
2544
  //# sourceMappingURL=index.js.map