@calimero-network/mero-js 19.6.0 → 19.7.1

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
@@ -697,6 +697,41 @@ function createAuthApiClientFromHttpClient(httpClient, _config) {
697
697
  return new AuthApiClient(httpClient);
698
698
  }
699
699
 
700
+ // src/capabilities.ts
701
+ var CAPABILITIES = {
702
+ CAN_CREATE_CONTEXT: 1 << 0,
703
+ CAN_INVITE_MEMBERS: 1 << 1,
704
+ CAN_JOIN_OPEN_SUBGROUPS: 1 << 2,
705
+ MANAGE_MEMBERS: 1 << 3,
706
+ MANAGE_APPLICATION: 1 << 4,
707
+ CAN_CREATE_SUBGROUP: 1 << 5,
708
+ CAN_DELETE_SUBGROUP: 1 << 6,
709
+ CAN_MANAGE_VISIBILITY: 1 << 7,
710
+ CAN_MANAGE_METADATA: 1 << 8,
711
+ /**
712
+ * Publish writes attributed to ANOTHER member, under a warrant that member
713
+ * signed — the grant delegated execution runs on.
714
+ *
715
+ * Implied by nothing: not by membership, not by admin, and not propagated by
716
+ * the subgroup-admit cascade. So every group is authorship-closed until an
717
+ * admin sets this bit, and a relay without it is refused at
718
+ * `performIntent` before anything executes. That is why it is worth naming
719
+ * here rather than leaving callers to write `1 << 9`: a client granting it
720
+ * has to be able to say what it is granting.
721
+ */
722
+ CAN_AUTHOR_ON_BEHALF: 1 << 9
723
+ };
724
+ function hasCap(mask, cap) {
725
+ const capU32 = cap >>> 0;
726
+ return (mask & capU32) >>> 0 === capU32;
727
+ }
728
+ function withCap(mask, cap) {
729
+ return (mask | cap) >>> 0;
730
+ }
731
+ function withoutCap(mask, cap) {
732
+ return (mask & ~cap) >>> 0;
733
+ }
734
+
700
735
  // src/admin-api/admin-client.ts
