@calimero-network/mero-js 25.3.1 → 25.3.3

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
@@ -2984,8 +2984,73 @@ function createAdminApiClientFromHttpClient(httpClient, _config) {
2984
2984
  return new AdminApiClient(httpClient);
2985
2985
  }
2986
2986
 
2987
+ // src/cloud/link-state.ts
2988
+ var KEY = "calimero.cloud.link.pending";
2989
+ var STATE_PARAM = "link_state";
2990
+ var LIFETIME_MS = 10 * 60 * 1e3;
2991
+ var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
2992
+ var MAX_PENDING = 20;
2993
+ function readPending(key, now) {
2994
+ try {
2995
+ const raw = localStorage.getItem(key);
2996
+ const pending = raw ? JSON.parse(raw) : {};
2997
+ return Object.fromEntries(
2998
+ Object.entries(pending).filter(
2999
+ ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
3000
+ )
3001
+ );
3002
+ } catch {
3003
+ return {};
3004
+ }
3005
+ }
3006
+ function writePending(key, pending) {
3007
+ try {
3008
+ localStorage.setItem(key, JSON.stringify(pending));
3009
+ } catch {
3010
+ }
3011
+ }
3012
+ function beginState(key, payload) {
3013
+ const now = Date.now();
3014
+ const pending = readPending(key, now);
3015
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
3016
+ const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
3017
+ ""
3018
+ );
3019
+ pending[state] = { ...payload, expiresAt: now + LIFETIME_MS };
3020
+ writePending(key, Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
3021
+ return state;
3022
+ }
3023
+ function takeState(key, state) {
3024
+ const pending = readPending(key, Date.now());
3025
+ if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
3026
+ return null;
3027
+ const entry = pending[state];
3028
+ delete pending[state];
3029
+ writePending(key, pending);
3030
+ return entry;
3031
+ }
3032
+ function beginLinkState(account, returnTo) {
3033
+ if (!ACCOUNT_HEX.test(account)) {
3034
+ throw new Error("accountId must be 64 lowercase hex characters");
3035
+ }
3036
+ const url2 = new URL(returnTo);
3037
+ url2.searchParams.set(STATE_PARAM, beginState(KEY, { account }));
3038
+ return url2.toString();
3039
+ }
3040
+ function takeLinkState(search) {
3041
+ return takeState(KEY, new URLSearchParams(search).get(STATE_PARAM));
3042
+ }
3043
+ function withoutLinkState(search) {
3044
+ const params = new URLSearchParams(search);
3045
+ params.delete(STATE_PARAM);
3046
+ const rest = params.toString();
3047
+ return rest ? `?${rest}` : "";
3048
+ }
3049
+
2987
3050
  // src/auth/index.ts
