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
This commit is contained in:
2026-08-28 22:36:53 +01:00
co-authored by Claude Opus 5
parent dc4fe758f3
commit 79ff366802
18 changed files with 131 additions and 130 deletions
+49
View File
@@ -0,0 +1,49 @@
import type { PluginManifest } from '@@/plugins/manifest';
/**
* av-club — sharp, ffmpeg and ffprobe over HTTP, for whoever needs to turn an upload into something a
* browser can serve.
*
* ── No screens, deliberately ──
*
* This is an API and a body of documentation, not a place to click. Its callers are a commander watching
* an upload folder, an agent, or a person with curl. A dock tile would advertise a page that has nothing
* to show — the work happens where the files are, which is not here.
*
* ── One permission, and it is not `personal` ──
*
* Every other permission's paths act on the caller's own data. These act on assets av-club itself issued
* ids for, in its own store, shared by every caller — so it is an ordinary grantable capability rather
* than a per-account one, and an account without it cannot reach the encoder at all.
*/
export const manifest: PluginManifest = {
publisher: 'officerdev',
version: '2026.08.28.01',
platform: '>=1.0.0',
label: 'AV Club',
summary: 'Convert and treat images, video and audio — sharp, ffmpeg and ffprobe behind one API',
tags: ['Development', 'Personal'],
icon: 'Clapperboard',
color: '#8b5cf6',
/*
Declared HERE rather than in a package.json, and that is not a style choice.
A package.json inside a plugin makes a package boundary, and Bun then stops walking up for the
platform's tsconfig — so `@@/sidecar/connect` stops resolving and the sidecar dies on its first
import. That is exactly how this plugin failed on the day it was extracted: it arrived carrying its
own package.json and bun.lock, and both the router and the sidecar could not find the platform.
`npm-deps.ts` collects these across every plugin into `plugins/node_modules/`, so atelier's copy of
sharp is the one used here too rather than a second 50 MB of platform binaries.
*/
npmDependencies: {
sharp: '^0.35.3',
},
permissions: [
{
key: 'av-club',
label: 'AV Club',
description: 'Convert and inspect media through sharp, ffmpeg and ffprobe',
},
],
};