@nxgt/janus 0.18.0 → 0.20.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 (74) hide show
  1. package/README.md +148 -12
  2. package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
  3. package/dist/auth/events.d.ts +3 -3
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/magic-link.d.ts +3 -0
  7. package/dist/auth/magic-link.d.ts.map +1 -1
  8. package/dist/auth/one-time/issue.d.ts +6 -0
  9. package/dist/auth/one-time/issue.d.ts.map +1 -1
  10. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  11. package/dist/auth/port/memory/users.d.ts.map +1 -1
  12. package/dist/auth/port/types/stores.d.ts +5 -0
  13. package/dist/auth/port/types/stores.d.ts.map +1 -1
  14. package/dist/auth/port/types/users.d.ts +17 -0
  15. package/dist/auth/port/types/users.d.ts.map +1 -1
  16. package/dist/auth/prepared.d.ts +21 -0
  17. package/dist/auth/prepared.d.ts.map +1 -0
  18. package/dist/auth/proven-sign-in.d.ts +22 -0
  19. package/dist/auth/proven-sign-in.d.ts.map +1 -0
  20. package/dist/auth/sign-in-code.d.ts +4 -0
  21. package/dist/auth/sign-in-code.d.ts.map +1 -1
  22. package/dist/auth/types/email-flows.d.ts +18 -0
  23. package/dist/auth/types/email-flows.d.ts.map +1 -1
  24. package/dist/auth/types/index.d.ts +2 -0
  25. package/dist/auth/types/index.d.ts.map +1 -1
  26. package/dist/auth/types/janus.d.ts +7 -5
  27. package/dist/auth/types/janus.d.ts.map +1 -1
  28. package/dist/auth/types/magic-link.d.ts +11 -0
  29. package/dist/auth/types/magic-link.d.ts.map +1 -1
  30. package/dist/auth/types/prepared.d.ts +41 -0
  31. package/dist/auth/types/prepared.d.ts.map +1 -0
  32. package/dist/auth/types/proven-sign-in.d.ts +43 -0
  33. package/dist/auth/types/proven-sign-in.d.ts.map +1 -0
  34. package/dist/auth/types/sign-in.d.ts +2 -2
  35. package/dist/auth/types/user-type.d.ts +15 -0
  36. package/dist/auth/types/user-type.d.ts.map +1 -1
  37. package/dist/auth/users/any-type-api.d.ts +2 -2
  38. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  39. package/dist/auth/users/find-many.d.ts +29 -0
  40. package/dist/auth/users/find-many.d.ts.map +1 -0
  41. package/dist/auth/users/record-flows.d.ts.map +1 -1
  42. package/dist/auth/users/type-api.d.ts +2 -2
  43. package/dist/auth/users/type-api.d.ts.map +1 -1
  44. package/dist/chunks/{index-kvjj5xkp.js → index-6bjfv4xv.js} +8 -2
  45. package/dist/chunks/{index-kvjj5xkp.js.map → index-6bjfv4xv.js.map} +3 -3
  46. package/dist/conformance/cases/outage.d.ts.map +1 -1
  47. package/dist/conformance/cases/users/batch.d.ts +8 -0
  48. package/dist/conformance/cases/users/batch.d.ts.map +1 -0
  49. package/dist/conformance/cases/users/index.d.ts.map +1 -1
  50. package/dist/conformance/describe.d.ts +2 -1
  51. package/dist/conformance/describe.d.ts.map +1 -1
  52. package/dist/conformance/index.d.ts +1 -1
  53. package/dist/conformance/index.d.ts.map +1 -1
  54. package/dist/conformance/index.js +104 -44
  55. package/dist/conformance/index.js.map +7 -6
  56. package/dist/conformance/types.d.ts +10 -2
  57. package/dist/conformance/types.d.ts.map +1 -1
  58. package/dist/index.js +150 -23
  59. package/dist/index.js.map +14 -11
  60. package/docs/README.md +1 -1
  61. package/docs/guide/adapters.md +44 -4
  62. package/docs/guide/devices.md +4 -0
  63. package/docs/guide/email-flows.md +105 -0
  64. package/docs/guide/errors.md +2 -2
  65. package/docs/guide/events.md +2 -2
  66. package/docs/guide/magic-link.md +49 -2
  67. package/docs/guide/passwords.md +5 -2
  68. package/docs/guide/second-factor.md +1 -1
  69. package/docs/guide/sessions.md +94 -1
  70. package/docs/guide/sign-in-code.md +54 -2
  71. package/docs/guide/users.md +49 -0
  72. package/docs/roadmap.md +28 -39
  73. package/docs/troubleshooting.md +120 -7
  74. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  isId2,
