@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/admin-api/admin-client.d.ts +69 -0
- package/dist/admin-api/admin-client.js +80 -0
- package/dist/cloud/cloud-client.d.ts +89 -0
- package/dist/cloud/cloud-client.js +58 -0
- package/dist/cloud/connect.js +8 -1
- package/dist/cloud/index.d.ts +1 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +174 -36
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +174 -36
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
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";
|