Independent review (cold fork = MERGEABLE-WITH-NITS; Codex = BLOCKED, caught
concurrency defects the fork missed). All actionable findings folded in:
- HIGH (Codex) atomic first-claim-wins: ClaimLocalAdmin now writes the three
credential rows in ONE transaction guarded by the unique ConfigElement.Key
index (lost race -> DbUpdateException -> 409), so concurrent claims can't
produce a mixed-state credential.
- HIGH (Codex) consistent login snapshot: VerifyLocalAdminLogin reads hash+stamp
in one query and drops rehash-on-verify, so a login racing a password change
can't capture a stamp newer than the hash it verified (concurrent change ->
old password fails, or the issued cookie carries the pre-change stamp ->
revoked next request).
- MEDIUM (Codex) env-seed migration race: LocalAdminSeedService is now a RunOnce
BackgroundService that awaits SystemStartup.WaitForDatabase (the migrator is a
BackgroundService; registration order didn't guarantee the schema) + try/catch.
- MEDIUM (fork M1) ForwardedHeaders: reverted the strict-opt-in flip — it would
regress /iptv M3U/XMLTV/HLS absolute-URL generation (Request.Scheme) behind a
proxy without KnownProxies. Kept #285 behavior; KnownProxies still recommended.
- LOW (Codex/fork) require X-CSRF on /api/auth/logout + /password (the
[SkipApiAuthorization] surface isn't covered by the filter's CSRF check;
closes forced-logout CSRF).
- ChangeLocalAdminPassword also writes hash+stamp atomically. Input length caps
on username/password.
Deferred with a tracked gate: MEDIUM (Codex) side-effecting [RequiresAuthentication]
GETs (troubleshoot playback/archive) aren't CSRF-covered -> #301, gates PR2
(latent in PR1: the SPA still uses the machine key).
Verify: full ErsatzTV.Tests green (1501); no OpenAPI/generated drift. Docs updated
(api-conventions §9, decisions.md).
Refs #295#301
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>