@calimero-network/mero-js 19.5.0 → 19.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.
- package/dist/account/account.d.ts +76 -0
- package/dist/account/account.js +0 -0
- package/dist/account/index.d.ts +2 -0
- package/dist/account/index.js +2 -0
- package/dist/account/wordlist.d.ts +16 -0
- package/dist/account/wordlist.js +273 -0
- package/dist/admin-api/admin-client.d.ts +69 -0
- package/dist/admin-api/admin-client.js +80 -0
- package/dist/cloud/cloud-client.d.ts +182 -0
- package/dist/cloud/cloud-client.js +142 -0
- package/dist/cloud/connect.js +8 -1
- package/dist/cloud/index.d.ts +1 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +2585 -212
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.mjs +2585 -212
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
|
@@ -94,6 +94,62 @@ export interface CloudRelay {
|
|
|
94
94
|
lastSeenAt?: string | null;
|
|
95
95
|
confirmedAt?: string | null;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* One node serving a namespace, as a caller with no cloud account sees it.
|
|
99
|
+
*
|
|
100
|
+
* The public counterpart to {@link CloudRelay}: same fleet rows, but keyed on a
|
|
101
|
+
* namespace id anyone holding an invitation already has, and answering both
|
|
102
|
+
* things a keyholder needs from a node rather than one of them.
|
|
103
|
+
*/
|
|
104
|
+
export interface CloudNamespaceNode {
|
|
105
|
+
peerId: string;
|
|
106
|
+
/**
|
|
107
|
+
* The node's own account id, hex.
|
|
108
|
+
*
|
|
109
|
+
* Intersect this against the invitation's signed `admitters` list before
|
|
110
|
+
* using the node. That list is a MINT-TIME SNAPSHOT, so a node assigned after
|
|
111
|
+
* an invitation was created is live, healthy, listed here, and still refused
|
|
112
|
+
* with 403 — the cloud cannot know which invitation you hold.
|
|
113
|
+
*/
|
|
114
|
+
account: string | null;
|
|
115
|
+
/** Base URL, or `null` when the cloud knows none yet. */
|
|
116
|
+
relayUrl: string | null;
|
|
117
|
+
/** Ready-made admission URL, so a caller never rebuilds the path. */
|
|
118
|
+
admitUrl: string | null;
|
|
119
|
+
/** `'assigned'` (admitted pending confirm) or `'active'`. */
|
|
120
|
+
status: string;
|
|
121
|
+
/** Whether the node's heartbeat for this namespace is recent. */
|
|
122
|
+
fresh: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Whether a signed join can be presented here now.
|
|
125
|
+
*
|
|
126
|
+
* Deliberately independent of {@link CloudNamespaceNode.authorshipReady}:
|
|
127
|
+
* relaying a join the joiner already signed is not authoring on anyone's
|
|
128
|
+
* behalf, so a node with no grant admits perfectly well.
|
|
129
|
+
*/
|
|
130
|
+
canAdmit: boolean;
|
|
131
|
+
/** Whether the node holds `CAN_AUTHOR_ON_BEHALF` on the namespace root group. */
|
|
132
|
+
authorshipReady: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Whether a delegated write can be posted here now — a URL, an account, a
|
|
135
|
+
* fresh heartbeat and the grant.
|
|
136
|
+
*
|
|
137
|
+
* Namespace-level. A context in a RESTRICTED SUBGROUP is governed by that
|
|
138
|
+
* subgroup, so per-context routing remains the exact answer if a write is
|
|
139
|
+
* refused despite this being true.
|
|
140
|
+
*/
|
|
141
|
+
canExecute: boolean;
|
|
142
|
+
}
|
|
143
|
+
/** What {@link CloudClient.getNamespaceRouting} reports. */
|
|
144
|
+
export interface CloudNamespaceRouting {
|
|
145
|
+
namespaceId: string;
|
|
146
|
+
/** Usable nodes first, then a stable order, so a polling caller sees no shuffle. */
|
|
147
|
+
nodes: CloudNamespaceNode[];
|
|
148
|
+
/** Whether any listed node can take a join now. */
|
|
149
|
+
servable: boolean;
|
|
150
|
+
/** Whether any listed node can take a delegated write now. */
|
|
151
|
+
writable: boolean;
|
|
152
|
+
}
|
|
97
153
|
/** One namespace of the caller's that a machine is serving. */
|
|
98
154
|
export interface CloudMachineNamespace {
|
|
99
155
|
namespaceId: string;
|
|
@@ -134,6 +190,52 @@ export interface CloudMachine {
|
|
|
134
190
|
/** Only the caller's own namespaces, even on a machine shared with others. */
|
|
135
191
|
namespaces: CloudMachineNamespace[];
|
|
136
192
|
}
|
|
193
|
+
/** A Calimero account linked to this cloud login. */
|
|
194
|
+
export interface CloudLinkedAccount {
|
|
195
|
+
/** The account id, 64 hex. */
|
|
196
|
+
accountId: string;
|
|
197
|
+
linkedAt?: string | null;
|
|
198
|
+
/**
|
|
199
|
+
* Whether a recovery envelope has been stored for this account, so a UI can
|
|
200
|
+
* say "recoverable" without fetching the blob.
|
|
201
|
+
*/
|
|
202
|
+
hasRecoveryEnvelope: boolean;
|
|
203
|
+
}
|
|
204
|
+
/** What {@link CloudClient.getMyAccounts} reports. */
|
|
205
|
+
export interface CloudLinkedAccounts {
|
|
206
|
+
accounts: CloudLinkedAccount[];
|
|
207
|
+
/** How many the caller's plan allows. `null` means no limit. */
|
|
208
|
+
limit: number | null;
|
|
209
|
+
}
|
|
210
|
+
/** A single-use challenge to sign with an account root. */
|
|
211
|
+
export interface AccountLinkChallenge {
|
|
212
|
+
/** Sign this verbatim — it is an opaque sealed string, not a decodable id. */
|
|
213
|
+
nonce: string;
|
|
214
|
+
/** When it stops being accepted, epoch milliseconds. */
|
|
215
|
+
expiresAtMs: number | null;
|
|
216
|
+
}
|
|
217
|
+
/** A proof assembled by whoever holds the root, for {@link CloudClient.submitAccountLink}. */
|
|
218
|
+
export interface AccountLinkProof {
|
|
219
|
+
/** The account being claimed, 64 hex. */
|
|
220
|
+
accountId: string;
|
|
221
|
+
/** The root that names it, 64 hex. */
|
|
222
|
+
rootPublicKey: string;
|
|
223
|
+
/** The challenge that was signed. */
|
|
224
|
+
nonce: string;
|
|
225
|
+
/** `signAccountLink`'s output — standard base64. */
|
|
226
|
+
signature: string;
|
|
227
|
+
}
|
|
228
|
+
/** The outcome of {@link CloudClient.linkAccount}. */
|
|
229
|
+
export interface CloudAccountLink {
|
|
230
|
+
accountId: string;
|
|
231
|
+
linkedAt?: string | null;
|
|
232
|
+
/**
|
|
233
|
+
* True when this account was already linked to this same login, which the
|
|
234
|
+
* cloud treats as a 200 no-op rather than an error. A caller re-running
|
|
235
|
+
* onboarding gets success, not a confusing failure.
|
|
236
|
+
*/
|
|
237
|
+
alreadyLinked: boolean;
|
|
238
|
+
}
|
|
137
239
|
export interface EnableHAOptions {
|
|
138
240
|
groupId: string;
|
|
139
241
|
contextId: string;
|
|
@@ -201,6 +303,39 @@ export declare class CloudClient {
|
|
|
201
303
|
* must be in place.
|
|
202
304
|
*/
|
|
203
305
|
findExecutingRelay(namespaceId: string): Promise<CloudRelay | null>;
|
|
306
|
+
/**
|
|
307
|
+
* Where to reach a namespace without owning it: one read, both verbs.
|
|
308
|
+
*
|
|
309
|
+
* **Anonymous.** A joiner is by construction not the namespace owner and holds
|
|
310
|
+
* no cloud session — needing one is exactly what this path exists to avoid.
|
|
311
|
+
* Discovery is not authorization: merod refuses an `/admit` call unless the
|
|
312
|
+
* relaying node's account is on the invitation's signed `admitters` list, and
|
|
313
|
+
* refuses an intent unless the warrant is genuinely the member's, so naming a
|
|
314
|
+
* node grants the caller nothing it did not already have.
|
|
315
|
+
*
|
|
316
|
+
* A keyholder resolves a namespace to a node ONCE and then admits, reads and
|
|
317
|
+
* posts intents against that same node, which is why admission and writability
|
|
318
|
+
* are answered together rather than across two endpoints — the per-context
|
|
319
|
+
* read needs a context id a joiner does not yet have.
|
|
320
|
+
*
|
|
321
|
+
* An empty `nodes` list is a state, not an error: a namespace with no fleet
|
|
322
|
+
* assignment has no cloud node, and the joiner must use one of the
|
|
323
|
+
* invitation's other admitters — another admin's node, or a self-hosted peer.
|
|
324
|
+
*/
|
|
325
|
+
getNamespaceRouting(namespaceId: string): Promise<CloudNamespaceRouting>;
|
|
326
|
+
/**
|
|
327
|
+
* The node to send a signed join to, chosen from those an invitation names.
|
|
328
|
+
*
|
|
329
|
+
* Pass the invitation's `admitters` and the intersection is done here, because
|
|
330
|
+
* skipping it is the failure this method exists to prevent: the cloud lists
|
|
331
|
+
* every node assigned to the namespace, while `admitters` is a snapshot signed
|
|
332
|
+
* when the invitation was minted. A node added afterwards looks perfect in
|
|
333
|
+
* every field here and answers 403.
|
|
334
|
+
*
|
|
335
|
+
* Omit `admitters` only for an invitation that names none — an empty list on
|
|
336
|
+
* the wire authorises any node to admit.
|
|
337
|
+
*/
|
|
338
|
+
findAdmitter(namespaceId: string, admitters?: readonly string[]): Promise<CloudNamespaceNode | null>;
|
|
204
339
|
/**
|
|
205
340
|
* Every attested machine currently serving something this account owns.
|
|
206
341
|
*
|
|
@@ -265,6 +400,53 @@ export declare class CloudClient {
|
|
|
265
400
|
enableHA(options: EnableHAOptions): void;
|
|
266
401
|
disableHA(options: DisableHAOptions): void;
|
|
267
402
|
/** Record a freshly-issued session and tell the app to persist it. */
|
|
403
|
+
/**
|
|
404
|
+
* Link a Calimero account to this cloud login, proving possession of its root.
|
|
405
|
+
*
|
|
406
|
+
* Two hops, folded into one call because they are useless apart: fetch a
|
|
407
|
+
* single-use challenge, then sign it with the account root and post the proof.
|
|
408
|
+
* The secret never leaves this process — only a signature over a nonce the
|
|
409
|
+
* cloud minted does.
|
|
410
|
+
*
|
|
411
|
+
* This is the binding the cloud bills against. A relay does not need it to run
|
|
412
|
+
* an intent; the cloud needs it to know whose plan a hosted node is spending,
|
|
413
|
+
* and to sign that account back in later.
|
|
414
|
+
*
|
|
415
|
+
* @param rootSecret the account root's signing secret, 64 hex — from
|
|
416
|
+
* {@link generateAccountRoot}, from a restored recovery phrase, or from a
|
|
417
|
+
* desktop app or hardware key that already holds one.
|
|
418
|
+
*
|
|
419
|
+
* Idempotent for the same login. An account already linked to a *different*
|
|
420
|
+
* cloud login is a `409`; a replayed or expired challenge is a `403`; and
|
|
421
|
+
* exceeding the plan's account limit is a `402`.
|
|
422
|
+
*/
|
|
423
|
+
linkAccount(rootSecret: string): Promise<CloudAccountLink>;
|
|
424
|
+
/**
|
|
425
|
+
* Mint a single-use challenge for an account root to sign.
|
|
426
|
+
*
|
|
427
|
+
* The split half of {@link linkAccount}, for the case that call cannot serve:
|
|
428
|
+
* a root held by a desktop app, a hardware key or an air-gapped machine, which
|
|
429
|
+
* signs without the secret ever reaching this process. Fetch the challenge
|
|
430
|
+
* here, have the holder sign it, then hand the proof to
|
|
431
|
+
* {@link submitAccountLink}.
|
|
432
|
+
*
|
|
433
|
+
* Bound to the signed-in login and short-lived, so it is not a standing
|
|
434
|
+
* capability if it leaks — but it IS single-use, so a challenge fetched and
|
|
435
|
+
* abandoned is spent.
|
|
436
|
+
*/
|
|
437
|
+
getAccountLinkChallenge(): Promise<AccountLinkChallenge>;
|
|
438
|
+
/**
|
|
439
|
+
* Post a proof assembled elsewhere. The other half of
|
|
440
|
+
* {@link getAccountLinkChallenge}.
|
|
441
|
+
*
|
|
442
|
+
* `accountId` must be the account `rootPublicKey` names — the cloud refuses a
|
|
443
|
+
* valid signature by a key that names a different account, or a caller could
|
|
444
|
+
* prove one root and link somebody else's account. Derive both from the same
|
|
445
|
+
* key rather than letting a user type either.
|
|
446
|
+
*/
|
|
447
|
+
submitAccountLink(proof: AccountLinkProof): Promise<CloudAccountLink>;
|
|
448
|
+
/** The accounts linked to this login, and how many the plan allows. */
|
|
449
|
+
getMyAccounts(): Promise<CloudLinkedAccounts>;
|
|
268
450
|
private adoptSession;
|
|
269
451
|
private notifySession;
|
|
270
452
|
private request;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* ever sent, and would be a throughput bottleneck on a path that has no reason
|
|
25
25
|
* to involve it.
|
|
26
26
|
*/
|
|
27
|
+
import { accountRootFromSecret, signAccountLink, } from '../account/index.js';
|
|
27
28
|
import { HTTPError } from '../http-client/web-client.js';
|
|
28
29
|
/** Where the hosted cloud lives when a caller names no other. */
|
|
29
30
|
const DEFAULT_CLOUD_BASE_URL = 'https://cloud.calimero.network';
|
|
@@ -148,6 +149,64 @@ export class CloudClient {
|
|
|
148
149
|
return (relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ??
|
|
149
150
|
null);
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Where to reach a namespace without owning it: one read, both verbs.
|
|
154
|
+
*
|
|
155
|
+
* **Anonymous.** A joiner is by construction not the namespace owner and holds
|
|
156
|
+
* no cloud session — needing one is exactly what this path exists to avoid.
|
|
157
|
+
* Discovery is not authorization: merod refuses an `/admit` call unless the
|
|
158
|
+
* relaying node's account is on the invitation's signed `admitters` list, and
|
|
159
|
+
* refuses an intent unless the warrant is genuinely the member's, so naming a
|
|
160
|
+
* node grants the caller nothing it did not already have.
|
|
161
|
+
*
|
|
162
|
+
* A keyholder resolves a namespace to a node ONCE and then admits, reads and
|
|
163
|
+
* posts intents against that same node, which is why admission and writability
|
|
164
|
+
* are answered together rather than across two endpoints — the per-context
|
|
165
|
+
* read needs a context id a joiner does not yet have.
|
|
166
|
+
*
|
|
167
|
+
* An empty `nodes` list is a state, not an error: a namespace with no fleet
|
|
168
|
+
* assignment has no cloud node, and the joiner must use one of the
|
|
169
|
+
* invitation's other admitters — another admin's node, or a self-hosted peer.
|
|
170
|
+
*/
|
|
171
|
+
async getNamespaceRouting(namespaceId) {
|
|
172
|
+
const body = await this.request('GET', `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/admitters`, {
|
|
173
|
+
anonymous: true,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
namespaceId: String(body.namespace_id ?? namespaceId),
|
|
177
|
+
nodes: (body.admitters ?? []).map((row) => ({
|
|
178
|
+
peerId: String(row.peer_id ?? ''),
|
|
179
|
+
account: row.account ?? null,
|
|
180
|
+
relayUrl: row.relay_url ?? null,
|
|
181
|
+
admitUrl: row.admit_url ?? null,
|
|
182
|
+
status: String(row.status ?? ''),
|
|
183
|
+
fresh: row.fresh === true,
|
|
184
|
+
canAdmit: row.can_admit === true,
|
|
185
|
+
authorshipReady: row.authorship_ready === true,
|
|
186
|
+
canExecute: row.can_execute === true,
|
|
187
|
+
})),
|
|
188
|
+
servable: body.servable === true,
|
|
189
|
+
writable: body.writable === true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The node to send a signed join to, chosen from those an invitation names.
|
|
194
|
+
*
|
|
195
|
+
* Pass the invitation's `admitters` and the intersection is done here, because
|
|
196
|
+
* skipping it is the failure this method exists to prevent: the cloud lists
|
|
197
|
+
* every node assigned to the namespace, while `admitters` is a snapshot signed
|
|
198
|
+
* when the invitation was minted. A node added afterwards looks perfect in
|
|
199
|
+
* every field here and answers 403.
|
|
200
|
+
*
|
|
201
|
+
* Omit `admitters` only for an invitation that names none — an empty list on
|
|
202
|
+
* the wire authorises any node to admit.
|
|
203
|
+
*/
|
|
204
|
+
async findAdmitter(namespaceId, admitters) {
|
|
205
|
+
const { nodes } = await this.getNamespaceRouting(namespaceId);
|
|
206
|
+
const allowed = admitters && admitters.length > 0 ? new Set(admitters.map((a) => a.toLowerCase())) : null;
|
|
207
|
+
return (nodes.find((n) => n.canAdmit &&
|
|
208
|
+
(allowed === null || (n.account !== null && allowed.has(n.account.toLowerCase())))) ?? null);
|
|
209
|
+
}
|
|
151
210
|
/**
|
|
152
211
|
* Every attested machine currently serving something this account owns.
|
|
153
212
|
*
|
|
@@ -260,6 +319,89 @@ export class CloudClient {
|
|
|
260
319
|
window.open(`${this.baseUrl}/disable-ha?${params.toString()}`);
|
|
261
320
|
}
|
|
262
321
|
/** Record a freshly-issued session and tell the app to persist it. */
|
|
322
|
+
/**
|
|
323
|
+
* Link a Calimero account to this cloud login, proving possession of its root.
|
|
324
|
+
*
|
|
325
|
+
* Two hops, folded into one call because they are useless apart: fetch a
|
|
326
|
+
* single-use challenge, then sign it with the account root and post the proof.
|
|
327
|
+
* The secret never leaves this process — only a signature over a nonce the
|
|
328
|
+
* cloud minted does.
|
|
329
|
+
*
|
|
330
|
+
* This is the binding the cloud bills against. A relay does not need it to run
|
|
331
|
+
* an intent; the cloud needs it to know whose plan a hosted node is spending,
|
|
332
|
+
* and to sign that account back in later.
|
|
333
|
+
*
|
|
334
|
+
* @param rootSecret the account root's signing secret, 64 hex — from
|
|
335
|
+
* {@link generateAccountRoot}, from a restored recovery phrase, or from a
|
|
336
|
+
* desktop app or hardware key that already holds one.
|
|
337
|
+
*
|
|
338
|
+
* Idempotent for the same login. An account already linked to a *different*
|
|
339
|
+
* cloud login is a `409`; a replayed or expired challenge is a `403`; and
|
|
340
|
+
* exceeding the plan's account limit is a `402`.
|
|
341
|
+
*/
|
|
342
|
+
async linkAccount(rootSecret) {
|
|
343
|
+
const root = await accountRootFromSecret(rootSecret);
|
|
344
|
+
const { nonce } = await this.getAccountLinkChallenge();
|
|
345
|
+
return this.submitAccountLink({
|
|
346
|
+
accountId: root.accountId,
|
|
347
|
+
rootPublicKey: root.publicKey,
|
|
348
|
+
nonce,
|
|
349
|
+
signature: await signAccountLink({ rootSecret, nonce }),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Mint a single-use challenge for an account root to sign.
|
|
354
|
+
*
|
|
355
|
+
* The split half of {@link linkAccount}, for the case that call cannot serve:
|
|
356
|
+
* a root held by a desktop app, a hardware key or an air-gapped machine, which
|
|
357
|
+
* signs without the secret ever reaching this process. Fetch the challenge
|
|
358
|
+
* here, have the holder sign it, then hand the proof to
|
|
359
|
+
* {@link submitAccountLink}.
|
|
360
|
+
*
|
|
361
|
+
* Bound to the signed-in login and short-lived, so it is not a standing
|
|
362
|
+
* capability if it leaks — but it IS single-use, so a challenge fetched and
|
|
363
|
+
* abandoned is spent.
|
|
364
|
+
*/
|
|
365
|
+
async getAccountLinkChallenge() {
|
|
366
|
+
const body = await this.request('GET', '/api/cloud/me/accounts/challenge');
|
|
367
|
+
return { nonce: body.nonce, expiresAtMs: body.expires_at_ms ?? null };
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Post a proof assembled elsewhere. The other half of
|
|
371
|
+
* {@link getAccountLinkChallenge}.
|
|
372
|
+
*
|
|
373
|
+
* `accountId` must be the account `rootPublicKey` names — the cloud refuses a
|
|
374
|
+
* valid signature by a key that names a different account, or a caller could
|
|
375
|
+
* prove one root and link somebody else's account. Derive both from the same
|
|
376
|
+
* key rather than letting a user type either.
|
|
377
|
+
*/
|
|
378
|
+
async submitAccountLink(proof) {
|
|
379
|
+
const body = await this.request('POST', '/api/cloud/me/accounts', {
|
|
380
|
+
body: {
|
|
381
|
+
account_id: proof.accountId,
|
|
382
|
+
root_public_key: proof.rootPublicKey,
|
|
383
|
+
nonce: proof.nonce,
|
|
384
|
+
signature: proof.signature,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
accountId: String(body.account_id ?? proof.accountId),
|
|
389
|
+
linkedAt: body.linked_at ?? null,
|
|
390
|
+
alreadyLinked: body.already_linked === true,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/** The accounts linked to this login, and how many the plan allows. */
|
|
394
|
+
async getMyAccounts() {
|
|
395
|
+
const body = await this.request('GET', '/api/cloud/me/accounts');
|
|
396
|
+
return {
|
|
397
|
+
accounts: (body.accounts ?? []).map((row) => ({
|
|
398
|
+
accountId: String(row.account_id ?? ''),
|
|
399
|
+
linkedAt: row.linked_at ?? null,
|
|
400
|
+
hasRecoveryEnvelope: row.has_recovery_envelope === true,
|
|
401
|
+
})),
|
|
402
|
+
limit: body.limit ?? null,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
263
405
|
adoptSession(body) {
|
|
264
406
|
this.session = {
|
|
265
407
|
sessionToken: body.session_token,
|
package/dist/cloud/connect.js
CHANGED
|
@@ -123,8 +123,15 @@ function explainNoRelay(namespace, relays) {
|
|
|
123
123
|
}
|
|
124
124
|
const waitingOnGrant = relays.filter((r) => !r.authorshipReady);
|
|
125
125
|
if (waitingOnGrant.length === relays.length) {
|
|
126
|
+
// Name the call, not just the capability. The grant is a governance op only
|
|
127
|
+
// a namespace admin can publish, so this message is read by someone who has
|
|
128
|
+
// to go ask for it — and "grant CAN_AUTHOR_ON_BEHALF" alone leaves them
|
|
129
|
+
// relaying a constant to an admin who then has to work out the command.
|
|
130
|
+
// `openToDelegatedExecution` is the durable half (every relay assigned from
|
|
131
|
+
// then on lands open), `grantAuthorship` the one for relays already here.
|
|
126
132
|
return (`every relay on namespace ${namespace.namespaceId} is waiting for the authorship grant: ` +
|
|
127
|
-
`an admin of the namespace must grant CAN_AUTHOR_ON_BEHALF to ${waitingOnGrant.map((r) => r.executorAccount ?? r.peerId).join(', ') || 'the relay account'}`
|
|
133
|
+
`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 ` +
|
|
134
|
+
`AdminClient.openToDelegatedExecution(namespaceId) so relays assigned later need no further op`);
|
|
128
135
|
}
|
|
129
136
|
return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none reported both a URL and an executor account yet — retry shortly`;
|
|
130
137
|
}
|
package/dist/cloud/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { CloudClient } from './cloud-client.js';
|
|
2
|
-
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudMachine, CloudMachineNamespace, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
|
|
2
|
+
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
|
|
3
3
|
export { connectCloud } from './connect.js';
|
|
4
4
|
export type { ConnectCloudOptions, CloudConnection } from './connect.js';
|