@nxgt/janus 0.23.1 → 0.25.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 (78) hide show
  1. package/README.md +119 -21
  2. package/dist/auth/email-flows/prove-first.d.ts +23 -1
  3. package/dist/auth/email-flows/prove-first.d.ts.map +1 -1
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/outage.d.ts +17 -6
  7. package/dist/auth/outage.d.ts.map +1 -1
  8. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  9. package/dist/auth/port/memory/tokens.d.ts.map +1 -1
  10. package/dist/auth/port/types/stores.d.ts +2 -0
  11. package/dist/auth/port/types/stores.d.ts.map +1 -1
  12. package/dist/auth/port/types/tokens.d.ts +12 -0
  13. package/dist/auth/port/types/tokens.d.ts.map +1 -1
  14. package/dist/auth/sessions/collect-expired.d.ts +14 -0
  15. package/dist/auth/sessions/collect-expired.d.ts.map +1 -0
  16. package/dist/auth/sessions/shared-api.d.ts +1 -1
  17. package/dist/auth/sessions/shared-api.d.ts.map +1 -1
  18. package/dist/auth/types/email-flows.d.ts +13 -2
  19. package/dist/auth/types/email-flows.d.ts.map +1 -1
  20. package/dist/auth/types/find-or-create.d.ts +59 -0
  21. package/dist/auth/types/find-or-create.d.ts.map +1 -0
  22. package/dist/auth/types/index.d.ts +1 -0
  23. package/dist/auth/types/index.d.ts.map +1 -1
  24. package/dist/auth/types/janus.d.ts +6 -4
  25. package/dist/auth/types/janus.d.ts.map +1 -1
  26. package/dist/auth/users/any-type-api.d.ts +2 -2
  27. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  28. package/dist/auth/users/find-or-create.d.ts +12 -0
  29. package/dist/auth/users/find-or-create.d.ts.map +1 -0
  30. package/dist/auth/users/insert.d.ts +8 -3
  31. package/dist/auth/users/insert.d.ts.map +1 -1
  32. package/dist/auth/users/type-api.d.ts +1 -1
  33. package/dist/auth/users/type-api.d.ts.map +1 -1
  34. package/dist/chunks/{index-6bjfv4xv.js → index-q3w3vr69.js} +13 -3
  35. package/dist/chunks/{index-6bjfv4xv.js.map → index-q3w3vr69.js.map} +3 -3
  36. package/dist/chunks/{index-cntabs9d.js → index-rpp0bw6e.js} +2 -2
  37. package/dist/chunks/{index-kppkcbn5.js → index-spwh8b9z.js} +2 -5
  38. package/dist/chunks/{index-kppkcbn5.js.map → index-spwh8b9z.js.map} +2 -2
  39. package/dist/chunks/{index-4khsm7mw.js → index-y9b327j8.js} +18 -18
  40. package/dist/chunks/{index-4khsm7mw.js.map → index-y9b327j8.js.map} +5 -5
  41. package/dist/conformance/cases/outage.d.ts.map +1 -1
  42. package/dist/conformance/cases/tokens/deletion.d.ts.map +1 -1
  43. package/dist/conformance/describe.d.ts +1 -0
  44. package/dist/conformance/describe.d.ts.map +1 -1
  45. package/dist/conformance/index.js +39 -5
  46. package/dist/conformance/index.js.map +5 -5
  47. package/dist/conformance/types.d.ts +1 -1
  48. package/dist/conformance/types.d.ts.map +1 -1
  49. package/dist/index.js +167 -44
  50. package/dist/index.js.map +11 -9
  51. package/dist/permissions/engine.d.ts.map +1 -1
  52. package/dist/permissions/index.d.ts +1 -1
  53. package/dist/permissions/index.d.ts.map +1 -1
  54. package/dist/permissions/index.js +52 -19
  55. package/dist/permissions/index.js.map +6 -5
  56. package/dist/permissions/input.d.ts +13 -1
  57. package/dist/permissions/input.d.ts.map +1 -1
  58. package/dist/permissions/model/grant.d.ts +14 -1
  59. package/dist/permissions/model/grant.d.ts.map +1 -1
  60. package/dist/permissions/model/permissions.d.ts +13 -1
  61. package/dist/permissions/model/permissions.d.ts.map +1 -1
  62. package/dist/permissions/port/types.d.ts +8 -2
  63. package/dist/permissions/port/types.d.ts.map +1 -1
  64. package/dist/permissions/replace.d.ts +25 -0
  65. package/dist/permissions/replace.d.ts.map +1 -0
  66. package/docs/README.md +2 -2
  67. package/docs/guide/adapters.md +40 -3
  68. package/docs/guide/email-flows.md +35 -5
  69. package/docs/guide/errors.md +2 -2
  70. package/docs/guide/passwords.md +7 -10
  71. package/docs/guide/permissions.md +42 -1
  72. package/docs/guide/sessions.md +21 -4
  73. package/docs/guide/users.md +44 -0
  74. package/docs/guide/vocabulary.md +1 -1
  75. package/docs/roadmap.md +33 -38
  76. package/docs/troubleshooting.md +50 -21
  77. package/package.json +1 -1
  78. /package/dist/chunks/{index-cntabs9d.js.map → index-rpp0bw6e.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-4khsm7mw.js";
10
+ } from "./chunks/index-y9b327j8.js";
11
11
  import {
12
12
  mintId2,
13
13
  isId2,
14
14
  mintedAt2,
15
15
  createMemoryStores2
16
- } from "./chunks/index-6bjfv4xv.js";
16
+ } from "./chunks/index-q3w3vr69.js";
17
17
  import {
18
18
  JanusError2,
19
19
  UserInvalidError2,
@@ -39,7 +39,7 @@ import {
39
39
  formatTuple2,
40
40
  parseTuple2,
41
41
  parseSubject2
42
- } from "./chunks/index-kppkcbn5.js";
42
+ } from "./chunks/index-spwh8b9z.js";
43
43
 
