@calimero-network/mero-js 25.0.1 → 25.0.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.mjs CHANGED
@@ -10075,17 +10075,6 @@ function normaliseAccount(account) {
10075
10075
  const bare = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
10076
10076
  return bare.length > 0 ? bare : null;
10077
10077
  }
10078
- function httpAdmitterOrigins(addrs) {
10079
- const out = [];
10080
- for (const addr of addrs ?? []) {
10081
- try {
10082
- const url = new URL(addr);
10083
- if (url.protocol === "http:" || url.protocol === "https:") out.push(addr.replace(/\/+$/, ""));
10084
- } catch {
10085
- }
10086
- }
10087
- return out;
10088
- }
10089
10078
  function isInvited(node, invited) {
10090
10079
  if (invited.size === 0) return true;
10091
10080
  const account = normaliseAccount(node.account);
@@ -10110,23 +10099,6 @@ async function resolveRelayFromInvitation(input) {
10110
10099
  };
10111
10100
  }
10112
10101
  if (nodes.length === 0) {
10113
- const [relayUrl] = httpAdmitterOrigins(input.admitterAddrs);
10114
- if (relayUrl) {
10115
- const admitters = (input.admitters ?? []).map(normaliseAccount).filter((a) => a !== null);
10116
- return {
10117
- ok: true,
10118
- relayUrl,
10119
- admitUrl: `${relayUrl}/admin-api/namespaces/${input.namespaceId}/admit`,
10120
- admitterAccount: admitters.length === 1 ? admitters[0] : null,
10121
- peerId: null,
10122
- // The relay a founder's session runs through executes that founder's
10123
- // warrants, so it authors on behalf; the node's answer to the first
10124
- // write is the authoritative one, as for a cloud-routed node.
10125
- writable: true,
10126
- stale: false,
10127
- via: "invitation"
10128
- };
10129
- }
10130
10102
  return {
10131
10103
  ok: false,
10132
10104
  step: "no-nodes",
@@ -10170,8 +10142,7 @@ async function resolveRelayFromInvitation(input) {
10170
10142
  // Stale when the node's own heartbeat lapsed OR when the cloud's summary
10171
10143
  // disagrees with trying it at all. Both mean the same thing to a caller:
10172
10144
  // this was attempted on advisory data, so if the admit fails that is why.
10173
- stale: !chosen.fresh || advisory,
10174
- via: "cloud"
10145
+ stale: !chosen.fresh || advisory
10175
10146
  };
10176
10147
  }
10177
10148
 
@@ -10457,7 +10428,6 @@ async function bootstrapFromInvitation(input) {
10457
10428
  // separately: two sources for one fact is how an intersection ends up
10458
10429
  // being performed against the wrong list.
10459
10430
  admitters: input.invitation.invitation.admitters,
10460
- admitterAddrs: input.invitation.admitter_addrs,
10461
10431
  credential: input.credential.credential,
10462
10432
  deviceSecret: input.credential.deviceSecret,
10463
10433
  cloudBaseUrl: input.cloudBaseUrl
@@ -10676,7 +10646,7 @@ async function attestedNodeKey(relay) {
10676
10646
  key.kind === "unavailable" ? "the relay's signing key could not be learned: its attestation did not answer, try again" : "the relay's attestation was refused, so founding through it is not possible: it is not a signed locked-read-only release"
10677
10647
  );
10678
10648
  }
10679
- var HA_ACCOUNT_NOT_LINKED_MESSAGE = "link this account to your cloud user in the wallet so invitees can find this namespace";
10649
+ var HA_ACCOUNT_NOT_LINKED_MESSAGE = "link this account to your cloud user in the wallet so the cloud can host this namespace on its fleet";
10680
10650
  var HA_REFUSAL_MESSAGES = {
10681
10651
  account_not_linked: HA_ACCOUNT_NOT_LINKED_MESSAGE,
10682
10652
  account_linked_to_several_users: "this account is linked to more than one cloud user, so the cloud cannot tell whose plan hosts the namespace: unlink it from all but one in the wallet",
@@ -10781,7 +10751,7 @@ var NoRelayError = class extends Error {
10781
10751
  var InvitationNotClaimableError = class extends Error {
10782
10752
  constructor(namespaceId, reason) {
10783
10753
  super(
10784
- reason === "not-hosted" ? `nobody could claim an invitation to ${namespaceId} yet: the namespace is not hosted in the cloud and has no relay in it, so an invitee with no node has no relay to be admitted through. Link this account to your cloud user in the wallet and enable HA for the namespace, then invite.` : `nobody could claim an invitation to ${namespaceId}: the cloud routes to none of the relays or admins it would name as admitters`
10754
+ reason === "not-hosted" ? `nobody could claim an invitation to ${namespaceId} yet: the cloud lists no node holding the namespace, so an invitee with no node of their own has no relay to be admitted through.` : `nobody could claim an invitation to ${namespaceId}: the cloud routes to none of the relays or admins it would name as admitters`
10785
10755
  );
10786
10756
  this.namespaceId = namespaceId;
10787
10757
  this.reason = reason;
@@ -10969,7 +10939,6 @@ function createAccountAdmin(input, deps = {}) {
10969
10939
  async createNamespaceInvitation(namespaceId) {
10970
10940
  const [{ members }, info] = await Promise.all([relay().listGroupMembers(namespaceId), relay().getGroupInfo(namespaceId)]);
10971
10941
  const relays = members.filter((m) => m.role === "RelayTee").map((m) => m.identity.toLowerCase());
10972
- let admitterAddrs;
10973
10942
  if (routing) {
10974
10943
  const named = new Set(
10975
10944
  relays.length > 0 ? relays : members.filter((m) => m.role === "Admin").map((m) => m.identity.toLowerCase())
@@ -10980,10 +10949,8 @@ function createAccountAdmin(input, deps = {}) {
10980
10949
  } catch {
10981
10950
  }
10982
10951
  if (nodes !== null) {
10983
- if (nodes.length === 0) {
10984
- if (relays.length === 0 || !s.relayUrl) throw new InvitationNotClaimableError(namespaceId, "not-hosted");
10985
- admitterAddrs = [s.relayUrl];
10986
- } else if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
10952
+ if (nodes.length === 0) throw new InvitationNotClaimableError(namespaceId, "not-hosted");
10953
+ if (!nodes.some((n) => n.account !== null && named.has(n.account.toLowerCase()))) {
10987
10954
  throw new InvitationNotClaimableError(namespaceId, "no-named-node");
10988
10955
  }
10989
10956
  }
@@ -10993,7 +10960,6 @@ function createAccountAdmin(input, deps = {}) {
10993
10960
  inviterAccount: s.account,
10994
10961
  deviceSecret: s.deviceSecret,
10995
10962
  ...relays.length > 0 ? { admitters: relays } : {},
10996
- ...admitterAddrs ? { admitterAddrs } : {},
10997
10963
  members,
10998
10964
  applicationId: info.targetApplicationId,
10999
10965
  appKey: info.appKey
@@ -11374,9 +11340,8 @@ var CloudClient = class {
11374
11340
  * are answered together rather than across two endpoints — the per-context
11375
11341
  * read needs a context id a joiner does not yet have.
11376
11342
  *
11377
- * An empty `nodes` list is a state, not an error: a namespace with no fleet
11378
- * assignment has no cloud node, and the joiner must use one of the
11379
- * invitation's other admitters — another admin's node, or a self-hosted peer.
11343
+ * An empty `nodes` list is a state, not an error: the cloud has no record of
11344
+ * a node holding the namespace, so there is no hosted admitter.
11380
11345
  */
11381
11346
  async getNamespaceRouting(namespaceId) {
11382
11347
  const body = await this.request("GET", `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
@@ -12051,6 +12016,13 @@ var MeroJs = class {
12051
12016
  constructor(config) {
12052
12017
  this.tokenData = null;
12053
12018
  this.refreshPromise = null;
12019
+ // True while the store is known to hold our tokens; a store that cannot persist never sets it.
12020
+ this.storeHoldsTokens = false;
12021
+ // Bumped when local tokens are dropped, so a refresh already in flight cannot restore them.
12022
+ this.tokenGeneration = 0;
12023
+ this.onStorage = () => {
12024
+ if (this.signedOutElsewhere()) this.dropLocalTokens();
12025
+ };
12054
12026
  this.rpcClient = null;
12055
12027
  this.sseClient = null;
12056
12028
  this.wsClient = null;
@@ -12062,6 +12034,8 @@ var MeroJs = class {
12062
12034
  this.tokenStore = config.tokenStore ?? null;
12063
12035
  if (this.tokenStore) {
12064
12036
  this.tokenData = this.tokenStore.getTokens();
12037
+ this.storeHoldsTokens = this.tokenData !== null;
12038
+ if (typeof window !== "undefined") window.addEventListener("storage", this.onStorage);
12065
12039
  }
12066
12040
  const isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
12067
12041
  const getProof = resolveProofSigner(this.config.requestProof);
@@ -12080,7 +12054,7 @@ var MeroJs = class {
12080
12054
  onTokenRefresh: async (newToken) => {
12081
12055
  if (this.tokenData) {
12082
12056
  this.tokenData.access_token = newToken;
12083
- this.tokenStore?.setTokens(this.tokenData);
12057
+ this.persistTokens(this.tokenData);
12084
12058
  }
12085
12059
  },
12086
12060
  onAuthRevoked: async () => {
@@ -12226,7 +12200,7 @@ var MeroJs = class {
12226
12200
  refresh_token: response.data.refresh_token,
12227
12201
  expires_at: expiresAtFromJwt(accessToken, Date.now() + 36e5)
12228
12202
  };
12229
- this.tokenStore?.setTokens(this.tokenData);
12203
+ this.persistTokens(this.tokenData);
12230
12204
  return this.tokenData;
12231
12205
  } catch (error) {
12232
12206
  throw new Error(
@@ -12234,6 +12208,21 @@ var MeroJs = class {
12234
12208
  );
12235
12209
  }
12236
12210
  }
12211
+ persistTokens(data) {
12212
+ if (!this.tokenStore) return;
12213
+ this.tokenStore.setTokens(data);
12214
+ this.storeHoldsTokens = this.tokenStore.getTokens() !== null;
12215
+ }
12216
+ // Raw store read: an empty store is a logout, another node's bundle is not.
12217
+ signedOutElsewhere() {
12218
+ return this.storeHoldsTokens && !this.tokenStore?.getTokens();
12219
+ }
12220
+ dropLocalTokens() {
12221
+ this.tokenGeneration++;
12222
+ this.refreshPromise = null;
12223
+ this.tokenData = null;
12224
+ this.storeHoldsTokens = false;
12225
+ }
12237
12226
  /**
12238
12227
  * Get a valid token. Returns the current token as-is.
12239
12228
  * The server rejects refresh attempts while the access token is still valid,
@@ -12287,8 +12276,13 @@ var MeroJs = class {
12287
12276
  * Perform the actual token refresh. Runs with the refresh lock held.
12288
12277
  */
12289
12278
  async performTokenRefreshLocked(triggeringAccessToken) {
12279
+ if (this.signedOutElsewhere()) {
12280
+ this.dropLocalTokens();
12281
+ throw new Error("Signed out in another tab");
12282
+ }
12290
12283
  const stored = this.tokenStore?.getTokens() ?? null;
12291
12284
  if (stored?.refresh_token) {
12285
+ this.storeHoldsTokens = true;
12292
12286
  this.tokenData = stored;
12293
12287
  if (triggeringAccessToken && stored.access_token !== triggeringAccessToken) {
12294
12288
  return stored;
@@ -12297,11 +12291,20 @@ var MeroJs = class {
12297
12291
  if (!this.tokenData?.refresh_token) {
12298
12292
  throw new Error("No refresh token available");
12299
12293
  }
12294
+ const generation = this.tokenGeneration;
12295
+ const presented = this.tokenData.access_token;
12296
+ const storeHeldPresented = this.tokenStore?.getTokens()?.access_token === presented;
12300
12297
  try {
12301
12298
  const response = await this.authClient.refreshToken({
12302
12299
  access_token: this.tokenData.access_token,
12303
12300
  refresh_token: this.tokenData.refresh_token
12304
12301
  });
12302
+ if (generation !== this.tokenGeneration) throw new Error("Signed out during token refresh");
12303
+ if (storeHeldPresented && this.tokenStore?.getTokens()?.access_token !== presented) {
12304
+ this.tokenData = this.tokenStore?.getTokens() ?? null;
12305
+ this.storeHoldsTokens = this.tokenData !== null;
12306
+ throw new Error("Signed out during token refresh");
12307
+ }
12305
12308
  const accessToken = response.data.access_token;
12306
12309
  this.tokenData = {
12307
12310
  access_token: accessToken,
@@ -12310,7 +12313,7 @@ var MeroJs = class {
12310
12313
  refresh_token: response.data.refresh_token,
12311
12314
  expires_at: expiresAtFromJwt(accessToken, Date.now() + 36e5)
12312
12315
  };
12313
- this.tokenStore?.setTokens(this.tokenData);
12316
+ this.persistTokens(this.tokenData);
12314
12317
  return this.tokenData;
12315
12318
  } catch (error) {
12316
12319
  throw new Error(
@@ -12322,8 +12325,7 @@ var MeroJs = class {
12322
12325
  * Clear the current token
12323
12326
  */
12324
12327
  clearToken() {
12325
- this.refreshPromise = null;
12326
- this.tokenData = null;
12328
+ this.dropLocalTokens();
12327
12329
  this.tokenStore?.clear();
12328
12330
  }
12329
12331
  /**
@@ -12340,7 +12342,7 @@ var MeroJs = class {
12340
12342
  setTokenData(data) {
12341
12343
  const expiresAt = data.expires_at || expiresAtFromJwt(data.access_token, Date.now() + 36e5);
12342
12344
  this.tokenData = { ...data, expires_at: expiresAt };
12343
- this.tokenStore?.setTokens(this.tokenData);
12345
+ this.persistTokens(this.tokenData);
12344
12346
  }
12345
12347
  /**
12346
12348
  * Get the current token data (for debugging)
@@ -12352,6 +12354,7 @@ var MeroJs = class {
12352
12354
  * Close all event connections and clean up resources
12353
12355
  */
12354
12356
  close() {
12357
+ if (typeof window !== "undefined") window.removeEventListener("storage", this.onStorage);
12355
12358
  this.sseClient?.close();
12356
12359
  this.wsClient?.close();
12357
12360
  }