fix(review): CSP nonce for FOUC script + feed_delta truncation signal

Two items surfaced by the branch re-review:

CSP regression (blocking): script-src 'self' blocked the template-authored
anti-FOUC theme script in app.html — SvelteKit's mode:'auto' only hashes
scripts it injects. Added nonce="%sveltekit.nonce%" so it's substituted per
request and included in the CSP (survives future edits, unlike a pinned hash).
Verified: emitted CSP nonce matches the script tag; no violation, no flash.

feed_delta silent truncation: the LIMIT 200 (added earlier to kill the
stale-`since` DoS) returned only the newest slice with no signal, so a client
that missed 200+ uploads during a reconnect could not tell it should full-
refresh — the older missed uploads were dropped and unrecoverable (the next
delta advances `since` past them). DeltaResponse now carries `truncated`; the
feed's feed-delta handler calls loadFeed(true) to resync from page 1 instead
of merging a partial slice.

Verified: cargo check clean, svelte-check 0 errors, production build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-01 21:51:08 +02:00
parent 91ff961850
commit a084ba86c5
4 changed files with 24 additions and 1 deletions

View File

@@ -21,7 +21,10 @@
theme=dark don't flash a white screen. Mirrors the logic in
`src/lib/theme-store.ts`; kept in sync by hand (it's 6 lines).
-->
<script>
<!-- nonce is required: our CSP sets script-src 'self', and SvelteKit's
mode:'auto' only hashes scripts *it* injects, not this template-authored
one. %sveltekit.nonce% is substituted per request and added to the CSP. -->
<script nonce="%sveltekit.nonce%">
(function () {
try {
var pref = localStorage.getItem('eventsnap_theme') || 'system';

View File

@@ -27,6 +27,9 @@ export interface FeedResponse {
export interface DeltaResponse {
uploads: FeedUpload[];
deleted_ids: string[];
// True when the delta hit the backend cap and is only the newest slice of the
// gap — the client must full-refresh rather than merge (see feed-delta handler).
truncated: boolean;
}
// mirrors backend/src/handlers/feed.rs::HashtagCount

View File

@@ -206,6 +206,13 @@
onSseEvent('feed-delta', (data) => {
try {
const delta = JSON.parse(data) as DeltaResponse;
if (delta.truncated) {
// Missed more than the backend delta cap while backgrounded — the
// delta is only the newest slice, so merging would leave a silent gap
// of older-but-still-new uploads. Resync from page 1 instead.
void loadFeed(true);
return;
}
if (delta.uploads.length) {
const seen = new Set(uploads.map((u) => u.id));
const fresh = delta.uploads.filter((u) => !seen.has(u.id));