@calimero-network/mero-js 22.4.0 → 22.5.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/README.md +4 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +317 -25
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +317 -25
- package/dist/index.mjs.map +4 -4
- package/dist/namespace-op/namespace-op.d.ts +12 -4
- package/dist/namespace-op/namespace-op.js +12 -4
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/relay-client.d.ts +56 -0
- package/dist/relay/relay-client.js +62 -7
- package/dist/warrant/governance-op.d.ts +81 -0
- package/dist/warrant/governance-op.js +125 -0
- package/dist/warrant/governance-warrant.d.ts +106 -0
- package/dist/warrant/governance-warrant.js +159 -0
- package/dist/warrant/index.d.ts +4 -0
- package/dist/warrant/index.js +2 -0
- package/package.json +1 -1
|
@@ -17,11 +17,19 @@ import type { SignedGroupOpenInvitation } from '../admin-api/admin-types.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* Core's `SIGNED_NAMESPACE_OP_SCHEMA_VERSION`. A node refuses any other value.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* 15: core went to 13 (TEE rows locked to the TEE roles, a TEE's relay role
|
|
21
|
+
* read at its namespace root row, inherited read-only roles read-only in Open
|
|
22
|
+
* subgroups), then 14 (`GroupOp::ContextRegisteredOnBehalf`) and 15
|
|
23
|
+
* (`GroupOp::OnBehalf` / `RootOp::OnBehalf`) for delegated context creation and
|
|
24
|
+
* governance through a relay. None of these changes a layout this module
|
|
25
|
+
* signs; the version moves with core's so nodes on different versions never
|
|
26
|
+
* share a namespace.
|
|
27
|
+
*
|
|
28
|
+
* 16: a namespace can be founded through a relay (a delegated
|
|
29
|
+
* `NamespaceCreatedV2`), and `GroupOp::FoundingRelayAttested` lets that relay
|
|
30
|
+
* admit itself as the namespace's first TEE. Again no layout this module signs.
|
|
23
31
|
*/
|
|
24
|
-
export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION =
|
|
32
|
+
export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 16;
|
|
25
33
|
/**
|
|
26
34
|
* Borsh-encode a `SignedGroupOpenInvitation`.
|
|
27
35
|
*
|
|
@@ -2,11 +2,19 @@ import { concat, derivePublicKey, fromHex, hex, importSigningKey, u32le, u64le,
|
|
|
2
2
|
/**
|
|
3
3
|
* Core's `SIGNED_NAMESPACE_OP_SCHEMA_VERSION`. A node refuses any other value.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 15: core went to 13 (TEE rows locked to the TEE roles, a TEE's relay role
|
|
6
|
+
* read at its namespace root row, inherited read-only roles read-only in Open
|
|
7
|
+
* subgroups), then 14 (`GroupOp::ContextRegisteredOnBehalf`) and 15
|
|
8
|
+
* (`GroupOp::OnBehalf` / `RootOp::OnBehalf`) for delegated context creation and
|
|
9
|
+
* governance through a relay. None of these changes a layout this module
|
|
10
|
+
* signs; the version moves with core's so nodes on different versions never
|
|
11
|
+
* share a namespace.
|
|
12
|
+
*
|
|
13
|
+
* 16: a namespace can be founded through a relay (a delegated
|
|
14
|
+
* `NamespaceCreatedV2`), and `GroupOp::FoundingRelayAttested` lets that relay
|
|
15
|
+
* admit itself as the namespace's first TEE. Again no layout this module signs.
|
|
8
16
|
*/
|
|
9
|
-
export const SIGNED_NAMESPACE_OP_SCHEMA_VERSION =
|
|
17
|
+
export const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 16;
|
|
10
18
|
/** Domain prefixed to the signable bytes; core's `NAMESPACE_GOVERNANCE_SIGN_DOMAIN`. */
|
|
11
19
|
const NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode('calimero.namespace.v1');
|
|
12
20
|
/** `NamespaceOp::Root` is the first variant. */
|
package/dist/relay/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { RelayClient, IntentRefusedError } from './relay-client.js';
|
|
2
|
-
export type { RelayClientConfig, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, } from './relay-client.js';
|
|
2
|
+
export type { RelayClientConfig, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, } from './relay-client.js';
|
|
3
3
|
export { createMemoryNonceSource, createLocalStorageNonceSource } from './nonce-source.js';
|
|
4
4
|
export type { NonceSource } from './nonce-source.js';
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* monotonic per-device sequence, and one minted against the wrong executor is
|
|
28
28
|
* unspendable: the number is gone and the write never happened.
|
|
29
29
|
*/
|
|
30
|
+
import type { GovernanceOp } from '../warrant/governance-op.js';
|
|
30
31
|
import type { NonceSource } from './nonce-source.js';
|
|
31
32
|
/** Where and as whom to write, plus the key that consents. */
|
|
32
33
|
export interface RelayClientConfig {
|
|
@@ -129,6 +130,34 @@ export interface CreatedContext {
|
|
|
129
130
|
/** The relay's identity in the new context, hex. */
|
|
130
131
|
memberPublicKey: string;
|
|
131
132
|
}
|
|
133
|
+
/** What a relay says about publishing governance ops for members of one group. */
|
|
134
|
+
export interface GovernanceDescription {
|
|
135
|
+
/** The account a governance warrant must name as `executor`, hex. */
|
|
136
|
+
executorAccount: string;
|
|
137
|
+
/** The group asked about, hex. */
|
|
138
|
+
groupId: string;
|
|
139
|
+
/**
|
|
140
|
+
* Whether the relay may act for members here (a `RelayTee` role or
|
|
141
|
+
* `CAN_AUTHOR_ON_BEHALF`). It needs no right to the op itself: that is
|
|
142
|
+
* checked on the author, as the op is applied as the author.
|
|
143
|
+
*/
|
|
144
|
+
canActOnBehalf: boolean;
|
|
145
|
+
}
|
|
146
|
+
/** What {@link RelayClient.govern} publishes. */
|
|
147
|
+
export interface GovernInput {
|
|
148
|
+
/**
|
|
149
|
+
* Where the op is published, hex: the group itself for a group op, the
|
|
150
|
+
* **namespace** for a root op (creating, moving or deleting a subgroup).
|
|
151
|
+
*/
|
|
152
|
+
groupId: string;
|
|
153
|
+
/** The op, from one of the encoders (`memberAddedOp`, `groupCreatedOp`, ...). */
|
|
154
|
+
op: GovernanceOp;
|
|
155
|
+
}
|
|
156
|
+
/** Where a delegated governance op landed. */
|
|
157
|
+
export interface GovernResult {
|
|
158
|
+
/** The group the op acted on, hex: the new subgroup, for a creation. */
|
|
159
|
+
groupId: string;
|
|
160
|
+
}
|
|
132
161
|
/** Where an accepted intent landed. */
|
|
133
162
|
export interface IntentResult<T = unknown> {
|
|
134
163
|
/** The context's scope root after the run — did this change anything? */
|
|
@@ -216,5 +245,32 @@ export declare class RelayClient {
|
|
|
216
245
|
* `403 sealed_route_unguarded`.
|
|
217
246
|
*/
|
|
218
247
|
createContext(input: CreateContextInput): Promise<CreatedContext>;
|
|
248
|
+
/**
|
|
249
|
+
* Ask the relay whether it may publish governance ops for members of
|
|
250
|
+
* `groupId`, and which account a warrant must name.
|
|
251
|
+
*/
|
|
252
|
+
describeGovernance(groupId: string): Promise<GovernanceDescription>;
|
|
253
|
+
/**
|
|
254
|
+
* Publish one governance op through the relay, as the author.
|
|
255
|
+
*
|
|
256
|
+
* The warrant commits to the op's exact bytes and is scoped to `groupId`, so
|
|
257
|
+
* the relay can neither change what the op does nor spend it elsewhere. Every
|
|
258
|
+
* peer applies the op as the author, so the author's own rights decide
|
|
259
|
+
* (`MANAGE_MEMBERS` to add a member, `CAN_CREATE_SUBGROUP` to create a
|
|
260
|
+
* subgroup, and so on); a lack of them comes back as an
|
|
261
|
+
* {@link IntentRefusedError} from the relay.
|
|
262
|
+
*
|
|
263
|
+
* Always asks {@link describeGovernance} first and refuses *before* taking a
|
|
264
|
+
* nonce when the relay has no standing, for the reason `createContext` does.
|
|
265
|
+
* The nonce comes from the configured {@link NonceSource}; it is spent in a
|
|
266
|
+
* per-group ledger, and a monotonic source is correct for it as for every
|
|
267
|
+
* other warrant.
|
|
268
|
+
*/
|
|
269
|
+
govern(input: GovernInput): Promise<GovernResult>;
|
|
270
|
+
/**
|
|
271
|
+
* The executor a warrant must name: the one the relay answered with, which a
|
|
272
|
+
* configured account must agree with. Remembered for later calls.
|
|
273
|
+
*/
|
|
274
|
+
private checkedExecutor;
|
|
219
275
|
private json;
|
|
220
276
|
}
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { signWarrant } from '../warrant/warrant.js';
|
|
31
31
|
import { signCreationWarrant } from '../warrant/creation-warrant.js';
|
|
32
|
+
import { signGovernanceWarrant } from '../warrant/governance-warrant.js';
|
|
32
33
|
import { HTTPError } from '../http-client/web-client.js';
|
|
34
|
+
import { hex } from '../crypto/internal.js';
|
|
33
35
|
/** How long a freshly minted warrant stays presentable, in seconds. */
|
|
34
36
|
const DEFAULT_TTL_SECONDS = 300;
|
|
35
37
|
/**
|
|
@@ -153,13 +155,7 @@ export class RelayClient {
|
|
|
153
155
|
`${described.groupId}; it needs CAN_CREATE_CONTEXT or admin ` +
|
|
154
156
|
'(checked before signing — no nonce was spent)', false, 403);
|
|
155
157
|
}
|
|
156
|
-
const
|
|
157
|
-
if (configured && configured.toLowerCase() !== described.executorAccount.toLowerCase()) {
|
|
158
|
-
throw new Error(`configured executorAccount ${configured} is not the relay's account ` +
|
|
159
|
-
`${described.executorAccount}; a warrant naming it would be unspendable`);
|
|
160
|
-
}
|
|
161
|
-
const executor = described.executorAccount;
|
|
162
|
-
this.executorAccount = executor;
|
|
158
|
+
const executor = this.checkedExecutor(described.executorAccount);
|
|
163
159
|
const initArgs = input.initArgs ?? {};
|
|
164
160
|
const nonce = await this.config.nonces.next();
|
|
165
161
|
const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
@@ -183,6 +179,65 @@ export class RelayClient {
|
|
|
183
179
|
memberPublicKey: body.data.memberPublicKey,
|
|
184
180
|
};
|
|
185
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Ask the relay whether it may publish governance ops for members of
|
|
184
|
+
* `groupId`, and which account a warrant must name.
|
|
185
|
+
*/
|
|
186
|
+
async describeGovernance(groupId) {
|
|
187
|
+
const body = await this.json('GET', `/admin-api/groups/${encodeURIComponent(groupId)}/governance-intents`);
|
|
188
|
+
return body.data;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Publish one governance op through the relay, as the author.
|
|
192
|
+
*
|
|
193
|
+
* The warrant commits to the op's exact bytes and is scoped to `groupId`, so
|
|
194
|
+
* the relay can neither change what the op does nor spend it elsewhere. Every
|
|
195
|
+
* peer applies the op as the author, so the author's own rights decide
|
|
196
|
+
* (`MANAGE_MEMBERS` to add a member, `CAN_CREATE_SUBGROUP` to create a
|
|
197
|
+
* subgroup, and so on); a lack of them comes back as an
|
|
198
|
+
* {@link IntentRefusedError} from the relay.
|
|
199
|
+
*
|
|
200
|
+
* Always asks {@link describeGovernance} first and refuses *before* taking a
|
|
201
|
+
* nonce when the relay has no standing, for the reason `createContext` does.
|
|
202
|
+
* The nonce comes from the configured {@link NonceSource}; it is spent in a
|
|
203
|
+
* per-group ledger, and a monotonic source is correct for it as for every
|
|
204
|
+
* other warrant.
|
|
205
|
+
*/
|
|
206
|
+
async govern(input) {
|
|
207
|
+
const described = await this.describeGovernance(input.groupId);
|
|
208
|
+
if (!described.canActOnBehalf) {
|
|
209
|
+
throw new IntentRefusedError(`the relay (${described.executorAccount}) has no standing to act for members of group ` +
|
|
210
|
+
`${described.groupId}; an admin must admit it as a relay or grant it CAN_AUTHOR_ON_BEHALF ` +
|
|
211
|
+
'(checked before signing, no nonce was spent)', false, 403);
|
|
212
|
+
}
|
|
213
|
+
const executor = this.checkedExecutor(described.executorAccount);
|
|
214
|
+
const nonce = await this.config.nonces.next();
|
|
215
|
+
const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
216
|
+
const warrant = await signGovernanceWarrant({
|
|
217
|
+
scope: input.groupId,
|
|
218
|
+
op: input.op,
|
|
219
|
+
authorAccount: this.config.authorAccount,
|
|
220
|
+
executor,
|
|
221
|
+
nonce,
|
|
222
|
+
notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
|
|
223
|
+
deviceSecret: this.config.deviceSecret,
|
|
224
|
+
});
|
|
225
|
+
const body = await this.json('POST', `/admin-api/groups/${encodeURIComponent(input.groupId)}/governance-intents`, { warrant, authorProof: this.config.authorProof, op: hex(input.op.bytes) });
|
|
226
|
+
return { groupId: body.data.groupId };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The executor a warrant must name: the one the relay answered with, which a
|
|
230
|
+
* configured account must agree with. Remembered for later calls.
|
|
231
|
+
*/
|
|
232
|
+
checkedExecutor(answered) {
|
|
233
|
+
const configured = this.config.executorAccount;
|
|
234
|
+
if (configured && configured.toLowerCase() !== answered.toLowerCase()) {
|
|
235
|
+
throw new Error(`configured executorAccount ${configured} is not the relay's account ` +
|
|
236
|
+
`${answered}; a warrant naming it would be unspendable`);
|
|
237
|
+
}
|
|
238
|
+
this.executorAccount = answered;
|
|
239
|
+
return answered;
|
|
240
|
+
}
|
|
186
241
|
async json(method, path, body) {
|
|
187
242
|
const headers = { Accept: 'application/json' };
|
|
188
243
|
if (body !== undefined)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode the governance ops a member may have a relay publish for them.
|
|
3
|
+
*
|
|
4
|
+
* A governance warrant commits to an op's **borsh bytes**, not to a description
|
|
5
|
+
* of it, so the client has to produce exactly the bytes the node decodes. These
|
|
6
|
+
* encoders cover the delegable ops (core's `GroupOp::delegable_form` and
|
|
7
|
+
* `RootOp::delegable_form`) that a keyholder app is likely to need. Every
|
|
8
|
+
* discriminant is a variant's position in core's enum, which is how borsh
|
|
9
|
+
* encodes a Rust enum; they are pinned against core's own vectors in the test.
|
|
10
|
+
*
|
|
11
|
+
* **Delegable form.** A removal, a leave and a cascade delete carry fields only
|
|
12
|
+
* the publisher can compute (post-state hashes, the enumerated subtree). The
|
|
13
|
+
* member signs them cleared, and these encoders only ever produce the cleared
|
|
14
|
+
* form, which is the one a relay accepts. The relay fills them in afterwards.
|
|
15
|
+
*
|
|
16
|
+
* Sources in core, for the next person to check a discriminant:
|
|
17
|
+
* `crates/governance-types/src/lib.rs` (`GroupOp`, `RootOp`),
|
|
18
|
+
* `crates/primitives/src/context.rs` (`GroupMemberRole`), and the vectors in
|
|
19
|
+
* `crates/governance-types/src/tests.rs` (`delegated_governance_op_vectors_are_stable`).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Which plane an op is published on: `group` is a `GroupOp` on the group's own
|
|
23
|
+
* log, `root` a `RootOp` on the namespace log. The kind is signed over, so
|
|
24
|
+
* consent to one can never be presented as the other.
|
|
25
|
+
*/
|
|
26
|
+
export type GovernanceOpKind = 'group' | 'root';
|
|
27
|
+
/** An encoded governance op and the plane it belongs to. */
|
|
28
|
+
export interface GovernanceOp {
|
|
29
|
+
kind: GovernanceOpKind;
|
|
30
|
+
/** The op's borsh bytes in delegable form: what the warrant commits to. */
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
}
|
|
33
|
+
/** A group member's role. Borsh discriminants follow core's `GroupMemberRole`. */
|
|
34
|
+
export type GovernanceMemberRole = 'Admin' | 'Member' | 'ReadOnly';
|
|
35
|
+
/**
|
|
36
|
+
* Add `member` (an account, hex) to the group with `role`.
|
|
37
|
+
*
|
|
38
|
+
* Applied as the author: adding a `Member` needs `MANAGE_MEMBERS` or admin,
|
|
39
|
+
* adding an `Admin` needs admin.
|
|
40
|
+
*/
|
|
41
|
+
export declare function memberAddedOp(member: string, memberRole: GovernanceMemberRole): GovernanceOp;
|
|
42
|
+
/**
|
|
43
|
+
* Remove `member` (an account, hex) from the group, in delegable form: both
|
|
44
|
+
* post-state claims cleared, for the relay to compute from its own view.
|
|
45
|
+
*/
|
|
46
|
+
export declare function memberRemovedOp(member: string): GovernanceOp;
|
|
47
|
+
/**
|
|
48
|
+
* The author leaves the group, in delegable form. The node applies it only when
|
|
49
|
+
* `member` is the author's own account.
|
|
50
|
+
*/
|
|
51
|
+
export declare function memberLeftOp(member: string): GovernanceOp;
|
|
52
|
+
/** Set `member`'s role (an account, hex). */
|
|
53
|
+
export declare function memberRoleSetOp(member: string, memberRole: GovernanceMemberRole): GovernanceOp;
|
|
54
|
+
/** What a subgroup is created with. */
|
|
55
|
+
export interface GroupCreatedInput {
|
|
56
|
+
/** The new subgroup's id, hex (32 bytes). Choose it fresh; a taken id is refused. */
|
|
57
|
+
groupId: string;
|
|
58
|
+
/** The group it is nested under, hex: the namespace root or an existing subgroup. */
|
|
59
|
+
parentId: string;
|
|
60
|
+
/** `true` for a Restricted subgroup (the usual default), `false` for born-Open. */
|
|
61
|
+
restricted: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The subgroup's founding admin, hex: the **author's** account. The node
|
|
64
|
+
* refuses any other, since the op is applied as the author.
|
|
65
|
+
*/
|
|
66
|
+
admin: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a subgroup under `parentId`, a root op published on the namespace.
|
|
70
|
+
*
|
|
71
|
+
* Needs `CAN_CREATE_SUBGROUP` or admin on the parent; the author, not the
|
|
72
|
+
* relay, becomes the new subgroup's admin.
|
|
73
|
+
*/
|
|
74
|
+
export declare function groupCreatedOp(input: GroupCreatedInput): GovernanceOp;
|
|
75
|
+
/** Move `childGroupId` under `newParentId`, a root op. */
|
|
76
|
+
export declare function groupReparentedOp(childGroupId: string, newParentId: string): GovernanceOp;
|
|
77
|
+
/**
|
|
78
|
+
* Delete `rootGroupId` and its subtree, in delegable form: both cascade lists
|
|
79
|
+
* empty, for the relay to enumerate.
|
|
80
|
+
*/
|
|
81
|
+
export declare function groupDeletedOp(rootGroupId: string): GovernanceOp;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode the governance ops a member may have a relay publish for them.
|
|
3
|
+
*
|
|
4
|
+
* A governance warrant commits to an op's **borsh bytes**, not to a description
|
|
5
|
+
* of it, so the client has to produce exactly the bytes the node decodes. These
|
|
6
|
+
* encoders cover the delegable ops (core's `GroupOp::delegable_form` and
|
|
7
|
+
* `RootOp::delegable_form`) that a keyholder app is likely to need. Every
|
|
8
|
+
* discriminant is a variant's position in core's enum, which is how borsh
|
|
9
|
+
* encodes a Rust enum; they are pinned against core's own vectors in the test.
|
|
10
|
+
*
|
|
11
|
+
* **Delegable form.** A removal, a leave and a cascade delete carry fields only
|
|
12
|
+
* the publisher can compute (post-state hashes, the enumerated subtree). The
|
|
13
|
+
* member signs them cleared, and these encoders only ever produce the cleared
|
|
14
|
+
* form, which is the one a relay accepts. The relay fills them in afterwards.
|
|
15
|
+
*
|
|
16
|
+
* Sources in core, for the next person to check a discriminant:
|
|
17
|
+
* `crates/governance-types/src/lib.rs` (`GroupOp`, `RootOp`),
|
|
18
|
+
* `crates/primitives/src/context.rs` (`GroupMemberRole`), and the vectors in
|
|
19
|
+
* `crates/governance-types/src/tests.rs` (`delegated_governance_op_vectors_are_stable`).
|
|
20
|
+
*/
|
|
21
|
+
import { concat, fromHex, u32le } from '../crypto/internal.js';
|
|
22
|
+
/**
|
|
23
|
+
* `GroupMemberRole` discriminants. The two TEE roles (3 and 4) are minted only
|
|
24
|
+
* by attestation admission and never by a member's op, so they are not offered.
|
|
25
|
+
*/
|
|
26
|
+
const ROLE = {
|
|
27
|
+
Admin: 0,
|
|
28
|
+
Member: 1,
|
|
29
|
+
ReadOnly: 2,
|
|
30
|
+
};
|
|
31
|
+
/** `GroupOp` discriminants, by position in core's enum. */
|
|
32
|
+
const GROUP_OP = {
|
|
33
|
+
MemberAdded: 1,
|
|
34
|
+
MemberRemoved: 2,
|
|
35
|
+
MemberLeft: 3,
|
|
36
|
+
MemberRoleSet: 4,
|
|
37
|
+
};
|
|
38
|
+
/** `RootOp` discriminants, by position in core's enum. */
|
|
39
|
+
const ROOT_OP = {
|
|
40
|
+
GroupCreated: 0,
|
|
41
|
+
GroupReparented: 1,
|
|
42
|
+
GroupDeleted: 2,
|
|
43
|
+
};
|
|
44
|
+
/** A post-state hash left for the relay to compute: 32 zero bytes. */
|
|
45
|
+
const CLEARED_HASH = new Uint8Array(32);
|
|
46
|
+
/** An empty borsh `Vec`: a zero `u32` count. */
|
|
47
|
+
const EMPTY_VEC = u32le(0);
|
|
48
|
+
function role(value) {
|
|
49
|
+
const byte = ROLE[value];
|
|
50
|
+
if (byte === undefined) {
|
|
51
|
+
throw new Error(`role must be Admin, Member or ReadOnly, got ${String(value)}`);
|
|
52
|
+
}
|
|
53
|
+
return new Uint8Array([byte]);
|
|
54
|
+
}
|
|
55
|
+
function tag(byte) {
|
|
56
|
+
return new Uint8Array([byte]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Add `member` (an account, hex) to the group with `role`.
|
|
60
|
+
*
|
|
61
|
+
* Applied as the author: adding a `Member` needs `MANAGE_MEMBERS` or admin,
|
|
62
|
+
* adding an `Admin` needs admin.
|
|
63
|
+
*/
|
|
64
|
+
export function memberAddedOp(member, memberRole) {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'group',
|
|
67
|
+
bytes: concat(tag(GROUP_OP.MemberAdded), fromHex(member, 'member', 32), role(memberRole)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove `member` (an account, hex) from the group, in delegable form: both
|
|
72
|
+
* post-state claims cleared, for the relay to compute from its own view.
|
|
73
|
+
*/
|
|
74
|
+
export function memberRemovedOp(member) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'group',
|
|
77
|
+
bytes: concat(tag(GROUP_OP.MemberRemoved), fromHex(member, 'member', 32), CLEARED_HASH, EMPTY_VEC),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The author leaves the group, in delegable form. The node applies it only when
|
|
82
|
+
* `member` is the author's own account.
|
|
83
|
+
*/
|
|
84
|
+
export function memberLeftOp(member) {
|
|
85
|
+
return {
|
|
86
|
+
kind: 'group',
|
|
87
|
+
bytes: concat(tag(GROUP_OP.MemberLeft), fromHex(member, 'member', 32), CLEARED_HASH, EMPTY_VEC),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Set `member`'s role (an account, hex). */
|
|
91
|
+
export function memberRoleSetOp(member, memberRole) {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'group',
|
|
94
|
+
bytes: concat(tag(GROUP_OP.MemberRoleSet), fromHex(member, 'member', 32), role(memberRole)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a subgroup under `parentId`, a root op published on the namespace.
|
|
99
|
+
*
|
|
100
|
+
* Needs `CAN_CREATE_SUBGROUP` or admin on the parent; the author, not the
|
|
101
|
+
* relay, becomes the new subgroup's admin.
|
|
102
|
+
*/
|
|
103
|
+
export function groupCreatedOp(input) {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'root',
|
|
106
|
+
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)),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Move `childGroupId` under `newParentId`, a root op. */
|
|
110
|
+
export function groupReparentedOp(childGroupId, newParentId) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'root',
|
|
113
|
+
bytes: concat(tag(ROOT_OP.GroupReparented), fromHex(childGroupId, 'childGroupId', 32), fromHex(newParentId, 'newParentId', 32)),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Delete `rootGroupId` and its subtree, in delegable form: both cascade lists
|
|
118
|
+
* empty, for the relay to enumerate.
|
|
119
|
+
*/
|
|
120
|
+
export function groupDeletedOp(rootGroupId) {
|
|
121
|
+
return {
|
|
122
|
+
kind: 'root',
|
|
123
|
+
bytes: concat(tag(ROOT_OP.GroupDeleted), fromHex(rootGroupId, 'rootGroupId', 32), EMPTY_VEC, EMPTY_VEC),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint a governance warrant: the author's consent for one relay to publish one
|
|
3
|
+
* governance op in the author's name, once.
|
|
4
|
+
*
|
|
5
|
+
* The intent warrant lets an account with no node write to a context and the
|
|
6
|
+
* creation warrant lets it create one. This covers the governance around them:
|
|
7
|
+
* adding the other person to a DM, creating a channel's subgroup, changing a
|
|
8
|
+
* role. The relay wraps the op in `OnBehalf` and publishes it; every peer applies
|
|
9
|
+
* the inner op **as the author**, so the author's own authority decides
|
|
10
|
+
* (`MANAGE_MEMBERS` to add a member, `CAN_CREATE_SUBGROUP` to create a subgroup,
|
|
11
|
+
* and so on) and the relay needs only standing to act for members.
|
|
12
|
+
*
|
|
13
|
+
* **It commits to the op's bytes.** The warrant carries
|
|
14
|
+
* `H(kind || op bytes)` over the op's delegable form (see `governance-op.ts`), so
|
|
15
|
+
* what the op does, to whom and where is signed byte for byte and the relay
|
|
16
|
+
* chooses nothing about it. `kind` is inside the commitment, so a group op can
|
|
17
|
+
* never be presented as a root op whose bytes happen to coincide.
|
|
18
|
+
*
|
|
19
|
+
* **`scope` is where the op is published**: the group itself for a group op,
|
|
20
|
+
* the namespace for a root op.
|
|
21
|
+
*
|
|
22
|
+
* Same primitives and the same two-widths trap as `warrant.ts`: counts inside
|
|
23
|
+
* the encoding are borsh `u32`, counts in the signing preimage are `u64`.
|
|
24
|
+
*
|
|
25
|
+
* **The byte contract is pinned in core**, at
|
|
26
|
+
* `crates/account/src/tests/governance_wire_fixture.rs`, and this module's test
|
|
27
|
+
* asserts the same vectors. A drift would surface at a relay as a 400.
|
|
28
|
+
*/
|
|
29
|
+
import type { GovernanceOp, GovernanceOpKind } from './governance-op.js';
|
|
30
|
+
/** What a governance warrant authorises. */
|
|
31
|
+
export interface GovernanceWarrantInput {
|
|
32
|
+
/**
|
|
33
|
+
* Where the op is published, hex (32 bytes): the group for a group op, the
|
|
34
|
+
* namespace for a root op. The relay refuses a warrant presented anywhere else.
|
|
35
|
+
*/
|
|
36
|
+
scope: string;
|
|
37
|
+
/** The op, from one of the encoders in `governance-op.ts`. Its kind is signed over. */
|
|
38
|
+
op: GovernanceOp;
|
|
39
|
+
/** The author's account, hex: whose authority the op is applied under. */
|
|
40
|
+
authorAccount: string;
|
|
41
|
+
/** The relay authorised to publish it, hex. An account, not a key. */
|
|
42
|
+
executor: string;
|
|
43
|
+
/**
|
|
44
|
+
* Monotonic per author **device**, spent in a per-group ledger. The same
|
|
45
|
+
* nonce source `RelayClient` uses for every other warrant is correct.
|
|
46
|
+
*/
|
|
47
|
+
nonce: number | bigint;
|
|
48
|
+
/** Unix seconds after which the relay must refuse it. */
|
|
49
|
+
notAfter: number | bigint;
|
|
50
|
+
/** Account-log heads the author saw, each hex (32 bytes). At most 64. */
|
|
51
|
+
accountHeads?: string[];
|
|
52
|
+
/** Governance heads the author's view descended from, each hex (32 bytes). At most 64. */
|
|
53
|
+
governanceFloor?: string[];
|
|
54
|
+
/** The author device's ed25519 signing secret, hex (32 bytes). Never sent. */
|
|
55
|
+
deviceSecret: string;
|
|
56
|
+
}
|
|
57
|
+
/** A governance warrant's fields, decoded from the wire encoding. */
|
|
58
|
+
export interface GovernanceWarrantFields {
|
|
59
|
+
scope: string;
|
|
60
|
+
kind: GovernanceOpKind;
|
|
61
|
+
authorAccount: string;
|
|
62
|
+
/** The author device key `signGovernanceWarrant` derived from the secret. */
|
|
63
|
+
deviceKey: string;
|
|
64
|
+
executor: string;
|
|
65
|
+
opHash: string;
|
|
66
|
+
accountHeads: string[];
|
|
67
|
+
governanceFloor: string[];
|
|
68
|
+
nonce: bigint;
|
|
69
|
+
notAfter: bigint;
|
|
70
|
+
signature: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The commitment a governance warrant carries in place of the op:
|
|
74
|
+
* `H(kind || op bytes)` under its own domain.
|
|
75
|
+
*/
|
|
76
|
+
export declare function governanceOpHash(op: GovernanceOp): Promise<Uint8Array>;
|
|
77
|
+
/** The already-decoded fields the signature covers. Internal. */
|
|
78
|
+
export interface GovernancePreimageParts {
|
|
79
|
+
scope: Uint8Array;
|
|
80
|
+
kind: GovernanceOpKind;
|
|
81
|
+
authorAccount: Uint8Array;
|
|
82
|
+
deviceKey: Uint8Array;
|
|
83
|
+
executor: Uint8Array;
|
|
84
|
+
opHash: Uint8Array;
|
|
85
|
+
accountHeads: Uint8Array[];
|
|
86
|
+
governanceFloor: Uint8Array[];
|
|
87
|
+
nonce: number | bigint;
|
|
88
|
+
notAfter: number | bigint;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The bytes the device key signs. Exported for the conformance test only, not
|
|
92
|
+
* from the package root.
|
|
93
|
+
*/
|
|
94
|
+
export declare function governanceWarrantPreimage(p: GovernancePreimageParts): Promise<Uint8Array>;
|
|
95
|
+
/**
|
|
96
|
+
* Sign a governance warrant and return it hex-encoded (borsh
|
|
97
|
+
* `GovernanceWarrant`), ready for the relay alongside the op's own bytes.
|
|
98
|
+
*/
|
|
99
|
+
export declare function signGovernanceWarrant(input: GovernanceWarrantInput): Promise<string>;
|
|
100
|
+
/**
|
|
101
|
+
* Decode a hex governance warrant back into its fields.
|
|
102
|
+
*
|
|
103
|
+
* Refuses truncated or trailing bytes and an unknown kind. Does not verify the
|
|
104
|
+
* signature.
|
|
105
|
+
*/
|
|
106
|
+
export declare function parseGovernanceWarrant(warrant: string): GovernanceWarrantFields;
|