2988
- function parseAuthCallback(url2) {
3051
+ var PENDING_KEY = "calimero.auth.login.pending";
3052
+ var STATE_PARAM2 = "auth_state";
3053
+ function parseAuthCallback(url2, options = {}) {
2989
3054
  try {
2990
3055
  const hashIndex = url2.indexOf("#");
2991
3056
  if (hashIndex === -1) return null;
@@ -2993,6 +3058,12 @@ function parseAuthCallback(url2) {
2993
3058
  const params = new URLSearchParams(hash);
2994
3059
  const accessToken = params.get("access_token");
2995
3060
  if (!accessToken) return null;
3061
+ if (!options.allowUnsolicited && !takeState(PENDING_KEY, stateOf(url2, hashIndex))) {
3062
+ console.warn(
3063
+ "[mero-js] ignoring auth callback tokens: they answer no login begun in this browser"
3064
+ );
3065
+ return null;
3066
+ }
2996
3067
  return {
2997
3068
  accessToken,
2998
3069
  refreshToken: params.get("refresh_token") ?? "",
@@ -3005,9 +3076,16 @@ function parseAuthCallback(url2) {
3005
3076
  return null;
3006
3077
  }
3007
3078
  }
3079
+ function stateOf(url2, hashIndex) {
3080
+ const queryIndex = url2.indexOf("?");
3081
+ if (queryIndex === -1 || queryIndex > hashIndex) return null;
3082
+ return new URLSearchParams(url2.substring(queryIndex + 1, hashIndex)).get(STATE_PARAM2);
3083
+ }
3008
3084
  function buildAuthLoginUrl(nodeUrl, opts) {
3085
+ const callback = new URL(opts.callbackUrl);
3086
+ callback.searchParams.set(STATE_PARAM2, beginState(PENDING_KEY, {}));
3009
3087
  const params = new URLSearchParams();
3010
- params.set("callback-url", opts.callbackUrl);
3088
+ params.set("callback-url", callback.toString());
3011
3089
  if (opts.permissions && opts.permissions.length > 0) {
3012
3090
  params.set("permissions", opts.permissions.join(","));
3013
3091
  }
@@ -3250,7 +3328,8 @@ var SseClient = class {
3250
3328
  return;
3251
3329
  }
3252
3330
  this.closed = false;
3253
- this.abortController = new AbortController();
3331
+ const controller = new AbortController();
3332
+ this.abortController = controller;
3254
3333
  try {
3255
3334
  const url2 = `${this.baseUrl}/sse`;
3256
3335
  const headers = {
@@ -3260,7 +3339,7 @@ var SseClient = class {
3260
3339
  const proofAttached = carriesProof(headers);
3261
3340
  const response = await this.fetchImpl(url2, {
3262
3341
  headers,
3263
- signal: this.abortController.signal,
3342
+ signal: controller.signal,
3264
3343
  ...proofAttached && { redirect: "manual" }
3265
3344
  });
3266
3345
  if (proofAttached) refuseProofRedirect(response, url2);
@@ -3288,21 +3367,31 @@ var SseClient = class {
3288
3367
  if (!response.body) {
3289
3368
  throw new Error("SSE response has no body");
3290
3369
  }
3291
- this.readStream(response.body).catch((err) => {
3292
- if (this.closed) return;
3370
+ this.readStream(response.body, controller).catch((err) => {
3371
+ if (this.superseded(controller)) return;
3293
3372
  const error = err instanceof Error ? err : new Error(String(err));
3294
3373
  this.emit("error", error);
3295
3374
  this.scheduleReconnect();
3296
3375
  });
3297
3376
  } catch (err) {
3298
- if (this.closed) return;
3377
+ if (this.superseded(controller)) return;
3299
3378
  const error = err instanceof Error ? err : new Error(String(err));
3300
3379
  this.emit("error", error);
3301
3380
  if (error instanceof AuthRevokedError) return;
3302
3381
  this.scheduleReconnect();
3303
3382
  }
3304
3383
  }
3305
- async readStream(body) {
3384
+ /**
3385
+ * Whether `controller`'s connection is no longer the one this client holds:
3386
+ * closed, or replaced by a later connect. Its abort lands asynchronously,
3387
+ * after a `close()` + `connect()` has already cleared `closed`, so `closed`
3388
+ * alone cannot tell; reporting it, and reconnecting for it, would abort the
3389
+ * live connection, whose own abort would do the same, without end.
3390
+ */
3391
+ superseded(controller) {
3392
+ return this.closed || controller.signal.aborted || this.abortController !== controller;
3393
+ }
3394
+ async readStream(body, controller) {
3306
3395
  const reader = body.getReader();
3307
3396
  const decoder = new TextDecoder();
3308
3397
  let buffer = "";
@@ -3330,11 +3419,11 @@ var SseClient = class {
3330
3419
  }
3331
3420
  }
3332
3421
  } catch (err) {
3333
- if (this.closed) return;
3422
+ if (this.superseded(controller)) return;
3334
3423
  const error = err instanceof Error ? err : new Error(String(err));
3335
3424
  this.emit("error", error);
3336
3425
  }
3337
- if (!this.closed) {
3426
+ if (!this.superseded(controller)) {
3338
3427
  this.scheduleReconnect();
3339
3428
  }
3340
3429
  }
@@ -9423,12 +9512,12 @@ function createMeroClient(config) {
9423
9512
  }
9424
9513
 
9425
9514
  // src/account/session.ts
9426
- var KEY = "calimero.delegated.connection";
9515
+ var KEY2 = "calimero.delegated.connection";
9427
9516
  var CREDENTIAL_KEY = "calimero.delegated.credential";
9428
9517
  function readDelegatedSession() {
9429
9518
  if (typeof sessionStorage === "undefined") return null;
9430
9519
  try {
9431
- const raw = sessionStorage.getItem(KEY);
9520
+ const raw = sessionStorage.getItem(KEY2);
9432
9521
  return raw ? JSON.parse(raw) : null;
9433
9522
  } catch {
9434
9523
  return null;
@@ -9436,13 +9525,13 @@ function readDelegatedSession() {
9436
9525
  }
9437
9526
  function saveDelegatedSession(s) {
9438
9527
  try {
9439
- sessionStorage.setItem(KEY, JSON.stringify(s));
9528
+ sessionStorage.setItem(KEY2, JSON.stringify(s));
9440
9529
  } catch {
9441
9530
  }
9442
9531
  }
9443
9532
  function clearDelegatedSession() {
9444
9533
  try {
9445
- sessionStorage.removeItem(KEY);
9534
+ sessionStorage.removeItem(KEY2);
9446
9535
  } catch {
9447
9536
  }
9448
9537
  }
@@ -10240,64 +10329,6 @@ function base643(bytes) {
10240
10329
  return btoa(binary);
10241
10330
  }
10242
10331
 
10243
- // src/cloud/link-state.ts
10244
- var KEY2 = "calimero.cloud.link.pending";
10245
- var STATE_PARAM = "link_state";
10246
- var LIFETIME_MS = 10 * 60 * 1e3;
10247
- var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
10248
- var MAX_PENDING = 20;
10249
- function readPending(now) {
10250
- try {
10251
- const raw = localStorage.getItem(KEY2);
10252
- const pending = raw ? JSON.parse(raw) : {};
10253
- return Object.fromEntries(
10254
- Object.entries(pending).filter(
10255
- ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
10256
- )
10257
- );
10258
- } catch {
10259
- return {};
10260
- }
10261
- }
10262
- function writePending(pending) {
10263
- try {
10264
- localStorage.setItem(KEY2, JSON.stringify(pending));
10265
- } catch {
10266
- }
10267
- }
10268
- function beginLinkState(account, returnTo) {
10269
- if (!ACCOUNT_HEX.test(account)) {
10270
- throw new Error("accountId must be 64 lowercase hex characters");
10271
- }
10272
- const url2 = new URL(returnTo);
10273
- const now = Date.now();
10274
- const pending = readPending(now);
10275
- const bytes = crypto.getRandomValues(new Uint8Array(16));
10276
- const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
10277
- ""
10278
- );
10279
- pending[state] = { account, expiresAt: now + LIFETIME_MS };
10280
- writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
10281
- url2.searchParams.set(STATE_PARAM, state);
10282
- return url2.toString();
10283
- }
10284
- function takeLinkState(search) {
10285
- const state = new URLSearchParams(search).get(STATE_PARAM);
10286
- const pending = readPending(Date.now());
10287
- if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
10288
- return null;
10289
- const entry = pending[state];
10290
- delete pending[state];
10291
- writePending(pending);
10292
- return entry;
10293
- }
10294
- function withoutLinkState(search) {
10295
- const params = new URLSearchParams(search);
10296
- params.delete(STATE_PARAM);
10297
- const rest = params.toString();
10298
- return rest ? `?${rest}` : "";
10299
- }
10300
-
10301
10332
  // src/cloud/link-redirect.ts
10302
10333
  var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
10303
10334
  function cloudLinkUrl(options) {
@@ -10354,7 +10385,8 @@ async function completeCloudLink(options) {
10354
10385
  var DEVICE_PARAM = "enrol-device";
10355
10386
  var KEM_PARAM = "enrol-kem";
10356
10387
  var CALLBACK_PARAM = "callback-url";
10357
- var STATE_PARAM2 = "state";
10388
+ var STATE_PARAM3 = "state";
10389
+ var PENDING_KEY2 = "calimero.device.enrol.pending";
10358
10390
  function deviceEnrolmentUrl(options) {
10359
10391
  assertHex(options.devicePublicKey, "devicePublicKey");
10360
10392
  assertHex(options.kemPublicKey, "kemPublicKey");
@@ -10364,7 +10396,7 @@ function deviceEnrolmentUrl(options) {
10364
10396
  url2.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
10365
10397
  url2.searchParams.set(KEM_PARAM, options.kemPublicKey);
10366
10398
  url2.searchParams.set(CALLBACK_PARAM, options.returnTo);
10367
- if (options.state) url2.searchParams.set(STATE_PARAM2, options.state);
10399
+ url2.searchParams.set(STATE_PARAM3, beginState(PENDING_KEY2, {}));
10368
10400
  return url2.toString();
10369
10401
  }
10370
10402
  function beginDeviceEnrolment(options) {
@@ -10375,6 +10407,7 @@ function readEnrolmentCallback(location2 = window.location) {
10375
10407
  const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
10376
10408
  const error = params.get("error");
10377
10409
  if (error) {
10410
+ takeState(PENDING_KEY2, params.get(STATE_PARAM3));
10378
10411
  stripFragment2();
10379
10412
  throw new Error(
10380
10413
  error === "denied" || error === "cancelled" ? "the device was not approved at the wallet" : `the wallet refused this enrolment: ${error}`
@@ -10393,9 +10426,9 @@ function stripFragment2() {
10393
10426
  history.replaceState(null, "", location.pathname + location.search);
10394
10427
  }
10395
10428
  async function completeDeviceEnrolment(options) {
10396
- if (options.expectState !== void 0 && options.expectState !== options.state) {
10429
+ if (!takeState(PENDING_KEY2, options.state)) {
10397
10430
  throw new Error(
10398
- "this enrolment answers a request this app did not make \u2014 the state it carries is not the one sent"
10431
+ "this enrolment answers a request this app did not make - start the enrolment again from here"
10399
10432
  );
10400
10433
  }
10401
10434
  const certificate = await verifyDeviceCredential(options.credential);
@@ -12788,8 +12821,8 @@ var MeroJs = class {
12788
12821
  /**
12789
12822
  * Parse an auth callback URL hash fragment (static utility)
12790
12823
  */
12791
- static parseAuthCallback(url2) {
12792
- return parseAuthCallback(url2);
12824
+ static parseAuthCallback(url2, options) {
12825
+ return parseAuthCallback(url2, options);
12793
12826
  }
12794
12827
  /**
12795
12828
  * Build an auth login URL (static utility)