aboutsummaryrefslogtreecommitdiffstats
path: root/nwcpush
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 22:07:13 -0700
committerAdam Soltys <[email protected]>2026-08-03 22:07:13 -0700
commit1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3 (patch)
tree04bb4bae281d9255ca45f9100c6903191b5c7b74 /nwcpush
parentdc8f4b841ff57f4ef74597c2f80b6f90b352db13 (diff)
downloadcoinosv3-1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3.tar.gz
coinosv3-1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3.zip
Only people you know get to buzz your phone
A NIP-17 wrap is signed by a throwaway key with the sender sealed under two layers of nip44, so no server can filter DMs by who sent them. The notifier now ships the wrap inside the push payload and the service worker opens it locally: a message from someone you follow arrives with their name on it, a stranger's is dropped, and your own sent-copy — every DM is wrapped to the sender too — no longer notifies you about what you just typed. That needs the nostr identity key in the worker's IndexedDB, which dm-inbox.js states plainly: for a seed-derived identity it is a second copy of a key localStorage's seed already yields, and it exists only to keep strangers out. A remote signer never hands its key over, so those wallets store no secret and keep getting unfiltered notifications rather than silence. Silence is always the riskier failure here, so the rule leans the other way whenever it isn't sure: a wrap we can't open notifies, an account with no contact list published notifies (otherwise a new user's welcome message would vanish), and anyone you already have a thread with counts as known whether or not you follow them. The server-side DM cooldown drops to a second — with the device discarding pushes, a throttle there would let a discarded one swallow the message behind it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'nwcpush')
-rw-r--r--nwcpush/server.js24
1 files changed, 21 insertions, 3 deletions
diff --git a/nwcpush/server.js b/nwcpush/server.js
index 5095151..6300732 100644
--- a/nwcpush/server.js
+++ b/nwcpush/server.js
@@ -119,7 +119,10 @@ function resubscribeNotify() {
}
// throttle: chat bursts collapse to one push per device per window
-const COOLDOWN = { payment: 10_000, dm: 10_000, chat: 90_000 };
+// DMs are throttled barely at all now: the device drops the ones it decides
+// are noise (a stranger, or our own sent-copy), and a server-side cooldown
+// would let a discarded push swallow the friend's message that followed it.
+const COOLDOWN = { payment: 10_000, dm: 1_000, chat: 90_000 };
const lastPush = new Map(); // endpointId:reason -> ts
const seenNotifyEvents = new Map(); // event id -> ts
function onNotifyEvent(ev) {
@@ -132,11 +135,17 @@ function onNotifyEvent(ev) {
const p = ev.tags?.find((t) => t[0] === 'p')?.[1];
const reason = ev.kind === 9735 || ev.kind === 9737 ? 'payment'
: p && notifyPtags().includes(p) ? 'dm' : 'chat';
+ // A wrapped DM hides its sender from us by design, so we can't decide here
+ // whether it's from a friend or a stranger — we hand the whole wrap to the
+ // device, which holds the only key that can open it. Web Push tops out
+ // around 4KB after encryption; anything larger goes as a bare nudge and the
+ // device shows its generic notification.
+ const extra = reason === 'dm' && JSON.stringify(ev).length < 3500 ? { wrap: ev } : {};
for (const [id, r] of Object.entries(notifyRegs)) {
const hit = reason === 'chat'
? (r.authors || []).includes(ev.pubkey)
: p && (r.ptags || []).includes(p);
- if (hit) pushNotify(id, r, reason, {}).catch(() => {});
+ if (hit) pushNotify(id, r, reason, extra).catch(() => {});
}
}
@@ -147,7 +156,16 @@ async function pushNotify(id, r, reason, extra) {
lastPush.set(key, Date.now());
const payload = JSON.stringify({ type: 'notify', reason, ...extra });
try {
- await webpush.sendNotification(r.sub, payload, { TTL: 3600, urgency: 'normal' });
+ try {
+ await webpush.sendNotification(r.sub, payload, { TTL: 3600, urgency: 'normal' });
+ } catch (e) {
+ // A payload the push service won't take (413, or the library refusing
+ // the size) must not cost the notification itself — send the bare nudge
+ // and let the device show its generic message.
+ if (!extra.wrap || (e.statusCode && e.statusCode !== 413)) throw e;
+ log(`notify(dm): payload rejected (${e.statusCode || e.message}), retrying bare`);
+ await webpush.sendNotification(r.sub, JSON.stringify({ type: 'notify', reason }), { TTL: 3600, urgency: 'normal' });
+ }
log(`notify(${reason}) -> device ${id}`);
} catch (e) {
if (e.statusCode === 404 || e.statusCode === 410) {