@scelar/nodepod 1.0.7 → 1.0.9

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 (42) hide show
  1. package/README.md +252 -240
  2. package/dist/__sw__.js +31 -5
  3. package/dist/{child_process-bGGe8mTj.cjs → child_process-DldQfPd9.cjs} +7431 -7435
  4. package/dist/child_process-DldQfPd9.cjs.map +1 -0
  5. package/dist/{child_process-CgnmoilU.js → child_process-eiM1_nmq.js} +8231 -8234
  6. package/dist/child_process-eiM1_nmq.js.map +1 -0
  7. package/dist/cross-origin.d.ts +2 -0
  8. package/dist/{index-DZpqX03n.js → index-CK6KRbI1.js} +37316 -36899
  9. package/dist/index-CK6KRbI1.js.map +1 -0
  10. package/dist/{index-NinyWmnj.cjs → index-DfyUKyNH.cjs} +39254 -38824
  11. package/dist/index-DfyUKyNH.cjs.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/request-proxy.d.ts +3 -0
  18. package/dist/script-engine.d.ts +1 -1
  19. package/dist/sdk/nodepod.d.ts +58 -58
  20. package/dist/sdk/types.d.ts +3 -0
  21. package/dist/threading/offload-types.d.ts +1 -0
  22. package/package.json +97 -97
  23. package/src/cross-origin.ts +75 -26
  24. package/src/iframe-sandbox.ts +145 -141
  25. package/src/isolation-helpers.ts +154 -148
  26. package/src/packages/archive-extractor.ts +251 -248
  27. package/src/packages/installer.ts +1 -0
  28. package/src/packages/version-resolver.ts +2 -0
  29. package/src/polyfills/net.ts +353 -353
  30. package/src/polyfills/util.ts +559 -559
  31. package/src/polyfills/worker_threads.ts +326 -326
  32. package/src/request-proxy.ts +43 -9
  33. package/src/script-engine.ts +3733 -3722
  34. package/src/sdk/nodepod.ts +8 -0
  35. package/src/sdk/types.ts +3 -0
  36. package/src/shell/shell-builtins.ts +19 -19
  37. package/src/threading/offload-types.ts +113 -112
  38. package/src/threading/offload-worker.ts +15 -0
  39. package/dist/child_process-CgnmoilU.js.map +0 -1
  40. package/dist/child_process-bGGe8mTj.cjs.map +0 -1
  41. package/dist/index-DZpqX03n.js.map +0 -1
  42. package/dist/index-NinyWmnj.cjs.map +0 -1
