sandboxedjs 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,25 @@ var PreviewRouter = class {
15
15
  this.connected = true;
16
16
  }
17
17
  /** Read a port out of a claim path, or null when this is not one. */
18
+ /**
19
+ * The path to ask the container for, given a request from a bound client.
20
+ *
21
+ * A document served at `/…/__sbx__/3000/` resolves its *relative* assets
22
+ * against that prefix, so `./app.js` arrives as
23
+ * `/…/__sbx__/3000/app.js`. Forwarding that verbatim asks the container for
24
+ * a file inside a directory it has never heard of, and every relative asset
25
+ * in the site 404s while the document itself loads perfectly — a page that
26
+ * renders unstyled and inert, with nothing to explain why.
27
+ *
28
+ * Root-relative assets (`/main.js`) carry no prefix and pass through
29
+ * unchanged, which is the case that always worked.
30
+ */
31
+ containerPath(pathname, search) {
32
+ const match = CLAIM.exec(pathname);
33
+ const rest = match ? pathname.slice(match.index + match[0].length) : pathname;
34
+ const normalised = rest.startsWith("/") ? rest : `/${rest}`;
35
+ return `${normalised}${search}`;
36
+ }
18
37
  claimedPort(pathname) {
19
38
  const match = CLAIM.exec(pathname);
20
39
  return match ? Number(match[1]) : null;
@@ -106,7 +125,9 @@ worker.addEventListener("fetch", (event) => {
106
125
  }
107
126
  const port = router.portFor(event.clientId);
108
127
  if (port === void 0) return;
109
- event.respondWith(router.fetch(port, url.pathname + url.search, event.request));
128
+ event.respondWith(
129
+ router.fetch(port, router.containerPath(url.pathname, url.search), event.request)
130
+ );
110
131
  });
111
132
  //# sourceMappingURL=service-worker.js.map
