The ingest snapshot loop silently `continue`d on re-encode and file-write
failures (evidence lost with a 201), and a DB-index throw after the file
landed left an orphan JPEG on disk with no row — or bubbled into a 500
that discarded an already-stored event.
Extracts storeSnapshot(): re-encode → write file → index, in that order
so bytes exist before a row points at them. Every failure is error_log'd
instead of swallowed; a DB-index failure after the write @unlinks the
orphan file. A snapshot problem no longer fails the ingest — the event is
already persisted, so the 201 stands.
Test: testSnapshotIndexFailureCleansOrphanFileButKeepsEvent uses a
throwing snapshot repo and asserts 201, the event is stored, and no *.jpg
is left in the snapshot dir.
phpunit: 90 passing (4 skipped: PDO-backed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
An unauthenticated attacker could guess a real device_id and 429-lock that
camera by hammering /api/events before HMAC verification ran, dropping its
accident events. Add a cheap pre-auth per-IP limiter (Config::ingestIpRateLimitPerMin,
default 300/min) that fires before any device-secret lookup, and move the
existing per-device limiter to run only after HMAC + timestamp + nonce checks
succeed, so only authenticated requests consume a device's own quota.