@nxgt/janus 0.23.0 → 0.24.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 (50) hide show
  1. package/README.md +102 -17
  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 +15 -4
  7. package/dist/auth/outage.d.ts.map +1 -1
  8. package/dist/auth/types/email-flows.d.ts +13 -2
  9. package/dist/auth/types/email-flows.d.ts.map +1 -1
  10. package/dist/auth/types/find-or-create.d.ts +59 -0
  11. package/dist/auth/types/find-or-create.d.ts.map +1 -0
  12. package/dist/auth/types/index.d.ts +1 -0
  13. package/dist/auth/types/index.d.ts.map +1 -1
  14. package/dist/auth/types/janus.d.ts +2 -1
  15. package/dist/auth/types/janus.d.ts.map +1 -1
  16. package/dist/auth/users/any-type-api.d.ts +2 -2
  17. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  18. package/dist/auth/users/find-or-create.d.ts +12 -0
  19. package/dist/auth/users/find-or-create.d.ts.map +1 -0
  20. package/dist/auth/users/insert.d.ts +8 -3
  21. package/dist/auth/users/insert.d.ts.map +1 -1
  22. package/dist/auth/users/type-api.d.ts +1 -1
  23. package/dist/auth/users/type-api.d.ts.map +1 -1
  24. package/dist/index.js +149 -36
  25. package/dist/index.js.map +8 -7
  26. package/dist/permissions/engine.d.ts.map +1 -1
  27. package/dist/permissions/index.d.ts +1 -1
  28. package/dist/permissions/index.d.ts.map +1 -1
  29. package/dist/permissions/index.js +49 -16
  30. package/dist/permissions/index.js.map +6 -5
  31. package/dist/permissions/input.d.ts +13 -1
  32. package/dist/permissions/input.d.ts.map +1 -1
  33. package/dist/permissions/model/grant.d.ts +14 -1
  34. package/dist/permissions/model/grant.d.ts.map +1 -1
  35. package/dist/permissions/model/permissions.d.ts +13 -1
  36. package/dist/permissions/model/permissions.d.ts.map +1 -1
  37. package/dist/permissions/port/types.d.ts +8 -2
  38. package/dist/permissions/port/types.d.ts.map +1 -1
  39. package/dist/permissions/replace.d.ts +25 -0
  40. package/dist/permissions/replace.d.ts.map +1 -0
  41. package/docs/README.md +2 -2
  42. package/docs/guide/email-flows.md +47 -11
  43. package/docs/guide/errors.md +1 -1
  44. package/docs/guide/permissions.md +42 -1
  45. package/docs/guide/sessions.md +6 -0
  46. package/docs/guide/users.md +44 -0
  47. package/docs/guide/vocabulary.md +2 -2
  48. package/docs/roadmap.md +25 -30
  49. package/docs/troubleshooting.md +44 -15
  50. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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) {
@@ -1648,19 +1657,69 @@ async function endWhatTheSquatterOpened(context, userId) {
1648
1657
  function proveFirstFlow(context, type, at) {
1649
1658
  return async (user) => {
1650
1659
  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
1660
+ const id = refId(user);
1661
+ const claimed = claimedEmail(type, user);
1662
+ const first = await readActive(context, type, id, where);
1663
+ assertClaimed(type, claimed, first, user, where);
1664
+ const answer = await unlessVersionConflict(proveOrAnswer(context, type, first, where));
1665
+ if (answer !== null)
1666
+ return answer;
1667
+ const again = await readActive(context, type, id, where);
1668
+ assertClaimed(type, claimed, again, user, where);
1669
+ if (!sameEmail(type, first, again)) {
1670
+ throw new StoreConflict2("version", `${where}: the e-mail changed since it was read`, {
1671
+ userId: first.id,
1672
+ expectedVersion: first.version,
1673
+ actualVersion: again.version,
1674
+ operation: where
1656
1675
  });
1657
1676
  }
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 };
1677
+ return proveOrAnswer(context, type, again, where);
1678
+ };
1679
+ }
1680
+ async function readActive(context, type, id, where) {
1681
+ const record = await getRecord(context, id, type.name, where);
1682
+ if (!record.active) {
1683
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1684
+ userId: record.id,
1685
+ userType: type.name
1686
+ });
1687
+ }
1688
+ return record;
1689
+ }
1690
+ async function proveOrAnswer(context, type, record, where) {
1691
+ if (record.emailVerifiedAt !== null) {
1692
+ return { user: toUser(record), proved: false };
1693
+ }
1694
+ const proved = await proveFirstEmail(context, type, record, where);
1695
+ return { user: toUser(proved), proved: true };
1696
+ }
1697
+ function sameEmail(type, a, b) {
1698
+ const held = (record) => {
1699
+ const email = emailOf(type, record.fields);
1700
+ return email === null ? null : normalizeEmail(email);
1663
1701
  };
1702
+ return held(a) === held(b);
1703
+ }
1704
+ function claimedEmail(type, user) {
1705
+ if (typeof user !== "object" || user === null)
1706
+ return null;
1707
+ const value = user[type.email];
1708
+ return typeof value === "string" ? normalizeEmail(value) : null;
1709
+ }
1710
+ function assertClaimed(type, claimed, record, user, where) {
1711
+ if (claimed === null)
1712
+ return;
1713
+ const email = emailOf(type, record.fields);
1714
+ if (email !== null && normalizeEmail(email) === claimed)
1715
+ return;
1716
+ const held = user.version;
1717
+ throw new StoreConflict2("version", `${where}: the e-mail changed since the user was read`, {
1718
+ userId: record.id,
1719
+ ...typeof held === "number" ? { expectedVersion: held } : {},
1720
+ actualVersion: record.version,
1721
+ operation: where
1722
+ });
1664
1723
  }