112
133
  //# sourceMappingURL=service-worker.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/preview/router.ts","../src/preview/service-worker.ts"],"names":[],"mappings":";AA2BA,IAAM,KAAA,GAAQ,gCAAA;AA0Bd,IAAM,aAAA,GAAgB,GAAA;AAEf,IAAM,gBAAN,MAAoB;AAAA,EAMzB,YAA6B,OAAA,EAA+B;AAA/B,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAAgC;AAAA,EAAhC,OAAA;AAAA,EALZ,WAAA,uBAAkB,GAAA,EAAoB;AAAA,EACtC,OAAA,uBAAc,GAAA,EAA8C;AAAA,EACrE,MAAA,GAAS,CAAA;AAAA,EACT,SAAA,GAAY,KAAA;AAAA;AAAA,EAKpB,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AAAA;AAAA,EAGA,YAAY,QAAA,EAAiC;AAC3C,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AACjC,IAAA,OAAO,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,EACpC;AAAA;AAAA,EAGA,IAAA,CAAK,UAAkB,IAAA,EAAoB;AACzC,IAAA,IAAI,QAAA,EAAU,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,UAAU,IAAI,CAAA;AAAA,EACnD;AAAA;AAAA,EAGA,QAAQ,QAAA,EAAsC;AAC5C,IAAA,OAAO,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AAAA,EACtC;AAAA;AAAA,EAGA,MAAA,CAAO,IAAY,QAAA,EAAiC;AAClD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA;AACnC,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,IAAA,CAAK,OAAA,CAAQ,OAAO,EAAE,CAAA;AACtB,IAAA,OAAA,CAAQ,QAAQ,CAAA;AAAA,EAClB;AAAA;AAAA,EAGA,QAAQ,QAAA,EAAwB;AAC9B,IAAA,IAAA,CAAK,WAAA,CAAY,OAAO,QAAQ,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,MAAM,KAAA,CAAM,IAAA,EAAc,IAAA,EAAc,OAAA,EAAqC;AAC3E,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACnB,MAAA,OAAO,IAAI,QAAA,CAAS,2CAAA,EAA6C,EAAE,MAAA,EAAQ,eAAe,CAAA;AAAA,IAC5F;AACA,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,KAAW,KAAA,IAAS,OAAA,CAAQ,WAAW,MAAA,GAAS,MAAA,GAAY,MAAM,OAAA,CAAQ,WAAA,EAAY;AAC3G,IAAA,MAAM,KAAK,IAAA,CAAK,MAAA,EAAA;AAChB,IAAA,MAAM,QAAA,GAAW,IAAI,OAAA,CAAyB,CAAC,OAAA,KAAY,KAAK,OAAA,CAAQ,GAAA,CAAI,EAAA,EAAI,OAAO,CAAC,CAAA;AAExF,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA;AAAA,MACX,EAAE,EAAA,EAAI,IAAA,EAAM,MAAM,MAAA,EAAQ,OAAA,CAAQ,QAAQ,OAAA,EAAS,YAAA,CAAa,OAAA,CAAQ,OAAO,GAAG,GAAI,IAAA,GAAO,EAAE,IAAA,EAAK,GAAI,EAAC,EAAG;AAAA,MAC5G,IAAA,GAAO,CAAC,IAAI,CAAA,GAAI;AAAC,KACnB;AAEA,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,OAAA,CAAQ,SAAA,IAAa,GAAA;AAC5C,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,IAAA,CAAK;AAAA,MAChC,QAAA;AAAA,MACA,IAAI,OAAA;AAAA,QAAyB,CAAC,OAAA,KAC5B,UAAA;AAAA,UACE,MAAM,OAAA,CAAQ,EAAE,MAAA,EAAQ,KAAK,UAAA,EAAY,iBAAA,EAAmB,OAAA,EAAS,IAAI,IAAA,EAAM,IAAI,WAAA,CAAY,CAAC,GAAG,CAAA;AAAA,UACnG;AAAA;AACF;AACF,KACD,CAAA;AACD,IAAA,IAAA,CAAK,OAAA,CAAQ,OAAO,EAAE,CAAA;AAEtB,IAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA;AAY1C,IAAA,OAAA,CAAQ,GAAA,CAAI,gCAAgC,cAAc,CAAA;AAC1D,IAAA,OAAA,CAAQ,GAAA,CAAI,gCAAgC,cAAc,CAAA;AAC1D,IAAA,OAAO,IAAI,QAAA,CAAS,MAAA,CAAO,IAAA,EAAM,EAAE,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,UAAA,EAAY,MAAA,CAAO,UAAA,EAAY,OAAA,EAAS,CAAA;AAAA,EACpG;AACF,CAAA;AAGA,SAAS,aAAa,OAAA,EAA0C;AAC9D,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAC,OAAA,CACE,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AAAE,IAAA,GAAA,CAAI,GAAG,CAAA,GAAI,KAAA;AAAA,EAAO,CAAC,CAAA;AAChD,EAAA,OAAO,GAAA;AACT;;;ACvGA,IAAM,MAAA,GAAS,UAAA;AAEf,IAAI,IAAA,GAA2B,IAAA;AAC/B,IAAM,MAAA,GAAS,IAAI,aAAA,CAAc;AAAA,EAC/B,MAAM,CAAC,OAAA,EAAS,aAAa,IAAA,EAAM,WAAA,CAAY,SAAS,QAAQ;AAClE,CAAC,CAAA;AAED,MAAA,CAAO,iBAAiB,SAAA,EAAW,MAAM,KAAK,MAAA,CAAO,aAAa,CAAA;AAClE,MAAA,CAAO,gBAAA,CAAiB,UAAA,EAAY,CAAC,KAAA,KAAU,KAAA,CAAM,UAAU,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,CAAC,CAAA;AAEtF,MAAA,CAAO,gBAAA,CAAiB,SAAA,EAAW,CAAC,KAAA,KAAU;AAC5C,EAAA,MAAM,OAAO,KAAA,CAAM,IAAA;AACnB,EAAA,IAAI,IAAA,EAAM,SAAS,qBAAA,EAAuB;AAC1C,EAAA,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA,IAAK,IAAA;AACzB,EAAA,IAAI,CAAC,IAAA,EAAM;AACX,EAAA,IAAA,CAAK,SAAA,GAAY,CAAC,OAAA,KAAY;AAC5B,IAAA,MAAM,QAAQ,OAAA,CAAQ,IAAA;AACtB,IAAA,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,EAAA,EAAI,KAAA,CAAM,QAAQ,CAAA;AAAA,EACxC,CAAA;AACA,EAAA,IAAA,CAAK,KAAA,EAAM;AACX,EAAA,MAAA,CAAO,OAAA,EAAQ;AACjB,CAAC,CAAA;AAED,MAAA,CAAO,gBAAA,CAAiB,OAAA,EAAS,CAAC,KAAA,KAAU;AAC1C,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,KAAA,CAAM,QAAQ,GAAG,CAAA;AACrC,EAAA,IAAI,GAAA,CAAI,MAAA,KAAW,MAAA,CAAO,QAAA,CAAS,MAAA,EAAQ;AAE3C,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AAC/C,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,KAAA,CAAM,OAAA,CAAQ,SAAS,UAAA,EAAY;AAIzD,IAAA,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,iBAAA,IAAqB,KAAA,CAAM,UAAU,OAAO,CAAA;AAC9D,IAAA,KAAA,CAAM,YAAY,MAAA,CAAO,KAAA,CAAM,SAAS,GAAA,EAAK,KAAA,CAAM,OAAO,CAAC,CAAA;AAC3D,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,KAAA,CAAM,QAAQ,CAAA;AAC1C,EAAA,IAAI,SAAS,MAAA,EAAW;AACxB,EAAA,KAAA,CAAM,WAAA,CAAY,MAAA,CAAO,KAAA,CAAM,IAAA,EAAM,GAAA,CAAI,WAAW,GAAA,CAAI,MAAA,EAAQ,KAAA,CAAM,OAAO,CAAC,CAAA;AAChF,CAAC,CAAA","file":"service-worker.js","sourcesContent":["/**\n * The routing a preview service worker does, separated from the worker itself.\n *\n * A service worker is awkward to test — it needs a browser, a registration and\n * a secure context — but almost none of what makes this correct is about being\n * a service worker. The interesting part is the bookkeeping: which client is\n * previewing which port, which requests belong to a preview at all, and how a\n * response is rebuilt on the way back. That lives here, where it can be\n * exercised directly.\n *\n * The rule this implements, and the reason it exists: requests are routed by\n * **who is asking**, not by what the path looks like. An iframe navigates once\n * to `/__sbx__/<port>/`; every later request from that client — however\n * absolute its path — is recognised by its client id. Nothing has to rewrite\n * `/src/main.js` or `/@vite/client`, which is what makes a real dev server\n * work rather than only a single page.\n */\n\n/**\n * The one-time path an iframe navigates to in order to claim a port.\n *\n * Deliberately not anchored to the start of the path. A service worker's scope\n * is the directory its script was served from, so in a published package the\n * claim arrives under something like\n * `/node_modules/sandboxedjs/dist/__sbx__/5173/` — anchoring here would match\n * only when the worker happened to be served from the site root.\n */\nconst CLAIM = /(?:^|\\/)__sbx__\\/(\\d+)(?:\\/|$)/;\n\nexport interface ProxiedRequest {\n id: number;\n port: number;\n path: string;\n method: string;\n headers: Record<string, string>;\n body?: ArrayBuffer;\n}\n\nexport interface ProxiedResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: ArrayBuffer;\n}\n\nexport interface PreviewRouterOptions {\n /** Hand a request to whichever page owns the container. */\n send(request: ProxiedRequest, transfer: ArrayBuffer[]): void;\n /** How long to wait for that page before giving up. */\n timeoutMs?: number;\n}\n\n/** The port to answer with when nothing has connected yet. */\nconst NOT_CONNECTED = 503;\n\nexport class PreviewRouter {\n private readonly clientPorts = new Map<string, number>();\n private readonly pending = new Map<number, (value: ProxiedResponse) => void>();\n private nextId = 1;\n private connected = false;\n\n constructor(private readonly options: PreviewRouterOptions) {}\n\n /** The page that answers requests has arrived. */\n connect(): void {\n this.connected = true;\n }\n\n /** Read a port out of a claim path, or null when this is not one. */\n claimedPort(pathname: string): number | null {\n const match = CLAIM.exec(pathname);\n return match ? Number(match[1]) : null;\n }\n\n /** Remember that everything from `clientId` belongs to `port`. */\n bind(clientId: string, port: number): void {\n if (clientId) this.clientPorts.set(clientId, port);\n }\n\n /** Which port this client is previewing, or undefined for ordinary traffic. */\n portFor(clientId: string): number | undefined {\n return this.clientPorts.get(clientId);\n }\n\n /** A response has come back from the page. */\n settle(id: number, response: ProxiedResponse): void {\n const resolve = this.pending.get(id);\n if (!resolve) return;\n this.pending.delete(id);\n resolve(response);\n }\n\n /** Forget a client that has gone away. */\n release(clientId: string): void {\n this.clientPorts.delete(clientId);\n }\n\n /** Ask the page for one response and rebuild it as a `Response`. */\n async fetch(port: number, path: string, request: Request): Promise<Response> {\n if (!this.connected) {\n return new Response(\"The sandbox preview is not connected yet.\", { status: NOT_CONNECTED });\n }\n const body = request.method === \"GET\" || request.method === \"HEAD\" ? undefined : await request.arrayBuffer();\n const id = this.nextId++;\n const answered = new Promise<ProxiedResponse>((resolve) => this.pending.set(id, resolve));\n\n this.options.send(\n { id, port, path, method: request.method, headers: headerRecord(request.headers), ...(body ? { body } : {}) },\n body ? [body] : [],\n );\n\n const timeoutMs = this.options.timeoutMs ?? 30_000;\n const result = await Promise.race([\n answered,\n new Promise<ProxiedResponse>((resolve) =>\n setTimeout(\n () => resolve({ status: 504, statusText: \"Gateway Timeout\", headers: {}, body: new ArrayBuffer(0) }),\n timeoutMs,\n ),\n ),\n ]);\n this.pending.delete(id);\n\n const headers = new Headers(result.headers);\n /*\n * Two headers, for two different refusals, both of which a cross-origin\n * isolated page makes — and pages that run Rolldown have to be isolated.\n *\n * `Cross-Origin-Resource-Policy` lets the response be loaded as a\n * subresource at all. `Cross-Origin-Embedder-Policy` lets it be *framed*:\n * without it the document is not isolation-compatible, so the iframe is\n * replaced by an error page and the parent sees an opaque, cross-origin\n * frame rather than the site it asked for. Everything served here carries\n * CORP, so the stricter policy is one this preview can satisfy.\n */\n headers.set(\"Cross-Origin-Resource-Policy\", \"cross-origin\");\n headers.set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n return new Response(result.body, { status: result.status, statusText: result.statusText, headers });\n }\n}\n\n/** `Headers` is iterable at runtime; the DOM lib used here does not say so. */\nfunction headerRecord(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n (headers as unknown as { forEach(fn: (value: string, key: string) => void): void })\n .forEach((value, key) => { out[key] = value; });\n return out;\n}\n","/**\n * A service worker that gives a container's HTTP servers real URLs.\n *\n * Without one, a page can reach a server inside the container only through\n * `box.request(port, …)`, which hands back a single response. That is enough to\n * read an API or drop some HTML into an element, but not to *run* a site: a\n * dev server emits absolute URLs like `/src/main.js` and `/@vite/client`, and\n * nothing in the page resolves those to the sandbox.\n *\n * This file is deliberately only the glue. The routing lives in\n * {@link PreviewRouter}, where it can be tested without a browser — a service\n * worker otherwise needs a registration, a secure context and a real page\n * before any of its behaviour can be observed at all.\n */\n\nimport { PreviewRouter, type ProxiedResponse } from \"./router.js\";\n\n/*\n * Typed locally rather than through `lib.webworker`: this package compiles with\n * the DOM lib, and the two declare a conflicting `self`. Only the handful of\n * members actually used are described here.\n */\ninterface FetchLikeEvent extends Event {\n request: Request;\n clientId: string;\n resultingClientId: string;\n respondWith(response: Promise<Response> | Response): void;\n}\ninterface ExtendableLikeEvent extends Event {\n waitUntil(promise: Promise<unknown>): void;\n}\ninterface MessageLikeEvent extends Event {\n data: unknown;\n ports: readonly MessagePort[];\n}\ninterface WorkerScope {\n location: { origin: string };\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n addEventListener(type: \"install\" | \"activate\", listener: (event: ExtendableLikeEvent) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchLikeEvent) => void): void;\n addEventListener(type: \"message\", listener: (event: MessageLikeEvent) => void): void;\n}\n\nconst worker = globalThis as unknown as WorkerScope;\n\nlet host: MessagePort | null = null;\nconst router = new PreviewRouter({\n send: (request, transfer) => host?.postMessage(request, transfer),\n});\n\nworker.addEventListener(\"install\", () => void worker.skipWaiting());\nworker.addEventListener(\"activate\", (event) => event.waitUntil(worker.clients.claim()));\n\nworker.addEventListener(\"message\", (event) => {\n const data = event.data as { type?: string } | null;\n if (data?.type !== \"sandboxedjs:connect\") return;\n host = event.ports[0] ?? null;\n if (!host) return;\n host.onmessage = (message) => {\n const reply = message.data as { id: number; response: ProxiedResponse };\n router.settle(reply.id, reply.response);\n };\n host.start();\n router.connect();\n});\n\nworker.addEventListener(\"fetch\", (event) => {\n const url = new URL(event.request.url);\n if (url.origin !== worker.location.origin) return;\n\n const claimed = router.claimedPort(url.pathname);\n if (claimed !== null && event.request.mode === \"navigate\") {\n /* A navigation's client does not exist yet, so it is named by\n * `resultingClientId`. Binding it here is what lets every later request\n * from that document be recognised without touching its URLs. */\n router.bind(event.resultingClientId || event.clientId, claimed);\n event.respondWith(router.fetch(claimed, \"/\", event.request));\n return;\n }\n\n const port = router.portFor(event.clientId);\n if (port === undefined) return; // not a preview: the host page's own traffic\n event.respondWith(router.fetch(port, url.pathname + url.search, event.request));\n});\n"]}
