@calimero-network/mero-js 22.5.1 → 22.7.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.
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * Sources in core, for the next person to check a discriminant:
17
17
  * `crates/governance-types/src/lib.rs` (`GroupOp`, `RootOp`),
18
- * `crates/primitives/src/context.rs` (`GroupMemberRole`), and the vectors in
18
+ * `crates/primitives/src/context.rs` (`GroupMemberRole`),
19
+ * `crates/account/src/namespace_id.rs` (`founded_namespace_id`), and the vectors in
19
20
  * `crates/governance-types/src/tests.rs` (`delegated_governance_op_vectors_are_stable`).
20
21
  */
21
22
  /**
@@ -51,6 +52,15 @@ export declare function memberRemovedOp(member: string): GovernanceOp;
51
52
  export declare function memberLeftOp(member: string): GovernanceOp;
52
53
  /** Set `member`'s role (an account, hex). */
53
54
  export declare function memberRoleSetOp(member: string, memberRole: GovernanceMemberRole): GovernanceOp;
55
+ /**
56
+ * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
57
+ * the capabilities every member holds without a per-member grant. Needs admin.
58
+ *
59
+ * The relay refuses any change to `CAN_AUTHOR_ON_BEHALF` (bit 9) through this
60
+ * path, and no namespace starts with it in its default mask, so a mask that
61
+ * sets it is refused here rather than burning a nonce on a certain 403.
62
+ */
63
+ export declare function defaultCapabilitiesSetOp(capabilities: number): GovernanceOp;
54
64
  /** What a subgroup is created with. */
