diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 23:19:23 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 23:19:23 -0700 |
| commit | 70b02f00c63ba6f6cae939a5ebdedac6e3552eb6 (patch) | |
| tree | 2f085ab0bd5e9a9ae8b2e5044e75602f7aec9a83 /src/nostr-login.js | |
| parent | 64823b30c167d8dd0ec8747bb4b7aa5d975e98a3 (diff) | |
| download | coinosv3-70b02f00c63ba6f6cae939a5ebdedac6e3552eb6.tar.gz coinosv3-70b02f00c63ba6f6cae939a5ebdedac6e3552eb6.zip | |
Nostr login: connect a signer app by deep link or QR (nostrconnect://)
The client-initiated NIP-46 flow: coinos mints a nostrconnect:// URI —
rendered as a tappable deep link (launches Amber & friends directly, no
bunker URL to copy) and a QR for cross-device scanning — then waits on
the relay; when the signer app answers with the secret, login continues
by itself. One live attempt at a time, cancellable, three-minute
timeout, and kind 24133 joins the relay whitelist so our own relay
carries the handshake. The bunker:// paste path stays for signers that
prefer it.
Co-Authored-By: Claude Fable 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 | 33 |
1 files changed, 32 insertions, 1 deletions
diff --git a/src/nostr-login.js b/src/nostr-login.js index 077d9a6..b142dd7 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -38,7 +38,7 @@ import { entropyToMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { getPublicKey, finalizeEvent, nip44 } from './nostr.js'; import { SimplePool } from 'nostr-tools/pool'; -import { BunkerSigner, parseBunkerInput } from 'nostr-tools/nip46'; +import { BunkerSigner, parseBunkerInput, createNostrConnectURI } from 'nostr-tools/nip46'; import { generateSecretKey } from 'nostr-tools/pure'; const SEED_DTAG = 'coinos:wallet:v1'; @@ -102,6 +102,10 @@ export async function bunkerSigner(uri, { onAuth } = {}) { }); await signer.connect(); const pubkey = await signer.getPublicKey(); + return bunkerAdapter(signer, pubkey); +} + +function bunkerAdapter(signer, pubkey) { return { kind: 'bunker', pubkey, label: 'remote signer', signEvent: (e) => signer.signEvent(e), @@ -113,6 +117,33 @@ export async function bunkerSigner(uri, { onAuth } = {}) { }; } +// Client-initiated NIP-46: WE mint a nostrconnect:// URI; a signer app +// (Amber, nsec.app, …) opens or scans it and connects back to us over the +// relay — no bunker URL to copy. Returns the URI (render it as a deep link +// and a QR), a promise that resolves to a signer adapter when the app +// answers, and a cancel. +export function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://nos.lol'], timeoutMs = 180_000 } = {}) { + const local = generateSecretKey(); + const secret = hex.encode(crypto.getRandomValues(new Uint8Array(16))); + const uri = createNostrConnectURI({ + clientPubkey: getPublicKey(local), + relays, + secret, + name: 'coinos', + url: 'https://v3.coinos.io', + perms: ['get_public_key', 'sign_event', 'nip44_encrypt', 'nip44_decrypt'], + }); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(new Error('signer connect timed out')), timeoutMs); + const ready = (async () => { + const signer = await BunkerSigner.fromURI(local, uri, {}, controller.signal); + const pubkey = await signer.getPublicKey(); + return bunkerAdapter(signer, pubkey); + })(); + ready.finally(() => clearTimeout(timer)).catch(() => {}); + return { uri, ready, cancel: (reason) => controller.abort(reason || new Error('cancelled')) }; +} + // A pasted key: signing happens locally, and the key is never persisted. export function keySigner(sk) { const pubkey = getPublicKey(sk); |
