openclaw/scripts/control-ui-mock-isolation.ts
Vyctor H. Brzezowski 58b894d3e2
chore: refresh Control UI mock fixtures (#138350)
* chore: refresh control ui mock fixtures

* fix: align Control UI mock task status

* fix: keep Control UI mock actions and visual fixtures consistent

* fix: align mock fixture helpers with lint contracts

* fix(ui): stabilize standalone mock worker and isolation proof
2026-09-08 15:02:29 -03:00

153 lines
5.9 KiB
TypeScript

import type { Plugin } from "vite";
/** Runs before preview and app modules; the preview owns Gateway selection. */
function installStandaloneNetworkBoundary(): void {
const nativeFetch = window.fetch.bind(window);
const localOrigin = window.location.origin;
const isLocalResource = (url: URL) =>
url.origin === localOrigin || url.protocol === "data:" || url.protocol === "blob:";
const blocked = (capability: string) =>
new DOMException(
`Standalone mock blocked ${capability}. Add a local fixture or use pnpm ui:dev for a real Gateway.`,
"NotSupportedError",
);
window.fetch = async (input, init) => {
const request = new Request(input, init);
const url = new URL(request.url);
if (!isLocalResource(url)) {
throw blocked(`fetch to ${url.origin}`);
}
// A local resource must not redirect an authenticated request off the fixture server.
return nativeFetch(request, { redirect: "error" });
};
// Full Chromium preconnects iframe destinations before checking frame-src.
// Reject URL writes before navigation starts; CSP still guards frame contents.
const checkFrameUrl = (value: string) => {
const url = new URL(value, window.location.href);
if (!isLocalResource(url) && url.href !== "about:blank") {
throw blocked(`iframe to ${url.origin}`);
}
};
const framePrototype = HTMLIFrameElement.prototype;
const frameSrc = Object.getOwnPropertyDescriptor(framePrototype, "src");
if (!frameSrc?.set) {
throw blocked("unsupported iframe implementation");
}
Object.defineProperty(framePrototype, "src", {
...frameSrc,
set(value: string) {
checkFrameUrl(value);
frameSrc.set!.call(this, value);
},
});
framePrototype.setAttribute = function (name, value) {
if (name.toLowerCase() === "src") {
checkFrameUrl(value);
}
Element.prototype.setAttribute.call(this, name, value);
};
framePrototype.setAttributeNS = function (namespace, name, value) {
if (!namespace && name.toLowerCase() === "src") {
checkFrameUrl(value);
}
Element.prototype.setAttributeNS.call(this, namespace, name, value);
};
// CSP covers resource loads, not native RTC or top-level navigation. Keep
// these side effects out of standalone demos, including deferred blank popups.
for (const capability of ["RTCPeerConnection", "webkitRTCPeerConnection", "WebTransport"]) {
function blockedTransport(): never {
throw blocked(capability);
}
Object.defineProperty(window, capability, { value: blockedTransport });
}
window.open = () => {
console.warn(blocked("popup").message);
return null;
};
window.navigation?.addEventListener("navigate", (event) => {
if (new URL(event.destination.url).origin !== localOrigin) {
event.preventDefault();
console.warn(blocked("external navigation").message);
}
});
for (const eventName of ["click", "auxclick"]) {
window.addEventListener(
eventName,
(event) => {
const anchor = event.composedPath().find((node) => node instanceof HTMLAnchorElement);
const url = anchor ? new URL(anchor.href) : null;
const localDownload =
anchor?.download && (url?.protocol === "blob:" || url?.protocol === "data:");
if (url && url.origin !== localOrigin && !localDownload) {
event.preventDefault();
console.warn(blocked("external link").message);
}
},
true,
);
}
}
export function createStandaloneMockIsolationPlugins(): Plugin[] {
// tsx can emit __name calls in serialized functions; keep that helper local
// to the injected script, just like the shared Gateway mock initializer.
const script = `(() => { const __name = (target) => target; (${installStandaloneNetworkBoundary.toString()})(); })();`;
return [
{
name: "openclaw-control-ui-mock-isolation",
enforce: "pre",
configureServer(server) {
// Install before *all* custom fixture handlers, not only Vite's HTML
// handler: frame/attachment documents need the same native boundary.
server.middlewares.use((req, res, next) => {
const origin = new URL(`http://${req.headers.host}`).origin;
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
`connect-src 'self' ${origin.replace(/^http:/, "ws:")} blob: data:`,
// The bundled terminal compiles local Wasm; JavaScript eval stays disabled.
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data:",
"media-src 'self' blob: data:",
"font-src 'self' data:",
"frame-src 'self' blob: data:",
"worker-src 'self'",
"object-src 'none'",
"base-uri 'none'",
"form-action 'none'",
"sandbox allow-scripts allow-same-origin allow-downloads allow-forms",
].join("; "),
);
res.setHeader("X-DNS-Prefetch-Control", "off");
next();
});
},
transformIndexHtml: {
order: "pre",
handler: () => [{ tag: "script", children: script, injectTo: "head-prepend" }],
},
},
{
name: "openclaw-control-ui-mock-http-misses",
enforce: "post",
configureServer(server) {
// Registered after fixture handlers but before Vite's SPA fallback.
server.middlewares.use((req, res, next) => {
const pathname = new URL(req.url ?? "/", "http://mock.invalid").pathname;
if (!/^\/(?:api|avatar|__openclaw__|__fixtures)\//.test(pathname)) {
next();
return;
}
res.statusCode = 404;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ error: "Standalone mock has no HTTP fixture for this route." }));
});
},
},
];
}