Skip to content

Commit 2fdff81

Browse files
koala73e
andauthored
fix(cors): allow eliewm team-scope Vercel previews across all CORS surfaces (#4248)
* fix(cors): allow eliewm team-scope Vercel previews Preview deployments moved from the personal Vercel scope (worldmonitor-*-elie-<hash>) to the "eliewm" team scope (worldmonitor-git-<branch>-eliewm.vercel.app and worldmonitor-<hash>-eliewm.vercel.app). Both CORS twins still pinned the dead personal-scope pattern, so the browser's Origin header on POST /api/wm-session was rejected by isDisallowedOrigin — the anonymous session could never be minted and every session-authed call 401'd on previews (dashboard + /welcome teasers stayed dark). Replace the dead pattern with the eliewm team-scope pattern in server/cors.ts and api/_cors.js, kept tight (no bare *.vercel.app, this is a security allowlist). Add isAllowedOrigin tests covering the git-branch alias URL, hash deployment URL, and non-worldmonitor rejection against both twins, plus a source twin-parity guard, and update the gateway CDN origin-policy preview origin to eliewm. * fix(cors): extend eliewm preview scope to the Cloudflare Worker + guard the superset invariant The api-cors-preflight Cloudflare Worker is the third CORS twin and short- circuits OPTIONS preflights at the edge, so its allowlist must be a superset of api/_cors.js. PR #4248 moved the functions to the eliewm team scope but left the Worker pinned to the dead -elie- personal scope — making the Worker strictly NARROWER than the functions. Result: eliewm preview preflights echo the canonical worldmonitor.app fallback and the browser blocks the request before it ever reaches Vercel, so previews stay dark even though the function would accept the origin. Worse, this was invisible to CI: the Worker's own test lives outside the test:data glob (it only runs in deploy-worker.yml on workers/** changes), and both the Worker and its test still pinned the old literal, so they were internally consistent and green while drifting from the function. - workers/api-cors-preflight/src/index.js: replace the dead -elie- pattern with the eliewm team-scope pattern (kept tight, no bare *.vercel.app). - workers/api-cors-preflight/index.test.mjs: update the preview-deploy assertions to the eliewm shape (git-branch alias + hash), and assert foreign team / non-worldmonitor / retired personal scope stay rejected. - tests/cors-fail-closed.test.mts: add the Worker to the source twin-parity guard (now a three-twin check) AND a gate-resident behavioral superset test that imports both the function and Worker predicates and asserts the Worker allows every origin the function allows. This always-run check is what catches function<->Worker drift (the gap that left this bug invisible). * fix(widget): allow eliewm team-scope previews to mount PRO widgets The widget postMessage sandbox (public/wm-widget-sandbox.html) gated parent origins on a stale Vercel hostname shape — `worldmonitor-<branch>-<team>-<hash>` with the team slug followed by a trailing hash — and slug `elie`. Real Vercel preview URLs put the team scope as the LAST segment (worldmonitor-git-<branch>-eliewm.vercel.app / worldmonitor-<hash>-eliewm.vercel.app), so the regex could never match an eliewm preview regardless of the slug value. Net effect: PRO widgets stayed dark on every preview deployment. Fix the regex shape to treat the team slug as the final hostname segment and set the slug to `eliewm`, matching the same tight shape now used by server/cors.ts, api/_cors.js, and the api-cors-preflight Worker (still no bare *.vercel.app — the team-slug gate remains the load-bearing invariant). Update tests/widget-builder.test.mjs: the hand-mirrored matcher shape, the slug assertion, the vm-driven sandbox behavior test, and the look-alike rejections (foreign team, suffix spoof, xeliewm prefix collision, and the retired personal scope worldmonitor-*-elie-<hash>). --------- Co-authored-by: e <e@e.co>
1 parent f6d652d commit 2fdff81

8 files changed

Lines changed: 177 additions & 42 deletions

File tree

api/_cors.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const ALLOWED_ORIGIN_PATTERNS = [
22
/^https:\/\/(.*\.)?worldmonitor\.app$/,
3-
/^https:\/\/worldmonitor-[a-z0-9-]+-elie-[a-z0-9]+\.vercel\.app$/,
3+
// Vercel preview deployments under the "eliewm" team scope, e.g.
4+
// worldmonitor-git-<branch>-eliewm.vercel.app (git-branch alias)
5+
// worldmonitor-<hash>-eliewm.vercel.app (deployment URL)
6+
// Tight on purpose: never a bare *.vercel.app (this is a security allowlist).
7+
/^https:\/\/worldmonitor-[a-z0-9-]+-eliewm\.vercel\.app$/,
48
/^https?:\/\/tauri\.localhost(:\d+)?$/,
59
/^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i,
610
/^tauri:\/\/localhost$/,

public/wm-widget-sandbox.html

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@
1111
var handled = false;
1212

1313
// Vercel preview hostnames have the shape
14-
// {project-slug}-{branch-slug}-{team-slug}-{hash}.vercel.app
15-
// The team-slug segment is the URL-safe Vercel team identifier and is the
14+
// worldmonitor-git-{branch-slug}-{team-slug}.vercel.app (git-branch alias)
15+
// worldmonitor-{hash}-{team-slug}.vercel.app (deployment URL)
16+
// The team-slug is the LAST segment before .vercel.app and is the
1617
// load-bearing security invariant: an attacker who registers a Vercel
1718
// project named `worldmonitor-...` under their OWN team account would get
1819
// a different team-slug, so gating on this list rejects look-alike previews.
19-
// Add a teammate's Vercel team slug here when they need to mount PRO
20-
// widgets in preview deployments; never widen this to a wildcard.
21-
var ALLOWED_VERCEL_TEAM_SLUGS = ['elie'];
20+
// The project deploys under the "eliewm" team scope. Add a teammate's Vercel
21+
// team slug here when they need to mount PRO widgets in preview deployments;
22+
// never widen this to a wildcard.
23+
var ALLOWED_VERCEL_TEAM_SLUGS = ['eliewm'];
2224

2325
function isAllowedVercelPreview(hostname) {
2426
for (var i = 0; i < ALLOWED_VERCEL_TEAM_SLUGS.length; i++) {
2527
var team = ALLOWED_VERCEL_TEAM_SLUGS[i];
2628
if (!/^[a-z0-9-]+$/.test(team)) continue;
27-
var re = new RegExp('^worldmonitor-[a-z0-9-]+-' + team + '-[a-z0-9]+\\.vercel\\.app$');
29+
var re = new RegExp('^worldmonitor-[a-z0-9-]+-' + team + '\\.vercel\\.app$');
2830
if (re.test(hostname)) return true;
2931
}
3032
return false;

server/cors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
const PRODUCTION_PATTERNS: RegExp[] = [
99
/^https:\/\/(.*\.)?worldmonitor\.app$/,
10-
/^https:\/\/worldmonitor-[a-z0-9-]+-elie-[a-z0-9]+\.vercel\.app$/,
10+
// Vercel preview deployments under the "eliewm" team scope, e.g.
11+
// worldmonitor-git-<branch>-eliewm.vercel.app (git-branch alias)
12+
// worldmonitor-<hash>-eliewm.vercel.app (deployment URL)
13+
// Tight on purpose: never a bare *.vercel.app (this is a security allowlist).
14+
/^https:\/\/worldmonitor-[a-z0-9-]+-eliewm\.vercel\.app$/,
1115
/^https?:\/\/tauri\.localhost(:\d+)?$/,
1216
/^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i,
1317
/^tauri:\/\/localhost$/,

tests/cors-fail-closed.test.mts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict';
22
import { readFile } from 'node:fs/promises';
33
import { describe, it } from 'node:test';
44

5-
import { getCorsHeaders } from '../server/cors.ts';
5+
import { getCorsHeaders, isAllowedOrigin } from '../server/cors.ts';
6+
import { isDisallowedOrigin as isDisallowedOriginJs } from '../api/_cors.js';
7+
import { isAllowedOrigin as isAllowedOriginWorker } from '../workers/api-cors-preflight/src/index.js';
68

79
// Regression coverage for issue #3705: CORS-header generation errors must
810
// fail closed rather than fall back to a wildcard ACAO.
@@ -43,6 +45,122 @@ describe('cors helper', () => {
4345
});
4446
});
4547

48+
// The Vercel project moved from the personal scope (worldmonitor-*-elie-<hash>)
49+
// to the "eliewm" team scope. Browsers send Origin on the POST to
50+
// /api/wm-session, so a stale allowlist 403s every preview deployment and the
51+
// anonymous session can never be minted (dashboard + /welcome teasers stay dark).
52+
describe('isAllowedOrigin — Vercel preview allowlist (eliewm team scope)', () => {
53+
// Origin for the JS twin (api/_cors.js exports isDisallowedOrigin, not the
54+
// bare predicate) — same allow/deny outcome proves both files stay in sync.
55+
const allowedByJsTwin = (origin: string) =>
56+
!isDisallowedOriginJs(new Request('https://worldmonitor.app/x', { headers: { Origin: origin } }));
57+
58+
const ALLOWED = [
59+
['git-branch alias URL', 'https://worldmonitor-git-feature-eliewm.vercel.app'],
60+
['hash deployment URL', 'https://worldmonitor-abc123def456-eliewm.vercel.app'],
61+
['apex production origin', 'https://worldmonitor.app'],
62+
['production subdomain', 'https://tech.worldmonitor.app'],
63+
];
64+
65+
const REJECTED = [
66+
['non-worldmonitor vercel.app origin', 'https://some-other-app-eliewm.vercel.app'],
67+
['foreign team scope', 'https://worldmonitor-git-feature-attacker.vercel.app'],
68+
['bare worldmonitor vercel.app (no scope segment)', 'https://worldmonitor.vercel.app'],
69+
['suffix-spoofed eliewm origin', 'https://worldmonitor-git-feature-eliewm.vercel.app.evil.com'],
70+
['dead personal-scope preview (post-migration)', 'https://worldmonitor-feature-elie-abc123.vercel.app'],
71+
];
72+
73+
for (const [label, origin] of ALLOWED) {
74+
it(`allows ${label}`, () => {
75+
assert.equal(isAllowedOrigin(origin), true, `server/cors.ts must allow ${origin}`);
76+
assert.equal(allowedByJsTwin(origin), true, `api/_cors.js must allow ${origin}`);
77+
});
78+
}
79+
80+
for (const [label, origin] of REJECTED) {
81+
it(`rejects ${label}`, () => {
82+
assert.equal(isAllowedOrigin(origin), false, `server/cors.ts must reject ${origin}`);
83+
assert.equal(allowedByJsTwin(origin), false, `api/_cors.js must reject ${origin}`);
84+
});
85+
}
86+
});
87+
88+
describe('CORS triplet parity — eliewm preview pattern stays tight in all three twins', () => {
89+
// Root cause of the original 403s was twins drifting. THREE surfaces gate
90+
// Vercel-preview CORS and must move together; guard each for:
91+
// (1) the eliewm-scoped preview pattern is present, and
92+
// (2) no bare *.vercel.app wildcard sneaks in as a "fix".
93+
// The Cloudflare Worker is the load-bearing one: it short-circuits OPTIONS at
94+
// the edge, so if it drifts narrower the browser blocks the preflight before
95+
// Vercel is ever consulted.
96+
const TWINS = [
97+
'../server/cors.ts',
98+
'../api/_cors.js',
99+
'../workers/api-cors-preflight/src/index.js',
100+
];
101+
102+
for (const rel of TWINS) {
103+
it(`${rel} scopes Vercel previews to the eliewm team`, async () => {
104+
const source = await readFile(new URL(rel, import.meta.url), 'utf8');
105+
assert.ok(
106+
source.includes('-eliewm\\.vercel\\.app'),
107+
`${rel} must allow worldmonitor-*-eliewm.vercel.app previews`,
108+
);
109+
assert.ok(
110+
!source.includes('worldmonitor-[a-z0-9-]+\\.vercel\\.app'),
111+
`${rel} must not widen to a bare *.vercel.app wildcard (security allowlist)`,
112+
);
113+
});
114+
}
115+
});
116+
117+
describe('CORS Worker superset invariant — edge allowlist ⊇ function allowlist', () => {
118+
// The api-cors-preflight Worker (workers/api-cors-preflight) short-circuits
119+
// OPTIONS preflights at the edge, so its allowlist MUST be a superset of
120+
// api/_cors.js. If the Worker rejects an origin the function would accept,
121+
// the preflight echoes the canonical worldmonitor.app fallback and the
122+
// browser blocks the request before it reaches Vercel.
123+
//
124+
// The Worker's own test (workers/api-cors-preflight/index.test.mjs) lives
125+
// OUTSIDE the test:data glob and only runs in deploy-worker.yml on
126+
// workers/** changes — so a function-only change can silently leave the
127+
// Worker narrower. THIS gate-resident check is what actually catches
128+
// function↔Worker drift (the bug that left eliewm previews dark).
129+
//
130+
// Localhost/127 are intentionally omitted: they are DEV-only on the function
131+
// side (NODE_ENV-gated) and never reach the prod-only Worker.
132+
const fnAllows = (origin: string) =>
133+
!isDisallowedOriginJs(new Request('https://worldmonitor.app/x', { headers: { Origin: origin } }));
134+
135+
const PROD_ORIGINS = [
136+
'https://worldmonitor.app',
137+
'https://www.worldmonitor.app',
138+
'https://tech.worldmonitor.app',
139+
'https://worldmonitor-git-feature-eliewm.vercel.app',
140+
'https://worldmonitor-abc123def456-eliewm.vercel.app',
141+
'tauri://localhost',
142+
'asset://localhost',
143+
// Negatives — the function rejects these, so the superset assertion is a
144+
// no-op for them; included to document the boundary.
145+
'https://some-other-app-eliewm.vercel.app',
146+
'https://worldmonitor-git-feature-attacker.vercel.app',
147+
'https://worldmonitor-feature-elie-abc123.vercel.app',
148+
'https://evil.com',
149+
];
150+
151+
for (const origin of PROD_ORIGINS) {
152+
it(`Worker allows everything the function allows: ${origin}`, () => {
153+
if (fnAllows(origin)) {
154+
assert.equal(
155+
isAllowedOriginWorker(origin),
156+
true,
157+
`Worker rejects ${origin} that api/_cors.js accepts — its OPTIONS preflight will echo the worldmonitor.app fallback and the browser will block it`,
158+
);
159+
}
160+
});
161+
}
162+
});
163+
46164
describe('gateway CORS error path (issue #3705)', () => {
47165
it('does not contain a wildcard ACAO fallback in source (comments stripped)', async () => {
48166
const source = await readFile(

tests/gateway-cdn-origin-policy.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('gateway CDN origin policy', () => {
6565
});
6666

6767
it('enables CDN caching for preview origins', async () => {
68-
const origin = 'https://worldmonitor-feature-elie-abc123.vercel.app';
68+
const origin = 'https://worldmonitor-git-feature-eliewm.vercel.app';
6969
const res = await requestPublicRoute(origin);
7070
assert.equal(res.status, 200);
7171
assert.equal(res.headers.get('Access-Control-Allow-Origin'), origin);

tests/widget-builder.test.mjs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,31 +1289,37 @@ describe('PRO widget — store and sanitizer', () => {
12891289
.split(',')
12901290
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
12911291
.filter(Boolean);
1292-
assert.ok(slugs.includes('elie'), 'project-owner team slug must remain in the allowlist');
1292+
assert.ok(slugs.includes('eliewm'), 'project-owner team slug must remain in the allowlist');
12931293
for (const slug of slugs) {
12941294
assert.match(slug, /^[a-z0-9-]+$/, `team slug "${slug}" must be url-safe`);
12951295
}
12961296
// Reconstruct the actual match function and exercise it against the
12971297
// production slug list — this is what protects against regex drift
1298-
// when teammate slugs are added later.
1298+
// when teammate slugs are added later. The team slug is the LAST hostname
1299+
// segment before .vercel.app (eliewm team scope); keep this shape in lock-
1300+
// step with isAllowedVercelPreview in public/wm-widget-sandbox.html.
12991301
const matchesAllowedTeam = (hostname) =>
13001302
slugs.some((team) =>
1301-
new RegExp('^worldmonitor-[a-z0-9-]+-' + team + '-[a-z0-9]+\\.vercel\\.app$').test(hostname),
1303+
new RegExp('^worldmonitor-[a-z0-9-]+-' + team + '\\.vercel\\.app$').test(hostname),
13021304
);
1303-
assert.equal(matchesAllowedTeam('worldmonitor-feature-elie-abc123.vercel.app'), true);
1304-
assert.equal(matchesAllowedTeam('worldmonitor-feature-attacker-abc123.vercel.app'), false);
1305-
assert.equal(matchesAllowedTeam('worldmonitor-feature-elie-abc123.vercel.app.evil.com'), false);
1305+
assert.equal(matchesAllowedTeam('worldmonitor-git-feature-eliewm.vercel.app'), true);
1306+
assert.equal(matchesAllowedTeam('worldmonitor-abc123-eliewm.vercel.app'), true);
1307+
assert.equal(matchesAllowedTeam('worldmonitor-feature-attacker.vercel.app'), false);
1308+
assert.equal(matchesAllowedTeam('worldmonitor-git-feature-eliewm.vercel.app.evil.com'), false);
1309+
assert.equal(matchesAllowedTeam('worldmonitor-feature-xeliewm.vercel.app'), false);
13061310
assert.equal(matchesAllowedTeam('evilworldmonitor.app'), false);
1311+
// The retired personal scope (worldmonitor-*-elie-<hash>) must no longer match.
1312+
assert.equal(matchesAllowedTeam('worldmonitor-feature-elie-abc123.vercel.app'), false);
13071313
// A teammate slug added to the list must extend coverage WITHOUT
13081314
// matching look-alike teams whose slug merely starts with the same
13091315
// letters.
1310-
const withTeammate = ['elie', 'kieran'];
1316+
const withTeammate = ['eliewm', 'kieran'];
13111317
const matchesWithTeammate = (hostname) =>
13121318
withTeammate.some((team) =>
1313-
new RegExp('^worldmonitor-[a-z0-9-]+-' + team + '-[a-z0-9]+\\.vercel\\.app$').test(hostname),
1319+
new RegExp('^worldmonitor-[a-z0-9-]+-' + team + '\\.vercel\\.app$').test(hostname),
13141320
);
1315-
assert.equal(matchesWithTeammate('worldmonitor-feature-kieran-abc123.vercel.app'), true);
1316-
assert.equal(matchesWithTeammate('worldmonitor-feature-kieranfake-abc123.vercel.app'), false);
1321+
assert.equal(matchesWithTeammate('worldmonitor-feature-kieran.vercel.app'), true);
1322+
assert.equal(matchesWithTeammate('worldmonitor-feature-kieranfake.vercel.app'), false);
13171323
});
13181324

13191325
it('widget sandbox behavior accepts Vercel previews and blocks spoofed parents', () => {
@@ -1354,26 +1360,26 @@ describe('PRO widget — store and sanitizer', () => {
13541360
return { parent, readyMessages, writes, message };
13551361
}
13561362

1357-
const allowed = runSandbox('https://worldmonitor-feature-elie-abc123.vercel.app/dashboard');
1363+
const allowed = runSandbox('https://worldmonitor-git-feature-eliewm.vercel.app/dashboard');
13581364
assert.equal(allowed.readyMessages.length, 1);
1359-
assert.equal(allowed.readyMessages[0].targetOrigin, 'https://worldmonitor-feature-elie-abc123.vercel.app');
1365+
assert.equal(allowed.readyMessages[0].targetOrigin, 'https://worldmonitor-git-feature-eliewm.vercel.app');
13601366
assert.deepEqual(JSON.parse(JSON.stringify(allowed.readyMessages[0].payload)), {
13611367
type: 'wm-widget-ready',
13621368
id: 'wm-1',
13631369
token: 'test-token',
13641370
});
13651371
allowed.message({
13661372
data: { type: 'wm-html', id: 'wm-1', token: 'test-token', html: '<p>ok</p>' },
1367-
origin: 'https://worldmonitor-feature-elie-abc123.vercel.app',
1373+
origin: 'https://worldmonitor-git-feature-eliewm.vercel.app',
13681374
source: allowed.parent,
13691375
});
13701376
assert.deepEqual(allowed.writes, ['<p>ok</p>']);
13711377

1372-
const spoofed = runSandbox('https://worldmonitor-feature-elie-abc123.vercel.app.evil.com/');
1378+
const spoofed = runSandbox('https://worldmonitor-git-feature-eliewm.vercel.app.evil.com/');
13731379
assert.deepEqual(spoofed.readyMessages, []);
13741380
spoofed.message({
13751381
data: { type: 'wm-html', id: 'wm-1', token: 'test-token', html: '<p>bad</p>' },
1376-
origin: 'https://worldmonitor-feature-elie-abc123.vercel.app.evil.com',
1382+
origin: 'https://worldmonitor-git-feature-eliewm.vercel.app.evil.com',
13771383
source: spoofed.parent,
13781384
});
13791385
assert.deepEqual(spoofed.writes, []);

workers/api-cors-preflight/index.test.mjs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,20 @@ test('isAllowedOrigin accepts apex worldmonitor.app and subdomains', () => {
4646
assert.equal(isAllowedOrigin('https://commodity.worldmonitor.app'), true);
4747
});
4848

49-
test('isAllowedOrigin accepts Vercel preview deploys (mirroring api/_cors.js shape)', () => {
50-
// The Worker mirrors api/_cors.js's preview regex EXACTLY. That regex
51-
// requires [a-z0-9]+ (no hyphens) after `-elie-`, so `*-elie-habib.vercel.app`
52-
// matches and `*-elie-habib-projects.vercel.app` does NOT. That looks like
53-
// a latent bug in api/_cors.js for teams named `elie-habib-projects`, but
54-
// tightening it is out of scope for this PR — the Worker must stay in lock-
55-
// step with the function, not silently widen.
56-
assert.equal(isAllowedOrigin('https://worldmonitor-r6q9o-elie-habib.vercel.app'), true);
57-
assert.equal(isAllowedOrigin('https://worldmonitor-git-feat-x-elie-habib.vercel.app'), true);
58-
// NOTE: assertion below documents the latent narrowness — flip to `true`
59-
// ONLY after updating both the Worker AND api/_cors.js together.
60-
assert.equal(
61-
isAllowedOrigin('https://worldmonitor-abc-elie-habib-projects.vercel.app'),
62-
false,
63-
'Worker preview regex mirrors api/_cors.js; widen BOTH together if needed',
64-
);
49+
test('isAllowedOrigin accepts Vercel preview deploys under the eliewm team scope (mirrors api/_cors.js)', () => {
50+
// The project deploys previews under the "eliewm" Vercel team scope, so URLs
51+
// end in `-eliewm.vercel.app` (git-branch alias AND hash deployment forms).
52+
// The Worker MUST mirror api/_cors.js exactly — if it stays narrower, eliewm
53+
// preview preflights echo the canonical worldmonitor.app fallback and the
54+
// browser blocks them before the request ever reaches Vercel.
55+
assert.equal(isAllowedOrigin('https://worldmonitor-git-feat-x-eliewm.vercel.app'), true);
56+
assert.equal(isAllowedOrigin('https://worldmonitor-r6q9o-eliewm.vercel.app'), true);
57+
// Tight allowlist: a foreign team scope, a non-worldmonitor app, and the
58+
// retired personal scope (worldmonitor-*-elie-<hash>, migration complete)
59+
// must all stay rejected. Never a bare *.vercel.app.
60+
assert.equal(isAllowedOrigin('https://worldmonitor-feat-x-attacker.vercel.app'), false);
61+
assert.equal(isAllowedOrigin('https://some-other-app-eliewm.vercel.app'), false);
62+
assert.equal(isAllowedOrigin('https://worldmonitor-abc-elie-habib.vercel.app'), false);
6563
});
6664

6765
test('isAllowedOrigin accepts Tauri desktop runtime origins', () => {

workers/api-cors-preflight/src/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
// echoed back and fail CORS at the browser.
2525
const ALLOWED_ORIGIN_PATTERNS = [
2626
/^https:\/\/(.*\.)?worldmonitor\.app$/,
27-
/^https:\/\/worldmonitor-[a-z0-9-]+-elie-[a-z0-9]+\.vercel\.app$/,
27+
// Vercel previews under the "eliewm" team scope, e.g.
28+
// worldmonitor-git-<branch>-eliewm.vercel.app / worldmonitor-<hash>-eliewm.vercel.app
29+
// Mirror of api/_cors.js + server/cors.ts (see superset note above).
30+
/^https:\/\/worldmonitor-[a-z0-9-]+-eliewm\.vercel\.app$/,
2831
/^https?:\/\/tauri\.localhost(:\d+)?$/,
2932
/^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i,
3033
/^tauri:\/\/localhost$/,

0 commit comments

Comments
 (0)