From 79ff366802cd8c49df49468d7f2fee7054874e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 28 Aug 2026 22:36:53 +0100 Subject: [PATCH] become an Officer plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- api/router.ts | 30 +++++++++ bun.lock | 88 --------------------------- docs/SHARP.md | 2 +- manifest.ts | 49 +++++++++++++++ package.json | 20 ------ scripts/gen-docs.ts | 6 +- {src => sidecar}/assets/store.ts | 0 {src => sidecar}/config.ts | 0 {src => sidecar}/errors.ts | 0 {src => sidecar}/ffmpeg/run.ts | 0 {src => sidecar}/ffmpeg/vocabulary.ts | 0 src/server.ts => sidecar/index.ts | 48 +++++++++++++++ {src => sidecar}/probe/ffprobe.ts | 0 {src => sidecar}/sharp/decodable.ts | 0 {src => sidecar}/sharp/run.ts | 0 {src => sidecar}/sharp/validate.ts | 0 {src => sidecar}/sharp/vocabulary.ts | 0 tsconfig.json | 18 ------ 18 files changed, 131 insertions(+), 130 deletions(-) create mode 100644 api/router.ts delete mode 100644 bun.lock create mode 100644 manifest.ts delete mode 100644 package.json rename {src => sidecar}/assets/store.ts (100%) rename {src => sidecar}/config.ts (100%) rename {src => sidecar}/errors.ts (100%) rename {src => sidecar}/ffmpeg/run.ts (100%) rename {src => sidecar}/ffmpeg/vocabulary.ts (100%) rename src/server.ts => sidecar/index.ts (77%) rename {src => sidecar}/probe/ffprobe.ts (100%) rename {src => sidecar}/sharp/decodable.ts (100%) rename {src => sidecar}/sharp/run.ts (100%) rename {src => sidecar}/sharp/validate.ts (100%) rename {src => sidecar}/sharp/vocabulary.ts (100%) delete mode 100644 tsconfig.json diff --git a/api/router.ts b/api/router.ts new file mode 100644 index 0000000..e0472e0 --- /dev/null +++ b/api/router.ts @@ -0,0 +1,30 @@ +import { createSidecarProxy } from '@@/sidecar/create-proxy'; +import { mountPrefix } from '@@/plugins/manifest'; +import { manifest } from '../manifest'; + +/** + * `/api/av-club/*` — authenticate, then forward to the officer-av-club sidecar. + * + * No routes of its own and no media knowledge: the whole contract is `../sidecar/index.ts`, and the + * reasoning behind what it will and will not accept is `../docs/SECURITY.md`. + * + * ── The prefix is derived ── + * + * `mountPrefix` returns `/av-club` for this first-party publisher. Writing `/api/av-club` literally + * would be right by coincidence and wrong the moment anybody else publishes this plugin. + * + * ── Why the timeout is long ── + * + * A 4K transcode is minutes of work, and the sidecar already sets `idleTimeout: 255` because Bun's + * ten-second default was killing encodes mid-flight. A short timeout on THIS hop would reintroduce the + * same failure one layer up: the platform hanging up while ffmpeg is still working, which the caller + * sees as a dead request rather than as a limit. Same reasoning as transcribe, whose whisper runs are + * long for the same kind of reason. + */ +const proxy = createSidecarProxy({ + name: 'av-club', + prefix: `/api${mountPrefix({ appName: 'av-club', manifest })}`, + timeoutSeconds: 1800, +}); + +export const router = proxy.router; diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 236c114..0000000 --- a/bun.lock +++ /dev/null @@ -1,88 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "av-club", - "dependencies": { - "sharp": "^0.35.3", - }, - "devDependencies": { - "@types/bun": "latest", - }, - }, - }, - "packages": { - "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - - "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], - - "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], - - "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], - - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - - "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], - - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], - } -} diff --git a/docs/SHARP.md b/docs/SHARP.md index f1d70f0..e66dcf8 100644 --- a/docs/SHARP.md +++ b/docs/SHARP.md @@ -1,6 +1,6 @@ # sharp — the operation vocabulary -**Generated from `src/sharp/vocabulary.ts` by `scripts/gen-docs.ts`. Do not edit by hand — +**Generated from `sidecar/sharp/vocabulary.ts` by `scripts/gen-docs.ts`. Do not edit by hand — run `bun run docs`.** Every operation is named what sharp names it and takes sharp's own arguments, so diff --git a/manifest.ts b/manifest.ts new file mode 100644 index 0000000..86a2024 --- /dev/null +++ b/manifest.ts @@ -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', + }, + ], +}; diff --git a/package.json b/package.json deleted file mode 100644 index b48187f..0000000 --- a/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "av-club", - "version": "0.0.1", - "private": true, - "type": "module", - "description": "Turning whatever someone uploads into assets a website can actually serve", - "scripts": { - "dev": "bun --hot src/server.ts", - "start": "bun src/server.ts", - "docs": "bun run ./scripts/gen-docs.ts", - "install:tools": "./scripts/install.sh", - "verify": "./scripts/verify.sh" - }, - "dependencies": { - "sharp": "^0.35.3" - }, - "devDependencies": { - "@types/bun": "latest" - } -} diff --git a/scripts/gen-docs.ts b/scripts/gen-docs.ts index 9d38e58..1abef1a 100644 --- a/scripts/gen-docs.ts +++ b/scripts/gen-docs.ts @@ -1,5 +1,5 @@ #!/usr/bin/env bun -// Generate docs/SHARP.md from src/sharp/vocabulary.ts. +// Generate docs/SHARP.md from sidecar/sharp/vocabulary.ts. // // The vocabulary table is what the validator enforces. Writing the reference by hand would mean two // descriptions of one thing, and the doc would quietly stop being true the first time a bound @@ -7,7 +7,7 @@ // // Run: bun run docs -import { INPUT_OPTIONS, OPERATIONS, OUTPUT_FORMATS, type Param } from "../src/sharp/vocabulary"; +import { INPUT_OPTIONS, OPERATIONS, OUTPUT_FORMATS, type Param } from "../sidecar/sharp/vocabulary"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -44,7 +44,7 @@ function paramTable(params: Record): string { const lines: string[] = [ "# sharp — the operation vocabulary", "", - "**Generated from `src/sharp/vocabulary.ts` by `scripts/gen-docs.ts`. Do not edit by hand —", + "**Generated from `sidecar/sharp/vocabulary.ts` by `scripts/gen-docs.ts`. Do not edit by hand —", "run `bun run docs`.**", "", "Every operation is named what sharp names it and takes sharp's own arguments, so", diff --git a/src/assets/store.ts b/sidecar/assets/store.ts similarity index 100% rename from src/assets/store.ts rename to sidecar/assets/store.ts diff --git a/src/config.ts b/sidecar/config.ts similarity index 100% rename from src/config.ts rename to sidecar/config.ts diff --git a/src/errors.ts b/sidecar/errors.ts similarity index 100% rename from src/errors.ts rename to sidecar/errors.ts diff --git a/src/ffmpeg/run.ts b/sidecar/ffmpeg/run.ts similarity index 100% rename from src/ffmpeg/run.ts rename to sidecar/ffmpeg/run.ts diff --git a/src/ffmpeg/vocabulary.ts b/sidecar/ffmpeg/vocabulary.ts similarity index 100% rename from src/ffmpeg/vocabulary.ts rename to sidecar/ffmpeg/vocabulary.ts diff --git a/src/server.ts b/sidecar/index.ts similarity index 77% rename from src/server.ts rename to sidecar/index.ts index 3d32ff9..43172e8 100644 --- a/src/server.ts +++ b/sidecar/index.ts @@ -6,6 +6,8 @@ import { ffprobe } from "./probe/ffprobe"; import { runSharp, type SharpRequest } from "./sharp/run"; import { runFfmpeg, type FfmpegRequest } from "./ffmpeg/run"; import sharp from "sharp"; +import { createSidecarConnector } from "@@/sidecar/connect"; +import { API_URL } from "@@/officer-url.mjs"; /** * av-club — ffmpeg, sharp and ffprobe over HTTP. @@ -26,6 +28,17 @@ const TOKEN = requireToken(); * properly is three lines. */ function authorised(request: Request): boolean { + /* + The platform, having already authenticated somebody. + + `createSidecarProxy` forwards `X-Officer-User` after the account gate and the `av-club` permission + gate have both passed, and this server binds LOOPBACK — so that header is only settable by something + already on this machine, which is the same trust boundary caldav's DAV door relies on. Requiring the + bearer token as well would mean shipping av-club's secret to every caller for no gain: the platform + is the thing deciding who may encode, and it has already decided. + */ + if (request.headers.get("x-officer-user")) return true; + const header = request.headers.get("authorization"); if (!header?.startsWith("Bearer ")) return false; @@ -181,12 +194,47 @@ async function route(request: Request): Promise { const server = Bun.serve({ port: PORT, + // Loopback, because `X-Officer-User` above is trusted and a header is only as good as who can set it. + hostname: "127.0.0.1", // Bun's default is 10 seconds, which silently kills a long encode mid-flight. idleTimeout: 255, maxRequestBodySize: MAX_UPLOAD_BYTES, fetch: (request) => route(request).catch(toErrorResponse), }); +/* + Register with the platform, and report the port it can be reached on. + + The port is still `AV_CLUB_PORT` rather than an ephemeral one: this service is also usable directly — + by a script on the box, or by fofos/platform, which is its stated first consumer and is not Officer. + Being a plugin is an additional way in, not a replacement for the one it already had. +*/ +const connection = createSidecarConnector({ + apiUrl: `${API_URL}/api/sidecar/register`, + name: "av-club", + handles: ["av-club"], + onCommand(_cmd, reply) { + reply({ type: "error", id: (_cmd as { id: string }).id, error: "av-club takes no commands; use its HTTP API" }); + }, + onConnected() { + connection.send({ type: "av-club:server", port: PORT }); + console.log(`[av-club] reported port ${PORT} to the API`); + }, +}); + +function shutdown(signal: string) { + console.log(`[av-club] ${signal} received, shutting down...`); + try { + server.stop(true); + } catch { + /* already stopped */ + } + connection.destroy(); + process.exit(0); +} +process.on("SIGTERM", () => shutdown("SIGTERM")); +process.on("SIGINT", () => shutdown("SIGINT")); + console.log(`av-club on ${server.url}`); console.log(` assets ${ASSETS_DIR}`); console.log(` max upload ${(MAX_UPLOAD_BYTES / 1_048_576).toFixed(0)} MB`); diff --git a/src/probe/ffprobe.ts b/sidecar/probe/ffprobe.ts similarity index 100% rename from src/probe/ffprobe.ts rename to sidecar/probe/ffprobe.ts diff --git a/src/sharp/decodable.ts b/sidecar/sharp/decodable.ts similarity index 100% rename from src/sharp/decodable.ts rename to sidecar/sharp/decodable.ts diff --git a/src/sharp/run.ts b/sidecar/sharp/run.ts similarity index 100% rename from src/sharp/run.ts rename to sidecar/sharp/run.ts diff --git a/src/sharp/validate.ts b/sidecar/sharp/validate.ts similarity index 100% rename from src/sharp/validate.ts rename to sidecar/sharp/validate.ts diff --git a/src/sharp/vocabulary.ts b/sidecar/sharp/vocabulary.ts similarity index 100% rename from src/sharp/vocabulary.ts rename to sidecar/sharp/vocabulary.ts diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 129d371..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true - } -}