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

@@ -172,6 +172,11 @@ pub struct DeltaQuery {
pub struct DeltaResponse {
pub uploads: Vec<FeedUpload>,
pub deleted_ids: Vec<Uuid>,
/// 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,
}))
}