fix(security,notifications): pre class allowlist, notification privacy + icon, sync-script safety (N100/N106/N109/N119)

- N100: restrict <pre> classes to language-* in sanitize-html allowedClasses;
  previously `class` was allowed on <pre> with no allowedClasses entry, so a
  remote sender could inject arbitrary class names that activate site CSS.
- N106: OS notifications for E2EE rooms no longer carry decrypted plaintext
  (which persists in the OS notification center / lock screen). Encrypted rooms
  show only the sender; the in-page toast still previews while focused.
- N109: OS notification icon/badge use the static app logo instead of an
  authenticated-media avatar URL the OS can't fetch (was 401 / no icon). The
  in-app toast keeps the real room avatar (it can fetch via the SW).
- N119: syncDecorations.mjs distinguishes a confirmed 404 (remove) from a
  network/5xx failure (abort) so a transient CDN outage can't silently wipe the
  whole decoration catalog from source control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:35:33 -04:00
parent 1c84556600
commit 51d468fbcc
3 changed files with 41 additions and 5 deletions
+23 -2
View File
@@ -56,7 +56,8 @@ async function headCheck(slug) {
const res = await fetch(`${CDN}/${slug}.png`, { method: 'HEAD' });
return { slug, ok: res.ok, status: res.status };
} catch {
return { slug, ok: false, status: 0 };
// Network/DNS/TLS failure — NOT a confirmation the file is gone.
return { slug, ok: false, status: 0, networkError: true };
}
}
@@ -68,7 +69,27 @@ for (let i = 0; i < slugMatches.length; i += BATCH) {
results.push(...batchResults);
}
const missing = results.filter((r) => !r.ok);
// Only a CONFIRMED HTTP 404 means the file is genuinely gone and safe to
// remove. A network error or any other non-ok status (5xx, 403, timeout) is
// ambiguous — the CDN may be unreachable — so refuse to remove anything and
// abort, otherwise a transient outage would wipe the whole catalog from source
// control (N119).
const transient = results.filter((r) => !r.ok && r.status !== 404);
if (transient.length > 0) {
console.error(
`Aborting: ${transient.length} decoration(s) returned a non-404 failure ` +
`(network error / server error). The CDN may be unreachable — refusing to ` +
`remove entries to avoid wiping the catalog.`,
);
transient
.slice(0, 8)
.forEach((r) =>
console.error(` ${r.slug}: ${r.networkError ? 'network error' : `HTTP ${r.status}`}`),
);
process.exit(1);
}
const missing = results.filter((r) => r.status === 404);
const found = results.filter((r) => r.ok);
if (missing.length === 0) {