Skip to content

feat(arena): make integrity flags server-authoritative (survive a page refresh) - #128

Merged
barunaniket merged 1 commit into
mainfrom
feat/server-integrity-flags
Aug 17, 2026
Merged

feat(arena): make integrity flags server-authoritative (survive a page refresh)#128
barunaniket merged 1 commit into
mainfrom
feat/server-integrity-flags

Conversation

@barunaniket

Copy link
Copy Markdown
Collaborator

Problem

Anti-cheat integrity flags (paste / copy / tab-switch / screenshot / …) were counted only in client React state and sent once at submit. A page refresh reset them to zero — so a user could rack up flags, refresh, and submit with flags: 0. (This is the "why do the flags reset on refresh?" bug.)

Fix — accumulate on the server as events happen

  • attempts gains flags (total) + flags_breakdown (JSON per category) — migration 0008.
  • POST /api/attempt/flag {slug, event} atomically increments the count on the user's ranked attempt (one UPDATE … json_set(…); it no-ops when there's no attempt row, so past-problem practice never accrues flags). GET ?slug= returns the accumulated count to seed the client on load, so a refresh shows the real total instead of 0.
  • useIntegrityMonitor now reports each event to the endpoint and seeds from the server on mount (keeps an optimistic local bump for instant UI, reconciles from the response).
  • /api/submit scores from attempts.flags, not the client payload — the submit route already read the attempt row for the solve clock, and now reads the flags too. The client flags/flagsBreakdown payload is dropped.

The client counter stays for instant feedback but is no longer trusted for scoring. This closes the casual refresh-to-reset hole. (A determined attacker who disables the client JS still won't emit events — client-side proctoring is inherently a deterrent, which the arena rules already state.)

⚠️ Deploy order

Apply migration 0008 (npx wrangler d1 migrations apply pesuecc-arena --remote) before deploying — the submit + flag routes select attempts.flags/flags_breakdown.

Verified

Local next dev + libSQL, end-to-end: 5 events → server total:5 with the exact per-category breakdown; a fresh re-fetch returns the same total (refresh survival); no-op for non-attempt slugs; 401 unauth; 400 invalid event. The atomic json_set increment tested directly on SQLite. tsc / eslint / vitest (47) / next build all green.

…e refresh)

Anti-cheat flags were counted only in client React state and sent once at submit, so
a page refresh reset them to zero — a user could rack up flags, refresh, and submit
clean. Flags are now accumulated on the server as they happen.

- attempts table gains `flags` (total) + `flags_breakdown` (JSON) — migration 0008.
- New POST /api/attempt/flag {slug, event} atomically increments the count on the
  user's ranked attempt (a single UPDATE with json_set; it no-ops when there's no
  attempt row, so past-problem practice never accrues flags). GET returns the
  accumulated count to seed the client on load, so a refresh shows the real total.
- useIntegrityMonitor reports each event to the endpoint and seeds from the server on
  mount; /api/submit now scores from attempts.flags, not the client payload.

The client counter stays for instant UI feedback but is no longer trusted for scoring.
This hardens against the casual refresh-to-reset; a determined attacker who disables
the client JS still won't report events — client-side proctoring is inherently a
deterrent, which the arena rules already state.

Verified locally: events accumulate and survive a re-fetch, the endpoint no-ops for
non-attempt slugs, and rejects unauth (401) + invalid events (400). tsc / eslint /
vitest (47) / next build all green; the atomic json_set increment tested on SQLite.

DEPLOY: apply migration 0008 (adds attempts.flags / flags_breakdown) BEFORE deploying
the code — /api/submit and /api/attempt/flag select those columns.
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
code-chef-pesuecc-chapter Ready Ready Preview Aug 17, 2026 1:02am

@barunaniket
barunaniket merged commit 8a1db68 into main Aug 17, 2026
3 checks passed
@barunaniket
barunaniket deleted the feat/server-integrity-flags branch August 17, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant