@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.cjs CHANGED
@@ -10191,15 +10191,70 @@ function base643(bytes) {
10191
10191
  return btoa(binary);
10192
10192
  }
10193
10193
 
10194
+ // src/cloud/link-state.ts
10195
+ var KEY2 = "calimero.cloud.link.pending";
10196
+ var STATE_PARAM = "link_state";
10197
+ var LIFETIME_MS = 10 * 60 * 1e3;
10198
+ var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
10199
+ var MAX_PENDING = 20;
10200
+ function readPending(now) {
10201
+ try {
10202
+ const raw = localStorage.getItem(KEY2);
10203
+ const pending = raw ? JSON.parse(raw) : {};
10204
+ return Object.fromEntries(
10205
+ Object.entries(pending).filter(
10206
+ ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
10207
+ )
10208
+ );
10209
+ } catch {
10210
+ return {};
10211
+ }
10212
+ }
10213
+ function writePending(pending) {
10214
+ try {
10215
+ localStorage.setItem(KEY2, JSON.stringify(pending));
10216
+ } catch {
10217
+ }
10218
+ }
10219
+ function beginLinkState(account, returnTo) {
10220
+ if (!ACCOUNT_HEX.test(account)) {
10221
+ throw new Error("accountId must be 64 lowercase hex characters");
10222
+ }
10223
+ const url = new URL(returnTo);
10224
+ const now = Date.now();
10225
+ const pending = readPending(now);
10226
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
10227
+ const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
10228
+ ""
10229
+ );
10230
+ pending[state] = { account, expiresAt: now + LIFETIME_MS };
10231
+ writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
10232
+ url.searchParams.set(STATE_PARAM, state);
10233
+ return url.toString();
10234
+ }
10235
+ function takeLinkState(search) {
10236
+ const state = new URLSearchParams(search).get(STATE_PARAM);
10237
+ const pending = readPending(Date.now());
10238
+ if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
10239
+ return null;
10240
+ const entry = pending[state];
10241
+ delete pending[state];
10242
+ writePending(pending);
10243
+ return entry;
10244
+ }
10245
+ function withoutLinkState(search) {
10246
+ const params = new URLSearchParams(search);
10247
+ params.delete(STATE_PARAM);
10248
+ const rest = params.toString();
10249
+ return rest ? `?${rest}` : "";
10250
+ }
10251
+
10194
10252
  // src/cloud/link-redirect.ts
10195
10253
  var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
10196
10254
  function cloudLinkUrl(options) {
10197
- if (!/^[0-9a-f]{64}$/.test(options.accountId)) {
10198
- throw new Error("accountId must be 64 lowercase hex characters");
10199
- }
10200
10255
  const url = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
10201
10256
  url.searchParams.set("link-account", options.accountId);
10202
- url.searchParams.set("callback-url", options.returnTo);
10257
+ url.searchParams.set("callback-url", beginLinkState(options.accountId, options.returnTo));
10203
10258
  return url.toString();
10204
10259
  }
