@calimero-network/mero-js 25.0.0 → 25.0.2

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/mero-js.d.ts CHANGED
@@ -98,6 +98,9 @@ export declare class MeroJs {
98
98
  private tokenData;
99
99
  private refreshPromise;
100
100
  private tokenStore;
101
+ private storeHoldsTokens;
102
+ private tokenGeneration;
103
+ private readonly onStorage;
101
104
  private rpcClient;
102
105
  private sseClient;
103
106
  private wsClient;
@@ -152,6 +155,9 @@ export declare class MeroJs {
152
155
  username: string;
153
156
  password: string;
154
157
  }): Promise<TokenData>;
158
+ private persistTokens;
159
+ private signedOutElsewhere;
160
+ private dropLocalTokens;
155
161
  /**
156
162
  * Get a valid token. Returns the current token as-is.
157
163
  * The server rejects refresh attempts while the access token is still valid,
package/dist/mero-js.js CHANGED
@@ -55,6 +55,14 @@ export class MeroJs {
55
55
  constructor(config) {
56
56
  this.tokenData = null;
57
57
  this.refreshPromise = null;
58
+ // True while the store is known to hold our tokens; a store that cannot persist never sets it.
59
+ this.storeHoldsTokens = false;
60
+ // Bumped when local tokens are dropped, so a refresh already in flight cannot restore them.
61
+ this.tokenGeneration = 0;
62
+ this.onStorage = () => {
63
+ if (this.signedOutElsewhere())
64
+ this.dropLocalTokens();
65
+ };
58
66
  this.rpcClient = null;
59
67
  this.sseClient = null;
60
68
  this.wsClient = null;
@@ -67,6 +75,9 @@ export class MeroJs {
67
75
  // Restore tokens from store if available
68
76
  if (this.tokenStore) {
69
77
  this.tokenData = this.tokenStore.getTokens();
78
+ this.storeHoldsTokens = this.tokenData !== null;
79
+ if (typeof window !== 'undefined')
80
+ window.addEventListener('storage', this.onStorage);
70
81
  }
71
82
  // Create HTTP client with token management
72
83
  // For Tauri, explicitly set credentials to 'omit' to avoid network errors
@@ -90,7 +101,7 @@ export class MeroJs {
90
101
  onTokenRefresh: async (newToken) => {
91
102
  if (this.tokenData) {
92
103
  this.tokenData.access_token = newToken;
93
- this.tokenStore?.setTokens(this.tokenData);
104
+ this.persistTokens(this.tokenData);
94
105
  }
95
106
  },
96
107
  onAuthRevoked: async () => {
@@ -242,13 +253,29 @@ export class MeroJs {
242
253
  refresh_token: response.data.refresh_token,
243
254
  expires_at: expiresAtFromJwt(accessToken, Date.now() + 3600000),
244
255
  };
245
- this.tokenStore?.setTokens(this.tokenData);
256
+ this.persistTokens(this.tokenData);
246
257
  return this.tokenData;
247
258
  }
248
259
  catch (error) {
249
260
  throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
250
261
  }
251
262
  }
263
+ persistTokens(data) {
264
+ if (!this.tokenStore)
265
+ return;
266
+ this.tokenStore.setTokens(data);
267
+ this.storeHoldsTokens = this.tokenStore.getTokens() !== null;
268
+ }
269
+ // Raw store read: an empty store is a logout, another node's bundle is not.
270
+ signedOutElsewhere() {
271
+ return this.storeHoldsTokens && !this.tokenStore?.getTokens();
272
+ }
273
+ dropLocalTokens() {
274
+ this.tokenGeneration++;
275
+ this.refreshPromise = null;
276
+ this.tokenData = null;
277
+ this.storeHoldsTokens = false;
278
+ }
252
279
  /**
253
280
  * Get a valid token. Returns the current token as-is.
254
281
  * The server rejects refresh attempts while the access token is still valid,
@@ -306,8 +333,14 @@ export class MeroJs {
306
333
  async performTokenRefreshLocked(triggeringAccessToken) {
307
334
  // Another instance/tab may have rotated the bundle while we were queued behind
308
335
  // the lock — the store, not our in-memory copy, is the source of truth.
336
+ if (this.signedOutElsewhere()) {
337
+ // Another holder signed out and emptied the store; refreshing would undo that.
338
+ this.dropLocalTokens();
339
+ throw new Error('Signed out in another tab');
340
+ }
309
341
  const stored = this.tokenStore?.getTokens() ?? null;
310
342
  if (stored?.refresh_token) {
343
+ this.storeHoldsTokens = true;
311
344
  this.tokenData = stored;
312
345
  if (triggeringAccessToken && stored.access_token !== triggeringAccessToken) {
313
346
  // Someone else already refreshed. Reuse their bundle rather than replaying
@@ -318,11 +351,24 @@ export class MeroJs {
318
351
  if (!this.tokenData?.refresh_token) {
319
352
  throw new Error('No refresh token available');
320
353
  }
354
+ // The store held the token we present; if it no longer does after the refresh, the
355
+ // session was replaced or signed out meanwhile and our rotated tokens must not be written.
356
+ const generation = this.tokenGeneration;
357
+ const presented = this.tokenData.access_token;
358
+ const storeHeldPresented = this.tokenStore?.getTokens()?.access_token === presented;
321
359
  try {
322
360
  const response = await this.authClient.refreshToken({
323
361
  access_token: this.tokenData.access_token,
324
362
  refresh_token: this.tokenData.refresh_token,
325
363
  });
364
+ // A local drop already set the state; a store change by another tab is adopted.
365
+ if (generation !== this.tokenGeneration)
366
+ throw new Error('Signed out during token refresh');
367
+ if (storeHeldPresented && this.tokenStore?.getTokens()?.access_token !== presented) {
368
+ this.tokenData = this.tokenStore?.getTokens() ?? null;
369
+ this.storeHoldsTokens = this.tokenData !== null;
370
+ throw new Error('Signed out during token refresh');
371
+ }
326
372
  const accessToken = response.data.access_token;
327
373
  this.tokenData = {
328
374
  access_token: accessToken,
@@ -331,7 +377,7 @@ export class MeroJs {
331
377
  refresh_token: response.data.refresh_token,
332
378
  expires_at: expiresAtFromJwt(accessToken, Date.now() + 3600000),
333
379
  };
334
- this.tokenStore?.setTokens(this.tokenData);
380
+ this.persistTokens(this.tokenData);
335
381
  return this.tokenData;
336
382
  }
337
383
  catch (error) {
@@ -346,8 +392,7 @@ export class MeroJs {
346
392
  * Clear the current token
347
393
  */
348
394
  clearToken() {
349
- this.refreshPromise = null;
350
- this.tokenData = null;
395
+ this.dropLocalTokens();
351
396
  this.tokenStore?.clear();
352
397
  }
353
398
  /**
@@ -364,7 +409,7 @@ export class MeroJs {
364
409
  setTokenData(data) {
365
410
  const expiresAt = data.expires_at || expiresAtFromJwt(data.access_token, Date.now() + 3600000);
366
411
  this.tokenData = { ...data, expires_at: expiresAt };
367
- this.tokenStore?.setTokens(this.tokenData);
412
+ this.persistTokens(this.tokenData);
368
413
  }
369
414
  /**
370
415
  * Get the current token data (for debugging)
@@ -376,6 +421,8 @@ export class MeroJs {
376
421
  * Close all event connections and clean up resources
377
422
  */
378
423
  close() {
424
+ if (typeof window !== 'undefined')
425
+ window.removeEventListener('storage', this.onStorage);
379
426
  this.sseClient?.close();
380
427
  this.wsClient?.close();
381
428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "25.0.0",
3
+ "version": "25.0.2",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {