@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.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var index_exports = {};
|
|
|
21
21
|
__export(index_exports, {
|
|
22
22
|
AdminApiClient: () => AdminApiClient,
|
|
23
23
|
AuthApiClient: () => AuthApiClient,
|
|
24
|
+
AuthRevokedError: () => AuthRevokedError,
|
|
24
25
|
CAPABILITIES: () => CAPABILITIES,
|
|
25
26
|
CloudClient: () => CloudClient,
|
|
26
27
|
HTTPError: () => HTTPError,
|
|
@@ -92,6 +93,7 @@ function createTimeoutSignal(timeoutMs) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// src/http-client/web-client.ts
|
|
96
|
+
var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set(["token_reuse", "token_revoked"]);
|
|
95
97
|
var HTTPError = class extends Error {
|
|
96
98
|
constructor(status, statusText, url, headers, bodyText) {
|
|
97
99
|
super(`HTTP ${status} ${statusText}`);
|
|
@@ -112,6 +114,14 @@ var HTTPError = class extends Error {
|
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
116
|
};
|
|
117
|
+
var AuthRevokedError = class extends HTTPError {
|
|
118
|
+
constructor(reason, status, statusText, url, headers, bodyText) {
|
|
119
|
+
super(status, statusText, url, headers, bodyText);
|
|
120
|
+
this.reason = reason;
|
|
121
|
+
this.name = "AuthRevokedError";
|
|
122
|
+
this.message = `Authentication revoked (${reason}): HTTP ${status} ${statusText}`;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
115
125
|
function headersToRecord(headers) {
|
|
116
126
|
const record = {};
|
|
117
127
|
headers.forEach((value, key) => {
|
|
@@ -256,8 +266,27 @@ var WebHttpClient = class {
|
|
|
256
266
|
response.headers,
|
|
257
267
|
bodyText
|
|
258
268
|
);
|
|
269
|
+
const authError = response.headers.get("x-auth-error");
|
|
270
|
+
if ((response.status === 401 || response.status === 403) && authError && TERMINAL_AUTH_ERRORS.has(authError)) {
|
|
271
|
+
this.refreshTokenPromise = null;
|
|
272
|
+
this.onTokenRefreshPromise = null;
|
|
273
|
+
if (this.transport.onAuthRevoked) {
|
|
274
|
+
try {
|
|
275
|
+
await this.transport.onAuthRevoked();
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
throw new AuthRevokedError(
|
|
280
|
+
authError,
|
|
281
|
+
response.status,
|
|
282
|
+
response.statusText,
|
|
283
|
+
url,
|
|
284
|
+
response.headers,
|
|
285
|
+
bodyText
|
|
286
|
+
);
|
|
287
|
+
}
|
|
259
288
|
const userAborted = init?.signal?.aborted === true;
|
|
260
|
-
if (response.status === 401 && this.transport.refreshToken &&
|
|
289
|
+
if (response.status === 401 && this.transport.refreshToken && authError === "token_expired" && retryCount < MAX_RETRY_ATTEMPTS && !isStreamBody && !userAborted) {
|
|
261
290
|
try {
|
|
262
291
|
let refreshPromise = this.refreshTokenPromise;
|
|
263
292
|
if (!refreshPromise) {
|
|
@@ -424,6 +453,7 @@ function createBrowserHttpClient(options) {
|
|
|
424
453
|
getAuthToken: options.getAuthToken,
|
|
425
454
|
onTokenRefresh: options.onTokenRefresh,
|
|
426
455
|
refreshToken: options.refreshToken,
|
|
456
|
+
onAuthRevoked: options.onAuthRevoked,
|
|
427
457
|
defaultHeaders: options.defaultHeaders,
|
|
428
458
|
timeoutMs: options.timeoutMs,
|
|
429
459
|
credentials: options.credentials,
|
|
@@ -449,6 +479,7 @@ function createNodeHttpClient(options) {
|
|
|
449
479
|
getAuthToken: options.getAuthToken,
|
|
450
480
|
onTokenRefresh: options.onTokenRefresh,
|
|
451
481
|
refreshToken: options.refreshToken,
|
|
482
|
+
onAuthRevoked: options.onAuthRevoked,
|
|
452
483
|
defaultHeaders: options.defaultHeaders,
|
|
453
484
|
timeoutMs: options.timeoutMs,
|
|
454
485
|
credentials: options.credentials,
|
|
@@ -1198,7 +1229,7 @@ var AdminApiClient = class {
|
|
|
1198
1229
|
const response = await this.httpClient.get(
|
|
1199
1230
|
`/admin-api/groups/${groupId}/metadata`
|
|
1200
1231
|
);
|
|
1201
|
-
return response?.data
|
|
1232
|
+
return response?.data ?? null;
|
|
1202
1233
|
}
|
|
1203
1234
|
async setMemberMetadata(groupId, identity, request) {
|
|
1204
1235
|
await this.httpClient.put(`/admin-api/groups/${groupId}/members/${identity}/metadata`, request);
|
|
@@ -1207,7 +1238,7 @@ var AdminApiClient = class {
|
|
|
1207
1238
|
const response = await this.httpClient.get(
|
|
1208
1239
|
`/admin-api/groups/${groupId}/members/${identity}/metadata`
|
|
1209
1240
|
);
|
|
1210
|
-
return response?.data
|
|
1241
|
+
return response?.data ?? null;
|
|
1211
1242
|
}
|
|
1212
1243
|
async setContextMetadata(groupId, contextId, request) {
|
|
1213
1244
|
await this.httpClient.put(`/admin-api/groups/${groupId}/contexts/${contextId}/metadata`, request);
|
|
@@ -1216,7 +1247,7 @@ var AdminApiClient = class {
|
|
|
1216
1247
|
const response = await this.httpClient.get(
|
|
1217
1248
|
`/admin-api/groups/${groupId}/contexts/${contextId}/metadata`
|
|
1218
1249
|
);
|
|
1219
|
-
return response?.data
|
|
1250
|
+
return response?.data ?? null;
|
|
1220
1251
|
}
|
|
1221
1252
|
async syncGroup(groupId, request) {
|
|
1222
1253
|
return unwrap(await this.httpClient.post(`/admin-api/groups/${groupId}/sync`, request ?? {}));
|
|
@@ -1912,6 +1943,12 @@ _WsClient.MAX_BACKOFF_MS = 3e4;
|
|
|
1912
1943
|
var WsClient = _WsClient;
|
|
1913
1944
|
|
|
1914
1945
|
// src/mero-js.ts
|
|
1946
|
+
var REFRESH_LOCK_NAME = "mero-js:token-refresh";
|
|
1947
|
+
function getLockManager() {
|
|
1948
|
+
const nav = globalThis.navigator;
|
|
1949
|
+
const locks = nav?.locks;
|
|
1950
|
+
return locks && typeof locks.request === "function" ? locks : null;
|
|
1951
|
+
}
|
|
1915
1952
|
function expiresAtFromJwt(token, fallbackMs) {
|
|
1916
1953
|
try {
|
|
1917
1954
|
const parts = token.split(".");
|
|
@@ -1960,6 +1997,9 @@ var MeroJs = class {
|
|
|
1960
1997
|
this.tokenStore?.setTokens(this.tokenData);
|
|
1961
1998
|
}
|
|
1962
1999
|
},
|
|
2000
|
+
onAuthRevoked: () => {
|
|
2001
|
+
this.clearToken();
|
|
2002
|
+
},
|
|
1963
2003
|
timeoutMs: this.config.timeoutMs,
|
|
1964
2004
|
credentials: this.config.requestCredentials ?? (isTauri ? "omit" : void 0)
|
|
1965
2005
|
});
|
|
@@ -2082,27 +2122,52 @@ var MeroJs = class {
|
|
|
2082
2122
|
return this.tokenData;
|
|
2083
2123
|
}
|
|
2084
2124
|
/**
|
|
2085
|
-
* Refresh the access token
|
|
2125
|
+
* Refresh the access token. Single-flight: every caller (including the HTTP
|
|
2126
|
+
* transport's 401 hook) goes through here.
|
|
2127
|
+
*
|
|
2128
|
+
* Refresh tokens are single-use (calimero-network/core#3083): each refresh
|
|
2129
|
+
* consumes the presented token and returns a new one, and replaying a consumed
|
|
2130
|
+
* token makes the server revoke the whole family. A refresh must therefore
|
|
2131
|
+
* happen exactly once per rotation, which needs three layers:
|
|
2132
|
+
* - an in-process promise, so concurrent 401s in this instance share one call;
|
|
2133
|
+
* - a Web Lock (when available), so other tabs/webviews don't refresh at the
|
|
2134
|
+
* same time on the same stored token;
|
|
2135
|
+
* - a re-read of the token store inside the lock, so a bundle that another
|
|
2136
|
+
* instance already rotated is adopted instead of replaying a stale token.
|
|
2086
2137
|
*/
|
|
2087
|
-
async
|
|
2088
|
-
if (!this.tokenData?.refresh_token) {
|
|
2089
|
-
throw new Error("No refresh token available");
|
|
2090
|
-
}
|
|
2138
|
+
async performTokenRefresh() {
|
|
2091
2139
|
if (this.refreshPromise) {
|
|
2092
2140
|
return this.refreshPromise;
|
|
2093
2141
|
}
|
|
2094
|
-
|
|
2142
|
+
const triggeringAccessToken = this.tokenData?.access_token;
|
|
2143
|
+
const promise = this.withRefreshLock(
|
|
2144
|
+
() => this.performTokenRefreshLocked(triggeringAccessToken)
|
|
2145
|
+
);
|
|
2146
|
+
this.refreshPromise = promise;
|
|
2095
2147
|
try {
|
|
2096
|
-
|
|
2097
|
-
return newToken;
|
|
2148
|
+
return await promise;
|
|
2098
2149
|
} finally {
|
|
2099
|
-
this.refreshPromise
|
|
2150
|
+
if (this.refreshPromise === promise) {
|
|
2151
|
+
this.refreshPromise = null;
|
|
2152
|
+
}
|
|
2100
2153
|
}
|
|
2101
2154
|
}
|
|
2155
|
+
/** Run `fn` under the cross-tab refresh lock, or directly if Web Locks are unavailable. */
|
|
2156
|
+
withRefreshLock(fn) {
|
|
2157
|
+
const locks = getLockManager();
|
|
2158
|
+
return locks ? locks.request(REFRESH_LOCK_NAME, fn) : fn();
|
|
2159
|
+
}
|
|
2102
2160
|
/**
|
|
2103
|
-
* Perform the actual token refresh
|
|
2161
|
+
* Perform the actual token refresh. Runs with the refresh lock held.
|
|
2104
2162
|
*/
|
|
2105
|
-
async
|
|
2163
|
+
async performTokenRefreshLocked(triggeringAccessToken) {
|
|
2164
|
+
const stored = this.tokenStore?.getTokens() ?? null;
|
|
2165
|
+
if (stored?.refresh_token) {
|
|
2166
|
+
this.tokenData = stored;
|
|
2167
|
+
if (triggeringAccessToken && stored.access_token !== triggeringAccessToken) {
|
|
2168
|
+
return stored;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2106
2171
|
if (!this.tokenData?.refresh_token) {
|
|
2107
2172
|
throw new Error("No refresh token available");
|
|
2108
2173
|
}
|
|
@@ -2114,6 +2179,8 @@ var MeroJs = class {
|
|
|
2114
2179
|
const accessToken = response.data.access_token;
|
|
2115
2180
|
this.tokenData = {
|
|
2116
2181
|
access_token: accessToken,
|
|
2182
|
+
// The refresh token is rotated on every refresh — persist the new one or
|
|
2183
|
+
// the next refresh replays a consumed token.
|
|
2117
2184
|
refresh_token: response.data.refresh_token,
|
|
2118
2185
|
expires_at: expiresAtFromJwt(accessToken, Date.now() + 36e5)
|
|
2119
2186
|
};
|
|
@@ -2129,6 +2196,7 @@ var MeroJs = class {
|
|
|
2129
2196
|
* Clear the current token
|
|
2130
2197
|
*/
|
|
2131
2198
|
clearToken() {
|
|
2199
|
+
this.refreshPromise = null;
|
|
2132
2200
|
this.tokenData = null;
|
|
2133
2201
|
this.tokenStore?.clear();
|
|
2134
2202
|
}
|