# Letting a plugin serve a path without a session Design proven by measurement on 2026-08-18, half implemented. The manifest field and its validator are in; the mounting is not. Everything below was tested against Hono rather than reasoned about — the probes are reproduced because two of the three obvious designs are wrong in ways that only show up when run. ## Why Some integrations receive requests that structurally cannot carry a session. Kiosk is the first: iOS `installd` fetches an install manifest and a binary **itself**, and an enrolling device posts its identifier from the OS. Neither is our code, so neither can set a header, and the platform's `?token=` escape hatch does not help. Today a plugin cannot serve such a path at all: `UNPROTECTED_API_PREFIXES` is a hardcoded list and plugin routers mount only on `protectedRouter`, behind `userMiddleware`. The rejected alternative was putting an `ofk_` API key in the URL. That is authority over the whole platform — files, terminal, vault — in a string that lands in a manifest, a server log and a phone's install history. ## What is built `PluginManifest.unauthenticatedPaths?: string[]`, and `publicPathProblem()` in `plugins/manifest.ts`, wired into `validateManifest`. Paths are relative to the plugin's own mount and refused — never corrected — if they start without `/`, contain `..`, `//`, a query or fragment, use a wildcard anywhere but as a trailing `/*`, or are the bare `/` or `/*`. The plugin is trusted with **what those paths do**, never with **which paths**. Authorising them is its own job; Kiosk mints a per-user key it can rotate, scoped to one plugin. ## What is not built: the mounting Three designs, and the measurements that killed two of them. **1. Mount the public router at the plugin prefix, before `/api`.** Fall-through works — an undeclared path reaches the gate and 401s correctly. **But the plugin's public router can define any route it likes**, and whatever it defines is served publicly. The manifest becomes documentation rather than enforcement. **2. Same, plus a `use('*')` guard that 404s undeclared paths.** Blocks the leak — and **breaks the plugin's authenticated API**. A `use('*')` middleware always matches, so it handles every request under the prefix and the parent never continues. Measured: `/api/kiosk/secret` returned 404 even _with_ a valid token, where it should have reached the protected router. **3. Register ONLY the declared patterns, delegating into a pre-mounted app. ← correct** ```ts const mounted = new Hono(); mounted.route(`/api${plugin.prefix}`, plugin.publicRouter); // so sub-routes see paths they recognise for (const pattern of declaredPatterns) { app.all(pattern, (ctx) => mounted.fetch(ctx.req.raw)); // BEFORE app.route('/api', protectedRouter) } ``` Undeclared paths never register a public handler, so they never match and fall through naturally. Measured, all four cases correct: | request | token | result | | --------------------------------------------------- | ----- | ------------------------------------------------- | | `/api/kiosk/artefact/a` (declared) | none | **200** from the public router | | `/api/kiosk/secret` (plugin defined it; undeclared) | none | **401** — falls to the gate, does not leak | | `/api/kiosk/secret` | valid | **200** from the protected router | | `/api/kiosk/apps` | valid | **200** — ordinary authenticated route unaffected | Enforcement is by **registration**, not by checking, which is why a plugin cannot widen it. ## Remaining work 1. `api/public.ts` in discovery (`discover.ts`), loaded only when `unauthenticatedPaths` is declared — a separate file from `api/router.ts` so the two surfaces cannot be confused by whoever reads either. 2. `MountedPlugin` gains `publicRouter` and the absolute declared patterns; `mount.ts` builds them from `mountPrefix` + each declared path, so the plugin never supplies an absolute path. 3. `buildHonoApp`: design 3 above, registered before `app.route('/api', protectedRouter)`. 4. **Boot-time re-check.** `validateManifest` runs at install; a file edited afterwards must not widen the surface. Re-validate where `assertPermissionTotality` already refuses to boot. 5. Surface it in the install dialog the way `osDependencies` is — the owner should be told, in those words, that a plugin wants to serve something without a login. 6. Tests that bite: an undeclared route the plugin defines must not be reachable, and the plugin's authenticated routes must keep working. Both are regressions that would otherwise be silent. Nothing here is wired, so no plugin serves anything publicly today and the platform behaves exactly as before.