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:
2026-08-27 04:22:27 +00:00
co-authored by Claude Opus 5
parent 6b5c87da10
commit e0df28ff5d
4 changed files with 126 additions and 23 deletions
+48 -4
View File
@@ -128,14 +128,58 @@ describe('top-level claims', () => {
expect(refused).toEqual([]);
});
// The whole point of the reserved list: a plugin claiming these would shadow the platform for every
// request, and `/api` in particular is where all of Officer lives.
test.each([['/api'], ['/auth'], ['/plugins'], ['/dav']])('refuses the reserved segment %s', (root) => {
const { segments, refused } = resolvePluginRootPaths(withRoots('evil', [root]), new Set());
const thirdParty = (appName: string, roots: string[]) => {
const p = plugin(appName, undefined, 'someone-else');
(p.manifest as { unauthenticatedRootPaths?: string[] }).unauthenticatedRootPaths = roots;
return p;
};
// `/api` is refused to EVERYONE, first-party included. It is where all of Officer lives, and
// `protocolHeaders` is the only sanctioned way to touch it.
test('refuses /api even to a first-party plugin', () => {
const { segments, refused } = resolvePluginRootPaths(withRoots('ours', ['/api']), new Set());
expect(segments).toEqual([]);
expect(refused).toHaveLength(1);
});
// The rest of the reserved list records what the PLATFORM serves today, which is a different claim from
// "dangerous". A first-party plugin may take one back — caldav owning `/dav` is the whole reason — and a
// third-party plugin may not. The owner drew that line on 2026-08-27: fine for a plugin we wrote,
// and a stranger's plugin claiming `/dav` is a question to think about properly rather than a flag.
test.each([['/auth'], ['/plugins'], ['/dav'], ['/assets']])(
'refuses the reserved segment %s to a THIRD-PARTY plugin',
(root) => {
const { segments, refused } = resolvePluginRootPaths(thirdParty('stranger', [root]), new Set());
expect(segments).toEqual([]);
expect(refused).toHaveLength(1);
},
);
test('lets a FIRST-PARTY plugin claim a reserved segment — caldav taking /dav', () => {
const { segments, refused } = resolvePluginRootPaths(withRoots('caldav', ['/dav']), new Set());
expect(segments).toEqual(['dav']);
expect(refused).toEqual([]);
});
// RFC 6764's discovery paths: a client is given a bare domain and probes these before it has any
// credential. Two segments, which the single-segment rule refuses — allowed by name, and by name only.
test('lets a first-party plugin claim an allow-listed .well-known name', () => {
const { segments, refused } = resolvePluginRootPaths(
withRoots('caldav', ['/.well-known/caldav', '/.well-known/carddav']),
new Set(),
);
expect(segments).toEqual(['.well-known/caldav', '.well-known/carddav']);
expect(refused).toEqual([]);
});
// The one that matters most in that namespace: acme-challenge is how this machine gets its TLS
// certificates, and a plugin owning it could take the domain. Not an allow-list entry, so refused —
// to us as much as to anyone.
test('refuses a .well-known name that is not allow-listed, first-party or not', () => {
expect(resolvePluginRootPaths(withRoots('ours', ['/.well-known/acme-challenge']), new Set()).segments).toEqual([]);
expect(resolvePluginRootPaths(thirdParty('stranger', ['/.well-known/caldav']), new Set()).segments).toEqual([]);
});
test.each([['/a/b'], ['identity'], ['/Identity'], ['/']])('refuses the malformed segment %s', (root) => {
expect(resolvePluginRootPaths(withRoots('evil', [root]), new Set()).segments).toEqual([]);
});