@calimero-network/mero-js 27.3.0 → 27.5.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 +33 -4
- package/dist/cloud-v2/attestation.d.ts +20 -3
- package/dist/cloud-v2/attestation.js +33 -3
- package/dist/cloud-v2/client.d.ts +61 -8
- package/dist/cloud-v2/client.js +113 -22
- package/dist/cloud-v2/directory.d.ts +50 -0
- package/dist/cloud-v2/directory.js +115 -0
- package/dist/cloud-v2/index.d.ts +4 -2
- package/dist/cloud-v2/index.js +2 -1
- package/dist/index.browser.mjs +4 -4
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +215 -20
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +215 -20
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +1 -1
- package/dist/sealed/index.js +1 -1
- package/dist/sealed/release.d.ts +6 -0
- package/dist/sealed/release.js +6 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1000,6 +1000,7 @@ var NODE_RELEASE_SIGNER = Object.freeze({
|
|
|
1000
1000
|
});
|
|
1001
1001
|
var MEASUREMENTS = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];
|
|
1002
1002
|
var DEFAULT_RELEASE_MIRROR = "https://cloud.calimero.network";
|
|
1003
|
+
var DEFAULT_COLLATERAL_MIRROR = `${DEFAULT_RELEASE_MIRROR}/api/tee/pcs`;
|
|
1003
1004
|
async function verifySignedNodeRelease(publishedMrtds, bundle, options = {}) {
|
|
1004
1005
|
const body = typeof publishedMrtds === "string" ? new TextEncoder().encode(publishedMrtds) : publishedMrtds;
|
|
1005
1006
|
await verifyCosignBundle(
|
|
@@ -1164,6 +1165,34 @@ async function releaseFor(version, policy, fetchImpl) {
|
|
|
1164
1165
|
if (source === "github") return fetchGithubNodeRelease(version, { fetch: fetchImpl });
|
|
1165
1166
|
return fetchNodeRelease(cloudNodeReleaseUrl(source ?? DEFAULT_RELEASE_MIRROR, version), { fetch: fetchImpl });
|
|
1166
1167
|
}
|
|
1168
|
+
function defaultCollateral(policy) {
|
|
1169
|
+
const source = policy.collateralSource ?? DEFAULT_COLLATERAL_MIRROR;
|
|
1170
|
+
if (source === "intel") {
|
|
1171
|
+
return async (quote) => (await dcap("getCollateralFromPcs"))(quote);
|
|
1172
|
+
}
|
|
1173
|
+
return async (quote) => (await dcap("getCollateral"))(source, quote);
|
|
1174
|
+
}
|
|
1175
|
+
function freshCollateral(fetch2, policy) {
|
|
1176
|
+
const maxDays = policy.maxCollateralAgeDays;
|
|
1177
|
+
if (maxDays === void 0) return fetch2;
|
|
1178
|
+
return async (quote) => {
|
|
1179
|
+
const collateral = await fetch2(quote);
|
|
1180
|
+
let issued;
|
|
1181
|
+
try {
|
|
1182
|
+
issued = Date.parse(JSON.parse(collateral.tcb_info).issueDate ?? "");
|
|
1183
|
+
} catch {
|
|
1184
|
+
issued = Number.NaN;
|
|
1185
|
+
}
|
|
1186
|
+
if (!Number.isFinite(issued)) throw new Error("The quote's collateral names no TCB info issue date");
|
|
1187
|
+
const ageDays = ((policy.now ?? Date.now)() - issued) / 864e5;
|
|
1188
|
+
if (ageDays > maxDays) {
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
`The quote's collateral was issued ${Math.floor(ageDays)} days ago, more than the ${maxDays} this client accepts`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
return collateral;
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1167
1196
|
async function verifyRelease(doc, args, policy, fetchImpl) {
|
|
1168
1197
|
const version = claimedRelease(doc, policy.minReleaseVersion);
|
|
1169
1198
|
const trusted = await trustSignedRelease({
|
|
@@ -1174,7 +1203,7 @@ async function verifyRelease(doc, args, policy, fetchImpl) {
|
|
|
1174
1203
|
identity: policy.identity,
|
|
1175
1204
|
issuer: policy.issuer
|
|
1176
1205
|
});
|
|
1177
|
-
const fetchCollateral = policy.fetchCollateral ?? (
|
|
1206
|
+
const fetchCollateral = freshCollateral(policy.fetchCollateral ?? defaultCollateral(policy), policy);
|
|
1178
1207
|
const verifier = createQuoteVerifier({
|
|
1179
1208
|
dcapVerify: policy.dcapVerify ?? await dcap("verify"),
|
|
1180
1209
|
fetchCollateral,
|
|
@@ -1219,6 +1248,85 @@ async function fetchAttestedKey(baseUrl, policy, fetchImpl = (input, init) => fe
|
|
|
1219
1248
|
return transportKey;
|
|
1220
1249
|
}
|
|
1221
1250
|
|
|
1251
|
+
// src/cloud-v2/directory.ts
|
|
1252
|
+
var DEFAULT_DIRECTORY = "https://manager.cloud.calimero.network/api/v2/directory";
|
|
1253
|
+
var DEFAULT_REFRESH_SECONDS = 60;
|
|
1254
|
+
var DirectoryError = class extends Error {
|
|
1255
|
+
constructor(cluster, reason, message, status, cause) {
|
|
1256
|
+
super(`directory for cluster ${cluster}: ${message}`);
|
|
1257
|
+
this.cluster = cluster;
|
|
1258
|
+
this.reason = reason;
|
|
1259
|
+
this.status = status;
|
|
1260
|
+
this.cause = cause;
|
|
1261
|
+
this.name = "DirectoryError";
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
function directoryUrl(directory, cluster) {
|
|
1265
|
+
return `${directory.replace(/\/+$/, "")}/${encodeURIComponent(cluster)}`;
|
|
1266
|
+
}
|
|
1267
|
+
async function readDirectory(directory, cluster, fetcher) {
|
|
1268
|
+
let response;
|
|
1269
|
+
try {
|
|
1270
|
+
response = await fetcher(directoryUrl(directory, cluster), { method: "GET", headers: { accept: "application/json" } });
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
throw new DirectoryError(cluster, "unreachable", `could not be reached: ${String(error)}`, void 0, error);
|
|
1273
|
+
}
|
|
1274
|
+
if (response.status === 404) throw new DirectoryError(cluster, "not-found", "unknown cluster", 404);
|
|
1275
|
+
if (!response.ok) throw new DirectoryError(cluster, "unreachable", `answered ${response.status}`, response.status);
|
|
1276
|
+
let body;
|
|
1277
|
+
try {
|
|
1278
|
+
body = await response.json();
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
throw new DirectoryError(cluster, "bad-response", "answered something other than JSON", response.status, error);
|
|
1281
|
+
}
|
|
1282
|
+
const listing = body;
|
|
1283
|
+
if (!listing || typeof listing !== "object" || !Array.isArray(listing.nodes)) {
|
|
1284
|
+
throw new DirectoryError(cluster, "bad-response", "answered no node list", response.status);
|
|
1285
|
+
}
|
|
1286
|
+
const nodes = [];
|
|
1287
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const entry of listing.nodes) {
|
|
1289
|
+
const node = usable(entry);
|
|
1290
|
+
if (node && !seen.has(node.url)) {
|
|
1291
|
+
seen.add(node.url);
|
|
1292
|
+
nodes.push(node);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (nodes.length === 0) throw new DirectoryError(cluster, "empty", "lists no usable node", response.status);
|
|
1296
|
+
const after = listing.refreshAfterSeconds;
|
|
1297
|
+
const refreshAfterSeconds = typeof after === "number" && Number.isFinite(after) && after >= 1 ? after : DEFAULT_REFRESH_SECONDS;
|
|
1298
|
+
return { nodes, refreshAfterSeconds };
|
|
1299
|
+
}
|
|
1300
|
+
function usable(entry) {
|
|
1301
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
1302
|
+
const { url: url2, zone, weight } = entry;
|
|
1303
|
+
if (typeof url2 !== "string" || typeof weight !== "number" || !Number.isInteger(weight) || weight < 1) return void 0;
|
|
1304
|
+
let parsed;
|
|
1305
|
+
try {
|
|
1306
|
+
parsed = new URL(url2);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
|
|
1311
|
+
return { url: url2.replace(/\/+$/, ""), zone: typeof zone === "string" ? zone : void 0, weight };
|
|
1312
|
+
}
|
|
1313
|
+
function orderNodes(nodes, zone, random) {
|
|
1314
|
+
const near = zone === void 0 ? [] : nodes.filter((node) => node.zone === zone);
|
|
1315
|
+
const far = zone === void 0 ? nodes : nodes.filter((node) => node.zone !== zone);
|
|
1316
|
+
return [...weightedOrder(near, random), ...weightedOrder(far, random)];
|
|
1317
|
+
}
|
|
1318
|
+
function weightedOrder(nodes, random) {
|
|
1319
|
+
const left = [...nodes];
|
|
1320
|
+
const order = [];
|
|
1321
|
+
while (left.length > 0) {
|
|
1322
|
+
let roll = random() * left.reduce((sum, node) => sum + node.weight, 0);
|
|
1323
|
+
let pick = 0;
|
|
1324
|
+
while (pick < left.length - 1 && roll >= left[pick].weight) roll -= left[pick++].weight;
|
|
1325
|
+
order.push(left.splice(pick, 1)[0].url);
|
|
1326
|
+
}
|
|
1327
|
+
return order;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1222
1330
|
// src/cloud-v2/errors.ts
|
|
1223
1331
|
var CalimeroError = class extends Error {
|
|
1224
1332
|
constructor(status, code, message, details = {}) {
|
|
@@ -1605,7 +1713,7 @@ function retryDelayMs(retry, options) {
|
|
|
1605
1713
|
function ceremony(path) {
|
|
1606
1714
|
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1607
1715
|
}
|
|
1608
|
-
var CalimeroClient = class {
|
|
1716
|
+
var CalimeroClient = class _CalimeroClient {
|
|
1609
1717
|
constructor(options) {
|
|
1610
1718
|
/** The one in use: an index into {@link nodeUrls}. */
|
|
1611
1719
|
this.at = 0;
|
|
@@ -1628,10 +1736,40 @@ var CalimeroClient = class {
|
|
|
1628
1736
|
};
|
|
1629
1737
|
this.raw = raw;
|
|
1630
1738
|
}
|
|
1631
|
-
/**
|
|
1739
|
+
/**
|
|
1740
|
+
* A client for the Calimero Cloud cluster named `cluster`: its node list read
|
|
1741
|
+
* from the public node directory, those in `zone` first, each group in
|
|
1742
|
+
* weighted-random order. The directory only picks which nodes to try: each is
|
|
1743
|
+
* attested under `policy` before anything is sent to it. Throws
|
|
1744
|
+
* {@link DirectoryError} when the directory names no usable node.
|
|
1745
|
+
*
|
|
1746
|
+
* The list is read again in the background after a failover, and at the
|
|
1747
|
+
* first request once the directory's `refreshAfterSeconds` has passed.
|
|
1748
|
+
*/
|
|
1749
|
+
static async connect(options) {
|
|
1750
|
+
const directory = options.directory ?? DEFAULT_DIRECTORY;
|
|
1751
|
+
const random = options.random ?? Math.random;
|
|
1752
|
+
const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1753
|
+
const listing = await readDirectory(directory, options.cluster, fetcher);
|
|
1754
|
+
const [baseUrl, ...nodes] = orderNodes(listing.nodes, options.zone, random);
|
|
1755
|
+
const client = new _CalimeroClient({ baseUrl, nodes, policy: options.policy, fetch: options.fetch, retry: options.retry });
|
|
1756
|
+
client.source = {
|
|
1757
|
+
directory,
|
|
1758
|
+
cluster: options.cluster,
|
|
1759
|
+
zone: options.zone,
|
|
1760
|
+
random,
|
|
1761
|
+
due: Date.now() + listing.refreshAfterSeconds * 1e3
|
|
1762
|
+
};
|
|
1763
|
+
return client;
|
|
1764
|
+
}
|
|
1765
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved the client on. */
|
|
1632
1766
|
get nodeUrl() {
|
|
1633
1767
|
return this.nodeUrls[this.at];
|
|
1634
1768
|
}
|
|
1769
|
+
/** Every node this client may use, in the order it moves through them. */
|
|
1770
|
+
get nodes() {
|
|
1771
|
+
return [...this.nodeUrls];
|
|
1772
|
+
}
|
|
1635
1773
|
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
1636
1774
|
channel(url2) {
|
|
1637
1775
|
let sealed = this.channels.get(url2);
|
|
@@ -1652,9 +1790,46 @@ var CalimeroClient = class {
|
|
|
1652
1790
|
*/
|
|
1653
1791
|
moveOn(from) {
|
|
1654
1792
|
if (this.nodeUrls.length < 2) return false;
|
|
1655
|
-
if (this.
|
|
1793
|
+
if (this.nodeUrl === from) this.at = (this.at + 1) % this.nodeUrls.length;
|
|
1656
1794
|
return true;
|
|
1657
1795
|
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Fail over from node `from`, where a request's retries ran out, to the next
|
|
1798
|
+
* node it has not `tried`. False when it has tried them all. A request whose
|
|
1799
|
+
* node another request moved away from already goes to that node, if untried.
|
|
1800
|
+
*/
|
|
1801
|
+
failOver(from, tried) {
|
|
1802
|
+
if (this.nodeUrl !== from && !tried.has(this.nodeUrl)) return true;
|
|
1803
|
+
const count = this.nodeUrls.length;
|
|
1804
|
+
for (let step = 1; step < count; step++) {
|
|
1805
|
+
const next = (this.at + step) % count;
|
|
1806
|
+
if (!tried.has(this.nodeUrls[next])) {
|
|
1807
|
+
this.at = next;
|
|
1808
|
+
this.reread();
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return false;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Read the node list again from the directory, in the background, for a
|
|
1816
|
+
* client made by {@link connect}: the node in use stays first, then the
|
|
1817
|
+
* directory's order; a node no longer listed is dropped. A failed read is
|
|
1818
|
+
* ignored. One read at a time.
|
|
1819
|
+
*/
|
|
1820
|
+
reread() {
|
|
1821
|
+
const source = this.source;
|
|
1822
|
+
if (!source || source.reading) return;
|
|
1823
|
+
source.reading = readDirectory(source.directory, source.cluster, this.raw).then((listing) => {
|
|
1824
|
+
const current = this.nodeUrl;
|
|
1825
|
+
const listed = orderNodes(listing.nodes, source.zone, source.random);
|
|
1826
|
+
this.nodeUrls = [current, ...listed.filter((url2) => url2 !== current)];
|
|
1827
|
+
this.at = 0;
|
|
1828
|
+
source.due = Date.now() + listing.refreshAfterSeconds * 1e3;
|
|
1829
|
+
}).catch(() => void 0).finally(() => {
|
|
1830
|
+
source.reading = void 0;
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1658
1833
|
/** Authenticate every request with an API key. */
|
|
1659
1834
|
useApiKey(key) {
|
|
1660
1835
|
this.auth = key;
|
|
@@ -1682,6 +1857,7 @@ var CalimeroClient = class {
|
|
|
1682
1857
|
headers["content-type"] = "application/json";
|
|
1683
1858
|
payload = JSON.stringify(body);
|
|
1684
1859
|
}
|
|
1860
|
+
if (this.source && Date.now() >= this.source.due) this.reread();
|
|
1685
1861
|
const url2 = this.nodeUrl;
|
|
1686
1862
|
try {
|
|
1687
1863
|
return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
|
|
@@ -1713,7 +1889,7 @@ var CalimeroClient = class {
|
|
|
1713
1889
|
let refreshed = false;
|
|
1714
1890
|
let moved = false;
|
|
1715
1891
|
for (; ; ) {
|
|
1716
|
-
const [token, at] = [this.auth, this.
|
|
1892
|
+
const [token, at] = [this.auth, this.nodeUrl];
|
|
1717
1893
|
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
1718
1894
|
if (response.ok) return response;
|
|
1719
1895
|
const error = await refusal(response);
|
|
@@ -1749,25 +1925,40 @@ var CalimeroClient = class {
|
|
|
1749
1925
|
* {@link json} for a call or a view (D70): while the node answers 503 or
|
|
1750
1926
|
* cannot be reached, the same request, with the same idempotency key, is sent
|
|
1751
1927
|
* again after {@link retryDelayMs} (or the pause the node asked for, if
|
|
1752
|
-
* longer), at most `retries` times. Every other answer is final.
|
|
1928
|
+
* longer), at most `retries` times. Every other answer is final.
|
|
1929
|
+
*
|
|
1930
|
+
* When a node's retries run out, the request fails over to the next node the
|
|
1931
|
+
* client knows and its retries start again there, with the same key: the
|
|
1932
|
+
* key is kept in the app's replicated log, so the cluster answers it once
|
|
1933
|
+
* whichever node it reaches. Each node is tried once per request. The last
|
|
1753
1934
|
* failure surfaces as {@link UnavailableError}: the node's 503, or reason
|
|
1754
1935
|
* `network` for a connection that failed, whose `committed` is `'unknown'`.
|
|
1755
|
-
*
|
|
1936
|
+
*
|
|
1937
|
+
* `once` sends the request exactly once, to the node in use (the confirmed
|
|
1938
|
+
* attempt of {@link callConfirmed}, whose challenge is single-use).
|
|
1756
1939
|
*/
|
|
1757
|
-
async retried(method, path, body,
|
|
1758
|
-
|
|
1940
|
+
async retried(method, path, body, once = false) {
|
|
1941
|
+
const retries = once ? 0 : this.retry.retries;
|
|
1942
|
+
const tried = /* @__PURE__ */ new Set();
|
|
1943
|
+
for (; ; ) {
|
|
1759
1944
|
let unavailable;
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1945
|
+
let node = this.nodeUrl;
|
|
1946
|
+
for (let attempt = 0; ; attempt++) {
|
|
1947
|
+
node = this.nodeUrl;
|
|
1948
|
+
tried.add(node);
|
|
1949
|
+
try {
|
|
1950
|
+
return await this.json(method, path, body);
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
if (error instanceof UnavailableError) unavailable = error;
|
|
1953
|
+
else if (error instanceof TypeError && this.networkFailures.has(error)) {
|
|
1954
|
+
unavailable = new UnavailableError("NETWORK", `the node could not be reached: ${error.message}`, {}, void 0, error);
|
|
1955
|
+
} else throw error;
|
|
1956
|
+
}
|
|
1957
|
+
if (attempt >= retries) break;
|
|
1958
|
+
const backoff = retryDelayMs(attempt, this.retry);
|
|
1959
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
|
|
1767
1960
|
}
|
|
1768
|
-
if (
|
|
1769
|
-
const backoff = retryDelayMs(attempt, this.retry);
|
|
1770
|
-
await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
|
|
1961
|
+
if (once || !this.failOver(node, tried)) throw unavailable;
|
|
1771
1962
|
}
|
|
1772
1963
|
}
|
|
1773
1964
|
post(path, body = {}) {
|
|
@@ -2062,7 +2253,7 @@ var CalimeroClient = class {
|
|
|
2062
2253
|
path,
|
|
2063
2254
|
{ method, args, idempotencyKey: key },
|
|
2064
2255
|
passkey,
|
|
2065
|
-
(body) => this.retried("POST", path, body, body.confirm
|
|
2256
|
+
(body) => this.retried("POST", path, body, body.confirm !== void 0)
|
|
2066
2257
|
);
|
|
2067
2258
|
}
|
|
2068
2259
|
/** Run a read-only method: strong by default, or stale from any replica. */
|
|
@@ -2869,8 +3060,11 @@ export {
|
|
|
2869
3060
|
ConfirmCancelled,
|
|
2870
3061
|
ConfirmRequired,
|
|
2871
3062
|
ConsentStale,
|
|
3063
|
+
DEFAULT_COLLATERAL_MIRROR,
|
|
3064
|
+
DEFAULT_DIRECTORY,
|
|
2872
3065
|
DEFAULT_ORG_CAPABILITIES,
|
|
2873
3066
|
DEFAULT_RELEASE_MIRROR,
|
|
3067
|
+
DirectoryError,
|
|
2874
3068
|
GITHUB_RELEASES,
|
|
2875
3069
|
HANDSHAKE_PATH,
|
|
2876
3070
|
IdempotencyMismatch,
|
|
@@ -2909,6 +3103,7 @@ export {
|
|
|
2909
3103
|
fetchAttestedKey,
|
|
2910
3104
|
fetchGithubNodeRelease,
|
|
2911
3105
|
fetchNodeRelease,
|
|
3106
|
+
freshCollateral,
|
|
2912
3107
|
fromB64url,
|
|
2913
3108
|
generateOwnerKey,
|
|
2914
3109
|
orgCapabilities,
|