@calimero-network/mero-js 24.2.0 → 24.4.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.
- package/dist/admin-api/admin-client.d.ts +7 -5
- package/dist/admin-api/admin-client.js +7 -5
- package/dist/admin-api/admin-types.d.ts +31 -4
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +38 -21
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +38 -21
- package/dist/index.mjs.map +3 -3
- package/dist/relay/relay-client.d.ts +15 -8
- package/dist/relay/relay-client.js +15 -15
- package/dist/warrant/warrant.d.ts +21 -19
- package/dist/warrant/warrant.js +30 -12
- package/package.json +1 -1
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* # The three preconditions, and why they are checked in this order
|
|
19
19
|
*
|
|
20
20
|
* `describe()` answers the first two in one call, before anything is signed:
|
|
21
|
-
* the relay's executor account and signing key
|
|
22
|
-
*
|
|
21
|
+
* the relay's executor account and signing key and the release the context's
|
|
22
|
+
* group names (which the warrant must name), and whether it holds
|
|
23
|
+
* `CAN_AUTHOR_ON_BEHALF` on the owning group. The third,
|
|
23
24
|
* that the author's account is a member, only the group knows, and it surfaces
|
|
24
25
|
* as a refusal.
|
|
25
26
|
*
|
|
@@ -43,7 +44,7 @@ export interface RelayClientConfig {
|
|
|
43
44
|
* The account the relay writes as, hex — a warrant's `executor`.
|
|
44
45
|
*
|
|
45
46
|
* Only checked against what the relay answers: a warrant also names the
|
|
46
|
-
* relay's signing key, which only discovery reports, so
|
|
47
|
+
* relay's signing key and the release, which only discovery reports, so every write asks.
|
|
47
48
|
*/
|
|
48
49
|
executorAccount?: string;
|
|
49
50
|
/** The author's account, hex — whose consent the warrant carries. */
|
|
@@ -83,9 +84,9 @@ export interface RelayClientConfig {
|
|
|
83
84
|
/**
|
|
84
85
|
* Seconds a minted warrant stays valid. Defaults to 300.
|
|
85
86
|
*
|
|
86
|
-
* Checked by the relay against its own clock and
|
|
87
|
-
*
|
|
88
|
-
*
|
|
87
|
+
* Checked by the relay against its own clock, and by peers against the
|
|
88
|
+
* delta's own signed stamp, never their clocks: this bounds how long *this*
|
|
89
|
+
* request may sit in flight before the relay runs it.
|
|
89
90
|
*/
|
|
90
91
|
ttlSeconds?: number;
|
|
91
92
|
/** Injected for tests and non-browser runtimes. Defaults to global `fetch`. */
|
|
@@ -113,6 +114,13 @@ export interface RelayDescription extends RelayExecutor {
|
|
|
113
114
|
canAuthorOnBehalf: boolean;
|
|
114
115
|
/** The group whose admin grants that capability, hex. */
|
|
115
116
|
groupId: string;
|
|
117
|
+
/**
|
|
118
|
+
* The blob id of the release the group names, hex: what a warrant must pin
|
|
119
|
+
* as `release_bytecode_id`. Discovery answers 404 while the group names none.
|
|
120
|
+
*/
|
|
121
|
+
releaseBytecodeId: string;
|
|
122
|
+
/** That release's semver, signed as the warrant's `release_version`. */
|
|
123
|
+
releaseVersion: string;
|
|
116
124
|
}
|
|
117
125
|
/** What a relay says about its ability to create contexts in one group. */
|
|
118
126
|
export interface CreationDescription extends RelayExecutor {
|
|
@@ -291,7 +299,6 @@ export declare class IntentRefusedError extends Error {
|
|
|
291
299
|
}
|
|
292
300
|
export declare class RelayClient {
|
|
293
301
|
private readonly baseUrl;
|
|
294
|
-
private executor;
|
|
295
302
|
private readonly config;
|
|
296
303
|
private readonly fetchImpl;
|
|
297
304
|
private readonly timeoutMs;
|
|
@@ -440,7 +447,7 @@ export declare class RelayClient {
|
|
|
440
447
|
foundNamespace(input: FoundNamespaceInput): Promise<FoundedNamespace>;
|
|
441
448
|
private postGovernance;
|
|
442
449
|
/**
|
|
443
|
-
* The relay's answer, checked against a configured account
|
|
450
|
+
* The relay's answer, checked against a configured account.
|
|
444
451
|
* Decoded here so a relay naming no key is refused before a nonce is taken.
|
|
445
452
|
*/
|
|
446
453
|
private checkedExecutor;
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* # The three preconditions, and why they are checked in this order
|
|
19
19
|
*
|
|
20
20
|
* `describe()` answers the first two in one call, before anything is signed:
|
|
21
|
-
* the relay's executor account and signing key
|
|
22
|
-
*
|
|
21
|
+
* the relay's executor account and signing key and the release the context's
|
|
22
|
+
* group names (which the warrant must name), and whether it holds
|
|
23
|
+
* `CAN_AUTHOR_ON_BEHALF` on the owning group. The third,
|
|
23
24
|
* that the author's account is a member, only the group knows, and it surfaces
|
|
24
25
|
* as a refusal.
|
|
25
26
|
*
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
* monotonic per-device sequence, and one minted against the wrong executor is
|
|
28
29
|
* unspendable: the number is gone and the write never happened.
|
|
29
30
|
*/
|
|
30
|
-
import { signWarrant } from '../warrant/warrant.js';
|
|
31
|
+
import { releaseVersionBytes, signWarrant } from '../warrant/warrant.js';
|
|
31
32
|
import { resolveSigner } from '../signer/signer.js';
|
|
32
33
|
import { signCreationWarrant } from '../warrant/creation-warrant.js';
|
|
33
34
|
import { signGovernanceWarrant } from '../warrant/governance-warrant.js';
|
|
@@ -137,9 +138,6 @@ export class RelayClient {
|
|
|
137
138
|
*/
|
|
138
139
|
async describe(contextId) {
|
|
139
140
|
const body = await this.json('GET', `/admin-api/contexts/${encodeURIComponent(contextId)}/intents`);
|
|
140
|
-
// Learned, not merely returned: a client that called `describe` should not
|
|
141
|
-
// then have to pass the executor back in to `execute`.
|
|
142
|
-
this.checkedExecutor(body.data);
|
|
143
141
|
return body.data;
|
|
144
142
|
}
|
|
145
143
|
/**
|
|
@@ -150,7 +148,13 @@ export class RelayClient {
|
|
|
150
148
|
* apply. Nothing accumulates and nothing is reusable.
|
|
151
149
|
*/
|
|
152
150
|
async execute(contextId, method, argsJson = {}) {
|
|
153
|
-
|
|
151
|
+
// Asked on every write: the release moves with each upgrade of the group, and a
|
|
152
|
+
// warrant pinning the one before is refused. All of it is checked before the nonce.
|
|
153
|
+
const described = await this.describe(contextId);
|
|
154
|
+
const executor = this.checkedExecutor(described);
|
|
155
|
+
const releaseBytecodeId = hex(fromHex(described.releaseBytecodeId ?? '', "the relay's releaseBytecodeId", 32));
|
|
156
|
+
const releaseVersion = described.releaseVersion ?? '';
|
|
157
|
+
releaseVersionBytes(releaseVersion);
|
|
154
158
|
// Resolved before the nonce, so a misconfigured key burns none.
|
|
155
159
|
const signer = await this.authorSigner();
|
|
156
160
|
const nonce = await this.config.nonces.next();
|
|
@@ -160,18 +164,15 @@ export class RelayClient {
|
|
|
160
164
|
authorAccount: this.config.authorAccount,
|
|
161
165
|
executor: executor.executorAccount,
|
|
162
166
|
executorKey: executor.executorKey,
|
|
167
|
+
releaseBytecodeId,
|
|
168
|
+
releaseVersion,
|
|
163
169
|
method,
|
|
164
170
|
argsJson,
|
|
165
171
|
nonce,
|
|
166
172
|
notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
|
|
167
173
|
signer,
|
|
168
174
|
});
|
|
169
|
-
const body = await this.json('POST', `/admin-api/contexts/${encodeURIComponent(contextId)}/intents`, { method, argsJson, warrant, authorProof: this.config.authorProof })
|
|
170
|
-
// A relay that re-keyed refuses every warrant naming its old key, so ask again next time.
|
|
171
|
-
if (err instanceof IntentRefusedError)
|
|
172
|
-
this.executor = undefined;
|
|
173
|
-
throw err;
|
|
174
|
-
});
|
|
175
|
+
const body = await this.json('POST', `/admin-api/contexts/${encodeURIComponent(contextId)}/intents`, { method, argsJson, warrant, authorProof: this.config.authorProof });
|
|
175
176
|
return { rootHash: body.data.rootHash, returns: body.data.returns ?? null };
|
|
176
177
|
}
|
|
177
178
|
/**
|
|
@@ -412,7 +413,7 @@ export class RelayClient {
|
|
|
412
413
|
return body.data;
|
|
413
414
|
}
|
|
414
415
|
/**
|
|
415
|
-
* The relay's answer, checked against a configured account
|
|
416
|
+
* The relay's answer, checked against a configured account.
|
|
416
417
|
* Decoded here so a relay naming no key is refused before a nonce is taken.
|
|
417
418
|
*/
|
|
418
419
|
checkedExecutor(answered) {
|
|
@@ -425,7 +426,6 @@ export class RelayClient {
|
|
|
425
426
|
throw new Error(`configured executorAccount ${configured} is not the relay's account ` +
|
|
426
427
|
`${executor.executorAccount}; a warrant naming it would be unspendable`);
|
|
427
428
|
}
|
|
428
|
-
this.executor = executor;
|
|
429
429
|
return executor;
|
|
430
430
|
}
|
|
431
431
|
/**
|
|
@@ -50,21 +50,19 @@ export interface WarrantInput {
|
|
|
50
50
|
*/
|
|
51
51
|
executorKey: string;
|
|
52
52
|
/**
|
|
53
|
-
* The
|
|
53
|
+
* The blob id of the release the context's group names, hex (32 bytes): the
|
|
54
|
+
* relay's `releaseBytecodeId` from discovery.
|
|
54
55
|
*
|
|
55
|
-
* Pins the code
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* the field set ahead of its enforcement so a signer is written once against
|
|
64
|
-
* the final bytes — but a warrant minted with the default will be refused once
|
|
65
|
-
* pinning lands, so pass the real value for anything meant to outlive it.
|
|
56
|
+
* Pins the code, so a relay cannot wait for an upgrade that widens what
|
|
57
|
+
* `method` does and then spend a warrant signed against the narrower one. The
|
|
58
|
+
* relay refuses a warrant pinning any release other than the one it runs.
|
|
59
|
+
*/
|
|
60
|
+
releaseBytecodeId: string;
|
|
61
|
+
/**
|
|
62
|
+
* That release's semver, discovery's `releaseVersion`: signed, never compared,
|
|
63
|
+
* at most 256 bytes of UTF-8. Defaults to empty, as `merod account warrant` does.
|
|
66
64
|
*/
|
|
67
|
-
|
|
65
|
+
releaseVersion?: string;
|
|
68
66
|
/**
|
|
69
67
|
* The method the relay may run.
|
|
70
68
|
*
|
|
@@ -149,6 +147,8 @@ export declare function signWarrant(input: WarrantInput): Promise<string>;
|
|
|
149
147
|
* relay, far from the signer at fault. Internal.
|
|
150
148
|
*/
|
|
151
149
|
export declare function checkedSignature(signature: Uint8Array): Uint8Array;
|
|
150
|
+
/** A release version as UTF-8, refused past what a node accepts. Shared with `RelayClient`; internal. */
|
|
151
|
+
export declare function releaseVersionBytes(version: string): Uint8Array;
|
|
152
152
|
export declare function citedHeads(heads: string[] | undefined, label: string): Uint8Array[];
|
|
153
153
|
/** A warrant's fields, as hex, decoded from the wire encoding. */
|
|
154
154
|
export interface WarrantFields {
|
|
@@ -158,7 +158,9 @@ export interface WarrantFields {
|
|
|
158
158
|
deviceKey: string;
|
|
159
159
|
executor: string;
|
|
160
160
|
executorKey: string;
|
|
161
|
-
|
|
161
|
+
releaseBytecodeId: string;
|
|
162
|
+
/** UTF-8, as hex like `method`. */
|
|
163
|
+
releaseVersion: string;
|
|
162
164
|
method: string;
|
|
163
165
|
intentHash: string;
|
|
164
166
|
accountHeads: string[];
|
|
@@ -173,11 +175,11 @@ export interface WarrantFields {
|
|
|
173
175
|
* Decode a hex warrant back into its fields.
|
|
174
176
|
*
|
|
175
177
|
* Exists because v2's offsets are no longer constants: `method` is a string and
|
|
176
|
-
* the head lists are vectors, so everything after `
|
|
177
|
-
* method's
|
|
178
|
-
* inspecting one it was handed
|
|
179
|
-
* failing, which is how the three call sites that did
|
|
180
|
-
* the method's length prefix to a nonce.
|
|
178
|
+
* the head lists are vectors, so everything after `releaseBytecodeId` moves with
|
|
179
|
+
* the release version's and the method's lengths. Anything reading a warrant by
|
|
180
|
+
* fixed offset - a test, a client inspecting one it was handed - silently reads
|
|
181
|
+
* the wrong field instead of failing, which is how the three call sites that did
|
|
182
|
+
* so passed while comparing the method's length prefix to a nonce.
|
|
181
183
|
*
|
|
182
184
|
* One copy of the layout, beside the encoder that produces it, so the two cannot
|
|
183
185
|
* drift. Internal: not exported from the package root, because a client that
|
package/dist/warrant/warrant.js
CHANGED
|
@@ -36,8 +36,8 @@ const INTENT_DOMAIN = new TextEncoder().encode('calimero.warrant.intent.v1');
|
|
|
36
36
|
* any Ed25519 work, so minting past it produces a warrant nothing will spend.
|
|
37
37
|
*/
|
|
38
38
|
const MAX_CITED_HEADS = 64;
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
39
|
+
/** core's `MAX_WARRANT_RELEASE_VERSION_LEN`, in UTF-8 bytes; a node refuses a longer one before any signature work. */
|
|
40
|
+
const MAX_RELEASE_VERSION_LEN = 256;
|
|
41
41
|
/**
|
|
42
42
|
* The commitment a warrant carries in place of the intent itself.
|
|
43
43
|
*
|
|
@@ -60,7 +60,8 @@ export async function signWarrant(input) {
|
|
|
60
60
|
const authorAccount = fromHex(input.authorAccount, 'authorAccount', 32);
|
|
61
61
|
const executor = fromHex(input.executor, 'executor', 32);
|
|
62
62
|
const executorKey = fromHex(input.executorKey, 'executorKey', 32);
|
|
63
|
-
const
|
|
63
|
+
const releaseBytecodeId = fromHex(input.releaseBytecodeId, 'releaseBytecodeId', 32);
|
|
64
|
+
const releaseVersion = releaseVersionBytes(input.releaseVersion ?? '');
|
|
64
65
|
const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
|
|
65
66
|
const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
|
|
66
67
|
const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
|
|
@@ -80,7 +81,8 @@ export async function signWarrant(input) {
|
|
|
80
81
|
publicKey,
|
|
81
82
|
executor,
|
|
82
83
|
executorKey,
|
|
83
|
-
|
|
84
|
+
releaseBytecodeId,
|
|
85
|
+
releaseVersion,
|
|
84
86
|
method,
|
|
85
87
|
commitment,
|
|
86
88
|
u64le(accountHeads.length),
|
|
@@ -93,7 +95,7 @@ export async function signWarrant(input) {
|
|
|
93
95
|
const signature = checkedSignature(await signer.sign(preimage));
|
|
94
96
|
// Borsh: a `String` is a u32 length then its UTF-8 bytes; a `Vec<[u8; 32]>` is
|
|
95
97
|
// a u32 count then the elements. u32 HERE; u64 in the preimage above.
|
|
96
|
-
return hex(concat(context, authorAccount, publicKey, executor, executorKey,
|
|
98
|
+
return hex(concat(context, authorAccount, publicKey, executor, executorKey, releaseBytecodeId, u32le(releaseVersion.length), releaseVersion, u32le(method.length), method, commitment, u32le(accountHeads.length), ...accountHeads, u32le(governanceFloor.length), ...governanceFloor, nonce, notAfter, signature));
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
101
|
* Decode a cited-head list, refusing one longer than a node will accept.
|
|
@@ -113,6 +115,17 @@ export function checkedSignature(signature) {
|
|
|
113
115
|
}
|
|
114
116
|
return signature;
|
|
115
117
|
}
|
|
118
|
+
/** A release version as UTF-8, refused past what a node accepts. Shared with `RelayClient`; internal. */
|
|
119
|
+
export function releaseVersionBytes(version) {
|
|
120
|
+
const bytes = new TextEncoder().encode(version);
|
|
121
|
+
checkReleaseVersionLength(bytes.length);
|
|
122
|
+
return bytes;
|
|
123
|
+
}
|
|
124
|
+
function checkReleaseVersionLength(len) {
|
|
125
|
+
if (len > MAX_RELEASE_VERSION_LEN) {
|
|
126
|
+
throw new Error(`releaseVersion is ${len} bytes, over the ${MAX_RELEASE_VERSION_LEN} a node accepts`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
export function citedHeads(heads, label) {
|
|
117
130
|
const list = heads ?? [];
|
|
118
131
|
if (list.length > MAX_CITED_HEADS) {
|
|
@@ -124,11 +137,11 @@ export function citedHeads(heads, label) {
|
|
|
124
137
|
* Decode a hex warrant back into its fields.
|
|
125
138
|
*
|
|
126
139
|
* Exists because v2's offsets are no longer constants: `method` is a string and
|
|
127
|
-
* the head lists are vectors, so everything after `
|
|
128
|
-
* method's
|
|
129
|
-
* inspecting one it was handed
|
|
130
|
-
* failing, which is how the three call sites that did
|
|
131
|
-
* the method's length prefix to a nonce.
|
|
140
|
+
* the head lists are vectors, so everything after `releaseBytecodeId` moves with
|
|
141
|
+
* the release version's and the method's lengths. Anything reading a warrant by
|
|
142
|
+
* fixed offset - a test, a client inspecting one it was handed - silently reads
|
|
143
|
+
* the wrong field instead of failing, which is how the three call sites that did
|
|
144
|
+
* so passed while comparing the method's length prefix to a nonce.
|
|
132
145
|
*
|
|
133
146
|
* One copy of the layout, beside the encoder that produces it, so the two cannot
|
|
134
147
|
* drift. Internal: not exported from the package root, because a client that
|
|
@@ -149,7 +162,11 @@ export function parseWarrant(warrant) {
|
|
|
149
162
|
const deviceKey = take(32);
|
|
150
163
|
const executor = take(32);
|
|
151
164
|
const executorKey = take(32);
|
|
152
|
-
const
|
|
165
|
+
const releaseBytecodeId = take(32);
|
|
166
|
+
const releaseVersionLen = u32At(o);
|
|
167
|
+
o += 4;
|
|
168
|
+
checkReleaseVersionLength(releaseVersionLen);
|
|
169
|
+
const releaseVersion = take(releaseVersionLen);
|
|
153
170
|
const methodLen = u32At(o);
|
|
154
171
|
o += 4;
|
|
155
172
|
const method = take(methodLen);
|
|
@@ -172,7 +189,8 @@ export function parseWarrant(warrant) {
|
|
|
172
189
|
deviceKey,
|
|
173
190
|
executor,
|
|
174
191
|
executorKey,
|
|
175
|
-
|
|
192
|
+
releaseBytecodeId,
|
|
193
|
+
releaseVersion,
|
|
176
194
|
method,
|
|
177
195
|
intentHash: intentHashField,
|
|
178
196
|
accountHeads,
|