@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.
@@ -29,8 +29,26 @@ import { type Signer } from '../signer/signer.js';
29
29
  * 16: a namespace can be founded through a relay (a delegated
30
30
  * `NamespaceCreatedV2`), and `GroupOp::FoundingRelayAttested` lets that relay
31
31
  * admit itself as the namespace's first TEE. Again no layout this module signs.
32
+ *
33
+ * 17: `RootOp::GroupCreated` carries a `salt`, and its `group_id` must be the
34
+ * one `createdSubgroupId(admin, parentId, restricted, salt)` derives
35
+ * (calimero-network/core#4244), so two concurrent creates for one id can never
36
+ * name different creators. The layout of that variant changed.
37
+ *
38
+ * 18: owner-level ops need the account ROOT, not only a device of it
39
+ * (calimero-network/core, root-guarded owner ops). `GroupOp::RootGuarded` and
40
+ * `RootOp::RootGuarded` are appended and carry a root-signed proof, and the bare
41
+ * `TransferOwnership`, `AdminChanged`, `GroupDelete` and TEE policy ops are
42
+ * refused. No layout this module signs changes; see `../owner-op` for the proof.
43
+ *
44
+ * 19 is reserved for calimero-network/core#4269.
45
+ *
46
+ * 20: no layout change. A delegated `GroupCreated` whose executor is a TEE at
47
+ * the namespace root now seats that TEE in the new subgroup
48
+ * (calimero-network/core#4276), an apply-time rule older peers disagree on.
49
+ * Core signs and checks at 20, so every op this module signs must too.
32
50
  */
33
- export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 16;
51
+ export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 20;
34
52
  /**
35
53
  * Borsh-encode a `SignedGroupOpenInvitation`.
36
54
  *
@@ -15,8 +15,26 @@ import { resolveSigner } from '../signer/signer.js';
15
15
  * 16: a namespace can be founded through a relay (a delegated
16
16
  * `NamespaceCreatedV2`), and `GroupOp::FoundingRelayAttested` lets that relay
17
17
  * admit itself as the namespace's first TEE. Again no layout this module signs.
18
+ *
19
+ * 17: `RootOp::GroupCreated` carries a `salt`, and its `group_id` must be the
20
+ * one `createdSubgroupId(admin, parentId, restricted, salt)` derives
21
+ * (calimero-network/core#4244), so two concurrent creates for one id can never
22
+ * name different creators. The layout of that variant changed.
23
+ *
24
+ * 18: owner-level ops need the account ROOT, not only a device of it
25
+ * (calimero-network/core, root-guarded owner ops). `GroupOp::RootGuarded` and
26
+ * `RootOp::RootGuarded` are appended and carry a root-signed proof, and the bare
27
+ * `TransferOwnership`, `AdminChanged`, `GroupDelete` and TEE policy ops are
28
+ * refused. No layout this module signs changes; see `../owner-op` for the proof.
29
+ *
30
+ * 19 is reserved for calimero-network/core#4269.
31
+ *
32
+ * 20: no layout change. A delegated `GroupCreated` whose executor is a TEE at
33
+ * the namespace root now seats that TEE in the new subgroup
34
+ * (calimero-network/core#4276), an apply-time rule older peers disagree on.
35
+ * Core signs and checks at 20, so every op this module signs must too.
18
36
  */
19
- export const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 16;
37
+ export const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 20;
20
38
  /** Domain prefixed to the signable bytes; core's `NAMESPACE_GOVERNANCE_SIGN_DOMAIN`. */
21
39
  const NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode('calimero.namespace.v1');
22
40
  /** `NamespaceOp::Root` is the first variant. */
@@ -0,0 +1,2 @@
1
+ export { OWNER_OP_KIND, transferOwnershipOp, groupDeleteOp, adminChangedOp, teeAuthoringPolicyOp, teeAdmissionPolicyOp, teeReleaseAdmissionPolicyOp, ownerOpDigest, ownerOpSigningPayload, signOwnerOpProof, rootGuardedOpBytes, } from './owner-op.js';
2
+ export type { OwnerOp, OwnerOpKind, OwnerOpPlane, OwnerOpProofInput, TeeAdmissionPolicyOpInput, TeeReleaseAdmissionPolicyOpInput, } from './owner-op.js';
@@ -0,0 +1,3 @@
1
+ // Root proofs for owner-level governance ops: what a nodeless or cold-root
2
+ // account signs offline so a stolen device alone cannot take a group.
3
+ export { OWNER_OP_KIND, transferOwnershipOp, groupDeleteOp, adminChangedOp, teeAuthoringPolicyOp, teeAdmissionPolicyOp, teeReleaseAdmissionPolicyOp, ownerOpDigest, ownerOpSigningPayload, signOwnerOpProof, rootGuardedOpBytes, } from './owner-op.js';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Which owner-level op a proof authorises. The value is the byte core signs,
3
+ * its `OwnerOpKind` discriminant.
4
+ */
5
+ export declare const OWNER_OP_KIND: {
6
+ readonly transferOwnership: 0;
7
+ readonly adminChanged: 1;
8
+ readonly groupDelete: 2;
9
+ readonly teeAdmissionPolicy: 3;
10
+ readonly teeAuthoringPolicy: 4;
11
+ readonly teeReleaseAdmissionPolicy: 5;
12
+ };
13
+ export type OwnerOpKind = keyof typeof OWNER_OP_KIND;
14
+ /** Which plane the op is on: a `GroupOp` on a group, or a `RootOp` on the namespace. */
15
+ export type OwnerOpPlane = 'group' | 'root';
16
+ /** An encoded owner-level op: its borsh bytes, its kind and its plane. */
17
+ export interface OwnerOp {
18
+ kind: OwnerOpKind;
19
+ plane: OwnerOpPlane;
20
+ /** The op's borsh bytes: exactly what the node builds, and what the digest covers. */
21
+ bytes: Uint8Array;
22
+ }
23
+ /** Hand the group to `newOwner` (an account, hex), who must already be an admin. */
24
+ export declare function transferOwnershipOp(newOwner: string): OwnerOp;
25
+ /** The owner-only deletion of a group that holds no contexts. */
26
+ export declare function groupDeleteOp(): OwnerOp;
27
+ /** Repoint the namespace's admin pin at `newAdmin` (an account, hex). A root op. */
28
+ export declare function adminChangedOp(newAdmin: string): OwnerOp;
29
+ /** The TEE authoring policy `PUT`/`DELETE …/settings/tee-authoring-policy` publishes. */
30
+ export declare function teeAuthoringPolicyOp(allowedMrtd: string[]): OwnerOp;
31
+ /** A measurement-list admission policy, as the node publishes it (the V2 form). */
32
+ export interface TeeAdmissionPolicyOpInput {
33
+ allowedMrtd: string[];
34
+ allowedRtmr0: string[];
35
+ allowedRtmr1: string[];
36
+ allowedRtmr2: string[];
37
+ allowedRtmr3: string[];
38
+ allowedTcbStatuses: string[];
39
+ acceptMock: boolean;
40
+ mode?: 'replica' | 'relay';
41
+ }
42
+ /**
43
+ * The measurement-list admission policy `PUT …/settings/tee-admission-policy`
44
+ * publishes. Pass the fields exactly as you send them: the node signs over the
45
+ * op it builds from them, and any difference is a different op.
46
+ */
47
+ export declare function teeAdmissionPolicyOp(input: TeeAdmissionPolicyOpInput): OwnerOp;
48
+ /** A signed-release admission policy, as the node publishes it (the V2 form). */
49
+ export interface TeeReleaseAdmissionPolicyOpInput {
50
+ allowedProfiles: string[];
51
+ /**
52
+ * The oldest release admitted. Give it in the node's normalised form
53
+ * (`X.Y.Z`, no tag prefix): the node normalises what it is sent before it
54
+ * builds the op, and the proof must be for the op it builds.
55
+ */
56
+ minReleaseVersion?: string;
57
+ allowedTcbStatuses: string[];
58
+ acceptMock: boolean;
59
+ mode?: 'replica' | 'relay';
60
+ }
61
+ /** The signed-release admission policy `PUT …/settings/tee-admission-policy` publishes. */
62
+ export declare function teeReleaseAdmissionPolicyOp(input: TeeReleaseAdmissionPolicyOpInput): OwnerOp;
63
+ /** core's `OwnerOpAuthorization::op_digest`: the op's bytes, hashed under the body domain. */
64
+ export declare function ownerOpDigest(opBytes: Uint8Array): Promise<Uint8Array>;
65
+ /** What an owner-op proof binds, apart from the op and the key that signs it. */
66
+ export interface OwnerOpProofInput {
67
+ /** The account root's signing secret, 64 hex. Never sent anywhere. */
68
+ rootSecret: string;
69
+ /** The namespace the op is published in (`getGroupInfo(...).namespaceId`), hex. */
70
+ namespaceId: string;
71
+ /**
72
+ * The group the op acts on, hex. For `adminChanged` and the TEE policies,
73
+ * the namespace root.
74
+ */
75
+ groupId: string;
76
+ /** The op, from one of the encoders here. */
77
+ op: OwnerOp;
78
+ /** The group's current guarded-op counter (`getGroupInfo(groupId).ownerOpCounter`). */
79
+ counter: number | bigint;
80
+ /**
81
+ * The root epoch that signs, 0 for a root that has never been handed off.
82
+ * A later epoch needs `chain`.
83
+ */
84
+ keyEpoch?: number;
85
+ /**
86
+ * The account's root-key handoffs, epoch 0 upward, each the hex borsh of a
87
+ * `RootKeyHandoff` (account 32, from_epoch u32, new root key 32, signature
88
+ * 64). It must reach at least `keyEpoch`, and at least the epoch the group
89
+ * has recorded for the account, or the node refuses the proof.
90
+ */
91
+ chain?: string[];
92
+ }
93
+ /** core's `OwnerOpTerms::signing_payload`. */
94
+ export declare function ownerOpSigningPayload(input: {
95
+ account: string;
96
+ namespaceId: string;
97
+ groupId: string;
98
+ kind: OwnerOpKind;
99
+ opDigest: Uint8Array;
100
+ counter: number | bigint;
101
+ keyEpoch: number;
102
+ }): Promise<Uint8Array>;
103
+ /**
104
+ * Sign a root proof for `op` and return it as core's `rootProof`: the hex
105
+ * borsh of an `AccountProof<OwnerOpAuthorization>`.
106
+ *
107
+ * Layout: `AccountGenesis` (version u8 + root pk 32), the handoff chain (u32
108
+ * count + 132 bytes each), then `OwnerOpAuthorization` (account 32, namespace
109
+ * 32, group 32, kind u8, op digest 32, counter u64, key epoch u32, signature
110
+ * 64).
111
+ */
112
+ export declare function signOwnerOpProof(input: OwnerOpProofInput): Promise<string>;
113
+ /**
114
+ * The `RootGuarded` wrapper core publishes: the op and its proof, as one
115
+ * `GroupOp` or `RootOp`. A client never sends this to a node (the endpoints
116
+ * take the proof and build the wrapper); it is here so the whole encoding is
117
+ * checkable against core's vector.
118
+ */
119
+ export declare function rootGuardedOpBytes(op: OwnerOp, rootProof: string): Uint8Array;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Root proofs for owner-level governance ops (core schema 18).
3
+ *
4
+ * A device key speaks for its account everywhere in governance, so a stolen
5
+ * device used to be enough to take a group from its owner: promote an
6
+ * accomplice, transfer ownership, remove the owner. Core now refuses the ops
7
+ * that can do that unless they carry an authorisation signed by the account's
8
+ * **root** key, which a device does not hold:
9
+ *
10
+ * - `GroupOp::TransferOwnership`, `GroupOp::GroupDelete` and
11
+ * `RootOp::AdminChanged`, which are owner-only;
12
+ * - the TEE admission, release-admission and authoring policies, which stay
13
+ * admin-level but need the signing admin's own root.
14
+ *
15
+ * A node that holds the account root (as `merod init` provisions) signs the
16
+ * proof itself, so a caller on such a node passes nothing. This module is for
17
+ * the other case: a nodeless account, or a root kept cold, signing offline and
18
+ * passing the hex proof as `rootProof`.
19
+ *
20
+ * What the root signs binds the account, the namespace, the group, the kind of
21
+ * op, a digest of the op's own borsh bytes, the root epoch, and the group's
22
+ * guarded-op counter. Read that counter (`ownerOpCounter`, with `namespaceId`)
23
+ * from `getGroupInfo` first: the node applies a proof only while the group is
24
+ * still at that count, and advances it once the op applies, so each proof is
25
+ * single-use. For `AdminChanged` and the TEE policies the group is the
26
+ * namespace root.
27
+ *
28
+ * Byte layouts follow core's `calimero_account::OwnerOpAuthorization` and
29
+ * `calimero_governance_types::{GroupOp, RootOp}`. They are pinned against core's
30
+ * own vectors (`crates/account/src/tests/owner_op.rs`,
31
+ * `root_guarded_vectors_are_stable` in `crates/governance-types/src/tests.rs`)
32
+ * in `owner-op.test.ts`.
33
+ */
34
+ import { concat, derivePublicKey, domainHash, fromHex, fromHexUnsized, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
35
+ /** core's `OWNER_OP_SIGN_DOMAIN`: what the root signs under. */
36
+ const OWNER_OP_SIGN_DOMAIN = new TextEncoder().encode('calimero.account.owner-op.v1');
37
+ /** core's `OWNER_OP_BODY_DOMAIN`: what the op's bytes are hashed under. */
38
+ const OWNER_OP_BODY_DOMAIN = new TextEncoder().encode('calimero.account.owner-op.body.v1');
39
+ /** core's `ACCOUNT_ID_DOMAIN`. */
40
+ const ACCOUNT_ID_DOMAIN = new TextEncoder().encode('calimero.account.genesis.v1');
41
+ /** core's `ACCOUNT_GENESIS_VERSION`. */
42
+ const ACCOUNT_GENESIS_VERSION = 2;
43
+ /**
44
+ * Which owner-level op a proof authorises. The value is the byte core signs,
45
+ * its `OwnerOpKind` discriminant.
46
+ */
47
+ export const OWNER_OP_KIND = {
48
+ transferOwnership: 0,
49
+ adminChanged: 1,
50
+ groupDelete: 2,
51
+ teeAdmissionPolicy: 3,
52
+ teeAuthoringPolicy: 4,
53
+ teeReleaseAdmissionPolicy: 5,
54
+ };
55
+ /** `GroupOp` discriminants, by position in core's enum. */
56
+ const GROUP_OP = {
57
+ GroupDelete: 14,
58
+ TransferOwnership: 21,
59
+ TeeAuthoringPolicySet: 33,
60
+ TeeAdmissionPolicySetV2: 37,
61
+ TeeReleaseAdmissionPolicySetV2: 38,
62
+ RootGuarded: 42,
63
+ };
64
+ /** `RootOp` discriminants, by position in core's enum. */
65
+ const ROOT_OP = {
66
+ AdminChanged: 3,
67
+ RootGuarded: 12,
68
+ };
69
+ /** core's `TeeAdmissionMode` discriminants. */
70
+ const TEE_MODE = { replica: 0, relay: 1 };
71
+ const te = new TextEncoder();
72
+ function tag(byte) {
73
+ return new Uint8Array([byte]);
74
+ }
75
+ function borshString(value) {
76
+ const bytes = te.encode(value);
77
+ return concat(u32le(bytes.length), bytes);
78
+ }
79
+ function borshStrings(values) {
80
+ return concat(u32le(values.length), ...values.map(borshString));
81
+ }
82
+ function borshBool(value) {
83
+ return tag(value ? 1 : 0);
84
+ }
85
+ function teeMode(mode) {
86
+ const byte = TEE_MODE[mode ?? 'replica'];
87
+ if (byte === undefined) {
88
+ throw new Error(`mode must be replica or relay, got ${String(mode)}`);
89
+ }
90
+ return tag(byte);
91
+ }
92
+ /** Hand the group to `newOwner` (an account, hex), who must already be an admin. */
93
+ export function transferOwnershipOp(newOwner) {
94
+ return {
95
+ kind: 'transferOwnership',
96
+ plane: 'group',
97
+ bytes: concat(tag(GROUP_OP.TransferOwnership), fromHex(newOwner, 'newOwner', 32)),
98
+ };
99
+ }
100
+ /** The owner-only deletion of a group that holds no contexts. */
101
+ export function groupDeleteOp() {
102
+ return { kind: 'groupDelete', plane: 'group', bytes: tag(GROUP_OP.GroupDelete) };
103
+ }
104
+ /** Repoint the namespace's admin pin at `newAdmin` (an account, hex). A root op. */
105
+ export function adminChangedOp(newAdmin) {
106
+ return {
107
+ kind: 'adminChanged',
108
+ plane: 'root',
109
+ bytes: concat(tag(ROOT_OP.AdminChanged), fromHex(newAdmin, 'newAdmin', 32)),
110
+ };
111
+ }
112
+ /** The TEE authoring policy `PUT`/`DELETE …/settings/tee-authoring-policy` publishes. */
113
+ export function teeAuthoringPolicyOp(allowedMrtd) {
114
+ return {
115
+ kind: 'teeAuthoringPolicy',
116
+ plane: 'group',
117
+ bytes: concat(tag(GROUP_OP.TeeAuthoringPolicySet), borshStrings(allowedMrtd)),
118
+ };
119
+ }
120
+ /**
121
+ * The measurement-list admission policy `PUT …/settings/tee-admission-policy`
122
+ * publishes. Pass the fields exactly as you send them: the node signs over the
123
+ * op it builds from them, and any difference is a different op.
124
+ */
125
+ export function teeAdmissionPolicyOp(input) {
126
+ return {
127
+ kind: 'teeAdmissionPolicy',
128
+ plane: 'group',
129
+ bytes: concat(tag(GROUP_OP.TeeAdmissionPolicySetV2), borshStrings(input.allowedMrtd), borshStrings(input.allowedRtmr0), borshStrings(input.allowedRtmr1), borshStrings(input.allowedRtmr2), borshStrings(input.allowedRtmr3), borshStrings(input.allowedTcbStatuses), borshBool(input.acceptMock), teeMode(input.mode)),
130
+ };
131
+ }
132
+ /** The signed-release admission policy `PUT …/settings/tee-admission-policy` publishes. */
133
+ export function teeReleaseAdmissionPolicyOp(input) {
134
+ const minRelease = input.minReleaseVersion === undefined
135
+ ? tag(0)
136
+ : concat(tag(1), borshString(input.minReleaseVersion));
137
+ return {
138
+ kind: 'teeReleaseAdmissionPolicy',
139
+ plane: 'group',
140
+ bytes: concat(tag(GROUP_OP.TeeReleaseAdmissionPolicySetV2), borshStrings(input.allowedProfiles), minRelease, borshStrings(input.allowedTcbStatuses), borshBool(input.acceptMock), teeMode(input.mode)),
141
+ };
142
+ }
143
+ /** core's `OwnerOpAuthorization::op_digest`: the op's bytes, hashed under the body domain. */
144
+ export async function ownerOpDigest(opBytes) {
145
+ return domainHash(OWNER_OP_BODY_DOMAIN, [opBytes]);
146
+ }
147
+ /** core's `OwnerOpTerms::signing_payload`. */
148
+ export async function ownerOpSigningPayload(input) {
149
+ return domainHash(OWNER_OP_SIGN_DOMAIN, [
150
+ fromHex(input.account, 'account', 32),
151
+ fromHex(input.namespaceId, 'namespaceId', 32),
152
+ fromHex(input.groupId, 'groupId', 32),
153
+ tag(kindByte(input.kind)),
154
+ input.opDigest,
155
+ u64le(input.counter),
156
+ u32le(input.keyEpoch),
157
+ ]);
158
+ }
159
+ function kindByte(kind) {
160
+ const byte = OWNER_OP_KIND[kind];
161
+ if (byte === undefined) {
162
+ throw new Error(`unknown owner-op kind ${String(kind)}`);
163
+ }
164
+ return byte;
165
+ }
166
+ const HANDOFF_BYTES = 32 + 4 + 32 + 64;
167
+ /**
168
+ * Sign a root proof for `op` and return it as core's `rootProof`: the hex
169
+ * borsh of an `AccountProof<OwnerOpAuthorization>`.
170
+ *
171
+ * Layout: `AccountGenesis` (version u8 + root pk 32), the handoff chain (u32
172
+ * count + 132 bytes each), then `OwnerOpAuthorization` (account 32, namespace
173
+ * 32, group 32, kind u8, op digest 32, counter u64, key epoch u32, signature
174
+ * 64).
175
+ */
176
+ export async function signOwnerOpProof(input) {
177
+ const rootPublicKey = await derivePublicKey(input.rootSecret);
178
+ const genesis = concat(tag(ACCOUNT_GENESIS_VERSION), rootPublicKey);
179
+ const account = hex(await domainHash(ACCOUNT_ID_DOMAIN, [genesis]));
180
+ const keyEpoch = input.keyEpoch ?? 0;
181
+ const chain = (input.chain ?? []).map((handoff, index) => {
182
+ const bytes = fromHexUnsized(handoff, `chain[${index}]`);
183
+ if (bytes.length !== HANDOFF_BYTES) {
184
+ throw new Error(`chain[${index}] must be a ${HANDOFF_BYTES}-byte RootKeyHandoff, got ${bytes.length}`);
185
+ }
186
+ return bytes;
187
+ });
188
+ if (keyEpoch > chain.length) {
189
+ throw new Error(`keyEpoch ${keyEpoch} needs ${keyEpoch} handoffs in chain, got ${chain.length}`);
190
+ }
191
+ const opDigest = await ownerOpDigest(input.op.bytes);
192
+ const payload = await ownerOpSigningPayload({
193
+ account,
194
+ namespaceId: input.namespaceId,
195
+ groupId: input.groupId,
196
+ kind: input.op.kind,
197
+ opDigest,
198
+ counter: input.counter,
199
+ keyEpoch,
200
+ });
201
+ const key = await importSigningKey(input.rootSecret);
202
+ const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', key, payload));
203
+ return hex(concat(genesis, u32le(chain.length), ...chain, fromHex(account, 'account', 32), fromHex(input.namespaceId, 'namespaceId', 32), fromHex(input.groupId, 'groupId', 32), tag(kindByte(input.op.kind)), opDigest, u64le(input.counter), u32le(keyEpoch), signature));
204
+ }
205
+ /**
206
+ * The `RootGuarded` wrapper core publishes: the op and its proof, as one
207
+ * `GroupOp` or `RootOp`. A client never sends this to a node (the endpoints
208
+ * take the proof and build the wrapper); it is here so the whole encoding is
209
+ * checkable against core's vector.
210
+ */
211
+ export function rootGuardedOpBytes(op, rootProof) {
212
+ const wrapper = op.plane === 'group' ? GROUP_OP.RootGuarded : ROOT_OP.RootGuarded;
213
+ return concat(tag(wrapper), op.bytes, fromHexUnsized(rootProof, 'rootProof'));
214
+ }
@@ -52,6 +52,17 @@ export declare function memberRemovedOp(member: string): GovernanceOp;
52
52
  export declare function memberLeftOp(member: string): GovernanceOp;
53
53
  /** Set `member`'s role (an account, hex). */
54
54
  export declare function memberRoleSetOp(member: string, memberRole: GovernanceMemberRole): GovernanceOp;
55
+ /**
56
+ * The id `admin` derives for a subgroup under `parentId` with birth visibility
57
+ * `restricted` and `salt` (all hex), as core's `created_subgroup_id` does:
58
+ * `domain_hash("calimero.subgroup.id.v1", [admin, parentId, [restricted], salt])`.
59
+ *
60
+ * A subgroup's id is bound to its create, so nobody but its creator can name
61
+ * it: a node refuses a `GroupCreated` whose fields do not reproduce its id.
62
+ * That is what stops a second member racing a concurrent create for an id it
63
+ * saw gossiped.
64
+ */
65
+ export declare function createdSubgroupId(admin: string, parentId: string, restricted: boolean, salt: string): Promise<string>;
55
66
  /**
56
67
  * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
57
68
  * the capabilities every member holds without a per-member grant. Needs admin.
@@ -63,7 +74,11 @@ export declare function memberRoleSetOp(member: string, memberRole: GovernanceMe
63
74
  export declare function defaultCapabilitiesSetOp(capabilities: number): GovernanceOp;
64
75
  /** What a subgroup is created with. */
65
76
  export interface GroupCreatedInput {
66
- /** The new subgroup's id, hex (32 bytes). Choose it fresh; a taken id is refused. */
77
+ /**
78
+ * The new subgroup's id, hex (32 bytes). It must be
79
+ * `createdSubgroupId(admin, parentId, restricted, salt)`; a node refuses any
80
+ * other. {@link subgroupCreation} draws the salt and derives it for you.
81
+ */
67
82
  groupId: string;
68
83
  /** The group it is nested under, hex: the namespace root or an existing subgroup. */
69
84
  parentId: string;
@@ -74,6 +89,8 @@ export interface GroupCreatedInput {
74
89
  * refuses any other, since the op is applied as the author.
75
90
  */
76
91
  admin: string;
92
+ /** The salt `groupId` was derived with, hex (32 bytes). Random; need not be kept. */
93
+ salt: string;
77
94
  }
78
95
  /**
79
96
  * Create a subgroup under `parentId`, a root op published on the namespace.
@@ -82,6 +99,19 @@ export interface GroupCreatedInput {
82
99
  * relay, becomes the new subgroup's admin.
83
100
  */
84
101
  export declare function groupCreatedOp(input: GroupCreatedInput): GovernanceOp;
102
+ /** A new subgroup's create: its derived id, the salt it was derived with, and the op. */
103
+ export interface SubgroupCreation {
104
+ groupId: string;
105
+ salt: string;
106
+ op: GovernanceOp;
107
+ }
108
+ /**
109
+ * Create a subgroup under `parentId`: draws a fresh salt (unless one is given),
110
+ * derives the id with {@link createdSubgroupId}, and encodes the op.
111
+ */
112
+ export declare function subgroupCreation(input: Omit<GroupCreatedInput, 'groupId' | 'salt'> & {
113
+ salt?: string;
114
+ }): Promise<SubgroupCreation>;
85
115
  /** Move `childGroupId` under `newParentId`, a root op. */
86
116
  export declare function groupReparentedOp(childGroupId: string, newParentId: string): GovernanceOp;
87
117
  /**
@@ -125,3 +155,39 @@ export interface NamespaceCreatedInput {
125
155
  * inline. The warrant's scope must be {@link foundedNamespaceId}`(founder, salt)`.
126
156
  */
127
157
  export declare function namespaceCreatedOp(input: NamespaceCreatedInput): GovernanceOp;
158
+ /** Set `member`'s capabilities in the group (`GroupOp::MemberCapabilitySet`). Bit 9 is refused. */
159
+ export declare function memberCapabilitySetOp(member: string, capabilities: number): GovernanceOp;
160
+ /** Detach a context from the group (`GroupOp::ContextDetached`). */
161
+ export declare function contextDetachedOp(contextId: string): GovernanceOp;
162
+ /** Make a subgroup open (members of the parent may join it themselves) or restricted. */
163
+ export declare function subgroupVisibilitySetOp(mode: 'open' | 'restricted'): GovernanceOp;
164
+ /** Name the group, with optional key/value data (`GroupOp::GroupMetadataSet`). */
165
+ export declare function groupMetadataSetOp(input?: {
166
+ name?: string;
167
+ data?: Record<string, string>;
168
+ }): GovernanceOp;
169
+ /** Name a member in the group (`GroupOp::MemberMetadataSet`). */
170
+ export declare function memberMetadataSetOp(member: string, input?: {
171
+ name?: string;
172
+ data?: Record<string, string>;
173
+ }): GovernanceOp;
174
+ /** Name a context in the group (`GroupOp::ContextMetadataSet`). */
175
+ export declare function contextMetadataSetOp(contextId: string, input?: {
176
+ name?: string;
177
+ data?: Record<string, string>;
178
+ }): GovernanceOp;
179
+ /** Grant `member` a per-context capability (`GroupOp::ContextCapabilityGranted`). */
180
+ export declare function contextCapabilityGrantedOp(contextId: string, member: string, capability: number): GovernanceOp;
181
+ /** Revoke a per-context capability from `member` (`GroupOp::ContextCapabilityRevoked`). */
182
+ export declare function contextCapabilityRevokedOp(contextId: string, member: string, capability: number): GovernanceOp;
183
+ /**
184
+ * Join an open subgroup yourself (`RootOp::MemberJoinedOpen`), a root op posted
185
+ * to the namespace. `member` must be the author; `credential` is the author's
186
+ * own `AccountProof<DeviceCert>`, hex, carried as-is. The author's
187
+ * `CAN_JOIN_OPEN_SUBGROUPS` and the open inheritance path are checked as usual.
188
+ */
189
+ export declare function memberJoinedOpenOp(input: {
190
+ member: string;
191
+ groupId: string;
192
+ credential: string;
193
+ }): GovernanceOp;