# @platform-modules/uploads

Validation leaves and thin R2 adapters for Workers hosts. Each capability is a separate subpath — no root export.

## Subpaths

- `uploads/magic-bytes` — header-only MIME detection (JPEG/PNG/WebP/AVIF/GIF; rejects SVG/HTML/script; strict AVIF brand)
- `uploads/image-dimensions` — PNG/WebP/JPEG dimension parse from headers
- `uploads/size-limit` — `assertSizeLimit(bytes, limit)`
- `uploads/quota` — pure `checkStorageQuota(used, req, limit)`
- `uploads/presign-s3compat` — `presignR2Put` via aws4fetch (peer)
- `uploads/r2-binding` — `putObject` over native `R2Bucket`

## Direct-PUT bypass contract

A presigned browser PUT uploads straight to R2; the worker never sees the bytes. That path bypasses `magic-bytes`, `size-limit`, and `checkStorageQuota` (SigV4 query signing cannot enforce content-length-range).

**Pre-issue (before minting the URL):** the host MUST call `checkStorageQuota(used, req, limit)` and refuse to issue when over quota. Bytes land in R2 on PUT — a post-upload quota check is after the side effect.

**Post-upload (HEAD + range-GET first bytes):** the host MUST run `detectMimeFromMagicBytes` on the fetched header and assert size out-of-band, or route through a proxy/direct-POST flow where the worker sees the bytes. Objects are untrusted until this runs.

**Content-Type binding is not spoof-protection.** `presignR2Put` pins the declared type in the signature only; clients can still PUT mismatched bytes. Binding lands in `X-Amz-SignedHeaders` or it is a no-op.
