@calimero-network/mero-js 27.1.0 → 27.2.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 +5 -2
- package/dist/cloud-v2/client.d.ts +67 -5
- package/dist/cloud-v2/client.js +151 -28
- package/dist/cloud-v2/errors.d.ts +26 -1
- package/dist/cloud-v2/errors.js +31 -1
- package/dist/cloud-v2/index.d.ts +1 -1
- package/dist/cloud-v2/index.js +1 -1
- package/dist/index.browser.mjs +5 -5
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +137 -22
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +137 -22
- 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,
|
|
@@ -1340,6 +1341,12 @@ var LoginRequired = class extends CalimeroError {
|
|
|
1340
1341
|
this.name = "LoginRequired";
|
|
1341
1342
|
}
|
|
1342
1343
|
};
|
|
1344
|
+
var WrongNode = class extends CalimeroError {
|
|
1345
|
+
constructor(message, details = {}) {
|
|
1346
|
+
super(421, "WRONG_NODE", message, details);
|
|
1347
|
+
this.name = "WrongNode";
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1343
1350
|
var NotFound = class extends CalimeroError {
|
|
1344
1351
|
constructor(message, details = {}) {
|
|
1345
1352
|
super(404, "NOT_FOUND", message, details);
|
|
@@ -1434,6 +1441,8 @@ function errorFrom(status, body, retryAfterHeader) {
|
|
|
1434
1441
|
switch (`${status} ${code}`) {
|
|
1435
1442
|
case "401 UNAUTHENTICATED":
|
|
1436
1443
|
return new LoginRequired(message, details);
|
|
1444
|
+
case "421 WRONG_NODE":
|
|
1445
|
+
return new WrongNode(message, details);
|
|
1437
1446
|
case "404 NOT_FOUND":
|
|
1438
1447
|
return new NotFound(message, details);
|
|
1439
1448
|
case "409 IDEMPOTENCY_MISMATCH":
|
|
@@ -1688,11 +1697,20 @@ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
|
|
|
1688
1697
|
function retryDelayMs(retry, options) {
|
|
1689
1698
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
1690
1699
|
}
|
|
1700
|
+
function ceremony(path) {
|
|
1701
|
+
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1702
|
+
}
|
|
1691
1703
|
var CalimeroClient = class {
|
|
1692
1704
|
constructor(options) {
|
|
1705
|
+
/** The one in use: an index into {@link nodeUrls}. */
|
|
1706
|
+
this.at = 0;
|
|
1707
|
+
/** A sealed fetch per node, each attesting its own node on first use. */
|
|
1708
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
1693
1709
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
1694
1710
|
this.networkFailures = /* @__PURE__ */ new WeakSet();
|
|
1695
|
-
|
|
1711
|
+
const trim = (url2) => url2.replace(/\/+$/, "");
|
|
1712
|
+
this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
|
|
1713
|
+
this.policy = options.policy;
|
|
1696
1714
|
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
1697
1715
|
const given = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1698
1716
|
const raw = async (input, init) => {
|
|
@@ -1704,11 +1722,33 @@ var CalimeroClient = class {
|
|
|
1704
1722
|
}
|
|
1705
1723
|
};
|
|
1706
1724
|
this.raw = raw;
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1725
|
+
}
|
|
1726
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` moved the client on. */
|
|
1727
|
+
get nodeUrl() {
|
|
1728
|
+
return this.nodeUrls[this.at];
|
|
1729
|
+
}
|
|
1730
|
+
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
1731
|
+
channel(url2) {
|
|
1732
|
+
let sealed = this.channels.get(url2);
|
|
1733
|
+
if (!sealed) {
|
|
1734
|
+
sealed = createSealedFetch({
|
|
1735
|
+
baseUrl: url2,
|
|
1736
|
+
transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
|
|
1737
|
+
fetch: this.raw
|
|
1738
|
+
});
|
|
1739
|
+
this.channels.set(url2, sealed);
|
|
1740
|
+
}
|
|
1741
|
+
return sealed;
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
|
|
1745
|
+
* next one. False when there is no other. A request that saw the same node
|
|
1746
|
+
* refuse after another request moved on already does not move again.
|
|
1747
|
+
*/
|
|
1748
|
+
moveOn(from) {
|
|
1749
|
+
if (this.nodeUrls.length < 2) return false;
|
|
1750
|
+
if (this.at === from) this.at = (from + 1) % this.nodeUrls.length;
|
|
1751
|
+
return true;
|
|
1712
1752
|
}
|
|
1713
1753
|
/** Authenticate every request with an API key. */
|
|
1714
1754
|
useApiKey(key) {
|
|
@@ -1722,7 +1762,11 @@ var CalimeroClient = class {
|
|
|
1722
1762
|
get session() {
|
|
1723
1763
|
return this.current;
|
|
1724
1764
|
}
|
|
1725
|
-
|
|
1765
|
+
/**
|
|
1766
|
+
* Send one request to the node in use, with the credential held now, and
|
|
1767
|
+
* answer the response, whatever its status. {@link send} judges it.
|
|
1768
|
+
*/
|
|
1769
|
+
async request(method, path, body, raw, contentType = "application/wasm", signal) {
|
|
1726
1770
|
const headers = {};
|
|
1727
1771
|
if (this.auth) headers.authorization = `Bearer ${this.auth}`;
|
|
1728
1772
|
let payload;
|
|
@@ -1733,15 +1777,66 @@ var CalimeroClient = class {
|
|
|
1733
1777
|
headers["content-type"] = "application/json";
|
|
1734
1778
|
payload = JSON.stringify(body);
|
|
1735
1779
|
}
|
|
1780
|
+
const url2 = this.nodeUrl;
|
|
1736
1781
|
try {
|
|
1737
|
-
return await this.
|
|
1782
|
+
return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
|
|
1738
1783
|
} catch (error) {
|
|
1739
1784
|
throw sealedError(error);
|
|
1740
1785
|
}
|
|
1741
1786
|
}
|
|
1787
|
+
/**
|
|
1788
|
+
* {@link request}, answering what a lost issuing node or a node on the
|
|
1789
|
+
* wrong release does to a session: a successful response, or the refusal as a
|
|
1790
|
+
* typed error ({@link errorFrom}).
|
|
1791
|
+
*
|
|
1792
|
+
* - `401 UNAUTHENTICATED` to a request that carried a session: the node that
|
|
1793
|
+
* signed the session may be gone, replaced, or out of reach of the node
|
|
1794
|
+
* asked, which any node of the cluster answers with a refresh. The client
|
|
1795
|
+
* refreshes once, shared by every request that needs it at the same time
|
|
1796
|
+
* ({@link refresh}), and repeats the request once. A refresh that is
|
|
1797
|
+
* refused surfaces as {@link LoginRequired}.
|
|
1798
|
+
* - `421 WRONG_NODE`: this node's release may not serve the account. The
|
|
1799
|
+
* client moves to the next node it knows and repeats the request once; with
|
|
1800
|
+
* none, the {@link WrongNode} reaches the caller.
|
|
1801
|
+
*
|
|
1802
|
+
* Repeating is safe even for a request that is not idempotent: the node
|
|
1803
|
+
* answers `401` and `421` when it judges the credential, before it reads what
|
|
1804
|
+
* the request asks, so the first attempt ran nothing. The exceptions are the
|
|
1805
|
+
* ceremonies that mint a session ({@link ceremony}), never repeated here.
|
|
1806
|
+
*/
|
|
1807
|
+
async send(method, path, body, raw, contentType, signal) {
|
|
1808
|
+
let refreshed = false;
|
|
1809
|
+
let moved = false;
|
|
1810
|
+
for (; ; ) {
|
|
1811
|
+
const [token, at] = [this.auth, this.at];
|
|
1812
|
+
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
1813
|
+
if (response.ok) return response;
|
|
1814
|
+
const error = await refusal(response);
|
|
1815
|
+
if (error instanceof WrongNode && !moved) {
|
|
1816
|
+
moved = true;
|
|
1817
|
+
if (this.moveOn(at) && !ceremony(path)) continue;
|
|
1818
|
+
} else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
|
|
1819
|
+
refreshed = true;
|
|
1820
|
+
await this.renew(token);
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
throw error;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/** Whether a `401` to a request sent with `token` is one a refresh may answer. */
|
|
1827
|
+
mayRefresh(token, path) {
|
|
1828
|
+
return token !== void 0 && this.current !== void 0 && !ceremony(path);
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* A fresh session in place of the one `token` belonged to: by the refresh
|
|
1832
|
+
* another request already made, or by one this request starts.
|
|
1833
|
+
*/
|
|
1834
|
+
async renew(token) {
|
|
1835
|
+
if (this.auth !== token) return;
|
|
1836
|
+
await this.refresh();
|
|
1837
|
+
}
|
|
1742
1838
|
async json(method, path, body, raw, contentType) {
|
|
1743
|
-
const response = await this.
|
|
1744
|
-
if (!response.ok) throw await refusal(response);
|
|
1839
|
+
const response = await this.send(method, path, body, raw, contentType);
|
|
1745
1840
|
const text = await response.text();
|
|
1746
1841
|
return text ? JSON.parse(text) : {};
|
|
1747
1842
|
}
|
|
@@ -1810,11 +1905,35 @@ var CalimeroClient = class {
|
|
|
1810
1905
|
this.useSession(session);
|
|
1811
1906
|
return session;
|
|
1812
1907
|
}
|
|
1813
|
-
/**
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1908
|
+
/**
|
|
1909
|
+
* Swap the refresh token for a new session, on whichever node the client
|
|
1910
|
+
* uses: any node of the cluster takes it. A refresh token is used once, so
|
|
1911
|
+
* calls made while one refresh is under way share it rather than sending the
|
|
1912
|
+
* same token again (which would revoke the session).
|
|
1913
|
+
*/
|
|
1914
|
+
refresh() {
|
|
1915
|
+
if (!this.refreshing) {
|
|
1916
|
+
const pending = this.exchangeRefresh();
|
|
1917
|
+
this.refreshing = pending;
|
|
1918
|
+
const clear = () => {
|
|
1919
|
+
if (this.refreshing === pending) this.refreshing = void 0;
|
|
1920
|
+
};
|
|
1921
|
+
pending.then(clear, clear);
|
|
1922
|
+
}
|
|
1923
|
+
return this.refreshing;
|
|
1924
|
+
}
|
|
1925
|
+
async exchangeRefresh() {
|
|
1926
|
+
const current = this.current;
|
|
1927
|
+
if (!current) throw new Error("no session to refresh");
|
|
1928
|
+
let session;
|
|
1929
|
+
for (let tried = 1; session === void 0; tried++) {
|
|
1930
|
+
try {
|
|
1931
|
+
session = await this.post("auth/refresh", { refreshToken: current.refreshToken });
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (this.current === current) this.useSession(session);
|
|
1818
1937
|
return session;
|
|
1819
1938
|
}
|
|
1820
1939
|
async signOut() {
|
|
@@ -1859,7 +1978,7 @@ var CalimeroClient = class {
|
|
|
1859
1978
|
*/
|
|
1860
1979
|
async attestation() {
|
|
1861
1980
|
const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
1862
|
-
const response = await this.raw(`${this.
|
|
1981
|
+
const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
|
|
1863
1982
|
if (!response.ok) throw await refusal(response);
|
|
1864
1983
|
return await response.json();
|
|
1865
1984
|
}
|
|
@@ -1887,8 +2006,7 @@ var CalimeroClient = class {
|
|
|
1887
2006
|
}
|
|
1888
2007
|
/** One of `app`'s blobs, by id (base58 or hex). */
|
|
1889
2008
|
async downloadBlob(app, id) {
|
|
1890
|
-
const response = await this.
|
|
1891
|
-
if (!response.ok) throw await refusal(response);
|
|
2009
|
+
const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
|
|
1892
2010
|
return new Uint8Array(await response.arrayBuffer());
|
|
1893
2011
|
}
|
|
1894
2012
|
/** `app`'s blobs. */
|
|
@@ -2057,10 +2175,7 @@ var CalimeroClient = class {
|
|
|
2057
2175
|
}
|
|
2058
2176
|
/** A server-sent event stream on `path`, sealed like every other answer. */
|
|
2059
2177
|
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);
|
|
2178
|
+
const response = await this.send("GET", path, void 0, void 0, void 0, signal);
|
|
2064
2179
|
if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
|
|
2065
2180
|
yield* sseFrames(response.body);
|
|
2066
2181
|
}
|