289f408d23
A DPoP proof carries a server-issued nonce. With none cached the server answers `401 use_dpop_nonce`, the client harvests the nonce and retries. `signAndFetch` already absorbed that, so it was invisible — but it did it PER REQUEST, with no coordination. The Service Worker always cold-starts without a nonce. Both mechanisms that pre-seed the page are unreachable from worker scope: workers have no `sessionStorage`, and `seedNonceFromCookie` early-returns on `typeof document === 'undefined'`. The SW also skips requests that already carry a `DPoP` header, so it never observes the page's responses and cannot harvest from them either. Browsers terminate idle workers after ~30s, so this happens routinely, not once. Uncoordinated, every request issued in that window discovers the nonce independently: N parallel requests → N challenges → 2N requests. That is precisely the photo grid, and serving `<img src>` is the SW's main job — those requests cannot sign themselves, which is why the worker exists. Each wasted challenge also costs the server a full ECDSA P-256 verify, because `verify_proof` runs before the nonce check. Now the first request through owns the discovery and the rest await it, so N challenges collapse to 1. The wait is capped (5s) and released in a `finally`: the SW is on the critical path for every thumbnail, so a hung discovery must degrade to the old behaviour rather than stall the grid behind a promise that never settles. Measured on a 763-line e2e server log: 115 `dpop.nonce_challenged` events across 97 logins. Scope, deliberately: this does NOT remove the one challenge per worker lifetime. Doing that needs the nonce persisted where a worker can read it — IndexedDB already holds the keypair — and it can never replace the challenge path anyway, since a persisted nonce can be stale. Left out until the audit line shows it is worth it; the numbers above are now legible enough to tell. Not a correctness fix. Nothing was broken and no test failed over this. The argument is waste, plus signal: 115 challenges per run is noise that would bury a real one. `hasNonce()` is exported for the gate — deliberately "will the next proof carry a nonce", not "is it still valid", since only the server knows the latter and the challenge path already handles it. Its tests assert it agrees with what `buildDpopProof` actually emits, because a wrong answer either reinstates the stampede or stalls every request behind a bootstrap that is not happening. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>