diff options
| author | Adam Soltys <[email protected]> | 2026-09-05 21:38:09 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-05 21:38:09 -0700 |
| commit | ec1d6b8c6da0f9e4e169f4e8f95b88133207b741 (patch) | |
| tree | 9e733166d98b7f1d64401e03fc4130629f9027d7 | |
| parent | 9ee75115c3cabee661866a8ade836cc6d9e519ac (diff) | |
| download | coinosv3-ec1d6b8c6da0f9e4e169f4e8f95b88133207b741.tar.gz coinosv3-ec1d6b8c6da0f9e4e169f4e8f95b88133207b741.zip | |
Payment push notifications are scoped to their network, so a mainnet payment stops opening the staging PWA. The same seed derives the same nostr pubkey on mainnet and staging, so both PWAs registered push under one pubkey and the notifier — matching only by pubkey — fanned every mainnet payment out to the staging subscription too; tapping it opened staging.coinos.io. Now the client stamps its network on the push registration (pushWatch net: getNetwork()), the registrar declares its own network on the /notify call (CFG.forwarder.network), and nwcpush delivers a payment only to registrations whose net matches — with a rollout-safe fallback: a device that hasn't re-registered yet (net=null) still gets the push, and a caller that omits net still reaches everyone, so nobody goes dark mid-deploy. Deployed names + nwcpush on cs
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
| -rw-r--r-- | names/server.js | 4 | ||||
| -rw-r--r-- | nwcpush/server.js | 15 | ||||
| -rw-r--r-- | src/features/messages.js | 7 |
3 files changed, 23 insertions, 3 deletions
diff --git a/names/server.js b/names/server.js index 6588108..971108c 100644 --- a/names/server.js +++ b/names/server.js @@ -520,7 +520,9 @@ async function settleLoop() { if (CFG.push && CFG.push.url && state.names[name]) { fetch(`${CFG.push.url}/notify`, { method: 'POST', headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ token: CFG.push.token, pubkey: state.names[name].pubkey, amountSat: sat }), + // this registrar's own network, so the notifier delivers only to the + // payee's devices on it (a seed makes the same pubkey on both nets) + body: JSON.stringify({ token: CFG.push.token, pubkey: state.names[name].pubkey, amountSat: sat, net: CFG.forwarder?.network || 'mainnet' }), }).catch(() => {}); } // ...and POST to their webhook, if they registered one diff --git a/nwcpush/server.js b/nwcpush/server.js index 7efebb1..61cab60 100644 --- a/nwcpush/server.js +++ b/nwcpush/server.js @@ -331,9 +331,18 @@ const server = Bun.serve({ if (!CFG.notifyToken || body?.token !== CFG.notifyToken) return json({ error: 'forbidden' }, 403); const pk = body?.pubkey; if (!/^[0-9a-f]{64}$/.test(pk || '')) return json({ error: 'pubkey required' }, 400); + // Network scoping: the same pubkey exists on mainnet and staging, so a + // payment goes only to devices on its own network. A device that hasn't + // re-registered since this shipped has net=null and still gets it (so + // no one goes dark mid-rollout); once both sides send a net, mismatches + // are filtered. A caller that omits net pushes to everyone (legacy). + const wantNet = typeof body.net === 'string' ? body.net : null; let n = 0; for (const [id, r] of Object.entries(notifyRegs)) { - if ((r.ptags || []).includes(pk)) { pushNotify(id, r, 'payment', { amountSat: Number(body.amountSat) || undefined }).catch(() => {}); n++; } + if (!(r.ptags || []).includes(pk)) continue; + if (wantNet && r.net && r.net !== wantNet) continue; + pushNotify(id, r, 'payment', { amountSat: Number(body.amountSat) || undefined }).catch(() => {}); + n++; } return json({ ok: true, devices: n }); } @@ -367,6 +376,10 @@ const server = Bun.serve({ ptags: hex(body.notify.ptags, 8), authors: hex(body.notify.authors, 64), reasons, + // the network this device is on: the same seed yields the same + // pubkey on mainnet and staging, so a payment push is delivered + // only to devices on the payment's network (see /notify) + net: typeof body.notify.net === 'string' ? body.notify.net.slice(0, 16) : null, }; persist(); resubscribeNotify(); diff --git a/src/features/messages.js b/src/features/messages.js index a370097..582bf3b 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -999,7 +999,12 @@ export function messagesFeature(ctx) { // never sends what the user turned off (a suppressed-but-delivered push // would earn Chrome's generic "updated in background" nag instead) const reasons = { payment: s.reasons?.payment !== false, dm: s.reasons?.dm !== false }; - return { ptags: myPubkeys(), authors, reasons }; + // Stamp the network: the same seed makes the same nostr pubkey on mainnet + // AND staging, so without this the notifier fans a mainnet payment push + // out to the staging PWA's subscription too — and tapping it opened + // staging. The notifier now delivers a payment only to registrations on + // the payment's own network. + return { ptags: myPubkeys(), authors, reasons, net: getNetwork() }; } const roomNotify = (cid) => !!st().notify[cid]; |
