@calimero-network/mero-js 19.6.0 → 19.7.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
@@ -786,6 +786,41 @@ function createAuthApiClientFromHttpClient(httpClient, _config) {
786
786
  return new AuthApiClient(httpClient);
787
787
  }
788
788
 
789
+ // src/capabilities.ts
790
+ var CAPABILITIES = {
791
+ CAN_CREATE_CONTEXT: 1 << 0,
792
+ CAN_INVITE_MEMBERS: 1 << 1,
793
+ CAN_JOIN_OPEN_SUBGROUPS: 1 << 2,
794
+ MANAGE_MEMBERS: 1 << 3,
795
+ MANAGE_APPLICATION: 1 << 4,
796
+ CAN_CREATE_SUBGROUP: 1 << 5,
797
+ CAN_DELETE_SUBGROUP: 1 << 6,
798
+ CAN_MANAGE_VISIBILITY: 1 << 7,
799
+ CAN_MANAGE_METADATA: 1 << 8,
800
+ /**
801
+ * Publish writes attributed to ANOTHER member, under a warrant that member
802
+ * signed — the grant delegated execution runs on.
803
+ *
804
+ * Implied by nothing: not by membership, not by admin, and not propagated by
805
+ * the subgroup-admit cascade. So every group is authorship-closed until an
806
+ * admin sets this bit, and a relay without it is refused at
807
+ * `performIntent` before anything executes. That is why it is worth naming
808
+ * here rather than leaving callers to write `1 << 9`: a client granting it
809
+ * has to be able to say what it is granting.
810
+ */
811
+ CAN_AUTHOR_ON_BEHALF: 1 << 9
812
+ };
813
+ function hasCap(mask, cap) {
814
+ const capU32 = cap >>> 0;
815
+ return (mask & capU32) >>> 0 === capU32;
816
+ }
817
+ function withCap(mask, cap) {
818
+ return (mask | cap) >>> 0;
819
+ }
820
+ function withoutCap(mask, cap) {
821
+ return (mask & ~cap) >>> 0;
822
+ }
823
+
789
824
  // src/admin-api/admin-client.ts
