@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.d.ts CHANGED
@@ -5,7 +5,7 @@ export * from "./auth-api/index.js";
5
5
  export * from "./admin-api/index.js";
6
6
  export * from "./errors/index.js";
7
7
  export { parseAuthCallback, buildAuthLoginUrl } from "./auth/index.js";
8
- export type { AuthCallbackResult, AuthLoginOptions } from "./auth/index.js";
8
+ export type { AuthCallbackResult, AuthLoginOptions, ParseAuthCallbackOptions, } from "./auth/index.js";
9
9
  export { MemoryTokenStore, LocalStorageTokenStore, } from "./token-store/index.js";
10
10
  export type { TokenStore } from "./token-store/index.js";
11
11
  export { RpcClient, RpcError } from "./rpc/index.js";
package/dist/index.mjs CHANGED
@@ -2684,8 +2684,73 @@ function createAdminApiClientFromHttpClient(httpClient, _config) {
2684
2684
  return new AdminApiClient(httpClient);
2685
2685
  }
2686
2686
 
2687
+ // src/cloud/link-state.ts
2688
+ var KEY = "calimero.cloud.link.pending";
2689
+ var STATE_PARAM = "link_state";
2690
+ var LIFETIME_MS = 10 * 60 * 1e3;
2691
+ var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
2692
+ var MAX_PENDING = 20;
2693
+ function readPending(key, now) {
2694
+ try {
2695
+ const raw = localStorage.getItem(key);
2696
+ const pending = raw ? JSON.parse(raw) : {};
2697
+ return Object.fromEntries(
2698
+ Object.entries(pending).filter(
2699
+ ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
2700
+ )
2701
+ );
2702
+ } catch {
2703
+ return {};
2704
+ }
2705
+ }
2706
+ function writePending(key, pending) {
2707
+ try {
2708
+ localStorage.setItem(key, JSON.stringify(pending));
2709
+ } catch {
2710
+ }
2711
+ }
2712
+ function beginState(key, payload) {
2713
+ const now = Date.now();
2714
+ const pending = readPending(key, now);
2715
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
2716
+ const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
2717
+ ""
2718
+ );
2719
+ pending[state] = { ...payload, expiresAt: now + LIFETIME_MS };
2720
+ writePending(key, Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
2721
+ return state;
2722
+ }
2723
+ function takeState(key, state) {
2724
+ const pending = readPending(key, Date.now());
2725
+ if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
2726
+ return null;
2727
+ const entry = pending[state];
2728
+ delete pending[state];
2729
+ writePending(key, pending);
2730
+ return entry;
2731
+ }
2732
+ function beginLinkState(account, returnTo) {
2733
+ if (!ACCOUNT_HEX.test(account)) {
2734
+ throw new Error("accountId must be 64 lowercase hex characters");
2735
+ }
2736
+ const url2 = new URL(returnTo);
2737
+ url2.searchParams.set(STATE_PARAM, beginState(KEY, { account }));
2738
+ return url2.toString();
2739
+ }
2740
+ function takeLinkState(search) {
2741
+ return takeState(KEY, new URLSearchParams(search).get(STATE_PARAM));
2742
+ }
2743
+ function withoutLinkState(search) {
2744
+ const params = new URLSearchParams(search);
2745
+ params.delete(STATE_PARAM);
2746
+ const rest = params.toString();
2747
+ return rest ? `?${rest}` : "";
2748
+ }
2749
+
2687
2750
  // src/auth/index.ts
