From fc7609eb6bdfcbf6325a821982dca80df4c01820 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Wed, 20 May 2026 00:18:36 +0100 Subject: [PATCH 1/3] Specify browser demo handler sandbox task --- tasks/browser-demo-handler-sandbox.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tasks/browser-demo-handler-sandbox.md diff --git a/tasks/browser-demo-handler-sandbox.md b/tasks/browser-demo-handler-sandbox.md new file mode 100644 index 0000000..c47a8a6 --- /dev/null +++ b/tasks/browser-demo-handler-sandbox.md @@ -0,0 +1,20 @@ +--- +status: in-progress +size: medium +base_pr: 7 +--- + +Summary: Stacked follow-up to the browser demo page. Move editable handler execution out of the first-party page context while preserving the no-build static docs demo and request/response logging. + +- [ ] Run editable handler source in an isolated execution context instead of `Function(...)` in `docs/browser-demo.js`. +- [ ] Preserve support for handlers that return `Response`, string, `Blob`, `ReadableStream`, or `Uint8Array`. +- [ ] Preserve tunnel request logs, handler logs, and the built-in probe behavior. +- [ ] Surface compile/runtime errors clearly in the existing log and probe output UI. +- [ ] Keep the demo static and GitHub-Pages-friendly; do not add a bundler or app framework. +- [ ] Verify with syntax checks, repo checks, tests, and a local browser smoke where practical. + +## Assumptions + +- A dedicated Web Worker is enough isolation for this follow-up: it prevents handler code from touching the page DOM and local storage, while keeping web-standard `Request`/`Response` available. +- Stronger sandboxing, such as origin isolation or SES-style policy controls, can remain out of scope unless the static worker approach proves insufficient. +- This PR should use PR #7 as its base and stay focused on handler execution isolation. From efd78d8574443b3d54a7bfa8337561be4d3b06c7 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Wed, 20 May 2026 00:29:35 +0100 Subject: [PATCH 2/3] Run browser demo handlers in a worker --- docs/browser-demo-handler-worker.js | 140 +++++++++++++ docs/browser-demo.html | 4 +- docs/browser-demo.js | 193 ++++++++++++++---- tasks/browser-demo-handler-sandbox.md | 20 -- ...2026-05-19-browser-demo-handler-sandbox.md | 34 +++ 5 files changed, 331 insertions(+), 60 deletions(-) create mode 100644 docs/browser-demo-handler-worker.js delete mode 100644 tasks/browser-demo-handler-sandbox.md create mode 100644 tasks/complete/2026-05-19-browser-demo-handler-sandbox.md diff --git a/docs/browser-demo-handler-worker.js b/docs/browser-demo-handler-worker.js new file mode 100644 index 0000000..3240e04 --- /dev/null +++ b/docs/browser-demo-handler-worker.js @@ -0,0 +1,140 @@ +self.addEventListener("message", (event) => { + handleMessage(event.data).catch((error) => { + postError(error); + }); +}); + +async function handleMessage(message) { + if (!message || typeof message.type !== "string") { + throw new Error("Handler worker received an invalid message."); + } + + if (message.type === "validate") { + compileHandler(message.source); + self.postMessage({ type: "validated" }); + return; + } + + if (message.type === "fetch") { + const handler = compileHandler(message.source); + const request = deserializeRequest(message.request); + const response = await normalizeResponse(await handler(request, handlerContext(message))); + const serialized = await serializeResponse(response); + self.postMessage({ type: "result", response: serialized.response }, serialized.transfer); + return; + } + + throw new Error("Handler worker received an unknown message: " + message.type); +} + +function compileHandler(source) { + const handlerSource = source ? String(source).trim() : ""; + + if (!handlerSource) { + throw new Error("Handler source is empty."); + } + + const value = Function( + '"use strict";\nreturn (\n' + + handlerSource + + "\n);\n//# sourceURL=captun-browser-demo-handler-source.js", + )(); + + if (typeof value === "function") return value; + if (value && typeof value.fetch === "function") return value.fetch.bind(value); + + throw new Error("Handler source must evaluate to a function or an object with fetch()."); +} + +function deserializeRequest(request) { + const init = { + headers: request.headers, + method: request.method, + }; + + if (request.body !== null && typeof request.body !== "undefined") { + init.body = request.body; + } + + return new Request(request.url, init); +} + +function handlerContext(message) { + const context = message.context; + + return { + corsHeaders: context.corsHeaders, + log(value) { + self.postMessage({ + type: "handler-log", + message: String(value), + requestId: context.requestId, + }); + }, + publicUrl: context.publicUrl, + requestId: context.requestId, + tunnelName: context.tunnelName, + }; +} + +async function normalizeResponse(value) { + const resolved = await value; + const isReadableStream = + typeof ReadableStream === "function" && resolved instanceof ReadableStream; + + if (resolved instanceof Response) return resolved; + if ( + typeof resolved === "string" || + resolved instanceof Blob || + isReadableStream || + resolved instanceof Uint8Array + ) { + return new Response(resolved); + } + + throw new Error("Handler must return a Response, string, Blob, ReadableStream, or Uint8Array."); +} + +async function serializeResponse(response) { + if (response.type === "error") { + return { + response: { type: "error" }, + transfer: [], + }; + } + + const body = response.body ? await response.arrayBuffer() : null; + + return { + response: { + body, + headers: Array.from(response.headers.entries()), + status: response.status, + statusText: response.statusText, + type: "default", + }, + transfer: body ? [body] : [], + }; +} + +function postError(error) { + self.postMessage({ + type: "error", + error: serializeError(error), + }); +} + +function serializeError(error) { + if (error instanceof Error) { + return { + message: error.message, + name: error.name, + stack: error.stack, + }; + } + + return { + message: String(error), + name: "Error", + }; +} diff --git a/docs/browser-demo.html b/docs/browser-demo.html index 1637822..928943b 100644 --- a/docs/browser-demo.html +++ b/docs/browser-demo.html @@ -70,8 +70,8 @@

Fetch Handler

- Handler source runs as first-party JavaScript in this page and is saved locally. Only - run code you wrote or trust. + Handler source runs in an isolated Web Worker and is saved locally. Only run code you + wrote or trust.