@nxgt/janus 0.11.0 → 0.12.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 (79) hide show
  1. package/README.md +101 -15
  2. package/dist/auth/config/janus-config.d.ts +2 -0
  3. package/dist/auth/config/janus-config.d.ts.map +1 -1
  4. package/dist/auth/config/resolved-config.d.ts +1 -0
  5. package/dist/auth/config/resolved-config.d.ts.map +1 -1
  6. package/dist/auth/index.d.ts +2 -1
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  9. package/dist/auth/port/memory/sessions.d.ts.map +1 -1
  10. package/dist/auth/port/types/sessions.d.ts +16 -1
  11. package/dist/auth/port/types/sessions.d.ts.map +1 -1
  12. package/dist/auth/port/types/tokens.d.ts +7 -2
  13. package/dist/auth/port/types/tokens.d.ts.map +1 -1
  14. package/dist/auth/second-factor/{regenerate-attempts.d.ts → app-code-attempts.d.ts} +13 -10
  15. package/dist/auth/second-factor/app-code-attempts.d.ts.map +1 -0
  16. package/dist/auth/sessions/authenticate.d.ts.map +1 -1
  17. package/dist/auth/sessions/fresh.d.ts +26 -0
  18. package/dist/auth/sessions/fresh.d.ts.map +1 -0
  19. package/dist/auth/sessions/standing.d.ts +20 -0
  20. package/dist/auth/sessions/standing.d.ts.map +1 -0
  21. package/dist/auth/step-up/codes.d.ts +29 -0
  22. package/dist/auth/step-up/codes.d.ts.map +1 -0
  23. package/dist/auth/step-up/flows.d.ts +19 -0
  24. package/dist/auth/step-up/flows.d.ts.map +1 -0
  25. package/dist/auth/step-up/open.d.ts +23 -0
  26. package/dist/auth/step-up/open.d.ts.map +1 -0
  27. package/dist/auth/step-up/request.d.ts +12 -0
  28. package/dist/auth/step-up/request.d.ts.map +1 -0
  29. package/dist/auth/types/index.d.ts +1 -0
  30. package/dist/auth/types/index.d.ts.map +1 -1
  31. package/dist/auth/types/janus.d.ts +2 -1
  32. package/dist/auth/types/janus.d.ts.map +1 -1
  33. package/dist/auth/types/step-up.d.ts +79 -0
  34. package/dist/auth/types/step-up.d.ts.map +1 -0
  35. package/dist/auth/users/any-type-api.d.ts +2 -2
  36. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  37. package/dist/auth/users/type-api.d.ts +1 -1
  38. package/dist/auth/users/type-api.d.ts.map +1 -1
  39. package/dist/chunks/{index-5e2fw7he.js → index-j2gd8fry.js} +12 -3
  40. package/dist/chunks/{index-5e2fw7he.js.map → index-j2gd8fry.js.map} +4 -4
  41. package/dist/chunks/{index-5tm04t5p.js → index-jp2kka6d.js} +2 -2
  42. package/dist/chunks/{index-6f6wja93.js → index-km7ffctx.js} +2 -2
  43. package/dist/chunks/{index-4m49cye4.js → index-rd2rddvv.js} +15 -12
  44. package/dist/chunks/{index-4m49cye4.js.map → index-rd2rddvv.js.map} +3 -3
  45. package/dist/conformance/cases/outage.d.ts.map +1 -1
  46. package/dist/conformance/cases/sessions/index.d.ts.map +1 -1
  47. package/dist/conformance/cases/sessions/reauthentication.d.ts +4 -0
  48. package/dist/conformance/cases/sessions/reauthentication.d.ts.map +1 -0
  49. package/dist/conformance/cases/tokens/index.d.ts.map +1 -1
  50. package/dist/conformance/cases/tokens/kinds.d.ts +4 -0
  51. package/dist/conformance/cases/tokens/kinds.d.ts.map +1 -0
  52. package/dist/conformance/index.js +147 -60
  53. package/dist/conformance/index.js.map +8 -6
  54. package/dist/errors/janus-error/codes.d.ts +7 -0
  55. package/dist/errors/janus-error/codes.d.ts.map +1 -1
  56. package/dist/errors/janus-error/index.d.ts +1 -1
  57. package/dist/errors/janus-error/index.d.ts.map +1 -1
  58. package/dist/errors/janus-error/refusals.d.ts +8 -0
  59. package/dist/errors/janus-error/refusals.d.ts.map +1 -1
  60. package/dist/errors/janus-error/status.d.ts.map +1 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +175 -18
  64. package/dist/index.js.map +16 -10
  65. package/dist/permissions/index.js +3 -3
  66. package/docs/README.md +1 -0
  67. package/docs/guide/adapters.md +38 -4
  68. package/docs/guide/errors.md +1 -0
  69. package/docs/guide/second-factor.md +22 -16
  70. package/docs/guide/sessions.md +7 -0
  71. package/docs/guide/step-up.md +178 -0
  72. package/docs/guide/users.md +1 -0
  73. package/docs/guide/vocabulary.md +3 -2
  74. package/docs/roadmap.md +12 -16
  75. package/docs/troubleshooting.md +182 -7
  76. package/package.json +1 -1
  77. package/dist/auth/second-factor/regenerate-attempts.d.ts.map +0 -1
  78. /package/dist/chunks/{index-5tm04t5p.js.map → index-jp2kka6d.js.map} +0 -0
  79. /package/dist/chunks/{index-6f6wja93.js.map → index-km7ffctx.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-5tm04t5p.js";
10
+ } from "./chunks/index-jp2kka6d.js";
11
11
  import {
12
12
  mintId2,
13
13
  isId2,
14
14
  mintedAt2,
15
15
  createMemoryStores2
16
- } from "./chunks/index-4m49cye4.js";
16
+ } from "./chunks/index-rd2rddvv.js";
17
17
  import {
18
18
  JanusError2,
19
19
  UserInvalidError2,
@@ -21,6 +21,7 @@ import {
21
21
  UserInactiveError2,
22
22
  TokenError2,
23
23
  SecondFactorError2,
24
+ StepUpRequiredError2,
24
25
  InvalidCursorError2,
25
26
  UnsupportedError2,
26
27
  PermissionDepthError2,
@@ -37,7 +38,7 @@ import {
37
38
  formatTuple2,
38
39
  parseTuple2,
39
40
  parseSubject2
40
- } from "./chunks/index-5e2fw7he.js";
41
+ } from "./chunks/index-j2gd8fry.js";
41
42
 
42
43
  // src/auth/hashers.ts
43
44
  import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
@@ -382,7 +383,8 @@ function resolveTokenTtls(config, where) {
382
383
  return {
383
384
  verifyEmail: parseDuration(config.tokens?.verifyEmail ?? "24h", `${where}: tokens.verifyEmail`),
384
385
  resetPassword: parseDuration(config.tokens?.resetPassword ?? "1h", `${where}: tokens.resetPassword`),
385
- signInCode: parseDuration(config.tokens?.signInCode ?? "10m", `${where}: tokens.signInCode`)
386
+ signInCode: parseDuration(config.tokens?.signInCode ?? "10m", `${where}: tokens.signInCode`),
387
+ stepUp: parseDuration(config.tokens?.stepUp ?? "10m", `${where}: tokens.stepUp`)
386
388
  };
387
389
  }
388
390
  // src/auth/context/create-context.ts
@@ -662,6 +664,7 @@ var REQUIRED = {
662
664
  "insertSession",
663
665
  "findSessionByTokenHash",
664
666
  "extendSession",
667
+ "reauthenticateSession",
665
668
  "revokeSession",
666
669
  "revokeUserSessions",
667
670
  "deleteUserSessions"
@@ -774,17 +777,24 @@ function headersOf(request) {
774
777
  return inner;
775
778
  return request;
776
779
  }
780
+ // src/auth/sessions/standing.ts
781
+ async function standingSession(context, request) {
782
+ const token = presentedToken(request, context.config.cookie.name);
783
+ if (token === null)
784
+ return null;
785
+ const session = await context.store.sessions.findSessionByTokenHash(hashSecret(token));
786
+ const now = context.clock.now().getTime();
787
+ return session === null || session.revokedAt !== null || session.expiresAt.getTime() <= now ? null : { token, session };
788
+ }
789
+
777
790
  // src/auth/sessions/authenticate.ts
778
791
  async function authenticate(context, request, options) {
779
792
  const { store, clock, config } = context;
780
- const token = presentedToken(request, config.cookie.name);
781
- if (token === null)
793
+ const presented = await standingSession(context, request);
794
+ if (presented === null)
782
795
  return null;
783
- const session = await store.sessions.findSessionByTokenHash(hashSecret(token));
796
+ const { token, session } = presented;
784
797
  const now = clock.now().getTime();
785
- if (session === null || session.revokedAt !== null || session.expiresAt.getTime() <= now) {
786
- return null;
787
- }
788
798
  const user = await store.users.findUser(session.userId);
789
799
  if (user === null || !user.active || options?.type !== undefined && user.type !== options.type) {
790
800
  return null;
@@ -1411,10 +1421,10 @@ function accountOf(type, record, where) {
1411
1421
  return String(record.fields[type.password.login]);
1412
1422
  }
1413
1423
 
1414
- // src/auth/second-factor/regenerate-attempts.ts
1424
+ // src/auth/second-factor/app-code-attempts.ts
1415
1425
  import { createHmac as createHmac3, hkdfSync as hkdfSync2 } from "node:crypto";
1416
- var REGENERATE_WINDOW_MS = 15 * 60000;
1417
- var WINDOW_MINUTES = REGENERATE_WINDOW_MS / 60000;
1426
+ var APP_CODE_WINDOW_MS = 15 * 60000;
1427
+ var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
1418
1428
  var PURPOSE2 = "janus/second-factor/regenerate-attempts/v1";
1419
1429
  var MAX_LINKS = 32;
1420
1430
  var derived2 = new WeakMap;
@@ -1436,10 +1446,10 @@ function linkHash(sealer, record, window, link) {
1436
1446
  ];
1437
1447
  return hashSecret(createHmac3("sha256", counterKey(sealer)).update(parts.join("\x00")).digest("base64url"));
1438
1448
  }
1439
- async function countRegenerateAttempt(context, sealer, record, where) {
1449
+ async function countAppCodeAttempt(context, sealer, record, where) {
1440
1450
  const now = context.clock.now().getTime();
1441
- const window = Math.floor(now / REGENERATE_WINDOW_MS);
1442
- const times = { now, expiresAt: (window + 2) * REGENERATE_WINDOW_MS };
1451
+ const window = Math.floor(now / APP_CODE_WINDOW_MS);
1452
+ const times = { now, expiresAt: (window + 2) * APP_CODE_WINDOW_MS };
1443
1453
  let spent = 0;
1444
1454
  for (let link = 0;link < MAX_LINKS; link += 1) {
1445
1455
  const hash = linkHash(sealer, record, window, link);
@@ -1493,7 +1503,7 @@ async function regenerateRecoveryCodes(context, type, user, code, options, where
1493
1503
  if (!isActive(factor)) {
1494
1504
  throw refusal(type, "SECOND_FACTOR_NOT_ENROLLED", "the user has no active second factor — recovery codes come with one", where, record);
1495
1505
  }
1496
- const left = await countRegenerateAttempt(context, configured.sealer, record, where);
1506
+ const left = await countAppCodeAttempt(context, configured.sealer, record, where);
1497
1507
  const accepted = acceptCode(configured, record, factor, String(code), now, where);
1498
1508
  if (accepted === null) {
1499
1509
  throw codeInvalid(where, record.id, type.name, left);
@@ -1624,6 +1634,142 @@ async function confirmCode(context, type, finish, challenge, code, at) {
1624
1634
  return finish(proved, where);
1625
1635
  }
1626
1636
 
1637
+ // src/auth/step-up/codes.ts
1638
+ var spend = (context, secret, where) => spendOneTime(context, secret, "stepUp", where, "challenge");
1639
+ async function checkEmailedCode(context, type, user, counted, code, where) {
1640
+ const { token, secret, attemptsLeft } = counted;
1641
+ if (isActive(user.secondFactor)) {
1642
+ await spend(context, secret, where);
1643
+ throw new SecondFactorError2("SECOND_FACTOR_ACTIVE", `${where}: the user's second factor became active since the code was sent — request a step-up again`, { operation: where, userId: user.id, userType: type.name });
1644
+ }
1645
+ if (!codeMatches(token, secret, code)) {
1646
+ if (attemptsLeft === 0)
1647
+ await burnOneTime(context, secret, "stepUp");
1648
+ throw codeInvalid(where, user.id, type.name, attemptsLeft);
1649
+ }
1650
+ await spend(context, secret, where);
1651
+ refuseStale(type, user, token, where, "code");
1652
+ }
1653
+ async function checkAppCode(context, type, user, counted, code, where) {
1654
+ const { secret, attemptsLeft } = counted;
1655
+ const factor = user.secondFactor;
1656
+ if (!isActive(factor)) {
1657
+ await spend(context, secret, where);
1658
+ throw new SecondFactorError2("SECOND_FACTOR_NOT_ENROLLED", `${where}: the user no longer has a second factor — request a step-up again`, { operation: where, userId: user.id, userType: type.name });
1659
+ }
1660
+ const configured = requireSettings(context, where, "the user's second factor is active");
1661
+ const windowLeft = await countAppCodeAttempt(context, configured.sealer, user, where);
1662
+ const now = context.clock.now();
1663
+ const accepted = acceptCode(configured, user, factor, code, now, where);
1664
+ if (accepted === null) {
1665
+ if (attemptsLeft === 0)
1666
+ await burnOneTime(context, secret, "stepUp");
1667
+ throw codeInvalid(where, user.id, type.name, Math.min(attemptsLeft, windowLeft));
1668
+ }
1669
+ await context.store.users.updateUser(user.id, { secondFactor: accepted, updatedAt: now }, user.version);
1670
+ await spend(context, secret, where);
1671
+ }
1672
+
1673
+ // src/auth/step-up/open.ts
1674
+ async function openStepUp(context, type, request, token, secret, where) {
1675
+ const session = (await standingSession(context, request))?.session ?? null;
1676
+ if (session === null || session.userId !== token.userId) {
1677
+ throw await unknownChallenge(context, token, secret, where);
1678
+ }
1679
+ const user = await findRecord(context, token.userId, type.name);
1680
+ if (user === null) {
1681
+ throw await unknownChallenge(context, token, secret, where);
1682
+ }
1683
+ if (!user.active) {
1684
+ await spendOneTime(context, secret, "stepUp", where, "challenge");
1685
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1686
+ userId: user.id,
1687
+ userType: type.name
1688
+ });
1689
+ }
1690
+ return { session, user };
1691
+ }
1692
+
1693
+ // src/auth/step-up/request.ts
1694
+ async function requestStepUp(context, type, user, where) {
1695
+ const record = await getRecord(context, idOf(user), type.name, where);
1696
+ if (!record.active) {
1697
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1698
+ userId: record.id,
1699
+ userType: type.name
1700
+ });
1701
+ }
1702
+ const issued = isActive(record.secondFactor) ? await byApp(context, record, where) : await byEmail(context, type, record, where);
1703
+ await context.store.tokens.spendUserTokens(record.id, "stepUp", context.clock.now(), hashSecret(issued.challenge));
1704
+ return issued;
1705
+ }
1706
+ async function byApp(context, record, where) {
1707
+ requireSettings(context, where, "the user's second factor is active");
1708
+ const { secret, expiresAt } = await issueOneTime(context, {
1709
+ kind: "stepUp",
1710
+ userId: record.id,
1711
+ address: "",
1712
+ ttlMs: context.config.tokenTtlMs.stepUp
1713
+ });
1714
+ return {
1715
+ via: "secondFactor",
1716
+ challenge: secret,
1717
+ expiresAt,
1718
+ user: toUser(record)
1719
+ };
1720
+ }
1721
+ async function byEmail(context, type, record, where) {
1722
+ const email = emailOf(type, record.fields);
1723
+ if (email === null) {
1724
+ throw new NotFoundError2(`${where}: the user has no e-mail`, {
1725
+ userId: record.id,
1726
+ userType: type.name,
1727
+ operation: where
1728
+ });
1729
+ }
1730
+ const { secret, code, expiresAt } = await issueCode(context, {
1731
+ kind: "stepUp",
1732
+ userId: record.id,
1733
+ address: email,
1734
+ ttlMs: context.config.tokenTtlMs.stepUp
1735
+ });
1736
+ return {
1737
+ via: "email",
1738
+ code,
1739
+ challenge: secret,
1740
+ email,
1741
+ expiresAt,
1742
+ user: toUser(record)
1743
+ };
1744
+ }
1745
+
1746
+ // src/auth/step-up/flows.ts
1747
+ function stepUpFlows(context, type, at) {
1748
+ return {
1749
+ async request(user) {
1750
+ return requestStepUp(context, type, user, at("stepUp.request"));
1751
+ },
1752
+ async confirm(request, challenge, code) {
1753
+ return confirmStepUp(context, type, request, String(challenge), String(code), at("stepUp.confirm"));
1754
+ }
1755
+ };
1756
+ }
1757
+ async function confirmStepUp(context, type, request, secret, code, where) {
1758
+ const { token, attemptsLeft } = await countCodeAttempt(context, secret, "stepUp", where, type.name);
1759
+ const { session, user } = await openStepUp(context, type, request, token, secret, where);
1760
+ const counted = { token, secret, attemptsLeft };
1761
+ if (token.codeHash === null) {
1762
+ await checkAppCode(context, type, user, counted, code, where);
1763
+ } else {
1764
+ await checkEmailedCode(context, type, user, counted, code, where);
1765
+ }
1766
+ const confirmed = await context.store.sessions.reauthenticateSession(session.id, context.clock.now());
1767
+ if (confirmed === null) {
1768
+ throw new TokenError2("TOKEN_UNKNOWN", `${where}: the session was signed out while the code was checked`, { operation: where });
1769
+ }
1770
+ return toSession(confirmed);
1771
+ }
1772
+
1627
1773
  // src/auth/users/by-login.ts
1628
1774
  var byLogin = async (context, type, login) => isStorable(login) ? context.store.users.findUserByLogin(type.name, login) : null;
1629
1775
 
@@ -1837,6 +1983,7 @@ function typeApi(context, type) {
1837
1983
  ...passwordFlows(context, type, at, secondFactor.finish),
1838
1984
  secondFactor: secondFactor.api,
1839
1985
  signInCode,
1986
+ stepUp: stepUpFlows(context, type, at),
1840
1987
  ...emailFlows(context, type, at)
1841
1988
  };
1842
1989
  }
@@ -1873,6 +2020,14 @@ function janus(config) {
1873
2020
  const surface = resolved.single ? { ...shared, ...types.user } : { ...shared, ...types };
1874
2021
  return surface;
1875
2022
  }
2023
+ // src/auth/sessions/fresh.ts
2024
+ function assertFresh(session, maxAge, clock = systemClock) {
2025
+ const maxAgeMs = parseDuration(maxAge, "assertFresh: maxAge");
2026
+ const age = clock.now().getTime() - session.authenticatedAt.getTime();
2027
+ if (age >= maxAgeMs) {
2028
+ throw new StepUpRequiredError2("assertFresh: the session proved who it is longer ago than maxAge — confirm with a step-up", { userId: session.userId });
2029
+ }
2030
+ }
1876
2031
  export {
1877
2032
  CredentialError2 as CredentialError,
1878
2033
  DEFAULT_PAGE_SIZE2 as DEFAULT_PAGE_SIZE,
@@ -1882,12 +2037,14 @@ export {
1882
2037
  NotFoundError2 as NotFoundError,
1883
2038
  PermissionDepthError2 as PermissionDepthError,
1884
2039
  SecondFactorError2 as SecondFactorError,
2040
+ StepUpRequiredError2 as StepUpRequiredError,
1885
2041
  StoreConflict2 as StoreConflict,
1886
2042
  StoreFailure2 as StoreFailure,
1887
2043
  TokenError2 as TokenError,
1888
2044
  UnsupportedError2 as UnsupportedError,
1889
2045
  UserInactiveError2 as UserInactiveError,
1890
2046
  UserInvalidError2 as UserInvalidError,
2047
+ assertFresh,
1891
2048
  assertStores,
1892
2049
  bunHasher,
1893
2050
  createMemoryStores2 as createMemoryStores,
@@ -1913,5 +2070,5 @@ export {
1913
2070
  systemClock
1914
2071
  };
1915
2072
 
1916
- //# debugId=863D9CAB1F0683FA64756E2164756E21
2073
+ //# debugId=301D5A6A554F106664756E2164756E21
1917
2074
  //# sourceMappingURL=index.js.map