diff options
| author | Adam Soltys <[email protected]> | 2026-08-01 20:46:12 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-01 20:46:12 -0700 |
| commit | ed1d996f9dbd6cedc18ec293daba0cfcd745ae2b (patch) | |
| tree | 6d552796e899314d402dc92f6f80886405e296b5 /nwcpush | |
| parent | 5986cbf5337593eb11fd984d284bd41da7719a87 (diff) | |
| download | coinosv3-ed1d996f9dbd6cedc18ec293daba0cfcd745ae2b.tar.gz coinosv3-ed1d996f9dbd6cedc18ec293daba0cfcd745ae2b.zip | |
Zappable offer codes and per-name Lightning offers
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 <[email protected]>
Diffstat (limited to 'nwcpush')
| -rw-r--r-- | nwcpush/server.js | 7 |
1 files changed, 4 insertions, 3 deletions
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]; |
