diff --git a/README.md b/README.md index 851a6b6..10738de 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,19 @@ Use the signer command once at least one encrypted share is saved locally: Use either `igloo status` or `igloo share status` to decrypt a saved share, connect a temporary bifrost node, and ping each peer. The command prints relay endpoints plus a color-coded list of online/offline peers. Provide `--password` or `--password-file` for automation, and customise relays with `--relays` when needed. +### Echo debugging + +- `--debug-echo` — turn on verbose echo logs (both listener and sender). This sets `IGLOO_DEBUG_ECHO=1` for the current run. +- `IGLOO_TEST_RELAY` — optionally pin a specific relay for both desktop and CLI, e.g. + + ```bash + IGLOO_TEST_RELAY=wss://your-relay igloo share load --debug-echo + ``` + + Desktop and CLI must share at least one relay to exchange echo events. + +Behind the scenes, the CLI normalizes Nostr subscribe filters so relays that require a single filter object (instead of a one-element array) accept subscriptions reliably. + ## Development scripts - Node (default) diff --git a/llm/context/igloo-core-readme.md b/llm/context/igloo-core-readme.md index a1cfbcb..ed14d5e 100644 --- a/llm/context/igloo-core-readme.md +++ b/llm/context/igloo-core-readme.md @@ -321,6 +321,8 @@ with `data === 'echo'` and newer challenge-based requests where `data` is an even-length hex string. No changes are required on the sender side when upgrading. +Tip (CLI): when testing end-to-end with igloo-cli and igloo-desktop, you can enable detailed echo logs with the CLI flag `--debug-echo` (sets `IGLOO_DEBUG_ECHO=1` for that run). To guarantee relay overlap, set `IGLOO_TEST_RELAY=wss://your-relay` in both apps. + ```typescript import { awaitShareEcho } from '@frostr/igloo-core'; @@ -366,6 +368,8 @@ try { ``` `challenge` must be an even-length hexadecimal string (32 bytes / 64 hex characters recommended). + +Troubleshooting: some Nostr relays reject a single-element filter array during subscription. igloo-cli patches `nostr-tools`’s `SimplePool.subscribeMany` at runtime to unwrap `[[filter]]` → `filter`. If you are wiring listeners manually outside the CLI, apply the same normalization. #### `startListeningForAllEchoes(groupCredential, shareCredentials, callback, options?)` Starts listening for echo events on all shares in a keyset. diff --git a/src/cli.tsx b/src/cli.tsx index 8d4b744..72b1012 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,4 +1,5 @@ import './polyfills/websocket.js'; +import './polyfills/nostr.js'; import React from 'react'; import {render} from 'ink'; import {PassThrough} from 'node:stream'; @@ -114,6 +115,12 @@ function parseArgv(argv: string[]): ParsedArgs { delete flags.T; } + // Short alias: -E → --debug-echo + if (flags.E !== undefined && flags['debug-echo'] === undefined) { + flags['debug-echo'] = flags.E; + delete flags.E; + } + return { command: positionals[0] ?? 'intro', args: positionals.slice(1), @@ -123,6 +130,16 @@ function parseArgv(argv: string[]): ParsedArgs { }; } +function toBool(value: string | boolean | undefined): boolean { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') { + const v = value.trim().toLowerCase(); + if (['1', 'true', 'yes', 'on'].includes(v)) return true; + if (['0', 'false', 'no', 'off'].includes(v)) return false; + } + return false; +} + function showHelpScreen(version: string, opts?: any) { const instance = render(, opts); instance.waitUntilExit().then(() => process.exit(0)); @@ -137,6 +154,15 @@ const {command, args, flags, showHelp, showVersion: shouldShowVersion} = parseAr process.argv.slice(2) ); +// Allow --debug-echo to enable/disable echo diagnostics without env vars. +// This is read by echo send/listen utilities. +(() => { + const raw = (flags['debug-echo'] ?? (flags as any).debugEcho) as string | boolean | undefined; + if (raw !== undefined) { + process.env.IGLOO_DEBUG_ECHO = toBool(raw) ? '1' : '0'; + } +})(); + // In non-interactive environments (CI/tests), Ink raw mode can throw. // Allow tests to opt-out via IGLOO_DISABLE_RAW_MODE=1 let inkOptions: any | undefined; diff --git a/src/components/Help.tsx b/src/components/Help.tsx index ae5ba4b..d913cff 100644 --- a/src/components/Help.tsx +++ b/src/components/Help.tsx @@ -44,6 +44,7 @@ export function Help({version}: HelpProps) { --relays list Override relay list (comma-separated) --verbose Stream signer diagnostics --log-level level Signer log level (debug|info|warn|error) + --debug-echo Enable echo debug logs (relay + events) ); diff --git a/src/components/keyset/KeysetSigner.tsx b/src/components/keyset/KeysetSigner.tsx index d59e6d7..bb08ff8 100644 --- a/src/components/keyset/KeysetSigner.tsx +++ b/src/components/keyset/KeysetSigner.tsx @@ -17,7 +17,8 @@ import { PeerManager } from '@frostr/igloo-core'; import type {BifrostNode} from '@frostr/igloo-core'; -import {SimplePool} from 'nostr-tools'; +// Ensure nostr subscribe shim is loaded if this component is used in isolation. +import '../../polyfills/nostr.js'; import {Prompt} from '../ui/Prompt.js'; import { readShareFiles, @@ -799,49 +800,3 @@ export function KeysetSigner({args, flags}: KeysetSignerProps) { } export default KeysetSigner; -let simplePoolPatched = false; - -function ensureSimplePoolPatched() { - if (simplePoolPatched) { - return; - } - - const prototype = (SimplePool as any)?.prototype; - if (!prototype) { - simplePoolPatched = true; - return; - } - - if (prototype.__iglooFilterNormalizePatched) { - simplePoolPatched = true; - return; - } - - const originalSubscribeMany = prototype.subscribeMany; - if (typeof originalSubscribeMany !== 'function') { - simplePoolPatched = true; - return; - } - - prototype.subscribeMany = function patchedSubscribeMany(this: unknown, relays: unknown, filters: unknown, params: unknown) { - const normalizedFilters = - Array.isArray(filters) && - filters.length === 1 && - filters[0] !== null && - typeof filters[0] === 'object' && - !Array.isArray(filters[0]) - ? filters[0] - : filters; - - return originalSubscribeMany.call(this, relays, normalizedFilters, params); - }; - - Object.defineProperty(prototype, '__iglooFilterNormalizePatched', { - value: true, - enumerable: false - }); - - simplePoolPatched = true; -} - -ensureSimplePoolPatched(); diff --git a/src/components/keyset/useShareEchoListener.ts b/src/components/keyset/useShareEchoListener.ts index b3aaefc..831c3f2 100644 --- a/src/components/keyset/useShareEchoListener.ts +++ b/src/components/keyset/useShareEchoListener.ts @@ -158,6 +158,12 @@ export function useShareEchoListener( } catch {} }) : undefined; + if (debugEnabled) { + try { + // eslint-disable-next-line no-console + console.log('[echo-listen] INFO using relays', relays ?? 'default'); + } catch {} + } const result = await awaitShareEcho( groupCredential, shareCredential, diff --git a/src/polyfills/nostr.ts b/src/polyfills/nostr.ts new file mode 100644 index 0000000..3ec61d9 --- /dev/null +++ b/src/polyfills/nostr.ts @@ -0,0 +1,25 @@ +// Global runtime patch for nostr-tools SimplePool.subscribeMany +// Some relays reject a single-element array for filters (expecting an object). +// Normalize `[[filter]]` to `filter` to avoid "provided filter is not an object" errors. +// Idempotent and safe to import multiple times. +import {SimplePool} from 'nostr-tools'; + +try { + const proto = (SimplePool as any)?.prototype; + if (proto && !proto.__iglooFilterNormalizePatched) { + const original = proto.subscribeMany; + if (typeof original === 'function') { + proto.subscribeMany = function patchedSubscribeMany(this: unknown, relays: unknown, filters: unknown, params: unknown) { + const normalized = Array.isArray(filters) && filters.length === 1 && filters[0] && + typeof filters[0] === 'object' && !Array.isArray(filters[0]) + ? filters[0] + : filters; + return original.call(this, relays, normalized, params); + }; + Object.defineProperty(proto, '__iglooFilterNormalizePatched', {value: true}); + } + } +} catch { + // Best-effort only; if nostr-tools changes, we fail silently. +} +