@volter/tabnode 0.4.1 → 0.5.1

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 (80) hide show
  1. package/dist/__sw__.js +259 -5
  2. package/dist/assets/{runtime-worker-LWfW6VWE.js → runtime-worker-DBZ2flHK.js} +4000 -2944
  3. package/dist/fetch-transport.d.ts +20 -0
  4. package/dist/guest-timers.d.ts +15 -0
  5. package/dist/http-client-transport.d.ts +19 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.mjs +5196 -3062
  8. package/dist/native-stream-binding.d.ts +57 -0
  9. package/dist/native-stream-owner.d.ts +121 -0
  10. package/dist/node-lib/binding/duplex_stream.d.ts +26 -0
  11. package/dist/node-lib/binding/fds.d.ts +8 -0
  12. package/dist/node-lib/binding/fs.d.ts +311 -78
  13. package/dist/node-lib/binding/misc.d.ts +1 -0
  14. package/dist/node-lib/binding/pipe_wrap.d.ts +4 -4
  15. package/dist/node-lib/binding/process_wrap.d.ts +12 -1
  16. package/dist/node-lib/binding/stream_wrap.d.ts +27 -18
  17. package/dist/node-lib/binding/tcp_wrap.d.ts +0 -1
  18. package/dist/node-lib/binding/tty_wrap.d.ts +43 -2
  19. package/dist/node-lib/binding/util.d.ts +83 -0
  20. package/dist/node-lib/binding/uv.d.ts +4 -0
  21. package/dist/node-lib/fs-module.d.ts +1 -0
  22. package/dist/node-lib/internals/buffer-and-streams.d.ts +30 -17
  23. package/dist/node-lib/internals/index.d.ts +1 -1
  24. package/dist/node-lib/load.d.ts +17 -3
  25. package/dist/node-lib/module-hooks.d.ts +1 -0
  26. package/dist/node-lib/public-modules.d.ts +1 -1
  27. package/dist/node-lib/timers.d.ts +38 -0
  28. package/dist/node-process-host.d.ts +41 -0
  29. package/dist/process-registry.d.ts +39 -0
  30. package/dist/process-tokens.d.ts +11 -0
  31. package/dist/promise-ownership.d.ts +3 -0
  32. package/dist/runtime.d.ts +10 -9
  33. package/dist/shims/child_process.d.ts +2 -0
  34. package/dist/shims/crypto.d.ts +660 -119
  35. package/dist/shims/dns.d.ts +76 -2
  36. package/dist/shims/process.d.ts +1 -0
  37. package/dist/shims/string_decoder.d.ts +19 -3
  38. package/dist/shims/tls.d.ts +853 -31
  39. package/dist/source-edits.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/src/code-transforms.ts +2 -3
  42. package/src/fetch-transport.ts +98 -0
  43. package/src/guest-timers.ts +89 -0
  44. package/src/http-client-transport.ts +169 -0
  45. package/src/index.ts +18 -0
  46. package/src/native-stream-binding.ts +393 -0
  47. package/src/native-stream-owner.ts +317 -0
  48. package/src/node-lib/binding/duplex_stream.ts +95 -0
  49. package/src/node-lib/binding/fds.ts +18 -1
  50. package/src/node-lib/binding/fs.ts +45 -17
  51. package/src/node-lib/binding/misc.ts +2 -0
  52. package/src/node-lib/binding/pipe_wrap.ts +35 -12
  53. package/src/node-lib/binding/process_wrap.ts +50 -9
  54. package/src/node-lib/binding/stream_wrap.ts +196 -72
  55. package/src/node-lib/binding/tcp_wrap.ts +43 -8
  56. package/src/node-lib/binding/tty_wrap.ts +135 -8
  57. package/src/node-lib/binding/util.ts +24 -15
  58. package/src/node-lib/binding/uv.ts +4 -0
  59. package/src/node-lib/fs-module.ts +2 -18
  60. package/src/node-lib/internal/navigator.js +149 -0
  61. package/src/node-lib/internals/buffer-and-streams.ts +50 -11
  62. package/src/node-lib/internals/events-util.ts +5 -0
  63. package/src/node-lib/internals/index.ts +12 -2
  64. package/src/node-lib/load.ts +170 -62
  65. package/src/node-lib/module-hooks.ts +5 -2
  66. package/src/node-lib/public-modules.ts +12 -48
  67. package/src/node-lib/sources.ts +4 -0
  68. package/src/node-lib/timers.ts +93 -0
  69. package/src/node-process-host.ts +124 -0
  70. package/src/process-registry.ts +144 -0
  71. package/src/process-tokens.ts +55 -6
  72. package/src/promise-ownership.ts +28 -0
  73. package/src/runtime.ts +90 -214
  74. package/src/shims/child_process.ts +310 -80
  75. package/src/shims/crypto.ts +115 -100
  76. package/src/shims/dns.ts +17 -1
  77. package/src/shims/process.ts +33 -11
  78. package/src/shims/string_decoder.ts +10 -6
  79. package/src/shims/tls.ts +49 -19
  80. package/src/source-edits.ts +20 -0
