@calimero-network/mero-js 25.0.3 → 25.0.4

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.
package/dist/index.mjs CHANGED
@@ -9906,15 +9906,70 @@ function base643(bytes) {
9906
9906
  return btoa(binary);
9907
9907
  }
9908
9908
 
9909
+ // src/cloud/link-state.ts
9910
+ var KEY2 = "calimero.cloud.link.pending";
9911
+ var STATE_PARAM = "link_state";
9912
+ var LIFETIME_MS = 10 * 60 * 1e3;
9913
+ var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
9914
+ var MAX_PENDING = 20;
9915
+ function readPending(now) {
9916
+ try {
9917
+ const raw = localStorage.getItem(KEY2);
9918
+ const pending = raw ? JSON.parse(raw) : {};
9919
+ return Object.fromEntries(
9920
+ Object.entries(pending).filter(
9921
+ ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
9922
+ )
9923
+ );
9924
+ } catch {
9925
+ return {};
9926
+ }
9927
+ }
9928
+ function writePending(pending) {
9929
+ try {
9930
+ localStorage.setItem(KEY2, JSON.stringify(pending));
9931
+ } catch {
9932
+ }
9933
+ }
9934
+ function beginLinkState(account, returnTo) {
9935
+ if (!ACCOUNT_HEX.test(account)) {
9936
+ throw new Error("accountId must be 64 lowercase hex characters");
9937
+ }
9938
+ const url = new URL(returnTo);
9939
+ const now = Date.now();
9940
+ const pending = readPending(now);
9941
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
9942
+ const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
9943
+ ""
9944
+ );
9945
+ pending[state] = { account, expiresAt: now + LIFETIME_MS };
9946
+ writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
9947
+ url.searchParams.set(STATE_PARAM, state);
9948
+ return url.toString();
9949
+ }
9950
+ function takeLinkState(search) {
9951
+ const state = new URLSearchParams(search).get(STATE_PARAM);
9952
+ const pending = readPending(Date.now());
9953
+ if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
9954
+ return null;
9955
+ const entry = pending[state];
9956
+ delete pending[state];
9957
+ writePending(pending);
9958
+ return entry;
9959
+ }
9960
+ function withoutLinkState(search) {
9961
+ const params = new URLSearchParams(search);
9962
+ params.delete(STATE_PARAM);
9963
+ const rest = params.toString();
9964
+ return rest ? `?${rest}` : "";
9965
+ }
9966
+
9909
9967
  // src/cloud/link-redirect.ts
9910
9968
  var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
9911
9969
  function cloudLinkUrl(options) {
9912
- if (!/^[0-9a-f]{64}$/.test(options.accountId)) {
9913
- throw new Error("accountId must be 64 lowercase hex characters");
9914
- }
9915
9970
  const url = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
9916
9971
  url.searchParams.set("link-account", options.accountId);
9917
- url.searchParams.set("callback-url", options.returnTo);
9972
+ url.searchParams.set("callback-url", beginLinkState(options.accountId, options.returnTo));
9918
9973
  return url.toString();
9919
9974
  }
