From a084ba86c5f382603a21b079c08e6cde78e7103b Mon Sep 17 00:00:00 2001 From: fabi Date: Wed, 1 Jul 2026 21:51:08 +0200 Subject: [PATCH] fix(review): CSP nonce for FOUC script + feed_delta truncation signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/src/handlers/feed.rs | 10 ++++++++++ frontend/src/app.html | 5 ++++- frontend/src/lib/types.ts | 3 +++ frontend/src/routes/feed/+page.svelte | 7 +++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/src/handlers/feed.rs b/backend/src/handlers/feed.rs index 830a634..29e46e3 100644 --- a/backend/src/handlers/feed.rs +++ b/backend/src/handlers/feed.rs @@ -172,6 +172,11 @@ pub struct DeltaQuery { pub struct DeltaResponse { pub uploads: Vec, pub deleted_ids: Vec, + /// True when the upload query hit `DELTA_LIMIT`: the response carries only the + /// newest slice of the gap, so the client must fall back to a full feed refresh + /// rather than merging (the older missed uploads are absent and unrecoverable + /// via a later delta, which advances `since` past them). + pub truncated: bool, } pub async fn feed_delta( @@ -197,6 +202,10 @@ pub async fn feed_delta( .fetch_all(&state.pool) .await?; + // Hit the cap => this is only the newest slice of a larger gap. Signal the + // client to full-refresh instead of merging a partial delta. + let truncated = rows.len() as i64 >= DELTA_LIMIT; + let deleted_ids: Vec<(Uuid,)> = sqlx::query_as( "SELECT id FROM upload WHERE event_id = $1 AND deleted_at IS NOT NULL AND deleted_at > $2", @@ -229,6 +238,7 @@ pub async fn feed_delta( Ok(Json(DeltaResponse { uploads, deleted_ids: deleted_ids.into_iter().map(|r| r.0).collect(), + truncated, })) } diff --git a/frontend/src/app.html b/frontend/src/app.html index 87b0e0c..b17193e 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -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). --> -