package/dist/__sw__.js CHANGED
@@ -15,6 +15,35 @@ let requestId = 0;
15
15
 
16
16
  // Registered virtual server ports
17
17
  const registeredPorts = new Set();
18
+ // HTTP response flow control is a transport property, never a path/package
19
+ // heuristic. Legacy ports keep their existing protocol until their host opts in.
20
+ const flowControlledPorts = new Map();
21
+ const controlledUploads = new Set();
22
+ const MAX_STREAM_CHUNK_BYTES = 65536;
23
+
24
+ function updateFlowControl(type, data) {
25
+ if (type === 'server-registered' || type === 'server-unregistered') {
26
+ for (const upload of controlledUploads) {
27
+ if (upload.port === data?.port) upload.controller.abort(new Error('Virtual server registration changed'));
28
+ }
29
+ // A port number is not a listener identity. Rebinding must terminate the
30
+ // old connection, even when the replacement advertises identical limits.
31
+ // Otherwise an already-headed response can wait forever on its old owner.
32
+ for (const pending of pendingRequests.values()) {
33
+ if (pending.flowControl === 1 && pending.port === data?.port)
34
+ pending.fail(new Error(type === 'server-unregistered'
35
+ ? 'Virtual server closed' : 'Virtual server registration changed'));
36
+ }
37
+ }
38
+ if (type === 'server-unregistered') {
39
+ flowControlledPorts.delete(data?.port);
40
+ }
41
+ if (type !== 'server-registered' || !data) return;
42
+ if (data.flowControl === 1 && Number.isSafeInteger(data.maxRequestBytes)
43
+ && data.maxRequestBytes > 0 && data.maxChunkBytes === MAX_STREAM_CHUNK_BYTES) {
44
+ flowControlledPorts.set(data.port, { maxRequestBytes: data.maxRequestBytes, channel: mainPort });
45
+ } else flowControlledPorts.delete(data.port);
46
+ }
18
47
 
19
48
  /**
20
49
  * Decode base64 string to Uint8Array
@@ -39,8 +68,24 @@ self.addEventListener('message', (event) => {
39
68
  // When a MessagePort is transferred, it's in event.ports[0], not event.data.port
40
69
  if (type === 'init' && event.ports && event.ports[0]) {
41
70
  // Initialize communication channel
71
+ for (const pending of pendingRequests.values()) {
72
+ if (pending.flowControl === 1) pending.fail(new Error('Host connection replaced'));
73
+ }
74
+ for (const upload of controlledUploads) upload.controller.abort(new Error('Host connection replaced'));
75
+ if (mainPort) {
76
+ mainPort.onmessage = null;
77
+ mainPort.close();
78
+ }
79
+ registeredPorts.clear();
80
+ flowControlledPorts.clear();
81
+ primaryPort = null;
82
+ // Existing preview documents/workers retain their virtual origin identity;
83
+ // a channel reconnect must not send their root-relative requests to network.
42
84
  mainPort = event.ports[0];
43
- mainPort.onmessage = handleMainMessage;
85
+ const initializedPort = mainPort;
86
+ mainPort.onmessage = (message) => {
87
+ if (mainPort === initializedPort) handleMainMessage(message);
88
+ };
44
89
  if (data && Array.isArray(data.ownDocuments)) ownDocuments = new Set(data.ownDocuments.map((path) => ownPath(String(path))));
45
90
  DEBUG && console.log('[SW] Initialized communication channel with transferred port');
46
91
  // Re-claim clients so that pages opened after SW activation get controlled.
@@ -66,6 +111,13 @@ self.addEventListener('message', (event) => {
66
111
  */
