@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.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);
@@ -1266,6 +1272,20 @@ var IdempotencyMismatch = class extends CalimeroError {
1266
1272
  this.name = "IdempotencyMismatch";
1267
1273
  }
1268
1274
  };
1275
+ var ReleaseDeadlineError = class extends CalimeroError {
1276
+ constructor(message, details = {}) {
1277
+ super(409, "RELEASE_DEADLINE", message, details);
1278
+ this.name = "ReleaseDeadlineError";
1279
+ this.release = typeof details.release === "string" ? details.release : "";
1280
+ this.after = typeof details.after === "string" ? details.after : "";
1281
+ }
1282
+ };
1283
+ var ConsentStale = class extends CalimeroError {
1284
+ constructor(message, details = {}) {
1285
+ super(409, "CONSENT_STALE", message, details);
1286
+ this.name = "ConsentStale";
1287
+ }
1288
+ };
1269
1289
  var AppError = class extends CalimeroError {
1270
1290
  constructor(message, details = {}) {
1271
1291
  super(422, "APP_ERROR", message, details);
@@ -1315,10 +1335,16 @@ function errorFrom(status, body, retryAfterHeader) {
1315
1335
  switch (`${status} ${code}`) {
1316
1336
  case "401 UNAUTHENTICATED":
1317
1337
  return new LoginRequired(message, details);
1338
+ case "421 WRONG_NODE":
1339
+ return new WrongNode(message, details);
1318
1340
  case "404 NOT_FOUND":
1319
1341
  return new NotFound(message, details);
1320
1342
  case "409 IDEMPOTENCY_MISMATCH":
1321
1343
  return new IdempotencyMismatch(message, details);
1344
+ case "409 RELEASE_DEADLINE":
1345
+ return new ReleaseDeadlineError(message, details);
1346
+ case "409 CONSENT_STALE":
1347
+ return new ConsentStale(message, details);
1322
1348
  case "412 CONFIRM_REQUIRED":
1323
1349
  return new ConfirmRequired(message, details);
1324
1350
  case "413 ARGS_TOO_LARGE":
@@ -1395,6 +1421,113 @@ function browserPasskey() {
1395
1421
  };
1396
1422
  }
1397
1423
 
1424
+ // src/cloud-v2/approvals.ts
1425
+ var MAX_REPORT_PAGE = 100;
1426
+ function pageQuery(page = {}) {
1427
+ const q = new URLSearchParams();
1428
+ if (page.after !== void 0) q.set("after", page.after);
1429
+ if (page.limit !== void 0) {
1430
+ if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > MAX_REPORT_PAGE) {
1431
+ throw new RangeError(`limit is 1 to ${MAX_REPORT_PAGE}`);
1432
+ }
1433
+ q.set("limit", String(page.limit));
1434
+ }
1435
+ const s = q.toString();
1436
+ return s ? `?${s}` : "";
1437
+ }
1438
+ function compareVersions(a, b) {
1439
+ try {
1440
+ return compareReleaseVersions(a, b);
1441
+ } catch {
1442
+ return a < b ? -1 : a > b ? 1 : 0;
1443
+ }
1444
+ }
1445
+ function stalledBy(approved, deadlines, now = Date.now() / 1e3) {
1446
+ if (approved === null || approved.length === 0) return null;
1447
+ const newest = approved.reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
1448
+ let stall = null;
1449
+ for (const d of deadlines) {
1450
+ if (d.afterSecs <= now && compareVersions(newest, d.version) < 0) {
1451
+ if (stall === null || compareVersions(d.version, stall.version) > 0) stall = d;
1452
+ }
1453
+ }
1454
+ return stall;
1455
+ }
1456
+ function publisherBindingMessage(cluster, org, signerId, generation) {
1457
+ return `calimero-publisher-binding/v1
1458
+ ${cluster}
1459
+ ${org}
1460
+ ${signerId}
1461
+ ${generation}`;
1462
+ }
1463
+ function publisherBindingBytes(cluster, org, signerId, generation) {
1464
+ return new TextEncoder().encode(publisherBindingMessage(cluster, org, signerId, generation));
1465
+ }
1466
+ function consentReturn(href) {
1467
+ const url2 = typeof href === "string" ? new URL(href) : href;
1468
+ const requestId = url2.searchParams.get("request_id");
1469
+ const result = url2.searchParams.get("result");
1470
+ if (!requestId || result !== "approved" && result !== "declined") return null;
1471
+ return { requestId, result };
1472
+ }
1473
+ var RELEASE_VERIFIER = "https://mero-tee.vercel.app";
1474
+ function releaseNotesUrl(version) {
1475
+ return `https://github.com/calimero-network/mero-tee/releases/tag/mero-tee-v${encodeURIComponent(version)}`;
1476
+ }
1477
+ function verifyReleaseUrl(version) {
1478
+ return `${RELEASE_VERIFIER}/mero-tee?release_tag=${encodeURIComponent(`mero-tee-v${version}`)}`;
1479
+ }
1480
+ function pendingApprovalsFrom(report, rules, now = Date.now() / 1e3) {
1481
+ const deadlineOf = (v) => rules.deadlines.find((d) => d.version === v) ?? null;
1482
+ const releasesActionable = report.governance.releases === "owner";
1483
+ const upgradesActionable = report.governance.appUpgrades === "owner";
1484
+ const releases = report.clusterReleases.filter((r) => !r.approved).sort((a, b) => compareVersions(b.release, a.release)).map((r) => ({
1485
+ version: r.release,
1486
+ nodes: r.nodes,
1487
+ actionable: releasesActionable,
1488
+ releaseNotesUrl: releaseNotesUrl(r.release),
1489
+ verifyUrl: verifyReleaseUrl(r.release),
1490
+ deadline: deadlineOf(r.release)
1491
+ }));
1492
+ const upgrades = [];
1493
+ for (const a of report.apps) {
1494
+ const available = a.availableVersions ?? [];
1495
+ if (a.package === null || available.length === 0) continue;
1496
+ upgrades.push({
1497
+ app: a.app,
1498
+ name: a.name ?? null,
1499
+ package: a.package.package,
1500
+ from: a.package.version,
1501
+ to: available[available.length - 1],
1502
+ available,
1503
+ actionable: upgradesActionable
1504
+ });
1505
+ }
1506
+ return {
1507
+ scope: report.scope,
1508
+ governance: report.governance,
1509
+ approvedReleases: report.approvedReleases,
1510
+ releases,
1511
+ upgrades,
1512
+ stalledBy: stalledBy(report.approvedReleases, rules.deadlines, now),
1513
+ actionable: releases.some((r) => r.actionable) || upgrades.some((u) => u.actionable)
1514
+ };
1515
+ }
1516
+ function releaseConsent(scope, version, returnTo, options = {}) {
1517
+ return {
1518
+ kind: "release",
1519
+ scope: scope.kind === "org" ? { org: scope.org } : "account",
1520
+ subject: options.withdraw ? { version, withdraw: true } : { version },
1521
+ returnTo
1522
+ };
1523
+ }
1524
+ function withdrawConsent(scope, version, returnTo) {
1525
+ return releaseConsent(scope, version, returnTo, { withdraw: true });
1526
+ }
1527
+ function upgradeConsent(app, toVersion, returnTo) {
1528
+ return { kind: "upgrade", subject: { app, toVersion }, returnTo };
1529
+ }
1530
+
1398
1531
  // src/cloud-v2/client.ts
1399
1532
  function seg(value) {
1400
1533
  return encodeURIComponent(value);
@@ -1458,11 +1591,20 @@ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
1458
1591
  function retryDelayMs(retry, options) {
1459
1592
  return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
1460
1593
  }
1594
+ function ceremony(path) {
1595
+ return path === "auth/refresh" || path.startsWith("auth/passkey/");
1596
+ }
1461
1597
  var CalimeroClient = class {
1462
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();
1463
1603
  /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
1464
1604
  this.networkFailures = /* @__PURE__ */ new WeakSet();
1465
- this.base = options.baseUrl.replace(/\/+$/, "");
1605
+ const trim = (url2) => url2.replace(/\/+$/, "");
1606
+ this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
1607
+ this.policy = options.policy;
1466
1608
  this.retry = { ...DEFAULT_RETRY, ...options.retry };
1467
1609
  const given = options.fetch ?? ((input, init) => fetch(input, init));
1468
1610
  const raw = async (input, init) => {
@@ -1474,11 +1616,33 @@ var CalimeroClient = class {
1474
1616
  }
1475
1617
  };
1476
1618
  this.raw = raw;
1477
- this.sealed = createSealedFetch({
1478
- baseUrl: this.base,
1479
- transportPublicKey: () => fetchAttestedKey(this.base, options.policy, raw),
1480
- fetch: raw
1481
- });
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;
1482
1646
  }
1483
1647
  /** Authenticate every request with an API key. */
1484
1648
  useApiKey(key) {
@@ -1492,7 +1656,11 @@ var CalimeroClient = class {
1492
1656
  get session() {
1493
1657
  return this.current;
1494
1658
  }
1495
- async request(method, path, body, raw, contentType = "application/wasm") {
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) {
1496
1664
  const headers = {};
1497
1665
  if (this.auth) headers.authorization = `Bearer ${this.auth}`;
1498
1666
  let payload;
@@ -1503,15 +1671,66 @@ var CalimeroClient = class {
1503
1671
  headers["content-type"] = "application/json";
1504
1672
  payload = JSON.stringify(body);
1505
1673
  }
1674
+ const url2 = this.nodeUrl;
1506
1675
  try {
1507
- return await this.sealed(`${this.base}/${path}`, { method, headers, body: payload });
1676
+ return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
1508
1677
  } catch (error) {
1509
1678
  throw sealedError(error);
1510
1679
  }
1511
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
+ }
1512
1732
  async json(method, path, body, raw, contentType) {
1513
- const response = await this.request(method, path, body, raw, contentType);
1514
- if (!response.ok) throw await refusal(response);
1733
+ const response = await this.send(method, path, body, raw, contentType);
1515
1734
  const text = await response.text();
1516
1735
  return text ? JSON.parse(text) : {};
1517
1736
  }
@@ -1580,11 +1799,35 @@ var CalimeroClient = class {
1580
1799
  this.useSession(session);
1581
1800
  return session;
1582
1801
  }
1583
- /** Swap the refresh token for a new session. */
1584
- async refresh() {
1585
- if (!this.current) throw new Error("no session to refresh");
1586
- const session = await this.post("auth/refresh", { refreshToken: this.current.refreshToken });
1587
- this.useSession(session);
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);
1588
1831
  return session;
1589
1832
  }
1590
1833
  async signOut() {
@@ -1629,7 +1872,7 @@ var CalimeroClient = class {
1629
1872
  */
1630
1873
  async attestation() {
1631
1874
  const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
1632
- const response = await this.raw(`${this.base}/attestation?nonce=${nonce2}`);
1875
+ const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
1633
1876
  if (!response.ok) throw await refusal(response);
1634
1877
  return await response.json();
1635
1878
  }
@@ -1657,8 +1900,7 @@ var CalimeroClient = class {
1657
1900
  }
1658
1901
  /** One of `app`'s blobs, by id (base58 or hex). */
1659
1902
  async downloadBlob(app, id) {
1660
- const response = await this.request("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
1661
- if (!response.ok) throw await refusal(response);
1903
+ const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
1662
1904
  return new Uint8Array(await response.arrayBuffer());
1663
1905
  }
1664
1906
  /** `app`'s blobs. */
@@ -1740,8 +1982,11 @@ var CalimeroClient = class {
1740
1982
  * Move an app to another release of its package (`{ version }`), another
1741
1983
  * package, or a bundle hash, running its migration. Answers the upgrade's
1742
1984
  * entry; a failed migration rejects with code `UPGRADE_FAILED` (422).
1743
- * Where the owner holds app upgrades, the node asks for the owner's approval
1744
- * (`412 CONFIRM_REQUIRED`), which `passkey` gives.
1985
+ * Where owners hold app upgrades, the node asks for the approval of
1986
+ * whoever the package's manifest names (`412 CONFIRM_REQUIRED`), which
1987
+ * `passkey` gives. Refused (`403`) to a session opened on an app's
1988
+ * frontend: an app asks its user with {@link requestConsent} instead. Past
1989
+ * a release deadline its approver has not met: {@link ReleaseDeadlineError}.
1745
1990
  */
1746
1991
  upgradeApp(app, target, passkey) {
1747
1992
  const path = `apps/${seg(app)}/upgrade`;
@@ -1824,10 +2069,7 @@ var CalimeroClient = class {
1824
2069
  }
1825
2070
  /** A server-sent event stream on `path`, sealed like every other answer. */
1826
2071
  async *stream(path, signal) {
1827
- const headers = {};
1828
- if (this.auth) headers.authorization = `Bearer ${this.auth}`;
1829
- const response = await this.sealed(`${this.base}/${path}`, { headers, signal });
1830
- if (!response.ok) throw await refusal(response);
2072
+ const response = await this.send("GET", path, void 0, void 0, void 0, signal);
1831
2073
  if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
1832
2074
  yield* sseFrames(response.body);
1833
2075
  }
@@ -1914,8 +2156,11 @@ var CalimeroClient = class {
1914
2156
  /**
1915
2157
  * Approve a mero-tee release to hold the apps of `org`, or of this account
1916
2158
  * outside any organization (`org` `null`); `approved: false` withdraws it.
1917
- * Until then, nodes on that release get none of those apps' data. The owner
1918
- * approves with `passkey`.
2159
+ * Until then, nodes on that release get none of those apps' data. For an
2160
+ * organization, a member holding `APPROVE_RELEASES` approves with `passkey`;
2161
+ * the node binds the release's measurements itself. Refused (`403`) to a
2162
+ * session opened on an app's frontend: an app asks its user with
2163
+ * {@link requestConsent} instead.
1919
2164
  */
1920
2165
  async approveRelease(org, version, passkey, approved = true) {
1921
2166
  const path = org === null ? "auth/releases" : `orgs/${seg(org)}/releases`;
@@ -1926,7 +2171,8 @@ var CalimeroClient = class {
1926
2171
  * `org`'s apps or this account's own (`org` `null`). `prefixes` `'manifest'`
1927
2172
  * (the default) approves every prefix the package's latest release declares;
1928
2173
  * `replace` sets exactly `prefixes`, which is how one is revoked. Approved
1929
- * with `passkey`.
2174
+ * with `passkey`. Refused (`403`) to a session opened on an app's frontend:
2175
+ * an app asks with {@link requestConsent} (`kind: 'outbound'`) instead.
1930
2176
  */
1931
2177
  async approveOutbound(org, pkg, passkey, options = {}) {
1932
2178
  const path = org === null ? "auth/outbound" : `orgs/${seg(org)}/outbound`;
@@ -1936,6 +2182,137 @@ var CalimeroClient = class {
1936
2182
  passkey
1937
2183
  );
1938
2184
  }
2185
+ // ---------------------------------------------------------------- approvals
2186
+ /**
2187
+ * One page of the report of a scope's apps: an organization's (any member;
2188
+ * `404` to anyone else) or, with `org` `null`, the account's own outside any
2189
+ * organization (a session bound to an app's frontend: that package's apps).
2190
+ * The governance, the scope's approved releases, the releases the cluster's
2191
+ * nodes run and whether each is approved, and per app its release, newer
2192
+ * releases it may move to, last upgrade and replicas.
2193
+ */
2194
+ appsReport(org, page = {}) {
2195
+ const path = org === null ? "auth/apps" : `orgs/${seg(org)}/apps`;
2196
+ return this.get(`${path}${pageQuery(page)}`);
2197
+ }
2198
+ /** {@link appsReport}, every page, with the apps of all of them in `apps`. */
2199
+ async appsReportAll(org) {
2200
+ const first = await this.appsReport(org);
2201
+ const apps = [...first.apps];
2202
+ let next = first.next;
2203
+ while (next !== null) {
2204
+ const page = await this.appsReport(org, { after: next });
2205
+ apps.push(...page.apps);
2206
+ next = page.next;
2207
+ }
2208
+ return { ...first, apps, next: null };
2209
+ }
2210
+ /** `GET /releases`: the cluster's governance and every release deadline. Public. */
2211
+ releaseRules() {
2212
+ return this.get("releases");
2213
+ }
2214
+ /**
2215
+ * What waits for a decision in a scope (`org`, or the account's own apps
2216
+ * with `null`): mero-tee releases the cluster's nodes run that the scope has
2217
+ * not approved, apps with newer releases of their package, and whether a
2218
+ * passed deadline stops new work. Read over the sealed channel; act on an
2219
+ * item by sending the user to the consent page ({@link requestConsent},
2220
+ * `releaseConsent`, `upgradeConsent`).
2221
+ */
2222
+ async pendingApprovals(org = null) {
2223
+ const [report, rules] = await Promise.all([this.appsReportAll(org), this.releaseRules()]);
2224
+ return pendingApprovalsFrom(report, rules);
2225
+ }
2226
+ /**
2227
+ * Ask the node to ask the user: a release, upgrade or outbound approval
2228
+ * that only the account holder may make, decided on the node's own consent
2229
+ * page, never in the app. Needs a passkey session (a session bound to the
2230
+ * app's frontend is fine). Send the person to the answer's `url`; the page
2231
+ * sends them back to `returnTo?request_id=…&result=approved|declined`
2232
+ * (`consentReturn`), and {@link consentResult} confirms the decision.
2233
+ */
2234
+ requestConsent(request) {
2235
+ return this.post("consent/requests", request);
2236
+ }
2237
+ /**
2238
+ * {@link requestConsent}, then send the browser to the consent page
2239
+ * (`navigate`, by default `window.location.assign`). Answers the request
2240
+ * once `navigate` returns: with a popup (`url => window.open(url)`), poll it
2241
+ * with {@link waitForConsent}.
2242
+ */
2243
+ async openConsentPage(request, navigate = (url2) => {
2244
+ const w = globalThis.window;
2245
+ if (!w) throw new Error("no window to open the consent page in; pass `navigate`");
2246
+ w.location.assign(url2);
2247
+ }) {
2248
+ const started = await this.requestConsent(request);
2249
+ navigate(started.url);
2250
+ return started;
2251
+ }
2252
+ /** The decision on a consent request (any node answers, for the account that asked). */
2253
+ consentResult(requestId) {
2254
+ return this.get(`consent/${seg(requestId)}/result`);
2255
+ }
2256
+ /**
2257
+ * Poll {@link consentResult} until the request is decided, every
2258
+ * `intervalMs` (2 s), until `signal` aborts or `expiresAt` (Unix seconds)
2259
+ * passes, when it answers the last `pending` result.
2260
+ */
2261
+ async waitForConsent(requestId, options = {}) {
2262
+ const interval = options.intervalMs ?? 2e3;
2263
+ for (; ; ) {
2264
+ const result = await this.consentResult(requestId);
2265
+ if (result.status !== "pending") return result;
2266
+ if (options.signal?.aborted) return result;
2267
+ if (options.expiresAt !== void 0 && Date.now() / 1e3 >= options.expiresAt) return result;
2268
+ await new Promise((resolve) => setTimeout(resolve, interval));
2269
+ }
2270
+ }
2271
+ /** `GET /publishers/{signerId}`: the cluster name and binding generation a key signs to bind it next. */
2272
+ publisherBinding(signerId) {
2273
+ return this.get(`publishers/${seg(signerId)}`);
2274
+ }
2275
+ /**
2276
+ * Bind a package signing key to `org`, which then approves the node images
2277
+ * every instance of the key's packages runs on, and upgrades those whose
2278
+ * package declares `upgrades: publisher`. `proof` is the key's signature
2279
+ * over `publisherBindingMessage(cluster, org, signerId, generation)`; a
2280
+ * member holding `APPROVE_RELEASES` sends it with `passkey`.
2281
+ */
2282
+ bindPublisher(org, proof, passkey) {
2283
+ return this.postConfirmed(`orgs/${seg(org)}/publishers`, { ...proof }, passkey);
2284
+ }
2285
+ /**
2286
+ * Upgrade every instance of `pkg` whose release declares `upgrades:
2287
+ * publisher`, whoever owns it, as its publisher `org` (a
2288
+ * `MANAGE_APPLICATION` holder there), with one passkey approval. Answers
2289
+ * the run, kept on the publisher.
2290
+ */
2291
+ upgradePublished(org, pkg, target, passkey) {
2292
+ return this.postConfirmed(`orgs/${seg(org)}/packages/${seg(pkg)}/upgrade`, { ...target }, passkey);
2293
+ }
2294
+ // ---------------------------------------------------------------- operator
2295
+ /** `GET /ops/apps`: what every app runs, without names or accounts. Operator token (`useApiKey`). */
2296
+ opsApps(page = {}) {
2297
+ return this.get(`ops/apps${pageQuery(page)}`);
2298
+ }
2299
+ /** `GET /ops/orgs`: each organization's approved releases. Operator token. */
2300
+ opsOrgs(page = {}) {
2301
+ return this.get(`ops/orgs${pageQuery(page)}`);
2302
+ }
2303
+ /**
2304
+ * Announce when work on images older than `version` ends: from `after` on,
2305
+ * a scope that has not approved it (or newer) starts no new work. Operator
2306
+ * token.
2307
+ */
2308
+ async setReleaseDeadline(version, after) {
2309
+ const at = typeof after === "string" ? after : after.toISOString();
2310
+ await this.post(`ops/releases/${seg(version)}/deadline`, { after: at });
2311
+ }
2312
+ /** Lift a release's deadline. Operator token. */
2313
+ async liftReleaseDeadline(version) {
2314
+ await this.delete(`ops/releases/${seg(version)}/deadline`);
2315
+ }
1939
2316
  /** Add a member, or change their role. */
1940
2317
  async setMember(org, account, role) {
1941
2318
  await this.post(`orgs/${seg(org)}/members`, { account, role });
@@ -1995,7 +2372,7 @@ var ORG_CAPABILITIES = {
1995
2372
  JOIN_OPEN_TEAMS: 1 << 2,
1996
2373
  /** Add and remove a team's members. */
1997
2374
  MANAGE_MEMBERS: 1 << 3,
1998
- /** Upgrade the organization's apps, start org upgrade runs, approve outbound hosts (org-wide). */
2375
+ /** Upgrade the organization's apps (and, as a publisher, every instance of its `publisher` packages), start org upgrade runs, approve outbound hosts (org-wide). */
1999
2376
  MANAGE_APPLICATION: 1 << 4,
2000
2377
  /** Create a top-level team without being an admin. */
2001
2378
  CREATE_TEAM: 1 << 5,
@@ -2004,7 +2381,14 @@ var ORG_CAPABILITIES = {
2004
2381
  /** Change who may join a team (open or invite-only). */
2005
2382
  MANAGE_VISIBILITY: 1 << 7,
2006
2383
  /** Set the organization's and its teams' metadata. */
2007
- MANAGE_METADATA: 1 << 8
2384
+ MANAGE_METADATA: 1 << 8,
2385
+ /**
2386
+ * Approve (or withdraw) the mero-tee releases that hold the apps the
2387
+ * organization governs (those of the packages it publishes, and its own of
2388
+ * unbound packages), and bind a package signing key to it
2389
+ * (`bindPublisher`). Organization-wide only. Bit 9 is left unused.
2390
+ */
2391
+ APPROVE_RELEASES: 1 << 10
2008
2392
  };
2009
2393
  var DEFAULT_ORG_CAPABILITIES = ORG_CAPABILITIES.JOIN_OPEN_TEAMS;
2010
2394
  function orgCapabilities(...names) {
@@ -2455,6 +2839,7 @@ export {
2455
2839
  CalimeroError,
2456
2840
  ConfirmCancelled,
2457
2841
  ConfirmRequired,
2842
+ ConsentStale,
2458
2843
  DEFAULT_ORG_CAPABILITIES,
2459
2844
  DEFAULT_RELEASE_MIRROR,
2460
2845
  GITHUB_RELEASES,
@@ -2462,17 +2847,21 @@ export {
2462
2847
  IdempotencyMismatch,
2463
2848
  KMS_RELEASE_SIGNER,
2464
2849
  LoginRequired,
2850
+ MAX_REPORT_PAGE,
2465
2851
  MOCK_QUOTE_HEADER,
2466
2852
  NODE_RELEASE_SIGNER,
2467
2853
  NotFound,
2468
2854
  ORG_CAPABILITIES,
2469
2855
  PRODUCTION_PROFILE,
2856
+ RELEASE_VERIFIER,
2857
+ ReleaseDeadlineError,
2470
2858
  SEALED_CONTENT_TYPE,
2471
2859
  SEALED_PATH,
2472
2860
  SealedTransportError,
2473
2861
  StaleTransportKeyError,
2474
2862
  TrapError,
2475
2863
  UnavailableError,
2864
+ WrongNode,
2476
2865
  agentKeyBinding,
2477
2866
  agentQuoteVerifier,
2478
2867
  attestAgent,
@@ -2481,6 +2870,8 @@ export {
2481
2870
  checkOwner,
2482
2871
  claimedRelease,
2483
2872
  cloudNodeReleaseUrl,
2873
+ compareVersions,
2874
+ consentReturn,
2484
2875
  createQuoteVerifier,
2485
2876
  createSealedFetch,
2486
2877
  createSignedReleaseVerifier,
@@ -2492,14 +2883,24 @@ export {
2492
2883
  generateOwnerKey,
2493
2884
  orgCapabilities,
2494
2885
  ownerPublicKey,
2886
+ pageQuery,
2887
+ pendingApprovalsFrom,
2495
2888
  provisionAgent,
2496
2889
  provisionInfo,
2890
+ publisherBindingBytes,
2891
+ publisherBindingMessage,
2497
2892
  releaseAgent,
2893
+ releaseConsent,
2894
+ releaseNotesUrl,
2895
+ stalledBy,
2498
2896
  transportKeyBinding,
2499
2897
  trustSignedRelease,
2500
2898
  trustedMeasurementsFromReleases,
2899
+ upgradeConsent,
2501
2900
  validateAgentId,
2901
+ verifyReleaseUrl,
2502
2902
  verifySignedAgentPolicy,
2503
- verifySignedNodeRelease
2903
+ verifySignedNodeRelease,
2904
+ withdrawConsent
2504
2905
  };
2505
2906
  //# sourceMappingURL=index.mjs.map