@calimero-network/mero-js 24.1.3 → 24.3.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/account/create-context.d.ts +2 -4
- package/dist/account/create-context.js +34 -14
- package/dist/account/session.d.ts +3 -2
- package/dist/admin-api/admin-client.d.ts +12 -10
- package/dist/admin-api/admin-client.js +12 -10
- package/dist/admin-api/admin-types.d.ts +19 -12
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +112 -53
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +112 -53
- package/dist/index.mjs.map +3 -3
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/relay-client.d.ts +34 -30
- package/dist/relay/relay-client.js +40 -32
- package/dist/warrant/creation-warrant.d.ts +5 -1
- package/dist/warrant/creation-warrant.js +5 -1
- package/dist/warrant/governance-warrant.d.ts +5 -1
- package/dist/warrant/governance-warrant.js +5 -1
- package/dist/warrant/warrant.d.ts +28 -20
- package/dist/warrant/warrant.js +34 -12
- package/package.json +1 -1
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, GovernanceDescription, GovernInput, GovernResult, FoundNamespaceInput, FoundedNamespace, } from './relay-client.js';
|
|
2
|
+
export type { RelayClientConfig, RelayExecutor, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, FoundNamespaceInput, FoundedNamespace, } from './relay-client.js';
|
|
3
3
|
export { createMemoryNonceSource, createLocalStorageNonceSource, createRecoveringNonceSource, authorNonceLookup, WarrantNonceExhaustedError, } from './nonce-source.js';
|
|
4
4
|
export type { NonceSource, WarrantNonceLookup, RecoveringNonceSourceOptions, } from './nonce-source.js';
|
|
@@ -18,10 +18,11 @@
|
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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,
|
|
24
|
+
* that the author's account is a member, only the group knows, and it surfaces
|
|
25
|
+
* as a refusal.
|
|
25
26
|
*
|
|
26
27
|
* Checking before signing is not politeness. A warrant consumes a nonce from a
|
|
27
28
|
* monotonic per-device sequence, and one minted against the wrong executor is
|
|
@@ -42,9 +43,8 @@ export interface RelayClientConfig {
|
|
|
42
43
|
/**
|
|
43
44
|
* The account the relay writes as, hex — a warrant's `executor`.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
-
* signing key
|
|
47
|
-
* first `execute` learns it from {@link RelayClient.describe}.
|
|
46
|
+
* Only checked against what the relay answers: a warrant also names the
|
|
47
|
+
* relay's signing key and the release, which only discovery reports, so every write asks.
|
|
48
48
|
*/
|
|
49
49
|
executorAccount?: string;
|
|
50
50
|
/** The author's account, hex — whose consent the warrant carries. */
|
|
@@ -84,9 +84,9 @@ export interface RelayClientConfig {
|
|
|
84
84
|
/**
|
|
85
85
|
* Seconds a minted warrant stays valid. Defaults to 300.
|
|
86
86
|
*
|
|
87
|
-
* Checked by the relay against its own clock and
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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.
|
|
90
90
|
*/
|
|
91
91
|
ttlSeconds?: number;
|
|
92
92
|
/** Injected for tests and non-browser runtimes. Defaults to global `fetch`. */
|
|
@@ -101,19 +101,29 @@ export interface RelayClientConfig {
|
|
|
101
101
|
*/
|
|
102
102
|
rateLimitRetries?: number;
|
|
103
103
|
}
|
|
104
|
-
/**
|
|
105
|
-
export interface
|
|
104
|
+
/** The relay a warrant names: its account, and the one key of it that may spend the warrant. */
|
|
105
|
+
export interface RelayExecutor {
|
|
106
106
|
/** The account a warrant must name as `executor`, hex. */
|
|
107
107
|
executorAccount: string;
|
|
108
|
+
/** The signing key a warrant must name as `executor_key`, hex. */
|
|
109
|
+
executorKey: string;
|
|
110
|
+
}
|
|
111
|
+
/** What a relay says about its ability to run intents in one context. */
|
|
112
|
+
export interface RelayDescription extends RelayExecutor {
|
|
108
113
|
/** Whether the relay holds `CAN_AUTHOR_ON_BEHALF` on the owning group. */
|
|
109
114
|
canAuthorOnBehalf: boolean;
|
|
110
115
|
/** The group whose admin grants that capability, hex. */
|
|
111
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;
|
|
112
124
|
}
|
|
113
125
|
/** What a relay says about its ability to create contexts in one group. */
|
|
114
|
-
export interface CreationDescription {
|
|
115
|
-
/** The account a creation warrant must name as `executor`, hex. */
|
|
116
|
-
executorAccount: string;
|
|
126
|
+
export interface CreationDescription extends RelayExecutor {
|
|
117
127
|
/** The group, hex. */
|
|
118
128
|
groupId: string;
|
|
119
129
|
/**
|
|
@@ -156,9 +166,7 @@ export interface CreatedContext {
|
|
|
156
166
|
memberPublicKey: string;
|
|
157
167
|
}
|
|
158
168
|
/** What a relay says about publishing governance ops for members of one group. */
|
|
159
|
-
export interface GovernanceDescription {
|
|
160
|
-
/** The account a governance warrant must name as `executor`, hex. */
|
|
161
|
-
executorAccount: string;
|
|
169
|
+
export interface GovernanceDescription extends RelayExecutor {
|
|
162
170
|
/** The group asked about, hex. */
|
|
163
171
|
groupId: string;
|
|
164
172
|
/**
|
|
@@ -192,11 +200,10 @@ export interface FoundNamespaceInput {
|
|
|
192
200
|
*/
|
|
193
201
|
salt?: string;
|
|
194
202
|
/**
|
|
195
|
-
* The relay's account
|
|
196
|
-
*
|
|
197
|
-
* to the client's configured or already-learned `executorAccount`.
|
|
203
|
+
* The relay's account and node key (pinned, or from `resolveRelayNodeKey`). Never
|
|
204
|
+
* a discovery answer: that key is unauthenticated, and no namespace exists yet.
|
|
198
205
|
*/
|
|
199
|
-
|
|
206
|
+
executor: RelayExecutor;
|
|
200
207
|
/**
|
|
201
208
|
* The namespace root's default capability mask (`MemberCapabilities` bits),
|
|
202
209
|
* set right after founding. A namespace is founded with core's minimal
|
|
@@ -292,7 +299,6 @@ export declare class IntentRefusedError extends Error {
|
|
|
292
299
|
}
|
|
293
300
|
export declare class RelayClient {
|
|
294
301
|
private readonly baseUrl;
|
|
295
|
-
private executorAccount;
|
|
296
302
|
private readonly config;
|
|
297
303
|
private readonly fetchImpl;
|
|
298
304
|
private readonly timeoutMs;
|
|
@@ -389,7 +395,7 @@ export declare class RelayClient {
|
|
|
389
395
|
createContext(input: CreateContextInput): Promise<CreatedContext>;
|
|
390
396
|
/**
|
|
391
397
|
* Ask the relay whether it may publish governance ops for members of
|
|
392
|
-
* `groupId`, and which account a warrant must name.
|
|
398
|
+
* `groupId`, and which account and key a warrant must name.
|
|
393
399
|
*/
|
|
394
400
|
describeGovernance(groupId: string): Promise<GovernanceDescription>;
|
|
395
401
|
/**
|
|
@@ -427,9 +433,7 @@ export declare class RelayClient {
|
|
|
427
433
|
*
|
|
428
434
|
* The executor cannot be learned from the relay first: the namespace does not
|
|
429
435
|
* exist, so `describeGovernance` has nothing to answer about. It comes from
|
|
430
|
-
* `input.
|
|
431
|
-
* (`describe`, `createContext`, `govern`), and without any of them this
|
|
432
|
-
* throws before taking a nonce.
|
|
436
|
+
* `input.executor` only, and without it this throws before taking a nonce.
|
|
433
437
|
* The nonce is spent in the new namespace's ledger.
|
|
434
438
|
*
|
|
435
439
|
* With `defaultCapabilities`, the founder then sets the namespace's default
|
|
@@ -440,11 +444,11 @@ export declare class RelayClient {
|
|
|
440
444
|
* Refusals (`400`/`403`) come back as {@link IntentRefusedError}; a salt
|
|
441
445
|
* that names an existing namespace is a `409`, an `HTTPError`.
|
|
442
446
|
*/
|
|
443
|
-
foundNamespace(input
|
|
447
|
+
foundNamespace(input: FoundNamespaceInput): Promise<FoundedNamespace>;
|
|
444
448
|
private postGovernance;
|
|
445
449
|
/**
|
|
446
|
-
* The
|
|
447
|
-
*
|
|
450
|
+
* The relay's answer, checked against a configured account.
|
|
451
|
+
* Decoded here so a relay naming no key is refused before a nonce is taken.
|
|
448
452
|
*/
|
|
449
453
|
private checkedExecutor;
|
|
450
454
|
/**
|
|
@@ -18,16 +18,17 @@
|
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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,
|
|
24
|
+
* that the author's account is a member, only the group knows, and it surfaces
|
|
25
|
+
* as a refusal.
|
|
25
26
|
*
|
|
26
27
|
* Checking before signing is not politeness. A warrant consumes a nonce from a
|
|
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';
|
|
@@ -85,7 +86,6 @@ export class RelayClient {
|
|
|
85
86
|
constructor(config) {
|
|
86
87
|
this.config = config;
|
|
87
88
|
this.baseUrl = config.relayUrl.replace(/\/+$/, '');
|
|
88
|
-
this.executorAccount = config.executorAccount;
|
|
89
89
|
const injected = config.fetch;
|
|
90
90
|
this.fetchImpl = injected
|
|
91
91
|
? (input, init) => injected(input, init)
|
|
@@ -138,9 +138,6 @@ export class RelayClient {
|
|
|
138
138
|
*/
|
|
139
139
|
async describe(contextId) {
|
|
140
140
|
const body = await this.json('GET', `/admin-api/contexts/${encodeURIComponent(contextId)}/intents`);
|
|
141
|
-
// Learned, not merely returned: a client that called `describe` should not
|
|
142
|
-
// then have to pass the account back in to `execute`.
|
|
143
|
-
this.executorAccount = body.data.executorAccount;
|
|
144
141
|
return body.data;
|
|
145
142
|
}
|
|
146
143
|
/**
|
|
@@ -151,7 +148,13 @@ export class RelayClient {
|
|
|
151
148
|
* apply. Nothing accumulates and nothing is reusable.
|
|
152
149
|
*/
|
|
153
150
|
async execute(contextId, method, argsJson = {}) {
|
|
154
|
-
|
|
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);
|
|
155
158
|
// Resolved before the nonce, so a misconfigured key burns none.
|
|
156
159
|
const signer = await this.authorSigner();
|
|
157
160
|
const nonce = await this.config.nonces.next();
|
|
@@ -159,7 +162,10 @@ export class RelayClient {
|
|
|
159
162
|
const warrant = await signWarrant({
|
|
160
163
|
context: contextId,
|
|
161
164
|
authorAccount: this.config.authorAccount,
|
|
162
|
-
executor,
|
|
165
|
+
executor: executor.executorAccount,
|
|
166
|
+
executorKey: executor.executorKey,
|
|
167
|
+
releaseBytecodeId,
|
|
168
|
+
releaseVersion,
|
|
163
169
|
method,
|
|
164
170
|
argsJson,
|
|
165
171
|
nonce,
|
|
@@ -224,7 +230,7 @@ export class RelayClient {
|
|
|
224
230
|
`${described.groupId}; it needs CAN_CREATE_CONTEXT or admin ` +
|
|
225
231
|
'(checked before signing — no nonce was spent)', false, 403);
|
|
226
232
|
}
|
|
227
|
-
const executor = this.checkedExecutor(described
|
|
233
|
+
const executor = this.checkedExecutor(described);
|
|
228
234
|
const initArgs = input.initArgs ?? {};
|
|
229
235
|
// Resolved before the nonce, so a misconfigured key burns none.
|
|
230
236
|
const signer = await this.authorSigner();
|
|
@@ -234,7 +240,8 @@ export class RelayClient {
|
|
|
234
240
|
group: input.groupId,
|
|
235
241
|
seed: input.seed,
|
|
236
242
|
authorAccount: this.config.authorAccount,
|
|
237
|
-
executor,
|
|
243
|
+
executor: executor.executorAccount,
|
|
244
|
+
executorKey: executor.executorKey,
|
|
238
245
|
applicationId: input.applicationId,
|
|
239
246
|
serviceName: input.serviceName,
|
|
240
247
|
name: input.name,
|
|
@@ -252,7 +259,7 @@ export class RelayClient {
|
|
|
252
259
|
}
|
|
253
260
|
/**
|
|
254
261
|
* Ask the relay whether it may publish governance ops for members of
|
|
255
|
-
* `groupId`, and which account a warrant must name.
|
|
262
|
+
* `groupId`, and which account and key a warrant must name.
|
|
256
263
|
*/
|
|
257
264
|
async describeGovernance(groupId) {
|
|
258
265
|
const body = await this.json('GET', `/admin-api/groups/${encodeURIComponent(groupId)}/governance-intents`);
|
|
@@ -281,7 +288,7 @@ export class RelayClient {
|
|
|
281
288
|
`${described.groupId}; an admin must admit it as a relay or grant it CAN_AUTHOR_ON_BEHALF ` +
|
|
282
289
|
'(checked before signing, no nonce was spent)', false, 403);
|
|
283
290
|
}
|
|
284
|
-
const executor = this.checkedExecutor(described
|
|
291
|
+
const executor = this.checkedExecutor(described);
|
|
285
292
|
// Resolved before the nonce, so a misconfigured key burns none.
|
|
286
293
|
const signer = await this.authorSigner();
|
|
287
294
|
const nonce = await this.config.nonces.next();
|
|
@@ -290,7 +297,8 @@ export class RelayClient {
|
|
|
290
297
|
scope: input.groupId,
|
|
291
298
|
op: input.op,
|
|
292
299
|
authorAccount: this.config.authorAccount,
|
|
293
|
-
executor,
|
|
300
|
+
executor: executor.executorAccount,
|
|
301
|
+
executorKey: executor.executorKey,
|
|
294
302
|
nonce,
|
|
295
303
|
notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
|
|
296
304
|
signer,
|
|
@@ -316,9 +324,7 @@ export class RelayClient {
|
|
|
316
324
|
*
|
|
317
325
|
* The executor cannot be learned from the relay first: the namespace does not
|
|
318
326
|
* exist, so `describeGovernance` has nothing to answer about. It comes from
|
|
319
|
-
* `input.
|
|
320
|
-
* (`describe`, `createContext`, `govern`), and without any of them this
|
|
321
|
-
* throws before taking a nonce.
|
|
327
|
+
* `input.executor` only, and without it this throws before taking a nonce.
|
|
322
328
|
* The nonce is spent in the new namespace's ledger.
|
|
323
329
|
*
|
|
324
330
|
* With `defaultCapabilities`, the founder then sets the namespace's default
|
|
@@ -329,14 +335,12 @@ export class RelayClient {
|
|
|
329
335
|
* Refusals (`400`/`403`) come back as {@link IntentRefusedError}; a salt
|
|
330
336
|
* that names an existing namespace is a `409`, an `HTTPError`.
|
|
331
337
|
*/
|
|
332
|
-
async foundNamespace(input
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
: this.executorAccount;
|
|
336
|
-
if (!executor) {
|
|
337
|
-
throw new Error("foundNamespace needs the relay's account: pass executorAccount or configure it " +
|
|
338
|
+
async foundNamespace(input) {
|
|
339
|
+
if (!input?.executor) {
|
|
340
|
+
throw new Error("foundNamespace needs the relay's account and signing key: pass executor " +
|
|
338
341
|
'(checked before signing, no nonce was spent)');
|
|
339
342
|
}
|
|
343
|
+
const executor = this.checkedExecutor(input.executor);
|
|
340
344
|
const salt = input.salt
|
|
341
345
|
? hex(fromHex(input.salt, 'salt', 32))
|
|
342
346
|
: hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
@@ -360,7 +364,8 @@ export class RelayClient {
|
|
|
360
364
|
scope: namespaceId,
|
|
361
365
|
op,
|
|
362
366
|
authorAccount: this.config.authorAccount,
|
|
363
|
-
executor,
|
|
367
|
+
executor: executor.executorAccount,
|
|
368
|
+
executorKey: executor.executorKey,
|
|
364
369
|
nonce,
|
|
365
370
|
notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
|
|
366
371
|
signer,
|
|
@@ -408,17 +413,20 @@ export class RelayClient {
|
|
|
408
413
|
return body.data;
|
|
409
414
|
}
|
|
410
415
|
/**
|
|
411
|
-
* The
|
|
412
|
-
*
|
|
416
|
+
* The relay's answer, checked against a configured account.
|
|
417
|
+
* Decoded here so a relay naming no key is refused before a nonce is taken.
|
|
413
418
|
*/
|
|
414
419
|
checkedExecutor(answered) {
|
|
420
|
+
const executor = {
|
|
421
|
+
executorAccount: hex(fromHex(answered.executorAccount ?? '', "the relay's executorAccount", 32)),
|
|
422
|
+
executorKey: hex(fromHex(answered.executorKey ?? '', "the relay's executorKey", 32)),
|
|
423
|
+
};
|
|
415
424
|
const configured = this.config.executorAccount;
|
|
416
|
-
if (configured && configured.toLowerCase() !==
|
|
425
|
+
if (configured && configured.toLowerCase() !== executor.executorAccount) {
|
|
417
426
|
throw new Error(`configured executorAccount ${configured} is not the relay's account ` +
|
|
418
|
-
`${
|
|
427
|
+
`${executor.executorAccount}; a warrant naming it would be unspendable`);
|
|
419
428
|
}
|
|
420
|
-
|
|
421
|
-
return answered;
|
|
429
|
+
return executor;
|
|
422
430
|
}
|
|
423
431
|
/**
|
|
424
432
|
* One request, waiting out a rate limit rather than failing the caller.
|
|
@@ -36,8 +36,10 @@ export interface CreationWarrantInput {
|
|
|
36
36
|
seed?: string;
|
|
37
37
|
/** The author's account, hex — whose consent this is, and who runs `init`. */
|
|
38
38
|
authorAccount: string;
|
|
39
|
-
/** The relay
|
|
39
|
+
/** The relay's account, hex: the subject its authorship grant is held by. */
|
|
40
40
|
executor: string;
|
|
41
|
+
/** The one signing key of `executor` that may carry it out, hex: discovery's `executorKey`. */
|
|
42
|
+
executorKey: string;
|
|
41
43
|
/** The application the context is created with, hex (32 bytes). */
|
|
42
44
|
applicationId: string;
|
|
43
45
|
/**
|
|
@@ -103,6 +105,7 @@ export interface CreationWarrantFields {
|
|
|
103
105
|
/** The author device key: the signer's public key. */
|
|
104
106
|
deviceKey: string;
|
|
105
107
|
executor: string;
|
|
108
|
+
executorKey: string;
|
|
106
109
|
applicationId: string;
|
|
107
110
|
/** `null` when absent — distinct from `""`. */
|
|
108
111
|
serviceName: string | null;
|
|
@@ -129,6 +132,7 @@ export interface CreationPreimageParts {
|
|
|
129
132
|
authorAccount: Uint8Array;
|
|
130
133
|
deviceKey: Uint8Array;
|
|
131
134
|
executor: Uint8Array;
|
|
135
|
+
executorKey: Uint8Array;
|
|
132
136
|
applicationId: Uint8Array;
|
|
133
137
|
serviceName: Uint8Array | null;
|
|
134
138
|
name: Uint8Array | null;
|
|
@@ -51,6 +51,7 @@ export async function creationWarrantPreimage(p) {
|
|
|
51
51
|
p.authorAccount,
|
|
52
52
|
p.deviceKey,
|
|
53
53
|
p.executor,
|
|
54
|
+
p.executorKey,
|
|
54
55
|
p.applicationId,
|
|
55
56
|
new Uint8Array([p.serviceName === null ? 0 : 1]),
|
|
56
57
|
p.serviceName ?? new Uint8Array(0),
|
|
@@ -78,6 +79,7 @@ export async function signCreationWarrant(input) {
|
|
|
78
79
|
: fromHex(input.seed, 'seed', 32);
|
|
79
80
|
const authorAccount = fromHex(input.authorAccount, 'authorAccount', 32);
|
|
80
81
|
const executor = fromHex(input.executor, 'executor', 32);
|
|
82
|
+
const executorKey = fromHex(input.executorKey, 'executorKey', 32);
|
|
81
83
|
const applicationId = fromHex(input.applicationId, 'applicationId', 32);
|
|
82
84
|
const serviceName = label(input.serviceName, 'serviceName');
|
|
83
85
|
const name = label(input.name, 'name');
|
|
@@ -92,6 +94,7 @@ export async function signCreationWarrant(input) {
|
|
|
92
94
|
authorAccount,
|
|
93
95
|
deviceKey,
|
|
94
96
|
executor,
|
|
97
|
+
executorKey,
|
|
95
98
|
applicationId,
|
|
96
99
|
serviceName,
|
|
97
100
|
name,
|
|
@@ -102,7 +105,7 @@ export async function signCreationWarrant(input) {
|
|
|
102
105
|
notAfter: input.notAfter,
|
|
103
106
|
});
|
|
104
107
|
const signature = checkedSignature(await signer.sign(preimage));
|
|
105
|
-
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));
|
|
108
|
+
const warrant = hex(concat(group, seed, authorAccount, deviceKey, executor, executorKey, applicationId, borshOption(serviceName), borshOption(name), initHash, u32le(accountHeads.length), ...accountHeads, u32le(governanceFloor.length), ...governanceFloor, u64le(input.nonce), u64le(input.notAfter), signature));
|
|
106
109
|
return { warrant, seed: hex(seed) };
|
|
107
110
|
}
|
|
108
111
|
/**
|
|
@@ -157,6 +160,7 @@ export function parseCreationWarrant(warrant) {
|
|
|
157
160
|
authorAccount: hex(take(32, 'authorAccount')),
|
|
158
161
|
deviceKey: hex(take(32, 'deviceKey')),
|
|
159
162
|
executor: hex(take(32, 'executor')),
|
|
163
|
+
executorKey: hex(take(32, 'executorKey')),
|
|
160
164
|
applicationId: hex(take(32, 'applicationId')),
|
|
161
165
|
serviceName: option('serviceName'),
|
|
162
166
|
name: option('name'),
|
|
@@ -39,8 +39,10 @@ export interface GovernanceWarrantInput {
|
|
|
39
39
|
op: GovernanceOp;
|
|
40
40
|
/** The author's account, hex: whose authority the op is applied under. */
|
|
41
41
|
authorAccount: string;
|
|
42
|
-
/** The relay
|
|
42
|
+
/** The relay's account, hex: the subject its authorship grant is held by. */
|
|
43
43
|
executor: string;
|
|
44
|
+
/** The one signing key of `executor` that may publish it, hex: discovery's `executorKey`. */
|
|
45
|
+
executorKey: string;
|
|
44
46
|
/**
|
|
45
47
|
* Monotonic per author **device**, spent in a per-group ledger. The same
|
|
46
48
|
* nonce source `RelayClient` uses for every other warrant is correct.
|
|
@@ -76,6 +78,7 @@ export interface GovernanceWarrantFields {
|
|
|
76
78
|
/** The author device key: the signer's public key. */
|
|
77
79
|
deviceKey: string;
|
|
78
80
|
executor: string;
|
|
81
|
+
executorKey: string;
|
|
79
82
|
opHash: string;
|
|
80
83
|
accountHeads: string[];
|
|
81
84
|
governanceFloor: string[];
|
|
@@ -95,6 +98,7 @@ export interface GovernancePreimageParts {
|
|
|
95
98
|
authorAccount: Uint8Array;
|
|
96
99
|
deviceKey: Uint8Array;
|
|
97
100
|
executor: Uint8Array;
|
|
101
|
+
executorKey: Uint8Array;
|
|
98
102
|
opHash: Uint8Array;
|
|
99
103
|
accountHeads: Uint8Array[];
|
|
100
104
|
governanceFloor: Uint8Array[];
|
|
@@ -53,6 +53,7 @@ export async function governanceWarrantPreimage(p) {
|
|
|
53
53
|
p.authorAccount,
|
|
54
54
|
p.deviceKey,
|
|
55
55
|
p.executor,
|
|
56
|
+
p.executorKey,
|
|
56
57
|
p.opHash,
|
|
57
58
|
u64le(p.accountHeads.length),
|
|
58
59
|
...p.accountHeads,
|
|
@@ -70,6 +71,7 @@ export async function signGovernanceWarrant(input) {
|
|
|
70
71
|
const scope = fromHex(input.scope, 'scope', 32);
|
|
71
72
|
const authorAccount = fromHex(input.authorAccount, 'authorAccount', 32);
|
|
72
73
|
const executor = fromHex(input.executor, 'executor', 32);
|
|
74
|
+
const executorKey = fromHex(input.executorKey, 'executorKey', 32);
|
|
73
75
|
const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
|
|
74
76
|
const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
|
|
75
77
|
const kind = kindByte(input.op.kind);
|
|
@@ -82,6 +84,7 @@ export async function signGovernanceWarrant(input) {
|
|
|
82
84
|
authorAccount,
|
|
83
85
|
deviceKey,
|
|
84
86
|
executor,
|
|
87
|
+
executorKey,
|
|
85
88
|
opHash,
|
|
86
89
|
accountHeads,
|
|
87
90
|
governanceFloor,
|
|
@@ -89,7 +92,7 @@ export async function signGovernanceWarrant(input) {
|
|
|
89
92
|
notAfter: input.notAfter,
|
|
90
93
|
});
|
|
91
94
|
const signature = checkedSignature(await signer.sign(preimage));
|
|
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));
|
|
95
|
+
return hex(concat(scope, new Uint8Array([kind]), authorAccount, deviceKey, executor, executorKey, opHash, u32le(accountHeads.length), ...accountHeads, u32le(governanceFloor.length), ...governanceFloor, u64le(input.nonce), u64le(input.notAfter), signature));
|
|
93
96
|
}
|
|
94
97
|
/**
|
|
95
98
|
* Decode a hex governance warrant back into its fields.
|
|
@@ -139,6 +142,7 @@ export function parseGovernanceWarrant(warrant) {
|
|
|
139
142
|
authorAccount: hex(take(32, 'authorAccount')),
|
|
140
143
|
deviceKey: hex(take(32, 'deviceKey')),
|
|
141
144
|
executor: hex(take(32, 'executor')),
|
|
145
|
+
executorKey: hex(take(32, 'executorKey')),
|
|
142
146
|
opHash: hex(take(32, 'opHash')),
|
|
143
147
|
accountHeads: heads('accountHeads'),
|
|
144
148
|
governanceFloor: heads('governanceFloor'),
|
|
@@ -42,24 +42,27 @@ export interface WarrantInput {
|
|
|
42
42
|
context: string;
|
|
43
43
|
/** The author's account, hex — whose consent this is. */
|
|
44
44
|
authorAccount: string;
|
|
45
|
-
/** The relay
|
|
45
|
+
/** The relay's account, hex: the subject its authorship grant is held by. */
|
|
46
46
|
executor: string;
|
|
47
47
|
/**
|
|
48
|
-
* The
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*
|
|
54
|
-
*
|
|
48
|
+
* The one signing key of `executor` that may spend this warrant, hex: the
|
|
49
|
+
* relay's `executorKey` from discovery. Any other key of the account is refused.
|
|
50
|
+
*/
|
|
51
|
+
executorKey: string;
|
|
52
|
+
/**
|
|
53
|
+
* The blob id of the release the context's group names, hex (32 bytes): the
|
|
54
|
+
* relay's `releaseBytecodeId` from discovery.
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
61
64
|
*/
|
|
62
|
-
|
|
65
|
+
releaseVersion?: string;
|
|
63
66
|
/**
|
|
64
67
|
* The method the relay may run.
|
|
65
68
|
*
|
|
@@ -144,6 +147,8 @@ export declare function signWarrant(input: WarrantInput): Promise<string>;
|
|
|
144
147
|
* relay, far from the signer at fault. Internal.
|
|
145
148
|
*/
|
|
146
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;
|
|
147
152
|
export declare function citedHeads(heads: string[] | undefined, label: string): Uint8Array[];
|
|
148
153
|
/** A warrant's fields, as hex, decoded from the wire encoding. */
|
|
149
154
|
export interface WarrantFields {
|
|
@@ -152,7 +157,10 @@ export interface WarrantFields {
|
|
|
152
157
|
/** The author device key `signWarrant` derived from the secret. */
|
|
153
158
|
deviceKey: string;
|
|
154
159
|
executor: string;
|
|
155
|
-
|
|
160
|
+
executorKey: string;
|
|
161
|
+
releaseBytecodeId: string;
|
|
162
|
+
/** UTF-8, as hex like `method`. */
|
|
163
|
+
releaseVersion: string;
|
|
156
164
|
method: string;
|
|
157
165
|
intentHash: string;
|
|
158
166
|
accountHeads: string[];
|
|
@@ -167,11 +175,11 @@ export interface WarrantFields {
|
|
|
167
175
|
* Decode a hex warrant back into its fields.
|
|
168
176
|
*
|
|
169
177
|
* Exists because v2's offsets are no longer constants: `method` is a string and
|
|
170
|
-
* the head lists are vectors, so everything after `
|
|
171
|
-
* method's
|
|
172
|
-
* inspecting one it was handed
|
|
173
|
-
* failing, which is how the three call sites that did
|
|
174
|
-
* 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.
|
|
175
183
|
*
|
|
176
184
|
* One copy of the layout, beside the encoder that produces it, so the two cannot
|
|
177
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
|
*
|
|
@@ -59,7 +59,9 @@ export async function signWarrant(input) {
|
|
|
59
59
|
const context = fromHex(input.context, 'context', 32);
|
|
60
60
|
const authorAccount = fromHex(input.authorAccount, 'authorAccount', 32);
|
|
61
61
|
const executor = fromHex(input.executor, 'executor', 32);
|
|
62
|
-
const
|
|
62
|
+
const executorKey = fromHex(input.executorKey, 'executorKey', 32);
|
|
63
|
+
const releaseBytecodeId = fromHex(input.releaseBytecodeId, 'releaseBytecodeId', 32);
|
|
64
|
+
const releaseVersion = releaseVersionBytes(input.releaseVersion ?? '');
|
|
63
65
|
const accountHeads = citedHeads(input.accountHeads, 'accountHeads');
|
|
64
66
|
const governanceFloor = citedHeads(input.governanceFloor, 'governanceFloor');
|
|
65
67
|
const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
|
|
@@ -78,7 +80,9 @@ export async function signWarrant(input) {
|
|
|
78
80
|
authorAccount,
|
|
79
81
|
publicKey,
|
|
80
82
|
executor,
|
|
81
|
-
|
|
83
|
+
executorKey,
|
|
84
|
+
releaseBytecodeId,
|
|
85
|
+
releaseVersion,
|
|
82
86
|
method,
|
|
83
87
|
commitment,
|
|
84
88
|
u64le(accountHeads.length),
|
|
@@ -91,7 +95,7 @@ export async function signWarrant(input) {
|
|
|
91
95
|
const signature = checkedSignature(await signer.sign(preimage));
|
|
92
96
|
// Borsh: a `String` is a u32 length then its UTF-8 bytes; a `Vec<[u8; 32]>` is
|
|
93
97
|
// a u32 count then the elements. u32 HERE; u64 in the preimage above.
|
|
94
|
-
return hex(concat(context, authorAccount, publicKey, executor,
|
|
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));
|
|
95
99
|
}
|
|
96
100
|
/**
|
|
97
101
|
* Decode a cited-head list, refusing one longer than a node will accept.
|
|
@@ -111,6 +115,17 @@ export function checkedSignature(signature) {
|
|
|
111
115
|
}
|
|
112
116
|
return signature;
|
|
113
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
|
+
}
|
|
114
129
|
export function citedHeads(heads, label) {
|
|
115
130
|
const list = heads ?? [];
|
|
116
131
|
if (list.length > MAX_CITED_HEADS) {
|
|
@@ -122,11 +137,11 @@ export function citedHeads(heads, label) {
|
|
|
122
137
|
* Decode a hex warrant back into its fields.
|
|
123
138
|
*
|
|
124
139
|
* Exists because v2's offsets are no longer constants: `method` is a string and
|
|
125
|
-
* the head lists are vectors, so everything after `
|
|
126
|
-
* method's
|
|
127
|
-
* inspecting one it was handed
|
|
128
|
-
* failing, which is how the three call sites that did
|
|
129
|
-
* 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.
|
|
130
145
|
*
|
|
131
146
|
* One copy of the layout, beside the encoder that produces it, so the two cannot
|
|
132
147
|
* drift. Internal: not exported from the package root, because a client that
|
|
@@ -146,7 +161,12 @@ export function parseWarrant(warrant) {
|
|
|
146
161
|
const authorAccount = take(32);
|
|
147
162
|
const deviceKey = take(32);
|
|
148
163
|
const executor = take(32);
|
|
149
|
-
const
|
|
164
|
+
const executorKey = take(32);
|
|
165
|
+
const releaseBytecodeId = take(32);
|
|
166
|
+
const releaseVersionLen = u32At(o);
|
|
167
|
+
o += 4;
|
|
168
|
+
checkReleaseVersionLength(releaseVersionLen);
|
|
169
|
+
const releaseVersion = take(releaseVersionLen);
|
|
150
170
|
const methodLen = u32At(o);
|
|
151
171
|
o += 4;
|
|
152
172
|
const method = take(methodLen);
|
|
@@ -168,7 +188,9 @@ export function parseWarrant(warrant) {
|
|
|
168
188
|
authorAccount,
|
|
169
189
|
deviceKey,
|
|
170
190
|
executor,
|
|
171
|
-
|
|
191
|
+
executorKey,
|
|
192
|
+
releaseBytecodeId,
|
|
193
|
+
releaseVersion,
|
|
172
194
|
method,
|
|
173
195
|
intentHash: intentHashField,
|
|
174
196
|
accountHeads,
|