55
65
  export interface GroupCreatedInput {
56
66
  /** The new subgroup's id, hex (32 bytes). Choose it fresh; a taken id is refused. */
@@ -79,3 +89,39 @@ export declare function groupReparentedOp(childGroupId: string, newParentId: str
79
89
  * empty, for the relay to enumerate.
80
90
  */
81
91
  export declare function groupDeletedOp(rootGroupId: string): GovernanceOp;
92
+ /**
93
+ * The id of the namespace `founder` founds with `salt`: core's
94
+ * `founded_namespace_id`, `domain_hash("calimero.namespace.id.v1", [founder, salt])`.
95
+ *
96
+ * Anyone holding the pair can recompute it, and only the founder's account
97
+ * hashes to it, which is why a relay founding a namespace for a member cannot
98
+ * choose the id: the member computes it and signs it.
99
+ */
100
+ export declare function foundedNamespaceId(founder: string, salt: string): Promise<string>;
101
+ /** What a namespace is founded with. */
102
+ export interface NamespaceCreatedInput {
103
+ /** The founding account, hex: the **author's**. It becomes founder, owner and admin. */
104
+ founder: string;
105
+ /**
106
+ * The founder's `AccountProof<DeviceCert>`, hex: the credential the author
107
+ * already presents as `authorProof`. Its account must be `founder` and its
108
+ * device key the one that signs the warrant; the genesis apply checks both
109
+ * and binds the founder's device from it.
110
+ */
111
+ credential: string;
112
+ /**
113
+ * The salt the namespace id is derived with, hex (32 bytes). Choose it at
114
+ * random; {@link foundedNamespaceId} gives the id it yields.
115
+ */
116
+ salt: string;
117
+ }
118
+ /**
119
+ * Found a namespace: core's `RootOp::NamespaceCreatedV2` genesis, a root op
120
+ * published on the new namespace's own log.
121
+ *
122
+ * Its delegable form is the op itself, nothing cleared, so the author signs the
123
+ * exact genesis. Borsh is `[9] || founder || credential || salt`: the credential
124
+ * is a nested struct, not a length-prefixed byte string, so it is written
125
+ * inline. The warrant's scope must be {@link foundedNamespaceId}`(founder, salt)`.
126
+ */
127
+ export declare function namespaceCreatedOp(input: NamespaceCreatedInput): GovernanceOp;
@@ -15,10 +15,12 @@
15
15
  *
16
16
  * Sources in core, for the next person to check a discriminant:
17
17
  * `crates/governance-types/src/lib.rs` (`GroupOp`, `RootOp`),
18
- * `crates/primitives/src/context.rs` (`GroupMemberRole`), and the vectors in
18
+ * `crates/primitives/src/context.rs` (`GroupMemberRole`),
19
+ * `crates/account/src/namespace_id.rs` (`founded_namespace_id`), and the vectors in
19
20
  * `crates/governance-types/src/tests.rs` (`delegated_governance_op_vectors_are_stable`).
20
21
  */
21
- import { concat, fromHex, u32le } from '../crypto/internal.js';
22
+ import { CAPABILITIES, hasCap } from '../capabilities.js';
23
+ import { concat, domainHash, fromHex, fromHexUnsized, hex, u32le } from '../crypto/internal.js';
22
24
  /**
23
25
  * `GroupMemberRole` discriminants. The two TEE roles (3 and 4) are minted only
24
26
  * by attestation admission and never by a member's op, so they are not offered.
@@ -34,13 +36,17 @@ const GROUP_OP = {
34
36
  MemberRemoved: 2,
35
37
  MemberLeft: 3,
36
38
  MemberRoleSet: 4,
39
+ DefaultCapabilitiesSet: 6,
37
40
  };
38
41
  /** `RootOp` discriminants, by position in core's enum. */
39
42
  const ROOT_OP = {
40
43
  GroupCreated: 0,
41
44
  GroupReparented: 1,
42
45
  GroupDeleted: 2,
46
+ NamespaceCreatedV2: 9,
43
47
  };
48
+ /** core's `NAMESPACE_ID_DOMAIN`, which a founded namespace id is hashed under. */
49
+ const NAMESPACE_ID_DOMAIN = new TextEncoder().encode('calimero.namespace.id.v1');
44
50
  /** A post-state hash left for the relay to compute: 32 zero bytes. */
45
51
  const CLEARED_HASH = new Uint8Array(32);
46
52
  /** An empty borsh `Vec`: a zero `u32` count. */
@@ -94,6 +100,27 @@ export function memberRoleSetOp(member, memberRole) {
94
100
  bytes: concat(tag(GROUP_OP.MemberRoleSet), fromHex(member, 'member', 32), role(memberRole)),
95
101
  };
96
102
  }
103
+ /**
104
+ * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
105
+ * the capabilities every member holds without a per-member grant. Needs admin.
106
+ *
107
+ * The relay refuses any change to `CAN_AUTHOR_ON_BEHALF` (bit 9) through this
108
+ * path, and no namespace starts with it in its default mask, so a mask that
109
+ * sets it is refused here rather than burning a nonce on a certain 403.
110
+ */
111
+ export function defaultCapabilitiesSetOp(capabilities) {
112
+ if (!Number.isInteger(capabilities) || capabilities < 0 || capabilities > 4294967295) {
113
+ throw new Error(`capabilities must be a u32 bit mask, got ${String(capabilities)}`);
114
+ }
115
+ // Never changed through a relay.
116
+ if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
117
+ throw new Error('capabilities may not include CAN_AUTHOR_ON_BEHALF (512): a relay never carries a change to it');
118
+ }
119
+ return {
120
+ kind: 'group',
121
+ bytes: concat(tag(GROUP_OP.DefaultCapabilitiesSet), u32le(capabilities)),
122
+ };
123
+ }
97
124
  /**
98
125
  * Create a subgroup under `parentId`, a root op published on the namespace.
99
126
  *
@@ -123,3 +150,36 @@ export function groupDeletedOp(rootGroupId) {
123
150
  bytes: concat(tag(ROOT_OP.GroupDeleted), fromHex(rootGroupId, 'rootGroupId', 32), EMPTY_VEC, EMPTY_VEC),
124
151
  };
125
152
  }
153
+ /**
154
+ * The id of the namespace `founder` founds with `salt`: core's
155
+ * `founded_namespace_id`, `domain_hash("calimero.namespace.id.v1", [founder, salt])`.
156
+ *
157
+ * Anyone holding the pair can recompute it, and only the founder's account
158
+ * hashes to it, which is why a relay founding a namespace for a member cannot
159
+ * choose the id: the member computes it and signs it.
160
+ */
161
+ export async function foundedNamespaceId(founder, salt) {
162
+ return hex(await domainHash(NAMESPACE_ID_DOMAIN, [
163
+ fromHex(founder, 'founder', 32),
164
+ fromHex(salt, 'salt', 32),
165
+ ]));
166
+ }
167
+ /**
168
+ * Found a namespace: core's `RootOp::NamespaceCreatedV2` genesis, a root op
169
+ * published on the new namespace's own log.
170
+ *
171
+ * Its delegable form is the op itself, nothing cleared, so the author signs the
172
+ * exact genesis. Borsh is `[9] || founder || credential || salt`: the credential
173
+ * is a nested struct, not a length-prefixed byte string, so it is written
174
+ * inline. The warrant's scope must be {@link foundedNamespaceId}`(founder, salt)`.
175
+ */
176
+ export function namespaceCreatedOp(input) {
177
+ const credential = fromHexUnsized(input.credential, 'credential');
178
+ if (credential.length === 0) {
179
+ throw new Error('credential must be the founder\'s AccountProof<DeviceCert>, got nothing');
180
+ }
181
+ return {
182
+ kind: 'root',
183
+ bytes: concat(tag(ROOT_OP.NamespaceCreatedV2), fromHex(input.founder, 'founder', 32), credential, fromHex(input.salt, 'salt', 32)),
184
+ };
185
+ }
@@ -26,6 +26,7 @@
26
26
  * `crates/account/src/tests/governance_wire_fixture.rs`, and this module's test
