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) => {