@nxgt/janus 0.11.0 → 0.13.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 (87) hide show
  1. package/README.md +109 -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/email-flows/reset-password.d.ts.map +1 -1
  7. package/dist/auth/index.d.ts +2 -1
  8. package/dist/auth/index.d.ts.map +1 -1
  9. package/dist/auth/password-written.d.ts +21 -5
  10. package/dist/auth/password-written.d.ts.map +1 -1
  11. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  12. package/dist/auth/port/memory/sessions.d.ts.map +1 -1
  13. package/dist/auth/port/types/sessions.d.ts +16 -1
  14. package/dist/auth/port/types/sessions.d.ts.map +1 -1
  15. package/dist/auth/port/types/tokens.d.ts +10 -4
  16. package/dist/auth/port/types/tokens.d.ts.map +1 -1
  17. package/dist/auth/second-factor/{regenerate-attempts.d.ts → app-code-attempts.d.ts} +13 -10
  18. package/dist/auth/second-factor/app-code-attempts.d.ts.map +1 -0
  19. package/dist/auth/sessions/authenticate.d.ts.map +1 -1
  20. package/dist/auth/sessions/fresh.d.ts +26 -0
  21. package/dist/auth/sessions/fresh.d.ts.map +1 -0
  22. package/dist/auth/sessions/standing.d.ts +20 -0
  23. package/dist/auth/sessions/standing.d.ts.map +1 -0
  24. package/dist/auth/step-up/codes.d.ts +29 -0
  25. package/dist/auth/step-up/codes.d.ts.map +1 -0
  26. package/dist/auth/step-up/flows.d.ts +19 -0
  27. package/dist/auth/step-up/flows.d.ts.map +1 -0
  28. package/dist/auth/step-up/open.d.ts +23 -0
  29. package/dist/auth/step-up/open.d.ts.map +1 -0
  30. package/dist/auth/step-up/request.d.ts +12 -0
  31. package/dist/auth/step-up/request.d.ts.map +1 -0
  32. package/dist/auth/types/email-flows.d.ts +6 -3
  33. package/dist/auth/types/email-flows.d.ts.map +1 -1
  34. package/dist/auth/types/index.d.ts +1 -0
  35. package/dist/auth/types/index.d.ts.map +1 -1
  36. package/dist/auth/types/janus.d.ts +2 -1
  37. package/dist/auth/types/janus.d.ts.map +1 -1
  38. package/dist/auth/types/step-up.d.ts +79 -0
  39. package/dist/auth/types/step-up.d.ts.map +1 -0
  40. package/dist/auth/users/any-type-api.d.ts +2 -2
  41. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  42. package/dist/auth/users/type-api.d.ts +1 -1
  43. package/dist/auth/users/type-api.d.ts.map +1 -1
  44. package/dist/chunks/{index-5e2fw7he.js → index-j2gd8fry.js} +12 -3
  45. package/dist/chunks/{index-5e2fw7he.js.map → index-j2gd8fry.js.map} +4 -4
  46. package/dist/chunks/{index-5tm04t5p.js → index-jp2kka6d.js} +2 -2
  47. package/dist/chunks/{index-6f6wja93.js → index-km7ffctx.js} +2 -2
  48. package/dist/chunks/{index-4m49cye4.js → index-rd2rddvv.js} +15 -12
  49. package/dist/chunks/{index-4m49cye4.js.map → index-rd2rddvv.js.map} +3 -3
  50. package/dist/conformance/cases/outage.d.ts.map +1 -1
  51. package/dist/conformance/cases/sessions/index.d.ts.map +1 -1
  52. package/dist/conformance/cases/sessions/reauthentication.d.ts +4 -0
  53. package/dist/conformance/cases/sessions/reauthentication.d.ts.map +1 -0
  54. package/dist/conformance/cases/tokens/index.d.ts.map +1 -1
  55. package/dist/conformance/cases/tokens/kinds.d.ts +4 -0
  56. package/dist/conformance/cases/tokens/kinds.d.ts.map +1 -0
  57. package/dist/conformance/index.js +147 -60
  58. package/dist/conformance/index.js.map +8 -6
  59. package/dist/errors/janus-error/codes.d.ts +7 -0
  60. package/dist/errors/janus-error/codes.d.ts.map +1 -1
  61. package/dist/errors/janus-error/index.d.ts +1 -1
  62. package/dist/errors/janus-error/index.d.ts.map +1 -1
  63. package/dist/errors/janus-error/refusals.d.ts +8 -0
  64. package/dist/errors/janus-error/refusals.d.ts.map +1 -1
  65. package/dist/errors/janus-error/status.d.ts.map +1 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +185 -23
  69. package/dist/index.js.map +20 -14
  70. package/dist/permissions/index.js +3 -3
  71. package/docs/README.md +2 -1
  72. package/docs/guide/adapters.md +41 -6
  73. package/docs/guide/email-flows.md +40 -1
  74. package/docs/guide/errors.md +1 -0
  75. package/docs/guide/events.md +1 -1
  76. package/docs/guide/passwords.md +22 -0
  77. package/docs/guide/second-factor.md +22 -16
  78. package/docs/guide/sessions.md +7 -0
  79. package/docs/guide/step-up.md +178 -0
  80. package/docs/guide/users.md +8 -3
  81. package/docs/guide/vocabulary.md +3 -2
  82. package/docs/roadmap.md +19 -24
  83. package/docs/troubleshooting.md +193 -8
  84. package/package.json +1 -1
  85. package/dist/auth/second-factor/regenerate-attempts.d.ts.map +0 -1
  86. /package/dist/chunks/{index-5tm04t5p.js.map → index-jp2kka6d.js.map} +0 -0
  87. /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;
