@calimero-network/mero-js 22.1.0 → 22.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
@@ -5379,6 +5379,15 @@ function base642(bytes) {
5379
5379
 
5380
5380
  // src/cloud/cloud-client.ts
5381
5381
  var DEFAULT_CLOUD_BASE_URL = "https://cloud.calimero.network";
5382
+ function canExecuteOf(row, legacy) {
5383
+ return typeof row.can_execute === "boolean" ? row.can_execute : legacy;
5384
+ }
5385
+ function teeRoleOf(row) {
5386
+ return typeof row.tee_role === "string" && row.tee_role !== "" ? row.tee_role : null;
5387
+ }
5388
+ function pickExecutingRelay(relays) {
5389
+ return relays.find((r) => r.canExecute && r.relayUrl !== null && r.executorAccount !== null) ?? null;
5390
+ }
5382
5391
  var REFRESH_HEADER = "x-mdma-session-refresh";
5383
5392
  var REFRESH_EXPIRES_HEADER = "x-mdma-session-expires";
5384
5393
  var CloudClient = class {
@@ -5527,37 +5536,48 @@ var CloudClient = class {
5527
5536
  * The relays serving one namespace: where to present an intent, and as whom.
5528
5537
  *
5529
5538
  * Everything a client needs in order to mint a warrant it can actually
5530
- * spend. Filter on `authorshipReady` before signing — a warrant presented to
5531
- * a relay with no grant is refused *after* the author has burned a nonce from
5532
- * its monotonic sequence on it.
5539
+ * spend. Filter on `canExecute` before signing — a warrant presented to a
5540
+ * relay that does not relay (a `ReadOnlyTee` replica, or a node with no role
5541
+ * and no grant) is refused *after* the author has burned a nonce from its
5542
+ * monotonic sequence on it.
5533
5543
  */
5534
5544
  async getNamespaceRelays(namespaceId) {
5535
5545
  const body = await this.request(
5536
5546
  "GET",
5537
5547
  `/api/cloud/me/namespaces/${encodeURIComponent(namespaceId)}/relays`
5538
5548
  );
5539
- return (body.relays ?? []).map((row) => ({
5540
- peerId: String(row.peer_id ?? ""),
5541
- relayUrl: row.relay_url ?? null,
5542
- executorAccount: row.executor_account ?? null,
5543
- status: String(row.status ?? ""),
5544
- authorshipReady: row.authorship_ready === true,
5545
- lastSeenAt: row.last_seen_at ?? null,
5546
- confirmedAt: row.confirmed_at ?? null
5547
- }));
5549
+ return (body.relays ?? []).map((row) => {
5550
+ const relayUrl = row.relay_url ?? null;
5551
+ const executorAccount = row.executor_account ?? null;
5552
+ const authorshipReady = row.authorship_ready === true;
5553
+ return {
5554
+ peerId: String(row.peer_id ?? ""),
5555
+ relayUrl,
5556
+ executorAccount,
5557
+ status: String(row.status ?? ""),
5558
+ authorshipReady,
5559
+ teeRole: teeRoleOf(row),
5560
+ canExecute: canExecuteOf(
5561
+ row,
5562
+ authorshipReady && relayUrl !== null && executorAccount !== null
5563
+ ),
5564
+ lastSeenAt: row.last_seen_at ?? null,
5565
+ confirmedAt: row.confirmed_at ?? null
5566
+ };
5567
+ });
5548
5568
  }
5549
5569
  /**
5550
5570
  * The first relay for `namespaceId` that can actually run an intent, or
5551
5571
  * `null`.
5552
5572
  *
5553
- * "Can actually run one" is three conditions, and a client that checks fewer
5554
- * gets a refusal it cannot explain: the cloud must know a URL, the relay must
5555
- * have reported the executor account a warrant has to name, and the grant
5556
- * must be in place.
5573
+ * "Can actually run one" is several conditions, and a client that checks
5574
+ * fewer gets a refusal it cannot explain: the cloud must know a URL, the
5575
+ * relay must have reported the executor account a warrant has to name, and
5576
+ * the node must relay — which the cloud folds into `canExecute`.
5557
5577
  */
5558
5578
  async findExecutingRelay(namespaceId) {
5559
5579
  const relays = await this.getNamespaceRelays(namespaceId);
5560
- return relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ?? null;
5580
+ return pickExecutingRelay(relays);
5561
5581
  }
5562
5582
  /**
5563
5583
  * Where to reach a namespace without owning it: one read, both verbs.
@@ -5585,17 +5605,27 @@ var CloudClient = class {
5585
5605
  });
5586
5606
  return {
5587
5607
  namespaceId: String(body.namespace_id ?? namespaceId),
5588
- nodes: (body.admitters ?? []).map((row) => ({
5589
- peerId: String(row.peer_id ?? ""),
5590
- account: row.account ?? null,
5591
- relayUrl: row.relay_url ?? null,
5592
- admitUrl: row.admit_url ?? null,
5593
- status: String(row.status ?? ""),
5594
- fresh: row.fresh === true,
5595
- canAdmit: row.can_admit === true,
5596
- authorshipReady: row.authorship_ready === true,
5597
- canExecute: row.can_execute === true
5598
- })),
5608
+ nodes: (body.admitters ?? []).map((row) => {
5609
+ const account = row.account ?? null;
5610
+ const relayUrl = row.relay_url ?? null;
5611
+ const fresh = row.fresh === true;
5612
+ const authorshipReady = row.authorship_ready === true;
5613
+ return {
5614
+ peerId: String(row.peer_id ?? ""),
5615
+ account,
5616
+ relayUrl,
5617
+ admitUrl: row.admit_url ?? null,
5618
+ status: String(row.status ?? ""),
5619
+ fresh,
5620
+ canAdmit: row.can_admit === true,
5621
+ authorshipReady,
5622
+ teeRole: teeRoleOf(row),
5623
+ canExecute: canExecuteOf(
5624
+ row,
5625
+ authorshipReady && relayUrl !== null && account !== null && fresh
5626
+ )
5627
+ };
5628
+ }),
5599
5629
  servable: body.servable === true,
5600
5630
  writable: body.writable === true
5601
5631
  };
@@ -5726,23 +5756,32 @@ var CloudClient = class {
5726
5756
  "GET",
5727
5757
  "/api/cloud/me/machines"
5728
5758
  );
5729
- return (body.machines ?? []).map((row) => ({
5730
- peerId: String(row.peer_id ?? ""),
5731
- relayUrl: row.relay_url ?? null,
5732
- executorAccount: row.executor_account ?? null,
5733
- canExecute: row.can_execute === true,
5734
- namespaces: (Array.isArray(row.namespaces) ? row.namespaces : []).map((n) => {
5759
+ return (body.machines ?? []).map((row) => {
5760
+ const relayUrl = row.relay_url ?? null;
5761
+ const executorAccount = row.executor_account ?? null;
5762
+ const namespaces = (Array.isArray(row.namespaces) ? row.namespaces : []).map((n) => {
5735
5763
  const ns = n;
5736
5764
  return {
5737
5765
  namespaceId: String(ns.namespace_id ?? ""),
5738
5766
  status: String(ns.status ?? ""),
5739
5767
  authorshipReady: ns.authorship_ready === true,
5768
+ teeRole: teeRoleOf(ns),
5740
5769
  fresh: ns.fresh === true,
5741
5770
  confirmedAt: ns.confirmed_at ?? null,
5742
5771
  lastSeenAt: ns.last_seen_at ?? null
5743
5772
  };
5744
- })
5745
- }));
5773
+ });
5774
+ return {
5775
+ peerId: String(row.peer_id ?? ""),
5776
+ relayUrl,
5777
+ executorAccount,
5778
+ canExecute: canExecuteOf(
5779
+ row,
5780
+ relayUrl !== null && executorAccount !== null && namespaces.some((n) => n.authorshipReady)
5781
+ ),
5782
+ namespaces
5783
+ };
5784
+ });
5746
5785
  }
5747
5786
  /**
5748
5787
  * Claim a namespace under this account, proving ownership.
@@ -7282,12 +7321,12 @@ async function connectCloud(options) {
7282
7321
  const namespaces = await cloud.getMyNamespaces();
7283
7322
  const namespace = pickNamespace(namespaces, options.namespaceId);
7284
7323
  const relays = await cloud.getNamespaceRelays(namespace.namespaceId);
7285
- const relayInfo = relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ?? null;
7324
+ const relayInfo = pickExecutingRelay(relays);
7286
7325
  if (!relayInfo) {
7287
7326
  throw new Error(explainNoRelay(namespace, relays));
7288
7327
  }
7289
7328
  const relay = new RelayClient({
7290
- // Checked by `findExecutingRelay`, which is the only thing that returns a
7329
+ // Checked by `pickExecutingRelay`, which is the only thing that returns a
7291
7330
  // relay — narrowed here rather than re-validated, so the invariant lives in
7292
7331
  // one place.
7293
7332
  relayUrl: relayInfo.relayUrl,
@@ -7341,11 +7380,15 @@ function explainNoRelay(namespace, relays) {
7341
7380
  if (relays.length === 0) {
7342
7381
  return namespace.haStatus === "enabled" ? `namespace ${namespace.namespaceId} has HA enabled but no relay has been assigned to it yet \u2014 retry shortly` : `namespace ${namespace.namespaceId} has no relays: enable HA on it in the cloud to have an attested node assigned`;
7343
7382
  }
7344
- const waitingOnGrant = relays.filter((r) => !r.authorshipReady);
7345
- if (waitingOnGrant.length === relays.length) {
7383
+ const replicas = relays.filter((r) => r.teeRole !== null && r.teeRole !== "RelayTee");
7384
+ if (replicas.length === relays.length) {
7385
+ return `every node on namespace ${namespace.namespaceId} is a read-only TEE replica (ReadOnlyTee), which never relays delegated writes: an admin of the namespace must set its TEE admission policy with mode: 'relay' \u2014 AdminClient.setTeeAdmissionPolicy(namespaceId, { ..., mode: 'relay' }) \u2014 so fleet nodes are admitted as RelayTee`;
7386
+ }
7387
+ const waitingOnGrant = relays.filter((r) => r.teeRole === null && !r.authorshipReady);
7388
+ if (waitingOnGrant.length > 0 && waitingOnGrant.length + replicas.length === relays.length) {
7346
7389
  return `every relay on namespace ${namespace.namespaceId} is waiting for the authorship grant: an admin of the namespace must grant CAN_AUTHOR_ON_BEHALF to ${waitingOnGrant.map((r) => r.executorAccount ?? r.peerId).join(", ") || "the relay account"} \u2014 with AdminClient.grantAuthorship(namespaceId, account) for these relays, and AdminClient.openToDelegatedExecution(namespaceId) so relays assigned later need no further op`;
7347
7390
  }
7348
- return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none reported both a URL and an executor account yet \u2014 retry shortly`;
7391
+ return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none is ready to relay yet (a URL, an executor account and a recent heartbeat) \u2014 retry shortly`;
7349
7392
  }
7350
7393
  async function defaultNonceSource(deviceSecret) {
7351
7394
  const key = `mero-js:warrant-nonce:${hex(await derivePublicKey(deviceSecret))}`;