@calimero-network/mero-js 22.9.0 → 23.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.
@@ -36,13 +36,22 @@ const GROUP_OP = {
36
36
  MemberRemoved: 2,
37
37
  MemberLeft: 3,
38
38
  MemberRoleSet: 4,
39
+ MemberCapabilitySet: 5,
39
40
  DefaultCapabilitiesSet: 6,
41
+ ContextDetached: 9,
42
+ SubgroupVisibilitySet: 10,
43
+ GroupMetadataSet: 11,
44
+ MemberMetadataSet: 12,
45
+ ContextMetadataSet: 13,
46
+ ContextCapabilityGranted: 16,
47
+ ContextCapabilityRevoked: 17,
40
48
  };
41
49
  /** `RootOp` discriminants, by position in core's enum. */
42
50
  const ROOT_OP = {
43
51
  GroupCreated: 0,
44
52
  GroupReparented: 1,
45
53
  GroupDeleted: 2,
54
+ MemberJoinedOpen: 7,
46
55
  NamespaceCreatedV2: 9,
47
56
  };
48
57
  /** core's `NAMESPACE_ID_DOMAIN`, which a founded namespace id is hashed under. */
@@ -100,6 +109,26 @@ export function memberRoleSetOp(member, memberRole) {
100
109
  bytes: concat(tag(GROUP_OP.MemberRoleSet), fromHex(member, 'member', 32), role(memberRole)),
101
110
  };
102
111
  }
112
+ /** Domain of core's `created_subgroup_id`. */
113
+ const SUBGROUP_ID_DOMAIN = new TextEncoder().encode('calimero.subgroup.id.v1');
114
+ /**
115
+ * The id `admin` derives for a subgroup under `parentId` with birth visibility
116
+ * `restricted` and `salt` (all hex), as core's `created_subgroup_id` does:
117
+ * `domain_hash("calimero.subgroup.id.v1", [admin, parentId, [restricted], salt])`.
118
+ *
119
+ * A subgroup's id is bound to its create, so nobody but its creator can name
120
+ * it: a node refuses a `GroupCreated` whose fields do not reproduce its id.
121
+ * That is what stops a second member racing a concurrent create for an id it
122
+ * saw gossiped.
123
+ */
124
+ export async function createdSubgroupId(admin, parentId, restricted, salt) {
125
+ return hex(await domainHash(SUBGROUP_ID_DOMAIN, [
126
+ fromHex(admin, 'admin', 32),
127
+ fromHex(parentId, 'parentId', 32),
128
+ new Uint8Array([restricted ? 1 : 0]),
129
+ fromHex(salt, 'salt', 32),
130
+ ]));
131
+ }
103
132
  /**
104
133
  * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
105
134
  * the capabilities every member holds without a per-member grant. Needs admin.
@@ -130,9 +159,18 @@ export function defaultCapabilitiesSetOp(capabilities) {
130
159
  export function groupCreatedOp(input) {
131
160
  return {
132
161
  kind: 'root',
133
- bytes: concat(tag(ROOT_OP.GroupCreated), fromHex(input.groupId, 'groupId', 32), fromHex(input.parentId, 'parentId', 32), tag(input.restricted ? 1 : 0), fromHex(input.admin, 'admin', 32)),
162
+ bytes: concat(tag(ROOT_OP.GroupCreated), fromHex(input.groupId, 'groupId', 32), fromHex(input.parentId, 'parentId', 32), tag(input.restricted ? 1 : 0), fromHex(input.admin, 'admin', 32), fromHex(input.salt, 'salt', 32)),
134
163
  };
135
164
  }
165
+ /**
166
+ * Create a subgroup under `parentId`: draws a fresh salt (unless one is given),
167
+ * derives the id with {@link createdSubgroupId}, and encodes the op.
168
+ */
169
+ export async function subgroupCreation(input) {
170
+ const salt = input.salt ?? hex(crypto.getRandomValues(new Uint8Array(32)));
171
+ const groupId = await createdSubgroupId(input.admin, input.parentId, input.restricted, salt);
172
+ return { groupId, salt, op: groupCreatedOp({ ...input, groupId, salt }) };
173
+ }
136
174
  /** Move `childGroupId` under `newParentId`, a root op. */