@@ -1,26 +1,75 @@
1
- // optional CORS proxy for APIs that don't allow browser origins
2
-
3
- let activeProxy: string | null = null;
4
-
5
- export function setProxy(url: string | null): void {
6
- activeProxy = url;
7
- }
8
-
9
- export function getProxy(): string | null {
10
- return activeProxy;
11
- }
12
-
13
- export function isProxyActive(): boolean {
14
- return activeProxy !== null;
15
- }
16
-
17
- export async function proxiedFetch(url: string, init?: RequestInit): Promise<Response> {
18
- if (activeProxy) {
19
- return fetch(activeProxy + encodeURIComponent(url), init);
20
- }
21
- return fetch(url, init);
22
- }
23
-
24
- export function resolveProxyUrl(url: string): string {
25
- return activeProxy ? activeProxy + encodeURIComponent(url) : url;
26
- }
1
+ // optional CORS proxy for APIs that don't allow browser origins
2
+
3
+ let activeProxy: string | null = null;
4
+ let allowedDomains: Set<string> | null = null;
5
+
6
+ const DEFAULT_ALLOWED_DOMAINS = [
7
+ 'registry.npmjs.org',
8
+ 'github.com',
9
+ 'raw.githubusercontent.com',
10
+ 'api.github.com',
11
+ 'objects.githubusercontent.com',
12
+ 'esm.sh',
13
+ 'unpkg.com',
14
+ 'cdn.jsdelivr.net',
15
+ 'localhost',
16
+ '127.0.0.1',
17
+ ];
18
+
19
+ export function setProxy(url: string | null): void {
20
+ activeProxy = url;
21
+ }
22
+
23
+ export function getProxy(): string | null {
24
+ return activeProxy;
25
+ }
26
+
27
+ export function isProxyActive(): boolean {
28
+ return activeProxy !== null;
29
+ }
30
+
31
+ // set allowed domains for proxied fetches. extra domains get merged with defaults.
32
+ // pass null to turn off the whitelist
33
+ export function setAllowedDomains(domains: string[] | null): void {
34
+ if (domains === null) {
35
+ allowedDomains = null;
36
+ return;
37
+ }
38
+ allowedDomains = new Set([...DEFAULT_ALLOWED_DOMAINS, ...domains]);
39
+ }
40
+
41
+ export function getAllowedDomains(): string[] | null {
42
+ return allowedDomains ? [...allowedDomains] : null;
43
+ }
44
+
45
+ function isDomainAllowed(url: string): boolean {
46
+ if (!allowedDomains) return true;
47
+ try {
48
+ const hostname = new URL(url).hostname;
49
+ for (const allowed of allowedDomains) {
50
+ if (hostname === allowed || hostname.endsWith('.' + allowed)) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export async function proxiedFetch(url: string, init?: RequestInit): Promise<Response> {
61
+ if (activeProxy) {
62
+ if (!isDomainAllowed(url)) {
63
+ throw new Error(`Fetch blocked: "${new URL(url).hostname}" is not in the allowedFetchDomains whitelist`);
64
+ }
65
+ return fetch(activeProxy + encodeURIComponent(url), init);
66
+ }
67
+ return fetch(url, init);
68
+ }
69
+
70
+ export function resolveProxyUrl(url: string): string {
71
+ if (activeProxy && !isDomainAllowed(url)) {
72
+ throw new Error(`Fetch blocked: "${new URL(url).hostname}" is not in the allowedFetchDomains whitelist`);
73
+ }
74
+ return activeProxy ? activeProxy + encodeURIComponent(url) : url;
75
+ }
@@ -1,141 +1,145 @@
1
- // executes code in a cross-origin iframe for maximum browser isolation
2
-
3
- import type { MemoryVolume } from './memory-volume';
4
- import type { IScriptEngine, ExecutionOutcome, EngineConfig, VolumeSnapshot } from './engine-types';
5
-
6
- interface CrossOriginMessage {
7
- type: 'init' | 'execute' | 'runFile' | 'clearCache' | 'syncFile' | 'ready' | 'result' | 'error' | 'console';
8
- id?: string;
9
- code?: string;
10
- filename?: string;
11
- snapshot?: VolumeSnapshot;
12
- config?: EngineConfig;
13
- result?: ExecutionOutcome;
14
- error?: string;
15
- path?: string;
16
- content?: string | null;
17
- consoleMethod?: string;
18
- consoleArgs?: unknown[];
19
- }
20
-
21
- export class IframeSandbox implements IScriptEngine {
22
- private frame: HTMLIFrameElement;
23
- private targetOrigin: string;
24
- private vol: MemoryVolume;
25
- private cfg: EngineConfig;
26
- private ready: Promise<void>;
27
- private pendingCalls = new Map<string, { resolve: (r: ExecutionOutcome) => void; reject: (e: Error) => void }>();
28
- private nextId = 0;
29
- private onFileChange: ((p: string, c: string) => void) | null = null;
30
- private onFileDelete: ((p: string) => void) | null = null;
31
- private onMessage: ((e: MessageEvent) => void) | null = null;
32
-
33
- constructor(sandboxUrl: string, vol: MemoryVolume, cfg: EngineConfig = {}) {
34
- this.targetOrigin = new URL(sandboxUrl).origin;
35
- this.vol = vol;
36
- this.cfg = cfg;
37
-
38
- this.frame = document.createElement('iframe');
39
- this.frame.src = sandboxUrl;
40
- this.frame.style.display = 'none';
41
- // @ts-expect-error - credentialless attribute may not exist in types
42
- this.frame.credentialless = true;
43
- this.frame.setAttribute('credentialless', '');
44
- document.body.appendChild(this.frame);
45
-
46
- this.bindMessageHandler();
47
- this.ready = this.awaitReady().then(() => this.sendInit());
48
- this.attachVolumeSync();
49
- }
50
-
51
- private bindMessageHandler(): void {
52
- this.onMessage = (event: MessageEvent) => {
53
- if (event.origin !== this.targetOrigin) return;
54
- const msg = event.data as CrossOriginMessage;
55
-
56
- if (msg.type === 'result' && msg.id) {
57
- const pending = this.pendingCalls.get(msg.id);
58
- if (pending && msg.result) { pending.resolve(msg.result); this.pendingCalls.delete(msg.id); }
59
- } else if (msg.type === 'error' && msg.id) {
60
- const pending = this.pendingCalls.get(msg.id);
61
- if (pending) { pending.reject(new Error(msg.error || 'Sandbox error')); this.pendingCalls.delete(msg.id); }
62
- } else if (msg.type === 'console' && this.cfg.onConsole) {
63
- this.cfg.onConsole(msg.consoleMethod || 'log', msg.consoleArgs || []);
64
- }
65
- };
66
- window.addEventListener('message', this.onMessage);
67
- }
68
-
69
- private awaitReady(): Promise<void> {
70
- return new Promise(resolve => {
71
- const handler = (event: MessageEvent) => {
72
- if (event.origin !== this.targetOrigin) return;
73
- if ((event.data as CrossOriginMessage).type === 'ready') {
74
- window.removeEventListener('message', handler);
75
- resolve();
76
- }
77
- };
78
- window.addEventListener('message', handler);
79
- });
80
- }
81
-
82
- private async sendInit(): Promise<void> {
83
- const msg: CrossOriginMessage = {
84
- type: 'init',
85
- snapshot: this.vol.toSnapshot(),
86
- config: { cwd: this.cfg.cwd, env: this.cfg.env },
87
- };
88
- this.frame.contentWindow?.postMessage(msg, this.targetOrigin);
89
- }
90
-
91
- private attachVolumeSync(): void {
92
- this.onFileChange = (path, content) => {
93
- this.frame.contentWindow?.postMessage({ type: 'syncFile', path, content } as CrossOriginMessage, this.targetOrigin);
94
- };
95
- this.vol.on('change', this.onFileChange);
96
-
97
- this.onFileDelete = (path) => {
98
- this.frame.contentWindow?.postMessage({ type: 'syncFile', path, content: null } as CrossOriginMessage, this.targetOrigin);
99
- };
100
- this.vol.on('delete', this.onFileDelete);
101
- }
102
-
103
- private dispatch(msg: CrossOriginMessage): Promise<ExecutionOutcome> {
104
- return new Promise((resolve, reject) => {
105
- const id = String(this.nextId++);
106
- this.pendingCalls.set(id, { resolve, reject });
107
- this.frame.contentWindow?.postMessage({ ...msg, id }, this.targetOrigin);
108
- setTimeout(() => {
109
- if (this.pendingCalls.has(id)) {
110
- this.pendingCalls.delete(id);
111
- reject(new Error('Sandbox execution timeout'));
112
- }
113
- }, 60000);
114
- });
115
- }
116
-
117
- async execute(code: string, filename?: string): Promise<ExecutionOutcome> {
118
- await this.ready;
119
- return this.dispatch({ type: 'execute', code, filename });
120
- }
121
-
122
- async runFile(filename: string): Promise<ExecutionOutcome> {
123
- await this.ready;
124
- return this.dispatch({ type: 'runFile', filename });
125
- }
126
-
127
- clearCache(): void {
128
- this.frame.contentWindow?.postMessage({ type: 'clearCache' } as CrossOriginMessage, this.targetOrigin);
129
- }
130
-
131
- getVolume(): MemoryVolume { return this.vol; }
132
-
133
- terminate(): void {
134
- if (this.onFileChange) this.vol.off('change', this.onFileChange);
135
- if (this.onFileDelete) this.vol.off('delete', this.onFileDelete);
136
- if (this.onMessage) window.removeEventListener('message', this.onMessage);
137
- this.frame.remove();
138
- for (const [, { reject }] of this.pendingCalls) reject(new Error('Sandbox terminated'));
139
- this.pendingCalls.clear();
140
- }
141
- }
1
+ // executes code in a cross-origin iframe for maximum browser isolation
2
+
3
+ import type { MemoryVolume } from './memory-volume';
4
+ import type { IScriptEngine, ExecutionOutcome, EngineConfig, VolumeSnapshot } from './engine-types';
5
+
6
+ interface CrossOriginMessage {
7
+ type: 'init' | 'execute' | 'runFile' | 'clearCache' | 'syncFile' | 'ready' | 'result' | 'error' | 'console';
8
+ id?: string;
9
+ code?: string;
10
+ filename?: string;
11
+ snapshot?: VolumeSnapshot;
12
+ config?: EngineConfig;
13
+ result?: ExecutionOutcome;
14
+ error?: string;
15
+ path?: string;
16
+ content?: string | null;
17
+ consoleMethod?: string;
18
+ consoleArgs?: unknown[];
19
+ }
20
+
21
+ export class IframeSandbox implements IScriptEngine {
22
+ private frame: HTMLIFrameElement;
23
+ private targetOrigin: string;
24
+ private vol: MemoryVolume;
25
+ private cfg: EngineConfig;
26
+ private ready: Promise<void>;
27
+ private pendingCalls = new Map<string, { resolve: (r: ExecutionOutcome) => void; reject: (e: Error) => void }>();
28
+ private nextId = 0;
29
+ private onFileChange: ((p: string, c: string) => void) | null = null;
30
+ private onFileDelete: ((p: string) => void) | null = null;
31
+ private onMessage: ((e: MessageEvent) => void) | null = null;
32
+
33
+ constructor(sandboxUrl: string, vol: MemoryVolume, cfg: EngineConfig = {}) {
34
+ this.targetOrigin = new URL(sandboxUrl).origin;
35
+ this.vol = vol;
36
+ this.cfg = cfg;
37
+
38
+ this.frame = document.createElement('iframe');
39
+ this.frame.src = sandboxUrl;
40
+ this.frame.style.display = 'none';
41
+ // @ts-expect-error - credentialless attribute may not exist in types
42
+ this.frame.credentialless = true;
43
+ this.frame.setAttribute('credentialless', '');
44
+ this.frame.setAttribute('sandbox', 'allow-scripts');
45
+ document.body.appendChild(this.frame);
46
+
47
+ this.bindMessageHandler();
48
+ this.ready = this.awaitReady().then(() => this.sendInit());
49
+ this.attachVolumeSync();
50
+ }
51
+
52
+ private bindMessageHandler(): void {
53
+ this.onMessage = (event: MessageEvent) => {
54
+ // sandbox="allow-scripts" without allow-same-origin makes the origin "null"
55
+ if (event.origin !== 'null' && event.origin !== this.targetOrigin) return;
56
+ const msg = event.data as CrossOriginMessage;
57
+
58
+ if (msg.type === 'result' && msg.id) {
59
+ const pending = this.pendingCalls.get(msg.id);
60
+ if (pending && msg.result) { pending.resolve(msg.result); this.pendingCalls.delete(msg.id); }
61
+ } else if (msg.type === 'error' && msg.id) {
62
+ const pending = this.pendingCalls.get(msg.id);
63
+ if (pending) { pending.reject(new Error(msg.error || 'Sandbox error')); this.pendingCalls.delete(msg.id); }
64
+ } else if (msg.type === 'console' && this.cfg.onConsole) {
65
+ this.cfg.onConsole(msg.consoleMethod || 'log', msg.consoleArgs || []);
66
+ }
67
+ };
68
+ window.addEventListener('message', this.onMessage);
69
+ }
70
+
71
+ private awaitReady(): Promise<void> {
72
+ return new Promise(resolve => {
73
+ const handler = (event: MessageEvent) => {
74
+ if (event.origin !== 'null' && event.origin !== this.targetOrigin) return;
75
+ if ((event.data as CrossOriginMessage).type === 'ready') {
76
+ window.removeEventListener('message', handler);
77
+ resolve();
78
+ }
79
+ };
80
+ window.addEventListener('message', handler);
81
+ });
82
+ }
83
+
84
+ private async sendInit(): Promise<void> {
85
+ const msg: CrossOriginMessage = {
86
+ type: 'init',
87
+ snapshot: this.vol.toSnapshot(),
88
+ config: { cwd: this.cfg.cwd, env: this.cfg.env },
89
+ };
90
+ // '*' is fine here — we're posting to this.frame.contentWindow directly,
91
+ // and sandboxed iframes have a "null" origin so we cant target them specifically
92
+ this.frame.contentWindow?.postMessage(msg, '*');
93
+ }
94
+
95
+ private attachVolumeSync(): void {
96
+ this.onFileChange = (path, content) => {
97
+ this.frame.contentWindow?.postMessage({ type: 'syncFile', path, content } as CrossOriginMessage, '*');
98
+ };
99
+ this.vol.on('change', this.onFileChange);
100
+
101
+ this.onFileDelete = (path) => {
102
+ this.frame.contentWindow?.postMessage({ type: 'syncFile', path, content: null } as CrossOriginMessage, '*');
103
+ };
104
+ this.vol.on('delete', this.onFileDelete);
105
+ }
106
+
107
+ private dispatch(msg: CrossOriginMessage): Promise<ExecutionOutcome> {
108
+ return new Promise((resolve, reject) => {
109
+ const id = String(this.nextId++);
110
+ this.pendingCalls.set(id, { resolve, reject });
111
+ this.frame.contentWindow?.postMessage({ ...msg, id }, '*');
112
+ setTimeout(() => {
113
+ if (this.pendingCalls.has(id)) {
114
+ this.pendingCalls.delete(id);
115
+ reject(new Error('Sandbox execution timeout'));
116
+ }
117
+ }, 60000);
118
+ });
119
+ }
120
+
121
+ async execute(code: string, filename?: string): Promise<ExecutionOutcome> {
122
+ await this.ready;
123
+ return this.dispatch({ type: 'execute', code, filename });
124
+ }
125
+
126
+ async runFile(filename: string): Promise<ExecutionOutcome> {
127
+ await this.ready;
128
+ return this.dispatch({ type: 'runFile', filename });
129
+ }
130
+
131
+ clearCache(): void {
132
+ this.frame.contentWindow?.postMessage({ type: 'clearCache' } as CrossOriginMessage, '*');
133
+ }
134
+
135
+ getVolume(): MemoryVolume { return this.vol; }
136
+
137
+ terminate(): void {
138
+ if (this.onFileChange) this.vol.off('change', this.onFileChange);
139
+ if (this.onFileDelete) this.vol.off('delete', this.onFileDelete);
140
+ if (this.onMessage) window.removeEventListener('message', this.onMessage);
141
+ this.frame.remove();
142
+ for (const [, { reject }] of this.pendingCalls) reject(new Error('Sandbox terminated'));
143
+ this.pendingCalls.clear();
144
+ }
145
+ }