@scelar/nodepod 1.0.8 → 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.
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
4
4
 
5
- const index = require('./index-DVbLKdL5.cjs');
5
+ const index = require('./index-DfyUKyNH.cjs');
6
6
 
7
7
 
8
8
 
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { q, I, s, t, v, w, x, y, z, P, A, C, F, G, S, H, J, K, O, Q, U, X, Z, _, $, a0, a1, a2, a2 as a22, a3, a4, a5, a6, a7, m, a8, a9, aa, ab, ac, ad, ae, af, ag, ah, ai, aj, ak, al, am, an, ao, ap, aq, ar, as, at, au } from "./index-DpqCP69G.js";
1
+ import { q, I, s, t, v, w, x, y, z, P, A, C, F, G, S, H, J, K, O, Q, U, X, Z, _, $, a0, a1, a2, a2 as a22, a3, a4, a5, a6, a7, m, a8, a9, aa, ab, ac, ad, ae, af, ag, ah, ai, aj, ak, al, am, an, ao, ap, aq, ar, as, at, au } from "./index-CK6KRbI1.js";
2
2
  export {
3
3
  q as DependencyInstaller,
4
4
  I as IframeSandbox,
@@ -23,7 +23,7 @@ export interface EngineOptions {
23
23
  workerData: unknown;
24
24
  threadId: number;
25
25
  };
26
- handler?: import('./memory-handler').MemoryHandler;
26
+ handler?: import("./memory-handler").MemoryHandler;
27
27
  }
28
28
  export interface ResolverFn {
29
29
  (id: string): unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scelar/nodepod",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Browser-native Node.js runtime environment",
5
5
  "type": "module",
6
6
  "license": "MIT WITH Commons-Clause",
@@ -1,75 +1,75 @@
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
+ // 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,145 +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
- 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
- }
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
+ }