@scelar/nodepod 1.0.6 → 1.0.8

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.
Files changed (37) hide show
  1. package/README.md +12 -0
  2. package/dist/__sw__.js +31 -5
  3. package/dist/{child_process-Cao4lyrb.cjs → child_process-BD0fAFc1.cjs} +7431 -7435
  4. package/dist/child_process-BD0fAFc1.cjs.map +1 -0
  5. package/dist/{child_process-4ZrgCVFu.js → child_process-DV4uXIWL.js} +8231 -8234
  6. package/dist/child_process-DV4uXIWL.js.map +1 -0
  7. package/dist/cross-origin.d.ts +2 -0
  8. package/dist/{index-DuYo2yDs.cjs → index-DVbLKdL5.cjs} +38909 -38842
  9. package/dist/index-DVbLKdL5.cjs.map +1 -0
  10. package/dist/{index-HkVqijtm.js → index-DpqCP69G.js} +36969 -36923
  11. package/dist/index-DpqCP69G.js.map +1 -0
  12. package/dist/index.cjs +67 -67
  13. package/dist/index.mjs +61 -61
  14. package/dist/isolation-helpers.d.ts +3 -0
  15. package/dist/packages/archive-extractor.d.ts +2 -0
  16. package/dist/packages/version-resolver.d.ts +1 -0
  17. package/dist/polyfills/readline.d.ts +108 -108
  18. package/dist/request-proxy.d.ts +3 -0
  19. package/dist/sdk/nodepod.d.ts +2 -3
  20. package/dist/sdk/types.d.ts +3 -0
  21. package/dist/threading/offload-types.d.ts +1 -0
  22. package/package.json +1 -1
  23. package/src/cross-origin.ts +49 -0
  24. package/src/iframe-sandbox.ts +11 -7
  25. package/src/isolation-helpers.ts +13 -7
  26. package/src/packages/archive-extractor.ts +3 -0
  27. package/src/packages/installer.ts +1 -0
  28. package/src/packages/version-resolver.ts +2 -0
  29. package/src/request-proxy.ts +43 -9
  30. package/src/sdk/nodepod.ts +134 -80
  31. package/src/sdk/types.ts +3 -0
  32. package/src/threading/offload-types.ts +1 -0
  33. package/src/threading/offload-worker.ts +15 -0
  34. package/dist/child_process-4ZrgCVFu.js.map +0 -1
  35. package/dist/child_process-Cao4lyrb.cjs.map +0 -1
  36. package/dist/index-DuYo2yDs.cjs.map +0 -1
  37. package/dist/index-HkVqijtm.js.map +0 -1
package/README.md CHANGED
@@ -119,6 +119,7 @@ Creates a fully wired nodepod instance.
119
119
  | `swUrl` | `string` | — | Service Worker URL (enables preview iframes) |
120
120
  | `watermark` | `boolean` | `true` | Show a small "nodepod" badge in preview iframes |
121
121
  | `onServerReady` | `(port, url) => void` | — | Callback when a virtual server starts listening |
122
+ | `allowedFetchDomains` | `string[] \| null` | npm/github defaults | Extra domains allowed through the CORS proxy. Pass `null` to allow all |
122
123
 
123
124
  ### Instance Methods
124
125
 
@@ -151,6 +152,17 @@ Returned by `spawn()`. An EventEmitter with:
151
152
 
152
153
  Property: `completion` — a `Promise<void>` that resolves when the process exits.
153
154
 
155
+ ## Security
156
+
157
+ nodepod includes several security measures for running untrusted code:
158
+
159
+ - **CORS proxy domain whitelist** — Proxied fetch requests only go through to whitelisted domains (npm registry, GitHub, esm.sh, etc by default). Extend via the `allowedFetchDomains` boot option or pass `null` to disable
160
+ - **Service Worker auth** — Control messages to the SW require a random token generated at boot, so other scripts on the same origin can't inject preview content
161
+ - **WebSocket bridge auth** — The BroadcastChannel used for WS bridging between preview iframes and the main thread is token-authenticated
162
+ - **Package integrity** — Downloaded npm tarballs are checked against the registry's `shasum` before extraction
163
+ - **Iframe sandbox** — The cross-origin iframe mode uses `sandbox="allow-scripts"` to prevent top-frame navigation, popups, and form submissions
164
+ - **Origin-checked messaging** — The sandbox page validates `event.origin` on incoming messages and only responds to the configured parent origin
165
+
154
166
  ## Architecture
155
167
 
