From 5dbd42fa330da23924335d8287b01bd23b1fcff9 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 21:56:02 -0700 Subject: One wallet per Nostr identity, NIP-98 auth, npub payment addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency fix with teeth: logging in with a pasted key derived one wallet while an extension login for the SAME identity found no backup and generated a different one. Now every signer kind runs one flow — look for the published association first (that's where the money is), then derive (key in hand) or generate (signer only) — and a derived seed is published too. That costs nothing, since the derivation is public and anyone holding the key could compute it anyway, and it makes the identity open the same wallet however you log in. Registrar auth is now NIP-98 (HTTP Auth, kind 27235) instead of a homemade envelope: the signature binds the URL, method and body hash, so a captured header can't be replayed elsewhere. NIP-78 already covered the encrypted association; NIP-26 delegation is deprecated, so owner→manager nomination lives in the registrar's own model: an npub-shaped name may only be claimed by that identity, which nominates the wallet key to keep the record updated afterwards (the wallet key is the only one always available — offline, in the worker). Payment addresses therefore default to the user's REAL npub in all three login methods, falling back to the wallet's own identity when no Nostr account is linked. Co-Authored-By: Claude Fable 5 --- tools/nostr-login-test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'tools') diff --git a/tools/nostr-login-test.js b/tools/nostr-login-test.js index 039ff84..42580fe 100644 --- a/tools/nostr-login-test.js +++ b/tools/nostr-login-test.js @@ -56,5 +56,22 @@ let denied = false; try { await stranger.decryptSelf(ct); } catch { denied = true; } check('another account cannot decrypt it', denied); +console.log('\n[NIP-98 auth]'); +{ + const { nip98Header } = await import('../src/nostr-login.js'); + const { verifyEvent } = await import('nostr-tools/pure'); + const { sha256: sha } = await import('@noble/hashes/sha256'); + const body = JSON.stringify({ name: 'x' }); + const hdr = await nip98Header(keySigner(sk), 'https://names.coinos.io/register', 'POST', body); + check('header uses the Nostr scheme', hdr.startsWith('Nostr ')); + const evt = JSON.parse(Buffer.from(hdr.slice(6), 'base64').toString('utf8')); + check('is kind 27235', evt.kind === 27235); + check('signature verifies', verifyEvent(evt)); + const tag = (k) => (evt.tags.find((x) => x[0] === k) || [])[1]; + check('binds the url', tag('u') === 'https://names.coinos.io/register'); + check('binds the method', tag('method') === 'POST'); + check('binds the body hash', tag('payload') === hex.encode(sha(new TextEncoder().encode(body)))); +} + console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ nostr login behaves'); process.exit(fails ? 1 : 0); -- cgit v1.2.3