790
825
  function unwrap(response) {
791
826
  return response?.data;
@@ -1544,6 +1579,85 @@ var AdminApiClient = class {
1544
1579
  async setDefaultCapabilities(groupId, request) {
1545
1580
  await this.httpClient.put(`/admin-api/groups/${groupId}/settings/default-capabilities`, request);
1546
1581
  }
1582
+ /**
1583
+ * Make every member admitted to `groupId` from now on able to relay a
1584
+ * delegated write, without a per-node governance op each time.
1585
+ *
1586
+ * `CAN_AUTHOR_ON_BEHALF` is implied by nothing — not membership, not admin —
1587
+ * so every group is authorship-closed and each fleet relay otherwise needs an
1588
+ * admin to grant it individually, at the moment the relay is assigned and the
1589
+ * admin is not watching. Setting it in the group's DEFAULT mask is core's own
1590
+ * answer to that: `add_member` seeds a non-admin's capability row from the
1591
+ * default, so an attested node admitted afterwards lands open.
1592
+ *
1593
+ * Call it on the NAMESPACE (the root group). A grant resolves through the
1594
+ * membership anchor, so one at the root reaches every Open subgroup beneath
1595
+ * it, which is where contexts actually live; a `Restricted` subgroup is a
1596
+ * membership boundary and needs its own call.
1597
+ *
1598
+ * ### Two things this does not do
1599
+ *
1600
+ * **It is not retroactive.** The default is copied into a member's row when
1601
+ * that member is admitted, so nodes already admitted keep the row they got.
1602
+ * Use {@link grantAuthorship} for those — or call this at namespace creation,
1603
+ * which is the only point that needs no backfill at all.
1604
+ *
1605
+ * **It does not reach admins.** Core seeds the default only for non-admin
1606
+ * roles, and `CAN_AUTHOR_ON_BEHALF` is deliberately not implied by admin. An
1607
+ * admin's own node therefore stays closed and needs {@link grantAuthorship}.
1608
+ *
1609
+ * ### What it widens
1610
+ *
1611
+ * Every future non-admin member of the group, not only attested TEE nodes, so
1612
+ * any of them may be named as a warrant's `executor`. The bound worth knowing
1613
+ * before deciding: an executor cannot FORGE. A delegated write is authorized
1614
+ * by the author's own warrant — signed by their device key, committed to one
1615
+ * context, method and exact arguments, with its nonce checked unspent — so a
1616
+ * holder can only spend a warrant it was deliberately handed. What it does
1617
+ * gain is the arguments in cleartext and the choice of when, or whether, to
1618
+ * publish.
1619
+ *
1620
+ * Read-modify-write rather than a blind set: core seeds a new group's default
1621
+ * with `CAN_JOIN_OPEN_SUBGROUPS`, and overwriting that would quietly stop
1622
+ * every later member inheriting into Open subgroups.
1623
+ *
1624
+ * Returns the resulting mask and whether an op was published — it is a no-op
1625
+ * when the bit is already set, so it is safe to call on every startup.
1626
+ */
1627
+ async openToDelegatedExecution(groupId) {
1628
+ const current = await this.getDefaultCapabilities(groupId);
1629
+ if (hasCap(current, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
1630
+ return { changed: false, defaultCapabilities: current };
1631
+ }
1632
+ const next = withCap(current, CAPABILITIES.CAN_AUTHOR_ON_BEHALF);
1633
+ await this.setDefaultCapabilities(groupId, { defaultCapabilities: next });
1634
+ return { changed: true, defaultCapabilities: next };
1635
+ }
1636
+ /**
1637
+ * Grant `account` the authorship bit on `groupId`, leaving its other
1638
+ * capabilities alone.
1639
+ *
1640
+ * The per-member counterpart to {@link openToDelegatedExecution}, and what
1641
+ * closes the gap that method cannot: a relay admitted BEFORE the default was
1642
+ * set, and an admin's own node, which never receives the default at all.
1643
+ *
1644
+ * `account` is the member's ACCOUNT id (64 hex) as `listGroupMembers` returns
1645
+ * it — for a fleet relay, the `account` the cloud reports for that node. Not
1646
+ * the signing key that added them; both are 64 hex, so nothing but the source
1647
+ * tells them apart.
1648
+ *
1649
+ * Read-modify-write, so it adds the bit rather than replacing the member's
1650
+ * mask. A no-op when the member already holds it.
1651
+ */
1652
+ async grantAuthorship(groupId, account) {
1653
+ const { capabilities } = await this.getMemberCapabilities(groupId, account);
1654
+ if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
1655
+ return { changed: false, capabilities };
1656
+ }
1657
+ const next = withCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF);
1658
+ await this.setMemberCapabilities(groupId, account, { capabilities: next });
1659
+ return { changed: true, capabilities: next };
1660
+ }
1547
1661
  async setSubgroupVisibility(groupId, request) {
1548
1662
  await this.httpClient.put(`/admin-api/groups/${groupId}/settings/subgroup-visibility`, request);
1549
1663
  }
@@ -4841,6 +4955,65 @@ var CloudClient = class {
4841
4955
  const relays = await this.getNamespaceRelays(namespaceId);
4842
4956
  return relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ?? null;
4843
4957
  }
4958
+ /**
4959
+ * Where to reach a namespace without owning it: one read, both verbs.
4960
+ *
4961
+ * **Anonymous.** A joiner is by construction not the namespace owner and holds
4962
+ * no cloud session — needing one is exactly what this path exists to avoid.
4963
+ * Discovery is not authorization: merod refuses an `/admit` call unless the
4964
+ * relaying node's account is on the invitation's signed `admitters` list, and
4965
+ * refuses an intent unless the warrant is genuinely the member's, so naming a
4966
+ * node grants the caller nothing it did not already have.
4967
+ *
4968
+ * A keyholder resolves a namespace to a node ONCE and then admits, reads and
4969
+ * posts intents against that same node, which is why admission and writability
4970
+ * are answered together rather than across two endpoints — the per-context
4971
+ * read needs a context id a joiner does not yet have.
4972
+ *
4973
+ * An empty `nodes` list is a state, not an error: a namespace with no fleet
4974
+ * assignment has no cloud node, and the joiner must use one of the
4975
+ * invitation's other admitters — another admin's node, or a self-hosted peer.
4976
+ */
4977
+ async getNamespaceRouting(namespaceId) {
4978
+ const body = await this.request("GET", `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
4979
+ anonymous: true
4980
+ });
4981
+ return {
4982
+ namespaceId: String(body.namespace_id ?? namespaceId),
4983
+ nodes: (body.admitters ?? []).map((row) => ({
4984
+ peerId: String(row.peer_id ?? ""),
4985
+ account: row.account ?? null,
4986
+ relayUrl: row.relay_url ?? null,
4987
+ admitUrl: row.admit_url ?? null,
4988
+ status: String(row.status ?? ""),
4989
+ fresh: row.fresh === true,
4990
+ canAdmit: row.can_admit === true,
4991
+ authorshipReady: row.authorship_ready === true,
4992
+ canExecute: row.can_execute === true
4993
+ })),
4994
+ servable: body.servable === true,
4995
+ writable: body.writable === true
4996
+ };
4997
+ }
4998
+ /**
4999
+ * The node to send a signed join to, chosen from those an invitation names.
5000
+ *
5001
+ * Pass the invitation's `admitters` and the intersection is done here, because
5002
+ * skipping it is the failure this method exists to prevent: the cloud lists
5003
+ * every node assigned to the namespace, while `admitters` is a snapshot signed
5004
+ * when the invitation was minted. A node added afterwards looks perfect in
5005
+ * every field here and answers 403.
5006
+ *
5007
+ * Omit `admitters` only for an invitation that names none — an empty list on
5008
+ * the wire authorises any node to admit.
5009
+ */
5010
+ async findAdmitter(namespaceId, admitters) {
5011
+ const { nodes } = await this.getNamespaceRouting(namespaceId);
5012
+ const allowed = admitters && admitters.length > 0 ? new Set(admitters.map((a) => a.toLowerCase())) : null;
5013
+ return nodes.find(
5014
+ (n) => n.canAdmit && (allowed === null || n.account !== null && allowed.has(n.account.toLowerCase()))
5015
+ ) ?? null;
5016
+ }
4844
5017
  /**
4845
5018
  * Every attested machine currently serving something this account owns.
4846
5019
  *
@@ -5833,7 +6006,7 @@ function explainNoRelay(namespace, relays) {
5833
6006
  }
5834
6007
  const waitingOnGrant = relays.filter((r) => !r.authorshipReady);
5835
6008
  if (waitingOnGrant.length === relays.length) {
5836
- 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"}`;
6009
+ 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`;
5837
6010
  }
5838
6011
  return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none reported both a URL and an executor account yet \u2014 retry shortly`;
5839
6012
  }
@@ -6120,41 +6293,6 @@ async function login(config) {
6120
6293
  };
6121
6294
  }
6122
6295
 
6123
- // src/capabilities.ts
6124
- var CAPABILITIES = {
6125
- CAN_CREATE_CONTEXT: 1 << 0,
6126
- CAN_INVITE_MEMBERS: 1 << 1,
6127
- CAN_JOIN_OPEN_SUBGROUPS: 1 << 2,
6128
- MANAGE_MEMBERS: 1 << 3,
6129
- MANAGE_APPLICATION: 1 << 4,
6130
- CAN_CREATE_SUBGROUP: 1 << 5,
6131
- CAN_DELETE_SUBGROUP: 1 << 6,
6132
- CAN_MANAGE_VISIBILITY: 1 << 7,
6133
- CAN_MANAGE_METADATA: 1 << 8,
6134
- /**
6135
- * Publish writes attributed to ANOTHER member, under a warrant that member
6136
- * signed — the grant delegated execution runs on.
6137
- *
6138
- * Implied by nothing: not by membership, not by admin, and not propagated by
6139
- * the subgroup-admit cascade. So every group is authorship-closed until an
6140
- * admin sets this bit, and a relay without it is refused at
6141
- * `performIntent` before anything executes. That is why it is worth naming
6142
- * here rather than leaving callers to write `1 << 9`: a client granting it
6143
- * has to be able to say what it is granting.
6144
- */
6145
- CAN_AUTHOR_ON_BEHALF: 1 << 9
6146
- };
6147
- function hasCap(mask, cap) {
6148
- const capU32 = cap >>> 0;
6149
- return (mask & capU32) >>> 0 === capU32;
6150
- }
6151
- function withCap(mask, cap) {
6152
- return (mask | cap) >>> 0;
6153
- }
6154
- function withoutCap(mask, cap) {
6155
- return (mask & ~cap) >>> 0;
6156
- }
6157
-
6158
6296
  // src/nodeDiscovery.ts
6159
6297
  var DEFAULT_LOCAL_NODE_PORTS = [2428, 2429, 2528, 2529];
6160
6298
  var LOCAL_HOST = "localhost";