@calimero-network/mero-js 22.3.0 → 22.5.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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Mint a creation warrant: the author's consent for one relay to create one
3
+ * context in one group, once.
4
+ *
5
+ * The intent warrant in `warrant.ts` lets an account with no node *write* to a
6
+ * context; this lets it *create* one. The relay executes the warrant and
7
+ * publishes `GroupOp::ContextRegisteredOnBehalf` carrying it, and every peer
8
+ * checks the **author's** `CAN_CREATE_CONTEXT` (or admin) — the relay needs no
9
+ * create rights of its own, only standing to act for members. The app's `init`
10
+ * runs as the author's account.
11
+ *
12
+ * Same primitives and the same two-widths trap as `warrant.ts`: counts **inside
13
+ * the encoding** are borsh `u32`, counts the **signing preimage** hashes are
14
+ * `u64`. The two `Option<String>` labels add a third shape: on the wire an
15
+ * option is a one-byte tag followed (when present) by a `u32`-prefixed string,
16
+ * while the preimage hashes the tag and the bytes as two separate parts, the
17
+ * bytes empty when absent. That is what keeps `None` and `Some("")` distinct in
18
+ * both.
19
+ *
20
+ * **The byte contract is pinned in core**, at
21
+ * `crates/account/src/tests/creation_wire_fixture.rs`, and this module's test
22
+ * asserts the same vectors. A drift would surface at a relay as a 403.
23
+ */
24
+ import { concat, derivePublicKey, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
25
+ import { citedHeads } from './warrant.js';
26
+ const SIGN_DOMAIN = new TextEncoder().encode('calimero.context-creation-warrant.v1');
27
+ const INIT_DOMAIN = new TextEncoder().encode('calimero.context-creation.init.v1');
28
+ /** core's cap on `service_name` and `name`, in UTF-8 bytes. */
29
+ const MAX_LABEL_BYTES = 256;
30
+ /**
31
+ * The commitment a creation warrant carries in place of the init arguments.
32
+ *
33
+ * Hashes `JSON.stringify(initArgs)`, the same bytes `RelayClient.createContext`
34
+ * sends, under its own domain.
35
+ */
36
+ export async function creationInitHash(initArgs) {
37
+ const bytes = new TextEncoder().encode(JSON.stringify(initArgs));
38
+ return domainHash(INIT_DOMAIN, [bytes]);
39
+ }
40
+ /**
41
+ * The bytes the device key signs. Exported for the conformance test only — not
42
+ * from the package root.
43
+ */
44
+ export async function creationWarrantPreimage(p) {
45
+ // An option is two parts here — its tag, then its bytes (empty when absent) —
46
+ // so `None` and `Some("")` hash differently. u64 counts HERE; u32 on the wire.
47
+ return domainHash(SIGN_DOMAIN, [
48
+ p.group,
49
+ p.seed,
50
+ p.authorAccount,
51
+ p.deviceKey,
52
+ p.executor,
53
+ p.applicationId,
54
+ new Uint8Array([p.serviceName === null ? 0 : 1]),
55
+ p.serviceName ?? new Uint8Array(0),
56
+ new Uint8Array([p.name === null ? 0 : 1]),
57
+ p.name ?? new Uint8Array(0),
58
+ p.initHash,
59
+ u64le(p.accountHeads.length),
60
+ ...p.accountHeads,
61
+ u64le(p.governanceFloor.length),
62
+ ...p.governanceFloor,
63
+ u64le(p.nonce),
64
+ u64le(p.notAfter),
65
+ ]);
66
+ }
67
+ /**
68
+ * Sign a creation warrant and return it hex-encoded, with its seed.
69
+ *
70
+ * Returns the encoding rather than an object for the reason `signWarrant`
71
+ * does: the signature covers exactly these bytes.
72
+ */
73
+ export async function signCreationWarrant(input) {
74
+ const group = fromHex(input.group, 'group', 32);
75
+ const seed = input.seed === undefined
76
+ ? crypto.getRandomValues(new Uint8Array(32))
77
+ : fromHex(input.seed, 'seed', 32);
78
+ const authorAccount = fromHex(input.authorAccount, 'authorAccount', 32);
79
+ const executor = fromHex(input.executor, 'executor', 32);
80
+ const applicationId = fromHex(input.applicationId, 'applicationId', 32);
81
+ const serviceName = label(input.serviceName, 'serviceName');
82
+ const name = label(input.name, 'name');
83
+ const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
84
+ const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
85
+ const key = await importSigningKey(input.deviceSecret);
86
+ const deviceKey = await derivePublicKey(input.deviceSecret);
87
+ const initHash = await creationInitHash(input.initArgs);
88
+ const preimage = await creationWarrantPreimage({
89
+ group,
90
+ seed,
91
+ authorAccount,
92
+ deviceKey,
93
+ executor,
94
+ applicationId,
95
+ serviceName,
96
+ name,
97
+ initHash,
98
+ accountHeads,
99
+ governanceFloor,
100
+ nonce: input.nonce,
101
+ notAfter: input.notAfter,
102
+ });
103
+ const signature = new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, preimage));
104
+ const warrant = hex(concat(group, seed, authorAccount, deviceKey, executor, applicationId, borshOption(serviceName), borshOption(name), initHash, u32le(accountHeads.length), ...accountHeads, u32le(governanceFloor.length), ...governanceFloor, u64le(input.nonce), u64le(input.notAfter), signature));
105
+ return { warrant, seed: hex(seed) };
106
+ }
107
+ /**
108
+ * Decode a hex creation warrant back into its fields.
109
+ *
110
+ * Refuses truncated or trailing bytes rather than reading a neighbouring field.
111
+ * Does not verify the signature.
112
+ */
113
+ export function parseCreationWarrant(warrant) {
114
+ const clean = warrant.trim();
115
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
116
+ throw new Error('creation warrant must be an even number of hex characters');
117
+ }
118
+ const bytes = new Uint8Array(clean.length / 2);
119
+ for (let i = 0; i < bytes.length; i += 1) {
120
+ bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
121
+ }
122
+ const view = new DataView(bytes.buffer);
123
+ let o = 0;
124
+ const take = (n, what) => {
125
+ if (o + n > bytes.length) {
126
+ throw new Error(`creation warrant ends inside ${what} (${bytes.length} bytes)`);
127
+ }
128
+ const out = bytes.subarray(o, o + n);
129
+ o += n;
130
+ return out;
131
+ };
132
+ const u32 = (what) => view.getUint32(take(4, what).byteOffset, true);
133
+ const u64 = (what) => view.getBigUint64(take(8, what).byteOffset, true);
134
+ const option = (what) => {
135
+ const tag = take(1, what)[0];
136
+ if (tag === 0)
137
+ return null;
138
+ if (tag !== 1)
139
+ throw new Error(`${what} has option tag ${tag}, expected 0 or 1`);
140
+ const len = u32(what);
141
+ if (len > MAX_LABEL_BYTES) {
142
+ throw new Error(`${what} is ${len} bytes, over the ${MAX_LABEL_BYTES} a node accepts`);
143
+ }
144
+ return new TextDecoder('utf-8', { fatal: true }).decode(take(len, what));
145
+ };
146
+ const heads = (what) => {
147
+ const count = u32(what);
148
+ if (count > 64) {
149
+ throw new Error(`${what} cites ${count} heads, over the 64 a node accepts`);
150
+ }
151
+ return Array.from({ length: count }, () => hex(take(32, what)));
152
+ };
153
+ const fields = {
154
+ group: hex(take(32, 'group')),
155
+ seed: hex(take(32, 'seed')),
156
+ authorAccount: hex(take(32, 'authorAccount')),
157
+ deviceKey: hex(take(32, 'deviceKey')),
158
+ executor: hex(take(32, 'executor')),
159
+ applicationId: hex(take(32, 'applicationId')),
160
+ serviceName: option('serviceName'),
161
+ name: option('name'),
162
+ initHash: hex(take(32, 'initHash')),
163
+ accountHeads: heads('accountHeads'),
164
+ governanceFloor: heads('governanceFloor'),
165
+ nonce: u64('nonce'),
166
+ notAfter: u64('notAfter'),
167
+ signature: hex(take(64, 'signature')),
168
+ };
169
+ if (o !== bytes.length) {
170
+ throw new Error(`creation warrant is ${bytes.length} bytes but its fields account for ${o}`);
171
+ }
172
+ return fields;
173
+ }
174
+ /** UTF-8 encode an optional label, refusing one longer than a node accepts. */
175
+ function label(value, what) {
176
+ if (value === undefined)
177
+ return null;
178
+ const bytes = new TextEncoder().encode(value);
179
+ if (bytes.length > MAX_LABEL_BYTES) {
180
+ throw new Error(`${what} is ${bytes.length} UTF-8 bytes, over the ${MAX_LABEL_BYTES} a node accepts`);
181
+ }
182
+ return bytes;
183
+ }
184
+ /** Borsh `Option<String>`: `0x00`, or `0x01` then a u32 length and the bytes. */
185
+ function borshOption(bytes) {
186
+ if (bytes === null)
187
+ return new Uint8Array([0]);
188
+ return concat(new Uint8Array([1]), u32le(bytes.length), bytes);
189
+ }
@@ -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;
@@ -0,0 +1,159 @@
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 { concat, derivePublicKey, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
30
+ import { citedHeads } from './warrant.js';
31
+ const SIGN_DOMAIN = new TextEncoder().encode('calimero.governance-warrant.v1');
32
+ const OP_DOMAIN = new TextEncoder().encode('calimero.governance-warrant.op.v1');
33
+ /** core's `GovernanceOpKind` discriminants. */
34
+ const KIND_BYTE = { group: 0, root: 1 };
35
+ /**
36
+ * The commitment a governance warrant carries in place of the op:
37
+ * `H(kind || op bytes)` under its own domain.
38
+ */
39
+ export async function governanceOpHash(op) {
40
+ return domainHash(OP_DOMAIN, [new Uint8Array([kindByte(op.kind)]), op.bytes]);
41
+ }
42
+ /**
43
+ * The bytes the device key signs. Exported for the conformance test only, not
44
+ * from the package root.
45
+ */
46
+ export async function governanceWarrantPreimage(p) {
47
+ // u64 counts HERE; u32 on the wire. The counts keep the two adjacent head
48
+ // lists from being relabelled into each other.
49
+ return domainHash(SIGN_DOMAIN, [
50
+ p.scope,
51
+ new Uint8Array([kindByte(p.kind)]),
52
+ p.authorAccount,
53
+ p.deviceKey,
54
+ p.executor,
55
+ p.opHash,
56
+ u64le(p.accountHeads.length),
57
+ ...p.accountHeads,
58
+ u64le(p.governanceFloor.length),
59
+ ...p.governanceFloor,
60
+ u64le(p.nonce),
61
+ u64le(p.notAfter),
62
+ ]);
63
+ }
64
+ /**
65
+ * Sign a governance warrant and return it hex-encoded (borsh
66
+ * `GovernanceWarrant`), ready for the relay alongside the op's own bytes.
67
+ */
68
+ export async function signGovernanceWarrant(input) {
69
+ const scope = fromHex(input.scope, 'scope', 32);
70
+ const authorAccount = fromHex(input.authorAccount, 'authorAccount', 32);
71
+ const executor = fromHex(input.executor, 'executor', 32);
72
+ const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
73
+ const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
74
+ const kind = kindByte(input.op.kind);
75
+ const key = await importSigningKey(input.deviceSecret);
76
+ const deviceKey = await derivePublicKey(input.deviceSecret);
77
+ const opHash = await governanceOpHash(input.op);
78
+ const preimage = await governanceWarrantPreimage({
79
+ scope,
80
+ kind: input.op.kind,
81
+ authorAccount,
82
+ deviceKey,
83
+ executor,
84
+ opHash,
85
+ accountHeads,
86
+ governanceFloor,
87
+ nonce: input.nonce,
88
+ notAfter: input.notAfter,
89
+ });
90
+ const signature = new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, preimage));
91
+ return hex(concat(scope, new Uint8Array([kind]), authorAccount, deviceKey, executor, opHash, u32le(accountHeads.length), ...accountHeads, u32le(governanceFloor.length), ...governanceFloor, u64le(input.nonce), u64le(input.notAfter), signature));
92
+ }
93
+ /**
94
+ * Decode a hex governance warrant back into its fields.
95
+ *
96
+ * Refuses truncated or trailing bytes and an unknown kind. Does not verify the
97
+ * signature.
98
+ */
99
+ export function parseGovernanceWarrant(warrant) {
100
+ const clean = warrant.trim();
101
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
102
+ throw new Error('governance warrant must be an even number of hex characters');
103
+ }
104
+ const bytes = new Uint8Array(clean.length / 2);
105
+ for (let i = 0; i < bytes.length; i += 1) {
106
+ bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
107
+ }
108
+ const view = new DataView(bytes.buffer);
109
+ let o = 0;
110
+ const take = (n, what) => {
111
+ if (o + n > bytes.length) {
112
+ throw new Error(`governance warrant ends inside ${what} (${bytes.length} bytes)`);
113
+ }
114
+ const out = bytes.subarray(o, o + n);
115
+ o += n;
116
+ return out;
117
+ };
118
+ const u32 = (what) => view.getUint32(take(4, what).byteOffset, true);
119
+ const u64 = (what) => view.getBigUint64(take(8, what).byteOffset, true);
120
+ const heads = (what) => {
121
+ const count = u32(what);
122
+ if (count > 64) {
123
+ throw new Error(`${what} cites ${count} heads, over the 64 a node accepts`);
124
+ }
125
+ return Array.from({ length: count }, () => hex(take(32, what)));
126
+ };
127
+ const kind = () => {
128
+ const byte = take(1, 'kind')[0];
129
+ if (byte === 0)
130
+ return 'group';
131
+ if (byte === 1)
132
+ return 'root';
133
+ throw new Error(`governance warrant has kind ${byte}, expected 0 (group) or 1 (root)`);
134
+ };
135
+ const fields = {
136
+ scope: hex(take(32, 'scope')),
137
+ kind: kind(),
138
+ authorAccount: hex(take(32, 'authorAccount')),
139
+ deviceKey: hex(take(32, 'deviceKey')),
140
+ executor: hex(take(32, 'executor')),
141
+ opHash: hex(take(32, 'opHash')),
142
+ accountHeads: heads('accountHeads'),
143
+ governanceFloor: heads('governanceFloor'),
144
+ nonce: u64('nonce'),
145
+ notAfter: u64('notAfter'),
146
+ signature: hex(take(64, 'signature')),
147
+ };
148
+ if (o !== bytes.length) {
149
+ throw new Error(`governance warrant is ${bytes.length} bytes but its fields account for ${o}`);
150
+ }
151
+ return fields;
152
+ }
153
+ function kindByte(kind) {
154
+ const byte = KIND_BYTE[kind];
155
+ if (byte === undefined) {
156
+ throw new Error(`op kind must be 'group' or 'root', got ${String(kind)}`);
157
+ }
158
+ return byte;
159
+ }
@@ -1,2 +1,8 @@
1
1
  export { signWarrant, intentHash } from './warrant.js';
2
2
  export type { WarrantInput } from './warrant.js';
3
+ export { signCreationWarrant, parseCreationWarrant, creationInitHash, } from './creation-warrant.js';
4
+ export type { CreationWarrantInput, CreationWarrantFields, SignedCreationWarrant, } from './creation-warrant.js';
5
+ export { signGovernanceWarrant, parseGovernanceWarrant, governanceOpHash, } from './governance-warrant.js';
6
+ export type { GovernanceWarrantInput, GovernanceWarrantFields, } from './governance-warrant.js';
7
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, } from './governance-op.js';
8
+ export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, } from './governance-op.js';
@@ -1,2 +1,5 @@
1
1
  // Warrant signing — the author's half of delegated authorship
2
2
  export { signWarrant, intentHash } from './warrant.js';
3
+ export { signCreationWarrant, parseCreationWarrant, creationInitHash, } from './creation-warrant.js';
4
+ export { signGovernanceWarrant, parseGovernanceWarrant, governanceOpHash, } from './governance-warrant.js';
5
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, } from './governance-op.js';