@calimero-network/mero-js 27.1.0 → 27.3.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/README.md +6 -3
- package/dist/cloud-v2/approvals.d.ts +27 -0
- package/dist/cloud-v2/approvals.js +18 -0
- package/dist/cloud-v2/client.d.ts +77 -10
- package/dist/cloud-v2/client.js +181 -38
- package/dist/cloud-v2/errors.d.ts +26 -1
- package/dist/cloud-v2/errors.js +31 -1
- package/dist/cloud-v2/index.d.ts +3 -3
- package/dist/cloud-v2/index.js +2 -2
- package/dist/index.browser.mjs +5 -5
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +175 -30
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +175 -30
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -60,6 +60,7 @@ __export(index_exports, {
|
|
|
60
60
|
StaleTransportKeyError: () => StaleTransportKeyError,
|
|
61
61
|
TrapError: () => TrapError,
|
|
62
62
|
UnavailableError: () => UnavailableError,
|
|
63
|
+
WrongNode: () => WrongNode,
|
|
63
64
|
agentKeyBinding: () => agentKeyBinding,
|
|
64
65
|
agentQuoteVerifier: () => agentQuoteVerifier,
|
|
65
66
|
attestAgent: () => attestAgent,
|
|
@@ -69,6 +70,7 @@ __export(index_exports, {
|
|
|
69
70
|
claimedRelease: () => claimedRelease,
|
|
70
71
|
cloudNodeReleaseUrl: () => cloudNodeReleaseUrl,
|
|
71
72
|
compareVersions: () => compareVersions,
|
|
73
|
+
confirmationCode: () => confirmationCode,
|
|
72
74
|
consentReturn: () => consentReturn,
|
|
73
75
|
createQuoteVerifier: () => createQuoteVerifier,
|
|
74
76
|
createSealedFetch: () => createSealedFetch,
|
|
@@ -1340,6 +1342,12 @@ var LoginRequired = class extends CalimeroError {
|
|
|
1340
1342
|
this.name = "LoginRequired";
|
|
1341
1343
|
}
|
|
1342
1344
|
};
|
|
1345
|
+
var WrongNode = class extends CalimeroError {
|
|
1346
|
+
constructor(message, details = {}) {
|
|
1347
|
+
super(421, "WRONG_NODE", message, details);
|
|
1348
|
+
this.name = "WrongNode";
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1343
1351
|
var NotFound = class extends CalimeroError {
|
|
1344
1352
|
constructor(message, details = {}) {
|
|
1345
1353
|
super(404, "NOT_FOUND", message, details);
|
|
@@ -1434,6 +1442,8 @@ function errorFrom(status, body, retryAfterHeader) {
|
|
|
1434
1442
|
switch (`${status} ${code}`) {
|
|
1435
1443
|
case "401 UNAUTHENTICATED":
|
|
1436
1444
|
return new LoginRequired(message, details);
|
|
1445
|
+
case "421 WRONG_NODE":
|
|
1446
|
+
return new WrongNode(message, details);
|
|
1437
1447
|
case "404 NOT_FOUND":
|
|
1438
1448
|
return new NotFound(message, details);
|
|
1439
1449
|
case "409 IDEMPOTENCY_MISMATCH":
|
|
@@ -1560,6 +1570,17 @@ ${generation}`;
|
|
|
1560
1570
|
function publisherBindingBytes(cluster, org, signerId, generation) {
|
|
1561
1571
|
return new TextEncoder().encode(publisherBindingMessage(cluster, org, signerId, generation));
|
|
1562
1572
|
}
|
|
1573
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1574
|
+
async function confirmationCode(requestId) {
|
|
1575
|
+
const h = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(requestId)));
|
|
1576
|
+
const word = (h[0] << 24 | h[1] << 16 | h[2] << 8 | h[3]) >>> 0;
|
|
1577
|
+
let code = "";
|
|
1578
|
+
for (let i = 0; i < 6; i++) {
|
|
1579
|
+
if (i === 3) code += "-";
|
|
1580
|
+
code += CROCKFORD[word >>> 27 - 5 * i & 31];
|
|
1581
|
+
}
|
|
1582
|
+
return code;
|
|
1583
|
+
}
|
|
1563
1584
|
function consentReturn(href) {
|
|
1564
1585
|
const url2 = typeof href === "string" ? new URL(href) : href;
|
|
1565
1586
|
const requestId = url2.searchParams.get("request_id");
|
|
@@ -1688,11 +1709,20 @@ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
|
|
|
1688
1709
|
function retryDelayMs(retry, options) {
|
|
1689
1710
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
1690
1711
|
}
|
|
1712
|
+
function ceremony(path) {
|
|
1713
|
+
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1714
|
+
}
|
|
1691
1715
|
var CalimeroClient = class {
|
|
1692
1716
|
constructor(options) {
|
|
1717
|
+
/** The one in use: an index into {@link nodeUrls}. */
|
|
1718
|
+
this.at = 0;
|
|
1719
|
+
/** A sealed fetch per node, each attesting its own node on first use. */
|
|
1720
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
1693
1721
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
1694
1722
|
this.networkFailures = /* @__PURE__ */ new WeakSet();
|
|
1695
|
-
|
|
1723
|
+
const trim = (url2) => url2.replace(/\/+$/, "");
|
|
1724
|
+
this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
|
|
1725
|
+
this.policy = options.policy;
|
|
1696
1726
|
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
1697
1727
|
const given = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1698
1728
|
const raw = async (input, init) => {
|
|
@@ -1704,11 +1734,33 @@ var CalimeroClient = class {
|
|
|
1704
1734
|
}
|
|
1705
1735
|
};
|
|
1706
1736
|
this.raw = raw;
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1737
|
+
}
|
|
1738
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` moved the client on. */
|
|
1739
|
+
get nodeUrl() {
|
|
1740
|
+
return this.nodeUrls[this.at];
|
|
1741
|
+
}
|
|
1742
|
+
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
1743
|
+
channel(url2) {
|
|
1744
|
+
let sealed = this.channels.get(url2);
|
|
1745
|
+
if (!sealed) {
|
|
1746
|
+
sealed = createSealedFetch({
|
|
1747
|
+
baseUrl: url2,
|
|
1748
|
+
transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
|
|
1749
|
+
fetch: this.raw
|
|
1750
|
+
});
|
|
1751
|
+
this.channels.set(url2, sealed);
|
|
1752
|
+
}
|
|
1753
|
+
return sealed;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
|
|
1757
|
+
* next one. False when there is no other. A request that saw the same node
|
|
1758
|
+
* refuse after another request moved on already does not move again.
|
|
1759
|
+
*/
|
|
1760
|
+
moveOn(from) {
|
|
1761
|
+
if (this.nodeUrls.length < 2) return false;
|
|
1762
|
+
if (this.at === from) this.at = (from + 1) % this.nodeUrls.length;
|
|
1763
|
+
return true;
|
|
1712
1764
|
}
|
|
1713
1765
|
/** Authenticate every request with an API key. */
|
|
1714
1766
|
useApiKey(key) {
|
|
@@ -1722,7 +1774,11 @@ var CalimeroClient = class {
|
|
|
1722
1774
|
get session() {
|
|
1723
1775
|
return this.current;
|
|
1724
1776
|
}
|
|
1725
|
-
|
|
1777
|
+
/**
|
|
1778
|
+
* Send one request to the node in use, with the credential held now, and
|
|
1779
|
+
* answer the response, whatever its status. {@link send} judges it.
|
|
1780
|
+
*/
|
|
1781
|
+
async request(method, path, body, raw, contentType = "application/wasm", signal) {
|
|
1726
1782
|
const headers = {};
|
|
1727
1783
|
if (this.auth) headers.authorization = `Bearer ${this.auth}`;
|
|
1728
1784
|
let payload;
|
|
@@ -1733,15 +1789,66 @@ var CalimeroClient = class {
|
|
|
1733
1789
|
headers["content-type"] = "application/json";
|
|
1734
1790
|
payload = JSON.stringify(body);
|
|
1735
1791
|
}
|
|
1792
|
+
const url2 = this.nodeUrl;
|
|
1736
1793
|
try {
|
|
1737
|
-
return await this.
|
|
1794
|
+
return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
|
|
1738
1795
|
} catch (error) {
|
|
1739
1796
|
throw sealedError(error);
|
|
1740
1797
|
}
|
|
1741
1798
|
}
|
|
1799
|
+
/**
|
|
1800
|
+
* {@link request}, answering what a lost issuing node or a node on the
|
|
1801
|
+
* wrong release does to a session: a successful response, or the refusal as a
|
|
1802
|
+
* typed error ({@link errorFrom}).
|
|
1803
|
+
*
|
|
1804
|
+
* - `401 UNAUTHENTICATED` to a request that carried a session: the node that
|
|
1805
|
+
* signed the session may be gone, replaced, or out of reach of the node
|
|
1806
|
+
* asked, which any node of the cluster answers with a refresh. The client
|
|
1807
|
+
* refreshes once, shared by every request that needs it at the same time
|
|
1808
|
+
* ({@link refresh}), and repeats the request once. A refresh that is
|
|
1809
|
+
* refused surfaces as {@link LoginRequired}.
|
|
1810
|
+
* - `421 WRONG_NODE`: this node's release may not serve the account. The
|
|
1811
|
+
* client moves to the next node it knows and repeats the request once; with
|
|
1812
|
+
* none, the {@link WrongNode} reaches the caller.
|
|
1813
|
+
*
|
|
1814
|
+
* Repeating is safe even for a request that is not idempotent: the node
|
|
1815
|
+
* answers `401` and `421` when it judges the credential, before it reads what
|
|
1816
|
+
* the request asks, so the first attempt ran nothing. The exceptions are the
|
|
1817
|
+
* ceremonies that mint a session ({@link ceremony}), never repeated here.
|
|
1818
|
+
*/
|
|
1819
|
+
async send(method, path, body, raw, contentType, signal) {
|
|
1820
|
+
let refreshed = false;
|
|
1821
|
+
let moved = false;
|
|
1822
|
+
for (; ; ) {
|
|
1823
|
+
const [token, at] = [this.auth, this.at];
|
|
1824
|
+
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
1825
|
+
if (response.ok) return response;
|
|
1826
|
+
const error = await refusal(response);
|
|
1827
|
+
if (error instanceof WrongNode && !moved) {
|
|
1828
|
+
moved = true;
|
|
1829
|
+
if (this.moveOn(at) && !ceremony(path)) continue;
|
|
1830
|
+
} else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
|
|
1831
|
+
refreshed = true;
|
|
1832
|
+
await this.renew(token);
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
throw error;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
/** Whether a `401` to a request sent with `token` is one a refresh may answer. */
|
|
1839
|
+
mayRefresh(token, path) {
|
|
1840
|
+
return token !== void 0 && this.current !== void 0 && !ceremony(path);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* A fresh session in place of the one `token` belonged to: by the refresh
|
|
1844
|
+
* another request already made, or by one this request starts.
|
|
1845
|
+
*/
|
|
1846
|
+
async renew(token) {
|
|
1847
|
+
if (this.auth !== token) return;
|
|
1848
|
+
await this.refresh();
|
|
1849
|
+
}
|
|
1742
1850
|
async json(method, path, body, raw, contentType) {
|
|
1743
|
-
const response = await this.
|
|
1744
|
-
if (!response.ok) throw await refusal(response);
|
|
1851
|
+
const response = await this.send(method, path, body, raw, contentType);
|
|
1745
1852
|
const text = await response.text();
|
|
1746
1853
|
return text ? JSON.parse(text) : {};
|
|
1747
1854
|
}
|
|
@@ -1810,11 +1917,35 @@ var CalimeroClient = class {
|
|
|
1810
1917
|
this.useSession(session);
|
|
1811
1918
|
return session;
|
|
1812
1919
|
}
|
|
1813
|
-
/**
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1920
|
+
/**
|
|
1921
|
+
* Swap the refresh token for a new session, on whichever node the client
|
|
1922
|
+
* uses: any node of the cluster takes it. A refresh token is used once, so
|
|
1923
|
+
* calls made while one refresh is under way share it rather than sending the
|
|
1924
|
+
* same token again (which would revoke the session).
|
|
1925
|
+
*/
|
|
1926
|
+
refresh() {
|
|
1927
|
+
if (!this.refreshing) {
|
|
1928
|
+
const pending = this.exchangeRefresh();
|
|
1929
|
+
this.refreshing = pending;
|
|
1930
|
+
const clear = () => {
|
|
1931
|
+
if (this.refreshing === pending) this.refreshing = void 0;
|
|
1932
|
+
};
|
|
1933
|
+
pending.then(clear, clear);
|
|
1934
|
+
}
|
|
1935
|
+
return this.refreshing;
|
|
1936
|
+
}
|
|
1937
|
+
async exchangeRefresh() {
|
|
1938
|
+
const current = this.current;
|
|
1939
|
+
if (!current) throw new Error("no session to refresh");
|
|
1940
|
+
let session;
|
|
1941
|
+
for (let tried = 1; session === void 0; tried++) {
|
|
1942
|
+
try {
|
|
1943
|
+
session = await this.post("auth/refresh", { refreshToken: current.refreshToken });
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (this.current === current) this.useSession(session);
|
|
1818
1949
|
return session;
|
|
1819
1950
|
}
|
|
1820
1951
|
async signOut() {
|
|
@@ -1859,7 +1990,7 @@ var CalimeroClient = class {
|
|
|
1859
1990
|
*/
|
|
1860
1991
|
async attestation() {
|
|
1861
1992
|
const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
1862
|
-
const response = await this.raw(`${this.
|
|
1993
|
+
const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
|
|
1863
1994
|
if (!response.ok) throw await refusal(response);
|
|
1864
1995
|
return await response.json();
|
|
1865
1996
|
}
|
|
@@ -1887,8 +2018,7 @@ var CalimeroClient = class {
|
|
|
1887
2018
|
}
|
|
1888
2019
|
/** One of `app`'s blobs, by id (base58 or hex). */
|
|
1889
2020
|
async downloadBlob(app, id) {
|
|
1890
|
-
const response = await this.
|
|
1891
|
-
if (!response.ok) throw await refusal(response);
|
|
2021
|
+
const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
|
|
1892
2022
|
return new Uint8Array(await response.arrayBuffer());
|
|
1893
2023
|
}
|
|
1894
2024
|
/** `app`'s blobs. */
|
|
@@ -2057,10 +2187,7 @@ var CalimeroClient = class {
|
|
|
2057
2187
|
}
|
|
2058
2188
|
/** A server-sent event stream on `path`, sealed like every other answer. */
|
|
2059
2189
|
async *stream(path, signal) {
|
|
2060
|
-
const
|
|
2061
|
-
if (this.auth) headers.authorization = `Bearer ${this.auth}`;
|
|
2062
|
-
const response = await this.sealed(`${this.base}/${path}`, { headers, signal });
|
|
2063
|
-
if (!response.ok) throw await refusal(response);
|
|
2190
|
+
const response = await this.send("GET", path, void 0, void 0, void 0, signal);
|
|
2064
2191
|
if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
|
|
2065
2192
|
yield* sseFrames(response.body);
|
|
2066
2193
|
}
|
|
@@ -2227,16 +2354,34 @@ var CalimeroClient = class {
|
|
|
2227
2354
|
}
|
|
2228
2355
|
/**
|
|
2229
2356
|
* {@link requestConsent}, then send the browser to the consent page
|
|
2230
|
-
* (`navigate`, by default `window.location.assign`).
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2357
|
+
* (`navigate`, by default `window.location.assign`). Pass `navigate` itself
|
|
2358
|
+
* or {@link OpenConsentOptions}, whose `confirm` runs first with the node's
|
|
2359
|
+
* answer so the app can show the person its `code`; when it answers `false`
|
|
2360
|
+
* the page is not opened. When the node sends a `code` that is not
|
|
2361
|
+
* {@link confirmationCode} of `requestId`, it throws `CalimeroError`
|
|
2362
|
+
* (`CONSENT_CODE_MISMATCH`) and does not navigate. Answers the request
|
|
2363
|
+
* once `navigate` returns (or `confirm` declines): with a popup
|
|
2364
|
+
* (`url => window.open(url)`), poll it with {@link waitForConsent}.
|
|
2233
2365
|
*/
|
|
2234
|
-
async openConsentPage(request,
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2366
|
+
async openConsentPage(request, navigateOrOptions = {}) {
|
|
2367
|
+
const options = typeof navigateOrOptions === "function" ? { navigate: navigateOrOptions } : navigateOrOptions;
|
|
2368
|
+
const navigate = options.navigate ?? ((url2) => {
|
|
2369
|
+
const w = globalThis.window;
|
|
2370
|
+
if (!w) throw new Error("no window to open the consent page in; pass `navigate`");
|
|
2371
|
+
w.location.assign(url2);
|
|
2372
|
+
});
|
|
2239
2373
|
const started = await this.requestConsent(request);
|
|
2374
|
+
if (started.code !== void 0) {
|
|
2375
|
+
const expected = await confirmationCode(started.requestId);
|
|
2376
|
+
if (started.code !== expected) {
|
|
2377
|
+
throw new CalimeroError(200, "CONSENT_CODE_MISMATCH", "the consent page code does not match the request", {
|
|
2378
|
+
requestId: started.requestId,
|
|
2379
|
+
code: started.code,
|
|
2380
|
+
expected
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (options.confirm && !await options.confirm(started)) return started;
|
|
2240
2385
|
navigate(started.url);
|
|
2241
2386
|
return started;
|
|
2242
2387
|
}
|