@calimero-network/mero-js 25.0.1 → 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/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +44 -6
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +44 -6
- package/dist/index.mjs.map +2 -2
- package/dist/mero-js.d.ts +6 -0
- package/dist/mero-js.js +53 -6
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|