feat(moderation): owner-scoped edit blocks, canonical owner DIDs, one-day image caching master
Implements the 2026-09-28 decisions Q-I6 and the header half of Q-CDN, and closes a DID-spelling gap in the image proxy. Nothing new is configured; the behaviour applies to every instance, including self-hosters with no CDN. - Q-I6: a blob an author edits into a post or comment already removed for illegal-content gets an owner-scoped block only. ReconcileTx and ReconcileIncomingTx build owner-scoped blocks for every reason and refuse an empty owner DID with ErrInvalidSubject, so the every-owner block comes only from removeContent and covers exactly the CIDs indexed when the admin acted. Restore is unchanged. The ownerless-block pins in the post and comment consumer tests are deliberately replaced, and an outer acceptance test drives the real consumers, moderation repository and image proxy handler. - Owner DID spelling: a block matches one (owner DID, CID) pair exactly and the disk cache keys by DID, so an uppercase did:plc, an uppercase or percent-encoded did:web host, a path-based did:web, or a did:web port with a leading zero could reach a blocked owner's blob. ValidateOwnerDID accepts only canonical spellings; the handler answers anything else with 400 no-store before the conditional path, block check, cache, resolution and fetch, and GetImageResolvingPDS and IsBlobBlocked enforce it too. - Cache headers: a served image carries Cache-Control: public, max-age=86400 with no immutable, so an unpurged browser or shared cache keeps a removed image for at most a day. The 304 for a matching If-None-Match carries the same Cache-Control and ETag as the 200 (RFC 9110 §15.4.5); error and blocked responses, including the blocked conditional path, stay no-store. testkit exposes response headers on binary and error responses. - T2 author view: TestModerationPostRemovalContract signs the author in once through AppView.SignIn and proves the author view chunk 04 proved at T1: after removal post.get is a content-free #moderatedPost, getComments is RootNotFound, and actor.getPosts and the community feed omit the post; after an edit the author keeps the content-free view while anonymous reads stay notFound; after restore the author reads the edited record. The getComments read quota is reset so the author's read cannot exceed the per-window limit. - Docs: PRD_ADMIN_MODERATION §1 and §9 record Q-I6. PRD_CSAM_SCANNING describes one-day caching, the Cloudflare cache rule (respect origin headers), the post-deploy edge purge in the rollout order, residual-gap rows for unpurged caches and pre-deploy immutable responses, and a corrected zone name and status line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>