137
175
  export function groupReparentedOp(childGroupId, newParentId) {
138
176
  return {
@@ -183,3 +221,109 @@ export function namespaceCreatedOp(input) {
183
221
  bytes: concat(tag(ROOT_OP.NamespaceCreatedV2), fromHex(input.founder, 'founder', 32), credential, fromHex(input.salt, 'salt', 32)),
184
222
  };
185
223
  }
224
+ // ---------------------------------------------------------------------------
225
+ // The rest of the delegable set. Layouts are pinned against core's
226
+ // `delegable_governance_op_vectors_for_non_rust_encoders_are_stable`
227
+ // (crates/governance-types/src/tests.rs, core#4272).
228
+ /** A borsh `String`: a u32 byte length, then the UTF-8 bytes. */
229
+ function str(value) {
230
+ const bytes = new TextEncoder().encode(value);
231
+ return concat(u32le(bytes.length), bytes);
232
+ }
233
+ /** A borsh `Option<String>`: 0, or 1 then the string. */
234
+ function optionalStr(value) {
235
+ return value === undefined ? tag(0) : concat(tag(1), str(value));
236
+ }
237
+ /**
238
+ * A borsh `BTreeMap<String, String>`: a u32 count, then each entry in the
239
+ * map's own order, which is by key BYTES (not JS string order).
240
+ */
241
+ function stringMap(data = {}) {
242
+ const enc = new TextEncoder();
243
+ const entries = Object.entries(data)
244
+ .map(([k, v]) => [enc.encode(k), v, k])
245
+ .sort(([a], [b]) => {
246
+ for (let i = 0; i < Math.min(a.length, b.length); i++)
247
+ if (a[i] !== b[i])
248
+ return a[i] - b[i];
249
+ return a.length - b.length;
250
+ });
251
+ return concat(u32le(entries.length), ...entries.flatMap(([, v, k]) => [str(k), str(v)]));
252
+ }
253
+ /** Validate a u32 capability mask a relay may carry: never bit 9. */
254
+ function relayableMask(capabilities, what) {
255
+ if (!Number.isInteger(capabilities) || capabilities < 0 || capabilities > 4294967295) {
256
+ throw new Error(`${what} must be a u32 bit mask, got ${String(capabilities)}`);
257
+ }
258
+ if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
259
+ throw new Error(`${what} may not include CAN_AUTHOR_ON_BEHALF (512): a relay never carries a change to it`);
260
+ }
261
+ return capabilities;
262
+ }
263
+ /** Set `member`'s capabilities in the group (`GroupOp::MemberCapabilitySet`). Bit 9 is refused. */
264
+ export function memberCapabilitySetOp(member, capabilities) {
265
+ return {
266
+ kind: 'group',
267
+ bytes: concat(tag(GROUP_OP.MemberCapabilitySet), fromHex(member, 'member', 32), u32le(relayableMask(capabilities, 'capabilities'))),
268
+ };
269
+ }
270
+ /** Detach a context from the group (`GroupOp::ContextDetached`). */
271
+ export function contextDetachedOp(contextId) {
272
+ return { kind: 'group', bytes: concat(tag(GROUP_OP.ContextDetached), fromHex(contextId, 'contextId', 32)) };
273
+ }
274
+ /** Make a subgroup open (members of the parent may join it themselves) or restricted. */
275
+ export function subgroupVisibilitySetOp(mode) {
276
+ if (mode !== 'open' && mode !== 'restricted')
277
+ throw new Error(`mode must be open or restricted, got ${String(mode)}`);
278
+ return { kind: 'group', bytes: concat(tag(GROUP_OP.SubgroupVisibilitySet), tag(mode === 'open' ? 0 : 1)) };
279
+ }
280
+ /** Name the group, with optional key/value data (`GroupOp::GroupMetadataSet`). */
281
+ export function groupMetadataSetOp(input = {}) {
282
+ return { kind: 'group', bytes: concat(tag(GROUP_OP.GroupMetadataSet), optionalStr(input.name), stringMap(input.data)) };
283
+ }
284
+ /** Name a member in the group (`GroupOp::MemberMetadataSet`). */
285
+ export function memberMetadataSetOp(member, input = {}) {
286
+ return {
287
+ kind: 'group',
288
+ bytes: concat(tag(GROUP_OP.MemberMetadataSet), fromHex(member, 'member', 32), optionalStr(input.name), stringMap(input.data)),
289
+ };
290
+ }
291
+ /** Name a context in the group (`GroupOp::ContextMetadataSet`). */
292
+ export function contextMetadataSetOp(contextId, input = {}) {
293
+ return {
294
+ kind: 'group',
295
+ bytes: concat(tag(GROUP_OP.ContextMetadataSet), fromHex(contextId, 'contextId', 32), optionalStr(input.name), stringMap(input.data)),
296
+ };
297
+ }
298
+ function contextCapability(capability) {
299
+ if (!Number.isInteger(capability) || capability < 1 || capability > 0xff) {
300
+ throw new Error(`capability must be a non-zero u8, got ${String(capability)}`);
301
+ }
302
+ return tag(capability);
303
+ }
304
+ /** Grant `member` a per-context capability (`GroupOp::ContextCapabilityGranted`). */
305
+ export function contextCapabilityGrantedOp(contextId, member, capability) {
306
+ return {
307
+ kind: 'group',
308
+ bytes: concat(tag(GROUP_OP.ContextCapabilityGranted), fromHex(contextId, 'contextId', 32), fromHex(member, 'member', 32), contextCapability(capability)),
309
+ };
310
+ }
311
+ /** Revoke a per-context capability from `member` (`GroupOp::ContextCapabilityRevoked`). */
312
+ export function contextCapabilityRevokedOp(contextId, member, capability) {
313
+ return {
314
+ kind: 'group',
315
+ bytes: concat(tag(GROUP_OP.ContextCapabilityRevoked), fromHex(contextId, 'contextId', 32), fromHex(member, 'member', 32), contextCapability(capability)),
316
+ };
317
+ }
318
+ /**
319
+ * Join an open subgroup yourself (`RootOp::MemberJoinedOpen`), a root op posted
320
+ * to the namespace. `member` must be the author; `credential` is the author's
321
+ * own `AccountProof<DeviceCert>`, hex, carried as-is. The author's
322
+ * `CAN_JOIN_OPEN_SUBGROUPS` and the open inheritance path are checked as usual.
323
+ */
324
+ export function memberJoinedOpenOp(input) {
325
+ return {
326
+ kind: 'root',
327
+ bytes: concat(tag(ROOT_OP.MemberJoinedOpen), fromHex(input.member, 'member', 32), fromHex(input.groupId, 'groupId', 32), fromHexUnsized(input.credential, 'credential')),
328
+ };
329
+ }
@@ -4,5 +4,5 @@ export { signCreationWarrant, parseCreationWarrant, creationInitHash, } from './
4
4
  export type { CreationWarrantInput, CreationWarrantFields, SignedCreationWarrant, } from './creation-warrant.js';
