@calimero-network/mero-react 4.3.3 → 4.4.0

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
@@ -67,12 +67,85 @@ function resolveTrustedNodeUrl(params) {
67
67
  return { url: null, rejected: true };
68
68
  }
69
69
 
70
+ // src/auth/token-adoption.ts
71
+ var HOUR_MS = 36e5;
72
+ function decodeJwtClaims(token) {
73
+ try {
74
+ const parts = token.split(".");
75
+ if (parts.length !== 3) return null;
76
+ let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
77
+ while (b64.length % 4) b64 += "=";
78
+ const payload = JSON.parse(atob(b64));
79
+ if (!payload || typeof payload !== "object") return null;
80
+ const { iat, exp } = payload;
81
+ return {
82
+ iat: typeof iat === "number" && iat > 0 ? iat : void 0,
83
+ exp: typeof exp === "number" && exp > 0 ? exp : void 0
84
+ };
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ function expiresAtFromJwt(token, fallbackMs = Date.now() + HOUR_MS) {
90
+ const exp = decodeJwtClaims(token)?.exp;
91
+ return exp ? exp * 1e3 : fallbackMs;
92
+ }
93
+ function isCallbackNewer(callbackAccessToken, stored) {
94
+ const incoming = decodeJwtClaims(callbackAccessToken);
95
+ if (!incoming) return false;
96
+ const current = decodeJwtClaims(stored.access_token);
97
+ if (current) {
98
+ if (incoming.iat !== void 0 && current.iat !== void 0 && incoming.iat !== current.iat) {
99
+ return incoming.iat > current.iat;
100
+ }
101
+ if (incoming.exp !== void 0 && current.exp !== void 0 && incoming.exp !== current.exp) {
102
+ return incoming.exp > current.exp;
103
+ }
104
+ return false;
105
+ }
106
+ return incoming.exp !== void 0 && incoming.exp * 1e3 > stored.expires_at;
107
+ }
108
+ function resolveTokenAdoption(params) {
109
+ const { callbackAccessToken, callbackRefreshToken, callbackNodeUrl, stored, storedNodeUrl } = params;
110
+ const bundle = (refreshToken2) => ({
111
+ access_token: callbackAccessToken,
112
+ refresh_token: refreshToken2,
113
+ expires_at: expiresAtFromJwt(callbackAccessToken)
114
+ });
115
+ if (!stored?.access_token) {
116
+ return { adopt: true, tokens: bundle(callbackRefreshToken), reason: "fresh-login" };
117
+ }
118
+ const sameNode = !!storedNodeUrl && sameOrigin(callbackNodeUrl, storedNodeUrl);
119
+ if (!sameNode) {
120
+ return { adopt: true, tokens: bundle(callbackRefreshToken), reason: "node-changed" };
121
+ }
122
+ const refreshToken = callbackRefreshToken || stored.refresh_token;
123
+ if (isCallbackNewer(callbackAccessToken, stored)) {
124
+ return { adopt: true, tokens: bundle(refreshToken), reason: "newer" };
125
+ }
126
+ if (!stored.refresh_token && callbackRefreshToken) {
127
+ return {
128
+ adopt: true,
129
+ tokens: { ...stored, refresh_token: callbackRefreshToken },
130
+ reason: "fill-refresh"
131
+ };
132
+ }
133
+ return { adopt: false, reason: "stale" };
134
+ }
135
+
70
136
  // src/storage/index.ts
71
137
  var STORAGE_KEYS = {
72
138
  ACCESS_TOKEN: "mero:access_token",
73
139
  REFRESH_TOKEN: "mero:refresh_token",
74
140
  EXPIRES_AT: "mero:expires_at",
75
141
  NODE_URL: "mero:node_url",
142
+ // The node the token bundle currently in the TokenStore was issued by. Distinct
143
+ // from NODE_URL, which `connectToNode` overwrites with the *target* node before
144
+ // redirecting to login — so at callback time NODE_URL names where we are going,
145
+ // not who minted the tokens we still hold. Telling those apart is what lets the
146
+ // SSO callback re-seed on a node change without mistaking a different node's
147
+ // bundle for a rotated one (see auth/token-adoption.ts).
148
+ TOKEN_NODE_URL: "mero:token_node_url",
76
149
  APPLICATION_ID: "mero:application_id",
77
150
  CONTEXT_ID: "mero:context_id",
78
151
  CONTEXT_IDENTITY: "mero:context_identity"
@@ -153,6 +226,18 @@ function clearNodeUrl() {
153
226
  if (!isLocalStorageAvailable()) return;
154
227
  localStorage.removeItem(STORAGE_KEYS.NODE_URL);
155
228
  }
229
+ function getTokenNodeUrl() {
230
+ if (!isLocalStorageAvailable()) return null;
231
+ return localStorage.getItem(STORAGE_KEYS.TOKEN_NODE_URL);
232
+ }
233
+ function setTokenNodeUrl(url) {
234
+ if (!isLocalStorageAvailable()) return;
235
+ localStorage.setItem(STORAGE_KEYS.TOKEN_NODE_URL, url);
236
+ }
237
+ function clearTokenNodeUrl() {
238
+ if (!isLocalStorageAvailable()) return;
239
+ localStorage.removeItem(STORAGE_KEYS.TOKEN_NODE_URL);
240
+ }
156
241
  function getApplicationId() {
157
242
  if (!isLocalStorageAvailable()) return null;
158
243
  return localStorage.getItem(STORAGE_KEYS.APPLICATION_ID);
@@ -218,21 +303,6 @@ function getPermissionsForMode(mode) {
218
303
  throw new Error(`Unsupported application mode: ${mode}`);
219
304
  }
220
305
  }
221
- function parseJwtExpiry(token) {
222
- try {
223
- const parts = token.split(".");
224
- if (parts.length === 3) {
225
- let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
226
- while (b64.length % 4) b64 += "=";
227
- const payload = JSON.parse(atob(b64));
228
- if (payload.exp && payload.exp > 0) {
229
- return payload.exp * 1e3;
230
- }
231
- }
232
- } catch {
233
- }
234
- return Date.now() + 36e5;
235
- }
236
306
  function MeroProvider({
237
307
  children,
238
308
  mode,
@@ -330,9 +400,11 @@ function MeroProvider({
330
400
  const callback = callbackRef.current;
331
401
  let nodeFromCallback = null;
332
402
  if (callback) {
403
+ const initiatedNodeUrl = getNodeUrl();
404
+ const tokenNodeUrl = getTokenNodeUrl() ?? initiatedNodeUrl;
333
405
  const { url, rejected } = resolveTrustedNodeUrl({
334
406
  candidate: callback.nodeUrl,
335
- initiated: getNodeUrl(),
407
+ initiated: initiatedNodeUrl,
336
408
  allowedNodeUrls
337
409
  });
338
410
  if (isBrowser) {
@@ -344,11 +416,21 @@ function MeroProvider({
344
416
  "[MeroProvider] OAuth callback node_url is not trusted (it does not match the node login was initiated with, nor `allowedNodeUrls`). Ignoring the callback; no tokens stored."
345
417
  );
346
418
  } else if (url) {
347
- tokenStore.setTokens({
348
- access_token: callback.accessToken,
349
- refresh_token: callback.refreshToken,
350
- expires_at: parseJwtExpiry(callback.accessToken)
419
+ const decision = resolveTokenAdoption({
420
+ callbackAccessToken: callback.accessToken,
421
+ callbackRefreshToken: callback.refreshToken,
422
+ callbackNodeUrl: url,
423
+ stored: tokenStore.getTokens(),
424
+ storedNodeUrl: tokenNodeUrl
351
425
  });
426
+ if (decision.adopt) {
427
+ tokenStore.setTokens(decision.tokens);
428
+ setTokenNodeUrl(url);
429
+ } else {
430
+ console.warn(
431
+ "[MeroProvider] Ignoring the SSO callback token bundle: it is stale (not newer than the tokens already stored for this node). Adopting it would replay an already-rotated refresh token, which the node revokes the whole token family for."
432
+ );
433
+ }
352
434
  if (callback.applicationId) {
353
435
  setApplicationId(callback.applicationId);
354
436
  if (active) setApplicationIdState(callback.applicationId);
@@ -2484,6 +2566,7 @@ exports.clearApplicationId = clearApplicationId;
2484
2566
  exports.clearContextId = clearContextId;
2485
2567
  exports.clearContextIdentity = clearContextIdentity;
2486
2568
  exports.clearNodeUrl = clearNodeUrl;
2569
+ exports.clearTokenNodeUrl = clearTokenNodeUrl;
2487
2570
  exports.cssVar = cssVar;
2488
2571
  exports.defaultMeroTheme = defaultMeroTheme;
2489
2572
  exports.discoverLocalNodes = discoverLocalNodes;
@@ -2491,6 +2574,7 @@ exports.getApplicationId = getApplicationId;
2491
2574
  exports.getContextId = getContextId;
2492
2575
  exports.getContextIdentity = getContextIdentity;
2493
2576
  exports.getNodeUrl = getNodeUrl;
2577
+ exports.getTokenNodeUrl = getTokenNodeUrl;
2494
2578
  exports.localNodeUrl = localNodeUrl;
2495
2579
  exports.localStorageTokenStorage = localStorageTokenStorage;
2496
2580
  exports.nodeEndpoint = nodeEndpoint;
@@ -2500,6 +2584,7 @@ exports.setApplicationId = setApplicationId;
2500
2584
  exports.setContextId = setContextId;
2501
2585
  exports.setContextIdentity = setContextIdentity;
2502
2586
  exports.setNodeUrl = setNodeUrl;
2587
+ exports.setTokenNodeUrl = setTokenNodeUrl;
2503
2588
  exports.themeToCssVars = themeToCssVars;
2504
2589
  exports.useAddGroupMembers = useAddGroupMembers;
2505
2590
  exports.useAppVersion = useAppVersion;