14
14
  mintedAt2,
15
15
  createMemoryStores2
16
- } from "./chunks/index-kvjj5xkp.js";
16
+ } from "./chunks/index-6bjfv4xv.js";
17
17
  import {
18
18
  JanusError2,
19
19
  UserInvalidError2,
@@ -774,12 +774,18 @@ function assertStores(stores, where) {
774
774
  }
775
775
  }
776
776
  }
777
- const sessions = stores.sessions;
778
- const collect = sessions["deleteExpiredSessions"];
779
- if (collect !== undefined && typeof collect !== "function") {
780
- 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`);
781
787
  }
782
- return { collectExpired: collect !== undefined };
788
+ return found !== undefined;
783
789
  }
784
790
 
785
791
  // src/auth/devices/token.ts
@@ -1147,7 +1153,7 @@ async function issueCode(context, token) {
1147
1153
  }
1148
1154
  async function insertOneTime(context, token, code) {
1149
1155
  const now = context.clock.now();
1150
- const secret = mintSecret();
1156
+ const secret = token.secret ?? mintSecret();
1151
1157
  const expiresAt = new Date(now.getTime() + token.ttlMs);
1152
1158
  await context.store.tokens.insertToken({
1153
1159
  tokenHash: hashSecret(secret),
@@ -1289,6 +1295,18 @@ async function heldByPassword(context, type, verified, password, result, where)
1289
1295
  return false;
1290
1296
  }
1291
1297
 
1298
+ // src/auth/prepared.ts
1299
+ function sendOnce(where, issue) {
1300
+ let sent = false;
1301
+ return async () => {
1302
+ if (sent) {
1303
+ throw new TypeError(`${where}(…).send: already called — a prepared request sends once; call ${where} again for another`);
1304
+ }
1305
+ sent = true;
1306
+ return issue();
1307
+ };
1308
+ }
1309
+
1292
1310
  // src/auth/email-flows/email-token.ts
1293
1311
  async function redeemEmailToken(context, type, kind, secret, where) {
1294
1312
  const token = await spendOneTime(context, secret, kind, where, "token");
@@ -1321,20 +1339,32 @@ function resetPasswordFlows(context, type, at) {
1321
1339
  return {
1322
1340
  async request(email) {
1323
1341
  const where = at("resetPassword.request");
1324
- passwordRule(type, where);
1325
- await countMailByAddress(context, type, "resetPassword", String(email), where);
1326
- const record = await holderOfEmail(context, type, String(email));
1327
- if (record === null)
1328
- return null;
1329
- const issued = await issueEmailToken(context, type, "resetPassword", record, where);
1330
- await context.store.tokens.spendUserTokens(record.id, "resetPassword", context.clock.now(), hashSecret(issued.token));
1331
- return { ...issued, user: toUser(record) };
1342
+ return (await prepareReset(context, type, String(email), where)).send();
1343
+ },
1344
+ async prepare(email) {
1345
+ const where = at("resetPassword.prepare");
1346
+ return prepareReset(context, type, String(email), where);
1332
1347
  },
1333
1348
  async confirm(secret, password) {
1334
1349
  return confirmReset(context, type, secret, password, at("resetPassword.confirm"));
1335
1350
  }
1336
1351
  };
1337
1352
  }
1353
+ async function prepareReset(context, type, email, where) {
1354
+ passwordRule(type, where);
1355
+ await countMailByAddress(context, type, "resetPassword", email, where);
1356
+ return Object.freeze({
1357
+ send: sendOnce(where, () => issueReset(context, type, email, where))
1358
+ });
1359
+ }
1360
+ async function issueReset(context, type, email, where) {
1361
+ const record = await holderOfEmail(context, type, email);
1362
+ if (record === null)
1363
+ return null;
1364
+ const issued = await issueEmailToken(context, type, "resetPassword", record, where);
1365
+ await context.store.tokens.spendUserTokens(record.id, "resetPassword", context.clock.now(), hashSecret(issued.token));
1366
+ return { ...issued, user: toUser(record) };
1367
+ }
1338
1368
  async function confirmReset(context, type, secret, password, where) {
1339
1369
  passwordRule(type, where);
1340
1370
  checkPassword(type, password, where);
@@ -1555,7 +1585,12 @@ async function finishEmailSignIn(context, type, finish, signIn) {
1555
1585
  function magicLinkFlows(context, type, at, finish) {
1556
1586
  return {
1557
1587
  async request(email) {
1558
- return requestLink(context, type, String(email), at("magicLink.request"));
1588
+ const where = at("magicLink.request");
1589
+ return (await prepareLink(context, type, String(email), where)).send();
1590
+ },
1591
+ async prepare(email) {
1592
+ const where = at("magicLink.prepare");
1593
+ return prepareLink(context, type, String(email), where);
1559
1594
  },
1560
1595
  async confirm(token, options) {
1561
1596
  const where = at("magicLink.confirm");
@@ -1564,8 +1599,13 @@ function magicLinkFlows(context, type, at, finish) {
1564
1599
  }
1565
1600
  };
1566
1601
  }
1567
- async function requestLink(context, type, email, where) {
1602
+ async function prepareLink(context, type, email, where) {
1568
1603
  await countMailByAddress(context, type, "magicLink", email, where);
1604
+ return Object.freeze({
1605
+ send: sendOnce(where, () => issueLink(context, type, email))
1606
+ });
1607
+ }
1608
+ async function issueLink(context, type, email) {
1569
1609
  const record = await signInHolder(context, type, email);
1570
1610
  if (record === null)
1571
1611
  return null;
@@ -1590,6 +1630,32 @@ async function confirmLink(context, type, finish, secret, where, device) {
1590
1630
  });
1591
1631
  }
1592
1632
 
1633
+ // src/auth/proven-sign-in.ts
1634
+ function provenSignInFlows(context, type, at, finish) {
1635
+ return {
1636
+ async signInProven(user, options) {
1637
+ const where = at("signInProven");
1638
+ const device = deviceHint(context, options, where);
1639
+ const record = await getRecord(context, idOf2(user), type.name, where);
1640
+ if (!record.active) {
1641
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1642
+ userId: record.id,
1643
+ userType: type.name
1644
+ });
1645
+ }
1646
+ const result = await finish(record, where, device);
1647
+ await reportNewDevice(context, result);
1648
+ return result;
1649
+ }
1650
+ };
1651
+ }
1652
+ function idOf2(user) {
1653
+ if (typeof user === "string")
1654
+ return user;
1655
+ const id = typeof user === "object" && user !== null ? user.id : undefined;
1656
+ return typeof id === "string" ? id : "";
1657
+ }
1658
+
1593
1659
  // src/auth/sign-in-attempts.ts
1594
1660
  var PURPOSE3 = "janus/sign-in/attempts/v1";
1595
1661
  async function countLogin(context, type, login, where) {
@@ -1968,29 +2034,43 @@ function secondFactorFlows(context, type, at) {
1968
2034
  function signInCodeFlows(context, type, at, finish) {
1969
2035
  return {
1970
2036
  async request(email) {
1971
- return requestCode(context, type, String(email), at("signInCode.request"));
2037
+ const where = at("signInCode.request");
2038
+ return (await prepareCode(context, type, String(email), where)).send();
2039
+ },
2040
+ async prepare(email) {
2041
+ const where = at("signInCode.prepare");
2042
+ return prepareCode(context, type, String(email), where);
1972
2043
  },
1973
2044
  async confirm(challenge, code, options) {
1974
2045
  return confirmCode(context, type, finish, { challenge, code, options }, at);
1975
2046
  }
1976
2047
  };
1977
2048
  }
1978
- async function requestCode(context, type, email, where) {
2049
+ async function prepareCode(context, type, email, where) {
1979
2050
  await countMailByAddress(context, type, "signInCode", email, where);
2051
+ const challenge = mintSecret();
2052
+ return Object.freeze({
2053
+ challenge,
2054
+ send: sendOnce(where, () => issueSignInCode(context, type, email, challenge))
2055
+ });
2056
+ }
2057
+ async function issueSignInCode(context, type, email, challenge) {
1980
2058
  const record = await signInHolder(context, type, email);
1981
2059
  if (record === null)
1982
2060
  return null;
2061
+ const address = String(record.fields[type.email]);
1983
2062
  const { secret, code, expiresAt } = await issueCode(context, {
1984
2063
  kind: "signInCode",
1985
2064
  userId: record.id,
1986
- address: String(record.fields[type.email]),
1987
- ttlMs: context.config.tokenTtlMs.signInCode
2065
+ address,
2066
+ ttlMs: context.config.tokenTtlMs.signInCode,
2067
+ secret: challenge
1988
2068
  });
1989
2069
  await keepOnlyLatest(context, record.id, "signInCode", secret);
1990
2070
  return {
1991
2071
  code,
1992
2072
  challenge: secret,
1993
- email: String(record.fields[type.email]),
2073
+ email: address,
1994
2074
  expiresAt,
1995
2075
  user: toUser(record)
1996
2076
  };
@@ -2317,6 +2397,48 @@ async function deleteUser(context, type, user) {
2317
2397
  return deleted;
2318
2398
  }
2319
2399
 
2400
+ // src/auth/users/find-many.ts
2401
+ var FIND_MANY_BATCH = 100;
2402
+ var FIND_MANY_CONCURRENCY = 10;
2403
+ async function findManyRecords(context, ids, type, where) {
2404
+ if (!Array.isArray(ids)) {
2405
+ throw new TypeError(`${where}: ids must be an array of user ids`);
2406
+ }
2407
+ const wanted = [
2408
+ ...new Set(ids.filter((id) => typeof id === "string" && isId2(id)))
2409
+ ];
2410
+ const found = new Map;
2411
+ for (let start = 0;start < wanted.length; start += FIND_MANY_BATCH) {
2412
+ const batch = wanted.slice(start, start + FIND_MANY_BATCH);
2413
+ for (const record of await readBatch(context, batch)) {
2414
+ if (record.type === type)
2415
+ found.set(record.id, record);
2416
+ }
2417
+ }
2418
+ return wanted.flatMap((id) => {
2419
+ const record = found.get(id);
2420
+ return record === undefined ? [] : [record];
2421
+ });
2422
+ }
2423
+ async function readBatch(context, ids) {
2424
+ const { users } = context.store;
2425
+ if (context.capabilities.findUsers && users.findUsers !== undefined) {
2426
+ const answer = await users.findUsers(ids);
2427
+ if (!Array.isArray(answer)) {
2428
+ throw new StoreFailure2("users.findUsers answered no list: an absence is a shorter list, so this store forgot to answer", { slot: "users", operation: "findUsers" });
2429
+ }
2430
+ return answer;
2431
+ }
2432
+ const records = [];
2433
+ for (let start = 0;start < ids.length; start += FIND_MANY_CONCURRENCY) {
2434
+ const answers = await Promise.all(ids.slice(start, start + FIND_MANY_CONCURRENCY).map((id) => users.findUser(id)));
2435
+ for (const record of answers)
2436
+ if (record !== null)
2437
+ records.push(record);
2438
+ }
2439
+ return records;
2440
+ }
2441
+
2320
2442
  // src/auth/users/fields-patch.ts
2321
2443
  var normalized = (email) => email === null ? null : normalizeEmail(email);
2322
2444
  function emailChanged(type, before, after) {
@@ -2359,6 +2481,10 @@ function recordFlows(context, type, at) {
2359
2481
  const record = await findRecord(context, id, type.name);
2360
2482
  return record === null ? null : toUser(record);
2361
2483
  },
2484
+ async findMany(ids) {
2485
+ const records = await findManyRecords(context, ids, type.name, at("findMany"));
2486
+ return records.map(toUser);
2487
+ },
2362
2488
  async get(id) {
2363
2489
  return toUser(await getRecord(context, id, type.name, at("get")));
2364
2490
  },
@@ -2396,6 +2522,7 @@ function typeApi(context, type) {
2396
2522
  return {
2397
2523
  ...recordFlows(context, type, at),
2398
2524
  ...passwordFlows(context, type, at, secondFactor.finish),
2525
+ ...provenSignInFlows(context, type, at, secondFactor.finish),
2399
2526
  secondFactor: secondFactor.api,
2400
2527
  signInCode,
2401
2528
  magicLink: magicLinkFlows(context, type, at, secondFactor.finish),
@@ -2488,5 +2615,5 @@ export {
2488
2615
  systemClock
2489
2616
  };
2490
2617
 
2491
- //# debugId=ACD85BEBE6A087E464756E2164756E21
2618
+ //# debugId=58DD1D230B6CE48B64756E2164756E21
2492
2619
  //# sourceMappingURL=index.js.map