5
5
  export { signGovernanceWarrant, parseGovernanceWarrant, governanceOpHash, } from './governance-warrant.js';
6
6
  export type { GovernanceWarrantInput, GovernanceWarrantFields, } from './governance-warrant.js';
7
- export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
8
- export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, NamespaceCreatedInput, } from './governance-op.js';
7
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, createdSubgroupId, subgroupCreation, groupReparentedOp, groupDeletedOp, memberCapabilitySetOp, contextDetachedOp, subgroupVisibilitySetOp, groupMetadataSetOp, memberMetadataSetOp, contextMetadataSetOp, contextCapabilityGrantedOp, contextCapabilityRevokedOp, memberJoinedOpenOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
8
+ export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, SubgroupCreation, NamespaceCreatedInput, } from './governance-op.js';
@@ -2,4 +2,4 @@
2
2
  export { signWarrant, intentHash } from './warrant.js';
3
3
  export { signCreationWarrant, parseCreationWarrant, creationInitHash, } from './creation-warrant.js';
4
4
  export { signGovernanceWarrant, parseGovernanceWarrant, governanceOpHash, } from './governance-warrant.js';
5
- export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
5
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, createdSubgroupId, subgroupCreation, groupReparentedOp, groupDeletedOp, memberCapabilitySetOp, contextDetachedOp, subgroupVisibilitySetOp, groupMetadataSetOp, memberMetadataSetOp, contextMetadataSetOp, contextCapabilityGrantedOp, contextCapabilityRevokedOp, memberJoinedOpenOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "22.9.0",
3
+ "version": "23.1.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {