@calimero-network/mero-js 6.1.0 → 7.0.1
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/admin-api/admin-client.js +9 -9
- package/dist/admin-api/admin-client.js.map +1 -1
- package/dist/http-client/http-factory.d.ts +18 -0
- package/dist/http-client/http-factory.d.ts.map +1 -1
- package/dist/http-client/http-factory.js +2 -0
- package/dist/http-client/http-factory.js.map +1 -1
- package/dist/http-client/http-types.d.ts +7 -0
- package/dist/http-client/http-types.d.ts.map +1 -1
- package/dist/http-client/index.d.ts +1 -1
- package/dist/http-client/index.d.ts.map +1 -1
- package/dist/http-client/index.js +1 -1
- package/dist/http-client/index.js.map +1 -1
- package/dist/http-client/web-client.d.ts +16 -1
- package/dist/http-client/web-client.d.ts.map +1 -1
- package/dist/http-client/web-client.js +48 -1
- package/dist/http-client/web-client.js.map +1 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +83 -15
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +83 -15
- package/dist/index.mjs.map +2 -2
- package/dist/mero-js.d.ts +17 -4
- package/dist/mero-js.d.ts.map +1 -1
- package/dist/mero-js.js +59 -13
- package/dist/mero-js.js.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -32,6 +32,7 @@ function createTimeoutSignal(timeoutMs) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/http-client/web-client.ts
|
|
35
|
+
var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set(["token_reuse", "token_revoked"]);
|
|
35
36
|
var HTTPError = class extends Error {
|
|
36
37
|
constructor(status, statusText, url, headers, bodyText) {
|
|
37
38
|
super(`HTTP ${status} ${statusText}`);
|
|
@@ -52,6 +53,14 @@ var HTTPError = class extends Error {
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
};
|
|
56
|
+
var AuthRevokedError = class extends HTTPError {
|
|
57
|
+
constructor(reason, status, statusText, url, headers, bodyText) {
|
|
58
|
+
super(status, statusText, url, headers, bodyText);
|
|
59
|
+
this.reason = reason;
|
|
60
|
+
this.name = "AuthRevokedError";
|
|
61
|
+
this.message = `Authentication revoked (${reason}): HTTP ${status} ${statusText}`;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
55
64
|
function headersToRecord(headers) {
|
|
56
65
|
const record = {};
|
|
57
66
|
headers.forEach((value, key) => {
|
|
@@ -196,8 +205,27 @@ var WebHttpClient = class {
|
|
|
196
205
|
response.headers,
|
|
197
206
|
bodyText
|
|
198
207
|
);
|
|
208
|
+
const authError = response.headers.get("x-auth-error");
|
|
209
|
+
if ((response.status === 401 || response.status === 403) && authError && TERMINAL_AUTH_ERRORS.has(authError)) {
|
|
210
|
+
this.refreshTokenPromise = null;
|
|
211
|
+
this.onTokenRefreshPromise = null;
|
|
212
|
+
if (this.transport.onAuthRevoked) {
|
|
213
|
+
try {
|
|
214
|
+
await this.transport.onAuthRevoked();
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
throw new AuthRevokedError(
|
|
219
|
+
authError,
|
|
220
|
+
response.status,
|
|
221
|
+
response.statusText,
|
|
222
|
+
url,
|
|
223
|
+
response.headers,
|
|
224
|
+
bodyText
|
|
225
|
+
);
|
|
226
|
+
}
|
|
199
227
|
const userAborted = init?.signal?.aborted === true;
|
|
200
|
-
if (response.status === 401 && this.transport.refreshToken &&
|
|
228
|
+
if (response.status === 401 && this.transport.refreshToken && authError === "token_expired" && retryCount < MAX_RETRY_ATTEMPTS && !isStreamBody && !userAborted) {
|
|
201
229
|
try {
|
|
202
230
|
let refreshPromise = this.refreshTokenPromise;
|
|
203
231
|
if (!refreshPromise) {
|
|
@@ -364,6 +392,7 @@ function createBrowserHttpClient(options) {
|
|
|
364
392
|
getAuthToken: options.getAuthToken,
|
|
365
393
|
onTokenRefresh: options.onTokenRefresh,
|
|
366
394
|
refreshToken: options.refreshToken,
|
|
395
|
+
onAuthRevoked: options.onAuthRevoked,
|
|
367
396
|
defaultHeaders: options.defaultHeaders,
|
|
368
397
|
timeoutMs: options.timeoutMs,
|
|
369
398
|
credentials: options.credentials,
|
|
@@ -389,6 +418,7 @@ function createNodeHttpClient(options) {
|
|
|
389
418
|
getAuthToken: options.getAuthToken,
|
|
390
419
|
onTokenRefresh: options.onTokenRefresh,
|
|
391
420
|
refreshToken: options.refreshToken,
|
|
421
|
+
onAuthRevoked: options.onAuthRevoked,
|
|
392
422
|
defaultHeaders: options.defaultHeaders,
|
|
393
423
|
timeoutMs: options.timeoutMs,
|
|
394
424
|
credentials: options.credentials,
|
|
@@ -1138,7 +1168,7 @@ var AdminApiClient = class {
|
|
|
1138
1168
|
const response = await this.httpClient.get(
|
|
1139
1169
|
`/admin-api/groups/${groupId}/metadata`
|
|
1140
1170
|
);
|
|
1141
|
-
return response?.data
|
|
1171
|
+
return response?.data ?? null;
|
|
1142
1172
|
}
|
|
1143
1173
|
async setMemberMetadata(groupId, identity, request) {
|
|
1144
1174
|
await this.httpClient.put(`/admin-api/groups/${groupId}/members/${identity}/metadata`, request);
|
|
@@ -1147,7 +1177,7 @@ var AdminApiClient = class {
|
|
|
1147
1177
|
const response = await this.httpClient.get(
|
|
1148
1178
|
`/admin-api/groups/${groupId}/members/${identity}/metadata`
|
|
1149
1179
|
);
|
|
1150
|
-
return response?.data
|
|
1180
|
+
return response?.data ?? null;
|
|
1151
1181
|
}
|
|
1152
1182
|
async setContextMetadata(groupId, contextId, request) {
|
|
1153
1183
|
await this.httpClient.put(`/admin-api/groups/${groupId}/contexts/${contextId}/metadata`, request);
|
|
@@ -1156,7 +1186,7 @@ var AdminApiClient = class {
|
|
|
1156
1186
|
const response = await this.httpClient.get(
|
|
1157
1187
|
`/admin-api/groups/${groupId}/contexts/${contextId}/metadata`
|
|
1158
1188
|
);
|
|
1159
|
-
return response?.data
|
|
1189
|
+
return response?.data ?? null;
|
|
1160
1190
|
}
|
|
1161
1191
|
async syncGroup(groupId, request) {
|
|
1162
1192
|
return unwrap(await this.httpClient.post(`/admin-api/groups/${groupId}/sync`, request ?? {}));
|
|
@@ -1852,6 +1882,12 @@ _WsClient.MAX_BACKOFF_MS = 3e4;
|
|
|
1852
1882
|
var WsClient = _WsClient;
|
|
1853
1883
|
|
|
1854
1884
|
// src/mero-js.ts
|
|
1885
|
+
var REFRESH_LOCK_NAME = "mero-js:token-refresh";
|
|
1886
|
+
function getLockManager() {
|
|
1887
|
+
const nav = globalThis.navigator;
|
|
1888
|
+
const locks = nav?.locks;
|
|
1889
|
+
return locks && typeof locks.request === "function" ? locks : null;
|
|
1890
|
+
}
|
|
1855
1891
|
function expiresAtFromJwt(token, fallbackMs) {
|
|
1856
1892
|
try {
|
|
1857
1893
|
const parts = token.split(".");
|
|
@@ -1900,6 +1936,9 @@ var MeroJs = class {
|
|
|
1900
1936
|
this.tokenStore?.setTokens(this.tokenData);
|
|
1901
1937
|
}
|
|
1902
1938
|
},
|
|
1939
|
+
onAuthRevoked: () => {
|
|
1940
|
+
this.clearToken();
|
|
1941
|
+
},
|
|
1903
1942
|
timeoutMs: this.config.timeoutMs,
|
|
1904
1943
|
credentials: this.config.requestCredentials ?? (isTauri ? "omit" : void 0)
|
|
1905
1944
|
});
|
|
@@ -2022,27 +2061,52 @@ var MeroJs = class {
|
|
|
2022
2061
|
return this.tokenData;
|
|
2023
2062
|
}
|
|
2024
2063
|
/**
|
|
2025
|
-
* Refresh the access token
|
|
2064
|
+
* Refresh the access token. Single-flight: every caller (including the HTTP
|
|
2065
|
+
* transport's 401 hook) goes through here.
|
|
2066
|
+
*
|
|
2067
|
+
* Refresh tokens are single-use (calimero-network/core#3083): each refresh
|
|
2068
|
+
* consumes the presented token and returns a new one, and replaying a consumed
|
|
2069
|
+
* token makes the server revoke the whole family. A refresh must therefore
|
|
2070
|
+
* happen exactly once per rotation, which needs three layers:
|
|
2071
|
+
* - an in-process promise, so concurrent 401s in this instance share one call;
|
|
2072
|
+
* - a Web Lock (when available), so other tabs/webviews don't refresh at the
|
|
2073
|
+
* same time on the same stored token;
|
|
2074
|
+
* - a re-read of the token store inside the lock, so a bundle that another
|
|
2075
|
+
* instance already rotated is adopted instead of replaying a stale token.
|
|
2026
2076
|
*/
|
|
2027
|
-
async
|
|
2028
|
-
if (!this.tokenData?.refresh_token) {
|
|
2029
|
-
throw new Error("No refresh token available");
|
|
2030
|
-
}
|
|
2077
|
+
async performTokenRefresh() {
|
|
2031
2078
|
if (this.refreshPromise) {
|
|
2032
2079
|
return this.refreshPromise;
|
|
2033
2080
|
}
|
|
2034
|
-
|
|
2081
|
+
const triggeringAccessToken = this.tokenData?.access_token;
|
|
2082
|
+
const promise = this.withRefreshLock(
|
|
2083
|
+
() => this.performTokenRefreshLocked(triggeringAccessToken)
|
|
2084
|
+
);
|
|
2085
|
+
this.refreshPromise = promise;
|
|
2035
2086
|
try {
|
|
2036
|
-
|
|
2037
|
-
return newToken;
|
|
2087
|
+
return await promise;
|
|
2038
2088
|
} finally {
|
|
2039
|
-
this.refreshPromise
|
|
2089
|
+
if (this.refreshPromise === promise) {
|
|
2090
|
+
this.refreshPromise = null;
|
|
2091
|
+
}
|
|
2040
2092
|
}
|
|
2041
2093
|
}
|
|
2094
|
+
/** Run `fn` under the cross-tab refresh lock, or directly if Web Locks are unavailable. */
|
|
2095
|
+
withRefreshLock(fn) {
|
|
2096
|
+
const locks = getLockManager();
|
|
2097
|
+
return locks ? locks.request(REFRESH_LOCK_NAME, fn) : fn();
|
|
2098
|
+
}
|
|
2042
2099
|
/**
|
|
2043
|
-
* Perform the actual token refresh
|
|
2100
|
+
* Perform the actual token refresh. Runs with the refresh lock held.
|
|
2044
2101
|
*/
|
|
2045
|
-
async
|
|
2102
|
+
async performTokenRefreshLocked(triggeringAccessToken) {
|
|
2103
|
+
const stored = this.tokenStore?.getTokens() ?? null;
|
|
2104
|
+
if (stored?.refresh_token) {
|
|
2105
|
+
this.tokenData = stored;
|
|
2106
|
+
if (triggeringAccessToken && stored.access_token !== triggeringAccessToken) {
|
|
2107
|
+
return stored;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2046
2110
|
if (!this.tokenData?.refresh_token) {
|
|
2047
2111
|
throw new Error("No refresh token available");
|
|
2048
2112
|
}
|
|
@@ -2054,6 +2118,8 @@ var MeroJs = class {
|
|
|
2054
2118
|
const accessToken = response.data.access_token;
|
|
2055
2119
|
this.tokenData = {
|
|
2056
2120
|
access_token: accessToken,
|
|
2121
|
+
// The refresh token is rotated on every refresh — persist the new one or
|
|
2122
|
+
// the next refresh replays a consumed token.
|
|
2057
2123
|
refresh_token: response.data.refresh_token,
|
|
2058
2124
|
expires_at: expiresAtFromJwt(accessToken, Date.now() + 36e5)
|
|
2059
2125
|
};
|
|
@@ -2069,6 +2135,7 @@ var MeroJs = class {
|
|
|
2069
2135
|
* Clear the current token
|
|
2070
2136
|
*/
|
|
2071
2137
|
clearToken() {
|
|
2138
|
+
this.refreshPromise = null;
|
|
2072
2139
|
this.tokenData = null;
|
|
2073
2140
|
this.tokenStore?.clear();
|
|
2074
2141
|
}
|
|
@@ -2224,6 +2291,7 @@ function withoutCap(mask, cap) {
|
|
|
2224
2291
|
export {
|
|
2225
2292
|
AdminApiClient,
|
|
2226
2293
|
AuthApiClient,
|
|
2294
|
+
AuthRevokedError,
|
|
2227
2295
|
CAPABILITIES,
|
|
2228
2296
|
CloudClient,
|
|
2229
2297
|
HTTPError,
|