9920
9975
  function beginCloudLink(options) {
@@ -9925,6 +9980,7 @@ function readCloudLinkCallback(location2 = window.location) {
9925
9980
  const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
9926
9981
  const error = params.get("error");
9927
9982
  if (error) {
9983
+ takeLinkState(location2.search ?? "");
9928
9984
  stripFragment();
9929
9985
  throw new Error(
9930
9986
  error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
@@ -9933,12 +9989,18 @@ function readCloudLinkCallback(location2 = window.location) {
9933
9989
  const grant = params.get("grant");
9934
9990
  const account = params.get("account");
9935
9991
  if (!grant || !account) return null;
9992
+ if (takeLinkState(location2.search ?? "")?.account !== account) {
9993
+ stripFragment();
9994
+ throw new Error(
9995
+ "this account link answers a request this browser did not make - start the link again from here"
9996
+ );
9997
+ }
9936
9998
  stripFragment();
9937
9999
  return { grant, account };
9938
10000
  }
9939
10001
  function stripFragment() {
9940
10002
  if (typeof history === "undefined" || !history.replaceState) return;
9941
- history.replaceState(null, "", location.pathname + location.search);
10003
+ history.replaceState(null, "", location.pathname + withoutLinkState(location.search));
9942
10004
  }
9943
10005
  async function completeCloudLink(options) {
9944
10006
  const root = await resolveRoot(options.root);
@@ -9958,7 +10020,7 @@ async function completeCloudLink(options) {
9958
10020
  var DEVICE_PARAM = "enrol-device";
9959
10021
  var KEM_PARAM = "enrol-kem";
9960
10022
  var CALLBACK_PARAM = "callback-url";
9961
- var STATE_PARAM = "state";
10023
+ var STATE_PARAM2 = "state";
9962
10024
  function deviceEnrolmentUrl(options) {
9963
10025
  assertHex(options.devicePublicKey, "devicePublicKey");
9964
10026
  assertHex(options.kemPublicKey, "kemPublicKey");
@@ -9968,7 +10030,7 @@ function deviceEnrolmentUrl(options) {
9968
10030
  url.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
9969
10031
  url.searchParams.set(KEM_PARAM, options.kemPublicKey);
9970
10032
  url.searchParams.set(CALLBACK_PARAM, options.returnTo);
9971
- if (options.state) url.searchParams.set(STATE_PARAM, options.state);
10033
+ if (options.state) url.searchParams.set(STATE_PARAM2, options.state);
9972
10034
  return url.toString();
9973
10035
  }
9974
10036
  function beginDeviceEnrolment(options) {
@@ -11066,7 +11128,6 @@ function joinOf(row) {
11066
11128
  reportedAt: typeof join.reported_at === "string" ? join.reported_at : null
11067
11129
  };
11068
11130
  }
11069
- var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
11070
11131
  function accountHexOrNull(value) {
11071
11132
  return typeof value === "string" && ACCOUNT_HEX.test(value) ? value : null;
11072
11133
  }
@@ -11794,7 +11855,7 @@ var CloudClient = class {
11794
11855
  const portal = options.portalUrl.replace(/\/+$/, "");
11795
11856
  const query = new URLSearchParams({
11796
11857
  "link-account": options.accountId,
11797
- "callback-url": options.callbackUrl
11858
+ "callback-url": beginLinkState(options.accountId, options.callbackUrl)
11798
11859
  });
11799
11860
  return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
11800
11861
  }
@@ -11804,14 +11865,19 @@ var CloudClient = class {
11804
11865
  * Returns `{}` when there is nothing to read, so it is safe to call on every
11805
11866
  * load — which is how a redirected app finds out it has been sent back.
11806
11867
  *
11868
+ * A grant is only returned when it answers a handoff this browser began, for the
11869
+ * account that handoff named; anything else comes back as
11870
+ * `error: 'unexpected_callback'`, because the redirect URL can be forwarded.
11871
+ *
11807
11872
  * @param href defaults to the current location, when there is one.
11808
11873
  */
11809
11874
  static readAccountLinkCallback(href) {
11810
11875
  const source = href ?? (typeof window === "undefined" ? "" : window.location.href);
11811
11876
  if (!source) return {};
11812
11877
  let hash;
11878
+ let search;
11813
11879
  try {
11814
- hash = new URL(source).hash;
11880
+ ({ hash, search } = new URL(source));
11815
11881
  } catch {
11816
11882
  return {};
11817
11883
  }
@@ -11820,6 +11886,10 @@ var CloudClient = class {
11820
11886
  const accountId = params.get("account");
11821
11887
  const error = params.get("error");
11822
11888
  const out = {};
11889
+ if (error) takeLinkState(search);
11890
+ if (grant && takeLinkState(search)?.account !== accountId) {
11891
+ return { error: "unexpected_callback" };
11892
+ }
11823
11893
  if (grant) out.grant = grant;
11824
11894
  if (accountId) out.accountId = accountId;
11825
11895
  if (error) out.error = error;