@calimero-network/mero-js 19.12.0 → 19.13.1
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/cloud/cloud-client.d.ts +45 -1
- package/dist/cloud/cloud-client.js +54 -0
- package/dist/cloud/index.d.ts +2 -2
- package/dist/cloud/routing-proof.d.ts +22 -1
- package/dist/cloud/routing-proof.js +4 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +55 -0
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +55 -0
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
|
@@ -24,7 +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 { type RoutingChallenge, type RoutingCredential } from './routing-proof.js';
|
|
27
|
+
import { type DiscoveryChallenge, type RoutingChallenge, type RoutingCredential } from './routing-proof.js';
|
|
28
28
|
export interface CloudClientConfig {
|
|
29
29
|
cloudBaseUrl?: string;
|
|
30
30
|
/**
|
|
@@ -116,6 +116,20 @@ export interface CloudRelay {
|
|
|
116
116
|
* namespace id anyone holding an invitation already has, and answering both
|
|
117
117
|
* things a keyholder needs from a node rather than one of them.
|
|
118
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* One relay that serves an account, as the bootstrap read reports it.
|
|
121
|
+
*
|
|
122
|
+
* `fresh` and a null `relayUrl` are reported rather than filtered, because
|
|
123
|
+
* "your relay is down", "we have no address for it" and "you have no relay"
|
|
124
|
+
* need different actions from a caller and an empty list erases the
|
|
125
|
+
* distinction. A caller looking for somewhere to talk to wants
|
|
126
|
+
* `fresh && relayUrl`; one diagnosing wants the rest.
|
|
127
|
+
*/
|
|
128
|
+
export interface CloudAccountRelay {
|
|
129
|
+
peerId: string;
|
|
130
|
+
relayUrl: string | null;
|
|
131
|
+
fresh: boolean;
|
|
132
|
+
}
|
|
119
133
|
export interface CloudNamespaceNode {
|
|
120
134
|
peerId: string;
|
|
121
135
|
/**
|
|
@@ -438,6 +452,36 @@ export declare class CloudClient {
|
|
|
438
452
|
* for a class of expiry bug that only shows up on a slow client.
|
|
439
453
|
*/
|
|
440
454
|
private routingProof;
|
|
455
|
+
/**
|
|
456
|
+
* Ask the cloud for a challenge bound to one ACCOUNT.
|
|
457
|
+
*
|
|
458
|
+
* Public and unauthenticated like {@link getRoutingChallenge}: an account id
|
|
459
|
+
* is `H(genesis(root_pk))` and travels in the clear in every device-link op,
|
|
460
|
+
* so naming one here reveals nothing and authorises nothing. MDMA re-derives
|
|
461
|
+
* the account from the certificate and refuses a mismatch.
|
|
462
|
+
*/
|
|
463
|
+
getAccountRelaysChallenge(accountId: string): Promise<DiscoveryChallenge>;
|
|
464
|
+
/**
|
|
465
|
+
* Which relays serve this account — the bootstrap, and nothing more.
|
|
466
|
+
*
|
|
467
|
+
* A device holding a certificate can resolve a namespace it can *name*
|
|
468
|
+
* ({@link getNamespaceRouting}) and can ask a node it can *reach* what its
|
|
469
|
+
* account belongs to. It starts with neither. This closes that one gap.
|
|
470
|
+
*
|
|
471
|
+
* **It does not return namespaces, deliberately.** The node answers that from
|
|
472
|
+
* the governance DAG, scoped to the calling account: log in at one of these
|
|
473
|
+
* relays with an account proof and read its namespace listing. A cloud-side
|
|
474
|
+
* copy would be a second source of truth built from rows that linger for an
|
|
475
|
+
* account which left a namespace.
|
|
476
|
+
*
|
|
477
|
+
* So the sequence is: this → log in at a relay → ask it what you are in.
|
|
478
|
+
*
|
|
479
|
+
* Proven with the **device certificate**, not a cloud session, so it needs a
|
|
480
|
+
* `routingCredential` and throws without one rather than returning an empty
|
|
481
|
+
* list — "you configured no credential" and "you have no relays" are
|
|
482
|
+
* different answers and must not look alike.
|
|
483
|
+
*/
|
|
484
|
+
getAccountRelays(accountId: string): Promise<CloudAccountRelay[]>;
|
|
441
485
|
/**
|
|
442
486
|
* The node to send a signed join to, chosen from those an invitation names.
|
|
443
487
|
*
|
|
@@ -288,6 +288,60 @@ export class CloudClient {
|
|
|
288
288
|
const challenge = await this.getRoutingChallenge(namespaceId);
|
|
289
289
|
return routingProofHeaders(challenge, this.routingCredential);
|
|
290
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Ask the cloud for a challenge bound to one ACCOUNT.
|
|
293
|
+
*
|
|
294
|
+
* Public and unauthenticated like {@link getRoutingChallenge}: an account id
|
|
295
|
+
* is `H(genesis(root_pk))` and travels in the clear in every device-link op,
|
|
296
|
+
* so naming one here reveals nothing and authorises nothing. MDMA re-derives
|
|
297
|
+
* the account from the certificate and refuses a mismatch.
|
|
298
|
+
*/
|
|
299
|
+
async getAccountRelaysChallenge(accountId) {
|
|
300
|
+
const body = await this.request('GET', `/api/cloud/accounts/${encodeURIComponent(accountId)}/challenge`, {
|
|
301
|
+
anonymous: true,
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
accountId: String(body.account_id ?? accountId),
|
|
305
|
+
nonce: String(body.nonce ?? ''),
|
|
306
|
+
expiresAtMs: Number(body.expires_at_ms ?? 0),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Which relays serve this account — the bootstrap, and nothing more.
|
|
311
|
+
*
|
|
312
|
+
* A device holding a certificate can resolve a namespace it can *name*
|
|
313
|
+
* ({@link getNamespaceRouting}) and can ask a node it can *reach* what its
|
|
314
|
+
* account belongs to. It starts with neither. This closes that one gap.
|
|
315
|
+
*
|
|
316
|
+
* **It does not return namespaces, deliberately.** The node answers that from
|
|
317
|
+
* the governance DAG, scoped to the calling account: log in at one of these
|
|
318
|
+
* relays with an account proof and read its namespace listing. A cloud-side
|
|
319
|
+
* copy would be a second source of truth built from rows that linger for an
|
|
320
|
+
* account which left a namespace.
|
|
321
|
+
*
|
|
322
|
+
* So the sequence is: this → log in at a relay → ask it what you are in.
|
|
323
|
+
*
|
|
324
|
+
* Proven with the **device certificate**, not a cloud session, so it needs a
|
|
325
|
+
* `routingCredential` and throws without one rather than returning an empty
|
|
326
|
+
* list — "you configured no credential" and "you have no relays" are
|
|
327
|
+
* different answers and must not look alike.
|
|
328
|
+
*/
|
|
329
|
+
async getAccountRelays(accountId) {
|
|
330
|
+
if (!this.routingCredential) {
|
|
331
|
+
throw new Error('getAccountRelays needs a routingCredential: construct the client with the device ' +
|
|
332
|
+
'certificate and its signing secret, since this read is proven by the device, not by a session.');
|
|
333
|
+
}
|
|
334
|
+
const challenge = await this.getAccountRelaysChallenge(accountId);
|
|
335
|
+
const body = await this.request('GET', `/api/cloud/accounts/${encodeURIComponent(accountId)}/relays`, {
|
|
336
|
+
anonymous: true,
|
|
337
|
+
headers: await routingProofHeaders(challenge, this.routingCredential),
|
|
338
|
+
});
|
|
339
|
+
return (body.relays ?? []).map((row) => ({
|
|
340
|
+
peerId: String(row.peer_id ?? ''),
|
|
341
|
+
relayUrl: row.relay_url ?? null,
|
|
342
|
+
fresh: row.fresh === true,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
291
345
|
/**
|
|
292
346
|
* The node to send a signed join to, chosen from those an invitation names.
|
|
293
347
|
*
|
package/dist/cloud/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { CloudClient } from './cloud-client.js';
|
|
2
|
-
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, AccountLinkHandoff, AccountLinkCallback, AccountLoginChallenge, AccountLoginProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
|
|
2
|
+
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudAccountRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, AccountLinkHandoff, AccountLinkCallback, AccountLoginChallenge, AccountLoginProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
|
|
3
3
|
export { connectCloud } from './connect.js';
|
|
4
4
|
export type { ConnectCloudOptions, CloudConnection } from './connect.js';
|
|
5
5
|
export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
|
|
6
|
-
export type { RoutingCredential, RoutingChallenge, RoutingProofHeaders, } from './routing-proof.js';
|
|
6
|
+
export type { RoutingCredential, DiscoveryChallenge, RoutingChallenge, RoutingProofHeaders, } from './routing-proof.js';
|
|
@@ -61,6 +61,23 @@ export interface RoutingChallenge {
|
|
|
61
61
|
/** Epoch **milliseconds**, as MDMA reports it. Nonces live about two minutes. */
|
|
62
62
|
expiresAtMs: number;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A challenge bound to an ACCOUNT rather than a namespace, for asking which
|
|
66
|
+
* relays serve that account.
|
|
67
|
+
*
|
|
68
|
+
* Separate from {@link RoutingChallenge} because the binding is the security
|
|
69
|
+
* property, not a field name: MDMA seals the account into this one and the
|
|
70
|
+
* namespace into that one, and refuses each at the other's route. Keeping the
|
|
71
|
+
* TypeScript types distinct is what stops a caller passing the wrong one and
|
|
72
|
+
* learning about it as a 403.
|
|
73
|
+
*/
|
|
74
|
+
export interface DiscoveryChallenge {
|
|
75
|
+
accountId: string;
|
|
76
|
+
/** Opaque and sealed — the client neither parses nor constructs this. */
|
|
77
|
+
nonce: string;
|
|
78
|
+
/** Epoch **milliseconds**, as MDMA reports it. Nonces live about two minutes. */
|
|
79
|
+
expiresAtMs: number;
|
|
80
|
+
}
|
|
64
81
|
/**
|
|
65
82
|
* The three header values a proven routing read carries.
|
|
66
83
|
*
|
|
@@ -88,5 +105,9 @@ export declare function signRoutingChallenge(nonce: string, deviceSecret: string
|
|
|
88
105
|
* Separated from the request so a caller that fetches its own challenge — a
|
|
89
106
|
* test, or a client batching several reads against one nonce — can use the same
|
|
90
107
|
* encoding rather than a second spelling of it.
|
|
108
|
+
*
|
|
109
|
+
* Takes either challenge kind: only the nonce is signed and both are signed
|
|
110
|
+
* over the same domain, so one encoder serves both. What separates them is what
|
|
111
|
+
* MDMA sealed inside, which is checked server-side.
|
|
91
112
|
*/
|
|
92
|
-
export declare function routingProofHeaders(challenge: RoutingChallenge, credential: RoutingCredential): Promise<RoutingProofHeaders>;
|
|
113
|
+
export declare function routingProofHeaders(challenge: RoutingChallenge | DiscoveryChallenge, credential: RoutingCredential): Promise<RoutingProofHeaders>;
|
|
@@ -69,6 +69,10 @@ export async function signRoutingChallenge(nonce, deviceSecret) {
|
|
|
69
69
|
* Separated from the request so a caller that fetches its own challenge — a
|
|
70
70
|
* test, or a client batching several reads against one nonce — can use the same
|
|
71
71
|
* encoding rather than a second spelling of it.
|
|
72
|
+
*
|
|
73
|
+
* Takes either challenge kind: only the nonce is signed and both are signed
|
|
74
|
+
* over the same domain, so one encoder serves both. What separates them is what
|
|
75
|
+
* MDMA sealed inside, which is checked server-side.
|
|
72
76
|
*/
|
|
73
77
|
export async function routingProofHeaders(challenge, credential) {
|
|
74
78
|
return {
|