pastilhasandClaude Opus 5 79ff366802 become an Officer plugin
av-club keeps doing exactly what it did — sharp, ffmpeg and ffprobe behind one
API, and the invariant that the caller never names a path. What changes is how it
is reached and who decides who may reach it.

`src/` is `sidecar/`, because that is the name the platform looks for:
`sidecar/index.ts` present means "this plugin ships a process", and presence is
the declaration for everything structural here. `manifest.ts` at the root is the
other half of that rule.

api/router.ts is a proxy and nothing else. the platform authenticates, checks the
`av-club` permission, and forwards — so the sidecar now accepts a request
carrying `X-Officer-User` and no longer demands its own bearer token from
callers who came that way. it binds LOOPBACK to make that trustworthy, which is
the same boundary caldav's DAV door already relies on. AV_CLUB_TOKEN stays, and
still guards anything calling the port directly: fofos/platform is the stated
first consumer and is not Officer.

THREE THINGS HAD TO GO, and each one broke it in a way worth writing down.

package.json and bun.lock: a package.json inside a plugin is a package boundary,
so bun stops walking up for the platform's tsconfig and `@@/sidecar/connect`
stops resolving. the sidecar died on its first import, fifteen times. npm
dependencies go in the manifest instead, where npm-deps.ts intersects them across
plugins into one plugins/node_modules — which is also how this surfaced a real
conflict: sharp ^0.33.5 (atelier) and ^0.35.3 here do not overlap, since a 0.x
caret pins the minor. atelier moved up; it calls one method.

tsconfig.json: same failure one layer earlier, shadowing the platform's `@@/*`
paths so api/router.ts would not mount. no other plugin has one.

no screens, deliberately. this is an API and a body of documentation; its callers
are a commander watching a folder, an agent, or curl. a dock tile would advertise
a page with nothing on it.

verified through /api/av-club with an agent key: a 3000x2000 jpeg uploaded,
probed, and resized to a 1200x800 avif — 166 KB to 1 KB — without the caller ever
holding av-club's own token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-28 22:36:53 +01:00
2026-08-28 22:36:53 +01:00
2026-08-28 22:36:53 +01:00
2026-08-28 22:36:53 +01:00
2026-08-28 22:36:53 +01:00
2026-08-28 22:36:53 +01:00

av-club

Turning whatever someone uploads into assets a website can actually serve.

People upload photos and videos straight off a phone or a camera: a 12 MB HEIC, a 4K clip in a codec Safari refuses to play, a JPEG carrying the GPS coordinates of the room it was shot in. None of that belongs on a page as-is. This is the thing in between.

It is an HTTP service that exposes as much of sharp, ffmpeg and ffprobe as can be exposed safely, keeping each tool's own vocabulary — sharp operations are named what sharp names them, ffprobe fields come back spelled the way ffprobe spells them. If you know the tool, you know the API.

What it will not let you do is name a file. That single restriction is what makes the rest safe to open up; see docs/SECURITY.md.

Status

Assets, probing and the full sharp surface work. ffmpeg is designed but not built — that is the next piece.

docs/API.md The HTTP surface — start here if you are calling it
docs/SHARP.md Every sharp operation and its bounds. Generated from the validator, so it cannot drift
docs/SECURITY.md The threat model and the invariants. Read before adding an endpoint
docs/TOOLING.md What is installed, and the two things that bite
docs/PIPELINE.md What this is for, and what is still undecided

Quick start

./scripts/install.sh                    # system packages + bun deps, then verifies
cp .env.example .env                    # then set AV_CLUB_TOKEN
bun dev

Verified on Ubuntu 24.04. The installer says so rather than guessing if it finds something else.

# upload something, then resize it
curl -X POST localhost:3020/v1/assets -H "Authorization: Bearer $AV_CLUB_TOKEN" -F file=@photo.jpg

curl -X POST localhost:3020/v1/sharp -H "Authorization: Bearer $AV_CLUB_TOKEN" \
  -H 'Content-Type: application/json' -d '{
    "input": "as_…",
    "pipeline": [{ "rotate": {} }, { "resize": { "width": 1200, "withoutEnlargement": true } }],
    "output": { "format": "avif", "options": { "quality": 55 } }
  }'

bun run verify checks the tooling; bun run docs regenerates docs/SHARP.md from the validator.

Why this is its own repo

It has system dependencies a web app should not have to assume. ffmpeg, ImageMagick, libvips and exiftool are a real installation, not an npm install. Keeping them here means a consuming project can state that it needs this, rather than quietly failing on a machine that happens not to have ffprobe on its PATH.

It changes on a different clock. Encoder settings, derivative sizes and format choices get revisited as browser support moves. Page templates and database schemas do not.

Nothing about it is specific to one site. Resizing an image and extracting a poster frame are the same problem everywhere, which is why this is not buried inside the project that first needed it.

Its first consumer

fofos/platform — a website with a public site, an admin, and a database. Admins upload media through the admin; this processes it; the platform stores what comes back.

How that handoff works is not decided yet. The honest options are a library the platform imports, a CLI it shells out to, or a service it talks to. Each has real consequences for deployment and for whether processing blocks an admin's upload while they wait. That question is written down in docs/PIPELINE.md as open, rather than being answered by whatever happens to get built first.

Working agreements

  • Commit and push every turn that changes anything — working, broken, or half-finished. The history is the artifact; it is how decisions stay traceable and mistakes stay learnable.
  • Docs are live documents, and the code is the source of truth. Docs move with the code in the same commit. A doc contradicting the code is a bug in the doc.
  • Direction changes fast, and that is expected. Nothing written here is frozen.
S
Description
Turning whatever someone uploads into assets a website can actually serve
Readme
256 KiB
Languages
TypeScript 90.8%
Shell 9.2%