@calimero-network/mero-js 19.17.0 → 19.19.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.
@@ -530,13 +530,24 @@ export declare class CloudClient {
530
530
  * Ask the fleet to host a claimed namespace — the step that gets a relay
531
531
  * assigned.
532
532
  *
533
- * Takes no body: {@link claimNamespace} already established ownership, and
534
- * this is the separate "now host it" half. Idempotent.
533
+ * Carries no proof: {@link claimNamespace} already established ownership,
534
+ * and this is the separate "now host it" half. Idempotent.
535
+ *
536
+ * `admitterAddrs` is where the owner's own node can be dialed — take it from
537
+ * that node's network status with {@link admitterAddrsFromNetworkStatus}.
538
+ * The first fleet node placed on a namespace has nobody but the owner to ask
539
+ * for admission, and without these it can only broadcast and hope the
540
+ * owner's node — often a laptop behind a relay — hears it. They are a dial
541
+ * hint, not a grant: the owner's node admits by its own policy. Omit them to
542
+ * leave whatever the cloud already holds; pass them again when the node's
543
+ * relay address changes, and they replace the stored ones.
535
544
  *
536
545
  * Not to be confused with {@link enableHA}, which opens the cloud's web flow
537
546
  * in a browser tab for a user to complete by hand. This is the API call.
538
547
  */
539
- enableNamespaceHa(namespaceId: string): Promise<unknown>;
548
+ enableNamespaceHa(namespaceId: string, options?: {
549
+ admitterAddrs?: readonly string[];
550
+ }): Promise<unknown>;
540
551
  /**
541
552
  * Stop hosting a claimed namespace.
542
553
  *
@@ -680,3 +691,15 @@ export declare class CloudClient {
680
691
  */
681
692
  private adoptRollingRefresh;
682
693
  }
