@calimero-network/mero-js 27.0.2 → 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/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  CalimeroError: () => CalimeroError,
39
39
  ConfirmCancelled: () => ConfirmCancelled,
40
40
  ConfirmRequired: () => ConfirmRequired,
41
+ ConsentStale: () => ConsentStale,
41
42
  DEFAULT_ORG_CAPABILITIES: () => DEFAULT_ORG_CAPABILITIES,
42
43
  DEFAULT_RELEASE_MIRROR: () => DEFAULT_RELEASE_MIRROR,
43
44
  GITHUB_RELEASES: () => GITHUB_RELEASES,
@@ -45,17 +46,21 @@ __export(index_exports, {
45
46
  IdempotencyMismatch: () => IdempotencyMismatch,
46
47
  KMS_RELEASE_SIGNER: () => KMS_RELEASE_SIGNER,
47
48
  LoginRequired: () => LoginRequired,
49
+ MAX_REPORT_PAGE: () => MAX_REPORT_PAGE,
48
50
  MOCK_QUOTE_HEADER: () => MOCK_QUOTE_HEADER,
49
51
  NODE_RELEASE_SIGNER: () => NODE_RELEASE_SIGNER,
50
52
  NotFound: () => NotFound,
51
53
  ORG_CAPABILITIES: () => ORG_CAPABILITIES,
52
54
  PRODUCTION_PROFILE: () => PRODUCTION_PROFILE,
55
+ RELEASE_VERIFIER: () => RELEASE_VERIFIER,
56
+ ReleaseDeadlineError: () => ReleaseDeadlineError,
53
57
  SEALED_CONTENT_TYPE: () => SEALED_CONTENT_TYPE,
54
58
  SEALED_PATH: () => SEALED_PATH,
55
59
  SealedTransportError: () => SealedTransportError,
56
60
  StaleTransportKeyError: () => StaleTransportKeyError,
57
61
  TrapError: () => TrapError,
58
62
  UnavailableError: () => UnavailableError,
63
+ WrongNode: () => WrongNode,
59
64
  agentKeyBinding: () => agentKeyBinding,
60
65
  agentQuoteVerifier: () => agentQuoteVerifier,
61
66
  attestAgent: () => attestAgent,
@@ -64,6 +69,8 @@ __export(index_exports, {
64
69
  checkOwner: () => checkOwner,
65
70
  claimedRelease: () => claimedRelease,
66
71
  cloudNodeReleaseUrl: () => cloudNodeReleaseUrl,
72
+ compareVersions: () => compareVersions,
73
+ consentReturn: () => consentReturn,
67
74
  createQuoteVerifier: () => createQuoteVerifier,
68
75
  createSealedFetch: () => createSealedFetch,
69
76
  createSignedReleaseVerifier: () => createSignedReleaseVerifier,
@@ -75,15 +82,25 @@ __export(index_exports, {
75
82
  generateOwnerKey: () => generateOwnerKey,
76
83
  orgCapabilities: () => orgCapabilities,
77
84
  ownerPublicKey: () => ownerPublicKey,
85
+ pageQuery: () => pageQuery,
86
+ pendingApprovalsFrom: () => pendingApprovalsFrom,
78
87
  provisionAgent: () => provisionAgent,
79
88
  provisionInfo: () => provisionInfo,
89
+ publisherBindingBytes: () => publisherBindingBytes,
90
+ publisherBindingMessage: () => publisherBindingMessage,
80
91
  releaseAgent: () => releaseAgent,
92
+ releaseConsent: () => releaseConsent,
93
+ releaseNotesUrl: () => releaseNotesUrl,
94
+ stalledBy: () => stalledBy,
81
95
  transportKeyBinding: () => transportKeyBinding,
82
96
  trustSignedRelease: () => trustSignedRelease,
83
97
  trustedMeasurementsFromReleases: () => trustedMeasurementsFromReleases,
98
+ upgradeConsent: () => upgradeConsent,
84
99
  validateAgentId: () => validateAgentId,
100
+ verifyReleaseUrl: () => verifyReleaseUrl,
85
101
  verifySignedAgentPolicy: () => verifySignedAgentPolicy,
86
- verifySignedNodeRelease: () => verifySignedNodeRelease
102
+ verifySignedNodeRelease: () => verifySignedNodeRelease,
103
+ withdrawConsent: () => withdrawConsent
87
104
  });
88
105
  module.exports = __toCommonJS(index_exports);
89
106
 
@@ -1324,6 +1341,12 @@ var LoginRequired = class extends CalimeroError {
1324
1341
  this.name = "LoginRequired";
1325
1342
  }
1326
1343
  };
1344
+ var WrongNode = class extends CalimeroError {
1345
+ constructor(message, details = {}) {
1346
+ super(421, "WRONG_NODE", message, details);
1347
+ this.name = "WrongNode";
1348
+ }
1349
+ };
1327
1350
  var NotFound = class extends CalimeroError {
1328
1351
  constructor(message, details = {}) {
1329
1352
  super(404, "NOT_FOUND", message, details);
@@ -1355,6 +1378,20 @@ var IdempotencyMismatch = class extends CalimeroError {
1355
1378
  this.name = "IdempotencyMismatch";
1356
1379
  }
1357
1380
  };
1381
+ var ReleaseDeadlineError = class extends CalimeroError {
1382
+ constructor(message, details = {}) {
1383
+ super(409, "RELEASE_DEADLINE", message, details);
1384
+ this.name = "ReleaseDeadlineError";
1385
+ this.release = typeof details.release === "string" ? details.release : "";
1386
+ this.after = typeof details.after === "string" ? details.after : "";
1387
+ }
1388
+ };
1389
+ var ConsentStale = class extends CalimeroError {
1390
+ constructor(message, details = {}) {
1391
+ super(409, "CONSENT_STALE", message, details);
1392
+ this.name = "ConsentStale";
1393
+ }
1394
+ };
1358
1395
  var AppError = class extends CalimeroError {
1359
1396
  constructor(message, details = {}) {
1360
1397
  super(422, "APP_ERROR", message, details);
@@ -1404,10 +1441,16 @@ function errorFrom(status, body, retryAfterHeader) {
1404
1441
  switch (`${status} ${code}`) {
1405
1442
  case "401 UNAUTHENTICATED":
1406
1443
  return new LoginRequired(message, details);
1444
+ case "421 WRONG_NODE":
1445
+ return new WrongNode(message, details);
1407
1446
  case "404 NOT_FOUND":
1408
1447
  return new NotFound(message, details);
1409
1448
  case "409 IDEMPOTENCY_MISMATCH":
1410
1449
  return new IdempotencyMismatch(message, details);
1450
+ case "409 RELEASE_DEADLINE":
1451
+ return new ReleaseDeadlineError(message, details);
1452
+ case "409 CONSENT_STALE":
1453
+ return new ConsentStale(message, details);
1411
1454
  case "412 CONFIRM_REQUIRED":
1412
1455
  return new ConfirmRequired(message, details);
1413
1456
  case "413 ARGS_TOO_LARGE":
@@ -1484,6 +1527,113 @@ function browserPasskey() {
1484
1527
  };
1485
1528
  }
1486
1529
 
1530
+ // src/cloud-v2/approvals.ts
1531
+ var MAX_REPORT_PAGE = 100;
1532
+ function pageQuery(page = {}) {
1533
+ const q = new URLSearchParams();
1534
+ if (page.after !== void 0) q.set("after", page.after);
1535
+ if (page.limit !== void 0) {
1536
+ if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > MAX_REPORT_PAGE) {
1537
+ throw new RangeError(`limit is 1 to ${MAX_REPORT_PAGE}`);
1538
+ }
1539
+ q.set("limit", String(page.limit));
1540
+ }
1541
+ const s = q.toString();
1542
+ return s ? `?${s}` : "";
1543
+ }
1544
+ function compareVersions(a, b) {
1545
+ try {
1546
+ return compareReleaseVersions(a, b);
1547
+ } catch {
1548
+ return a < b ? -1 : a > b ? 1 : 0;
1549
+ }
1550
+ }
1551
+ function stalledBy(approved, deadlines, now = Date.now() / 1e3) {
1552
+ if (approved === null || approved.length === 0) return null;
1553
+ const newest = approved.reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
1554
+ let stall = null;
1555
+ for (const d of deadlines) {
1556
+ if (d.afterSecs <= now && compareVersions(newest, d.version) < 0) {
1557
+ if (stall === null || compareVersions(d.version, stall.version) > 0) stall = d;
1558
+ }
1559
+ }
1560
+ return stall;
1561
+ }
1562
+ function publisherBindingMessage(cluster, org, signerId, generation) {
1563
+ return `calimero-publisher-binding/v1
1564
+ ${cluster}
1565
+ ${org}
1566
+ ${signerId}
1567
+ ${generation}`;
1568
+ }
1569
+ function publisherBindingBytes(cluster, org, signerId, generation) {
1570
+ return new TextEncoder().encode(publisherBindingMessage(cluster, org, signerId, generation));
1571
+ }
1572
+ function consentReturn(href) {
1573
+ const url2 = typeof href === "string" ? new URL(href) : href;
1574
+ const requestId = url2.searchParams.get("request_id");
1575
+ const result = url2.searchParams.get("result");
1576
+ if (!requestId || result !== "approved" && result !== "declined") return null;
1577
+ return { requestId, result };
1578
+ }
1579
+ var RELEASE_VERIFIER = "https://mero-tee.vercel.app";
1580
+ function releaseNotesUrl(version) {
1581
+ return `https://github.com/calimero-network/mero-tee/releases/tag/mero-tee-v${encodeURIComponent(version)}`;
1582
+ }
1583
+ function verifyReleaseUrl(version) {
1584
+ return `${RELEASE_VERIFIER}/mero-tee?release_tag=${encodeURIComponent(`mero-tee-v${version}`)}`;
1585
+ }
1586
+ function pendingApprovalsFrom(report, rules, now = Date.now() / 1e3) {
1587
+ const deadlineOf = (v) => rules.deadlines.find((d) => d.version === v) ?? null;
1588
+ const releasesActionable = report.governance.releases === "owner";
1589
+ const upgradesActionable = report.governance.appUpgrades === "owner";
1590
+ const releases = report.clusterReleases.filter((r) => !r.approved).sort((a, b) => compareVersions(b.release, a.release)).map((r) => ({
1591
+ version: r.release,
1592
+ nodes: r.nodes,
1593
+ actionable: releasesActionable,
1594
+ releaseNotesUrl: releaseNotesUrl(r.release),
1595
+ verifyUrl: verifyReleaseUrl(r.release),
1596
+ deadline: deadlineOf(r.release)
1597
+ }));
1598
+ const upgrades = [];
1599
+ for (const a of report.apps) {
1600
+ const available = a.availableVersions ?? [];
1601
+ if (a.package === null || available.length === 0) continue;
1602
+ upgrades.push({
1603
+ app: a.app,
1604
+ name: a.name ?? null,
1605
+ package: a.package.package,
1606
+ from: a.package.version,
1607
+ to: available[available.length - 1],
1608
+ available,
1609
+ actionable: upgradesActionable
1610
+ });
1611
+ }
1612
+ return {
1613
+ scope: report.scope,
1614
+ governance: report.governance,
1615
+ approvedReleases: report.approvedReleases,
1616
+ releases,
1617
+ upgrades,
1618
+ stalledBy: stalledBy(report.approvedReleases, rules.deadlines, now),
1619
+ actionable: releases.some((r) => r.actionable) || upgrades.some((u) => u.actionable)
1620
+ };
1621
+ }
1622
+ function releaseConsent(scope, version, returnTo, options = {}) {
1623
+ return {
1624
+ kind: "release",
1625
+ scope: scope.kind === "org" ? { org: scope.org } : "account",
1626
+ subject: options.withdraw ? { version, withdraw: true } : { version },
1627
+ returnTo
1628
+ };
1629
+ }
1630
+ function withdrawConsent(scope, version, returnTo) {
1631
+ return releaseConsent(scope, version, returnTo, { withdraw: true });
1632
+ }
1633
+ function upgradeConsent(app, toVersion, returnTo) {
1634
+ return { kind: "upgrade", subject: { app, toVersion }, returnTo };
1635
+ }
1636
+
1487
1637
  // src/cloud-v2/client.ts
1488
1638
  function seg(value) {
1489
1639
  return encodeURIComponent(value);
@@ -1547,11 +1697,20 @@ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
1547
1697
  function retryDelayMs(retry, options) {
1548
1698
  return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
1549
1699
  }
1700
+ function ceremony(path) {
1701
+ return path === "auth/refresh" || path.startsWith("auth/passkey/");
1702
+ }
1550
1703
  var CalimeroClient = class {
1551
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();
1552
1709
  /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
1553
1710
  this.networkFailures = /* @__PURE__ */ new WeakSet();
1554
- this.base = options.baseUrl.replace(/\/+$/, "");
1711
+ const trim = (url2) => url2.replace(/\/+$/, "");
1712
+ this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
1713
+ this.policy = options.policy;
1555
1714
  this.retry = { ...DEFAULT_RETRY, ...options.retry };
1556
1715
  const given = options.fetch ?? ((input, init) => fetch(input, init));
1557
1716
  const raw = async (input, init) => {
@@ -1563,11 +1722,33 @@ var CalimeroClient = class {
1563
1722
  }
1564
1723
  };
1565
1724
  this.raw = raw;
1566
- this.sealed = createSealedFetch({
1567
- baseUrl: this.base,
1568
- transportPublicKey: () => fetchAttestedKey(this.base, options.policy, raw),
1569
- fetch: raw
1570
- });
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;
1571
1752
  }
1572
1753
  /** Authenticate every request with an API key. */
1573
1754
  useApiKey(key) {
@@ -1581,7 +1762,11 @@ var CalimeroClient = class {
1581
1762
  get session() {
1582
1763
  return this.current;
1583
1764
  }
1584
- async request(method, path, body, raw, contentType = "application/wasm") {
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) {
1585
1770
  const headers = {};
1586
1771
  if (this.auth) headers.authorization = `Bearer ${this.auth}`;
1587
1772
  let payload;
@@ -1592,15 +1777,66 @@ var CalimeroClient = class {
1592
1777
  headers["content-type"] = "application/json";
1593
1778
  payload = JSON.stringify(body);
1594
1779
  }
1780
+ const url2 = this.nodeUrl;
1595
1781
  try {
1596
- return await this.sealed(`${this.base}/${path}`, { method, headers, body: payload });
1782
+ return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
1597
1783
  } catch (error) {
1598
1784
  throw sealedError(error);
1599
1785
  }
1600
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
+ }
1601
1838
  async json(method, path, body, raw, contentType) {
1602
- const response = await this.request(method, path, body, raw, contentType);
1603
- if (!response.ok) throw await refusal(response);
1839
+ const response = await this.send(method, path, body, raw, contentType);
1604
1840
  const text = await response.text();
1605
1841
  return text ? JSON.parse(text) : {};
1606
1842
  }
@@ -1669,11 +1905,35 @@ var CalimeroClient = class {
1669
1905
  this.useSession(session);
1670
1906
  return session;
1671
1907
  }
1672
- /** Swap the refresh token for a new session. */
1673
- async refresh() {
1674
- if (!this.current) throw new Error("no session to refresh");
1675
- const session = await this.post("auth/refresh", { refreshToken: this.current.refreshToken });
1676
- this.useSession(session);
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);
1677
1937
  return session;
1678
1938
  }
1679
1939
  async signOut() {
@@ -1718,7 +1978,7 @@ var CalimeroClient = class {
1718
1978
  */
1719
1979
  async attestation() {
1720
1980
  const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
1721
- const response = await this.raw(`${this.base}/attestation?nonce=${nonce2}`);
1981
+ const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
1722
1982
  if (!response.ok) throw await refusal(response);
1723
1983
  return await response.json();
1724
1984
  }
@@ -1746,8 +2006,7 @@ var CalimeroClient = class {
1746
2006
  }
1747
2007
  /** One of `app`'s blobs, by id (base58 or hex). */
1748
2008
  async downloadBlob(app, id) {
1749
- const response = await this.request("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
1750
- if (!response.ok) throw await refusal(response);
2009
+ const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
1751
2010
  return new Uint8Array(await response.arrayBuffer());
1752
2011
  }
1753
2012
  /** `app`'s blobs. */
@@ -1829,8 +2088,11 @@ var CalimeroClient = class {
1829
2088
  * Move an app to another release of its package (`{ version }`), another
1830
2089
  * package, or a bundle hash, running its migration. Answers the upgrade's
1831
2090
  * entry; a failed migration rejects with code `UPGRADE_FAILED` (422).
1832
- * Where the owner holds app upgrades, the node asks for the owner's approval
1833
- * (`412 CONFIRM_REQUIRED`), which `passkey` gives.
2091
+ * Where owners hold app upgrades, the node asks for the approval of
2092
+ * whoever the package's manifest names (`412 CONFIRM_REQUIRED`), which
2093
+ * `passkey` gives. Refused (`403`) to a session opened on an app's
2094
+ * frontend: an app asks its user with {@link requestConsent} instead. Past
2095
+ * a release deadline its approver has not met: {@link ReleaseDeadlineError}.
1834
2096
  */
1835
2097
  upgradeApp(app, target, passkey) {
1836
2098
  const path = `apps/${seg(app)}/upgrade`;
@@ -1913,10 +2175,7 @@ var CalimeroClient = class {
1913
2175
  }
1914
2176
  /** A server-sent event stream on `path`, sealed like every other answer. */
1915
2177
  async *stream(path, signal) {
1916
- const headers = {};
1917
- if (this.auth) headers.authorization = `Bearer ${this.auth}`;
1918
- const response = await this.sealed(`${this.base}/${path}`, { headers, signal });
1919
- if (!response.ok) throw await refusal(response);
2178
+ const response = await this.send("GET", path, void 0, void 0, void 0, signal);
1920
2179
  if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
1921
2180
  yield* sseFrames(response.body);
1922
2181
  }
@@ -2003,8 +2262,11 @@ var CalimeroClient = class {
2003
2262
  /**
2004
2263
  * Approve a mero-tee release to hold the apps of `org`, or of this account
2005
2264
  * outside any organization (`org` `null`); `approved: false` withdraws it.
2006
- * Until then, nodes on that release get none of those apps' data. The owner
2007
- * approves with `passkey`.
2265
+ * Until then, nodes on that release get none of those apps' data. For an
2266
+ * organization, a member holding `APPROVE_RELEASES` approves with `passkey`;
2267
+ * the node binds the release's measurements itself. Refused (`403`) to a
2268
+ * session opened on an app's frontend: an app asks its user with
2269
+ * {@link requestConsent} instead.
2008
2270
  */
2009
2271
  async approveRelease(org, version, passkey, approved = true) {
2010
2272
  const path = org === null ? "auth/releases" : `orgs/${seg(org)}/releases`;
@@ -2015,7 +2277,8 @@ var CalimeroClient = class {
2015
2277
  * `org`'s apps or this account's own (`org` `null`). `prefixes` `'manifest'`
2016
2278
  * (the default) approves every prefix the package's latest release declares;
2017
2279
  * `replace` sets exactly `prefixes`, which is how one is revoked. Approved
2018
- * with `passkey`.
2280
+ * with `passkey`. Refused (`403`) to a session opened on an app's frontend:
2281
+ * an app asks with {@link requestConsent} (`kind: 'outbound'`) instead.
2019
2282
  */
2020
2283
  async approveOutbound(org, pkg, passkey, options = {}) {
2021
2284
  const path = org === null ? "auth/outbound" : `orgs/${seg(org)}/outbound`;
@@ -2025,6 +2288,137 @@ var CalimeroClient = class {
2025
2288
  passkey
2026
2289
  );
2027
2290
  }
2291
+ // ---------------------------------------------------------------- approvals
2292
+ /**
2293
+ * One page of the report of a scope's apps: an organization's (any member;
2294
+ * `404` to anyone else) or, with `org` `null`, the account's own outside any
2295
+ * organization (a session bound to an app's frontend: that package's apps).
2296
+ * The governance, the scope's approved releases, the releases the cluster's
2297
+ * nodes run and whether each is approved, and per app its release, newer
2298
+ * releases it may move to, last upgrade and replicas.
2299
+ */
2300
+ appsReport(org, page = {}) {
2301
+ const path = org === null ? "auth/apps" : `orgs/${seg(org)}/apps`;
2302
+ return this.get(`${path}${pageQuery(page)}`);
2303
+ }
2304
+ /** {@link appsReport}, every page, with the apps of all of them in `apps`. */
2305
+ async appsReportAll(org) {
2306
+ const first = await this.appsReport(org);
2307
+ const apps = [...first.apps];
2308
+ let next = first.next;
2309
+ while (next !== null) {
2310
+ const page = await this.appsReport(org, { after: next });
2311
+ apps.push(...page.apps);
2312
+ next = page.next;
2313
+ }
2314
+ return { ...first, apps, next: null };
2315
+ }
2316
+ /** `GET /releases`: the cluster's governance and every release deadline. Public. */
2317
+ releaseRules() {
2318
+ return this.get("releases");
2319
+ }
2320
+ /**
2321
+ * What waits for a decision in a scope (`org`, or the account's own apps
2322
+ * with `null`): mero-tee releases the cluster's nodes run that the scope has
2323
+ * not approved, apps with newer releases of their package, and whether a
2324
+ * passed deadline stops new work. Read over the sealed channel; act on an
2325
+ * item by sending the user to the consent page ({@link requestConsent},
2326
+ * `releaseConsent`, `upgradeConsent`).
2327
+ */
2328
+ async pendingApprovals(org = null) {
2329
+ const [report, rules] = await Promise.all([this.appsReportAll(org), this.releaseRules()]);
2330
+ return pendingApprovalsFrom(report, rules);
2331
+ }
2332
+ /**
2333
+ * Ask the node to ask the user: a release, upgrade or outbound approval
2334
+ * that only the account holder may make, decided on the node's own consent
2335
+ * page, never in the app. Needs a passkey session (a session bound to the
2336
+ * app's frontend is fine). Send the person to the answer's `url`; the page
2337
+ * sends them back to `returnTo?request_id=…&result=approved|declined`
2338
+ * (`consentReturn`), and {@link consentResult} confirms the decision.
2339
+ */
2340
+ requestConsent(request) {
2341
+ return this.post("consent/requests", request);
2342
+ }
2343
+ /**
2344
+ * {@link requestConsent}, then send the browser to the consent page
2345
+ * (`navigate`, by default `window.location.assign`). Answers the request
2346
+ * once `navigate` returns: with a popup (`url => window.open(url)`), poll it
2347
+ * with {@link waitForConsent}.
2348
+ */
2349
+ async openConsentPage(request, navigate = (url2) => {
2350
+ const w = globalThis.window;
2351
+ if (!w) throw new Error("no window to open the consent page in; pass `navigate`");
2352
+ w.location.assign(url2);
2353
+ }) {
2354
+ const started = await this.requestConsent(request);
2355
+ navigate(started.url);
2356
+ return started;
2357
+ }
2358
+ /** The decision on a consent request (any node answers, for the account that asked). */
2359
+ consentResult(requestId) {
2360
+ return this.get(`consent/${seg(requestId)}/result`);
2361
+ }
2362
+ /**
2363
+ * Poll {@link consentResult} until the request is decided, every
2364
+ * `intervalMs` (2 s), until `signal` aborts or `expiresAt` (Unix seconds)
2365
+ * passes, when it answers the last `pending` result.
2366
+ */
2367
+ async waitForConsent(requestId, options = {}) {
2368
+ const interval = options.intervalMs ?? 2e3;
2369
+ for (; ; ) {
2370
+ const result = await this.consentResult(requestId);
2371
+ if (result.status !== "pending") return result;
2372
+ if (options.signal?.aborted) return result;
2373
+ if (options.expiresAt !== void 0 && Date.now() / 1e3 >= options.expiresAt) return result;
2374
+ await new Promise((resolve) => setTimeout(resolve, interval));
2375
+ }
2376
+ }
2377
+ /** `GET /publishers/{signerId}`: the cluster name and binding generation a key signs to bind it next. */
2378
+ publisherBinding(signerId) {
2379
+ return this.get(`publishers/${seg(signerId)}`);
2380
+ }
2381
+ /**
2382
+ * Bind a package signing key to `org`, which then approves the node images
2383
+ * every instance of the key's packages runs on, and upgrades those whose
2384
+ * package declares `upgrades: publisher`. `proof` is the key's signature
2385
+ * over `publisherBindingMessage(cluster, org, signerId, generation)`; a
2386
+ * member holding `APPROVE_RELEASES` sends it with `passkey`.
2387
+ */
2388
+ bindPublisher(org, proof, passkey) {
2389
+ return this.postConfirmed(`orgs/${seg(org)}/publishers`, { ...proof }, passkey);
2390
+ }
2391
+ /**
2392
+ * Upgrade every instance of `pkg` whose release declares `upgrades:
2393
+ * publisher`, whoever owns it, as its publisher `org` (a
2394
+ * `MANAGE_APPLICATION` holder there), with one passkey approval. Answers
2395
+ * the run, kept on the publisher.
2396
+ */
2397
+ upgradePublished(org, pkg, target, passkey) {
2398
+ return this.postConfirmed(`orgs/${seg(org)}/packages/${seg(pkg)}/upgrade`, { ...target }, passkey);
2399
+ }
2400
+ // ---------------------------------------------------------------- operator
2401
+ /** `GET /ops/apps`: what every app runs, without names or accounts. Operator token (`useApiKey`). */
2402
+ opsApps(page = {}) {
2403
+ return this.get(`ops/apps${pageQuery(page)}`);
2404
+ }
2405
+ /** `GET /ops/orgs`: each organization's approved releases. Operator token. */
2406
+ opsOrgs(page = {}) {
2407
+ return this.get(`ops/orgs${pageQuery(page)}`);
2408
+ }
2409
+ /**
2410
+ * Announce when work on images older than `version` ends: from `after` on,
2411
+ * a scope that has not approved it (or newer) starts no new work. Operator
2412
+ * token.
2413
+ */
2414
+ async setReleaseDeadline(version, after) {
2415
+ const at = typeof after === "string" ? after : after.toISOString();
2416
+ await this.post(`ops/releases/${seg(version)}/deadline`, { after: at });
2417
+ }
2418
+ /** Lift a release's deadline. Operator token. */
2419
+ async liftReleaseDeadline(version) {
2420
+ await this.delete(`ops/releases/${seg(version)}/deadline`);
2421
+ }
2028
2422
  /** Add a member, or change their role. */
2029
2423
  async setMember(org, account, role) {
2030
2424
  await this.post(`orgs/${seg(org)}/members`, { account, role });
@@ -2084,7 +2478,7 @@ var ORG_CAPABILITIES = {
2084
2478
  JOIN_OPEN_TEAMS: 1 << 2,
2085
2479
  /** Add and remove a team's members. */
2086
2480
  MANAGE_MEMBERS: 1 << 3,
2087
- /** Upgrade the organization's apps, start org upgrade runs, approve outbound hosts (org-wide). */
2481
+ /** Upgrade the organization's apps (and, as a publisher, every instance of its `publisher` packages), start org upgrade runs, approve outbound hosts (org-wide). */
2088
2482
  MANAGE_APPLICATION: 1 << 4,
2089
2483
  /** Create a top-level team without being an admin. */
2090
2484
  CREATE_TEAM: 1 << 5,
@@ -2093,7 +2487,14 @@ var ORG_CAPABILITIES = {
2093
2487
  /** Change who may join a team (open or invite-only). */
2094
2488
  MANAGE_VISIBILITY: 1 << 7,
2095
2489
  /** Set the organization's and its teams' metadata. */
2096
- MANAGE_METADATA: 1 << 8
2490
+ MANAGE_METADATA: 1 << 8,
2491
+ /**
2492
+ * Approve (or withdraw) the mero-tee releases that hold the apps the
2493
+ * organization governs (those of the packages it publishes, and its own of
2494
+ * unbound packages), and bind a package signing key to it
2495
+ * (`bindPublisher`). Organization-wide only. Bit 9 is left unused.
2496
+ */
2497
+ APPROVE_RELEASES: 1 << 10
2097
2498
  };
2098
2499
  var DEFAULT_ORG_CAPABILITIES = ORG_CAPABILITIES.JOIN_OPEN_TEAMS;
2099
2500
  function orgCapabilities(...names) {