plugins: declare unauthenticated paths, validated; mounting designed and measured

half of the primitive kiosk needs. the manifest field and its validator are in;
the mounting is documented and not wired, so nothing serves anything publicly
today.

paths are relative to the plugin's own mount and refused rather than corrected:
no leading-slash-missing, no .., no //, no query or fragment, wildcard only as a
trailing /*, and never the bare / or /*. the plugin is trusted with what those
paths DO, never with which paths.

the mounting was measured rather than reasoned about, and two obvious designs
are wrong:

mounting the public router at the plugin prefix gives correct fall-through but
lets the router serve any route it defines - the manifest becomes documentation
rather than enforcement.

adding a use('*') guard blocks that leak and BREAKS the plugin's authenticated
api: a use('*') middleware always matches, so the parent never continues.
measured - an authenticated route 404'd with a valid token.

what works is registering only the declared patterns, delegating into a
pre-mounted app. undeclared paths never register a handler, so they fall through
naturally. enforcement by registration rather than by checking, which is why a
plugin cannot widen it.

docs/plugin-unauthenticated-paths.md has the probe results and the six
remaining steps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 03:19:48 +00:00
co-authored by Claude Opus 5
parent f6658bd092
commit 2f31d464be
2 changed files with 150 additions and 0 deletions
+70
View File
@@ -128,6 +128,44 @@ export type PluginManifest = {
permissions: PluginPermission[];
/**
* Paths under this plugin's own mount that are served ABOVE the account gate.
*
* ── Why this exists ──
*
* Some integrations receive requests that structurally cannot carry a session. Kiosk is the first:
* iOS `installd` fetches an install manifest and a binary ITSELF — not the app, not the browser — and a
* device enrolling posts its identifier from the OS, not from a page. None of those has anywhere to put
* a bearer token, and the platform's own `?token=` escape hatch does not help because none of them is
* our code.
*
* The alternative was smuggling a full-power credential through a URL, which is strictly worse: an
* `ofk_` key is authority over the whole platform — files, terminal, vault — and it would end up in a
* manifest, a server log and a phone's install history.
*
* So this is the honest model. These paths really are public, and saying so lets the platform enforce
* exactly which ones rather than having a plugin invent its own bypass.
*
* ── What a plugin is trusted with, and what it is not ──
*
* NOT trusted: which paths. `validateManifest` refuses anything that could reach beyond the plugin's
* own mount, and `assertPluginPublicPaths` re-checks at boot, so a plugin cannot widen its surface by
* editing a file after review.
*
* Trusted: what those paths do. Authorising them is the plugin's job — Kiosk uses a per-user key it
* mints and can rotate, which is a credential scoped to one plugin and one purpose.
*
* ── Rules, each refused rather than corrected ──
*
* - must start with `/`, and is relative to the plugin's mount (`/artefact/…`, never `/api/kiosk/…`)
* - no `..`, no `//`, no query or fragment
* - a wildcard only as a trailing `/*`, never bare `*` and never mid-path
* - a bare `/` or `/*` is refused: that is the whole plugin, which is not a declaration
*
* Absent means the plugin serves nothing without a session, which is what every plugin today wants.
*/
unauthenticatedPaths?: string[];
/**
* Host binaries this plugin needs. Absent means it is self-sufficient, which offscale and example are.
*
@@ -194,6 +232,28 @@ export type DiscoveredPlugin = {
*/
export const FIRST_PARTY_PUBLISHER = 'officerdev';
/**
* Check one declared public path, returning a reason it is refused or null.
*
* Pure and exported so it can be tested directly. Every rule here exists to make one sentence true: **a
* declared path can only ever reach code inside this plugin's own mount.** A plugin that could escape it
* would be declaring the platform's routes public, which is a different and much worse thing than
* declaring its own.
*/
export function publicPathProblem(path: unknown): string | null {
if (typeof path !== 'string' || !path) return 'must be a non-empty string';
if (!path.startsWith('/')) return `"${path}" must start with "/" and be relative to the plugin's mount`;
if (path.includes('..')) return `"${path}" may not contain ".."`;
if (path.includes('//')) return `"${path}" may not contain "//"`;
if (path.includes('?') || path.includes('#')) return `"${path}" may not contain a query or fragment`;
// A wildcard is allowed only as the final segment. `/a/*/b` and `/a*` are both refused: the first is a
// pattern this does not need, and the second is the kind of thing that looks narrow and is not.
const star = path.indexOf('*');
if (star !== -1 && !path.endsWith('/*')) return `"${path}" may only use a wildcard as a trailing "/*"`;
if (path === '/' || path === '/*') return 'the whole plugin cannot be public — declare the paths you mean';
return null;
}
export function mountPrefix(plugin: { appName: string; manifest: { publisher: string } }): string {
const { appName } = plugin;
return plugin.manifest.publisher === FIRST_PARTY_PUBLISHER
@@ -285,6 +345,16 @@ export function manifestProblems(appName: string, manifest: Partial<PluginManife
for (const field of ['version', 'platform', 'label', 'summary', 'color'] as const) {
if (typeof manifest[field] !== 'string' || !manifest[field]) problems.push(`${field} is required`);
}
if (manifest.unauthenticatedPaths !== undefined) {
if (!Array.isArray(manifest.unauthenticatedPaths)) {
problems.push('unauthenticatedPaths must be an array of paths');
} else {
for (const [i, path] of manifest.unauthenticatedPaths.entries()) {
const problem = publicPathProblem(path);
if (problem) problems.push(`unauthenticatedPaths[${i}] ${problem}`);
}
}
}
if (!Array.isArray(manifest.permissions)) {
problems.push('permissions must be an array (use [] when the plugin gates nothing)');
} else {