67
112
  function handleMainMessage(event) {
68
113
  const { type, id, data, error } = event.data;
114
+ updateFlowControl(type, data);
115
+
116
+ const controlled = pendingRequests.get(id);
117
+ if (controlled?.flowControl === 1) {
118
+ controlled.message(type, data, error);
119
+ return;
120
+ }
69
121
 
70
122
  // The page's bridge speaks over this port: which servers it holds, which
71
123
  // is the preview's, and which documents are the page's own frames.
@@ -257,6 +309,150 @@ async function sendStreamingRequest(port, method, url, headers, body) {
257
309
  return { stream, headersPromise, id };
258
310
  }
259
311
 
312
+ // A Node HTTP producer must stop when its consumer stops reading and must
313
+ // observe disconnect. One pull credit bounds the service-worker response queue;
314
+ // this protocol requires a paired host bridge and is negotiated per port.
315
+ async function sendControlledRequest(port, method, url, headers, body, signal) {
316
+ if (!mainPort) throw new Error('Service Worker not initialized');
317
+ if (signal.aborted) throw signal.reason;
318
+ const channel = mainPort;
319
+ const id = ++requestId;
320
+ let controller, resolveHeaders, rejectHeaders, resolvePull;
321
+ let headed = false, credited = false, ended = false;
322
+ const headersPromise = new Promise((resolve, reject) => {
323
+ resolveHeaders = resolve;
324
+ rejectHeaders = reject;
325
+ });
326
+ // A synchronous postMessage failure can happen before the first await.
327
+ void headersPromise.catch(() => {});
328
+ const send = (type) => channel.postMessage({ type, id });
329
+ const finish = () => {
330
+ ended = true;
331
+ clearTimeout(headerTimeout);
332
+ pendingRequests.delete(id);
333
+ signal.removeEventListener('abort', abort);
334
+ resolvePull?.();
335
+ resolvePull = undefined;
336
+ };
337
+ const fail = (error, notify = true) => {
338
+ if (ended) return;
339
+ if (notify) {
340
+ try { send('stream-cancel'); } catch { /* connection gone */ }
341
+ }
342
+ rejectHeaders(error);
343
+ controller.error(error);
344
+ finish();
345
+ };
346
+ const abort = () => fail(signal.reason ?? new DOMException('Request cancelled', 'AbortError'));
347
+ // Match the existing buffered request's finite header wait. A disappeared
348
+ // Host must not retain this request forever when no AbortSignal fires.
349
+ const headerTimeout = setTimeout(() => fail(new Error('Request header timeout')), 30000);
350
+ const stream = new ReadableStream({
351
+ start(value) { controller = value; },
352
+ pull() {
353
+ if (ended) return;
354
+ if (credited) throw new Error('Duplicate stream credit');
355
+ credited = true;
356
+ return new Promise((resolve) => {
357
+ resolvePull = resolve;
358
+ try { send('stream-pull'); } catch (error) { fail(error, false); }
359
+ });
360
+ },
361
+ cancel() {
362
+ if (ended) return;
363
+ try { send('stream-cancel'); } finally { finish(); }
364
+ },
365
+ }, { highWaterMark: 0 });
366
+ pendingRequests.set(id, {
367
+ flowControl: 1,
368
+ fail,
369
+ port,
370
+ message(type, data, error) {
371
+ try {
372
+ if (type === 'stream-start' && !headed) {
373
+ headed = true;
374
+ clearTimeout(headerTimeout);
375
+ resolveHeaders(data);
376
+ } else if (type === 'stream-chunk' && headed && credited) {
377
+ if (typeof data?.chunkBase64 !== 'string'
378
+ || data.chunkBase64.length > Math.ceil(MAX_STREAM_CHUNK_BYTES / 3) * 4)
379
+ throw new Error('Stream chunk exceeds transport limit');
380
+ const bytes = base64ToBytes(data.chunkBase64);
381
+ if (bytes.byteLength > MAX_STREAM_CHUNK_BYTES)
382
+ throw new Error('Stream chunk exceeds transport limit');
383
+ credited = false;
384
+ controller.enqueue(bytes);
385
+ resolvePull?.();
386
+ resolvePull = undefined;
387
+ } else if (type === 'stream-end' && headed) {
388
+ controller.close();
389
+ finish();
390
+ } else if (type === 'stream-error' || (type === 'response' && error)) {
391
+ fail(new Error(error || data?.message || 'Host stream failed'), false);
392
+ } else throw new Error('Invalid flow-controlled stream message');
393
+ } catch (cause) { fail(cause); }
394
+ },
395
+ });
396
+ signal.addEventListener('abort', abort, { once: true });
397
+ try {
398
+ channel.postMessage({ type: 'request', id,
399
+ data: { port, method, url, headers, body, streaming: true, flowControl: 1 } });
400
+ if (signal.aborted) abort();
401
+ const head = await headersPromise;
402
+ const noBody = method === 'HEAD' || [204, 205, 304].includes(head.statusCode);
403
+ if (noBody) await stream.cancel();
404
+ // Match the existing virtual-port document isolation policy. Negotiating
405
+ // flow control must not turn a working preview into a blocked iframe.
406
+ const responseHeaders = new Headers(head.headers);
407
+ responseHeaders.set('Cross-Origin-Embedder-Policy', 'credentialless');
408
+ responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
409
+ responseHeaders.set('Cross-Origin-Resource-Policy', 'cross-origin');
410
+ responseHeaders.delete('X-Frame-Options');
411
+ return new Response(noBody ? null : stream, {
412
+ status: head.statusCode, statusText: head.statusMessage, headers: responseHeaders,
413
+ });
414
+ } catch (error) {
415
+ fail(error);
416
+ throw error;
417
+ }
418
+ }
419
+
420
+ async function readRequestBody(request, limit, registrationSignal) {
421
+ if (!request.body) return null;
422
+ const reader = request.body.getReader();
423
+ const chunks = [];
424
+ let bytes = 0;
425
+ const checkAbort = () => {
426
+ if (request.signal.aborted) throw request.signal.reason;
427
+ if (registrationSignal.aborted) throw registrationSignal.reason;
428
+ };
429
+ const abort = () => { void reader.cancel().catch(() => {}); };
430
+ request.signal.addEventListener('abort', abort, { once: true });
431
+ registrationSignal.addEventListener('abort', abort, { once: true });
432
+ try {
433
+ while (true) {
434
+ checkAbort();
435
+ const { value, done } = await reader.read();
436
+ checkAbort();
437
+ if (done) break;
438
+ bytes += value.byteLength;
439
+ if (bytes > limit) throw Object.assign(new Error('Request body exceeds transport limit'), { status: 413 });
440
+ chunks.push(value);
441
+ }
442
+ const result = new Uint8Array(bytes);
443
+ let offset = 0;
444
+ for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.byteLength; }
445
+ return result.buffer;
446
+ } catch (error) {
447
+ void reader.cancel(error).catch(() => {});
448
+ throw error;
449
+ } finally {
450
+ request.signal.removeEventListener('abort', abort);
451
+ registrationSignal.removeEventListener('abort', abort);
452
+ reader.releaseLock();
453
+ }
454
+ }
455
+
260
456
  // The preview's server, whose documents are served at this origin's root as
