@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.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":
|
|
@@ -1455,6 +1463,17 @@ ${generation}`;
|
|
|
1455
1463
|
function publisherBindingBytes(cluster, org, signerId, generation) {
|
|
1456
1464
|
return new TextEncoder().encode(publisherBindingMessage(cluster, org, signerId, generation));
|
|
1457
1465
|
}
|
|
1466
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1467
|
+
async function confirmationCode(requestId) {
|
|
1468
|
+
const h = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(requestId)));
|
|
1469
|
+
const word = (h[0] << 24 | h[1] << 16 | h[2] << 8 | h[3]) >>> 0;
|
|
1470
|
+
let code = "";
|
|
1471
|
+
for (let i = 0; i < 6; i++) {
|
|
1472
|
+
if (i === 3) code += "-";
|
|
1473
|
+
code += CROCKFORD[word >>> 27 - 5 * i & 31];
|
|
1474
|
+
}
|
|
1475
|
+
return code;
|
|
1476
|
+
}
|
|
1458
1477
|
function consentReturn(href) {
|
|
1459
1478
|
const url2 = typeof href === "string" ? new URL(href) : href;
|
|
1460
1479
|
const requestId = url2.searchParams.get("request_id");
|
|
@@ -1583,11 +1602,20 @@ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
|
|
|
1583
1602
|
function retryDelayMs(retry, options) {
|
|
1584
1603
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
1585
1604
|
}
|
|
1605
|
+
function ceremony(path) {
|
|
1606
|
+
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1607
|
+
}
|
|
1586
1608
|
var CalimeroClient = class {
|
|
1587
1609
|
constructor(options) {
|
|
1610
|
+
/** The one in use: an index into {@link nodeUrls}. */
|
|
1611
|
+
this.at = 0;
|
|
1612
|
+
/** A sealed fetch per node, each attesting its own node on first use. */
|
|
1613
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
1588
1614
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
1589
1615
|
this.networkFailures = /* @__PURE__ */ new WeakSet();
|
|
1590
|
-
|
|
1616
|
+
const trim = (url2) => url2.replace(/\/+$/, "");
|
|
1617
|
+
this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
|
|
1618
|
+
this.policy = options.policy;
|
|
1591
1619
|
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
1592
1620
|
const given = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1593
1621
|
const raw = async (input, init) => {
|
|
@@ -1599,11 +1627,33 @@ var CalimeroClient = class {
|
|
|
1599
1627
|
}
|
|
1600
1628
|
};
|
|
1601
1629
|
this.raw = raw;
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1630
|
+
}
|
|
1631
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` moved the client on. */
|
|
1632
|
+
get nodeUrl() {
|
|
1633
|
+
return this.nodeUrls[this.at];
|
|
1634
|
+
}
|
|
1635
|
+
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
1636
|
+
channel(url2) {
|
|
1637
|
+
let sealed = this.channels.get(url2);
|
|
1638
|
+
if (!sealed) {
|
|
1639
|
+
sealed = createSealedFetch({
|
|
1640
|
+
baseUrl: url2,
|
|
1641
|
+
transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
|
|
1642
|
+
fetch: this.raw
|
|
1643
|
+
});
|
|
1644
|
+
this.channels.set(url2, sealed);
|
|
1645
|
+
}
|
|
1646
|
+
return sealed;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
|
|
1650
|
+
* next one. False when there is no other. A request that saw the same node
|
|
1651
|
+
* refuse after another request moved on already does not move again.
|
|
1652
|
+
*/
|
|
1653
|
+
moveOn(from) {
|
|
1654
|
+
if (this.nodeUrls.length < 2) return false;
|
|
1655
|
+
if (this.at === from) this.at = (from + 1) % this.nodeUrls.length;
|
|
1656
|
+
return true;
|
|
1607
1657
|
}
|
|
1608
1658
|
/** Authenticate every request with an API key. */
|
|
1609
1659
|
useApiKey(key) {
|
|
@@ -1617,7 +1667,11 @@ var CalimeroClient = class {
|
|
|
1617
1667
|
get session() {
|
|
1618
1668
|
return this.current;
|
|
1619
1669
|
}
|
|
1620
|
-
|
|
1670
|
+
/**
|
|
1671
|
+
* Send one request to the node in use, with the credential held now, and
|
|
1672
|
+
* answer the response, whatever its status. {@link send} judges it.
|
|
1673
|
+
*/
|
|
1674
|
+
async request(method, path, body, raw, contentType = "application/wasm", signal) {
|
|
1621
1675
|
const headers = {};
|
|
1622
1676
|
if (this.auth) headers.authorization = `Bearer ${this.auth}`;
|
|
1623
1677
|
let payload;
|
|
@@ -1628,15 +1682,66 @@ var CalimeroClient = class {
|
|
|
1628
1682
|
headers["content-type"] = "application/json";
|
|
1629
1683
|
payload = JSON.stringify(body);
|
|
1630
1684
|
}
|
|
1685
|
+
const url2 = this.nodeUrl;
|
|
1631
1686
|
try {
|
|
1632
|
-
return await this.
|
|
1687
|
+
return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
|
|
1633
1688
|
} catch (error) {
|
|
1634
1689
|
throw sealedError(error);
|
|
1635
1690
|
}
|
|
1636
1691
|
}
|
|
1692
|
+
/**
|
|
1693
|
+
* {@link request}, answering what a lost issuing node or a node on the
|
|
1694
|
+
* wrong release does to a session: a successful response, or the refusal as a
|
|
1695
|
+
* typed error ({@link errorFrom}).
|
|
1696
|
+
*
|
|
1697
|
+
* - `401 UNAUTHENTICATED` to a request that carried a session: the node that
|
|
1698
|
+
* signed the session may be gone, replaced, or out of reach of the node
|
|
1699
|
+
* asked, which any node of the cluster answers with a refresh. The client
|
|
1700
|
+
* refreshes once, shared by every request that needs it at the same time
|
|
1701
|
+
* ({@link refresh}), and repeats the request once. A refresh that is
|
|
1702
|
+
* refused surfaces as {@link LoginRequired}.
|
|
1703
|
+
* - `421 WRONG_NODE`: this node's release may not serve the account. The
|
|
1704
|
+
* client moves to the next node it knows and repeats the request once; with
|
|
1705
|
+
* none, the {@link WrongNode} reaches the caller.
|
|
1706
|
+
*
|
|
1707
|
+
* Repeating is safe even for a request that is not idempotent: the node
|
|
1708
|
+
* answers `401` and `421` when it judges the credential, before it reads what
|
|
1709
|
+
* the request asks, so the first attempt ran nothing. The exceptions are the
|
|
1710
|
+
* ceremonies that mint a session ({@link ceremony}), never repeated here.
|
|
1711
|
+
*/
|
|
1712
|
+
async send(method, path, body, raw, contentType, signal) {
|
|
1713
|
+
let refreshed = false;
|
|
1714
|
+
let moved = false;
|
|
1715
|
+
for (; ; ) {
|
|
1716
|
+
const [token, at] = [this.auth, this.at];
|
|
1717
|
+
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
1718
|
+
if (response.ok) return response;
|
|
1719
|
+
const error = await refusal(response);
|
|
1720
|
+
if (error instanceof WrongNode && !moved) {
|
|
1721
|
+
moved = true;
|
|
1722
|
+
if (this.moveOn(at) && !ceremony(path)) continue;
|
|
1723
|
+
} else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
|
|
1724
|
+
refreshed = true;
|
|
1725
|
+
await this.renew(token);
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
throw error;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/** Whether a `401` to a request sent with `token` is one a refresh may answer. */
|
|
1732
|
+
mayRefresh(token, path) {
|
|
1733
|
+
return token !== void 0 && this.current !== void 0 && !ceremony(path);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* A fresh session in place of the one `token` belonged to: by the refresh
|
|
1737
|
+
* another request already made, or by one this request starts.
|
|
1738
|
+
*/
|
|
1739
|
+
async renew(token) {
|
|
1740
|
+
if (this.auth !== token) return;
|
|
1741
|
+
await this.refresh();
|
|
1742
|
+
}
|
|
1637
1743
|
async json(method, path, body, raw, contentType) {
|
|
1638
|
-
const response = await this.
|
|
1639
|
-
if (!response.ok) throw await refusal(response);
|
|
1744
|
+
const response = await this.send(method, path, body, raw, contentType);
|
|
1640
1745
|
const text = await response.text();
|
|
1641
1746
|
return text ? JSON.parse(text) : {};
|
|
1642
1747
|
}
|
|
@@ -1705,11 +1810,35 @@ var CalimeroClient = class {
|
|
|
1705
1810
|
this.useSession(session);
|
|
1706
1811
|
return session;
|
|
1707
1812
|
}
|
|
1708
|
-
/**
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1813
|
+
/**
|
|
1814
|
+
* Swap the refresh token for a new session, on whichever node the client
|
|
1815
|
+
* uses: any node of the cluster takes it. A refresh token is used once, so
|
|
1816
|
+
* calls made while one refresh is under way share it rather than sending the
|
|
1817
|
+
* same token again (which would revoke the session).
|
|
1818
|
+
*/
|
|
1819
|
+
refresh() {
|
|
1820
|
+
if (!this.refreshing) {
|
|
1821
|
+
const pending = this.exchangeRefresh();
|
|
1822
|
+
this.refreshing = pending;
|
|
1823
|
+
const clear = () => {
|
|
1824
|
+
if (this.refreshing === pending) this.refreshing = void 0;
|
|
1825
|
+
};
|
|
1826
|
+
pending.then(clear, clear);
|
|
1827
|
+
}
|
|
1828
|
+
return this.refreshing;
|
|
1829
|
+
}
|
|
1830
|
+
async exchangeRefresh() {
|
|
1831
|
+
const current = this.current;
|
|
1832
|
+
if (!current) throw new Error("no session to refresh");
|
|
1833
|
+
let session;
|
|
1834
|
+
for (let tried = 1; session === void 0; tried++) {
|
|
1835
|
+
try {
|
|
1836
|
+
session = await this.post("auth/refresh", { refreshToken: current.refreshToken });
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (this.current === current) this.useSession(session);
|
|
1713
1842
|
return session;
|
|
1714
1843
|
}
|
|
1715
1844
|
async signOut() {
|
|
@@ -1754,7 +1883,7 @@ var CalimeroClient = class {
|
|
|
1754
1883
|
*/
|
|
1755
1884
|
async attestation() {
|
|
1756
1885
|
const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
1757
|
-
const response = await this.raw(`${this.
|
|
1886
|
+
const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
|
|
1758
1887
|
if (!response.ok) throw await refusal(response);
|
|
1759
1888
|
return await response.json();
|
|
1760
1889
|
}
|
|
@@ -1782,8 +1911,7 @@ var CalimeroClient = class {
|
|
|
1782
1911
|
}
|
|
1783
1912
|
/** One of `app`'s blobs, by id (base58 or hex). */
|
|
1784
1913
|
async downloadBlob(app, id) {
|
|
1785
|
-
const response = await this.
|
|
1786
|
-
if (!response.ok) throw await refusal(response);
|
|
1914
|
+
const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
|
|
1787
1915
|
return new Uint8Array(await response.arrayBuffer());
|
|
1788
1916
|
}
|
|
1789
1917
|
/** `app`'s blobs. */
|
|
@@ -1952,10 +2080,7 @@ var CalimeroClient = class {
|
|
|
1952
2080
|
}
|
|
1953
2081
|
/** A server-sent event stream on `path`, sealed like every other answer. */
|
|
1954
2082
|
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);
|
|
2083
|
+
const response = await this.send("GET", path, void 0, void 0, void 0, signal);
|
|
1959
2084
|
if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
|
|
1960
2085
|
yield* sseFrames(response.body);
|
|
1961
2086
|
}
|
|
@@ -2122,16 +2247,34 @@ var CalimeroClient = class {
|
|
|
2122
2247
|
}
|
|
2123
2248
|
/**
|
|
2124
2249
|
* {@link requestConsent}, then send the browser to the consent page
|
|
2125
|
-
* (`navigate`, by default `window.location.assign`).
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2250
|
+
* (`navigate`, by default `window.location.assign`). Pass `navigate` itself
|
|
2251
|
+
* or {@link OpenConsentOptions}, whose `confirm` runs first with the node's
|
|
2252
|
+
* answer so the app can show the person its `code`; when it answers `false`
|
|
2253
|
+
* the page is not opened. When the node sends a `code` that is not
|
|
2254
|
+
* {@link confirmationCode} of `requestId`, it throws `CalimeroError`
|
|
2255
|
+
* (`CONSENT_CODE_MISMATCH`) and does not navigate. Answers the request
|
|
2256
|
+
* once `navigate` returns (or `confirm` declines): with a popup
|
|
2257
|
+
* (`url => window.open(url)`), poll it with {@link waitForConsent}.
|
|
2128
2258
|
*/
|
|
2129
|
-
async openConsentPage(request,
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2259
|
+
async openConsentPage(request, navigateOrOptions = {}) {
|
|
2260
|
+
const options = typeof navigateOrOptions === "function" ? { navigate: navigateOrOptions } : navigateOrOptions;
|
|
2261
|
+
const navigate = options.navigate ?? ((url2) => {
|
|
2262
|
+
const w = globalThis.window;
|
|
2263
|
+
if (!w) throw new Error("no window to open the consent page in; pass `navigate`");
|
|
2264
|
+
w.location.assign(url2);
|
|
2265
|
+
});
|
|
2134
2266
|
const started = await this.requestConsent(request);
|
|
2267
|
+
if (started.code !== void 0) {
|
|
2268
|
+
const expected = await confirmationCode(started.requestId);
|
|
2269
|
+
if (started.code !== expected) {
|
|
2270
|
+
throw new CalimeroError(200, "CONSENT_CODE_MISMATCH", "the consent page code does not match the request", {
|
|
2271
|
+
requestId: started.requestId,
|
|
2272
|
+
code: started.code,
|
|
2273
|
+
expected
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (options.confirm && !await options.confirm(started)) return started;
|
|
2135
2278
|
navigate(started.url);
|
|
2136
2279
|
return started;
|
|
2137
2280
|
}
|
|
@@ -2747,6 +2890,7 @@ export {
|
|
|
2747
2890
|
StaleTransportKeyError,
|
|
2748
2891
|
TrapError,
|
|
2749
2892
|
UnavailableError,
|
|
2893
|
+
WrongNode,
|
|
2750
2894
|
agentKeyBinding,
|
|
2751
2895
|
agentQuoteVerifier,
|
|
2752
2896
|
attestAgent,
|
|
@@ -2756,6 +2900,7 @@ export {
|
|
|
2756
2900
|
claimedRelease,
|
|
2757
2901
|
cloudNodeReleaseUrl,
|
|
2758
2902
|
compareVersions,
|
|
2903
|
+
confirmationCode,
|
|
2759
2904
|
consentReturn,
|
|
2760
2905
|
createQuoteVerifier,
|
|
2761
2906
|
createSealedFetch,
|