10205
10260
  function beginCloudLink(options) {
@@ -10210,6 +10265,7 @@ function readCloudLinkCallback(location2 = window.location) {
10210
10265
  const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
10211
10266
  const error = params.get("error");
10212
10267
  if (error) {
10268
+ takeLinkState(location2.search ?? "");
10213
10269
  stripFragment();
10214
10270
  throw new Error(
10215
10271
  error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
@@ -10218,12 +10274,18 @@ function readCloudLinkCallback(location2 = window.location) {
10218
10274
  const grant = params.get("grant");
10219
10275
  const account = params.get("account");
10220
10276
  if (!grant || !account) return null;
10277
+ if (takeLinkState(location2.search ?? "")?.account !== account) {
10278
+ stripFragment();
10279
+ throw new Error(
10280
+ "this account link answers a request this browser did not make - start the link again from here"
10281
+ );
10282
+ }
10221
10283
  stripFragment();
10222
10284
  return { grant, account };
10223
10285
  }
10224
10286
  function stripFragment() {
10225
10287
  if (typeof history === "undefined" || !history.replaceState) return;
10226
- history.replaceState(null, "", location.pathname + location.search);
10288
+ history.replaceState(null, "", location.pathname + withoutLinkState(location.search));
10227
10289
  }
10228
10290
  async function completeCloudLink(options) {
10229
10291
  const root = await resolveRoot(options.root);
@@ -10243,7 +10305,7 @@ async function completeCloudLink(options) {
10243
10305
  var DEVICE_PARAM = "enrol-device";
10244
10306
  var KEM_PARAM = "enrol-kem";
10245
10307
  var CALLBACK_PARAM = "callback-url";
10246
- var STATE_PARAM = "state";
10308
+ var STATE_PARAM2 = "state";
10247
10309
  function deviceEnrolmentUrl(options) {
10248
10310
  assertHex(options.devicePublicKey, "devicePublicKey");
10249
10311
  assertHex(options.kemPublicKey, "kemPublicKey");
@@ -10253,7 +10315,7 @@ function deviceEnrolmentUrl(options) {
10253
10315
  url.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
10254
10316
  url.searchParams.set(KEM_PARAM, options.kemPublicKey);
10255
10317
  url.searchParams.set(CALLBACK_PARAM, options.returnTo);
10256
- if (options.state) url.searchParams.set(STATE_PARAM, options.state);
10318
+ if (options.state) url.searchParams.set(STATE_PARAM2, options.state);
10257
10319
  return url.toString();
10258
10320
  }
10259
10321
  function beginDeviceEnrolment(options) {
@@ -11351,7 +11413,6 @@ function joinOf(row) {
11351
11413
  reportedAt: typeof join.reported_at === "string" ? join.reported_at : null
11352
11414
  };
11353
11415
  }
11354
- var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
11355
11416
  function accountHexOrNull(value) {
11356
11417
  return typeof value === "string" && ACCOUNT_HEX.test(value) ? value : null;
11357
11418
  }
@@ -12079,7 +12140,7 @@ var CloudClient = class {
12079
12140
  const portal = options.portalUrl.replace(/\/+$/, "");
12080
12141
  const query = new URLSearchParams({
12081
12142
  "link-account": options.accountId,
12082
- "callback-url": options.callbackUrl
12143
+ "callback-url": beginLinkState(options.accountId, options.callbackUrl)
12083
12144
  });
12084
12145
  return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
12085
12146
  }
@@ -12089,14 +12150,19 @@ var CloudClient = class {
12089
12150
  * Returns `{}` when there is nothing to read, so it is safe to call on every
12090
12151
  * load — which is how a redirected app finds out it has been sent back.
12091
12152
  *
12153
+ * A grant is only returned when it answers a handoff this browser began, for the
12154
+ * account that handoff named; anything else comes back as
12155
+ * `error: 'unexpected_callback'`, because the redirect URL can be forwarded.
12156
+ *
12092
12157
  * @param href defaults to the current location, when there is one.
12093
12158
  */
12094
12159
  static readAccountLinkCallback(href) {
12095
12160
  const source = href ?? (typeof window === "undefined" ? "" : window.location.href);
12096
12161
  if (!source) return {};
12097
12162
  let hash;
12163
+ let search;
12098
12164
  try {
12099
- hash = new URL(source).hash;
12165
+ ({ hash, search } = new URL(source));
12100
12166
  } catch {
12101
12167
  return {};
12102
12168
  }
@@ -12105,6 +12171,10 @@ var CloudClient = class {
12105
12171
  const accountId = params.get("account");
12106
12172
  const error = params.get("error");
12107
12173
  const out = {};
12174
+ if (error) takeLinkState(search);
12175
+ if (grant && takeLinkState(search)?.account !== accountId) {
12176
+ return { error: "unexpected_callback" };
12177
+ }
12108
12178
  if (grant) out.grant = grant;
12109
12179
  if (accountId) out.accountId = accountId;
12110
12180
  if (error) out.error = error;