261
457
  // they would be at their own origin: an app reads its routes from
262
458
  // `location.pathname`, and a document under /__virtual__/<port>/ is not at
@@ -268,6 +464,41 @@ let ownDocuments = new Set();
268
464
  // The documents and workers served from a virtual server at the root, by
269
465
  // client id, so their requests reach that server without a prefix to read.
270
466
  const rootedClients = new Map();
467
+ const pendingBlobContexts = new Map();
468
+
469
+ // Blob workers have no script fetch at which to remember resultingClientId,
470
+ // and their URL contains no virtual port. Ask the creating preview document
471
+ // to identify its object URL, then derive the port from that document's own
472
+ // routing context. The document supplies ownership, never a trusted port.
473
+ // Querying also recovers attribution after this service worker restarts.
474
+ async function blobWorkerContext(client, requestUrl) {
475
+ if (pendingBlobContexts.has(client.id)) return pendingBlobContexts.get(client.id);
476
+ const pending = (async () => {
477
+ const windows = await self.clients.matchAll({ type: 'window' });
478
+ const candidates = await Promise.all(windows.map(async (owner) => {
479
+ const context = await virtualContext({ clientId: owner.id, request: {
480
+ url: requestUrl, referrer: owner.url, mode: 'cors', destination: '',
481
+ } });
482
+ if (!context) return null;
483
+ return new Promise((resolve) => {
484
+ const channel = new MessageChannel();
485
+ const finish = (value) => { clearTimeout(timer); channel.port1.close(); resolve(value); };
486
+ const timer = setTimeout(() => finish(null), 1000);
487
+ channel.port1.onmessage = (event) => finish(event.data === true ? context : null);
488
+ try { owner.postMessage({ type: 'virtual-blob-owner', url: client.url }, [channel.port2]); }
489
+ catch { finish(null); }
490
+ });
491
+ }));
492
+ const owners = candidates.filter(Boolean);
493
+ if (owners.length === 0) return null;
494
+ if (owners.some(owner => owner.port !== owners[0].port)) throw new Error('Ambiguous virtual blob owner');
495
+ rootedClients.set(client.id, owners[0].port);
496
+ if (rootedClients.size > 256) pruneRootedClients();
497
+ return owners[0];
498
+ })();
499
+ pendingBlobContexts.set(client.id, pending);
500
+ try { return await pending; } finally { pendingBlobContexts.delete(client.id); }
501
+ }
271
502
 
