diff options
| author | Adam Soltys <[email protected]> | 2026-09-14 18:00:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-14 18:00:19 -0700 |
| commit | cb4e155ae6fb72282d5f186389d2ecca6db6a0f0 (patch) | |
| tree | 4d00f5696f377ba73a4d76244a86f7fb5f56235e | |
| parent | 082fc2e4005387697f77e485ae1c783ed3203e91 (diff) | |
| download | coinosv3-cb4e155ae6fb72282d5f186389d2ecca6db6a0f0.tar.gz coinosv3-cb4e155ae6fb72282d5f186389d2ecca6db6a0f0.zip | |
A remote signer that drops its socket comes back by itself.
"My signer keeps disconnecting": a phone freezing the tab cut the relay
socket the bunker session talks through, every request after that hung
to its deadline, and the app declared the signer gone — a Reconnect
button, tapped over and over. Three changes: the bunker client now talks
through a pool that pings and reconnects on its own, so a dropped socket
is re-opened and the subscription re-fired without anyone noticing; a
request that does fail on a dead connection drops the session and dials
again silently, so the notice only appears if that resume fails; and
coming back from the background probes the signer with a quick question,
replacing a dead session before the next real request runs into it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
| -rw-r--r-- | src/features/nostrlogin.js | 23 | ||||
| -rw-r--r-- | src/nostr-login.js | 18 |
2 files changed, 38 insertions, 3 deletions
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index 4d3fe50..503384f 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -67,7 +67,13 @@ export function nostrLoginFeature(ctx) { if (!signer || signer.kind !== 'bunker') return signer; const guard = (fn) => async (...args) => { try { return await fn(...args); } catch (e) { - if (live === wrapped && SIGNER_DEAD.test(e?.message || '')) live = null; + if (live === wrapped && SIGNER_DEAD.test(e?.message || '')) { + // drop it AND dial again, silently: the notice (and its Reconnect + // button) is for a resume that failed, not for every hiccup + live = null; + try { signer.close && signer.close(); } catch {} + resumeQuietly(); + } throw e; } }; @@ -701,6 +707,21 @@ export function nostrLoginFeature(ctx) { // null for signers we cannot silently reattach (pasted keys, bunkers // without a stored session). nostrLoginResume() { return resumeLogin(); }, + // Back from the background: a remote signer's socket may be gone even if + // the pool will bring it back — ask it something small now, and if the + // answer never comes, resume a fresh session before anyone notices. + resumed(awayMs) { + const s = liveSigner(); + if (!s || s.kind !== 'bunker' || !s.ping) return; + if (awayMs && awayMs < 5_000) return; + s.ping().catch(() => { + if (live !== s) return; + live = null; + try { s.close && s.close(); } catch {} + resumeQuietly(); + }); + return true; + }, unlockExtra() { return unlockExtra(); }, // The welcome screen's sign-in block: Google and passkey act right there; // the Nostr button steps into the wizard's signer list. diff --git a/src/nostr-login.js b/src/nostr-login.js index 56eb195..6666ed7 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -106,6 +106,14 @@ export async function extensionSigner() { }; } +// The relay pool a remote signer talks through. nostr-tools' own default +// never reconnects: a phone freezing the tab for a minute cut the socket, +// every request after that hung to its deadline, and the app declared the +// signer gone — "my signer keeps disconnecting", reconnect tapped over and +// over. Pinged and reconnecting, the socket comes back on its own and the +// subscription is re-fired, so the next request simply gets its answer. +const bunkerPool = () => new SimplePool({ enablePing: true, enableReconnect: true }); + export async function bunkerSigner(uri, { onAuth } = {}) { const { BunkerSigner, parseBunkerInput } = await loadNip46(); const bp = await parseBunkerInput(String(uri || '').trim()); @@ -115,6 +123,7 @@ export async function bunkerSigner(uri, { onAuth } = {}) { // NB the pointer goes through fromBunker — the constructor takes params // only, and passing the pointer there leaves the signer without one. const signer = BunkerSigner.fromBunker(local, bp, { + pool: bunkerPool(), onauth: (url) => { if (onAuth) onAuth(url); else if (typeof window !== 'undefined') window.open(url, '_blank'); }, }); await signer.connect(); @@ -131,6 +140,7 @@ export async function resumeBunker(session, { onAuth, timeoutMs = 15000 } = {}) const { BunkerSigner } = await loadNip46(); const local = hex.decode(session.local); const signer = BunkerSigner.fromBunker(local, session.bp, { + pool: bunkerPool(), onauth: (url) => { if (onAuth) onAuth(url); }, }); const give = new Promise((_, rej) => setTimeout(() => rej(new Error('signer did not answer')), timeoutMs)); @@ -163,6 +173,10 @@ function bunkerAdapter(signer, pubkey, local) { decryptSelf: (ct) => answered(signer.nip44Decrypt(pubkey, ct)), encryptTo: (peer, txt) => answered(signer.nip44Encrypt(peer, txt)), decryptFrom: (peer, ct) => answered(signer.nip44Decrypt(peer, ct)), + // Is anyone there? A quick round trip after the app comes back from the + // background, so a dead session is replaced before the user's next + // action runs into a twenty-second silence. + ping: () => answered(signer.getPublicKey(), 7_000), close: () => { try { signer.close(); } catch {} }, }; } @@ -217,7 +231,7 @@ export async function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://n Math.max(5_000, timeoutMs - (Date.now() - startedAt))); // Path 1: the library's live wait — instant while the tab stays awake. - const live = (async () => BunkerSigner.fromURI(local, uri, {}, controller.signal))(); + const live = (async () => BunkerSigner.fromURI(local, uri, { pool: bunkerPool() }, controller.signal))(); live.catch(() => {}); // Path 2: the replay poller — finds an approval published while we slept. @@ -252,7 +266,7 @@ export async function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://n const resp = JSON.parse(nip44.decrypt(ev.content, key)); // some signers echo the secret, others answer a bare "ack" if (resp.result === secret || resp.result === 'ack') { - return BunkerSigner.fromBunker(local, { pubkey: ev.pubkey, relays, secret }); + return BunkerSigner.fromBunker(local, { pubkey: ev.pubkey, relays, secret }, { pool: bunkerPool() }); } } catch {} } |
