diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 22:28:53 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 22:28:53 -0700 |
| commit | 75ec27747ec05da8990208d67052ad11d25f3c44 (patch) | |
| tree | 0d404cd0e8c9ecce57ba3c55359e17efa0d4b799 /src/nostr-login.js | |
| parent | 1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3 (diff) | |
| download | coinosv3-75ec27747ec05da8990208d67052ad11d25f3c44.tar.gz coinosv3-75ec27747ec05da8990208d67052ad11d25f3c44.zip | |
Speak as the account you logged in with, or not at all
Logging in with a remote signer and reloading left the app showing your
avatar and profile while signing everything with the wallet's own key —
so chat messages arrived under a different npub with a stranger's punk
on them. identity() had a fallback: when the login signer wasn't
available it quietly used the wallet key instead. That fallback is gone.
If you logged in as a nostr account, the app speaks as that account or
it refuses and says so.
Refusing is only reasonable if the signer usually comes back, so a login
now persists what it takes to be that identity again. A remote signer
stores its NIP-46 client key and pointer — the signer authorised that
client key, not one page load, so rebuilding from it resumes the session
without prompting. A pasted key stores the key, which is already the
seed of everything here since the wallet's mnemonic derives from it.
Resume is single-flight with a pause after failure, because identity()
is asked on every send and a signer that isn't answering must not mean a
relay connection per keystroke.
Existing remote-signer logins predate the stored session, so they need
one more sign-in to become resumable.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src/nostr-login.js')
| -rw-r--r-- | src/nostr-login.js | 31 |
1 files changed, 28 insertions, 3 deletions
diff --git a/src/nostr-login.js b/src/nostr-login.js index 8a4603f..ec18942 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -102,12 +102,37 @@ export async function bunkerSigner(uri, { onAuth } = {}) { }); await signer.connect(); const pubkey = await signer.getPublicKey(); - return bunkerAdapter(signer, pubkey); + return bunkerAdapter(signer, pubkey, local); } -function bunkerAdapter(signer, pubkey) { +// Reconnect a remote signer a page reload dropped. The remote end granted +// permission to this client key, not to this page load, so rebuilding from +// the same client key and pointer picks the session back up — usually without +// prompting the user again. +export async function resumeBunker(session, { onAuth, timeoutMs = 8000 } = {}) { + if (!session || !session.local || !session.bp || !session.bp.pubkey) return null; + const local = hex.decode(session.local); + const signer = BunkerSigner.fromBunker(local, session.bp, { + onauth: (url) => { if (onAuth) onAuth(url); }, + }); + const give = new Promise((_, rej) => setTimeout(() => rej(new Error('signer did not answer')), timeoutMs)); + try { + await Promise.race([signer.connect(), give]); + const pubkey = await Promise.race([signer.getPublicKey(), give]); + return bunkerAdapter(signer, pubkey, local); + } catch (e) { + try { signer.close(); } catch {} + throw e; + } +} + +function bunkerAdapter(signer, pubkey, local) { return { kind: 'bunker', pubkey, label: 'remote signer', + // Everything needed to rebuild this connection later. `local` is the + // client key the remote signer authorised — not the user's identity key, + // which never leaves the signer. + session: local ? { local: hex.encode(local), bp: signer.bp } : null, signEvent: (e) => signer.signEvent(e), encryptSelf: (txt) => signer.nip44Encrypt(pubkey, txt), decryptSelf: (ct) => signer.nip44Decrypt(pubkey, ct), @@ -138,7 +163,7 @@ export function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://nos.lol const ready = (async () => { const signer = await BunkerSigner.fromURI(local, uri, {}, controller.signal); const pubkey = await signer.getPublicKey(); - return bunkerAdapter(signer, pubkey); + return bunkerAdapter(signer, pubkey, local); })(); ready.finally(() => clearTimeout(timer)).catch(() => {}); return { uri, ready, cancel: (reason) => controller.abort(reason || new Error('cancelled')) }; |