272
503
  /**
273
504
  * The virtual server a request outside /__virtual__/ is for, if any: the
@@ -302,6 +533,9 @@ async function virtualContext(event) {
302
533
  } catch (e) { /* not a URL */ }
303
534
  }
304
535
  if (event.clientId && rootedClients.has(event.clientId)) return { prefix: null, port: rootedClients.get(event.clientId) };
536
+ if (client && (client.type === 'worker' || client.type === 'sharedworker') && client.url.startsWith('blob:')) {
537
+ return blobWorkerContext(client, event.request.url);
538
+ }
305
539
  if (primaryPort === null) return null;
306
540
  if (event.request.mode === 'navigate') {
307
541
  if (event.request.destination !== 'iframe' && event.request.destination !== 'frame') return null;
@@ -395,16 +629,29 @@ self.addEventListener('fetch', (event) => {
395
629
  */
396
630
  async function handleVirtualRequest(request, port, path, rooted = false) {
397
631
  try {
632
+ const registration = flowControlledPorts.get(port);
398
633
  // Build headers object
399
634
  const headers = {};
400
635
  request.headers.forEach((value, key) => {
401
636
  headers[key] = value;
402
637
  });
638
+ // Fetch omits Host from Request.headers. Preserve the authority addressed
639
+ // by the browser, as an HTTP socket would, rather than letting the server
640
+ // substitute its listening port. Routing to a virtual port does not change
641
+ // the client's authority; server-generated resource identities must agree
642
+ // with those the client computes from its own URL.
643
+ if (headers.host === undefined) headers.host = new URL(request.url).host;
403
644
 
404
645
  // Get body if present
405
646
  let body = null;
406
647
  if (request.method !== 'GET' && request.method !== 'HEAD') {
407
- body = await request.arrayBuffer();
648
+ if (registration) {
649
+ const upload = { port, controller: new AbortController() };
650
+ controlledUploads.add(upload);
651
+ try {
652
+ body = await readRequestBody(request, registration.maxRequestBytes, upload.controller.signal);
653
+ } finally { controlledUploads.delete(upload); }
654
+ } else body = await request.arrayBuffer();
408
655
  // A body's length is a header Node's server always has, content-length
409
656
  // or a chunked transfer-encoding; a fetch's headers carry neither, and
410
657
  // Express 5's body parser takes their absence for no body at all.
@@ -413,7 +660,13 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
413
660
  }
414
661
  }
415
662
 
416
- // Check if this is an API route that might stream (POST to /api/*)
663
+ if (registration) {
664
+ if (flowControlledPorts.get(port) !== registration || mainPort !== registration.channel)
665
+ throw new Error('Virtual server registration changed during upload');
666
+ return await sendControlledRequest(port, request.method, path, headers, body, request.signal);
667
+ }
668
+
669
+ // Legacy peers retain their old selection until they negotiate flow control.
417
670
  const isStreamingCandidate = request.method === 'POST' && path.startsWith('/api/');
418
671
 
419
672
  if (isStreamingCandidate) {
@@ -475,10 +728,11 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
475
728
 
476
729
  return finalResponse;
477
730
  } catch (error) {
731
+ if (request.signal.aborted) throw error;
478
732
  console.error('[SW] Error handling virtual request:', error);
479
733
  return new Response(`Service Worker Error: ${error.message}`, {
480
- status: 500,
481
- statusText: 'Internal Server Error',
734
+ status: error.status === 413 ? 413 : 500,
735
+ statusText: error.status === 413 ? 'Content Too Large' : 'Internal Server Error',
482
736
  headers: { 'Content-Type': 'text/plain' },
483
737
  });
484
738
  }