@calimero-network/mero-js 27.0.1 → 27.1.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
@@ -1266,6 +1266,20 @@ var IdempotencyMismatch = class extends CalimeroError {
1266
1266
  this.name = "IdempotencyMismatch";
1267
1267
  }
1268
1268
  };
1269
+ var ReleaseDeadlineError = class extends CalimeroError {
1270
+ constructor(message, details = {}) {
1271
+ super(409, "RELEASE_DEADLINE", message, details);
1272
+ this.name = "ReleaseDeadlineError";
1273
+ this.release = typeof details.release === "string" ? details.release : "";
1274
+ this.after = typeof details.after === "string" ? details.after : "";
1275
+ }
1276
+ };
1277
+ var ConsentStale = class extends CalimeroError {
1278
+ constructor(message, details = {}) {
1279
+ super(409, "CONSENT_STALE", message, details);
1280
+ this.name = "ConsentStale";
1281
+ }
1282
+ };
1269
1283
  var AppError = class extends CalimeroError {
1270
1284
  constructor(message, details = {}) {
1271
1285
  super(422, "APP_ERROR", message, details);
@@ -1319,6 +1333,10 @@ function errorFrom(status, body, retryAfterHeader) {
1319
1333
  return new NotFound(message, details);
1320
1334
  case "409 IDEMPOTENCY_MISMATCH":
1321
1335
  return new IdempotencyMismatch(message, details);
1336
+ case "409 RELEASE_DEADLINE":
1337
+ return new ReleaseDeadlineError(message, details);
1338
+ case "409 CONSENT_STALE":
1339
+ return new ConsentStale(message, details);
1322
1340
  case "412 CONFIRM_REQUIRED":
1323
1341
  return new ConfirmRequired(message, details);
1324
1342
  case "413 ARGS_TOO_LARGE":
@@ -1395,6 +1413,113 @@ function browserPasskey() {
1395
1413
  };
1396
1414
  }
1397
1415
 
1416
+ // src/cloud-v2/approvals.ts
1417
+ var MAX_REPORT_PAGE = 100;
1418
+ function pageQuery(page = {}) {
1419
+ const q = new URLSearchParams();
1420
+ if (page.after !== void 0) q.set("after", page.after);
1421
+ if (page.limit !== void 0) {
1422
+ if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > MAX_REPORT_PAGE) {
1423
+ throw new RangeError(`limit is 1 to ${MAX_REPORT_PAGE}`);
1424
+ }
1425
+ q.set("limit", String(page.limit));
1426
+ }
1427
+ const s = q.toString();
1428
+ return s ? `?${s}` : "";
1429
+ }
1430
+ function compareVersions(a, b) {
1431
+ try {
1432
+ return compareReleaseVersions(a, b);
1433
+ } catch {
1434
+ return a < b ? -1 : a > b ? 1 : 0;
1435
+ }
1436
+ }
1437
+ function stalledBy(approved, deadlines, now = Date.now() / 1e3) {
1438
+ if (approved === null || approved.length === 0) return null;
1439
+ const newest = approved.reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
1440
+ let stall = null;
1441
+ for (const d of deadlines) {
1442
+ if (d.afterSecs <= now && compareVersions(newest, d.version) < 0) {
1443
+ if (stall === null || compareVersions(d.version, stall.version) > 0) stall = d;
1444
+ }
1445
+ }
1446
+ return stall;
1447
+ }
1448
+ function publisherBindingMessage(cluster, org, signerId, generation) {
1449
+ return `calimero-publisher-binding/v1
1450
+ ${cluster}
1451
+ ${org}
1452
+ ${signerId}
1453
+ ${generation}`;
1454
+ }
1455
+ function publisherBindingBytes(cluster, org, signerId, generation) {
1456
+ return new TextEncoder().encode(publisherBindingMessage(cluster, org, signerId, generation));
1457
+ }
1458
+ function consentReturn(href) {
1459
+ const url2 = typeof href === "string" ? new URL(href) : href;
1460
+ const requestId = url2.searchParams.get("request_id");
1461
+ const result = url2.searchParams.get("result");
1462
+ if (!requestId || result !== "approved" && result !== "declined") return null;
1463
+ return { requestId, result };
1464
+ }
1465
+ var RELEASE_VERIFIER = "https://mero-tee.vercel.app";
1466
+ function releaseNotesUrl(version) {
1467
+ return `https://github.com/calimero-network/mero-tee/releases/tag/mero-tee-v${encodeURIComponent(version)}`;
1468
+ }
1469
+ function verifyReleaseUrl(version) {
1470
+ return `${RELEASE_VERIFIER}/mero-tee?release_tag=${encodeURIComponent(`mero-tee-v${version}`)}`;
1471
+ }
1472
+ function pendingApprovalsFrom(report, rules, now = Date.now() / 1e3) {
1473
+ const deadlineOf = (v) => rules.deadlines.find((d) => d.version === v) ?? null;
1474
+ const releasesActionable = report.governance.releases === "owner";
1475
+ const upgradesActionable = report.governance.appUpgrades === "owner";
1476
+ const releases = report.clusterReleases.filter((r) => !r.approved).sort((a, b) => compareVersions(b.release, a.release)).map((r) => ({
1477
+ version: r.release,
1478
+ nodes: r.nodes,
1479
+ actionable: releasesActionable,
1480
+ releaseNotesUrl: releaseNotesUrl(r.release),
1481
+ verifyUrl: verifyReleaseUrl(r.release),
1482
+ deadline: deadlineOf(r.release)
1483
+ }));
1484
+ const upgrades = [];
1485
+ for (const a of report.apps) {
1486
+ const available = a.availableVersions ?? [];
1487
+ if (a.package === null || available.length === 0) continue;
1488
+ upgrades.push({
1489
+ app: a.app,
1490
+ name: a.name ?? null,
1491
+ package: a.package.package,
1492
+ from: a.package.version,
1493
+ to: available[available.length - 1],
1494
+ available,
1495
+ actionable: upgradesActionable
1496
+ });
1497
+ }
1498
+ return {
1499
+ scope: report.scope,
1500
+ governance: report.governance,
1501
+ approvedReleases: report.approvedReleases,
1502
+ releases,
1503
+ upgrades,
1504
+ stalledBy: stalledBy(report.approvedReleases, rules.deadlines, now),
1505
+ actionable: releases.some((r) => r.actionable) || upgrades.some((u) => u.actionable)
1506
+ };
1507
+ }
1508
+ function releaseConsent(scope, version, returnTo, options = {}) {
1509
+ return {
1510
+ kind: "release",
1511
+ scope: scope.kind === "org" ? { org: scope.org } : "account",
1512
+ subject: options.withdraw ? { version, withdraw: true } : { version },
1513
+ returnTo
1514
+ };
1515
+ }
1516
+ function withdrawConsent(scope, version, returnTo) {
1517
+ return releaseConsent(scope, version, returnTo, { withdraw: true });
1518
+ }
1519
+ function upgradeConsent(app, toVersion, returnTo) {
1520
+ return { kind: "upgrade", subject: { app, toVersion }, returnTo };
1521
+ }
1522
+
1398
1523
  // src/cloud-v2/client.ts
1399
1524
  function seg(value) {
1400
1525
  return encodeURIComponent(value);
@@ -1740,8 +1865,11 @@ var CalimeroClient = class {
1740
1865
  * Move an app to another release of its package (`{ version }`), another
1741
1866
  * package, or a bundle hash, running its migration. Answers the upgrade's
1742
1867
  * 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.
1868
+ * Where owners hold app upgrades, the node asks for the approval of
1869
+ * whoever the package's manifest names (`412 CONFIRM_REQUIRED`), which
1870
+ * `passkey` gives. Refused (`403`) to a session opened on an app's
1871
+ * frontend: an app asks its user with {@link requestConsent} instead. Past
1872
+ * a release deadline its approver has not met: {@link ReleaseDeadlineError}.
1745
1873
  */
1746
1874
  upgradeApp(app, target, passkey) {
1747
1875
  const path = `apps/${seg(app)}/upgrade`;
@@ -1914,8 +2042,11 @@ var CalimeroClient = class {
1914
2042
  /**
1915
2043
  * Approve a mero-tee release to hold the apps of `org`, or of this account
1916
2044
  * 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`.
2045
+ * Until then, nodes on that release get none of those apps' data. For an
2046
+ * organization, a member holding `APPROVE_RELEASES` approves with `passkey`;
2047
+ * the node binds the release's measurements itself. Refused (`403`) to a
2048
+ * session opened on an app's frontend: an app asks its user with
2049
+ * {@link requestConsent} instead.
1919
2050
  */
1920
2051
  async approveRelease(org, version, passkey, approved = true) {
1921
2052
  const path = org === null ? "auth/releases" : `orgs/${seg(org)}/releases`;
@@ -1926,7 +2057,8 @@ var CalimeroClient = class {
1926
2057
  * `org`'s apps or this account's own (`org` `null`). `prefixes` `'manifest'`
1927
2058
  * (the default) approves every prefix the package's latest release declares;
1928
2059
  * `replace` sets exactly `prefixes`, which is how one is revoked. Approved
1929
- * with `passkey`.
2060
+ * with `passkey`. Refused (`403`) to a session opened on an app's frontend:
2061
+ * an app asks with {@link requestConsent} (`kind: 'outbound'`) instead.
1930
2062
  */
1931
2063
  async approveOutbound(org, pkg, passkey, options = {}) {
1932
2064
  const path = org === null ? "auth/outbound" : `orgs/${seg(org)}/outbound`;
@@ -1936,6 +2068,137 @@ var CalimeroClient = class {
1936
2068
  passkey
1937
2069
  );
1938
2070
  }
2071
+ // ---------------------------------------------------------------- approvals
2072
+ /**
2073
+ * One page of the report of a scope's apps: an organization's (any member;
2074
+ * `404` to anyone else) or, with `org` `null`, the account's own outside any
2075
+ * organization (a session bound to an app's frontend: that package's apps).
2076
+ * The governance, the scope's approved releases, the releases the cluster's
2077
+ * nodes run and whether each is approved, and per app its release, newer
2078
+ * releases it may move to, last upgrade and replicas.
2079
+ */
2080
+ appsReport(org, page = {}) {
2081
+ const path = org === null ? "auth/apps" : `orgs/${seg(org)}/apps`;
2082
+ return this.get(`${path}${pageQuery(page)}`);
2083
+ }
2084
+ /** {@link appsReport}, every page, with the apps of all of them in `apps`. */
2085
+ async appsReportAll(org) {
2086
+ const first = await this.appsReport(org);
2087
+ const apps = [...first.apps];
2088
+ let next = first.next;
2089
+ while (next !== null) {
2090
+ const page = await this.appsReport(org, { after: next });
2091
+ apps.push(...page.apps);
2092
+ next = page.next;
2093
+ }
2094
+ return { ...first, apps, next: null };
2095
+ }
2096
+ /** `GET /releases`: the cluster's governance and every release deadline. Public. */
2097
+ releaseRules() {
2098
+ return this.get("releases");
2099
+ }
2100
+ /**
2101
+ * What waits for a decision in a scope (`org`, or the account's own apps
2102
+ * with `null`): mero-tee releases the cluster's nodes run that the scope has
2103
+ * not approved, apps with newer releases of their package, and whether a
2104
+ * passed deadline stops new work. Read over the sealed channel; act on an
2105
+ * item by sending the user to the consent page ({@link requestConsent},
2106
+ * `releaseConsent`, `upgradeConsent`).
2107
+ */
2108
+ async pendingApprovals(org = null) {
2109
+ const [report, rules] = await Promise.all([this.appsReportAll(org), this.releaseRules()]);
2110
+ return pendingApprovalsFrom(report, rules);
2111
+ }
2112
+ /**
2113
+ * Ask the node to ask the user: a release, upgrade or outbound approval
2114
+ * that only the account holder may make, decided on the node's own consent
2115
+ * page, never in the app. Needs a passkey session (a session bound to the
2116
+ * app's frontend is fine). Send the person to the answer's `url`; the page
2117
+ * sends them back to `returnTo?request_id=…&result=approved|declined`
2118
+ * (`consentReturn`), and {@link consentResult} confirms the decision.
2119
+ */
2120
+ requestConsent(request) {
2121
+ return this.post("consent/requests", request);
2122
+ }
2123
+ /**
2124
+ * {@link requestConsent}, then send the browser to the consent page
2125
+ * (`navigate`, by default `window.location.assign`). Answers the request
2126
+ * once `navigate` returns: with a popup (`url => window.open(url)`), poll it
2127
+ * with {@link waitForConsent}.
2128
+ */
2129
+ async openConsentPage(request, navigate = (url2) => {
2130
+ const w = globalThis.window;
2131
+ if (!w) throw new Error("no window to open the consent page in; pass `navigate`");
2132
+ w.location.assign(url2);
2133
+ }) {
2134
+ const started = await this.requestConsent(request);
2135
+ navigate(started.url);
2136
+ return started;
2137
+ }
2138
+ /** The decision on a consent request (any node answers, for the account that asked). */
2139
+ consentResult(requestId) {
2140
+ return this.get(`consent/${seg(requestId)}/result`);
2141
+ }
2142
+ /**
2143
+ * Poll {@link consentResult} until the request is decided, every
2144
+ * `intervalMs` (2 s), until `signal` aborts or `expiresAt` (Unix seconds)
2145
+ * passes, when it answers the last `pending` result.
2146
+ */
2147
+ async waitForConsent(requestId, options = {}) {
2148
+ const interval = options.intervalMs ?? 2e3;
2149
+ for (; ; ) {
2150
+ const result = await this.consentResult(requestId);
2151
+ if (result.status !== "pending") return result;
2152
+ if (options.signal?.aborted) return result;
2153
+ if (options.expiresAt !== void 0 && Date.now() / 1e3 >= options.expiresAt) return result;
2154
+ await new Promise((resolve) => setTimeout(resolve, interval));
2155
+ }
2156
+ }
2157
+ /** `GET /publishers/{signerId}`: the cluster name and binding generation a key signs to bind it next. */
2158
+ publisherBinding(signerId) {
2159
+ return this.get(`publishers/${seg(signerId)}`);
2160
+ }
2161
+ /**
2162
+ * Bind a package signing key to `org`, which then approves the node images
2163
+ * every instance of the key's packages runs on, and upgrades those whose
2164
+ * package declares `upgrades: publisher`. `proof` is the key's signature
2165
+ * over `publisherBindingMessage(cluster, org, signerId, generation)`; a
2166
+ * member holding `APPROVE_RELEASES` sends it with `passkey`.
2167
+ */
2168
+ bindPublisher(org, proof, passkey) {
2169
+ return this.postConfirmed(`orgs/${seg(org)}/publishers`, { ...proof }, passkey);
2170
+ }
2171
+ /**
2172
+ * Upgrade every instance of `pkg` whose release declares `upgrades:
2173
+ * publisher`, whoever owns it, as its publisher `org` (a
2174
+ * `MANAGE_APPLICATION` holder there), with one passkey approval. Answers
2175
+ * the run, kept on the publisher.
2176
+ */
2177
+ upgradePublished(org, pkg, target, passkey) {
2178
+ return this.postConfirmed(`orgs/${seg(org)}/packages/${seg(pkg)}/upgrade`, { ...target }, passkey);
2179
+ }
2180
+ // ---------------------------------------------------------------- operator
2181
+ /** `GET /ops/apps`: what every app runs, without names or accounts. Operator token (`useApiKey`). */
2182
+ opsApps(page = {}) {
2183
+ return this.get(`ops/apps${pageQuery(page)}`);
2184
+ }
2185
+ /** `GET /ops/orgs`: each organization's approved releases. Operator token. */
2186
+ opsOrgs(page = {}) {
2187
+ return this.get(`ops/orgs${pageQuery(page)}`);
2188
+ }
2189
+ /**
2190
+ * Announce when work on images older than `version` ends: from `after` on,
2191
+ * a scope that has not approved it (or newer) starts no new work. Operator
2192
+ * token.
2193
+ */
2194
+ async setReleaseDeadline(version, after) {
2195
+ const at = typeof after === "string" ? after : after.toISOString();
2196
+ await this.post(`ops/releases/${seg(version)}/deadline`, { after: at });
2197
+ }
2198
+ /** Lift a release's deadline. Operator token. */
2199
+ async liftReleaseDeadline(version) {
2200
+ await this.delete(`ops/releases/${seg(version)}/deadline`);
2201
+ }
1939
2202
  /** Add a member, or change their role. */
1940
2203
  async setMember(org, account, role) {
1941
2204
  await this.post(`orgs/${seg(org)}/members`, { account, role });
@@ -1995,7 +2258,7 @@ var ORG_CAPABILITIES = {
1995
2258
  JOIN_OPEN_TEAMS: 1 << 2,
1996
2259
  /** Add and remove a team's members. */
1997
2260
  MANAGE_MEMBERS: 1 << 3,
1998
- /** Upgrade the organization's apps, start org upgrade runs, approve outbound hosts (org-wide). */
2261
+ /** 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
2262
  MANAGE_APPLICATION: 1 << 4,
2000
2263
  /** Create a top-level team without being an admin. */
2001
2264
  CREATE_TEAM: 1 << 5,
@@ -2004,7 +2267,14 @@ var ORG_CAPABILITIES = {
2004
2267
  /** Change who may join a team (open or invite-only). */
2005
2268
  MANAGE_VISIBILITY: 1 << 7,
2006
2269
  /** Set the organization's and its teams' metadata. */
2007
- MANAGE_METADATA: 1 << 8
2270
+ MANAGE_METADATA: 1 << 8,
2271
+ /**
2272
+ * Approve (or withdraw) the mero-tee releases that hold the apps the
2273
+ * organization governs (those of the packages it publishes, and its own of
2274
+ * unbound packages), and bind a package signing key to it
2275
+ * (`bindPublisher`). Organization-wide only. Bit 9 is left unused.
2276
+ */
2277
+ APPROVE_RELEASES: 1 << 10
2008
2278
  };
2009
2279
  var DEFAULT_ORG_CAPABILITIES = ORG_CAPABILITIES.JOIN_OPEN_TEAMS;
2010
2280
  function orgCapabilities(...names) {
@@ -2455,6 +2725,7 @@ export {
2455
2725
  CalimeroError,
2456
2726
  ConfirmCancelled,
2457
2727
  ConfirmRequired,
2728
+ ConsentStale,
2458
2729
  DEFAULT_ORG_CAPABILITIES,
2459
2730
  DEFAULT_RELEASE_MIRROR,
2460
2731
  GITHUB_RELEASES,
@@ -2462,11 +2733,14 @@ export {
2462
2733
  IdempotencyMismatch,
2463
2734
  KMS_RELEASE_SIGNER,
2464
2735
  LoginRequired,
2736
+ MAX_REPORT_PAGE,
2465
2737
  MOCK_QUOTE_HEADER,
2466
2738
  NODE_RELEASE_SIGNER,
2467
2739
  NotFound,
2468
2740
  ORG_CAPABILITIES,
2469
2741
  PRODUCTION_PROFILE,
2742
+ RELEASE_VERIFIER,
2743
+ ReleaseDeadlineError,
2470
2744
  SEALED_CONTENT_TYPE,
2471
2745
  SEALED_PATH,
2472
2746
  SealedTransportError,
@@ -2481,6 +2755,8 @@ export {
2481
2755
  checkOwner,
2482
2756
  claimedRelease,
2483
2757
  cloudNodeReleaseUrl,
2758
+ compareVersions,
2759
+ consentReturn,
2484
2760
  createQuoteVerifier,
2485
2761
  createSealedFetch,
2486
2762
  createSignedReleaseVerifier,
@@ -2492,14 +2768,24 @@ export {
2492
2768
  generateOwnerKey,
2493
2769
  orgCapabilities,
2494
2770
  ownerPublicKey,
2771
+ pageQuery,
2772
+ pendingApprovalsFrom,
2495
2773
  provisionAgent,
2496
2774
  provisionInfo,
2775
+ publisherBindingBytes,
2776
+ publisherBindingMessage,
2497
2777
  releaseAgent,
2778
+ releaseConsent,
2779
+ releaseNotesUrl,
2780
+ stalledBy,
2498
2781
  transportKeyBinding,
2499
2782
  trustSignedRelease,
2500
2783
  trustedMeasurementsFromReleases,
2784
+ upgradeConsent,
2501
2785
  validateAgentId,
2786
+ verifyReleaseUrl,
2502
2787
  verifySignedAgentPolicy,
2503
- verifySignedNodeRelease
2788
+ verifySignedNodeRelease,
2789
+ withdrawConsent
2504
2790
  };
2505
2791
  //# sourceMappingURL=index.mjs.map