@calimero-network/mero-react 4.3.2 → 4.3.4
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 +105 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +103 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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:
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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;
|