@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,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete HostNetworkAdapter for Node.js, delegating to node:net,
|
|
3
|
+
* node:dgram, and node:dns for real external I/O.
|
|
4
|
+
*/
|
|
5
|
+
import type { HostNetworkAdapter } from "@secure-exec/core";
|
|
6
|
+
/** Create a Node.js HostNetworkAdapter that uses real OS networking. */
|
|
7
|
+
export declare function createNodeHostNetworkAdapter(): HostNetworkAdapter;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete HostNetworkAdapter for Node.js, delegating to node:net,
|
|
3
|
+
* node:dgram, and node:dns for real external I/O.
|
|
4
|
+
*/
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import * as dgram from "node:dgram";
|
|
7
|
+
import * as dns from "node:dns";
|
|
8
|
+
/**
|
|
9
|
+
* Queued-read adapter: incoming data/EOF/errors are buffered so that
|
|
10
|
+
* each read() call returns the next chunk or null for EOF.
|
|
11
|
+
*/
|
|
12
|
+
class NodeHostSocket {
|
|
13
|
+
socket;
|
|
14
|
+
readQueue = [];
|
|
15
|
+
waiters = [];
|
|
16
|
+
ended = false;
|
|
17
|
+
errored = null;
|
|
18
|
+
constructor(socket) {
|
|
19
|
+
this.socket = socket;
|
|
20
|
+
socket.on("data", (chunk) => {
|
|
21
|
+
const data = new Uint8Array(chunk);
|
|
22
|
+
const waiter = this.waiters.shift();
|
|
23
|
+
if (waiter) {
|
|
24
|
+
waiter(data);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.readQueue.push(data);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
socket.on("end", () => {
|
|
31
|
+
this.ended = true;
|
|
32
|
+
const waiter = this.waiters.shift();
|
|
33
|
+
if (waiter) {
|
|
34
|
+
waiter(null);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.readQueue.push(null);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
socket.on("error", (err) => {
|
|
41
|
+
this.errored = err;
|
|
42
|
+
// Wake all pending readers with EOF
|
|
43
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
44
|
+
waiter(null);
|
|
45
|
+
}
|
|
46
|
+
if (!this.ended) {
|
|
47
|
+
this.ended = true;
|
|
48
|
+
this.readQueue.push(null);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async write(data) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
this.socket.write(data, (err) => {
|
|
55
|
+
if (err)
|
|
56
|
+
reject(err);
|
|
57
|
+
else
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async read() {
|
|
63
|
+
const queued = this.readQueue.shift();
|
|
64
|
+
if (queued !== undefined)
|
|
65
|
+
return queued;
|
|
66
|
+
if (this.ended)
|
|
67
|
+
return null;
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
this.waiters.push(resolve);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async close() {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
if (this.socket.destroyed) {
|
|
75
|
+
resolve();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.socket.once("close", () => resolve());
|
|
79
|
+
this.socket.destroy();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
setOption(level, optname, optval) {
|
|
83
|
+
// Forward common options to the real socket
|
|
84
|
+
this.socket.setNoDelay(optval !== 0);
|
|
85
|
+
}
|
|
86
|
+
shutdown(how) {
|
|
87
|
+
if (how === "write" || how === "both") {
|
|
88
|
+
this.socket.end();
|
|
89
|
+
}
|
|
90
|
+
if (how === "read" || how === "both") {
|
|
91
|
+
this.socket.pause();
|
|
92
|
+
this.socket.removeAllListeners("data");
|
|
93
|
+
if (!this.ended) {
|
|
94
|
+
this.ended = true;
|
|
95
|
+
const waiter = this.waiters.shift();
|
|
96
|
+
if (waiter)
|
|
97
|
+
waiter(null);
|
|
98
|
+
else
|
|
99
|
+
this.readQueue.push(null);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* TCP listener backed by node:net.Server. Incoming connections are
|
|
106
|
+
* queued so each accept() call returns the next one.
|
|
107
|
+
*/
|
|
108
|
+
class NodeHostListener {
|
|
109
|
+
server;
|
|
110
|
+
_port;
|
|
111
|
+
connQueue = [];
|
|
112
|
+
waiters = [];
|
|
113
|
+
closed = false;
|
|
114
|
+
constructor(server, port) {
|
|
115
|
+
this.server = server;
|
|
116
|
+
this._port = port;
|
|
117
|
+
server.on("connection", (socket) => {
|
|
118
|
+
const waiter = this.waiters.shift();
|
|
119
|
+
if (waiter) {
|
|
120
|
+
waiter(socket);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.connQueue.push(socket);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
get port() {
|
|
128
|
+
return this._port;
|
|
129
|
+
}
|
|
130
|
+
async accept() {
|
|
131
|
+
const queued = this.connQueue.shift();
|
|
132
|
+
if (queued)
|
|
133
|
+
return new NodeHostSocket(queued);
|
|
134
|
+
if (this.closed)
|
|
135
|
+
throw new Error("Listener closed");
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
if (this.closed) {
|
|
138
|
+
reject(new Error("Listener closed"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.waiters.push((socket) => {
|
|
142
|
+
resolve(new NodeHostSocket(socket));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async close() {
|
|
147
|
+
this.closed = true;
|
|
148
|
+
// Reject pending accept waiters
|
|
149
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
150
|
+
// Resolve with a destroyed socket to signal closure — caller handles
|
|
151
|
+
// the error via the socket's error/close events
|
|
152
|
+
}
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
this.server.close((err) => {
|
|
155
|
+
if (err)
|
|
156
|
+
reject(err);
|
|
157
|
+
else
|
|
158
|
+
resolve();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* UDP socket backed by node:dgram.Socket. Messages are queued
|
|
165
|
+
* so each recv() call returns the next datagram.
|
|
166
|
+
*/
|
|
167
|
+
class NodeHostUdpSocket {
|
|
168
|
+
socket;
|
|
169
|
+
msgQueue = [];
|
|
170
|
+
waiters = [];
|
|
171
|
+
closed = false;
|
|
172
|
+
constructor(socket) {
|
|
173
|
+
this.socket = socket;
|
|
174
|
+
socket.on("message", (msg, rinfo) => {
|
|
175
|
+
const entry = {
|
|
176
|
+
data: new Uint8Array(msg),
|
|
177
|
+
remoteAddr: { host: rinfo.address, port: rinfo.port },
|
|
178
|
+
};
|
|
179
|
+
const waiter = this.waiters.shift();
|
|
180
|
+
if (waiter) {
|
|
181
|
+
waiter(entry);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.msgQueue.push(entry);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async recv() {
|
|
189
|
+
const queued = this.msgQueue.shift();
|
|
190
|
+
if (queued)
|
|
191
|
+
return queued;
|
|
192
|
+
if (this.closed)
|
|
193
|
+
throw new Error("UDP socket closed");
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
if (this.closed) {
|
|
196
|
+
reject(new Error("UDP socket closed"));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.waiters.push(resolve);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async close() {
|
|
203
|
+
this.closed = true;
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
this.socket.close(() => resolve());
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Create a Node.js HostNetworkAdapter that uses real OS networking. */
|
|
210
|
+
export function createNodeHostNetworkAdapter() {
|
|
211
|
+
return {
|
|
212
|
+
async tcpConnect(host, port) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const socket = net.connect({ host, port });
|
|
215
|
+
socket.once("connect", () => {
|
|
216
|
+
socket.removeListener("error", reject);
|
|
217
|
+
resolve(new NodeHostSocket(socket));
|
|
218
|
+
});
|
|
219
|
+
socket.once("error", (err) => {
|
|
220
|
+
socket.removeListener("connect", resolve);
|
|
221
|
+
reject(err);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
async tcpListen(host, port) {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const server = net.createServer();
|
|
228
|
+
server.once("listening", () => {
|
|
229
|
+
server.removeListener("error", reject);
|
|
230
|
+
const addr = server.address();
|
|
231
|
+
resolve(new NodeHostListener(server, addr.port));
|
|
232
|
+
});
|
|
233
|
+
server.once("error", (err) => {
|
|
234
|
+
server.removeListener("listening", resolve);
|
|
235
|
+
reject(err);
|
|
236
|
+
});
|
|
237
|
+
server.listen(port, host);
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
async udpBind(host, port) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const socket = dgram.createSocket("udp4");
|
|
243
|
+
socket.once("listening", () => {
|
|
244
|
+
socket.removeListener("error", reject);
|
|
245
|
+
resolve(new NodeHostUdpSocket(socket));
|
|
246
|
+
});
|
|
247
|
+
socket.once("error", (err) => {
|
|
248
|
+
socket.removeListener("listening", resolve);
|
|
249
|
+
reject(err);
|
|
250
|
+
});
|
|
251
|
+
socket.bind(port, host);
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
async udpSend(socket, data, host, port) {
|
|
255
|
+
// Access the underlying dgram socket via the wrapper
|
|
256
|
+
const udp = socket;
|
|
257
|
+
const inner = udp.socket;
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
inner.send(data, 0, data.length, port, host, (err) => {
|
|
260
|
+
if (err)
|
|
261
|
+
reject(err);
|
|
262
|
+
else
|
|
263
|
+
resolve();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
async dnsLookup(hostname, rrtype) {
|
|
268
|
+
const family = rrtype === "AAAA" ? 6 : 4;
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
dns.lookup(hostname, { family }, (err, address, resultFamily) => {
|
|
271
|
+
if (err)
|
|
272
|
+
reject(err);
|
|
273
|
+
else
|
|
274
|
+
resolve({ address, family: resultFamily });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { getRawBridgeCode, getBridgeAttachCode } from "./bridge-loader.js";
|
|
2
|
+
export { bundlePolyfill, getAvailableStdlib, hasPolyfill, prebundleAllPolyfills, } from "./polyfills.js";
|
|
3
|
+
export { NodeExecutionDriver } from "./execution-driver.js";
|
|
4
|
+
export type { NodeExecutionDriverOptions } from "./isolate-bootstrap.js";
|
|
5
|
+
export { createDefaultNetworkAdapter, createNodeDriver, createNodeRuntimeDriverFactory, NodeFileSystem, filterEnv, isPrivateIp, } from "./driver.js";
|
|
6
|
+
export type { NodeDriverOptions, NodeRuntimeDriverFactoryOptions, } from "./driver.js";
|
|
7
|
+
export { ModuleAccessFileSystem } from "./module-access.js";
|
|
8
|
+
export type { ModuleAccessOptions } from "./module-access.js";
|
|
9
|
+
export { emitConsoleEvent, stripDangerousEnv, createProcessConfigForExecution, } from "./bridge-handlers.js";
|
|
10
|
+
export type { BindingTree, BindingFunction } from "./bindings.js";
|
|
11
|
+
export { BINDING_PREFIX, flattenBindingTree } from "./bindings.js";
|
|
12
|
+
export { createNodeRuntime } from "./kernel-runtime.js";
|
|
13
|
+
export type { NodeRuntimeOptions } from "./kernel-runtime.js";
|
|
14
|
+
export { createKernelCommandExecutor, createKernelVfsAdapter, createHostFallbackVfs, } from "./kernel-runtime.js";
|
|
15
|
+
export { HostNodeFileSystem } from "./os-filesystem.js";
|
|
16
|
+
export type { HostNodeFileSystemOptions } from "./os-filesystem.js";
|
|
17
|
+
export { NodeWorkerAdapter } from "./worker-adapter.js";
|
|
18
|
+
export type { WorkerHandle } from "./worker-adapter.js";
|
|
19
|
+
export { createNodeHostNetworkAdapter } from "./host-network-adapter.js";
|
|
20
|
+
export { TIMEOUT_EXIT_CODE, TIMEOUT_ERROR_MESSAGE, } from "@secure-exec/core";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Bridge compilation
|
|
2
|
+
export { getRawBridgeCode, getBridgeAttachCode } from "./bridge-loader.js";
|
|
3
|
+
// Stdlib polyfill bundling
|
|
4
|
+
export { bundlePolyfill, getAvailableStdlib, hasPolyfill, prebundleAllPolyfills, } from "./polyfills.js";
|
|
5
|
+
// Node execution driver
|
|
6
|
+
export { NodeExecutionDriver } from "./execution-driver.js";
|
|
7
|
+
// Node system driver
|
|
8
|
+
export { createDefaultNetworkAdapter, createNodeDriver, createNodeRuntimeDriverFactory, NodeFileSystem, filterEnv, isPrivateIp, } from "./driver.js";
|
|
9
|
+
// Module access filesystem
|
|
10
|
+
export { ModuleAccessFileSystem } from "./module-access.js";
|
|
11
|
+
// Bridge handlers
|
|
12
|
+
export { emitConsoleEvent, stripDangerousEnv, createProcessConfigForExecution, } from "./bridge-handlers.js";
|
|
13
|
+
export { BINDING_PREFIX, flattenBindingTree } from "./bindings.js";
|
|
14
|
+
// Kernel runtime driver (RuntimeDriver for kernel.mount())
|
|
15
|
+
export { createNodeRuntime } from "./kernel-runtime.js";
|
|
16
|
+
export { createKernelCommandExecutor, createKernelVfsAdapter, createHostFallbackVfs, } from "./kernel-runtime.js";
|
|
17
|
+
// OS platform adapters (host filesystem with root, worker threads)
|
|
18
|
+
export { HostNodeFileSystem } from "./os-filesystem.js";
|
|
19
|
+
export { NodeWorkerAdapter } from "./worker-adapter.js";
|
|
20
|
+
// Host network adapter (HostNetworkAdapter for kernel delegation)
|
|
21
|
+
export { createNodeHostNetworkAdapter } from "./host-network-adapter.js";
|
|
22
|
+
// Timeout utilities (re-exported from core)
|
|
23
|
+
export { TIMEOUT_EXIT_CODE, TIMEOUT_ERROR_MESSAGE, } from "@secure-exec/core";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Permissions, VirtualFileSystem } from "@secure-exec/core";
|
|
2
|
+
import type { CommandExecutor, NetworkAdapter, RuntimeDriverOptions, SpawnedProcess } from "@secure-exec/core";
|
|
3
|
+
import type { StdioHook, OSConfig, ProcessConfig, TimingMitigation } from "@secure-exec/core/internal/shared/api-types";
|
|
4
|
+
import type { ResolutionCache } from "./package-bundler.js";
|
|
5
|
+
import type { BindingTree } from "./bindings.js";
|
|
6
|
+
export interface NodeExecutionDriverOptions extends RuntimeDriverOptions {
|
|
7
|
+
createIsolate?(memoryLimit: number): unknown;
|
|
8
|
+
bindings?: BindingTree;
|
|
9
|
+
/** Callback to toggle PTY raw mode — wired by kernel runtime when PTY is attached. */
|
|
10
|
+
onPtySetRawMode?: (mode: boolean) => void;
|
|
11
|
+
/** Kernel socket table — routes net.connect through kernel instead of host TCP. */
|
|
12
|
+
socketTable?: import("@secure-exec/core").SocketTable;
|
|
13
|
+
/** Kernel process table — registers child processes for cross-runtime visibility. */
|
|
14
|
+
processTable?: import("@secure-exec/core").ProcessTable;
|
|
15
|
+
/** Kernel timer table — tracks sandbox timers for budget enforcement and cleanup. */
|
|
16
|
+
timerTable?: import("@secure-exec/core").TimerTable;
|
|
17
|
+
/** Process ID for kernel socket/process ownership. Required when socketTable/processTable is set. */
|
|
18
|
+
pid?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface BudgetState {
|
|
21
|
+
outputBytes: number;
|
|
22
|
+
bridgeCalls: number;
|
|
23
|
+
activeTimers: number;
|
|
24
|
+
childProcesses: number;
|
|
25
|
+
}
|
|
26
|
+
/** Shared mutable state owned by NodeExecutionDriver, passed to extracted modules. */
|
|
27
|
+
export interface DriverDeps {
|
|
28
|
+
filesystem: VirtualFileSystem;
|
|
29
|
+
commandExecutor: CommandExecutor;
|
|
30
|
+
networkAdapter: NetworkAdapter;
|
|
31
|
+
permissions?: Permissions;
|
|
32
|
+
processConfig: ProcessConfig;
|
|
33
|
+
osConfig: OSConfig;
|
|
34
|
+
onStdio?: StdioHook;
|
|
35
|
+
cpuTimeLimitMs?: number;
|
|
36
|
+
timingMitigation: TimingMitigation;
|
|
37
|
+
bridgeBase64TransferLimitBytes: number;
|
|
38
|
+
isolateJsonPayloadLimitBytes: number;
|
|
39
|
+
maxOutputBytes?: number;
|
|
40
|
+
maxBridgeCalls?: number;
|
|
41
|
+
maxTimers?: number;
|
|
42
|
+
maxChildProcesses?: number;
|
|
43
|
+
maxHandles?: number;
|
|
44
|
+
budgetState: BudgetState;
|
|
45
|
+
activeHttpServerIds: Set<number>;
|
|
46
|
+
activeHttpServerClosers: Map<number, () => Promise<void>>;
|
|
47
|
+
activeChildProcesses: Map<number, SpawnedProcess>;
|
|
48
|
+
activeHostTimers: Set<ReturnType<typeof setTimeout>>;
|
|
49
|
+
moduleFormatCache: Map<string, "esm" | "cjs" | "json">;
|
|
50
|
+
packageTypeCache: Map<string, "module" | "commonjs" | null>;
|
|
51
|
+
resolutionCache: ResolutionCache;
|
|
52
|
+
/** Optional callback for PTY setRawMode — wired by kernel when PTY is attached. */
|
|
53
|
+
onPtySetRawMode?: (mode: boolean) => void;
|
|
54
|
+
}
|
|
55
|
+
export declare const DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES: number;
|
|
56
|
+
export declare const DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES: number;
|
|
57
|
+
export declare const MIN_CONFIGURED_PAYLOAD_BYTES = 1024;
|
|
58
|
+
export declare const MAX_CONFIGURED_PAYLOAD_BYTES: number;
|
|
59
|
+
export declare const PAYLOAD_LIMIT_ERROR_CODE = "ERR_SANDBOX_PAYLOAD_TOO_LARGE";
|
|
60
|
+
export declare const RESOURCE_BUDGET_ERROR_CODE = "ERR_RESOURCE_BUDGET_EXCEEDED";
|
|
61
|
+
export declare const DEFAULT_MAX_TIMERS = 10000;
|
|
62
|
+
export declare const DEFAULT_MAX_HANDLES = 10000;
|
|
63
|
+
export declare const DEFAULT_SANDBOX_CWD = "/root";
|
|
64
|
+
export declare const DEFAULT_SANDBOX_HOME = "/root";
|
|
65
|
+
export declare const DEFAULT_SANDBOX_TMPDIR = "/tmp";
|
|
66
|
+
export declare class PayloadLimitError extends Error {
|
|
67
|
+
constructor(payloadLabel: string, maxBytes: number, actualBytes: number);
|
|
68
|
+
}
|
|
69
|
+
export declare function normalizePayloadLimit(configuredValue: number | undefined, defaultValue: number, optionName: string): number;
|
|
70
|
+
export declare function getUtf8ByteLength(text: string): number;
|
|
71
|
+
export declare function getBase64EncodedByteLength(rawByteLength: number): number;
|
|
72
|
+
export declare function assertPayloadByteLength(payloadLabel: string, actualBytes: number, maxBytes: number): void;
|
|
73
|
+
export declare function assertTextPayloadSize(payloadLabel: string, text: string, maxBytes: number): void;
|
|
74
|
+
export declare function createBudgetState(): BudgetState;
|
|
75
|
+
export declare function clearActiveHostTimers(deps: Pick<DriverDeps, "activeHostTimers">): void;
|
|
76
|
+
export declare function killActiveChildProcesses(deps: Pick<DriverDeps, "activeChildProcesses">): void;
|
|
77
|
+
export declare function checkBridgeBudget(deps: Pick<DriverDeps, "maxBridgeCalls" | "budgetState">): void;
|
|
78
|
+
export declare function parseJsonWithLimit<T>(payloadLabel: string, jsonText: string, maxBytes: number): T;
|
|
79
|
+
export declare function getExecutionTimeoutMs(override: number | undefined, cpuTimeLimitMs: number | undefined): number | undefined;
|
|
80
|
+
export declare function getTimingMitigation(override: TimingMitigation | undefined, defaultMitigation: TimingMitigation): TimingMitigation;
|
|
81
|
+
export declare const polyfillCodeCache: Map<string, string>;
|
|
82
|
+
export declare const polyfillNamedExportsCache: Map<string, string[]>;
|
|
83
|
+
export declare const hostBuiltinNamedExportsCache: Map<string, string[]>;
|
|
84
|
+
export declare const hostRequire: NodeJS.Require;
|
|
85
|
+
export declare function isValidExportName(name: string): boolean;
|
|
86
|
+
export declare function getHostBuiltinNamedExports(moduleName: string): string[];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
// Constants
|
|
3
|
+
export const DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
export const DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES = 4 * 1024 * 1024;
|
|
5
|
+
export const MIN_CONFIGURED_PAYLOAD_BYTES = 1024;
|
|
6
|
+
export const MAX_CONFIGURED_PAYLOAD_BYTES = 64 * 1024 * 1024;
|
|
7
|
+
export const PAYLOAD_LIMIT_ERROR_CODE = "ERR_SANDBOX_PAYLOAD_TOO_LARGE";
|
|
8
|
+
export const RESOURCE_BUDGET_ERROR_CODE = "ERR_RESOURCE_BUDGET_EXCEEDED";
|
|
9
|
+
export const DEFAULT_MAX_TIMERS = 10_000;
|
|
10
|
+
export const DEFAULT_MAX_HANDLES = 10_000;
|
|
11
|
+
export const DEFAULT_SANDBOX_CWD = "/root";
|
|
12
|
+
export const DEFAULT_SANDBOX_HOME = "/root";
|
|
13
|
+
export const DEFAULT_SANDBOX_TMPDIR = "/tmp";
|
|
14
|
+
export class PayloadLimitError extends Error {
|
|
15
|
+
constructor(payloadLabel, maxBytes, actualBytes) {
|
|
16
|
+
super(`${PAYLOAD_LIMIT_ERROR_CODE}: ${payloadLabel} exceeds ${maxBytes} bytes (got ${actualBytes})`);
|
|
17
|
+
this.name = "PayloadLimitError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function normalizePayloadLimit(configuredValue, defaultValue, optionName) {
|
|
21
|
+
if (configuredValue === undefined) {
|
|
22
|
+
return defaultValue;
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isFinite(configuredValue) || configuredValue <= 0) {
|
|
25
|
+
throw new RangeError(`${optionName} must be a positive finite number`);
|
|
26
|
+
}
|
|
27
|
+
const normalizedValue = Math.floor(configuredValue);
|
|
28
|
+
if (normalizedValue < MIN_CONFIGURED_PAYLOAD_BYTES) {
|
|
29
|
+
throw new RangeError(`${optionName} must be at least ${MIN_CONFIGURED_PAYLOAD_BYTES} bytes`);
|
|
30
|
+
}
|
|
31
|
+
if (normalizedValue > MAX_CONFIGURED_PAYLOAD_BYTES) {
|
|
32
|
+
throw new RangeError(`${optionName} must be at most ${MAX_CONFIGURED_PAYLOAD_BYTES} bytes`);
|
|
33
|
+
}
|
|
34
|
+
return normalizedValue;
|
|
35
|
+
}
|
|
36
|
+
export function getUtf8ByteLength(text) {
|
|
37
|
+
return Buffer.byteLength(text, "utf8");
|
|
38
|
+
}
|
|
39
|
+
export function getBase64EncodedByteLength(rawByteLength) {
|
|
40
|
+
return Math.ceil(rawByteLength / 3) * 4;
|
|
41
|
+
}
|
|
42
|
+
export function assertPayloadByteLength(payloadLabel, actualBytes, maxBytes) {
|
|
43
|
+
if (actualBytes <= maxBytes) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new PayloadLimitError(payloadLabel, maxBytes, actualBytes);
|
|
47
|
+
}
|
|
48
|
+
export function assertTextPayloadSize(payloadLabel, text, maxBytes) {
|
|
49
|
+
assertPayloadByteLength(payloadLabel, getUtf8ByteLength(text), maxBytes);
|
|
50
|
+
}
|
|
51
|
+
export function createBudgetState() {
|
|
52
|
+
return { outputBytes: 0, bridgeCalls: 0, activeTimers: 0, childProcesses: 0 };
|
|
53
|
+
}
|
|
54
|
+
export function clearActiveHostTimers(deps) {
|
|
55
|
+
for (const id of deps.activeHostTimers) {
|
|
56
|
+
clearTimeout(id);
|
|
57
|
+
}
|
|
58
|
+
deps.activeHostTimers.clear();
|
|
59
|
+
}
|
|
60
|
+
export function killActiveChildProcesses(deps) {
|
|
61
|
+
for (const proc of deps.activeChildProcesses.values()) {
|
|
62
|
+
try {
|
|
63
|
+
proc.kill(9); // SIGKILL
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Process may already be dead
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
deps.activeChildProcesses.clear();
|
|
70
|
+
}
|
|
71
|
+
export function checkBridgeBudget(deps) {
|
|
72
|
+
if (deps.maxBridgeCalls === undefined)
|
|
73
|
+
return;
|
|
74
|
+
deps.budgetState.bridgeCalls++;
|
|
75
|
+
if (deps.budgetState.bridgeCalls > deps.maxBridgeCalls) {
|
|
76
|
+
throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum bridge calls exceeded`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function parseJsonWithLimit(payloadLabel, jsonText, maxBytes) {
|
|
80
|
+
assertTextPayloadSize(payloadLabel, jsonText, maxBytes);
|
|
81
|
+
return JSON.parse(jsonText);
|
|
82
|
+
}
|
|
83
|
+
export function getExecutionTimeoutMs(override, cpuTimeLimitMs) {
|
|
84
|
+
const timeoutMs = override ?? cpuTimeLimitMs;
|
|
85
|
+
if (timeoutMs === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
89
|
+
throw new RangeError("cpuTimeLimitMs must be a positive finite number");
|
|
90
|
+
}
|
|
91
|
+
return Math.floor(timeoutMs);
|
|
92
|
+
}
|
|
93
|
+
export function getTimingMitigation(override, defaultMitigation) {
|
|
94
|
+
return override ?? defaultMitigation;
|
|
95
|
+
}
|
|
96
|
+
// Module-level caches for polyfill and host builtin named exports
|
|
97
|
+
export const polyfillCodeCache = new Map();
|
|
98
|
+
export const polyfillNamedExportsCache = new Map();
|
|
99
|
+
export const hostBuiltinNamedExportsCache = new Map();
|
|
100
|
+
export const hostRequire = createRequire(import.meta.url);
|
|
101
|
+
export function isValidExportName(name) {
|
|
102
|
+
return /^[A-Za-z_$][\w$]*$/.test(name);
|
|
103
|
+
}
|
|
104
|
+
export function getHostBuiltinNamedExports(moduleName) {
|
|
105
|
+
const cached = hostBuiltinNamedExportsCache.get(moduleName);
|
|
106
|
+
if (cached) {
|
|
107
|
+
return cached;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const loaded = hostRequire(`node:${moduleName}`);
|
|
111
|
+
const names = Array.from(new Set([
|
|
112
|
+
...Object.keys(loaded ?? {}),
|
|
113
|
+
...Object.getOwnPropertyNames(loaded ?? {}),
|
|
114
|
+
]))
|
|
115
|
+
.filter((name) => name !== "default")
|
|
116
|
+
.filter(isValidExportName)
|
|
117
|
+
.sort();
|
|
118
|
+
hostBuiltinNamedExportsCache.set(moduleName, names);
|
|
119
|
+
return names;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
hostBuiltinNamedExportsCache.set(moduleName, []);
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Compatibility shim for bridge calling conventions.
|
|
2
|
+
//
|
|
3
|
+
// The bridge bundle code calls host functions via ivm.Reference methods:
|
|
4
|
+
// .applySync(ctx, args) — sync call
|
|
5
|
+
// .applySyncPromise(ctx, args) — sync call (host may be async)
|
|
6
|
+
// .apply(ctx, args, opts) — async call (opts ignored, Function.prototype.apply works)
|
|
7
|
+
//
|
|
8
|
+
// The Rust V8 runtime registers host functions as plain FunctionTemplate functions.
|
|
9
|
+
// This shim adds the missing .applySync() and .applySyncPromise() methods.
|
|
10
|
+
// .apply() already works via Function.prototype.apply (third arg is ignored).
|
|
11
|
+
import { HOST_BRIDGE_GLOBAL_KEY_LIST, } from "./bridge-contract.js";
|
|
12
|
+
/**
|
|
13
|
+
* Generate JS source for the ivm-compat shim.
|
|
14
|
+
*
|
|
15
|
+
* Must run AFTER the Rust side registers bridge functions on the global,
|
|
16
|
+
* and BEFORE the bridge bundle IIFE executes.
|
|
17
|
+
*/
|
|
18
|
+
export function getIvmCompatShimSource() {
|
|
19
|
+
const keyListJson = JSON.stringify(HOST_BRIDGE_GLOBAL_KEY_LIST.filter(
|
|
20
|
+
// _processConfig and _osConfig are config objects, not callable functions
|
|
21
|
+
(k) => k !== "_processConfig" && k !== "_osConfig"));
|
|
22
|
+
return `(function(){
|
|
23
|
+
var keys = ${keyListJson};
|
|
24
|
+
for (var i = 0; i < keys.length; i++) {
|
|
25
|
+
var fn = globalThis[keys[i]];
|
|
26
|
+
if (typeof fn !== 'function') continue;
|
|
27
|
+
fn.applySync = function(ctx, args) { return this.call(null, ...(args || [])); };
|
|
28
|
+
fn.applySyncPromise = function(ctx, args) { return this.call(null, ...(args || [])); };
|
|
29
|
+
}
|
|
30
|
+
})();`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 type { KernelRuntimeDriver as RuntimeDriver, KernelInterface, Permissions, VirtualFileSystem } from '@secure-exec/core';
|
|
11
|
+
import type { BindingTree } from './bindings.js';
|
|
12
|
+
import type { CommandExecutor } from '@secure-exec/core';
|
|
13
|
+
export interface NodeRuntimeOptions {
|
|
14
|
+
/** Memory limit in MB for each V8 isolate (default: 128). */
|
|
15
|
+
memoryLimit?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Host filesystem paths that the isolate may read for module resolution
|
|
18
|
+
* (e.g. npm's own install directory). By default, the driver discovers
|
|
19
|
+
* the host npm location automatically.
|
|
20
|
+
*/
|
|
21
|
+
moduleAccessPaths?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Bridge permissions for isolate processes. Defaults to allowAllChildProcess
|
|
24
|
+
* (fs/network/env deny-by-default). Use allowAll for full sandbox access.
|
|
25
|
+
*/
|
|
26
|
+
permissions?: Partial<Permissions>;
|
|
27
|
+
/**
|
|
28
|
+
* Host-side functions exposed to sandbox code via SecureExec.bindings.
|
|
29
|
+
* Nested objects become dot-separated paths (max depth 4, max 64 leaves).
|
|
30
|
+
*/
|
|
31
|
+
bindings?: BindingTree;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a Node.js RuntimeDriver that can be mounted into the kernel.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createNodeRuntime(options?: NodeRuntimeOptions): RuntimeDriver;
|
|
37
|
+
/**
|
|
38
|
+
* CommandExecutor adapter that wraps KernelInterface.spawn().
|
|
39
|
+
* This is the critical integration point: when code inside the V8 isolate
|
|
40
|
+
* calls child_process.spawn('sh', ['-c', 'echo hello']), the bridge
|
|
41
|
+
* delegates here, which calls kernel.spawn() to route 'sh' to WasmVM.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createKernelCommandExecutor(kernel: KernelInterface, parentPid: number): CommandExecutor;
|
|
44
|
+
/**
|
|
45
|
+
* Thin adapter from kernel VFS to secure-exec VFS interface.
|
|
46
|
+
* The kernel VFS is a superset, so this just narrows the type.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createKernelVfsAdapter(kernelVfs: KernelInterface['vfs']): VirtualFileSystem;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap a VFS with host filesystem fallback for read operations.
|
|
51
|
+
*
|
|
52
|
+
* When npm/npx runs inside the V8 isolate, require() must resolve npm's own
|
|
53
|
+
* internal modules (e.g. '../lib/cli/entry'). These live on the host
|
|
54
|
+
* filesystem, not in the kernel VFS. This wrapper tries the kernel VFS first
|
|
55
|
+
* and falls back to the host filesystem for reads. Writes always go to the
|
|
56
|
+
* kernel VFS.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createHostFallbackVfs(base: VirtualFileSystem): VirtualFileSystem;
|