opencode discovers AGENTS.md and nothing else; claude code discovers CLAUDE.md. this repo had neither, so an agent sent to change something here arrived with no guidance at all under either. it deliberately does NOT repeat README.md. that file says what the plugin is, for someone deciding whether to install it. this one answers the other question: i have to change something in here, what will i break. the facts in it are read from the repository rather than recalled — the panel keys from web/panels.ts, the tables from db/schema.ts, the permission keys, the host binaries and the route claims from manifest.ts. presence is the declaration everywhere in this system, so the file says what the platform will actually conclude from these files existing. CLAUDE.md beside it is a pointer, not a copy: two copies drift and the stale one is the one somebody reads.
Kiosk
Build, sign and install Officer's iOS and Android apps on your own registered devices. What EAS does, self-hosted.
- A catalogue of builds, newest per app per platform, with an Install button that hands
itms-services://to iOS or a plain URL to Android. - A per-user key that authorises artefact downloads. Held in memory by the app and re-read on demand, so rotating one costs nothing.
- Device records — who owns which iPhone or iPad, and whether Apple knows about it yet.
Status — read before installing
Nothing here has run. Ported on 2026-08-18 from the standalone service that was called OAS. The authenticated API and the two panels exist and typecheck; no endpoint has answered a request.
What works, in principle
GET /apps, GET /apps/:appName, GET /key, POST /key/rotate,
GET /install/:appName/:platform/:version, POST /installs, and the device and build verbs. All under
/api/kiosk, all requiring a platform session.
What is missing, and why
Installing will not work yet. Four requests in the flow arrive with no session — iOS installd fetching
the manifest and the .ipa, the device posting its UDID, and Safari downloading a .mobileconfig — and a
plugin cannot serve an unauthenticated route today. UNPROTECTED_API_PREFIXES is hardcoded in the platform
and plugin routers mount only behind the account gate.
So GET /install/... returns a correct-looking URL pointing at a route that does not exist. That is
deliberate: the URL shape is what the mobile app builds against, and it stops being a dead end the day the
platform gains unauthenticatedPaths. Those routes then land in api/public.ts, kept separate from
api/router.ts so the two surfaces cannot be confused by anyone reading either.
Enrolment is not ported for the same reason — it is entirely public routes.
Artefacts arrive by rsync, into $OFFICER_ROOT/data/kiosk/. The plugin records metadata about files
already on disk; it does not accept uploads, because the platform's shared upload route caps at 5MB and no
.ipa will ever satisfy that.
Notes
PUBLIC_URLmust be HTTPS with a certificate iOS already trusts. It fetches the manifest and binary itself and declines silently otherwise, which looks exactly like a broken build.- Table names are prefixed
kiosk_. There are no migrations — see the note at the top ofdb/schema.ts. - The plugin is currently
appName: kiosk. If the plugin storefront takes that name, this one is renamed; seedocs/kiosk-ecosystem.mdin the platform.