44
44
  // src/auth/hashers.ts
45
45
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
@@ -550,6 +550,15 @@ async function unlessVersionConflict(write) {
550
550
  throw error;
551
551
  }
552
552
  }
553
+ async function unlessLoginTaken(write) {
554
+ try {
555
+ return await write;
556
+ } catch (error) {
557
+ if (error instanceof StoreConflict2 && error.on === "login")
558
+ return error;
559
+ throw error;
560
+ }
561
+ }
553
562
 
554
563
  // src/auth/context/password.ts
555
564
  function passwordRule(type, where) {
@@ -785,6 +794,7 @@ function assertStores(stores, where) {
785
794
  const slots = stores;
786
795
  return {
787
796
  collectExpired: optional(slots, "sessions", "deleteExpiredSessions", where),
797
+ collectExpiredTokens: optional(slots, "tokens", "deleteExpiredTokens", where),
788
798
  findUsers: optional(slots, "users", "findUsers", where)
789
799
  };
790
800
  }
@@ -1014,6 +1024,19 @@ function renewalDue(type, session, now) {
1014
1024
  return type.renewAfterMs !== null && type.lifespanMs - (session.expiresAt.getTime() - now) >= type.renewAfterMs;
1015
1025
  }
1016
1026
 
1027
+ // src/auth/sessions/collect-expired.ts
1028
+ async function collectExpired(context) {
1029
+ const { store, clock, capabilities } = context;
1030
+ const sessions = capabilities.collectExpired ? store.sessions.deleteExpiredSessions : undefined;
1031
+ const tokens = capabilities.collectExpiredTokens ? store.tokens.deleteExpiredTokens : undefined;
1032
+ if (sessions === undefined && tokens === undefined) {
1033
+ throw new UnsupportedError2("collectExpired: store.sessions does not implement deleteExpiredSessions, nor store.tokens deleteExpiredTokens — their stores expire records on their own, or implement the methods", { slot: "sessions", operation: "deleteExpiredSessions" });
1034
+ }
1035
+ const before = clock.now();
1036
+ const collected = sessions === undefined ? 0 : await sessions(before);
1037
+ return collected + (tokens === undefined ? 0 : await tokens(before));
1038
+ }
1039
+
1017
1040
  // src/auth/sessions/cookie.ts
1018
1041
  function cookieOf(context) {
1019
1042
  const { name, domain, path, sameSite, secure } = context.config.cookie;
@@ -1062,11 +1085,7 @@ function sharedApi(context) {
1062
1085
  },
1063
1086
  cookie: cookieOf(context),
1064
1087
  async collectExpired() {
1065
- const collect = store.sessions.deleteExpiredSessions;
1066
- if (!context.capabilities.collectExpired || collect === undefined) {
1067
- throw new UnsupportedError2("collectExpired: store.sessions does not implement deleteExpiredSessions — its store expires sessions on its own, or implement the method", { slot: "sessions", operation: "deleteExpiredSessions" });
1068
- }
1069
- return collect(clock.now());
1088
+ return collectExpired(context);
1070
1089
  },
1071
1090
  types: [...config.types.keys()]
1072
1091
  };
@@ -1648,19 +1667,69 @@ async function endWhatTheSquatterOpened(context, userId) {
1648
1667
  function proveFirstFlow(context, type, at) {
1649
1668
  return async (user) => {
1650
1669
  const where = at("verifyEmail.proveFirst");
1651
- const record = await getRecord(context, refId(user), type.name, where);
1652
- if (!record.active) {
1653
- throw new UserInactiveError2(`${where}: the user is inactive`, {
1654
- userId: record.id,
1655
- userType: type.name
1670
+ const id = refId(user);
1671
+ const claimed = claimedEmail(type, user);
1672
+ const first = await readActive(context, type, id, where);
1673
+ assertClaimed(type, claimed, first, user, where);
1674
+ const answer = await unlessVersionConflict(proveOrAnswer(context, type, first, where));
1675
+ if (answer !== null)
1676
+ return answer;
1677
+ const again = await readActive(context, type, id, where);
1678
+ assertClaimed(type, claimed, again, user, where);
1679
+ if (!sameEmail(type, first, again)) {
1680
+ throw new StoreConflict2("version", `${where}: the e-mail changed since it was read`, {
1681
+ userId: first.id,
1682
+ expectedVersion: first.version,
1683
+ actualVersion: again.version,
1684
+ operation: where
1656
1685
  });
1657
1686
  }
1658
- if (record.emailVerifiedAt !== null) {
1659
- return { user: toUser(record), proved: false };
1660
- }
1661
- const proved = await proveFirstEmail(context, type, record, where);
1662
- return { user: toUser(proved), proved: true };
1687
+ return proveOrAnswer(context, type, again, where);
1688
+ };
1689
+ }
1690
+ async function readActive(context, type, id, where) {
1691
+ const record = await getRecord(context, id, type.name, where);
1692
+ if (!record.active) {
1693
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1694
+ userId: record.id,
1695
+ userType: type.name
1696
+ });
1697
+ }
1698
+ return record;
1699
+ }
1700
+ async function proveOrAnswer(context, type, record, where) {
1701
+ if (record.emailVerifiedAt !== null) {
1702
+ return { user: toUser(record), proved: false };
1703
+ }
1704
+ const proved = await proveFirstEmail(context, type, record, where);
1705
+ return { user: toUser(proved), proved: true };
1706
+ }
1707
+ function sameEmail(type, a, b) {
1708
+ const held = (record) => {
1709
+ const email = emailOf(type, record.fields);
1710
+ return email === null ? null : normalizeEmail(email);
1663
1711
  };
1712
+ return held(a) === held(b);
1713
+ }
1714
+ function claimedEmail(type, user) {
1715
+ if (typeof user !== "object" || user === null)
1716
+ return null;
1717
+ const value = user[type.email];
1718
+ return typeof value === "string" ? normalizeEmail(value) : null;
1719
+ }
1720
+ function assertClaimed(type, claimed, record, user, where) {
1721
+ if (claimed === null)
1722
+ return;
1723
+ const email = emailOf(type, record.fields);
1724
+ if (email !== null && normalizeEmail(email) === claimed)
1725
+ return;
1726
+ const held = user.version;
1727
+ throw new StoreConflict2("version", `${where}: the e-mail changed since the user was read`, {
1728
+ userId: record.id,
1729
+ ...typeof held === "number" ? { expectedVersion: held } : {},
1730
+ actualVersion: record.version,
1731
+ operation: where
1732
+ });
1664
1733
  }
1665
1734
 
1666
1735
  // src/auth/email-flows/verify-email.ts
@@ -2387,36 +2456,13 @@ function findByEmailFlows(context, type) {
2387
2456
  // src/auth/users/by-login.ts
2388
2457
  var byLogin = async (context, type, login) => isStorable(login) ? context.store.users.findUserByLogin(type.name, login) : null;
2389
2458
 
2390
- // src/auth/users/change-password.ts
2391
- async function changePassword(context, type, user, change, options, where) {
2392
- passwordRule(type, where);
2393
- checkPassword(type, change?.next, where);
2394
- const hasher = requireHasher(context, where);
2395
- const written = await writeUser(context, user, type, options, where, async (record, now) => {
2396
- if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
2397
- throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
2398
- reason: record.password === null ? "noPassword" : "wrongPassword",
2399
- userId: record.id,
2400
- userType: type.name
2401
- });
2402
- }
2403
- return {
2404
- password: {
2405
- hash: await hasher.hash(change.next),
2406
- updatedAt: now
2407
- }
2408
- };
2409
- });
2410
- await passwordChanged(context, written);
2411
- return written;
2412
- }
2413
-
2414
2459
  // src/auth/users/insert.ts
2415
- async function insert(context, type, input, where) {
2460
+ async function insert(context, type, input, where, accept) {
2416
2461
  const { store, clock } = context;
2417
2462
  const { password, active, ...rest } = input ?? {};
2418
2463
  const now = clock.now();
2419
2464
  const fields = await validateFields(type, rest, where);
2465
+ accept?.(fields);
2420
2466
  let hash = null;
2421
2467
  if (password !== undefined) {
2422
2468
  checkPassword(type, password, where);
@@ -2443,6 +2489,82 @@ async function insert(context, type, input, where) {
2443
2489
  return inserted;
2444
2490
  }
2445
2491
 
2492
+ // src/auth/users/find-or-create.ts
2493
+ async function holder(context, type, key) {
2494
+ if (type.password !== null) {
2495
+ return byLogin(context, type, type.password.normalize(key));
2496
+ }
2497
+ return key.trim() === "" ? null : holderOfEmail(context, type, key);
2498
+ }
2499
+ function refusal2(type, where, field) {
2500
+ return new UserInvalidError2(`${where}: the fields do not match the ${type.name} schema (1 issue, at ${field})`, {
2501
+ issues: [
2502
+ { path: [field], message: "is not the key findOrCreate was given" }
2503
+ ],
2504
+ userType: type.name
2505
+ });
2506
+ }
2507
+ function mustNameKey(type, key, where) {
2508
+ return (fields) => {
2509
+ const field = type.password === null ? type.email : type.password.login;
2510
+ const value = fields[field];
2511
+ const named = typeof value !== "string" ? null : type.password === null ? normalizeEmail(value) : type.password.normalize(value);
2512
+ const wanted = type.password === null ? normalizeEmail(key) : type.password.normalize(key);
2513
+ if (named === null || named !== wanted) {
2514
+ throw refusal2(type, where, field);
2515
+ }
2516
+ };
2517
+ }
2518
+ function findOrCreateFlows(context, type, at) {
2519
+ return {
2520
+ async findOrCreate(key, input) {
2521
+ const where = at("findOrCreate");
2522
+ if (typeof key !== "string") {
2523
+ throw new UserInvalidError2(`${where}: the key must be a string`, {
2524
+ issues: [{ path: [], message: "is not a string" }],
2525
+ userType: type.name
2526
+ });
2527
+ }
2528
+ const found = await holder(context, type, key);
2529
+ if (found !== null)
2530
+ return { user: toUser(found), created: false };
2531
+ const record = await input();
2532
+ const written = await unlessLoginTaken(insert(context, type, record, where, mustNameKey(type, key, where)));
2533
+ if (!(written instanceof StoreConflict2)) {
2534
+ return { user: toUser(written), created: true };
2535
+ }
2536
+ const winner = await holder(context, type, key);
2537
+ if (winner === null)
2538
+ throw written;
2539
+ return { user: toUser(winner), created: false };
2540
+ }
2541
+ };
2542
+ }
2543
+
2544
+ // src/auth/users/change-password.ts
2545
+ async function changePassword(context, type, user, change, options, where) {
2546
+ passwordRule(type, where);
2547
+ checkPassword(type, change?.next, where);
2548
+ const hasher = requireHasher(context, where);
2549
+ const written = await writeUser(context, user, type, options, where, async (record, now) => {
2550
+ if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
2551
+ throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
2552
+ reason: record.password === null ? "noPassword" : "wrongPassword",
2553
+ userId: record.id,
2554
+ userType: type.name
2555
+ });
2556
+ }
2557
+ return {
2558
+ password: {
2559
+ hash: await hasher.hash(change.next),
2560
+ updatedAt: now
2561
+ }
2562
+ };
2563
+ });
2564
+ await passwordChanged(context, written);
2565
+ return written;
2566
+ }
2567
+
2446
2568
  // src/auth/users/sign-in.ts
2447
2569
  async function signIn(context, type, finish, input, call) {
2448
2570
  const { where, device } = call;
@@ -2663,6 +2785,7 @@ function typeApi(context, type) {
2663
2785
  magicLink: magicLinkFlows(context, type, at, secondFactor.finish),
2664
2786
  stepUp: stepUpFlows(context, type, at),
2665
2787
  ...findByEmailFlows(context, type),
2788
+ ...findOrCreateFlows(context, type, at),
2666
2789
  ...emailFlows(context, type, at)
2667
2790
  };
2668
2791
  }
@@ -2751,5 +2874,5 @@ export {
2751
2874
  systemClock
2752
2875
  };
2753
2876
 
2754
- //# debugId=33064E7A9A36409664756E2164756E21
2877
+ //# debugId=D10ED7160A09120864756E2164756E21
2755
2878
  //# sourceMappingURL=index.js.map