sandboxedjs 0.1.44 → 0.1.45

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.
@@ -1,5 +1,6 @@
1
1
  // src/preview/router.ts
2
2
  var CLAIM = /(?:^|\/)__sbx__\/(\d+)(?:\/|$)/;
3
+ var OUTDATED = /outdated optimize dep/i;
3
4
  var NOT_CONNECTED = 503;
4
5
  var PreviewRouter = class {
5
6
  constructor(options) {
@@ -94,6 +95,9 @@ var PreviewRouter = class {
94
95
  )
95
96
  ]);
96
97
  this.pending.delete(id);
98
+ if (result.status === 504 && OUTDATED.test(result.statusText)) {
99
+ this.options.onStale?.();
100
+ }
97
101
  const headers = new Headers(result.headers);
98
102
  headers.set("Cross-Origin-Resource-Policy", "cross-origin");
99
103
  headers.set("Cross-Origin-Embedder-Policy", "require-corp");
@@ -150,6 +154,14 @@ var router = new PreviewRouter({
150
154
  await promise;
151
155
  clearTimeout(timer);
152
156
  awaitingHost = null;
157
+ },
158
+ /* Broadcast, for the same reason `requestHost` does: after a restart this
159
+ * worker has no idea which page owns what. A page with no preview ignores
160
+ * it. */
161
+ onStale: () => {
162
+ void worker.clients.matchAll({ includeUncontrolled: true, type: "window" }).then((clients) => {
163
+ for (const client of clients) client.postMessage({ type: "sandboxedjs:stale" });
164
+ });
153
165
  }
154
166
  });
155
167
  worker.addEventListener("install", () => void worker.skipWaiting());