2688
- function parseAuthCallback(url2) {
2751
+ var PENDING_KEY = "calimero.auth.login.pending";
2752
+ var STATE_PARAM2 = "auth_state";
2753
+ function parseAuthCallback(url2, options = {}) {
2689
2754
  try {
2690
2755
  const hashIndex = url2.indexOf("#");
2691
2756
  if (hashIndex === -1) return null;
@@ -2693,6 +2758,12 @@ function parseAuthCallback(url2) {
2693
2758
  const params = new URLSearchParams(hash);
2694
2759
  const accessToken = params.get("access_token");
2695
2760
  if (!accessToken) return null;
2761
+ if (!options.allowUnsolicited && !takeState(PENDING_KEY, stateOf(url2, hashIndex))) {
2762
+ console.warn(
2763
+ "[mero-js] ignoring auth callback tokens: they answer no login begun in this browser"
2764
+ );
2765
+ return null;
2766
+ }
2696
2767
  return {
2697
2768
  accessToken,
2698
2769
  refreshToken: params.get("refresh_token") ?? "",
@@ -2705,9 +2776,16 @@ function parseAuthCallback(url2) {
2705
2776
  return null;
2706
2777
  }
2707
2778
  }
2779
+ function stateOf(url2, hashIndex) {
2780
+ const queryIndex = url2.indexOf("?");
2781
+ if (queryIndex === -1 || queryIndex > hashIndex) return null;
2782
+ return new URLSearchParams(url2.substring(queryIndex + 1, hashIndex)).get(STATE_PARAM2);
2783
+ }
2708
2784
  function buildAuthLoginUrl(nodeUrl, opts) {
2785
+ const callback = new URL(opts.callbackUrl);
2786
+ callback.searchParams.set(STATE_PARAM2, beginState(PENDING_KEY, {}));
2709
2787
  const params = new URLSearchParams();
2710
- params.set("callback-url", opts.callbackUrl);
2788
+ params.set("callback-url", callback.toString());
2711
2789
  if (opts.permissions && opts.permissions.length > 0) {
2712
2790
  params.set("permissions", opts.permissions.join(","));
2713
2791
  }
@@ -2950,7 +3028,8 @@ var SseClient = class {
2950
3028
  return;
2951
3029
  }
2952
3030
  this.closed = false;
2953
- this.abortController = new AbortController();
3031
+ const controller = new AbortController();
3032
+ this.abortController = controller;
2954
3033
  try {
2955
3034
  const url2 = `${this.baseUrl}/sse`;
2956
3035
  const headers = {
@@ -2960,7 +3039,7 @@ var SseClient = class {
2960
3039
  const proofAttached = carriesProof(headers);
2961
3040
  const response = await this.fetchImpl(url2, {
2962
3041
  headers,
2963
- signal: this.abortController.signal,
3042
+ signal: controller.signal,
2964
3043
  ...proofAttached && { redirect: "manual" }
2965
3044
  });
2966
3045
  if (proofAttached) refuseProofRedirect(response, url2);
@@ -2988,21 +3067,31 @@ var SseClient = class {
2988
3067
  if (!response.body) {
2989
3068
  throw new Error("SSE response has no body");
2990
3069
  }
2991
- this.readStream(response.body).catch((err) => {
2992
- if (this.closed) return;
3070
+ this.readStream(response.body, controller).catch((err) => {
3071
+ if (this.superseded(controller)) return;
2993
3072
  const error = err instanceof Error ? err : new Error(String(err));
2994
3073
  this.emit("error", error);
2995
3074
  this.scheduleReconnect();
2996
3075
  });
2997
3076
  } catch (err) {
2998
- if (this.closed) return;
3077
+ if (this.superseded(controller)) return;
2999
3078
  const error = err instanceof Error ? err : new Error(String(err));
3000
3079
  this.emit("error", error);
3001
3080
  if (error instanceof AuthRevokedError) return;
3002
3081
  this.scheduleReconnect();
3003
3082
  }
3004
3083
  }
3005
- async readStream(body) {
3084
+ /**
3085
+ * Whether `controller`'s connection is no longer the one this client holds:
3086
+ * closed, or replaced by a later connect. Its abort lands asynchronously,
3087
+ * after a `close()` + `connect()` has already cleared `closed`, so `closed`
3088
+ * alone cannot tell; reporting it, and reconnecting for it, would abort the
3089
+ * live connection, whose own abort would do the same, without end.
3090
+ */
3091
+ superseded(controller) {
3092
+ return this.closed || controller.signal.aborted || this.abortController !== controller;
3093
+ }
3094
+ async readStream(body, controller) {
3006
3095
  const reader = body.getReader();
3007
3096
  const decoder = new TextDecoder();
3008
3097
  let buffer = "";
@@ -3030,11 +3119,11 @@ var SseClient = class {
3030
3119
  }
3031
3120
  }
3032
3121
  } catch (err) {
3033
- if (this.closed) return;
3122
+ if (this.superseded(controller)) return;
3034
3123
  const error = err instanceof Error ? err : new Error(String(err));
3035
3124
  this.emit("error", error);
3036
3125
  }
3037
- if (!this.closed) {
3126
+ if (!this.superseded(controller)) {
3038
3127
  this.scheduleReconnect();
3039
3128
  }
3040
3129
  }
@@ -9123,12 +9212,12 @@ function createMeroClient(config) {
9123
9212
  }
9124
9213
 
9125
9214
  // src/account/session.ts
9126
- var KEY = "calimero.delegated.connection";
9215
+ var KEY2 = "calimero.delegated.connection";
9127
9216
  var CREDENTIAL_KEY = "calimero.delegated.credential";
9128
9217
  function readDelegatedSession() {
9129
9218
  if (typeof sessionStorage === "undefined") return null;
9130
9219
  try {
9131
- const raw = sessionStorage.getItem(KEY);
9220
+ const raw = sessionStorage.getItem(KEY2);
9132
9221
  return raw ? JSON.parse(raw) : null;
9133
9222
  } catch {
9134
9223
  return null;
@@ -9136,13 +9225,13 @@ function readDelegatedSession() {
9136
9225
  }
9137
9226
  function saveDelegatedSession(s) {
9138
9227
  try {
9139
- sessionStorage.setItem(KEY, JSON.stringify(s));
9228
+ sessionStorage.setItem(KEY2, JSON.stringify(s));
9140
9229
  } catch {
9141
9230
  }
9142
9231
  }
9143
9232
  function clearDelegatedSession() {
9144
9233
  try {
9145
- sessionStorage.removeItem(KEY);
9234
+ sessionStorage.removeItem(KEY2);
9146
9235
  } catch {
9147
9236
  }
9148
9237
  }
@@ -9940,64 +10029,6 @@ function base643(bytes) {
9940
10029
  return btoa(binary);
9941
10030
  }
9942
10031
 
9943
- // src/cloud/link-state.ts
9944
- var KEY2 = "calimero.cloud.link.pending";
9945
- var STATE_PARAM = "link_state";
9946
- var LIFETIME_MS = 10 * 60 * 1e3;
9947
- var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
9948
- var MAX_PENDING = 20;
9949
- function readPending(now) {
9950
- try {
9951
- const raw = localStorage.getItem(KEY2);
9952
- const pending = raw ? JSON.parse(raw) : {};
9953
- return Object.fromEntries(
9954
- Object.entries(pending).filter(
9955
- ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
9956
- )
9957
- );
9958
- } catch {
9959
- return {};
9960
- }
9961
- }
9962
- function writePending(pending) {
9963
- try {
9964
- localStorage.setItem(KEY2, JSON.stringify(pending));
9965
- } catch {
9966
- }
9967
- }
9968
- function beginLinkState(account, returnTo) {
9969
- if (!ACCOUNT_HEX.test(account)) {
9970
- throw new Error("accountId must be 64 lowercase hex characters");
9971
- }
9972
- const url2 = new URL(returnTo);
9973
- const now = Date.now();
9974
- const pending = readPending(now);
9975
- const bytes = crypto.getRandomValues(new Uint8Array(16));
9976
- const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
9977
- ""
9978
- );
9979
- pending[state] = { account, expiresAt: now + LIFETIME_MS };
9980
- writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
9981
- url2.searchParams.set(STATE_PARAM, state);
9982
- return url2.toString();
9983
- }
9984
- function takeLinkState(search) {
9985
- const state = new URLSearchParams(search).get(STATE_PARAM);
9986
- const pending = readPending(Date.now());
9987
- if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
9988
- return null;
9989
- const entry = pending[state];
9990
- delete pending[state];
9991
- writePending(pending);
9992
- return entry;
9993
- }
9994
- function withoutLinkState(search) {
9995
- const params = new URLSearchParams(search);
9996
- params.delete(STATE_PARAM);
9997
- const rest = params.toString();
9998
- return rest ? `?${rest}` : "";
9999
- }
10000
-
10001
10032
  // src/cloud/link-redirect.ts
10002
10033
  var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
10003
10034
  function cloudLinkUrl(options) {
@@ -10054,7 +10085,8 @@ async function completeCloudLink(options) {
10054
10085
  var DEVICE_PARAM = "enrol-device";
10055
10086
  var KEM_PARAM = "enrol-kem";
10056
10087
  var CALLBACK_PARAM = "callback-url";
10057
- var STATE_PARAM2 = "state";
10088
+ var STATE_PARAM3 = "state";
10089
+ var PENDING_KEY2 = "calimero.device.enrol.pending";
10058
10090
  function deviceEnrolmentUrl(options) {
10059
10091
  assertHex(options.devicePublicKey, "devicePublicKey");
10060
10092
  assertHex(options.kemPublicKey, "kemPublicKey");
@@ -10064,7 +10096,7 @@ function deviceEnrolmentUrl(options) {
10064
10096
  url2.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
10065
10097
  url2.searchParams.set(KEM_PARAM, options.kemPublicKey);
10066
10098
  url2.searchParams.set(CALLBACK_PARAM, options.returnTo);
10067
- if (options.state) url2.searchParams.set(STATE_PARAM2, options.state);
10099
+ url2.searchParams.set(STATE_PARAM3, beginState(PENDING_KEY2, {}));
10068
10100
  return url2.toString();
10069
10101
  }
10070
10102
  function beginDeviceEnrolment(options) {
@@ -10075,6 +10107,7 @@ function readEnrolmentCallback(location2 = window.location) {
10075
10107
  const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
10076
10108
  const error = params.get("error");
10077
10109
  if (error) {
10110
+ takeState(PENDING_KEY2, params.get(STATE_PARAM3));
10078
10111
  stripFragment2();
10079
10112
  throw new Error(
10080
10113
  error === "denied" || error === "cancelled" ? "the device was not approved at the wallet" : `the wallet refused this enrolment: ${error}`
@@ -10093,9 +10126,9 @@ function stripFragment2() {
10093
10126
  history.replaceState(null, "", location.pathname + location.search);
10094
10127
  }
10095
10128
  async function completeDeviceEnrolment(options) {
10096
- if (options.expectState !== void 0 && options.expectState !== options.state) {
10129
+ if (!takeState(PENDING_KEY2, options.state)) {
10097
10130
  throw new Error(
10098
- "this enrolment answers a request this app did not make \u2014 the state it carries is not the one sent"
10131
+ "this enrolment answers a request this app did not make - start the enrolment again from here"
10099
10132
  );
10100
10133
  }
10101
10134
  const certificate = await verifyDeviceCredential(options.credential);
@@ -12488,8 +12521,8 @@ var MeroJs = class {
12488
12521
  /**
12489
12522
  * Parse an auth callback URL hash fragment (static utility)
12490
12523
  */
12491
- static parseAuthCallback(url2) {
12492
- return parseAuthCallback(url2);
12524
+ static parseAuthCallback(url2, options) {
12525
+ return parseAuthCallback(url2, options);
12493
12526
  }
12494
12527
  /**
12495
12528
  * Build an auth login URL (static utility)