Support panel hosts that serve the page from their own scheme

Codex serves panels from codex-sandbox:// pages, where origins are
opaque: the panel recognizes the app's own addresses by scheme and host,
the vessels feed resolves against the document address, and the dev
server answers Codex's sandbox origins with CORS.
This commit is contained in:
Sameh Khamis committed 2026-10-02 15:21:37 -07:00
1 parent 64c1b1951f
commit b11d54d8b8
5 files changed
+16 -10

No files matched your search

+1
View File
@@ -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$/,
]);
/**
+3 -3
View File
@@ -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 <base>.
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,
+3 -3
View File
@@ -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 <base>.
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(),
+5
View File
@@ -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);
+4 -4
View File
@@ -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 <base>, 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) => {