@secure-exec/nodejs 0.2.0-rc.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.
- package/LICENSE +191 -0
- package/README.md +7 -0
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +67 -0
- package/dist/bridge/active-handles.d.ts +22 -0
- package/dist/bridge/active-handles.js +112 -0
- package/dist/bridge/child-process.d.ts +99 -0
- package/dist/bridge/child-process.js +672 -0
- package/dist/bridge/dispatch.d.ts +2 -0
- package/dist/bridge/dispatch.js +40 -0
- package/dist/bridge/fs.d.ts +502 -0
- package/dist/bridge/fs.js +3307 -0
- package/dist/bridge/index.d.ts +10 -0
- package/dist/bridge/index.js +41 -0
- package/dist/bridge/module.d.ts +75 -0
- package/dist/bridge/module.js +325 -0
- package/dist/bridge/network.d.ts +1093 -0
- package/dist/bridge/network.js +8651 -0
- package/dist/bridge/os.d.ts +13 -0
- package/dist/bridge/os.js +256 -0
- package/dist/bridge/polyfills.d.ts +9 -0
- package/dist/bridge/polyfills.js +67 -0
- package/dist/bridge/process.d.ts +121 -0
- package/dist/bridge/process.js +1382 -0
- package/dist/bridge/whatwg-url.d.ts +67 -0
- package/dist/bridge/whatwg-url.js +712 -0
- package/dist/bridge-contract.d.ts +774 -0
- package/dist/bridge-contract.js +172 -0
- package/dist/bridge-handlers.d.ts +199 -0
- package/dist/bridge-handlers.js +4263 -0
- package/dist/bridge-loader.d.ts +9 -0
- package/dist/bridge-loader.js +87 -0
- package/dist/bridge-setup.d.ts +1 -0
- package/dist/bridge-setup.js +3 -0
- package/dist/bridge.js +21652 -0
- package/dist/builtin-modules.d.ts +25 -0
- package/dist/builtin-modules.js +312 -0
- package/dist/default-network-adapter.d.ts +13 -0
- package/dist/default-network-adapter.js +351 -0
- package/dist/driver.d.ts +87 -0
- package/dist/driver.js +191 -0
- package/dist/esm-compiler.d.ts +14 -0
- package/dist/esm-compiler.js +68 -0
- package/dist/execution-driver.d.ts +37 -0
- package/dist/execution-driver.js +977 -0
- package/dist/host-network-adapter.d.ts +7 -0
- package/dist/host-network-adapter.js +279 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/isolate-bootstrap.d.ts +86 -0
- package/dist/isolate-bootstrap.js +125 -0
- package/dist/ivm-compat.d.ts +7 -0
- package/dist/ivm-compat.js +31 -0
- package/dist/kernel-runtime.d.ts +58 -0
- package/dist/kernel-runtime.js +535 -0
- package/dist/module-access.d.ts +75 -0
- package/dist/module-access.js +606 -0
- package/dist/module-resolver.d.ts +8 -0
- package/dist/module-resolver.js +150 -0
- package/dist/os-filesystem.d.ts +42 -0
- package/dist/os-filesystem.js +161 -0
- package/dist/package-bundler.d.ts +36 -0
- package/dist/package-bundler.js +497 -0
- package/dist/polyfills.d.ts +17 -0
- package/dist/polyfills.js +97 -0
- package/dist/worker-adapter.d.ts +21 -0
- package/dist/worker-adapter.js +34 -0
- package/package.json +123 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js runtime driver for kernel integration.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the existing NodeExecutionDriver behind the kernel RuntimeDriver
|
|
5
|
+
* interface. Each spawn() creates a fresh V8 isolate via NodeExecutionDriver
|
|
6
|
+
* and executes the target script. The bridge child_process.spawn routes
|
|
7
|
+
* through KernelInterface.spawn() so shell commands dispatch to WasmVM
|
|
8
|
+
* or other mounted runtimes.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import * as fsPromises from 'node:fs/promises';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { NodeExecutionDriver } from './execution-driver.js';
|
|
14
|
+
import { createNodeDriver } from './driver.js';
|
|
15
|
+
import { allowAllChildProcess, allowAllFs, createProcessScopedFileSystem, } from '@secure-exec/core';
|
|
16
|
+
/**
|
|
17
|
+
* Create a Node.js RuntimeDriver that can be mounted into the kernel.
|
|
18
|
+
*/
|
|
19
|
+
export function createNodeRuntime(options) {
|
|
20
|
+
return new NodeRuntimeDriver(options);
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// npm/npx host entry-point resolution
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Cached result of npm entry script resolution. */
|
|
26
|
+
let _npmEntryCache = null;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the npm CLI entry script on the host filesystem.
|
|
29
|
+
* Walks up from `process.execPath` (the Node binary) to find the npm
|
|
30
|
+
* package, then returns the path to `npm-cli.js`.
|
|
31
|
+
*/
|
|
32
|
+
function resolveNpmEntry() {
|
|
33
|
+
if (_npmEntryCache)
|
|
34
|
+
return _npmEntryCache;
|
|
35
|
+
// Strategy 1: resolve from node's prefix (works for most installs)
|
|
36
|
+
const nodeDir = dirname(process.execPath);
|
|
37
|
+
const candidates = [
|
|
38
|
+
// nvm / standard installs: <prefix>/lib/node_modules/npm/bin/npm-cli.js
|
|
39
|
+
join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
40
|
+
// Homebrew / some Linux layouts
|
|
41
|
+
join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.cjs'),
|
|
42
|
+
// Windows
|
|
43
|
+
join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
44
|
+
];
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
const resolved = resolve(candidate);
|
|
47
|
+
if (existsSync(resolved)) {
|
|
48
|
+
_npmEntryCache = resolved;
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Strategy 2: require.resolve from the host
|
|
53
|
+
try {
|
|
54
|
+
const npmPkg = require.resolve('npm/package.json', { paths: [nodeDir] });
|
|
55
|
+
const entry = join(dirname(npmPkg), 'bin', 'npm-cli.js');
|
|
56
|
+
if (existsSync(entry)) {
|
|
57
|
+
_npmEntryCache = entry;
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// fall through
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Could not resolve npm CLI entry script. Searched:\n' +
|
|
65
|
+
candidates.map(c => ` - ${resolve(c)}`).join('\n'));
|
|
66
|
+
}
|
|
67
|
+
/** Cached result of npx entry script resolution. */
|
|
68
|
+
let _npxEntryCache = null;
|
|
69
|
+
function resolveNpxEntry() {
|
|
70
|
+
if (_npxEntryCache)
|
|
71
|
+
return _npxEntryCache;
|
|
72
|
+
const npmEntry = resolveNpmEntry();
|
|
73
|
+
const npmBinDir = dirname(npmEntry);
|
|
74
|
+
const candidates = [
|
|
75
|
+
join(npmBinDir, 'npx-cli.js'),
|
|
76
|
+
join(npmBinDir, 'npx-cli.cjs'),
|
|
77
|
+
];
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
if (existsSync(candidate)) {
|
|
80
|
+
_npxEntryCache = candidate;
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw new Error('Could not resolve npx CLI entry script. Searched:\n' +
|
|
85
|
+
candidates.map(c => ` - ${c}`).join('\n'));
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// KernelCommandExecutor — routes child_process.spawn through the kernel
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
/**
|
|
91
|
+
* CommandExecutor adapter that wraps KernelInterface.spawn().
|
|
92
|
+
* This is the critical integration point: when code inside the V8 isolate
|
|
93
|
+
* calls child_process.spawn('sh', ['-c', 'echo hello']), the bridge
|
|
94
|
+
* delegates here, which calls kernel.spawn() to route 'sh' to WasmVM.
|
|
95
|
+
*/
|
|
96
|
+
export function createKernelCommandExecutor(kernel, parentPid) {
|
|
97
|
+
return {
|
|
98
|
+
spawn(command, args, options) {
|
|
99
|
+
// Route through kernel — this dispatches to WasmVM for shell commands,
|
|
100
|
+
// other Node instances for node commands, etc.
|
|
101
|
+
const managed = kernel.spawn(command, args, {
|
|
102
|
+
ppid: parentPid,
|
|
103
|
+
env: options.env ?? {},
|
|
104
|
+
cwd: options.cwd ?? kernel.getcwd(parentPid),
|
|
105
|
+
onStdout: options.onStdout,
|
|
106
|
+
onStderr: options.onStderr,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
writeStdin(data) {
|
|
110
|
+
managed.writeStdin(data);
|
|
111
|
+
},
|
|
112
|
+
closeStdin() {
|
|
113
|
+
managed.closeStdin();
|
|
114
|
+
},
|
|
115
|
+
kill(signal) {
|
|
116
|
+
managed.kill(signal);
|
|
117
|
+
},
|
|
118
|
+
wait() {
|
|
119
|
+
return managed.wait();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Kernel VFS adapter — adapts kernel VFS to secure-exec VirtualFileSystem
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Thin adapter from kernel VFS to secure-exec VFS interface.
|
|
130
|
+
* The kernel VFS is a superset, so this just narrows the type.
|
|
131
|
+
*/
|
|
132
|
+
export function createKernelVfsAdapter(kernelVfs) {
|
|
133
|
+
return {
|
|
134
|
+
readFile: (path) => kernelVfs.readFile(path),
|
|
135
|
+
readTextFile: (path) => kernelVfs.readTextFile(path),
|
|
136
|
+
readDir: (path) => kernelVfs.readDir(path),
|
|
137
|
+
readDirWithTypes: (path) => kernelVfs.readDirWithTypes(path),
|
|
138
|
+
writeFile: (path, content) => kernelVfs.writeFile(path, content),
|
|
139
|
+
createDir: (path) => kernelVfs.createDir(path),
|
|
140
|
+
mkdir: (path, options) => kernelVfs.mkdir(path, options),
|
|
141
|
+
exists: (path) => kernelVfs.exists(path),
|
|
142
|
+
stat: (path) => kernelVfs.stat(path),
|
|
143
|
+
removeFile: (path) => kernelVfs.removeFile(path),
|
|
144
|
+
removeDir: (path) => kernelVfs.removeDir(path),
|
|
145
|
+
rename: (oldPath, newPath) => kernelVfs.rename(oldPath, newPath),
|
|
146
|
+
symlink: (target, linkPath) => kernelVfs.symlink(target, linkPath),
|
|
147
|
+
readlink: (path) => kernelVfs.readlink(path),
|
|
148
|
+
lstat: (path) => kernelVfs.lstat(path),
|
|
149
|
+
link: (oldPath, newPath) => kernelVfs.link(oldPath, newPath),
|
|
150
|
+
chmod: (path, mode) => kernelVfs.chmod(path, mode),
|
|
151
|
+
chown: (path, uid, gid) => kernelVfs.chown(path, uid, gid),
|
|
152
|
+
utimes: (path, atime, mtime) => kernelVfs.utimes(path, atime, mtime),
|
|
153
|
+
truncate: (path, length) => kernelVfs.truncate(path, length),
|
|
154
|
+
realpath: (path) => kernelVfs.realpath(path),
|
|
155
|
+
pread: (path, offset, length) => kernelVfs.pread(path, offset, length),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Host filesystem fallback — npm/npx module resolution
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/**
|
|
162
|
+
* Wrap a VFS with host filesystem fallback for read operations.
|
|
163
|
+
*
|
|
164
|
+
* When npm/npx runs inside the V8 isolate, require() must resolve npm's own
|
|
165
|
+
* internal modules (e.g. '../lib/cli/entry'). These live on the host
|
|
166
|
+
* filesystem, not in the kernel VFS. This wrapper tries the kernel VFS first
|
|
167
|
+
* and falls back to the host filesystem for reads. Writes always go to the
|
|
168
|
+
* kernel VFS.
|
|
169
|
+
*/
|
|
170
|
+
export function createHostFallbackVfs(base) {
|
|
171
|
+
return {
|
|
172
|
+
readFile: async (path) => {
|
|
173
|
+
try {
|
|
174
|
+
return await base.readFile(path);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return new Uint8Array(await fsPromises.readFile(path));
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
readTextFile: async (path) => {
|
|
181
|
+
try {
|
|
182
|
+
return await base.readTextFile(path);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return await fsPromises.readFile(path, 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
readDir: async (path) => {
|
|
189
|
+
try {
|
|
190
|
+
return await base.readDir(path);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return await fsPromises.readdir(path);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
readDirWithTypes: async (path) => {
|
|
197
|
+
try {
|
|
198
|
+
return await base.readDirWithTypes(path);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
const entries = await fsPromises.readdir(path, { withFileTypes: true });
|
|
202
|
+
return entries.map(e => ({ name: e.name, isDirectory: e.isDirectory() }));
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
exists: async (path) => {
|
|
206
|
+
if (await base.exists(path))
|
|
207
|
+
return true;
|
|
208
|
+
try {
|
|
209
|
+
await fsPromises.access(path);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
stat: async (path) => {
|
|
217
|
+
try {
|
|
218
|
+
return await base.stat(path);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
const s = await fsPromises.stat(path);
|
|
222
|
+
return {
|
|
223
|
+
mode: s.mode,
|
|
224
|
+
size: s.size,
|
|
225
|
+
isDirectory: s.isDirectory(),
|
|
226
|
+
isSymbolicLink: false,
|
|
227
|
+
atimeMs: s.atimeMs,
|
|
228
|
+
mtimeMs: s.mtimeMs,
|
|
229
|
+
ctimeMs: s.ctimeMs,
|
|
230
|
+
birthtimeMs: s.birthtimeMs,
|
|
231
|
+
ino: s.ino,
|
|
232
|
+
nlink: s.nlink,
|
|
233
|
+
uid: s.uid,
|
|
234
|
+
gid: s.gid,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
writeFile: (path, content) => base.writeFile(path, content),
|
|
239
|
+
createDir: (path) => base.createDir(path),
|
|
240
|
+
mkdir: (path, options) => base.mkdir(path, options),
|
|
241
|
+
removeFile: (path) => base.removeFile(path),
|
|
242
|
+
removeDir: (path) => base.removeDir(path),
|
|
243
|
+
rename: (oldPath, newPath) => base.rename(oldPath, newPath),
|
|
244
|
+
symlink: (target, linkPath) => base.symlink(target, linkPath),
|
|
245
|
+
readlink: (path) => base.readlink(path),
|
|
246
|
+
lstat: (path) => base.lstat(path),
|
|
247
|
+
link: (oldPath, newPath) => base.link(oldPath, newPath),
|
|
248
|
+
chmod: (path, mode) => base.chmod(path, mode),
|
|
249
|
+
chown: (path, uid, gid) => base.chown(path, uid, gid),
|
|
250
|
+
utimes: (path, atime, mtime) => base.utimes(path, atime, mtime),
|
|
251
|
+
truncate: (path, length) => base.truncate(path, length),
|
|
252
|
+
realpath: async (path) => {
|
|
253
|
+
try {
|
|
254
|
+
return await base.realpath(path);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return await fsPromises.realpath(path);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
pread: async (path, offset, length) => {
|
|
261
|
+
try {
|
|
262
|
+
return await base.pread(path, offset, length);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
const handle = await fsPromises.open(path, 'r');
|
|
266
|
+
try {
|
|
267
|
+
const buf = new Uint8Array(length);
|
|
268
|
+
const { bytesRead } = await handle.read(buf, 0, length, offset);
|
|
269
|
+
return buf.slice(0, bytesRead);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await handle.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Node RuntimeDriver
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
class NodeRuntimeDriver {
|
|
282
|
+
name = 'node';
|
|
283
|
+
commands = ['node', 'npm', 'npx'];
|
|
284
|
+
_kernel = null;
|
|
285
|
+
_memoryLimit;
|
|
286
|
+
_permissions;
|
|
287
|
+
_bindings;
|
|
288
|
+
_activeDrivers = new Map();
|
|
289
|
+
constructor(options) {
|
|
290
|
+
this._memoryLimit = options?.memoryLimit ?? 128;
|
|
291
|
+
this._permissions = options?.permissions ?? { ...allowAllChildProcess };
|
|
292
|
+
this._bindings = options?.bindings;
|
|
293
|
+
}
|
|
294
|
+
async init(kernel) {
|
|
295
|
+
this._kernel = kernel;
|
|
296
|
+
}
|
|
297
|
+
spawn(command, args, ctx) {
|
|
298
|
+
const kernel = this._kernel;
|
|
299
|
+
if (!kernel)
|
|
300
|
+
throw new Error('Node driver not initialized');
|
|
301
|
+
// Exit plumbing
|
|
302
|
+
let resolveExit;
|
|
303
|
+
let exitResolved = false;
|
|
304
|
+
const exitPromise = new Promise((resolve) => {
|
|
305
|
+
resolveExit = (code) => {
|
|
306
|
+
if (exitResolved)
|
|
307
|
+
return;
|
|
308
|
+
exitResolved = true;
|
|
309
|
+
resolve(code);
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
// Stdin buffering — writeStdin collects data, closeStdin resolves the promise
|
|
313
|
+
const stdinChunks = [];
|
|
314
|
+
let stdinResolve = null;
|
|
315
|
+
const stdinPromise = new Promise((resolve) => {
|
|
316
|
+
stdinResolve = resolve;
|
|
317
|
+
// Auto-resolve on next microtask if nobody calls writeStdin
|
|
318
|
+
queueMicrotask(() => {
|
|
319
|
+
if (stdinChunks.length === 0 && stdinResolve) {
|
|
320
|
+
stdinResolve = null;
|
|
321
|
+
resolve(undefined);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
const proc = {
|
|
326
|
+
onStdout: null,
|
|
327
|
+
onStderr: null,
|
|
328
|
+
onExit: null,
|
|
329
|
+
writeStdin: (data) => {
|
|
330
|
+
stdinChunks.push(data);
|
|
331
|
+
},
|
|
332
|
+
closeStdin: () => {
|
|
333
|
+
if (stdinResolve) {
|
|
334
|
+
if (stdinChunks.length === 0) {
|
|
335
|
+
// No data written — pass undefined (no stdin), not empty string
|
|
336
|
+
stdinResolve(undefined);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// Concatenate buffered chunks and decode to string for exec()
|
|
340
|
+
const totalLen = stdinChunks.reduce((sum, c) => sum + c.length, 0);
|
|
341
|
+
const merged = new Uint8Array(totalLen);
|
|
342
|
+
let offset = 0;
|
|
343
|
+
for (const chunk of stdinChunks) {
|
|
344
|
+
merged.set(chunk, offset);
|
|
345
|
+
offset += chunk.length;
|
|
346
|
+
}
|
|
347
|
+
stdinResolve(new TextDecoder().decode(merged));
|
|
348
|
+
}
|
|
349
|
+
stdinResolve = null;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
kill: (_signal) => {
|
|
353
|
+
const driver = this._activeDrivers.get(ctx.pid);
|
|
354
|
+
if (driver) {
|
|
355
|
+
driver.dispose();
|
|
356
|
+
this._activeDrivers.delete(ctx.pid);
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
wait: () => exitPromise,
|
|
360
|
+
};
|
|
361
|
+
// Launch async — spawn() returns synchronously per RuntimeDriver contract
|
|
362
|
+
this._executeAsync(command, args, ctx, proc, resolveExit, stdinPromise);
|
|
363
|
+
return proc;
|
|
364
|
+
}
|
|
365
|
+
async dispose() {
|
|
366
|
+
for (const driver of this._activeDrivers.values()) {
|
|
367
|
+
try {
|
|
368
|
+
driver.dispose();
|
|
369
|
+
}
|
|
370
|
+
catch { /* best effort */ }
|
|
371
|
+
}
|
|
372
|
+
this._activeDrivers.clear();
|
|
373
|
+
this._kernel = null;
|
|
374
|
+
}
|
|
375
|
+
// -------------------------------------------------------------------------
|
|
376
|
+
// Async execution
|
|
377
|
+
// -------------------------------------------------------------------------
|
|
378
|
+
async _executeAsync(command, args, ctx, proc, resolveExit, stdinPromise) {
|
|
379
|
+
const kernel = this._kernel;
|
|
380
|
+
try {
|
|
381
|
+
// Resolve the code to execute
|
|
382
|
+
const { code, filePath } = await this._resolveEntry(command, args, kernel);
|
|
383
|
+
// Wait for stdin data (resolves immediately if no writeStdin called)
|
|
384
|
+
const stdinData = await stdinPromise;
|
|
385
|
+
// Build kernel-backed system driver
|
|
386
|
+
const commandExecutor = createKernelCommandExecutor(kernel, ctx.pid);
|
|
387
|
+
let filesystem = createProcessScopedFileSystem(createKernelVfsAdapter(kernel.vfs), ctx.pid);
|
|
388
|
+
// npm/npx need host filesystem fallback and fs permissions for module resolution
|
|
389
|
+
let permissions = { ...this._permissions };
|
|
390
|
+
if (command === 'npm' || command === 'npx') {
|
|
391
|
+
filesystem = createHostFallbackVfs(filesystem);
|
|
392
|
+
permissions = { ...permissions, ...allowAllFs };
|
|
393
|
+
}
|
|
394
|
+
// Detect PTY on stdio FDs
|
|
395
|
+
const stdinIsTTY = ctx.stdinIsTTY ?? false;
|
|
396
|
+
const stdoutIsTTY = ctx.stdoutIsTTY ?? false;
|
|
397
|
+
const stderrIsTTY = ctx.stderrIsTTY ?? false;
|
|
398
|
+
const systemDriver = createNodeDriver({
|
|
399
|
+
filesystem,
|
|
400
|
+
commandExecutor,
|
|
401
|
+
permissions,
|
|
402
|
+
processConfig: {
|
|
403
|
+
cwd: ctx.cwd,
|
|
404
|
+
env: ctx.env,
|
|
405
|
+
argv: [process.execPath, filePath ?? command, ...args],
|
|
406
|
+
stdinIsTTY,
|
|
407
|
+
stdoutIsTTY,
|
|
408
|
+
stderrIsTTY,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
// Wire PTY raw mode callback when stdin is a terminal
|
|
412
|
+
const onPtySetRawMode = stdinIsTTY
|
|
413
|
+
? (mode) => {
|
|
414
|
+
kernel.ptySetDiscipline(ctx.pid, 0, {
|
|
415
|
+
canonical: !mode,
|
|
416
|
+
echo: !mode,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
: undefined;
|
|
420
|
+
// Create a per-process isolate with kernel socket routing
|
|
421
|
+
const executionDriver = new NodeExecutionDriver({
|
|
422
|
+
system: systemDriver,
|
|
423
|
+
runtime: systemDriver.runtime,
|
|
424
|
+
memoryLimit: this._memoryLimit,
|
|
425
|
+
bindings: this._bindings,
|
|
426
|
+
onPtySetRawMode,
|
|
427
|
+
socketTable: kernel.socketTable,
|
|
428
|
+
processTable: kernel.processTable,
|
|
429
|
+
timerTable: kernel.timerTable,
|
|
430
|
+
pid: ctx.pid,
|
|
431
|
+
});
|
|
432
|
+
this._activeDrivers.set(ctx.pid, executionDriver);
|
|
433
|
+
// Execute with stdout/stderr capture and stdin data
|
|
434
|
+
const result = await executionDriver.exec(code, {
|
|
435
|
+
filePath,
|
|
436
|
+
env: ctx.env,
|
|
437
|
+
cwd: ctx.cwd,
|
|
438
|
+
stdin: stdinData,
|
|
439
|
+
onStdio: (event) => {
|
|
440
|
+
const data = new TextEncoder().encode(event.message + '\n');
|
|
441
|
+
if (event.channel === 'stdout') {
|
|
442
|
+
ctx.onStdout?.(data);
|
|
443
|
+
proc.onStdout?.(data);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
ctx.onStderr?.(data);
|
|
447
|
+
proc.onStderr?.(data);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
// Emit errorMessage as stderr (covers ReferenceError, SyntaxError, throw)
|
|
452
|
+
if (result.errorMessage) {
|
|
453
|
+
const errBytes = new TextEncoder().encode(result.errorMessage + '\n');
|
|
454
|
+
ctx.onStderr?.(errBytes);
|
|
455
|
+
proc.onStderr?.(errBytes);
|
|
456
|
+
}
|
|
457
|
+
// Cleanup isolate
|
|
458
|
+
executionDriver.dispose();
|
|
459
|
+
this._activeDrivers.delete(ctx.pid);
|
|
460
|
+
resolveExit(result.code);
|
|
461
|
+
proc.onExit?.(result.code);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
465
|
+
const errBytes = new TextEncoder().encode(`node: ${errMsg}\n`);
|
|
466
|
+
ctx.onStderr?.(errBytes);
|
|
467
|
+
proc.onStderr?.(errBytes);
|
|
468
|
+
// Cleanup on error
|
|
469
|
+
const driver = this._activeDrivers.get(ctx.pid);
|
|
470
|
+
if (driver) {
|
|
471
|
+
try {
|
|
472
|
+
driver.dispose();
|
|
473
|
+
}
|
|
474
|
+
catch { /* best effort */ }
|
|
475
|
+
this._activeDrivers.delete(ctx.pid);
|
|
476
|
+
}
|
|
477
|
+
resolveExit(1);
|
|
478
|
+
proc.onExit?.(1);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// -------------------------------------------------------------------------
|
|
482
|
+
// Entry point resolution
|
|
483
|
+
// -------------------------------------------------------------------------
|
|
484
|
+
/**
|
|
485
|
+
* Resolve the entry code and filePath for a given command.
|
|
486
|
+
* - 'node script.js' → read script from VFS
|
|
487
|
+
* - 'node -e "code"' → inline code
|
|
488
|
+
* - 'npm ...' → host npm CLI entry script
|
|
489
|
+
* - 'npx ...' → host npx CLI entry script
|
|
490
|
+
*/
|
|
491
|
+
async _resolveEntry(command, args, kernel) {
|
|
492
|
+
if (command === 'npm') {
|
|
493
|
+
const entry = resolveNpmEntry();
|
|
494
|
+
return { code: readFileSync(entry, 'utf-8'), filePath: entry };
|
|
495
|
+
}
|
|
496
|
+
if (command === 'npx') {
|
|
497
|
+
const entry = resolveNpxEntry();
|
|
498
|
+
return { code: readFileSync(entry, 'utf-8'), filePath: entry };
|
|
499
|
+
}
|
|
500
|
+
// 'node' command — parse args to find code/script
|
|
501
|
+
return this._resolveNodeArgs(args, kernel);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Parse Node CLI args to extract the code to execute.
|
|
505
|
+
* Supports: node script.js, node -e "code", node --eval "code",
|
|
506
|
+
* node -p "expr", node --print "expr"
|
|
507
|
+
*/
|
|
508
|
+
async _resolveNodeArgs(args, kernel) {
|
|
509
|
+
for (let i = 0; i < args.length; i++) {
|
|
510
|
+
const arg = args[i];
|
|
511
|
+
// -e / --eval: next arg is code
|
|
512
|
+
if ((arg === '-e' || arg === '--eval') && i + 1 < args.length) {
|
|
513
|
+
return { code: args[i + 1] };
|
|
514
|
+
}
|
|
515
|
+
// -p / --print: wrap in console.log
|
|
516
|
+
if ((arg === '-p' || arg === '--print') && i + 1 < args.length) {
|
|
517
|
+
return { code: `console.log(${args[i + 1]})` };
|
|
518
|
+
}
|
|
519
|
+
// Skip flags
|
|
520
|
+
if (arg.startsWith('-'))
|
|
521
|
+
continue;
|
|
522
|
+
// First non-flag arg is the script path
|
|
523
|
+
const scriptPath = arg;
|
|
524
|
+
try {
|
|
525
|
+
const content = await kernel.vfs.readTextFile(scriptPath);
|
|
526
|
+
return { code: content, filePath: scriptPath };
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
throw new Error(`Cannot find module '${scriptPath}'`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// No script or -e flag — read from stdin (not supported yet)
|
|
533
|
+
throw new Error('node: missing script argument (stdin mode not supported)');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { VirtualDirEntry, VirtualFileSystem, VirtualStat } from "@secure-exec/core";
|
|
2
|
+
/**
|
|
3
|
+
* Options controlling which host node_modules are projected into the sandbox.
|
|
4
|
+
* The overlay exposes `<cwd>/node_modules` read-only by default.
|
|
5
|
+
*/
|
|
6
|
+
export interface ModuleAccessOptions {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Deprecated: retained for API compatibility only.
|
|
10
|
+
* The overlay now exposes scoped <cwd>/node_modules read-only by default.
|
|
11
|
+
*/
|
|
12
|
+
allowPackages?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Union filesystem that overlays host `node_modules` (read-only) onto a base
|
|
16
|
+
* VFS. Sandbox code sees `/root/node_modules/...` which maps to the host's
|
|
17
|
+
* real `<cwd>/node_modules/...`. Write operations to the overlay throw EACCES.
|
|
18
|
+
* Symlinks are resolved and validated against the allowed-roots allowlist to
|
|
19
|
+
* prevent path-traversal escapes. Native `.node` addons are rejected.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ModuleAccessFileSystem implements VirtualFileSystem {
|
|
22
|
+
private readonly baseFileSystem?;
|
|
23
|
+
private readonly hostNodeModulesRoot;
|
|
24
|
+
private readonly overlayAllowedRoots;
|
|
25
|
+
constructor(baseFileSystem: VirtualFileSystem | undefined, options: ModuleAccessOptions);
|
|
26
|
+
private isWithinAllowedOverlayRoots;
|
|
27
|
+
private isSyntheticPath;
|
|
28
|
+
private syntheticChildren;
|
|
29
|
+
private isReadOnlyProjectionPath;
|
|
30
|
+
private shouldMergeBase;
|
|
31
|
+
private overlayHostPathFor;
|
|
32
|
+
prepareOpenSync(pathValue: string, flags: number): boolean;
|
|
33
|
+
/** Translate a sandbox path to the corresponding host path (for sync module resolution). */
|
|
34
|
+
toHostPath(sandboxPath: string): string | null;
|
|
35
|
+
/** Translate a host path back to the sandbox path (reverse of toHostPath). */
|
|
36
|
+
toSandboxPath(hostPath: string): string;
|
|
37
|
+
private resolveOverlayHostPath;
|
|
38
|
+
private readMergedDir;
|
|
39
|
+
private fallbackReadFile;
|
|
40
|
+
private fallbackReadTextFile;
|
|
41
|
+
private fallbackReadDir;
|
|
42
|
+
private fallbackReadDirWithTypes;
|
|
43
|
+
private fallbackWriteFile;
|
|
44
|
+
private fallbackCreateDir;
|
|
45
|
+
private fallbackMkdir;
|
|
46
|
+
private fallbackExists;
|
|
47
|
+
private fallbackStat;
|
|
48
|
+
private fallbackRemoveFile;
|
|
49
|
+
private fallbackRemoveDir;
|
|
50
|
+
private fallbackRename;
|
|
51
|
+
readFile(pathValue: string): Promise<Uint8Array>;
|
|
52
|
+
readTextFile(pathValue: string): Promise<string>;
|
|
53
|
+
readDir(pathValue: string): Promise<string[]>;
|
|
54
|
+
readDirWithTypes(pathValue: string): Promise<VirtualDirEntry[]>;
|
|
55
|
+
writeFile(pathValue: string, content: string | Uint8Array): Promise<void>;
|
|
56
|
+
createDir(pathValue: string): Promise<void>;
|
|
57
|
+
mkdir(pathValue: string, _options?: {
|
|
58
|
+
recursive?: boolean;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
exists(pathValue: string): Promise<boolean>;
|
|
61
|
+
stat(pathValue: string): Promise<VirtualStat>;
|
|
62
|
+
removeFile(pathValue: string): Promise<void>;
|
|
63
|
+
removeDir(pathValue: string): Promise<void>;
|
|
64
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
65
|
+
symlink(target: string, linkPath: string): Promise<void>;
|
|
66
|
+
readlink(path: string): Promise<string>;
|
|
67
|
+
lstat(path: string): Promise<VirtualStat>;
|
|
68
|
+
link(oldPath: string, newPath: string): Promise<void>;
|
|
69
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
70
|
+
chown(path: string, uid: number, gid: number): Promise<void>;
|
|
71
|
+
utimes(path: string, atime: number, mtime: number): Promise<void>;
|
|
72
|
+
truncate(path: string, length: number): Promise<void>;
|
|
73
|
+
realpath(pathValue: string): Promise<string>;
|
|
74
|
+
pread(pathValue: string, offset: number, length: number): Promise<Uint8Array>;
|
|
75
|
+
}
|