@calimero-network/mero-js 22.1.0 → 22.2.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/README.md +7 -5
- package/dist/cloud/cloud-client.d.ts +44 -14
- package/dist/cloud/cloud-client.js +82 -37
- package/dist/cloud/connect.d.ts +2 -1
- package/dist/cloud/connect.js +23 -12
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +84 -41
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +84 -41
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,11 +132,13 @@ builds the relay client. `sdk.cloud` exposes the same cloud API from an existing
|
|
|
132
132
|
`MeroJs` instance for apps that have both a node and a cloud account, and
|
|
133
133
|
`RelayClient` is available directly for a relay you were told about out of band.
|
|
134
134
|
|
|
135
|
-
One precondition is not yours to satisfy:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
`
|
|
135
|
+
One precondition is not yours to satisfy: the node must relay. A fleet node
|
|
136
|
+
admitted as a `RelayTee` (the namespace's TEE admission policy set with
|
|
137
|
+
`mode: 'relay'`) relays by role; a `ReadOnlyTee` is a replica and never relays;
|
|
138
|
+
and a node too old to report a role needs an admin of the namespace to grant it
|
|
139
|
+
`CAN_AUTHOR_ON_BEHALF`. `connectCloud` picks a relay on the cloud's `canExecute`,
|
|
140
|
+
which folds that rule in, and otherwise throws saying which of those is missing —
|
|
141
|
+
naming the account that needs the grant when that is the fix.
|
|
140
142
|
|
|
141
143
|
See the [cloud client](https://calimero-network.github.io/mero-js/reference/cloud/),
|
|
142
144
|
[relay client](https://calimero-network.github.io/mero-js/reference/relay/) and
|
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
* to involve it.
|
|
26
26
|
*/
|
|
27
27
|
import { type DiscoveryChallenge, type RoutingChallenge, type RoutingCredential } from './routing-proof.js';
|
|
28
|
+
/**
|
|
29
|
+
* The first relay a warrant can be spent at, or `null`.
|
|
30
|
+
*
|
|
31
|
+
* Shared by {@link CloudClient.findExecutingRelay} and `connectCloud`, which
|
|
32
|
+
* already holds the list and must not fetch it twice.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pickExecutingRelay(relays: readonly CloudRelay[]): CloudRelay | null;
|
|
28
35
|
export interface CloudClientConfig {
|
|
29
36
|
cloudBaseUrl?: string;
|
|
30
37
|
/**
|
|
@@ -90,11 +97,13 @@ export interface CloudNamespace {
|
|
|
90
97
|
* One attested node the cloud has assigned to a namespace, described as a
|
|
91
98
|
* relay.
|
|
92
99
|
*
|
|
93
|
-
* `
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
100
|
+
* `canExecute` is the field that decides whether a write can happen, and it is
|
|
101
|
+
* the cloud's own verdict rather than something to re-derive: a node that
|
|
102
|
+
* reports a role relays only as a `RelayTee` (the namespace's TEE admission
|
|
103
|
+
* policy set with `mode: 'relay'`), whatever its grant; a `ReadOnlyTee` is a
|
|
104
|
+
* replica and never relays. Only a node too old to report a role is decided by
|
|
105
|
+
* `authorshipReady` — the `CAN_AUTHOR_ON_BEHALF` grant, which is reported
|
|
106
|
+
* rather than assumed because it is not the cloud's to give.
|
|
98
107
|
*/
|
|
99
108
|
export interface CloudRelay {
|
|
100
109
|
peerId: string;
|
|
@@ -106,6 +115,20 @@ export interface CloudRelay {
|
|
|
106
115
|
status: string;
|
|
107
116
|
/** Whether this relay holds `CAN_AUTHOR_ON_BEHALF` on the namespace. */
|
|
108
117
|
authorshipReady: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* The core role the node reported for itself here — `'RelayTee'` or
|
|
120
|
+
* `'ReadOnlyTee'` — or `null` for a sidecar too old to report one.
|
|
121
|
+
*/
|
|
122
|
+
teeRole: string | null;
|
|
123
|
+
/**
|
|
124
|
+
* Whether an intent can be presented here now: a URL, an executor account, a
|
|
125
|
+
* fresh heartbeat, and a node that relays by role (or, for one reporting no
|
|
126
|
+
* role, holds the grant).
|
|
127
|
+
*
|
|
128
|
+
* The cloud's `can_execute`. Against an older cloud that does not send it,
|
|
129
|
+
* derived from `authorshipReady` plus the URL and account, as before.
|
|
130
|
+
*/
|
|
131
|
+
canExecute: boolean;
|
|
109
132
|
lastSeenAt?: string | null;
|
|
110
133
|
confirmedAt?: string | null;
|
|
111
134
|
}
|
|
@@ -159,9 +182,12 @@ export interface CloudNamespaceNode {
|
|
|
159
182
|
canAdmit: boolean;
|
|
160
183
|
/** Whether the node holds `CAN_AUTHOR_ON_BEHALF` on the namespace root group. */
|
|
161
184
|
authorshipReady: boolean;
|
|
185
|
+
/** `'RelayTee'`, `'ReadOnlyTee'`, or `null` for a node too old to report one. */
|
|
186
|
+
teeRole: string | null;
|
|
162
187
|
/**
|
|
163
188
|
* Whether a delegated write can be posted here now — a URL, an account, a
|
|
164
|
-
* fresh heartbeat and the
|
|
189
|
+
* fresh heartbeat, and a `RelayTee` (or, for a node reporting no role, the
|
|
190
|
+
* grant). A `ReadOnlyTee` is a replica and never relays.
|
|
165
191
|
*
|
|
166
192
|
* Namespace-level. A context in a RESTRICTED SUBGROUP is governed by that
|
|
167
193
|
* subgroup, so per-context routing remains the exact answer if a write is
|
|
@@ -186,6 +212,8 @@ export interface CloudMachineNamespace {
|
|
|
186
212
|
status: string;
|
|
187
213
|
/** Whether the machine holds `CAN_AUTHOR_ON_BEHALF` on this namespace. */
|
|
188
214
|
authorshipReady: boolean;
|
|
215
|
+
/** `'RelayTee'`, `'ReadOnlyTee'`, or `null` for a machine too old to report one. */
|
|
216
|
+
teeRole: string | null;
|
|
189
217
|
/**
|
|
190
218
|
* Whether the machine's heartbeat for this namespace is recent.
|
|
191
219
|
*
|
|
@@ -210,7 +238,8 @@ export interface CloudMachine {
|
|
|
210
238
|
executorAccount: string | null;
|
|
211
239
|
/**
|
|
212
240
|
* Whether this machine can write for the caller *somewhere* — a URL, an
|
|
213
|
-
* executor account, and
|
|
241
|
+
* executor account, and at least one namespace it relays in: as a
|
|
242
|
+
* `RelayTee`, or, reporting no role, holding the grant.
|
|
214
243
|
*
|
|
215
244
|
* Folded here so a client does not re-derive the three-way precondition and
|
|
216
245
|
* get a refusal it cannot explain.
|
|
@@ -395,19 +424,20 @@ export declare class CloudClient {
|
|
|
395
424
|
* The relays serving one namespace: where to present an intent, and as whom.
|
|
396
425
|
*
|
|
397
426
|
* Everything a client needs in order to mint a warrant it can actually
|
|
398
|
-
* spend. Filter on `
|
|
399
|
-
*
|
|
400
|
-
*
|
|
427
|
+
* spend. Filter on `canExecute` before signing — a warrant presented to a
|
|
428
|
+
* relay that does not relay (a `ReadOnlyTee` replica, or a node with no role
|
|
429
|
+
* and no grant) is refused *after* the author has burned a nonce from its
|
|
430
|
+
* monotonic sequence on it.
|
|
401
431
|
*/
|
|
402
432
|
getNamespaceRelays(namespaceId: string): Promise<CloudRelay[]>;
|
|
403
433
|
/**
|
|
404
434
|
* The first relay for `namespaceId` that can actually run an intent, or
|
|
405
435
|
* `null`.
|
|
406
436
|
*
|
|
407
|
-
* "Can actually run one" is
|
|
408
|
-
* gets a refusal it cannot explain: the cloud must know a URL, the
|
|
409
|
-
* have reported the executor account a warrant has to name, and
|
|
410
|
-
* must
|
|
437
|
+
* "Can actually run one" is several conditions, and a client that checks
|
|
438
|
+
* fewer gets a refusal it cannot explain: the cloud must know a URL, the
|
|
439
|
+
* relay must have reported the executor account a warrant has to name, and
|
|
440
|
+
* the node must relay — which the cloud folds into `canExecute`.
|
|
411
441
|
*/
|
|
412
442
|
findExecutingRelay(namespaceId: string): Promise<CloudRelay | null>;
|
|
413
443
|
/**
|
|
@@ -29,6 +29,31 @@ import { HTTPError } from '../http-client/web-client.js';
|
|
|
29
29
|
import { routingProofHeaders, } from './routing-proof.js';
|
|
30
30
|
/** Where the hosted cloud lives when a caller names no other. */
|
|
31
31
|
const DEFAULT_CLOUD_BASE_URL = 'https://cloud.calimero.network';
|
|
32
|
+
/**
|
|
33
|
+
* The cloud's own `can_execute` where it sends one, else `legacy`.
|
|
34
|
+
*
|
|
35
|
+
* The cloud decides relaying by role (`RelayTee` relays, `ReadOnlyTee` never
|
|
36
|
+
* does) and only falls back to the `CAN_AUTHOR_ON_BEHALF` grant for a node
|
|
37
|
+
* that reports none, so re-deriving it here from `authorship_ready` would call
|
|
38
|
+
* a grant-holding `ReadOnlyTee` writable. `legacy` is the grant-based answer,
|
|
39
|
+
* for a cloud older than the field — the only rule such a cloud knew.
|
|
40
|
+
*/
|
|
41
|
+
function canExecuteOf(row, legacy) {
|
|
42
|
+
return typeof row.can_execute === 'boolean' ? row.can_execute : legacy;
|
|
43
|
+
}
|
|
44
|
+
/** The reported `tee_role`, or `null` when absent (older cloud or sidecar). */
|
|
45
|
+
function teeRoleOf(row) {
|
|
46
|
+
return typeof row.tee_role === 'string' && row.tee_role !== '' ? row.tee_role : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The first relay a warrant can be spent at, or `null`.
|
|
50
|
+
*
|
|
51
|
+
* Shared by {@link CloudClient.findExecutingRelay} and `connectCloud`, which
|
|
52
|
+
* already holds the list and must not fetch it twice.
|
|
53
|
+
*/
|
|
54
|
+
export function pickExecutingRelay(relays) {
|
|
55
|
+
return (relays.find((r) => r.canExecute && r.relayUrl !== null && r.executorAccount !== null) ?? null);
|
|
56
|
+
}
|
|
32
57
|
/** Response headers MDMA uses to hand back a rolling-refreshed session. */
|
|
33
58
|
const REFRESH_HEADER = 'x-mdma-session-refresh';
|
|
34
59
|
const REFRESH_EXPIRES_HEADER = 'x-mdma-session-expires';
|
|
@@ -181,35 +206,42 @@ export class CloudClient {
|
|
|
181
206
|
* The relays serving one namespace: where to present an intent, and as whom.
|
|
182
207
|
*
|
|
183
208
|
* Everything a client needs in order to mint a warrant it can actually
|
|
184
|
-
* spend. Filter on `
|
|
185
|
-
*
|
|
186
|
-
*
|
|
209
|
+
* spend. Filter on `canExecute` before signing — a warrant presented to a
|
|
210
|
+
* relay that does not relay (a `ReadOnlyTee` replica, or a node with no role
|
|
211
|
+
* and no grant) is refused *after* the author has burned a nonce from its
|
|
212
|
+
* monotonic sequence on it.
|
|
187
213
|
*/
|
|
188
214
|
async getNamespaceRelays(namespaceId) {
|
|
189
215
|
const body = await this.request('GET', `/api/cloud/me/namespaces/${encodeURIComponent(namespaceId)}/relays`);
|
|
190
|
-
return (body.relays ?? []).map((row) =>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
return (body.relays ?? []).map((row) => {
|
|
217
|
+
const relayUrl = row.relay_url ?? null;
|
|
218
|
+
const executorAccount = row.executor_account ?? null;
|
|
219
|
+
const authorshipReady = row.authorship_ready === true;
|
|
220
|
+
return {
|
|
221
|
+
peerId: String(row.peer_id ?? ''),
|
|
222
|
+
relayUrl,
|
|
223
|
+
executorAccount,
|
|
224
|
+
status: String(row.status ?? ''),
|
|
225
|
+
authorshipReady,
|
|
226
|
+
teeRole: teeRoleOf(row),
|
|
227
|
+
canExecute: canExecuteOf(row, authorshipReady && relayUrl !== null && executorAccount !== null),
|
|
228
|
+
lastSeenAt: row.last_seen_at ?? null,
|
|
229
|
+
confirmedAt: row.confirmed_at ?? null,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
199
232
|
}
|
|
200
233
|
/**
|
|
201
234
|
* The first relay for `namespaceId` that can actually run an intent, or
|
|
202
235
|
* `null`.
|
|
203
236
|
*
|
|
204
|
-
* "Can actually run one" is
|
|
205
|
-
* gets a refusal it cannot explain: the cloud must know a URL, the
|
|
206
|
-
* have reported the executor account a warrant has to name, and
|
|
207
|
-
* must
|
|
237
|
+
* "Can actually run one" is several conditions, and a client that checks
|
|
238
|
+
* fewer gets a refusal it cannot explain: the cloud must know a URL, the
|
|
239
|
+
* relay must have reported the executor account a warrant has to name, and
|
|
240
|
+
* the node must relay — which the cloud folds into `canExecute`.
|
|
208
241
|
*/
|
|
209
242
|
async findExecutingRelay(namespaceId) {
|
|
210
243
|
const relays = await this.getNamespaceRelays(namespaceId);
|
|
211
|
-
return (relays
|
|
212
|
-
null);
|
|
244
|
+
return pickExecutingRelay(relays);
|
|
213
245
|
}
|
|
214
246
|
/**
|
|
215
247
|
* Where to reach a namespace without owning it: one read, both verbs.
|
|
@@ -237,17 +269,24 @@ export class CloudClient {
|
|
|
237
269
|
});
|
|
238
270
|
return {
|
|
239
271
|
namespaceId: String(body.namespace_id ?? namespaceId),
|
|
240
|
-
nodes: (body.admitters ?? []).map((row) =>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
nodes: (body.admitters ?? []).map((row) => {
|
|
273
|
+
const account = row.account ?? null;
|
|
274
|
+
const relayUrl = row.relay_url ?? null;
|
|
275
|
+
const fresh = row.fresh === true;
|
|
276
|
+
const authorshipReady = row.authorship_ready === true;
|
|
277
|
+
return {
|
|
278
|
+
peerId: String(row.peer_id ?? ''),
|
|
279
|
+
account,
|
|
280
|
+
relayUrl,
|
|
281
|
+
admitUrl: row.admit_url ?? null,
|
|
282
|
+
status: String(row.status ?? ''),
|
|
283
|
+
fresh,
|
|
284
|
+
canAdmit: row.can_admit === true,
|
|
285
|
+
authorshipReady,
|
|
286
|
+
teeRole: teeRoleOf(row),
|
|
287
|
+
canExecute: canExecuteOf(row, authorshipReady && relayUrl !== null && account !== null && fresh),
|
|
288
|
+
};
|
|
289
|
+
}),
|
|
251
290
|
servable: body.servable === true,
|
|
252
291
|
writable: body.writable === true,
|
|
253
292
|
};
|
|
@@ -374,23 +413,29 @@ export class CloudClient {
|
|
|
374
413
|
*/
|
|
375
414
|
async getMyMachines() {
|
|
376
415
|
const body = await this.request('GET', '/api/cloud/me/machines');
|
|
377
|
-
return (body.machines ?? []).map((row) =>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
canExecute: row.can_execute === true,
|
|
382
|
-
namespaces: (Array.isArray(row.namespaces) ? row.namespaces : []).map((n) => {
|
|
416
|
+
return (body.machines ?? []).map((row) => {
|
|
417
|
+
const relayUrl = row.relay_url ?? null;
|
|
418
|
+
const executorAccount = row.executor_account ?? null;
|
|
419
|
+
const namespaces = (Array.isArray(row.namespaces) ? row.namespaces : []).map((n) => {
|
|
383
420
|
const ns = n;
|
|
384
421
|
return {
|
|
385
422
|
namespaceId: String(ns.namespace_id ?? ''),
|
|
386
423
|
status: String(ns.status ?? ''),
|
|
387
424
|
authorshipReady: ns.authorship_ready === true,
|
|
425
|
+
teeRole: teeRoleOf(ns),
|
|
388
426
|
fresh: ns.fresh === true,
|
|
389
427
|
confirmedAt: ns.confirmed_at ?? null,
|
|
390
428
|
lastSeenAt: ns.last_seen_at ?? null,
|
|
391
429
|
};
|
|
392
|
-
})
|
|
393
|
-
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
peerId: String(row.peer_id ?? ''),
|
|
433
|
+
relayUrl,
|
|
434
|
+
executorAccount,
|
|
435
|
+
canExecute: canExecuteOf(row, relayUrl !== null && executorAccount !== null && namespaces.some((n) => n.authorshipReady)),
|
|
436
|
+
namespaces,
|
|
437
|
+
};
|
|
438
|
+
});
|
|
394
439
|
}
|
|
395
440
|
/**
|
|
396
441
|
* Claim a namespace under this account, proving ownership.
|
package/dist/cloud/connect.d.ts
CHANGED
|
@@ -106,7 +106,8 @@ export interface CloudConnection {
|
|
|
106
106
|
*
|
|
107
107
|
* Throws rather than returning a half-usable connection, and the message says
|
|
108
108
|
* which step is missing — no namespace, several to choose between, HA not
|
|
109
|
-
* enabled, or no relay
|
|
109
|
+
* enabled, or no relay that relays (only replicas, or relays waiting for a
|
|
110
|
+
* grant). Every one of those is
|
|
110
111
|
* something the user has to go and do, and a client that reports them alike
|
|
111
112
|
* cannot tell them what.
|
|
112
113
|
*/
|
package/dist/cloud/connect.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* rather than presenting a half-working `MeroJs` whose `admin` and `rpc` would
|
|
35
35
|
* need a credential nobody holds.
|
|
36
36
|
*/
|
|
37
|
-
import { CloudClient } from './cloud-client.js';
|
|
37
|
+
import { CloudClient, pickExecutingRelay } from './cloud-client.js';
|
|
38
38
|
import { RelayClient } from '../relay/relay-client.js';
|
|
39
39
|
import { createLocalStorageNonceSource, createMemoryNonceSource } from '../relay/nonce-source.js';
|
|
40
40
|
import { derivePublicKey, hex } from '../crypto/internal.js';
|
|
@@ -44,7 +44,8 @@ import { createAttestedSealedFetch } from '../sealed/sealed.js';
|
|
|
44
44
|
*
|
|
45
45
|
* Throws rather than returning a half-usable connection, and the message says
|
|
46
46
|
* which step is missing — no namespace, several to choose between, HA not
|
|
47
|
-
* enabled, or no relay
|
|
47
|
+
* enabled, or no relay that relays (only replicas, or relays waiting for a
|
|
48
|
+
* grant). Every one of those is
|
|
48
49
|
* something the user has to go and do, and a client that reports them alike
|
|
49
50
|
* cannot tell them what.
|
|
50
51
|
*/
|
|
@@ -69,13 +70,12 @@ export async function connectCloud(options) {
|
|
|
69
70
|
// first — reporting "waiting for the grant" about a relay that was already
|
|
70
71
|
// usable a moment ago.
|
|
71
72
|
const relays = await cloud.getNamespaceRelays(namespace.namespaceId);
|
|
72
|
-
const relayInfo = relays
|
|
73
|
-
null;
|
|
73
|
+
const relayInfo = pickExecutingRelay(relays);
|
|
74
74
|
if (!relayInfo) {
|
|
75
75
|
throw new Error(explainNoRelay(namespace, relays));
|
|
76
76
|
}
|
|
77
77
|
const relay = new RelayClient({
|
|
78
|
-
// Checked by `
|
|
78
|
+
// Checked by `pickExecutingRelay`, which is the only thing that returns a
|
|
79
79
|
// relay — narrowed here rather than re-validated, so the invariant lives in
|
|
80
80
|
// one place.
|
|
81
81
|
relayUrl: relayInfo.relayUrl,
|
|
@@ -125,11 +125,11 @@ function pickNamespace(namespaces, requested) {
|
|
|
125
125
|
return namespaces[0];
|
|
126
126
|
}
|
|
127
127
|
/**
|
|
128
|
-
* Why no relay can write here — the
|
|
128
|
+
* Why no relay can write here — the states, kept apart.
|
|
129
129
|
*
|
|
130
|
-
* They are
|
|
131
|
-
*
|
|
132
|
-
* the wrong one.
|
|
130
|
+
* They are different asks of the user (turn HA on, wait, have an admin admit
|
|
131
|
+
* TEEs as relays, or have an admin grant a capability) and one message for all
|
|
132
|
+
* of them would send them to the wrong one.
|
|
133
133
|
*/
|
|
134
134
|
function explainNoRelay(namespace, relays) {
|
|
135
135
|
if (relays.length === 0) {
|
|
@@ -137,8 +137,19 @@ function explainNoRelay(namespace, relays) {
|
|
|
137
137
|
? `namespace ${namespace.namespaceId} has HA enabled but no relay has been assigned to it yet — retry shortly`
|
|
138
138
|
: `namespace ${namespace.namespaceId} has no relays: enable HA on it in the cloud to have an attested node assigned`;
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// A node that reports a role is decided by it alone: a `ReadOnlyTee` is a
|
|
141
|
+
// replica and never relays, whatever its grant, so asking for the grant would
|
|
142
|
+
// send the admin to the wrong fix. Only a node too old to report a role is
|
|
143
|
+
// still waiting on `CAN_AUTHOR_ON_BEHALF`.
|
|
144
|
+
const replicas = relays.filter((r) => r.teeRole !== null && r.teeRole !== 'RelayTee');
|
|
145
|
+
if (replicas.length === relays.length) {
|
|
146
|
+
return (`every node on namespace ${namespace.namespaceId} is a read-only TEE replica (ReadOnlyTee), ` +
|
|
147
|
+
`which never relays delegated writes: an admin of the namespace must set its TEE admission ` +
|
|
148
|
+
`policy with mode: 'relay' — AdminClient.setTeeAdmissionPolicy(namespaceId, { ..., mode: 'relay' }) ` +
|
|
149
|
+
`— so fleet nodes are admitted as RelayTee`);
|
|
150
|
+
}
|
|
151
|
+
const waitingOnGrant = relays.filter((r) => r.teeRole === null && !r.authorshipReady);
|
|
152
|
+
if (waitingOnGrant.length > 0 && waitingOnGrant.length + replicas.length === relays.length) {
|
|
142
153
|
// Name the call, not just the capability. The grant is a governance op only
|
|
143
154
|
// a namespace admin can publish, so this message is read by someone who has
|
|
144
155
|
// to go ask for it — and "grant CAN_AUTHOR_ON_BEHALF" alone leaves them
|
|
@@ -149,7 +160,7 @@ function explainNoRelay(namespace, relays) {
|
|
|
149
160
|
`an admin of the namespace must grant CAN_AUTHOR_ON_BEHALF to ${waitingOnGrant.map((r) => r.executorAccount ?? r.peerId).join(', ') || 'the relay account'} — with AdminClient.grantAuthorship(namespaceId, account) for these relays, and ` +
|
|
150
161
|
`AdminClient.openToDelegatedExecution(namespaceId) so relays assigned later need no further op`);
|
|
151
162
|
}
|
|
152
|
-
return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none
|
|
163
|
+
return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none is ready to relay yet (a URL, an executor account and a recent heartbeat) — retry shortly`;
|
|
153
164
|
}
|
|
154
165
|
/**
|
|
155
166
|
* A persisted nonce source keyed by the author's device public key.
|