@@ -968,8 +978,12 @@ async function insertOneTime(context, token, code) {
968
978
  return { secret, expiresAt };
969
979
  }
970
980
  // src/auth/password-written.ts
971
- async function endSignInsWaiting(context, userId) {
972
- await context.store.tokens.spendUserTokens(userId, "secondFactor", context.clock.now());
981
+ async function endWhatThePasswordOpened(context, userId) {
982
+ const now = context.clock.now();
983
+ const spent = await Promise.allSettled(["resetPassword", "secondFactor"].map((kind) => context.store.tokens.spendUserTokens(userId, kind, now)));
984
+ const failed = spent.find((result) => result.status === "rejected");
985
+ if (failed !== undefined)
986
+ throw failed.reason;
973
987
  }
974
988
  async function heldByPassword(context, type, verified, password, result, where) {
975
989
  const now = await findRecord(context, verified.id, type.name);
@@ -1021,6 +1035,7 @@ function resetPasswordFlows(context, type, at) {
1021
1035
  if (record === null)
1022
1036
  return null;
1023
1037
  const issued = await issueEmailToken(context, type, "resetPassword", record, where);
1038
+ await context.store.tokens.spendUserTokens(record.id, "resetPassword", context.clock.now(), hashSecret(issued.token));
1024
1039
  return { ...issued, user: toUser(record) };
1025
1040
  },
1026
1041
  async confirm(secret, password) {
@@ -1044,7 +1059,7 @@ async function confirmReset(context, type, secret, password, where) {
1044
1059
  });
1045
1060
  try {
1046
1061
  await context.store.sessions.revokeUserSessions(written.id, context.clock.now());
1047
- await endSignInsWaiting(context, written.id);
1062
+ await endWhatThePasswordOpened(context, written.id);
1048
1063
  } finally {
1049
1064
  await emit(context, "user.passwordReset", written, written.updatedAt);
1050
1065
  if (newlyVerified) {
@@ -1411,10 +1426,10 @@ function accountOf(type, record, where) {
1411
1426
  return String(record.fields[type.password.login]);
1412
1427
  }
1413
1428
 
1414
- // src/auth/second-factor/regenerate-attempts.ts
1429
+ // src/auth/second-factor/app-code-attempts.ts
1415
1430
  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;
1431
+ var APP_CODE_WINDOW_MS = 15 * 60000;
1432
+ var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
1418
1433
  var PURPOSE2 = "janus/second-factor/regenerate-attempts/v1";
1419
1434
  var MAX_LINKS = 32;
1420
1435
  var derived2 = new WeakMap;
@@ -1436,10 +1451,10 @@ function linkHash(sealer, record, window, link) {
1436
1451
  ];
1437
1452
  return hashSecret(createHmac3("sha256", counterKey(sealer)).update(parts.join("\x00")).digest("base64url"));
1438
1453
  }
1439
- async function countRegenerateAttempt(context, sealer, record, where) {
1454
+ async function countAppCodeAttempt(context, sealer, record, where) {
1440
1455
  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 };
1456
+ const window = Math.floor(now / APP_CODE_WINDOW_MS);
1457
+ const times = { now, expiresAt: (window + 2) * APP_CODE_WINDOW_MS };
1443
1458
  let spent = 0;
1444
1459
  for (let link = 0;link < MAX_LINKS; link += 1) {
1445
1460
  const hash = linkHash(sealer, record, window, link);
@@ -1493,7 +1508,7 @@ async function regenerateRecoveryCodes(context, type, user, code, options, where
1493
1508
  if (!isActive(factor)) {
1494
1509
  throw refusal(type, "SECOND_FACTOR_NOT_ENROLLED", "the user has no active second factor — recovery codes come with one", where, record);
1495
1510
  }
1496
- const left = await countRegenerateAttempt(context, configured.sealer, record, where);
1511
+ const left = await countAppCodeAttempt(context, configured.sealer, record, where);
1497
1512
  const accepted = acceptCode(configured, record, factor, String(code), now, where);
1498
1513
  if (accepted === null) {
1499
1514
  throw codeInvalid(where, record.id, type.name, left);
@@ -1624,6 +1639,142 @@ async function confirmCode(context, type, finish, challenge, code, at) {
1624
1639
  return finish(proved, where);
1625
1640
  }
1626
1641
 
1642
+ // src/auth/step-up/codes.ts
1643
+ var spend = (context, secret, where) => spendOneTime(context, secret, "stepUp", where, "challenge");
1644
+ async function checkEmailedCode(context, type, user, counted, code, where) {
1645
+ const { token, secret, attemptsLeft } = counted;
1646
+ if (isActive(user.secondFactor)) {
1647
+ await spend(context, secret, where);
1648
+ 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 });
1649
+ }
1650
+ if (!codeMatches(token, secret, code)) {
1651
+ if (attemptsLeft === 0)
1652
+ await burnOneTime(context, secret, "stepUp");
1653
+ throw codeInvalid(where, user.id, type.name, attemptsLeft);
1654
+ }
1655
+ await spend(context, secret, where);
1656
+ refuseStale(type, user, token, where, "code");
1657
+ }
1658
+ async function checkAppCode(context, type, user, counted, code, where) {
1659
+ const { secret, attemptsLeft } = counted;
1660
+ const factor = user.secondFactor;
1661
+ if (!isActive(factor)) {
1662
+ await spend(context, secret, where);
1663
+ 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 });
1664
+ }
1665
+ const configured = requireSettings(context, where, "the user's second factor is active");
1666
+ const windowLeft = await countAppCodeAttempt(context, configured.sealer, user, where);
1667
+ const now = context.clock.now();
1668
+ const accepted = acceptCode(configured, user, factor, code, now, where);
1669
+ if (accepted === null) {
1670
+ if (attemptsLeft === 0)
1671
+ await burnOneTime(context, secret, "stepUp");
1672
+ throw codeInvalid(where, user.id, type.name, Math.min(attemptsLeft, windowLeft));
1673
+ }
1674
+ await context.store.users.updateUser(user.id, { secondFactor: accepted, updatedAt: now }, user.version);
1675
+ await spend(context, secret, where);
1676
+ }
1677
+
1678
+ // src/auth/step-up/open.ts
1679
+ async function openStepUp(context, type, request, token, secret, where) {
1680
+ const session = (await standingSession(context, request))?.session ?? null;
1681
+ if (session === null || session.userId !== token.userId) {
1682
+ throw await unknownChallenge(context, token, secret, where);
1683
+ }
1684
+ const user = await findRecord(context, token.userId, type.name);
1685
+ if (user === null) {
1686
+ throw await unknownChallenge(context, token, secret, where);
1687
+ }
1688
+ if (!user.active) {
1689
+ await spendOneTime(context, secret, "stepUp", where, "challenge");
1690
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1691
+ userId: user.id,
1692
+ userType: type.name
1693
+ });
1694
+ }
1695
+ return { session, user };
1696
+ }
1697
+
1698
+ // src/auth/step-up/request.ts
1699
+ async function requestStepUp(context, type, user, where) {
1700
+ const record = await getRecord(context, idOf(user), type.name, where);
1701
+ if (!record.active) {
1702
+ throw new UserInactiveError2(`${where}: the user is inactive`, {
1703
+ userId: record.id,
1704
+ userType: type.name
1705
+ });
1706
+ }
1707
+ const issued = isActive(record.secondFactor) ? await byApp(context, record, where) : await byEmail(context, type, record, where);
1708
+ await context.store.tokens.spendUserTokens(record.id, "stepUp", context.clock.now(), hashSecret(issued.challenge));
1709
+ return issued;
1710
+ }
1711
+ async function byApp(context, record, where) {
1712
+ requireSettings(context, where, "the user's second factor is active");
1713
+ const { secret, expiresAt } = await issueOneTime(context, {
1714
+ kind: "stepUp",
1715
+ userId: record.id,
1716
+ address: "",
1717
+ ttlMs: context.config.tokenTtlMs.stepUp
1718
+ });
1719
+ return {
1720
+ via: "secondFactor",
1721
+ challenge: secret,
1722
+ expiresAt,
1723
+ user: toUser(record)
1724
+ };
1725
+ }
1726
+ async function byEmail(context, type, record, where) {
1727
+ const email = emailOf(type, record.fields);
1728
+ if (email === null) {
1729
+ throw new NotFoundError2(`${where}: the user has no e-mail`, {
1730
+ userId: record.id,
1731
+ userType: type.name,
1732
+ operation: where
1733
+ });
1734
+ }
1735
+ const { secret, code, expiresAt } = await issueCode(context, {
1736
+ kind: "stepUp",
1737
+ userId: record.id,
1738
+ address: email,
1739
+ ttlMs: context.config.tokenTtlMs.stepUp
1740
+ });
1741
+ return {
1742
+ via: "email",
1743
+ code,
1744
+ challenge: secret,
1745
+ email,
1746
+ expiresAt,
1747
+ user: toUser(record)
1748
+ };
1749
+ }
1750
+
1751
+ // src/auth/step-up/flows.ts
1752
+ function stepUpFlows(context, type, at) {
1753
+ return {
1754
+ async request(user) {
1755
+ return requestStepUp(context, type, user, at("stepUp.request"));
1756
+ },
1757
+ async confirm(request, challenge, code) {
1758
+ return confirmStepUp(context, type, request, String(challenge), String(code), at("stepUp.confirm"));
1759
+ }
1760
+ };
1761
+ }
1762
+ async function confirmStepUp(context, type, request, secret, code, where) {
1763
+ const { token, attemptsLeft } = await countCodeAttempt(context, secret, "stepUp", where, type.name);
1764
+ const { session, user } = await openStepUp(context, type, request, token, secret, where);
1765
+ const counted = { token, secret, attemptsLeft };
1766
+ if (token.codeHash === null) {
1767
+ await checkAppCode(context, type, user, counted, code, where);
1768
+ } else {
1769
+ await checkEmailedCode(context, type, user, counted, code, where);
1770
+ }
1771
+ const confirmed = await context.store.sessions.reauthenticateSession(session.id, context.clock.now());
1772
+ if (confirmed === null) {
1773
+ throw new TokenError2("TOKEN_UNKNOWN", `${where}: the session was signed out while the code was checked`, { operation: where });
1774
+ }
1775
+ return toSession(confirmed);
1776
+ }
1777
+
1627
1778
  // src/auth/users/by-login.ts
1628
1779
  var byLogin = async (context, type, login) => isStorable(login) ? context.store.users.findUserByLogin(type.name, login) : null;
1629
1780
 
@@ -1647,7 +1798,7 @@ async function changePassword(context, type, user, change, options, where) {
1647
1798
  }
1648
1799
  };
