Add regression tests for token-authorized CORS on non-localhost dev hosts

Covers the fix for the ddev breakage reported in #304: the live server
now reflects Access-Control-Allow-Origin for any request bearing the
valid session token, so dev servers on loopback aliases (https://*.ddev.site,
Valet's *.test, hosts-file entries) work again while tokenless remote
origins stay blocked. The server/browser source changes shipped in
b1c5707f; this adds the test coverage that was written alongside them:

- tokenless remote origins get no ACAO on any route, token'd or not
- a non-loopback origin with the valid token is reflected, with
  Vary: Origin, on both the real request and its OPTIONS preflight
- the /manual-edit-stash source assertion tracks the token-bearing URL

Prepared with AI assistance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-31 18:02:54 -07:00
co-authored by Claude Fable 5
parent f2f73edb33
commit 0f80c1f5aa
2 changed files with 27 additions and 5 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ describe('live-browser source contracts', () => {
);
assert.match(
SOURCE,
/fetch\('http:\/\/localhost:' \+ PORT \+ '\/manual-edit-stash'[\s\S]{0,260}?pageUrl: location\.pathname[\s\S]{0,80}?element: extractContext\(contextElement\)[\s\S]{0,40}?ops,/,
/fetch\('http:\/\/localhost:' \+ PORT \+ '\/manual-edit-stash\?token='[\s\S]{0,300}?pageUrl: location\.pathname[\s\S]{0,80}?element: extractContext\(contextElement\)[\s\S]{0,40}?ops,/,
'Save should stage edits through /manual-edit-stash with element context and ops',
);
assert.match(
+26 -4
View File
@@ -393,17 +393,17 @@ describe('live-server integration', () => {
assert.ok(body.includes('__IMPECCABLE_LIVE_INIT__'), 'authorized /live.js returns the assembled bundle');
});
it('CORS: a remote origin gets no Access-Control-Allow-Origin on any route', async () => {
it('CORS: a tokenless remote origin gets no Access-Control-Allow-Origin on any route', async () => {
const evil = 'https://evil.example';
for (const path of ['/health', `/live.js?token=${server.token}`, `/status?token=${server.token}`]) {
for (const path of ['/health', '/live.js', '/status', '/status?token=not-the-token']) {
const res = await fetch(`http://localhost:${server.port}${path}`, { headers: { Origin: evil } });
assert.equal(
res.headers.get('access-control-allow-origin'),
null,
`remote origin must not be reflected on ${path}`,
`tokenless remote origin must not be reflected on ${path}`,
);
}
// Preflight from a remote origin is likewise unauthorized to read.
// Preflight from a tokenless remote origin is likewise unauthorized to read.
const preflight = await fetch(`http://localhost:${server.port}/poll`, {
method: 'OPTIONS',
headers: { Origin: evil, 'Access-Control-Request-Method': 'POST' },
@@ -411,6 +411,28 @@ describe('live-server integration', () => {
assert.equal(preflight.headers.get('access-control-allow-origin'), null);
});
it('CORS: a non-loopback origin with the valid token is reflected (ddev-style dev hosts)', async () => {
// A dev server on a loopback alias (https://my-site.ddev.site, *.test)
// sends a non-loopback Origin, but its overlay requests carry the session
// token — that token, not the origin, is the trust signal.
const origin = 'https://my-site.ddev.site';
const res = await fetch(`http://localhost:${server.port}/status?token=${server.token}`, {
headers: { Origin: origin },
});
assert.equal(res.status, 200);
assert.equal(res.headers.get('access-control-allow-origin'), origin);
assert.ok(/\bOrigin\b/i.test(res.headers.get('vary') || ''), 'Vary: Origin accompanies the reflected origin');
// Preflight to the same token-bearing URL is authorized too (OPTIONS hits
// the same URL, query string included).
const preflight = await fetch(`http://localhost:${server.port}/poll?token=${server.token}`, {
method: 'OPTIONS',
headers: { Origin: origin, 'Access-Control-Request-Method': 'POST' },
});
assert.equal(preflight.status, 204);
assert.equal(preflight.headers.get('access-control-allow-origin'), origin);
});
it('CORS: a loopback origin is reflected with Vary: Origin', async () => {
for (const origin of [
`http://localhost:${server.port}`,