From ed1d996f9dbd6cedc18ec293daba0cfcd745ae2b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 20:46:12 -0700 Subject: Zappable offer codes and per-name Lightning offers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLINK offers (noffer): every wallet gets a static offer keypair (synced across devices, first-minted wins) answering kind-21001 invoice requests — from open tabs and from the service worker while closed, which mints against the mirrored state (the record now carries chain-4 and chain-5 key windows) and raises an open-to-receive notification. Zap requests ride along and their kind-9735 receipts publish once the payment is claimed. The code is shown in the payment-address card for pasting into a Nostr profile; the notifier watches and relays kind 21001 alongside NWC. Payment names also gain a Lightning offer: the registrar creates a BOLT 12 offer per name on the node and appends lno= to the DNS record; a settle-forwarder pushes each received payment on to the name's ark destination from a small float wallet (custody window: seconds between Lightning settle and the ark send). Name resolution now runs full client-side DNSSEC validation (src/dnssec.js): binary DoH, root-anchor chain walk, RSA + ECDSA via WebCrypto — the resolver is untrusted transport, verified against live signed zones including the canonical BIP-353 test name. Co-Authored-By: Claude Fable 5 --- nwcpush/server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'nwcpush/server.js') diff --git a/nwcpush/server.js b/nwcpush/server.js index 951869c..ededa8e 100644 --- a/nwcpush/server.js +++ b/nwcpush/server.js @@ -45,6 +45,7 @@ const STATE = CFG.stateFile || join(import.meta.dir, 'data', 'registrations.json const MAX_PK = CFG.maxPubkeysPerDevice || 20; const STALE_MS = (CFG.staleDays || 30) * 86400_000; const REQ_KIND = 23194; +const OFFER_KIND = 21001; webpush.setVapidDetails(CFG.vapid.subject, CFG.vapid.publicKey, CFG.vapid.privateKey); const log = (...a) => console.log(new Date().toISOString(), ...a); @@ -95,7 +96,7 @@ function resubscribe() { // only live traffic: a replayed old request must not wake devices. // NOTE: nostr-tools ≥2.23 wants a single filter object, NOT an array — // an array serializes as an invalid REQ that relays reject with CLOSED. - { kinds: [REQ_KIND], '#p': pks, since: Math.floor(Date.now() / 1000) }, + { kinds: [REQ_KIND, OFFER_KIND], '#p': pks, since: Math.floor(Date.now() / 1000) }, { onevent: (ev) => { const target = ev.tags?.find((t) => t[0] === 'p')?.[1]; @@ -109,7 +110,7 @@ function resubscribe() { try { answeredSub?.close(); } catch {} answeredSub = pool.subscribeMany( RELAYS, - { kinds: [RES_KIND], since: Math.floor(Date.now() / 1000) }, + { kinds: [RES_KIND, OFFER_KIND], since: Math.floor(Date.now() / 1000) }, { onevent: (ev) => { for (const t of ev.tags || []) { @@ -217,7 +218,7 @@ const server = Bun.serve({ if (!rateOk(ip)) return json({ error: 'rate limited' }, 429); const body = await req.json().catch(() => null); const ev = body?.event; - if (!ev || ev.kind !== RES_KIND) return json({ error: 'kind 23195 events only' }, 400); + if (!ev || ![RES_KIND, OFFER_KIND].includes(ev.kind)) return json({ error: 'kind 23195/21001 events only' }, 400); if (JSON.stringify(ev).length > 4096) return json({ error: 'event too large' }, 400); if (!verifyEvent(ev)) return json({ error: 'bad signature' }, 400); const eTag = (ev.tags || []).find((t) => t[0] === 'e')?.[1]; -- cgit v1.2.3