694
+ /**
695
+ * The addresses a fleet node can dial to ask this node for admission, from the
696
+ * node's `GET /admin-api/network/status` (see `AdminApiClient`).
697
+ *
698
+ * Its `externalAddrs`, each made to end in `/p2p/<localPeerId>`: that is the
699
+ * shape core dials, and an external address is usually reported without it. A
700
+ * relay-circuit address already contains `/p2p/` — the relay's — so the test is
701
+ * whether it ends in this node's own id, not whether it names a peer at all.
702
+ * Anything that is not a multiaddr is dropped. Empty when the status names no
703
+ * peer id or no external address, which is an honest "not dialable yet".
704
+ */
705
+ export declare function admitterAddrsFromNetworkStatus(status: unknown): string[];
@@ -418,14 +418,26 @@ export class CloudClient {
418
418
  * Ask the fleet to host a claimed namespace — the step that gets a relay
419
419
  * assigned.
420
420
  *
421
- * Takes no body: {@link claimNamespace} already established ownership, and
422
- * this is the separate "now host it" half. Idempotent.
421
+ * Carries no proof: {@link claimNamespace} already established ownership,
422
+ * and this is the separate "now host it" half. Idempotent.
423
+ *
424
+ * `admitterAddrs` is where the owner's own node can be dialed — take it from
425
+ * that node's network status with {@link admitterAddrsFromNetworkStatus}.
426
+ * The first fleet node placed on a namespace has nobody but the owner to ask
427
+ * for admission, and without these it can only broadcast and hope the
428
+ * owner's node — often a laptop behind a relay — hears it. They are a dial
429
+ * hint, not a grant: the owner's node admits by its own policy. Omit them to
430
+ * leave whatever the cloud already holds; pass them again when the node's
431
+ * relay address changes, and they replace the stored ones.
423
432
  *
424
433
  * Not to be confused with {@link enableHA}, which opens the cloud's web flow
425
434
  * in a browser tab for a user to complete by hand. This is the API call.
426
435
  */
427
- async enableNamespaceHa(namespaceId) {
428
- return this.request('POST', `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/enable-ha`, { body: {} });
436
+ async enableNamespaceHa(namespaceId, options = {}) {
437
+ const body = options.admitterAddrs === undefined
438
+ ? {}
439
+ : { admitter_addrs: [...options.admitterAddrs] };
440
+ return this.request('POST', `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/enable-ha`, { body });
429
441
  }
430
442
  /**
431
443
  * Stop hosting a claimed namespace.
@@ -737,3 +749,38 @@ export class CloudClient {
737
749
  void this.notifySession(this.session);
738
750
  }
739
751
  }
752
+ /**
753
+ * The addresses a fleet node can dial to ask this node for admission, from the
754
+ * node's `GET /admin-api/network/status` (see `AdminApiClient`).
755
+ *
756
+ * Its `externalAddrs`, each made to end in `/p2p/<localPeerId>`: that is the
757
+ * shape core dials, and an external address is usually reported without it. A
758
+ * relay-circuit address already contains `/p2p/` — the relay's — so the test is
759
+ * whether it ends in this node's own id, not whether it names a peer at all.
760
+ * Anything that is not a multiaddr is dropped. Empty when the status names no
761
+ * peer id or no external address, which is an honest "not dialable yet".
762
+ */
763
+ export function admitterAddrsFromNetworkStatus(status) {
764
+ const record = status && typeof status === 'object'
765
+ ? (status.data ?? status)
766
+ : undefined;
767
+ if (!record || typeof record !== 'object')
768
+ return [];
769
+ const peerId = record.localPeerId;
770
+ const external = record.externalAddrs;
771
+ if (typeof peerId !== 'string' || !peerId || !Array.isArray(external))
772
+ return [];
773
+ const suffix = `/p2p/${peerId}`;
774
+ const out = [];
775
+ for (const entry of external) {
776
+ if (typeof entry !== 'string')
777
+ continue;
778
+ const addr = entry.trim();
779
+ if (!addr.startsWith('/'))
780
+ continue;
781
+ const dialable = addr.endsWith(suffix) ? addr : `${addr}${suffix}`;
782
+ if (!out.includes(dialable))
783
+ out.push(dialable);
784
+ }
785
+ return out;
786
+ }
@@ -1,4 +1,4 @@
1
- export { CloudClient } from './cloud-client.js';
1
+ export { CloudClient, admitterAddrsFromNetworkStatus } from './cloud-client.js';
2
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';
@@ -1,3 +1,3 @@
1
- export { CloudClient } from './cloud-client.js';
1
+ export { CloudClient, admitterAddrsFromNetworkStatus } from './cloud-client.js';
2
2
  export { connectCloud } from './connect.js';
3
3
  export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
@@ -30,10 +30,12 @@ export declare class SseClient {
30
30
  private subscribedGroupIds;
31
31
  private closed;
32
32
  private listeners;
33
+ private fetchImpl;
33
34
  constructor(opts: {
34
35
  baseUrl: string;
35
36
  getAuthToken: () => Promise<string>;
36
37
  reconnectDelayMs?: number;
38
+ fetch?: typeof fetch;
37
39
  });
38
40
  on(event: 'connect', handler: SseConnectHandler): void;
39
41
  on(event: 'event', handler: SseEventHandler): void;
@@ -44,6 +44,9 @@ export class SseClient {
44
44
  this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
45
45
  this.getAuthToken = opts.getAuthToken;
46
46
  this.reconnectDelayMs = opts.reconnectDelayMs ?? 3000;
47
+ // globalThis.fetch must be called as a method on globalThis, not through a
48
+ // variable, or browsers throw "Illegal invocation".
49
+ this.fetchImpl = (url, init) => opts.fetch ? opts.fetch(url, init) : globalThis.fetch(url, init);
47
50
  }
48
51
  on(event, handler) {
49
52
  const key = event;
@@ -122,7 +125,7 @@ export class SseClient {
122
125
  try {
123
126
  const token = await this.getAuthToken();
124
127
  const url = `${this.baseUrl}/sse`;
125
- const response = await fetch(url, {
128
+ const response = await this.fetchImpl(url, {
126
129
  headers: {
127
130
  'Authorization': `Bearer ${token}`,
128
131
  'Accept': 'text/event-stream',
@@ -300,7 +303,7 @@ export class SseClient {
300
303
  params.contextIds = ids.contextIds;
301
304
  if (ids.groupIds && ids.groupIds.length > 0)
302
305
  params.groupIds = ids.groupIds;
303
- const response = await fetch(`${this.baseUrl}/sse/subscription`, {
306
+ const response = await this.fetchImpl(`${this.baseUrl}/sse/subscription`, {
304
307
  method: 'POST',
305
308
  headers: {
306
309
  'Authorization': `Bearer ${token}`,
@@ -2,6 +2,7 @@ import { Transport, HttpClient } from './http-types.js';
2
2
  export declare function createHttpClient(transport: Transport): HttpClient;
3
3
  export declare function createBrowserHttpClient(options: {
4
4
  baseUrl: string;
5
+ fetch?: typeof fetch;
5
6
  getAuthToken?: () => Promise<string | undefined>;
6
7
  getProof?: Transport['getProof'];
7
8
  onTokenRefresh?: (newToken: string) => Promise<void>;
@@ -6,9 +6,9 @@ export function createHttpClient(transport) {
6
6
  // Factory function for browser environments
7
7
  export function createBrowserHttpClient(options) {
8
8
  const transport = {
9
- // Wrap fetch in arrow function to prevent "Illegal invocation" error
10
- // This preserves the correct 'this' context when fetch is called
11
- fetch: (url, init) => globalThis.fetch(url, init),
9
+ // globalThis.fetch must be called as a method on globalThis, not through a
10
+ // variable, or browsers throw "Illegal invocation".
11
+ fetch: (url, init) => options.fetch ? options.fetch(url, init) : globalThis.fetch(url, init),
12
12
  baseUrl: options.baseUrl,
13
13
  getAuthToken: options.getAuthToken,
14
14
  getProof: options.getProof,