Two things the platform needed and could not get.
HEIC: sharp reported "corrupt header" on every iPhone photograph, which they are
not. Two separate causes. libheif caps iref references and burst/depth HEICs exceed
it — `unlimited` gets past that, and limitInputPixels is now passed explicitly
alongside because unlimited also loosens the bomb guard (verified the pixel limit
still rejects a bomb). Then sharp still could not decode the pixels: its bundled
libheif is built without libde265, so HEVC-coded HEIF is unreadable to it. The
system libheif does have the plugin, so HEIF now routes through ImageMagick to a
lossless PNG first. Verified: 1.4 MB HEIC in, 102 KB WebP out.
/v1/ffmpeg is built. ffmpeg's own option names, structured filters rather than a
-vf string, and four presets — web-1080/720/480 and poster. The presets are where
the encoder knowledge lives, so a consumer asks for "web-720" rather than knowing
flags, and revising quality is a change here for every site that uses it.
Default-deny throughout, same as the sharp surface: filters that read files or load
native code are absent from the table rather than blocklisted, protocols are
whitelisted to file, and the caller still never names a path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exposes sharp and ffprobe with their own vocabulary — operations are named what
sharp names them, ffprobe fields come back spelled the way ffprobe spells them —
so the tools' own documentation applies and there is no house dialect to learn.
Safety comes from one architectural invariant rather than from filtering: the
caller never supplies a path, a URL or a protocol. Input is an id we issued, output
is a name we chose. That removes the entire class of attack that makes "just pass
the flags through" a remote shell, which is precisely what makes it safe to open up
everything else. docs/SECURITY.md has the reasoning and the specific attacks.
The vocabulary is a data table, so the validator and docs/SHARP.md are generated
from one source and cannot drift. Two things are deliberately absent from it:
withMetadata, because that is how a client's GPS coordinates get published, and
limitInputPixels, because a caller who could raise it could turn a 1 KB PNG into a
40 GB allocation.
Verified against a running server, not asserted: traversal ids rejected, unknown
operations and typo'd parameters rejected by name, out-of-range values rejected
with the bound, a GPS-tagged photo coming out clean end to end, video probe, Range
requests, and a rejected upload leaving nothing on disk.
Found while building: config threw at import if AV_CLUB_TOKEN was unset, so
generating documentation required a production secret. The token is read at boot in
server.ts now — still a refusal to start, no longer a module-scope landmine.
ffmpeg is designed in docs/API.md and not built yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Media processing pulled out of the website that first needed it. ffmpeg,
ImageMagick, libvips and exiftool are a real installation rather than an npm
install, and encoder settings get revisited as browser support moves — neither of
which belongs on the same clock as page templates and database schemas.
No processing code yet. What is here is the tooling requirement, an installer, and
docs/PIPELINE.md writing down what has NOT been decided — how a consumer talks to
this, whether processing blocks an upload, where output goes — so those get chosen
deliberately instead of by whatever gets written first.
verify.sh checks by running: it encodes a video and probes the result against what
ffmpeg was told to make, loads sharp under Bun specifically, and asserts a
re-encode still strips GPS. That last one guards a real leak — if a future sharp
changes its default, this fails a check rather than quietly publishing the
coordinates of a client's living room. Confirmed non-vacuous by hiding ffmpeg and
watching it exit 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>