aboutsummaryrefslogtreecommitdiffstats
path: root/src/nostr-login.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-02 23:19:23 -0700
committerAdam Soltys <[email protected]>2026-08-02 23:19:23 -0700
commit70b02f00c63ba6f6cae939a5ebdedac6e3552eb6 (patch)
tree2f085ab0bd5e9a9ae8b2e5044e75602f7aec9a83 /src/nostr-login.js
parent64823b30c167d8dd0ec8747bb4b7aa5d975e98a3 (diff)
downloadcoinosv3-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.js33
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);