diff --git a/build/vite.js b/build/vite.js index 696c740de..53b7e92e2 100644 --- a/build/vite.js +++ b/build/vite.js @@ -6,6 +6,7 @@ import { embedFramingPlugin } from './embed-framing.js'; export const PANEL_CORS_ORIGINS = Object.freeze([ /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/, /^https:\/\/[a-z0-9]+\.claudemcpcontent\.com$/, + /^(?:https|codex-sandbox):\/\/[a-z0-9-]+\.web-sandbox\.oaiusercontent\.com$/, ]); /** diff --git a/src/data/aisLiveVessels.js b/src/data/aisLiveVessels.js index 320318742..b3800db68 100644 --- a/src/data/aisLiveVessels.js +++ b/src/data/aisLiveVessels.js @@ -5,9 +5,9 @@ import { createAisStreamSource } from '../sources/live/standalone.js'; const aisLiveVesselsLayer = createApplicationVessels({ source: createAisStreamSource({ apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live', - // The app's server, also when a panel page loads the app under . - origin: () => - new URL(globalThis.document?.baseURI ?? 'http://localhost').origin, + // Resolved against the document's address, which a panel host may serve + // from its own scheme. + origin: () => globalThis.document?.baseURI ?? 'http://localhost', }), options: { maxRows: import.meta.env?.VITE_AIS_LIVE_MAX_ROWS, diff --git a/src/standalone/layerSources.js b/src/standalone/layerSources.js index e51e64530..c84595483 100644 --- a/src/standalone/layerSources.js +++ b/src/standalone/layerSources.js @@ -29,9 +29,9 @@ export function createStandaloneLayerSources() { military: createAdsbLolSource(), vessels: createAisStreamSource({ apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live', - // The app's server, also when a panel page loads the app under . - origin: () => - new URL(globalThis.document?.baseURI ?? 'http://localhost').origin, + // Resolved against the document's address, which a panel host may + // serve from its own scheme. + origin: () => globalThis.document?.baseURI ?? 'http://localhost', }), cctv: createCctvSource(), radio: createRadioSource(), diff --git a/src/tooling/viteBuild.test.mjs b/src/tooling/viteBuild.test.mjs index f2c0c586a..6550c5235 100644 --- a/src/tooling/viteBuild.test.mjs +++ b/src/tooling/viteBuild.test.mjs @@ -72,6 +72,11 @@ test('panel origins may read every dev server path, other sites may not', () => request('https://abc123.claudemcpcontent.com', 'OPTIONS').ended, true, ); + assert.equal( + request('codex-sandbox://mcp-app-ab12.web-sandbox.oaiusercontent.com') + .headers['Access-Control-Allow-Origin'], + 'codex-sandbox://mcp-app-ab12.web-sandbox.oaiusercontent.com', + ); const other = request('https://example.com'); assert.deepEqual(other.headers, {}); assert.equal(other.passed, true); diff --git a/src/tools/globePanel.js b/src/tools/globePanel.js index a3412baaa..7f0a29138 100644 --- a/src/tools/globePanel.js +++ b/src/tools/globePanel.js @@ -99,8 +99,9 @@ function panelHtml(appOrigin) { // The panel page lives on the host's site, which serves none of the app. // Hosts may refuse , so the app's own addresses (relative ones, and // absolute ones on this page's site) are sent to the app's server instead. - // The page's own site; location.origin is "null" in a srcdoc document. - const PAGE_ORIGIN = new URL(document.baseURI).origin; + // The page's own site, by scheme and host: location.origin is "null" in a + // srcdoc document, and a host may serve the page from its own scheme. + const PAGE = new URL(document.baseURI); const toApp = (value) => { let url; try { @@ -108,8 +109,7 @@ function panelHtml(appOrigin) { } catch { return value; } - if (!/^https?:$/.test(url.protocol) || url.origin !== PAGE_ORIGIN) - return value; + if (url.protocol !== PAGE.protocol || url.host !== PAGE.host) return value; return APP_ORIGIN + url.pathname + url.search + url.hash; }; const isRemote = (value) => {