a first-party plugin may claim a root path the platform reserved
caldav is not only /api/caldav. four of its surfaces are top-level and above the account gate on purpose, because DAVx5, iOS and Thunderbird send HTTP Basic on every request and have nowhere to put a JWT: /dav, the ios profile under it, and the two .well-known discovery paths a client probes before it has any credential. the mechanism for that already existed — unauthenticatedRootPaths, exclusive claims, first come wins. what stopped caldav using it was RESERVED_ROOT, and what that list was recording is worth being exact about: `dav` and `.well-known` are in it because the PLATFORM serves them today, not because they are dangerous in themselves. once caldav is a plugin, the thing the reservation protects IS the plugin, and keeping it would mean the platform permanently owning routes for a feature it no longer contains. so a reserved name is claimable by a first-party plugin and refused to everyone else. the owner drew that line today: fine for a plugin we wrote, and letting a stranger's plugin claim /dav is a question that needs thinking about properly rather than falling out of a flag. /api stays refused to us too — everything of officer's lives under it and protocolHeaders is the only sanctioned way in. .well-known needed two segments, which the single-segment rule refuses. allowed by NAME rather than by pattern, and the reason is in the same namespace: acme-challenge is how this machine gets its TLS certificates, so a plugin owning it could take the domain. caldav and carddav are on the list; nothing else is, for us as much as for anyone. the OPTIONS carve-out moved with it. hono's cors() answers every OPTIONS as a preflight and never calls the route beneath — which to a DAV client is not a preflight but the question "what can you do", answered by a header iOS refuses the account without. that was a literal list of four DAV paths; it asks the claim registry now, so it follows whoever owns the route rather than naming it here. verified: server boots (totality passes), /.well-known/caldav still 301s and /dav/ still asks for basic auth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -589,20 +589,62 @@ const RESERVED_ROOT = new Set([
|
||||
'index.html',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Names under `/.well-known` a first-party plugin may claim.
|
||||
*
|
||||
* `.well-known` is IANA's registry namespace (RFC 8615) and the whole point of it is that a name in there
|
||||
* means one specific thing — so claiming a name from it is not the platform inventing a URL, it is a
|
||||
* plugin answering the question that name asks. `caldav` and `carddav` are RFC 6764's discovery paths:
|
||||
* a client is given a bare domain and probes them before it has any credential.
|
||||
*
|
||||
* An ALLOW-LIST rather than "any name under .well-known", because the namespace also holds things that
|
||||
* must never move — `acme-challenge` is how this machine gets its TLS certificates, and a plugin owning
|
||||
* it could take the domain. Adding a name here is a deliberate act; there is no pattern that grants one.
|
||||
*/
|
||||
const WELL_KNOWN_CLAIMABLE = new Set(['caldav', 'carddav']);
|
||||
|
||||
/**
|
||||
* Check one declared ROOT path, returning a reason it is refused or null.
|
||||
*
|
||||
* Stricter than `publicPathProblem` because the surface is: this is the platform's own namespace, not the
|
||||
* plugin's, so a plugin gets to claim a single name and nothing structural.
|
||||
*
|
||||
* ── firstParty ──
|
||||
*
|
||||
* A reserved name is refused for everyone EXCEPT a first-party plugin, and that exception exists because
|
||||
* of what "reserved" was recording. `dav` and `.well-known` are in the list because the PLATFORM serves
|
||||
* them today, not because they are dangerous in themselves — and when caldav becomes a plugin, the thing
|
||||
* the reservation was protecting is the plugin. Refusing it would mean the platform permanently owning
|
||||
* routes for a feature it no longer contains.
|
||||
*
|
||||
* A THIRD-PARTY plugin is still refused every reserved name, and that is the line the owner drew on
|
||||
* 2026-08-27: this is fine for a plugin we wrote, and letting a stranger's plugin claim `/dav` — or
|
||||
* anything else the platform is trusted to answer — is a question that needs thinking about properly
|
||||
* rather than falling out of a flag. `api` stays refused for everyone, first-party included: it is where
|
||||
* all of Officer lives and `protocolHeaders` is the only sanctioned way to touch it.
|
||||
*/
|
||||
export function rootPathProblem(path: unknown): string | null {
|
||||
export function rootPathProblem(path: unknown, firstParty = false): string | null {
|
||||
if (typeof path !== 'string' || !path) return 'must be a non-empty string';
|
||||
if (!path.startsWith('/')) return `"${path}" must start with "/"`;
|
||||
const segment = path.slice(1);
|
||||
|
||||
// A two-segment claim under `.well-known`, which the single-segment rule below would refuse. First-party
|
||||
// only and allow-listed by name — see WELL_KNOWN_CLAIMABLE.
|
||||
const wellKnown = /^\.well-known\/([a-z][a-z0-9-]{0,38})$/.exec(segment);
|
||||
if (wellKnown) {
|
||||
if (!firstParty) return `"${path}" is reserved by the platform`;
|
||||
if (!WELL_KNOWN_CLAIMABLE.has(wellKnown[1]!)) {
|
||||
return `"${path}" is not a claimable .well-known name`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!/^[a-z][a-z0-9-]{0,38}$/.test(segment)) {
|
||||
return `"${path}" must be a single lowercase segment, e.g. "/identity"`;
|
||||
}
|
||||
if (RESERVED_ROOT.has(segment)) return `"${path}" is reserved by the platform`;
|
||||
// `api` is refused even to us. Everything of Officer's lives under it.
|
||||
if (segment === 'api') return `"${path}" is reserved by the platform`;
|
||||
if (RESERVED_ROOT.has(segment) && !firstParty) return `"${path}" is reserved by the platform`;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -742,7 +784,7 @@ export function manifestProblems(appName: string, manifest: Partial<PluginManife
|
||||
problems.push('unauthenticatedRootPaths must be an array of paths');
|
||||
} else {
|
||||
for (const [i, path] of manifest.unauthenticatedRootPaths.entries()) {
|
||||
const problem = rootPathProblem(path);
|
||||
const problem = rootPathProblem(path, manifest.publisher === FIRST_PARTY_PUBLISHER);
|
||||
if (problem) problems.push(`unauthenticatedRootPaths[${i}] ${problem}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user