1665
1724
 
1666
1725
  // src/auth/email-flows/verify-email.ts
@@ -2387,36 +2446,13 @@ function findByEmailFlows(context, type) {
2387
2446
  // src/auth/users/by-login.ts
2388
2447
  var byLogin = async (context, type, login) => isStorable(login) ? context.store.users.findUserByLogin(type.name, login) : null;
2389
2448
 
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
2449
  // src/auth/users/insert.ts
2415
- async function insert(context, type, input, where) {
2450
+ async function insert(context, type, input, where, accept) {
2416
2451
  const { store, clock } = context;
2417
2452
  const { password, active, ...rest } = input ?? {};
2418
2453
  const now = clock.now();
2419
2454
  const fields = await validateFields(type, rest, where);
2455
+ accept?.(fields);
2420
2456
  let hash = null;
2421
2457
  if (password !== undefined) {
2422
2458
  checkPassword(type, password, where);
@@ -2443,6 +2479,82 @@ async function insert(context, type, input, where) {
2443
2479
  return inserted;
2444
2480
  }
2445
2481
 
2482
+ // src/auth/users/find-or-create.ts
2483
+ async function holder(context, type, key) {
2484
+ if (type.password !== null) {
2485
+ return byLogin(context, type, type.password.normalize(key));
2486
+ }
2487
+ return key.trim() === "" ? null : holderOfEmail(context, type, key);
2488
+ }
2489
+ function refusal2(type, where, field) {
2490
+ return new UserInvalidError2(`${where}: the fields do not match the ${type.name} schema (1 issue, at ${field})`, {
2491
+ issues: [
2492
+ { path: [field], message: "is not the key findOrCreate was given" }
2493
+ ],
2494
+ userType: type.name
2495
+ });
2496
+ }
2497
+ function mustNameKey(type, key, where) {
2498
+ return (fields) => {
2499
+ const field = type.password === null ? type.email : type.password.login;
2500
+ const value = fields[field];
2501
+ const named = typeof value !== "string" ? null : type.password === null ? normalizeEmail(value) : type.password.normalize(value);
2502
+ const wanted = type.password === null ? normalizeEmail(key) : type.password.normalize(key);
2503
+ if (named === null || named !== wanted) {
2504
+ throw refusal2(type, where, field);
2505
+ }
2506
+ };
2507
+ }
2508
+ function findOrCreateFlows(context, type, at) {
2509
+ return {
2510
+ async findOrCreate(key, input) {
2511
+ const where = at("findOrCreate");
2512
+ if (typeof key !== "string") {
2513
+ throw new UserInvalidError2(`${where}: the key must be a string`, {
2514
+ issues: [{ path: [], message: "is not a string" }],
2515
+ userType: type.name
2516
+ });
2517
+ }
2518
+ const found = await holder(context, type, key);
2519
+ if (found !== null)
2520
+ return { user: toUser(found), created: false };
2521
+ const record = await input();
2522
+ const written = await unlessLoginTaken(insert(context, type, record, where, mustNameKey(type, key, where)));
2523
+ if (!(written instanceof StoreConflict2)) {
2524
+ return { user: toUser(written), created: true };
2525
+ }
2526
+ const winner = await holder(context, type, key);
2527
+ if (winner === null)
2528
+ throw written;
2529
+ return { user: toUser(winner), created: false };
2530
+ }
2531
+ };
2532
+ }
2533
+
2534
+ // src/auth/users/change-password.ts
2535
+ async function changePassword(context, type, user, change, options, where) {
2536
+ passwordRule(type, where);
2537
+ checkPassword(type, change?.next, where);
2538
+ const hasher = requireHasher(context, where);
2539
+ const written = await writeUser(context, user, type, options, where, async (record, now) => {
2540
+ if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
2541
+ throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
2542
+ reason: record.password === null ? "noPassword" : "wrongPassword",
2543
+ userId: record.id,
2544
+ userType: type.name
2545
+ });
2546
+ }
2547
+ return {
2548
+ password: {
2549
+ hash: await hasher.hash(change.next),
2550
+ updatedAt: now
2551
+ }
2552
+ };
2553
+ });
2554
+ await passwordChanged(context, written);
2555
+ return written;
2556
+ }
2557
+
2446
2558
  // src/auth/users/sign-in.ts
2447
2559
  async function signIn(context, type, finish, input, call) {
2448
2560
  const { where, device } = call;
@@ -2663,6 +2775,7 @@ function typeApi(context, type) {
2663
2775
  magicLink: magicLinkFlows(context, type, at, secondFactor.finish),
2664
2776
  stepUp: stepUpFlows(context, type, at),
2665
2777
  ...findByEmailFlows(context, type),
2778
+ ...findOrCreateFlows(context, type, at),
2666
2779
  ...emailFlows(context, type, at)
2667
2780
  };
2668
2781
  }
@@ -2751,5 +2864,5 @@ export {
2751
2864
  systemClock
2752
2865
  };
2753
2866
 
2754
- //# debugId=33064E7A9A36409664756E2164756E21
2867
+ //# debugId=DBC7C1267CB6B80364756E2164756E21
2755
2868
  //# sourceMappingURL=index.js.map