701
736
  function unwrap(response) {
702
737
  return response?.data;
@@ -1455,6 +1490,85 @@ var AdminApiClient = class {
1455
1490
  async setDefaultCapabilities(groupId, request) {
1456
1491
  await this.httpClient.put(`/admin-api/groups/${groupId}/settings/default-capabilities`, request);
1457
1492
  }
1493
+ /**
1494
+ * Make every member admitted to `groupId` from now on able to relay a
1495
+ * delegated write, without a per-node governance op each time.
1496
+ *
1497
+ * `CAN_AUTHOR_ON_BEHALF` is implied by nothing — not membership, not admin —
1498
+ * so every group is authorship-closed and each fleet relay otherwise needs an
1499
+ * admin to grant it individually, at the moment the relay is assigned and the
1500
+ * admin is not watching. Setting it in the group's DEFAULT mask is core's own
1501
+ * answer to that: `add_member` seeds a non-admin's capability row from the
1502
+ * default, so an attested node admitted afterwards lands open.
1503
+ *
1504
+ * Call it on the NAMESPACE (the root group). A grant resolves through the
1505
+ * membership anchor, so one at the root reaches every Open subgroup beneath
1506
+ * it, which is where contexts actually live; a `Restricted` subgroup is a
1507
+ * membership boundary and needs its own call.
1508
+ *
1509
+ * ### Two things this does not do
1510
+ *
1511
+ * **It is not retroactive.** The default is copied into a member's row when
1512
+ * that member is admitted, so nodes already admitted keep the row they got.
1513
+ * Use {@link grantAuthorship} for those — or call this at namespace creation,
1514
+ * which is the only point that needs no backfill at all.
1515
+ *
1516
+ * **It does not reach admins.** Core seeds the default only for non-admin
1517
+ * roles, and `CAN_AUTHOR_ON_BEHALF` is deliberately not implied by admin. An
1518
+ * admin's own node therefore stays closed and needs {@link grantAuthorship}.
1519
+ *
1520
+ * ### What it widens
1521
+ *
1522
+ * Every future non-admin member of the group, not only attested TEE nodes, so
1523
+ * any of them may be named as a warrant's `executor`. The bound worth knowing
1524
+ * before deciding: an executor cannot FORGE. A delegated write is authorized
1525
+ * by the author's own warrant — signed by their device key, committed to one
1526
+ * context, method and exact arguments, with its nonce checked unspent — so a
1527
+ * holder can only spend a warrant it was deliberately handed. What it does
1528
+ * gain is the arguments in cleartext and the choice of when, or whether, to
1529
+ * publish.
1530
+ *
1531
+ * Read-modify-write rather than a blind set: core seeds a new group's default
1532
+ * with `CAN_JOIN_OPEN_SUBGROUPS`, and overwriting that would quietly stop
1533
+ * every later member inheriting into Open subgroups.
1534
+ *
1535
+ * Returns the resulting mask and whether an op was published — it is a no-op
1536
+ * when the bit is already set, so it is safe to call on every startup.
1537
+ */
1538
+ async openToDelegatedExecution(groupId) {
1539
+ const current = await this.getDefaultCapabilities(groupId);
1540
+ if (hasCap(current, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
1541
+ return { changed: false, defaultCapabilities: current };
1542
+ }
1543
+ const next = withCap(current, CAPABILITIES.CAN_AUTHOR_ON_BEHALF);
1544
+ await this.setDefaultCapabilities(groupId, { defaultCapabilities: next });
1545
+ return { changed: true, defaultCapabilities: next };
1546
+ }
1547
+ /**
1548
+ * Grant `account` the authorship bit on `groupId`, leaving its other
1549
+ * capabilities alone.
1550
+ *
1551
+ * The per-member counterpart to {@link openToDelegatedExecution}, and what
1552
+ * closes the gap that method cannot: a relay admitted BEFORE the default was
1553
+ * set, and an admin's own node, which never receives the default at all.
1554
+ *
1555
+ * `account` is the member's ACCOUNT id (64 hex) as `listGroupMembers` returns
1556
+ * it — for a fleet relay, the `account` the cloud reports for that node. Not
1557
+ * the signing key that added them; both are 64 hex, so nothing but the source
1558
+ * tells them apart.
1559
+ *
1560
+ * Read-modify-write, so it adds the bit rather than replacing the member's
1561
+ * mask. A no-op when the member already holds it.
1562
+ */
1563
+ async grantAuthorship(groupId, account) {
1564
+ const { capabilities } = await this.getMemberCapabilities(groupId, account);
1565
+ if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
1566
+ return { changed: false, capabilities };
1567
+ }
1568
+ const next = withCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF);
1569
+ await this.setMemberCapabilities(groupId, account, { capabilities: next });
1570
+ return { changed: true, capabilities: next };
1571
+ }
1458
1572
  async setSubgroupVisibility(groupId, request) {
1459
1573
  await this.httpClient.put(`/admin-api/groups/${groupId}/settings/subgroup-visibility`, request);
1460
1574
  }
@@ -4752,6 +4866,65 @@ var CloudClient = class {
4752
4866
  const relays = await this.getNamespaceRelays(namespaceId);
4753
4867
  return relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ?? null;
4754
4868
  }
4869
+ /**
4870
+ * Where to reach a namespace without owning it: one read, both verbs.
4871
+ *
4872
+ * **Anonymous.** A joiner is by construction not the namespace owner and holds
4873
+ * no cloud session — needing one is exactly what this path exists to avoid.
4874
+ * Discovery is not authorization: merod refuses an `/admit` call unless the
4875
+ * relaying node's account is on the invitation's signed `admitters` list, and
4876
+ * refuses an intent unless the warrant is genuinely the member's, so naming a
4877
+ * node grants the caller nothing it did not already have.
4878
+ *
4879
+ * A keyholder resolves a namespace to a node ONCE and then admits, reads and
4880
+ * posts intents against that same node, which is why admission and writability
4881
+ * are answered together rather than across two endpoints — the per-context
4882
+ * read needs a context id a joiner does not yet have.
4883
+ *
4884
+ * An empty `nodes` list is a state, not an error: a namespace with no fleet
4885
+ * assignment has no cloud node, and the joiner must use one of the
4886
+ * invitation's other admitters — another admin's node, or a self-hosted peer.
4887
+ */
4888
+ async getNamespaceRouting(namespaceId) {
4889
+ const body = await this.request("GET", `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
4890
+ anonymous: true
4891
+ });
4892
+ return {
4893
+ namespaceId: String(body.namespace_id ?? namespaceId),
4894
+ nodes: (body.admitters ?? []).map((row) => ({
4895
+ peerId: String(row.peer_id ?? ""),
4896
+ account: row.account ?? null,
4897
+ relayUrl: row.relay_url ?? null,
4898
+ admitUrl: row.admit_url ?? null,
4899
+ status: String(row.status ?? ""),
4900
+ fresh: row.fresh === true,
4901
+ canAdmit: row.can_admit === true,
4902
+ authorshipReady: row.authorship_ready === true,
4903
+ canExecute: row.can_execute === true
4904
+ })),
4905
+ servable: body.servable === true,
4906
+ writable: body.writable === true
4907
+ };
4908
+ }
4909
+ /**
4910
+ * The node to send a signed join to, chosen from those an invitation names.
4911
+ *
4912
+ * Pass the invitation's `admitters` and the intersection is done here, because
4913
+ * skipping it is the failure this method exists to prevent: the cloud lists
4914
+ * every node assigned to the namespace, while `admitters` is a snapshot signed
4915
+ * when the invitation was minted. A node added afterwards looks perfect in
4916
+ * every field here and answers 403.
4917
+ *
4918
+ * Omit `admitters` only for an invitation that names none — an empty list on
4919
+ * the wire authorises any node to admit.
4920
+ */
4921
+ async findAdmitter(namespaceId, admitters) {
4922
+ const { nodes } = await this.getNamespaceRouting(namespaceId);
4923
+ const allowed = admitters && admitters.length > 0 ? new Set(admitters.map((a) => a.toLowerCase())) : null;
4924
+ return nodes.find(
4925
+ (n) => n.canAdmit && (allowed === null || n.account !== null && allowed.has(n.account.toLowerCase()))
4926
+ ) ?? null;
4927
+ }
4755
4928
  /**
4756
4929
  * Every attested machine currently serving something this account owns.
4757
4930
  *
@@ -5744,7 +5917,7 @@ function explainNoRelay(namespace, relays) {
5744
5917
  }
5745
5918
  const waitingOnGrant = relays.filter((r) => !r.authorshipReady);
5746
5919
  if (waitingOnGrant.length === relays.length) {
5747
- 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"}`;
5920
+ 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`;
5748
5921
  }
5749
5922
  return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none reported both a URL and an executor account yet \u2014 retry shortly`;
5750
5923
  }
@@ -6031,41 +6204,6 @@ async function login(config) {
6031
6204
  };
6032
6205
  }
6033
6206
 
6034
- // src/capabilities.ts
6035
- var CAPABILITIES = {
6036
- CAN_CREATE_CONTEXT: 1 << 0,
6037
- CAN_INVITE_MEMBERS: 1 << 1,
6038
- CAN_JOIN_OPEN_SUBGROUPS: 1 << 2,
6039
- MANAGE_MEMBERS: 1 << 3,
6040
- MANAGE_APPLICATION: 1 << 4,
6041
- CAN_CREATE_SUBGROUP: 1 << 5,
6042
- CAN_DELETE_SUBGROUP: 1 << 6,
6043
- CAN_MANAGE_VISIBILITY: 1 << 7,
6044
- CAN_MANAGE_METADATA: 1 << 8,
6045
- /**
6046
- * Publish writes attributed to ANOTHER member, under a warrant that member
6047
- * signed — the grant delegated execution runs on.
6048
- *
6049
- * Implied by nothing: not by membership, not by admin, and not propagated by
6050
- * the subgroup-admit cascade. So every group is authorship-closed until an
6051
- * admin sets this bit, and a relay without it is refused at
6052
- * `performIntent` before anything executes. That is why it is worth naming
6053
- * here rather than leaving callers to write `1 << 9`: a client granting it
6054
- * has to be able to say what it is granting.
6055
- */
6056
- CAN_AUTHOR_ON_BEHALF: 1 << 9
6057
- };
6058
- function hasCap(mask, cap) {
6059
- const capU32 = cap >>> 0;
6060
- return (mask & capU32) >>> 0 === capU32;
6061
- }
6062
- function withCap(mask, cap) {
6063
- return (mask | cap) >>> 0;
6064
- }
6065
- function withoutCap(mask, cap) {
6066
- return (mask & ~cap) >>> 0;
6067
- }
6068
-
6069
6207
  // src/nodeDiscovery.ts
6070
6208
  var DEFAULT_LOCAL_NODE_PORTS = [2428, 2429, 2528, 2529];
6071
6209
  var LOCAL_HOST = "localhost";