morphit/apps/web/scripts/push-privacy-honesty-smoke.ts

114 lines
5.3 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Morphit — push-privacy honesty smoke (v1.7.7, t.txt #6).
*
* A privacy control that does nothing is worse than no control: it converts a
* cautious user's care into false confidence, on the one panel where that costs
* the most.
*
* WHAT HAPPENED. Settings offered "Self-hosted only" under "Push privacy level".
* The relay validated `privacy_mode` (api/push.ts), wrote it to the DB
* (policy/pushSubscriptions.ts) — and **nothing ever read it back**.
* `pushSender.ts` never looked at it; the indexer never saw it. So a
* privacy-focused user picked the private option and Chrome kept delivering
* through Google FCM exactly as before, while the FAQ told them "no Google, no
* Mozilla, no third parties ever see that you received a ping".
*
* It also CANNOT be made real under Web Push: `pushManager.subscribe()` returns
* an endpoint minted by the BROWSER's push service, and there is no API to
* redirect it. UnifiedPush is the only design where a user genuinely picks their
* own push server — a feature, not a radio button.
*
* Ken chose: remove the option, fix the FAQ.
*
* These checks pin the OUTCOME, so the option cannot creep back without the
* delivery path that would make it true:
* 1. the settings radio offers only what works
* 2. the client never claims a mode nothing implements
* 3. the FAQ does not make the promise again
* 4. IF `self_hosted` ever returns, `pushSender` must actually branch on it
*/
import { readFileSync } from 'node:fs';
import { resolve, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
const read = (p: string): string => readFileSync(join(ROOT, p), 'utf8');
/** Comments must never count as evidence — a guard that documents a bug must not
* thereby whitelist it (the lesson from public-doc-drift-smoke). */
const strip = (s: string): string =>
s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1').replace(/<!--[\s\S]*?-->/g, ' ');
let pass = 0;
let fail = 0;
const check = (name: string, ok: boolean, detail = ''): void => {
if (ok) { pass++; console.log(` \u2713 ${name}`); }
else { fail++; console.log(` \u2717 ${name}${detail ? `\n ${detail}` : ''}`); }
};
const settings = strip(read('apps/web/src/lib/components/NotificationSettings.svelte'));
const nudge = strip(read('apps/web/src/lib/components/ChatNotificationNudge.svelte'));
const prefs = strip(read('apps/web/src/lib/notifications/preferences.ts'));
const sender = strip(read('apps/relay/src/policy/pushSender.ts'));
const en = JSON.parse(read('apps/web/src/lib/i18n/locales/en.json')) as {
faq: { entries: Record<string, { a: string }> };
settings: { notifications: Record<string, string> };
};
// ── 1. the radio offers only what works ────────────────────────────
check(
'1 the push-privacy radio does NOT offer self_hosted',
!/'self_hosted'/.test(settings),
'the option must not exist while nothing downstream implements it'
);
check(
'2 …and still offers standard + off',
/'standard'/.test(settings) && /'off'/.test(settings)
);
check('3 the PushPrivacy type excludes self_hosted', !/'self_hosted'/.test(prefs));
// ── 2. the client never claims a mode nothing implements ───────────
check('4 settings subscribes as standard', !/self_hosted/.test(settings));
check('5 the chat nudge subscribes as standard', !/self_hosted/.test(nudge));
// ── 3. the FAQ does not make the promise again ─────────────────────
const faq = en.faq.entries['push_notifications_privacy']?.a ?? '';
check('6 the FAQ entry still exists', faq.length > 200);
check(
'7 the FAQ does NOT promise that no third party sees your pings',
!/no third parties ever see|no Google, no Mozilla/i.test(faq),
'this was false: privacy_mode was stored and never read'
);
check(
'8 the FAQ says plainly that the BROWSER picks the push service',
/browser picks that push service|browser picks/i.test(faq)
);
check(
'9 …and names the removal rather than quietly dropping it',
/did nothing|It did nothing/i.test(faq)
);
check(
'10 …and keeps the true part: content is e2e-encrypted, only metadata leaks',
/end-to-end encrypted/i.test(faq) && /metadata/i.test(faq)
);
check(
'11 …and gives the one honest mitigation (turn push off; badges stay local)',
/turn push off/i.test(faq) && /never leave your browser/i.test(faq)
);
check(
'12 the dead self-hosted label is gone from the locale',
en.settings.notifications['channel_push_privacy_self'] === undefined
);
// ── 4. the trap: self_hosted may only return WITH a delivery path ──
// This is the check that matters in a year. If someone re-adds the option
// because it "should" exist, this fails until pushSender actually honours it.
check(
'13 if self_hosted ever returns to the client, pushSender MUST branch on it',
!/'self_hosted'/.test(settings) || /privacy_mode|privacyMode/.test(sender),
'the option is only honest once delivery actually respects it — otherwise it is decoration'
);
console.log('');
if (fail === 0) console.log(`\u2713 all ${pass} push-privacy-honesty checks passed`);
else { console.error(`\u2717 ${fail} of ${pass + fail} push-privacy-honesty checks FAILED`); process.exit(1); }