1649
1800
  });
1650
- await endSignInsWaiting(context, written.id);
1801
+ await endWhatThePasswordOpened(context, written.id);
1651
1802
  return written;
1652
1803
  }
1653
1804
 
@@ -1738,7 +1889,7 @@ function passwordFlows(context, type, at, finish) {
1738
1889
  checkPassword(type, password, where);
1739
1890
  const hash = await requireHasher(context, where).hash(password);
1740
1891
  const written = await writeUser(context, user, type, options, where, (_, now) => ({ password: { hash, updatedAt: now } }));
1741
- await endSignInsWaiting(context, written.id);
1892
+ await endWhatThePasswordOpened(context, written.id);
1742
1893
  return toUser(written);
1743
1894
  },
1744
1895
  async changePassword(user, change, options) {
@@ -1837,6 +1988,7 @@ function typeApi(context, type) {
1837
1988
  ...passwordFlows(context, type, at, secondFactor.finish),
1838
1989
  secondFactor: secondFactor.api,
1839
1990
  signInCode,
1991
+ stepUp: stepUpFlows(context, type, at),
1840
1992
  ...emailFlows(context, type, at)
1841
1993
  };
1842
1994
  }
@@ -1873,6 +2025,14 @@ function janus(config) {
1873
2025
  const surface = resolved.single ? { ...shared, ...types.user } : { ...shared, ...types };
1874
2026
  return surface;
1875
2027
  }
