@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.mjs
CHANGED
|
@@ -1235,6 +1235,12 @@ var LoginRequired = class extends CalimeroError {
|
|
|
1235
1235
|
this.name = "LoginRequired";
|
|
1236
1236
|
}
|
|
1237
1237
|
};
|
|
1238
|
+
var WrongNode = class extends CalimeroError {
|
|
1239
|
+
constructor(message, details = {}) {
|
|
1240
|
+
super(421, "WRONG_NODE", message, details);
|
|
1241
|
+
this.name = "WrongNode";
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1238
1244
|
var NotFound = class extends CalimeroError {
|
|
1239
1245
|
constructor(message, details = {}) {
|
|
1240
1246
|
super(404, "NOT_FOUND", message, details);
|
|
@@ -1329,6 +1335,8 @@ function errorFrom(status, body, retryAfterHeader) {
|
|
|
1329
1335
|
switch (`${status} ${code}`) {
|
|
1330
1336
|
case "401 UNAUTHENTICATED":
|
|
1331
1337
|
return new LoginRequired(message, details);
|
|
1338
|
+
case "421 WRONG_NODE":
|
|
1339
|
+
return new WrongNode(message, details);
|
|
1332
1340
|
case "404 NOT_FOUND":
|
|
1333
1341
|
return new NotFound(message, details);
|
|
1334
1342
|
case "409 IDEMPOTENCY_MISMATCH":
|
|
@@ -1583,11 +1591,20 @@ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
|
|
|
1583
1591
|
function retryDelayMs(retry, options) {
|
|
1584
1592
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
1585
1593
|
}
|
|
1594
|
+
function ceremony(path) {
|
|
1595
|
+
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1596
|
+
}
|
|
1586
1597
|
var CalimeroClient = class {
|
|
1587
1598
|
constructor(options) {
|
|
1599
|
+
/** The one in use: an index into {@link nodeUrls}. */
|
|
1600
|
+
this.at = 0;
|
|
1601
|
+
/** A sealed fetch per node, each attesting its own node on first use. */
|
|
1602
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
1588
1603
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
1589
1604
|
this.networkFailures = /* @__PURE__ */ new WeakSet();
|
|
1590
|
-
|
|
1605
|
+
const trim = (url2) => url2.replace(/\/+$/, "");
|
|
1606
|
+
this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
|
|
1607
|
+
this.policy = options.policy;
|
|
1591
1608
|
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
1592
1609
|
const given = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1593
1610
|
const raw = async (input, init) => {
|
|
@@ -1599,11 +1616,33 @@ var CalimeroClient = class {
|
|
|
1599
1616
|
}
|
|
1600
1617
|
};
|
|
1601
1618
|
this.raw = raw;
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1619
|
+
}
|
|
1620
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` moved the client on. */
|
|
1621
|
+
get nodeUrl() {
|
|
1622
|
+
return this.nodeUrls[this.at];
|
|
1623
|
+
}
|
|
1624
|
+
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
1625
|
+
channel(url2) {
|
|
1626
|
+
let sealed = this.channels.get(url2);
|
|
1627
|
+
if (!sealed) {
|
|
1628
|
+
sealed = createSealedFetch({
|
|
1629
|
+
baseUrl: url2,
|
|
1630
|
+
transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
|
|
1631
|
+
fetch: this.raw
|
|
1632
|
+
});
|
|
1633
|
+
this.channels.set(url2, sealed);
|
|
1634
|
+
}
|
|
1635
|
+
return sealed;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
|
|
1639
|
+
* next one. False when there is no other. A request that saw the same node
|
|
1640
|
+
* refuse after another request moved on already does not move again.
|
|
1641
|
+
*/
|
|
1642
|
+
moveOn(from) {
|
|
1643
|
+
if (this.nodeUrls.length < 2) return false;
|
|
1644
|
+
if (this.at === from) this.at = (from + 1) % this.nodeUrls.length;
|
|
1645
|
+
return true;
|
|
1607
1646
|
}
|
|
1608
1647
|
/** Authenticate every request with an API key. */
|
|
1609
1648
|
useApiKey(key) {
|
|
@@ -1617,7 +1656,11 @@ var CalimeroClient = class {
|
|
|
1617
1656
|
get session() {
|
|
1618
1657
|
return this.current;
|
|
1619
1658
|
}
|
|
1620
|
-
|
|
1659
|
+
/**
|
|
1660
|
+
* Send one request to the node in use, with the credential held now, and
|
|
1661
|
+
* answer the response, whatever its status. {@link send} judges it.
|
|
1662
|
+
*/
|
|
1663
|
+
async request(method, path, body, raw, contentType = "application/wasm", signal) {
|
|
1621
1664
|
const headers = {};
|
|
1622
1665
|
if (this.auth) headers.authorization = `Bearer ${this.auth}`;
|
|
1623
1666
|
let payload;
|
|
@@ -1628,15 +1671,66 @@ var CalimeroClient = class {
|
|
|
1628
1671
|
headers["content-type"] = "application/json";
|
|
1629
1672
|
payload = JSON.stringify(body);
|
|
1630
1673
|
}
|
|
1674
|
+
const url2 = this.nodeUrl;
|
|
1631
1675
|
try {
|
|
1632
|
-
return await this.
|
|
1676
|
+
return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
|
|
1633
1677
|
} catch (error) {
|
|
1634
1678
|
throw sealedError(error);
|
|
1635
1679
|
}
|
|
1636
1680
|
}
|
|
1681
|
+
/**
|
|
1682
|
+
* {@link request}, answering what a lost issuing node or a node on the
|
|
1683
|
+
* wrong release does to a session: a successful response, or the refusal as a
|
|
1684
|
+
* typed error ({@link errorFrom}).
|
|
1685
|
+
*
|
|
1686
|
+
* - `401 UNAUTHENTICATED` to a request that carried a session: the node that
|
|
1687
|
+
* signed the session may be gone, replaced, or out of reach of the node
|
|
1688
|
+
* asked, which any node of the cluster answers with a refresh. The client
|
|
1689
|
+
* refreshes once, shared by every request that needs it at the same time
|
|
1690
|
+
* ({@link refresh}), and repeats the request once. A refresh that is
|
|
1691
|
+
* refused surfaces as {@link LoginRequired}.
|
|
1692
|
+
* - `421 WRONG_NODE`: this node's release may not serve the account. The
|
|
1693
|
+
* client moves to the next node it knows and repeats the request once; with
|
|
1694
|
+
* none, the {@link WrongNode} reaches the caller.
|
|
1695
|
+
*
|
|
1696
|
+
* Repeating is safe even for a request that is not idempotent: the node
|
|
1697
|
+
* answers `401` and `421` when it judges the credential, before it reads what
|
|
1698
|
+
* the request asks, so the first attempt ran nothing. The exceptions are the
|
|
1699
|
+
* ceremonies that mint a session ({@link ceremony}), never repeated here.
|
|
1700
|
+
*/
|
|
1701
|
+
async send(method, path, body, raw, contentType, signal) {
|
|
1702
|
+
let refreshed = false;
|
|
1703
|
+
let moved = false;
|
|
1704
|
+
for (; ; ) {
|
|
1705
|
+
const [token, at] = [this.auth, this.at];
|
|
1706
|
+
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
1707
|
+
if (response.ok) return response;
|
|
1708
|
+
const error = await refusal(response);
|
|
1709
|
+
if (error instanceof WrongNode && !moved) {
|
|
1710
|
+
moved = true;
|
|
1711
|
+
if (this.moveOn(at) && !ceremony(path)) continue;
|
|
1712
|
+
} else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
|
|
1713
|
+
refreshed = true;
|
|
1714
|
+
await this.renew(token);
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
throw error;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
/** Whether a `401` to a request sent with `token` is one a refresh may answer. */
|
|
1721
|
+
mayRefresh(token, path) {
|
|
1722
|
+
return token !== void 0 && this.current !== void 0 && !ceremony(path);
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* A fresh session in place of the one `token` belonged to: by the refresh
|
|
1726
|
+
* another request already made, or by one this request starts.
|
|
1727
|
+
*/
|
|
1728
|
+
async renew(token) {
|
|
1729
|
+
if (this.auth !== token) return;
|
|
1730
|
+
await this.refresh();
|
|
1731
|
+
}
|
|
1637
1732
|
async json(method, path, body, raw, contentType) {
|
|
1638
|
-
const response = await this.
|
|
1639
|
-
if (!response.ok) throw await refusal(response);
|
|
1733
|
+
const response = await this.send(method, path, body, raw, contentType);
|
|
1640
1734
|
const text = await response.text();
|
|
1641
1735
|
return text ? JSON.parse(text) : {};
|
|
1642
1736
|
}
|
|
@@ -1705,11 +1799,35 @@ var CalimeroClient = class {
|
|
|
1705
1799
|
this.useSession(session);
|
|
1706
1800
|
return session;
|
|
1707
1801
|
}
|
|
1708
|
-
/**
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1802
|
+
/**
|
|
1803
|
+
* Swap the refresh token for a new session, on whichever node the client
|
|
1804
|
+
* uses: any node of the cluster takes it. A refresh token is used once, so
|
|
1805
|
+
* calls made while one refresh is under way share it rather than sending the
|
|
1806
|
+
* same token again (which would revoke the session).
|
|
1807
|
+
*/
|
|
1808
|
+
refresh() {
|
|
1809
|
+
if (!this.refreshing) {
|
|
1810
|
+
const pending = this.exchangeRefresh();
|
|
1811
|
+
this.refreshing = pending;
|
|
1812
|
+
const clear = () => {
|
|
1813
|
+
if (this.refreshing === pending) this.refreshing = void 0;
|
|
1814
|
+
};
|
|
1815
|
+
pending.then(clear, clear);
|
|
1816
|
+
}
|
|
1817
|
+
return this.refreshing;
|
|
1818
|
+
}
|
|
1819
|
+
async exchangeRefresh() {
|
|
1820
|
+
const current = this.current;
|
|
1821
|
+
if (!current) throw new Error("no session to refresh");
|
|
1822
|
+
let session;
|
|
1823
|
+
for (let tried = 1; session === void 0; tried++) {
|
|
1824
|
+
try {
|
|
1825
|
+
session = await this.post("auth/refresh", { refreshToken: current.refreshToken });
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
if (this.current === current) this.useSession(session);
|
|
1713
1831
|
return session;
|
|
1714
1832
|
}
|
|
1715
1833
|
async signOut() {
|
|
@@ -1754,7 +1872,7 @@ var CalimeroClient = class {
|
|
|
1754
1872
|
*/
|
|
1755
1873
|
async attestation() {
|
|
1756
1874
|
const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
1757
|
-
const response = await this.raw(`${this.
|
|
1875
|
+
const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
|
|
1758
1876
|
if (!response.ok) throw await refusal(response);
|
|
1759
1877
|
return await response.json();
|
|
1760
1878
|
}
|
|
@@ -1782,8 +1900,7 @@ var CalimeroClient = class {
|
|
|
1782
1900
|
}
|
|
1783
1901
|
/** One of `app`'s blobs, by id (base58 or hex). */
|
|
1784
1902
|
async downloadBlob(app, id) {
|
|
1785
|
-
const response = await this.
|
|
1786
|
-
if (!response.ok) throw await refusal(response);
|
|
1903
|
+
const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
|
|
1787
1904
|
return new Uint8Array(await response.arrayBuffer());
|
|
1788
1905
|
}
|
|
1789
1906
|
/** `app`'s blobs. */
|
|
@@ -1952,10 +2069,7 @@ var CalimeroClient = class {
|
|
|
1952
2069
|
}
|
|
1953
2070
|
/** A server-sent event stream on `path`, sealed like every other answer. */
|
|
1954
2071
|
async *stream(path, signal) {
|
|
1955
|
-
const
|
|
1956
|
-
if (this.auth) headers.authorization = `Bearer ${this.auth}`;
|
|
1957
|
-
const response = await this.sealed(`${this.base}/${path}`, { headers, signal });
|
|
1958
|
-
if (!response.ok) throw await refusal(response);
|
|
2072
|
+
const response = await this.send("GET", path, void 0, void 0, void 0, signal);
|
|
1959
2073
|
if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
|
|
1960
2074
|
yield* sseFrames(response.body);
|
|
1961
2075
|
}
|
|
@@ -2747,6 +2861,7 @@ export {
|
|
|
2747
2861
|
StaleTransportKeyError,
|
|
2748
2862
|
TrapError,
|
|
2749
2863
|
UnavailableError,
|
|
2864
|
+
WrongNode,
|
|
2750
2865
|
agentKeyBinding,
|
|
2751
2866
|
agentQuoteVerifier,
|
|
2752
2867
|
attestAgent,
|