@@ -1 +1 @@
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 * Ask the owning page to hand over a fresh channel, and resolve once it has.\n *\n * A service worker is not a process that stays running. The browser stops it\n * whenever it looks idle — seconds, in practice — and starts it again for\n * the next request, with every module-level value gone. The `MessagePort`\n * that reaches the container is one of those values, so a worker that has\n * been restarted has no way to answer anything, however healthy the\n * container behind it still is.\n *\n * Nothing about that is visible: the server is running, the port is open,\n * and the preview simply stops working — on reload, in a new tab, or after\n * a pause long enough for the browser to reclaim the worker. The page is\n * still there and can always supply another port, so the worker asks for one\n * rather than failing.\n */\n requestHost?(): Promise<void>;\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 /** Whether a page has handed over a channel this worker can still use. */\n get ready(): boolean {\n return this.connected;\n }\n\n /** The page that answers requests has arrived. */\n connect(): void {\n this.connected = true;\n }\n\n /** The channel is gone; the next request has to ask for another. */\n disconnect(): void {\n this.connected = false;\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 /* Restarted, most likely, rather than never started. Ask the page for a\n * channel before concluding there is nobody to ask. */\n if (!this.connected) await this.options.requestHost?.().catch(() => undefined);\n\n if (!this.connected) {\n return isolated(\n new Response(\n 'The sandbox preview is not connected yet. The page that owns this container has to be open.',\n { status: NOT_CONNECTED, statusText: 'Service Unavailable' },\n ),\n );\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/**\n * The isolation headers, on a response this router made up itself.\n *\n * Every failure here is still framed by a cross-origin isolated page, and a\n * document without these is not merely shown as an error — it is refused\n * before it renders, so the reader gets the browser's \"refused to connect\"\n * instead of the sentence explaining what went wrong. An error nobody can read\n * is worse than the failure it describes.\n */\nfunction isolated(response: Response): Response {\n const headers = new Headers(response.headers);\n headers.set(\"Content-Type\", \"text/plain; charset=utf-8\");\n headers.set(\"Cross-Origin-Resource-Policy\", \"cross-origin\");\n headers.set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n 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 WorkerClient {\n postMessage(message: unknown): void;\n}\ninterface WorkerScope {\n location: { origin: string };\n clients: {\n claim(): Promise<void>;\n matchAll(options?: { includeUncontrolled?: boolean; type?: string }): Promise<WorkerClient[]>;\n };\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;\n\n/** Resolves when a page answers the call for a channel; see `requestHost`. */\nlet awaitingHost: { promise: Promise<void>; resolve: () => void } | null = null;\n\nconst HOST_WAIT_MS = 5_000;\n\nconst router = new PreviewRouter({\n send: (request, transfer) => host?.postMessage(request, transfer),\n /*\n * The worker was stopped and started again, taking the channel with it.\n *\n * Every page that owns a container listens for this and hands over a fresh\n * port. Broadcasting rather than tracking an owner is deliberate: after a\n * restart the worker knows nothing about who registered it, and a page with\n * no container simply ignores the call.\n */\n requestHost: async () => {\n if (awaitingHost) return awaitingHost.promise;\n\n let resolve!: () => void;\n const promise = new Promise<void>((done) => {\n resolve = done;\n });\n awaitingHost = { promise, resolve };\n\n const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: \"window\" });\n for (const client of clients) client.postMessage({ type: \"sandboxedjs:host-needed\" });\n\n /* Bounded: a page that cannot answer must not hold the request open until\n * the request timeout, which is six times longer. */\n const timer = setTimeout(() => {\n awaitingHost?.resolve();\n awaitingHost = null;\n }, HOST_WAIT_MS);\n\n await promise;\n clearTimeout(timer);\n awaitingHost = null;\n },\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 /* Release anything that was waiting on this channel arriving. */\n awaitingHost?.resolve();\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"]}
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 * Ask the owning page to hand over a fresh channel, and resolve once it has.\n *\n * A service worker is not a process that stays running. The browser stops it\n * whenever it looks idle — seconds, in practice — and starts it again for\n * the next request, with every module-level value gone. The `MessagePort`\n * that reaches the container is one of those values, so a worker that has\n * been restarted has no way to answer anything, however healthy the\n * container behind it still is.\n *\n * Nothing about that is visible: the server is running, the port is open,\n * and the preview simply stops working — on reload, in a new tab, or after\n * a pause long enough for the browser to reclaim the worker. The page is\n * still there and can always supply another port, so the worker asks for one\n * rather than failing.\n */\n requestHost?(): Promise<void>;\n /**\n * The dev server has invalidated what the page is holding; reload it.\n *\n * Vite answers a request for a pre-bundled dependency with `504 Outdated\n * Optimize Dep` whenever the hash in the URL no longer matches — after the\n * optimizer re-runs, or after the server restarts. Its own client recovers\n * by reloading the whole page, and it learns to do that over the HMR\n * WebSocket.\n *\n * This bridge carries HTTP and nothing else, so that signal never arrives.\n * The page goes on requesting URLs stamped with a hash the server has\n * forgotten, every module 504s, and the app never mounts — while the\n * terminal reports a dev server that started perfectly. A plain static\n * server never does this, which is why vanilla pages worked and React did\n * not.\n */\n onStale?(): void;\n}\n\n/** Vite's answer when the page is holding a dependency hash it has dropped. */\nconst OUTDATED = /outdated optimize dep/i;\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 /** Whether a page has handed over a channel this worker can still use. */\n get ready(): boolean {\n return this.connected;\n }\n\n /** The page that answers requests has arrived. */\n connect(): void {\n this.connected = true;\n }\n\n /** The channel is gone; the next request has to ask for another. */\n disconnect(): void {\n this.connected = false;\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 /* Restarted, most likely, rather than never started. Ask the page for a\n * channel before concluding there is nobody to ask. */\n if (!this.connected) await this.options.requestHost?.().catch(() => undefined);\n\n if (!this.connected) {\n return isolated(\n new Response(\n 'The sandbox preview is not connected yet. The page that owns this container has to be open.',\n { status: NOT_CONNECTED, statusText: 'Service Unavailable' },\n ),\n );\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 /* Nothing here can rewrite the URL the page asked for — only the page can\n * ask again, and only after reloading. Report it and let the host decide;\n * a reload driven from inside the frame would fight the app for control of\n * its own navigation. */\n if (result.status === 504 && OUTDATED.test(result.statusText)) {\n this.options.onStale?.();\n }\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/**\n * The isolation headers, on a response this router made up itself.\n *\n * Every failure here is still framed by a cross-origin isolated page, and a\n * document without these is not merely shown as an error — it is refused\n * before it renders, so the reader gets the browser's \"refused to connect\"\n * instead of the sentence explaining what went wrong. An error nobody can read\n * is worse than the failure it describes.\n */\nfunction isolated(response: Response): Response {\n const headers = new Headers(response.headers);\n headers.set(\"Content-Type\", \"text/plain; charset=utf-8\");\n headers.set(\"Cross-Origin-Resource-Policy\", \"cross-origin\");\n headers.set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n 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 WorkerClient {\n postMessage(message: unknown): void;\n}\ninterface WorkerScope {\n location: { origin: string };\n clients: {\n claim(): Promise<void>;\n matchAll(options?: { includeUncontrolled?: boolean; type?: string }): Promise<WorkerClient[]>;\n };\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;\n\n/** Resolves when a page answers the call for a channel; see `requestHost`. */\nlet awaitingHost: { promise: Promise<void>; resolve: () => void } | null = null;\n\nconst HOST_WAIT_MS = 5_000;\n\nconst router = new PreviewRouter({\n send: (request, transfer) => host?.postMessage(request, transfer),\n /*\n * The worker was stopped and started again, taking the channel with it.\n *\n * Every page that owns a container listens for this and hands over a fresh\n * port. Broadcasting rather than tracking an owner is deliberate: after a\n * restart the worker knows nothing about who registered it, and a page with\n * no container simply ignores the call.\n */\n requestHost: async () => {\n if (awaitingHost) return awaitingHost.promise;\n\n let resolve!: () => void;\n const promise = new Promise<void>((done) => {\n resolve = done;\n });\n awaitingHost = { promise, resolve };\n\n const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: \"window\" });\n for (const client of clients) client.postMessage({ type: \"sandboxedjs:host-needed\" });\n\n /* Bounded: a page that cannot answer must not hold the request open until\n * the request timeout, which is six times longer. */\n const timer = setTimeout(() => {\n awaitingHost?.resolve();\n awaitingHost = null;\n }, HOST_WAIT_MS);\n\n await promise;\n clearTimeout(timer);\n awaitingHost = null;\n },\n /* Broadcast, for the same reason `requestHost` does: after a restart this\n * worker has no idea which page owns what. A page with no preview ignores\n * it. */\n onStale: () => {\n void worker.clients\n .matchAll({ includeUncontrolled: true, type: \"window\" })\n .then((clients) => {\n for (const client of clients) client.postMessage({ type: \"sandboxedjs:stale\" });\n });\n },\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 /* Release anything that was waiting on this channel arriving. */\n awaitingHost?.resolve();\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.44",
3
+ "version": "0.1.45",
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",