2028
+ // src/auth/sessions/fresh.ts
2029
+ function assertFresh(session, maxAge, clock = systemClock) {
2030
+ const maxAgeMs = parseDuration(maxAge, "assertFresh: maxAge");
2031
+ const age = clock.now().getTime() - session.authenticatedAt.getTime();
2032
+ if (age >= maxAgeMs) {
2033
+ throw new StepUpRequiredError2("assertFresh: the session proved who it is longer ago than maxAge — confirm with a step-up", { userId: session.userId });
2034
+ }
2035
+ }
1876
2036
  export {
1877
2037
  CredentialError2 as CredentialError,
1878
2038
  DEFAULT_PAGE_SIZE2 as DEFAULT_PAGE_SIZE,
@@ -1882,12 +2042,14 @@ export {
1882
2042
  NotFoundError2 as NotFoundError,
1883
2043
  PermissionDepthError2 as PermissionDepthError,
1884
2044
  SecondFactorError2 as SecondFactorError,
2045
+ StepUpRequiredError2 as StepUpRequiredError,
1885
2046
  StoreConflict2 as StoreConflict,
1886
2047
  StoreFailure2 as StoreFailure,
1887
2048
  TokenError2 as TokenError,
1888
2049
  UnsupportedError2 as UnsupportedError,
1889
2050
  UserInactiveError2 as UserInactiveError,
1890
2051
  UserInvalidError2 as UserInvalidError,
2052
+ assertFresh,
1891
2053
  assertStores,
1892
2054
  bunHasher,
1893
2055
  createMemoryStores2 as createMemoryStores,
@@ -1913,5 +2075,5 @@ export {
1913
2075
  systemClock
1914
2076
  };
1915
2077
 
1916
- //# debugId=863D9CAB1F0683FA64756E2164756E21
2078
+ //# debugId=D61F9F351D899C2964756E2164756E21
1917
2079
  //# sourceMappingURL=index.js.map