aboutsummaryrefslogtreecommitdiffstats
path: root/nwcpush/server.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 20:46:12 -0700
committerAdam Soltys <[email protected]>2026-08-01 20:46:12 -0700
commited1d996f9dbd6cedc18ec293daba0cfcd745ae2b (patch)
tree6d552796e899314d402dc92f6f80886405e296b5 /nwcpush/server.js
parent5986cbf5337593eb11fd984d284bd41da7719a87 (diff)
downloadcoinosv3-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/server.js')
-rw-r--r--nwcpush/server.js7
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];