1
+ {"version":3,"sources":["../src/preview/router.ts","../src/preview/service-worker.ts"],"names":[],"mappings":"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","file":"service-worker.js","sourcesContent":["/**\n * The routing a preview service worker does, separated from the worker itself.\n *\n * A service worker is awkward to test — it needs a browser, a registration and\n * a secure context — but almost none of what makes this correct is about being\n * a service worker. The interesting part is the bookkeeping: which client is\n * previewing which port, which requests belong to a preview at all, and how a\n * response is rebuilt on the way back. That lives here, where it can be\n * exercised directly.\n *\n * The rule this implements, and the reason it exists: requests are routed by\n * **who is asking**, not by what the path looks like. An iframe navigates once\n * to `/__sbx__/<port>/`; every later request from that client — however\n * absolute its path — is recognised by its client id. Nothing has to rewrite\n * `/src/main.js` or `/@vite/client`, which is what makes a real dev server\n * work rather than only a single page.\n */\n\n/**\n * The one-time path an iframe navigates to in order to claim a port.\n *\n * Deliberately not anchored to the start of the path. A service worker's scope\n * is the directory its script was served from, so in a published package the\n * claim arrives under something like\n * `/node_modules/sandboxedjs/dist/__sbx__/5173/` — anchoring here would match\n * only when the worker happened to be served from the site root.\n */\nconst CLAIM = /(?:^|\\/)__sbx__\\/(\\d+)(?:\\/|$)/;\n\nexport interface ProxiedRequest {\n id: number;\n port: number;\n path: string;\n method: string;\n headers: Record<string, string>;\n body?: ArrayBuffer;\n}\n\nexport interface ProxiedResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: ArrayBuffer;\n}\n\nexport interface PreviewRouterOptions {\n /** Hand a request to whichever page owns the container. */\n send(request: ProxiedRequest, transfer: ArrayBuffer[]): void;\n /** How long to wait for that page before giving up. */\n timeoutMs?: number;\n}\n\n/** The port to answer with when nothing has connected yet. */\nconst NOT_CONNECTED = 503;\n\nexport class PreviewRouter {\n private readonly clientPorts = new Map<string, number>();\n private readonly pending = new Map<number, (value: ProxiedResponse) => void>();\n private nextId = 1;\n private connected = false;\n\n constructor(private readonly options: PreviewRouterOptions) {}\n\n /** The page that answers requests has arrived. */\n connect(): void {\n this.connected = true;\n }\n\n /** Read a port out of a claim path, or null when this is not one. */\n /**\n * The path to ask the container for, given a request from a bound client.\n *\n * A document served at `/…/__sbx__/3000/` resolves its *relative* assets\n * against that prefix, so `./app.js` arrives as\n * `/…/__sbx__/3000/app.js`. Forwarding that verbatim asks the container for\n * a file inside a directory it has never heard of, and every relative asset\n * in the site 404s while the document itself loads perfectly — a page that\n * renders unstyled and inert, with nothing to explain why.\n *\n * Root-relative assets (`/main.js`) carry no prefix and pass through\n * unchanged, which is the case that always worked.\n */\n containerPath(pathname: string, search: string): string {\n const match = CLAIM.exec(pathname);\n const rest = match ? pathname.slice(match.index + match[0].length) : pathname;\n const normalised = rest.startsWith(\"/\") ? rest : `/${rest}`;\n return `${normalised}${search}`;\n }\n\n claimedPort(pathname: string): number | null {\n const match = CLAIM.exec(pathname);\n return match ? Number(match[1]) : null;\n }\n\n /** Remember that everything from `clientId` belongs to `port`. */\n bind(clientId: string, port: number): void {\n if (clientId) this.clientPorts.set(clientId, port);\n }\n\n /** Which port this client is previewing, or undefined for ordinary traffic. */\n portFor(clientId: string): number | undefined {\n return this.clientPorts.get(clientId);\n }\n\n /** A response has come back from the page. */\n settle(id: number, response: ProxiedResponse): void {\n const resolve = this.pending.get(id);\n if (!resolve) return;\n this.pending.delete(id);\n resolve(response);\n }\n\n /** Forget a client that has gone away. */\n release(clientId: string): void {\n this.clientPorts.delete(clientId);\n }\n\n /** Ask the page for one response and rebuild it as a `Response`. */\n async fetch(port: number, path: string, request: Request): Promise<Response> {\n if (!this.connected) {\n return new Response(\"The sandbox preview is not connected yet.\", { status: NOT_CONNECTED });\n }\n const body = request.method === \"GET\" || request.method === \"HEAD\" ? undefined : await request.arrayBuffer();\n const id = this.nextId++;\n const answered = new Promise<ProxiedResponse>((resolve) => this.pending.set(id, resolve));\n\n this.options.send(\n { id, port, path, method: request.method, headers: headerRecord(request.headers), ...(body ? { body } : {}) },\n body ? [body] : [],\n );\n\n const timeoutMs = this.options.timeoutMs ?? 30_000;\n const result = await Promise.race([\n answered,\n new Promise<ProxiedResponse>((resolve) =>\n setTimeout(\n () => resolve({ status: 504, statusText: \"Gateway Timeout\", headers: {}, body: new ArrayBuffer(0) }),\n timeoutMs,\n ),\n ),\n ]);\n this.pending.delete(id);\n\n const headers = new Headers(result.headers);\n /*\n * Two headers, for two different refusals, both of which a cross-origin\n * isolated page makes — and pages that run Rolldown have to be isolated.\n *\n * `Cross-Origin-Resource-Policy` lets the response be loaded as a\n * subresource at all. `Cross-Origin-Embedder-Policy` lets it be *framed*:\n * without it the document is not isolation-compatible, so the iframe is\n * replaced by an error page and the parent sees an opaque, cross-origin\n * frame rather than the site it asked for. Everything served here carries\n * CORP, so the stricter policy is one this preview can satisfy.\n */\n headers.set(\"Cross-Origin-Resource-Policy\", \"cross-origin\");\n headers.set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n return new Response(result.body, { status: result.status, statusText: result.statusText, headers });\n }\n}\n\n/** `Headers` is iterable at runtime; the DOM lib used here does not say so. */\nfunction headerRecord(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n (headers as unknown as { forEach(fn: (value: string, key: string) => void): void })\n .forEach((value, key) => { out[key] = value; });\n return out;\n}\n","/**\n * A service worker that gives a container's HTTP servers real URLs.\n *\n * Without one, a page can reach a server inside the container only through\n * `box.request(port, …)`, which hands back a single response. That is enough to\n * read an API or drop some HTML into an element, but not to *run* a site: a\n * dev server emits absolute URLs like `/src/main.js` and `/@vite/client`, and\n * nothing in the page resolves those to the sandbox.\n *\n * This file is deliberately only the glue. The routing lives in\n * {@link PreviewRouter}, where it can be tested without a browser — a service\n * worker otherwise needs a registration, a secure context and a real page\n * before any of its behaviour can be observed at all.\n */\n\nimport { PreviewRouter, type ProxiedResponse } from \"./router.js\";\n\n/*\n * Typed locally rather than through `lib.webworker`: this package compiles with\n * the DOM lib, and the two declare a conflicting `self`. Only the handful of\n * members actually used are described here.\n */\ninterface FetchLikeEvent extends Event {\n request: Request;\n clientId: string;\n resultingClientId: string;\n respondWith(response: Promise<Response> | Response): void;\n}\ninterface ExtendableLikeEvent extends Event {\n waitUntil(promise: Promise<unknown>): void;\n}\ninterface MessageLikeEvent extends Event {\n data: unknown;\n ports: readonly MessagePort[];\n}\ninterface WorkerScope {\n location: { origin: string };\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n addEventListener(type: \"install\" | \"activate\", listener: (event: ExtendableLikeEvent) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchLikeEvent) => void): void;\n addEventListener(type: \"message\", listener: (event: MessageLikeEvent) => void): void;\n}\n\nconst worker = globalThis as unknown as WorkerScope;\n\nlet host: MessagePort | null = null;\nconst router = new PreviewRouter({\n send: (request, transfer) => host?.postMessage(request, transfer),\n});\n\nworker.addEventListener(\"install\", () => void worker.skipWaiting());\nworker.addEventListener(\"activate\", (event) => event.waitUntil(worker.clients.claim()));\n\nworker.addEventListener(\"message\", (event) => {\n const data = event.data as { type?: string } | null;\n if (data?.type !== \"sandboxedjs:connect\") return;\n host = event.ports[0] ?? null;\n if (!host) return;\n host.onmessage = (message) => {\n const reply = message.data as { id: number; response: ProxiedResponse };\n router.settle(reply.id, reply.response);\n };\n host.start();\n router.connect();\n});\n\nworker.addEventListener(\"fetch\", (event) => {\n const url = new URL(event.request.url);\n if (url.origin !== worker.location.origin) return;\n\n const claimed = router.claimedPort(url.pathname);\n if (claimed !== null && event.request.mode === \"navigate\") {\n /* A navigation's client does not exist yet, so it is named by\n * `resultingClientId`. Binding it here is what lets every later request\n * from that document be recognised without touching its URLs. */\n router.bind(event.resultingClientId || event.clientId, claimed);\n event.respondWith(router.fetch(claimed, \"/\", event.request));\n return;\n }\n\n const port = router.portFor(event.clientId);\n if (port === undefined) return; // not a preview: the host page's own traffic\n /* Strip this worker's own claim prefix before asking the container: a\n * relative asset resolves against the document's URL, so it arrives carrying\n * the prefix the container knows nothing about. */\n event.respondWith(\n router.fetch(port, router.containerPath(url.pathname, url.search), event.request),\n );\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "A Linux-like container that runs entirely inside Node.js — POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
5
5
  "type": "module",
6
6
  "license": "MIT",