fix(html-view): answer the rendered page's CORS preflight

A sandboxed page is opaque-origin, so a fetch() of its own data file with
a non-safelisted header preflights with `Origin: null`. The auth hook
refused the OPTIONS (the html-view exemption admitted GET and HEAD only)
and, without a password, the security-headers hook answered a bare 204
with no Access-Control-Allow-Origin, so the fetch failed either way.

The exemption now admits OPTIONS for a live capability, and
registerSecurityHeaders answers it itself with Allow-Origin `*`,
Allow-Methods `GET, HEAD` and Allow-Headers `*` (no credentials are
involved). An unknown or revoked capability still gets the 401, and
writes on the capability path are still refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-13 17:12:45 +02:00
1 parent 4544379b3b
commit 06f1e5ddc8
3 files changed
+34 -3

No files matched your search

+19
View File
@@ -336,6 +336,25 @@ describe('the html-view exemption', () => {
}
});
it("answers the page's CORS preflight, read-only, and nothing else", async () => {
const preflight = (url: string) =>
app.inject({
method: 'OPTIONS',
url,
headers: { origin: 'null', 'access-control-request-method': 'GET', 'access-control-request-headers': 'x-req' },
});
const res = await preflight(`/html-view/${htmlCapability}/data.json`);
expect(res.statusCode).toBe(204);
expect(res.headers['access-control-allow-origin']).toBe('*');
expect(res.headers['access-control-allow-methods']).toBe('GET, HEAD');
expect(res.headers['access-control-allow-headers']).toBe('*');
expect(res.headers['access-control-allow-credentials']).toBeUndefined();
// An unknown or revoked capability is not a preflight anyone answers.
expect((await preflight('/html-view/AAAAAAAAAAAAAAAAAAAAAAAA/data.json')).statusCode).toBe(401);
htmlViewCapabilities.revokeOwner(undefined);
expect((await preflight(`/html-view/${htmlCapability}/data.json`)).statusCode).toBe(401);
});
it('never exempts a write on the capability path', async () => {
for (const method of ['POST', 'PUT'] as const) {
const res = await app.inject({ method, url: `/html-view/${htmlCapability}/index.html`, payload: {} });