156
168
  ```
package/dist/__sw__.js CHANGED
@@ -29,6 +29,12 @@ let previewScript = null;
29
29
  // Watermark badge shown in preview iframes. On by default.
30
30
  let watermarkEnabled = true;
31
31
 
32
+ // auth token from init, checked on control messages
33
+ let authToken = null;
34
+
35
+ // ws bridge token, gets baked into the shim script
36
+ let wsToken = null;
37
+
32
38
  // Standard MIME types by file extension — used as a safety net when
33
39
  // the virtual server returns text/html (SPA fallback) or omits Content-Type
34
40
  // for paths that are clearly not HTML.
@@ -110,10 +116,20 @@ self.addEventListener("activate", (event) => {
110
116
 
111
117
  self.addEventListener("message", (event) => {
112
118
  const data = event.data;
119
+
120
+ // init sets up the port + grabs the auth token
113
121
  if (data?.type === "init" && data.port) {
114
122
  port = data.port;
115
123
  port.onmessage = onPortMessage;
124
+ if (data.token) {
125
+ authToken = data.token;
126
+ }
127
+ return;
116
128
  }
129
+
130
+ // everything else needs the right token
131
+ if (authToken && data?.token !== authToken) return;
132
+
117
133
  // Allow main thread to register/unregister preview clients
118
134
  if (data?.type === "register-preview") {
119
135
  previewClients.set(data.clientId, data.serverPort);
@@ -127,6 +143,9 @@ self.addEventListener("message", (event) => {
127
143
  if (data?.type === "set-watermark") {
128
144
  watermarkEnabled = !!data.enabled;
129
145
  }
146
+ if (data?.type === "set-ws-token") {
147
+ wsToken = data.wsToken ?? null;
148
+ }
130
149
  });
131
150
 
132
151
  function onPortMessage(event) {
@@ -245,12 +264,15 @@ self.addEventListener("fetch", (event) => {
245
264
  // to the main thread's request-proxy, which dispatches upgrade events on the
246
265
  // virtual HTTP server. Works with any framework/library, not specific to Vite.
247
266
 
248
- const WS_SHIM_SCRIPT = `<script>
267
+ function getWsShimScript() {
268
+ const tokenStr = wsToken ? JSON.stringify(wsToken) : "null";
269
+ return `<script>
249
270
  (function() {
250
271
  if (window.__nodepodWsShim) return;
251
272
  window.__nodepodWsShim = true;
252
273
  var NativeWS = window.WebSocket;
253
274
  var bc = new BroadcastChannel("nodepod-ws");
275
+ var _wsToken = ${tokenStr};
254
276
  var nextId = 0;
255
277
  var active = {};
256
278
 
@@ -300,7 +322,8 @@ const WS_SHIM_SCRIPT = `<script>
300
322
  uid: uid,
301
323
  port: port,
302
324
  path: path,
303
- protocols: Array.isArray(protocols) ? protocols.join(",") : (protocols || "")
325
+ protocols: Array.isArray(protocols) ? protocols.join(",") : (protocols || ""),
326
+ token: _wsToken
304
327
  });
305
328
 
306
329
  // Timeout: if no ws-open within 5s, fire error
@@ -347,12 +370,12 @@ const WS_SHIM_SCRIPT = `<script>
347
370
  type = "binary";
348
371
  payload = Array.from(data);
349
372
  }
350
- bc.postMessage({ kind: "ws-send", uid: this._uid, data: payload, type: type });
373
+ bc.postMessage({ kind: "ws-send", uid: this._uid, data: payload, type: type, token: _wsToken });
351
374
  };
352
375
  NodepodWS.prototype.close = function(code, reason) {
353
376
  if (this.readyState >= 2) return;
354
377
  this.readyState = 2;
355
- bc.postMessage({ kind: "ws-close", uid: this._uid, code: code || 1000, reason: reason || "" });
378
+ bc.postMessage({ kind: "ws-close", uid: this._uid, code: code || 1000, reason: reason || "", token: _wsToken });
356
379
  var self = this;
357
380
  setTimeout(function() {
358
381
  self.readyState = 3;
@@ -375,6 +398,8 @@ const WS_SHIM_SCRIPT = `<script>
375
398
  bc.onmessage = function(ev) {
376
399
  var d = ev.data;
377
400
  if (!d || !d.uid) return;
401
+ // check bridge token
402
+ if (_wsToken && d.token !== _wsToken) return;
378
403
  var ws = active[d.uid];
379
404
  if (!ws) return;
380
405
 
@@ -414,6 +439,7 @@ const WS_SHIM_SCRIPT = `<script>
414
439
  window.WebSocket = NodepodWS;
415
440
  })();
416
441
  </script>`;
442
+ }
417
443
 
418
444
  // Small "nodepod" badge in the bottom-right corner of preview iframes.
419
445
  const WATERMARK_SCRIPT = `<script>
@@ -571,7 +597,7 @@ async function proxyToVirtualServer(request, serverPort, path, originalRequest)
571
597
  let finalBody = responseBody;
572
598
  const ct = respHeaders["content-type"] || respHeaders["Content-Type"] || "";
573
599
  if (ct.includes("text/html") && responseBody) {
574
- let injection = WS_SHIM_SCRIPT;
600
+ let injection = getWsShimScript();
575
601
  if (previewScript) {
576
602
  injection += `<script>${previewScript}<` + `/script>`;
577
603
  }