diff --git a/.changeset/expose-operational-relay-urls.md b/.changeset/expose-operational-relay-urls.md new file mode 100644 index 0000000..602de0a --- /dev/null +++ b/.changeset/expose-operational-relay-urls.md @@ -0,0 +1,5 @@ +--- +'@contextvm/sdk': patch +--- + +`NostrClientTransport` now exposes `getOperationalRelayUrls()`, returning the current handler's relay URLs: configured URLs before `start()`, and the final set after relay resolution (server-identity hints, kind-10002 discovery, fallback probe) once `start()` resolves. Callers can read the resolved set once and persist it, so future sessions construct a plain configured client without re-paying discovery. diff --git a/docs/contextvm-docs b/docs/contextvm-docs index 20d01e2..a268677 160000 --- a/docs/contextvm-docs +++ b/docs/contextvm-docs @@ -1 +1 @@ -Subproject commit 20d01e2cb5cef568fb0fc213842f3c57ba9d2c0f +Subproject commit a2686779dec6db135edb3280a88f65163a2d3bb4 diff --git a/src/transport/nostr-client-transport.test.ts b/src/transport/nostr-client-transport.test.ts index 79dbfa3..8153738 100644 --- a/src/transport/nostr-client-transport.test.ts +++ b/src/transport/nostr-client-transport.test.ts @@ -1245,6 +1245,47 @@ describe('NostrClientTransport instance shape', () => { ]); }); + test('getOperationalRelayUrls returns configured URLs before start and resolved hints after', async () => { + const configuredUrl = 'wss://configured.example.com'; + const relayHintUrl = 'wss://relay.example.com'; + const transport = new NostrClientTransport({ + serverPubkey: nip19.nprofileEncode({ + pubkey: 'b'.repeat(64), + relays: [relayHintUrl], + }), + signer: new PrivateKeySigner('a'.repeat(64)), + relayHandler: [configuredUrl], + }); + + expect(transport.getOperationalRelayUrls()).toEqual([configuredUrl]); + + await transport['resolveOperationalRelayHandler'](); + + // Configured URLs win: resolution must not swap a configured handler. + expect(transport.getOperationalRelayUrls()).toEqual([configuredUrl]); + }); + + test('getOperationalRelayUrls returns resolved URLs after resolution swaps the handler', async () => { + const relayHintUrl = 'wss://relay.example.com'; + const transport = new NostrClientTransport({ + serverPubkey: nip19.nprofileEncode({ + pubkey: 'b'.repeat(64), + relays: [relayHintUrl], + }), + signer: new PrivateKeySigner('a'.repeat(64)), + relayHandler: [], + }); + + expect(transport.getOperationalRelayUrls()).toEqual([]); + + await transport['resolveOperationalRelayHandler'](); + + expect(transport.getOperationalRelayUrls()).toEqual([relayHintUrl]); + // Second resolution is a no-op: the set is stable after the first one. + await transport['resolveOperationalRelayHandler'](); + expect(transport.getOperationalRelayUrls()).toEqual([relayHintUrl]); + }); + test('uses bootstrap discovery relays by default when none are provided', () => { const transport = new NostrClientTransport({ serverPubkey: 'b'.repeat(64), diff --git a/src/transport/nostr-client-transport.ts b/src/transport/nostr-client-transport.ts index 39492a0..159429b 100644 --- a/src/transport/nostr-client-transport.ts +++ b/src/transport/nostr-client-transport.ts @@ -559,6 +559,17 @@ export class NostrClientTransport await this.inboundCoordinator.processIncomingEvent(event); } + /** + * Gets the transport's current operational relay URLs. + * + * Returns configured URLs before `start()`; once `start()` resolves, the + * final set after relay resolution (hints, kind-10002 discovery, fallback + * probe) — stable for the transport's lifetime. + */ + public getOperationalRelayUrls(): string[] { + return this.relayHandler.getRelayUrls?.() ?? []; + } + /** * Gets the server's initialize event if received. * @returns The server initialize event or undefined