27
27
  * asserts the same vectors. A drift would surface at a relay as a 400.
28
28
  */
29
+ import { type Signer } from '../signer/signer.js';
29
30
  import type { GovernanceOp, GovernanceOpKind } from './governance-op.js';
30
31
  /** What a governance warrant authorises. */
31
32
  export interface GovernanceWarrantInput {
@@ -51,15 +52,28 @@ export interface GovernanceWarrantInput {
51
52
  accountHeads?: string[];
52
53
  /** Governance heads the author's view descended from, each hex (32 bytes). At most 64. */
53
54
  governanceFloor?: string[];
54
- /** The author device's ed25519 signing secret, hex (32 bytes). Never sent. */
55
- deviceSecret: string;
55
+ /**
56
+ * The author device's ed25519 signing secret, hex (32 bytes). Never sent.
57
+ *
58
+ * Mutually exclusive with {@link GovernanceWarrantInput.signer}: exactly one is required.
59
+ * Prefer `signer` in a browser, since a secret held as a string is readable
60
+ * by anything on the origin.
61
+ */
62
+ deviceSecret?: string;
63
+ /**
64
+ * The author device's signer, in place of {@link GovernanceWarrantInput.deviceSecret}: for a
65
+ * key that cannot be exported to hex (a non-extractable WebCrypto key, a
66
+ * passkey, a hardware or remote signer). Its `publicKey` becomes the
67
+ * warrant's `author_device_key`, and it signs the 32-byte preimage as is.
68
+ */
69
+ signer?: Signer;
56
70
  }
57
71
  /** A governance warrant's fields, decoded from the wire encoding. */
58
72
  export interface GovernanceWarrantFields {
59
73
  scope: string;
60
74
  kind: GovernanceOpKind;
61
75
  authorAccount: string;
62
- /** The author device key `signGovernanceWarrant` derived from the secret. */
76
+ /** The author device key: the signer's public key. */
63
77
  deviceKey: string;
64
78
  executor: string;
65
79
  opHash: string;
@@ -26,8 +26,9 @@
26
26
  * `crates/account/src/tests/governance_wire_fixture.rs`, and this module's test
27
27
  * asserts the same vectors. A drift would surface at a relay as a 400.
28
28
  */
29
- import { concat, derivePublicKey, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
30
- import { citedHeads } from './warrant.js';
29
+ import { concat, domainHash, fromHex, hex, u32le, u64le } from '../crypto/internal.js';
30
+ import { resolveSigner } from '../signer/signer.js';
31
+ import { checkedSignature, citedHeads } from './warrant.js';
31
32
  const SIGN_DOMAIN = new TextEncoder().encode('calimero.governance-warrant.v1');
32
33
  const OP_DOMAIN = new TextEncoder().encode('calimero.governance-warrant.op.v1');
33
34
  /** core's `GovernanceOpKind` discriminants. */
@@ -72,8 +73,8 @@ export async function signGovernanceWarrant(input) {
72
73
  const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
73
74
  const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
74
75
  const kind = kindByte(input.op.kind);
75
- const key = await importSigningKey(input.deviceSecret);
76
- const deviceKey = await derivePublicKey(input.deviceSecret);
76
+ const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
77
+ const deviceKey = fromHex(signer.publicKey, 'signer.publicKey', 32);
77
78
  const opHash = await governanceOpHash(input.op);
78
79
  const preimage = await governanceWarrantPreimage({
79
80
  scope,
@@ -87,7 +88,7 @@ export async function signGovernanceWarrant(input) {
87
88
  nonce: input.nonce,
88
89
  notAfter: input.notAfter,
89
90
  });
90
- const signature = new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, preimage));
91
+ const signature = checkedSignature(await signer.sign(preimage));
91
92
  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
  }
93
94
  /**
@@ -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, } from './governance-op.js';
8
- export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, } from './governance-op.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';
@@ -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, } from './governance-op.js';
5
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
@@ -27,6 +27,7 @@
27
27
  * this producing well-formed warrants whose signatures verify nowhere, surfacing
28
28
  * at a relay as a 403: an authorization refusal, nowhere near the cause.
29
29
  */
30
+ import { type Signer } from '../signer/signer.js';
30
31
  /** Ed25519 PKCS#8 prefix, so a raw 32-byte seed can be imported by WebCrypto. */
31
32
  /** What a warrant authorises. */
32
33
  export interface WarrantInput {
@@ -103,8 +104,18 @@ export interface WarrantInput {
103
104
  *
104
105
  * Never sent anywhere. It signs locally and only the signature travels, which
105
106
  * is the whole reason a warrant can be minted by something holding no node.
107
+ *
108
+ * Mutually exclusive with {@link WarrantInput.signer}: exactly one is
109
+ * required. Prefer `signer` in a browser, since a secret held as a string is
110
+ * readable by anything on the origin.
111
+ */
112
+ deviceSecret?: string;
113
+ /**
114
+ * The author device's signer, in place of {@link WarrantInput.deviceSecret}:
115
+ * for a key that cannot be exported to hex (a non-extractable WebCrypto key,
116
+ * a passkey, a hardware or remote signer).
106
117
  */
107
- deviceSecret: string;
118
+ signer?: Signer;
108
119
  }
109
120
  /**
110
121
  * The commitment a warrant carries in place of the intent itself.
@@ -125,6 +136,14 @@ export declare function signWarrant(input: WarrantInput): Promise<string>;
125
136
  * Decode a cited-head list, refusing one longer than a node will accept.
126
137
  * Shared with `creation-warrant.ts`; internal, not exported from the package root.
127
138
  */
139
+ /**
140
+ * A signer's output, refused unless it is a 64-byte Ed25519 signature.
141
+ *
142
+ * The signature is spliced into the encoding at a fixed width, so a signer that
143
+ * returned anything else would yield a warrant that parses as garbage at a
144
+ * relay, far from the signer at fault. Internal.
145
+ */
146
+ export declare function checkedSignature(signature: Uint8Array): Uint8Array;
128
147
  export declare function citedHeads(heads: string[] | undefined, label: string): Uint8Array[];
129
148
  /** A warrant's fields, as hex, decoded from the wire encoding. */
130
149
  export interface WarrantFields {
@@ -27,7 +27,8 @@
27
27
  * this producing well-formed warrants whose signatures verify nowhere, surfacing
28
28
  * at a relay as a 403: an authorization refusal, nowhere near the cause.
29
29
  */
30
- import { concat, derivePublicKey, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
30
+ import { concat, domainHash, fromHex, hex, u32le, u64le } from '../crypto/internal.js';
31
+ import { resolveSigner } from '../signer/signer.js';
31
32
  const SIGN_DOMAIN = new TextEncoder().encode('calimero.warrant.v2');
32
33
  const INTENT_DOMAIN = new TextEncoder().encode('calimero.warrant.intent.v1');
33
34
  /**
@@ -61,8 +62,8 @@ export async function signWarrant(input) {
61
62
  const appVersion = fromHex(input.appVersion ?? UNPINNED_APP, 'appVersion', 32);
62
63
  const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
63
64
  const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
64
- const key = await importSigningKey(input.deviceSecret);
65
- const publicKey = await derivePublicKey(input.deviceSecret);
65
+ const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
66
+ const publicKey = fromHex(signer.publicKey, 'signer.publicKey', 32);
66
67
  const method = new TextEncoder().encode(input.method);
67
68
  const commitment = await intentHash(input.method, input.argsJson);
68
69
  const nonce = u64le(input.nonce);
@@ -87,7 +88,7 @@ export async function signWarrant(input) {
87
88
  nonce,
88
89
  notAfter,
89
90
  ]);
90
- const signature = new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, preimage));
91
+ const signature = checkedSignature(await signer.sign(preimage));
91
92
  // Borsh: a `String` is a u32 length then its UTF-8 bytes; a `Vec<[u8; 32]>` is
92
93
  // a u32 count then the elements. u32 HERE; u64 in the preimage above.
93
94
  return hex(concat(context, authorAccount, publicKey, executor, appVersion, u32le(method.length), method, commitment, u32le(accountHeads.length), ...accountHeads, u32le(governanceFloor.length), ...governanceFloor, nonce, notAfter, signature));
@@ -96,6 +97,20 @@ export async function signWarrant(input) {
96
97
  * Decode a cited-head list, refusing one longer than a node will accept.
97
98
  * Shared with `creation-warrant.ts`; internal, not exported from the package root.
98
99
  */
100
+ /**
101
+ * A signer's output, refused unless it is a 64-byte Ed25519 signature.
102
+ *
103
+ * The signature is spliced into the encoding at a fixed width, so a signer that
104
+ * returned anything else would yield a warrant that parses as garbage at a
105
+ * relay, far from the signer at fault. Internal.
106
+ */
107
+ export function checkedSignature(signature) {
108
+ if (!(signature instanceof Uint8Array) || signature.length !== 64) {
109
+ const got = signature instanceof Uint8Array ? `${signature.length} bytes` : typeof signature;
110
+ throw new Error(`signer returned ${got}, expected a 64-byte Ed25519 signature`);
111
+ }
112
+ return signature;
113
+ }
99
114
  export function citedHeads(heads, label) {
100
115
  const list = heads ?? [];
101
116
  if (list.length > MAX_CITED_HEADS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "22.5.1",
3
+ "version": "22.7.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {