@secure-exec/browser 0.1.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/driver.d.ts +71 -0
- package/dist/driver.js +315 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/runtime-driver.d.ts +26 -0
- package/dist/runtime-driver.js +217 -0
- package/dist/worker-protocol.d.ts +66 -0
- package/dist/worker-protocol.js +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +547 -0
- package/package.json +52 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { transform } from "sucrase";
|
|
2
|
+
import { getRequireSetupCode, createCommandExecutorStub, createFsStub, createNetworkStub, filterEnv, wrapFileSystem, wrapNetworkAdapter, createInMemoryFileSystem, isESM, transformDynamicImport, getIsolateRuntimeSource, POLYFILL_CODE_MAP, loadFile, resolveModule, mkdir, exposeCustomGlobal, exposeMutableRuntimeStateGlobal, } from "@secure-exec/core";
|
|
3
|
+
import { createBrowserNetworkAdapter, createOpfsFileSystem, } from "./driver.js";
|
|
4
|
+
let filesystem = null;
|
|
5
|
+
let networkAdapter = null;
|
|
6
|
+
let commandExecutor = null;
|
|
7
|
+
let permissions;
|
|
8
|
+
let initialized = false;
|
|
9
|
+
const dynamicImportCache = new Map();
|
|
10
|
+
const MAX_ERROR_MESSAGE_CHARS = 8192;
|
|
11
|
+
const MAX_STDIO_MESSAGE_CHARS = 8192;
|
|
12
|
+
const MAX_STDIO_DEPTH = 6;
|
|
13
|
+
const MAX_STDIO_OBJECT_KEYS = 60;
|
|
14
|
+
const MAX_STDIO_ARRAY_ITEMS = 120;
|
|
15
|
+
const dynamicImportModule = new Function("specifier", "return import(specifier);");
|
|
16
|
+
function boundErrorMessage(message) {
|
|
17
|
+
if (message.length <= MAX_ERROR_MESSAGE_CHARS) {
|
|
18
|
+
return message;
|
|
19
|
+
}
|
|
20
|
+
return `${message.slice(0, MAX_ERROR_MESSAGE_CHARS)}...[Truncated]`;
|
|
21
|
+
}
|
|
22
|
+
function boundStdioMessage(message) {
|
|
23
|
+
if (message.length <= MAX_STDIO_MESSAGE_CHARS) {
|
|
24
|
+
return message;
|
|
25
|
+
}
|
|
26
|
+
return `${message.slice(0, MAX_STDIO_MESSAGE_CHARS)}...[Truncated]`;
|
|
27
|
+
}
|
|
28
|
+
function revivePermission(source) {
|
|
29
|
+
if (!source)
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
const fn = new Function(`return (${source});`)();
|
|
33
|
+
if (typeof fn === "function")
|
|
34
|
+
return fn;
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Deserialize permission callbacks that were stringified for transfer across the Worker boundary. */
|
|
42
|
+
function revivePermissions(serialized) {
|
|
43
|
+
if (!serialized)
|
|
44
|
+
return undefined;
|
|
45
|
+
const perms = {};
|
|
46
|
+
perms.fs = revivePermission(serialized.fs);
|
|
47
|
+
perms.network = revivePermission(serialized.network);
|
|
48
|
+
perms.childProcess = revivePermission(serialized.childProcess);
|
|
49
|
+
perms.env = revivePermission(serialized.env);
|
|
50
|
+
return perms;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Wrap a sync function in the bridge calling convention (`applySync`) so
|
|
54
|
+
* bridge code can call it the same way it calls isolated-vm References.
|
|
55
|
+
*/
|
|
56
|
+
function makeApplySync(fn) {
|
|
57
|
+
const applySync = (_ctx, args) => fn(...args);
|
|
58
|
+
return {
|
|
59
|
+
applySync,
|
|
60
|
+
applySyncPromise: applySync,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function makeApplySyncPromise(fn) {
|
|
64
|
+
return {
|
|
65
|
+
applySyncPromise(_ctx, args) {
|
|
66
|
+
return fn(...args);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function makeApplyPromise(fn) {
|
|
71
|
+
return {
|
|
72
|
+
apply(_ctx, args) {
|
|
73
|
+
return fn(...args);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function postResponse(message) {
|
|
78
|
+
self.postMessage(message);
|
|
79
|
+
}
|
|
80
|
+
function postStdio(requestId, channel, message) {
|
|
81
|
+
const payload = {
|
|
82
|
+
type: "stdio",
|
|
83
|
+
requestId,
|
|
84
|
+
channel,
|
|
85
|
+
message,
|
|
86
|
+
};
|
|
87
|
+
self.postMessage(payload);
|
|
88
|
+
}
|
|
89
|
+
function formatConsoleValue(value, seen = new WeakSet(), depth = 0) {
|
|
90
|
+
if (value === null) {
|
|
91
|
+
return "null";
|
|
92
|
+
}
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
return "undefined";
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
100
|
+
return String(value);
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === "bigint") {
|
|
103
|
+
return `${value.toString()}n`;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === "symbol") {
|
|
106
|
+
return value.toString();
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === "function") {
|
|
109
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value !== "object") {
|
|
112
|
+
return String(value);
|
|
113
|
+
}
|
|
114
|
+
if (seen.has(value)) {
|
|
115
|
+
return "[Circular]";
|
|
116
|
+
}
|
|
117
|
+
if (depth >= MAX_STDIO_DEPTH) {
|
|
118
|
+
return "[MaxDepth]";
|
|
119
|
+
}
|
|
120
|
+
seen.add(value);
|
|
121
|
+
try {
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
const out = value
|
|
124
|
+
.slice(0, MAX_STDIO_ARRAY_ITEMS)
|
|
125
|
+
.map((item) => formatConsoleValue(item, seen, depth + 1));
|
|
126
|
+
if (value.length > MAX_STDIO_ARRAY_ITEMS) {
|
|
127
|
+
out.push('"[Truncated]"');
|
|
128
|
+
}
|
|
129
|
+
return `[${out.join(", ")}]`;
|
|
130
|
+
}
|
|
131
|
+
const entries = [];
|
|
132
|
+
for (const key of Object.keys(value).slice(0, MAX_STDIO_OBJECT_KEYS)) {
|
|
133
|
+
entries.push(`${key}: ${formatConsoleValue(value[key], seen, depth + 1)}`);
|
|
134
|
+
}
|
|
135
|
+
if (Object.keys(value).length > MAX_STDIO_OBJECT_KEYS) {
|
|
136
|
+
entries.push('"[Truncated]"');
|
|
137
|
+
}
|
|
138
|
+
return `{ ${entries.join(", ")} }`;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return "[Unserializable]";
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
seen.delete(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function emitStdio(requestId, channel, args) {
|
|
148
|
+
const message = boundStdioMessage(args.map((arg) => formatConsoleValue(arg)).join(" "));
|
|
149
|
+
postStdio(requestId, channel, message);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Initialize the worker-side runtime: set up filesystem, network, bridge
|
|
153
|
+
* globals, and load the bridge bundle. Called once before any exec/run.
|
|
154
|
+
*/
|
|
155
|
+
async function initRuntime(payload) {
|
|
156
|
+
if (initialized)
|
|
157
|
+
return;
|
|
158
|
+
permissions = revivePermissions(payload.permissions);
|
|
159
|
+
const baseFs = payload.filesystem === "memory"
|
|
160
|
+
? createInMemoryFileSystem()
|
|
161
|
+
: await createOpfsFileSystem();
|
|
162
|
+
filesystem = wrapFileSystem(baseFs, permissions);
|
|
163
|
+
if (payload.networkEnabled) {
|
|
164
|
+
networkAdapter = wrapNetworkAdapter(createBrowserNetworkAdapter(), permissions);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
networkAdapter = createNetworkStub();
|
|
168
|
+
}
|
|
169
|
+
commandExecutor = createCommandExecutorStub();
|
|
170
|
+
const fsOps = filesystem ?? createFsStub();
|
|
171
|
+
const processConfig = payload.processConfig ?? {};
|
|
172
|
+
processConfig.env = filterEnv(processConfig.env, permissions);
|
|
173
|
+
exposeCustomGlobal("_processConfig", processConfig);
|
|
174
|
+
exposeCustomGlobal("_osConfig", payload.osConfig ?? {});
|
|
175
|
+
// Set up filesystem bridge globals before loading runtime shims.
|
|
176
|
+
const readFileRef = makeApplySyncPromise(async (path) => {
|
|
177
|
+
return fsOps.readTextFile(path);
|
|
178
|
+
});
|
|
179
|
+
const writeFileRef = makeApplySyncPromise(async (path, content) => {
|
|
180
|
+
return fsOps.writeFile(path, content);
|
|
181
|
+
});
|
|
182
|
+
const readFileBinaryRef = makeApplySyncPromise(async (path) => {
|
|
183
|
+
const data = await fsOps.readFile(path);
|
|
184
|
+
return btoa(String.fromCharCode(...data));
|
|
185
|
+
});
|
|
186
|
+
const writeFileBinaryRef = makeApplySyncPromise(async (path, base64) => {
|
|
187
|
+
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
|
188
|
+
return fsOps.writeFile(path, bytes);
|
|
189
|
+
});
|
|
190
|
+
const readDirRef = makeApplySyncPromise(async (path) => {
|
|
191
|
+
const entries = await fsOps.readDirWithTypes(path);
|
|
192
|
+
return JSON.stringify(entries);
|
|
193
|
+
});
|
|
194
|
+
const mkdirRef = makeApplySyncPromise(async (path) => {
|
|
195
|
+
return mkdir(fsOps, path);
|
|
196
|
+
});
|
|
197
|
+
const rmdirRef = makeApplySyncPromise(async (path) => {
|
|
198
|
+
return fsOps.removeDir(path);
|
|
199
|
+
});
|
|
200
|
+
const existsRef = makeApplySyncPromise(async (path) => {
|
|
201
|
+
return fsOps.exists(path);
|
|
202
|
+
});
|
|
203
|
+
const statRef = makeApplySyncPromise(async (path) => {
|
|
204
|
+
const statInfo = await fsOps.stat(path);
|
|
205
|
+
return JSON.stringify(statInfo);
|
|
206
|
+
});
|
|
207
|
+
const unlinkRef = makeApplySyncPromise(async (path) => {
|
|
208
|
+
return fsOps.removeFile(path);
|
|
209
|
+
});
|
|
210
|
+
const renameRef = makeApplySyncPromise(async (oldPath, newPath) => {
|
|
211
|
+
return fsOps.rename(oldPath, newPath);
|
|
212
|
+
});
|
|
213
|
+
exposeCustomGlobal("_fs", {
|
|
214
|
+
readFile: readFileRef,
|
|
215
|
+
writeFile: writeFileRef,
|
|
216
|
+
readFileBinary: readFileBinaryRef,
|
|
217
|
+
writeFileBinary: writeFileBinaryRef,
|
|
218
|
+
readDir: readDirRef,
|
|
219
|
+
mkdir: mkdirRef,
|
|
220
|
+
rmdir: rmdirRef,
|
|
221
|
+
exists: existsRef,
|
|
222
|
+
stat: statRef,
|
|
223
|
+
unlink: unlinkRef,
|
|
224
|
+
rename: renameRef,
|
|
225
|
+
});
|
|
226
|
+
exposeCustomGlobal("_loadPolyfill", makeApplySyncPromise(async (moduleName) => {
|
|
227
|
+
const name = moduleName.replace(/^node:/, "");
|
|
228
|
+
const polyfillMap = POLYFILL_CODE_MAP;
|
|
229
|
+
return polyfillMap[name] ?? null;
|
|
230
|
+
}));
|
|
231
|
+
exposeCustomGlobal("_resolveModule", makeApplySyncPromise(async (request, fromDir) => {
|
|
232
|
+
return resolveModule(request, fromDir, fsOps);
|
|
233
|
+
}));
|
|
234
|
+
exposeCustomGlobal("_loadFile", makeApplySyncPromise(async (path) => {
|
|
235
|
+
const source = await loadFile(path, fsOps);
|
|
236
|
+
if (source === null)
|
|
237
|
+
return null;
|
|
238
|
+
let code = source;
|
|
239
|
+
if (isESM(source, path)) {
|
|
240
|
+
code = transform(code, { transforms: ["imports"] }).code;
|
|
241
|
+
}
|
|
242
|
+
return transformDynamicImport(code);
|
|
243
|
+
}));
|
|
244
|
+
exposeCustomGlobal("_scheduleTimer", {
|
|
245
|
+
apply(_ctx, args) {
|
|
246
|
+
return new Promise((resolve) => {
|
|
247
|
+
setTimeout(resolve, args[0]);
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const netAdapter = networkAdapter ?? createNetworkStub();
|
|
252
|
+
exposeCustomGlobal("_networkFetchRaw", makeApplyPromise(async (url, optionsJson) => {
|
|
253
|
+
const options = JSON.parse(optionsJson);
|
|
254
|
+
const result = await netAdapter.fetch(url, options);
|
|
255
|
+
return JSON.stringify(result);
|
|
256
|
+
}));
|
|
257
|
+
exposeCustomGlobal("_networkDnsLookupRaw", makeApplyPromise(async (hostname) => {
|
|
258
|
+
const result = await netAdapter.dnsLookup(hostname);
|
|
259
|
+
return JSON.stringify(result);
|
|
260
|
+
}));
|
|
261
|
+
exposeCustomGlobal("_networkHttpRequestRaw", makeApplyPromise(async (url, optionsJson) => {
|
|
262
|
+
const options = JSON.parse(optionsJson);
|
|
263
|
+
const result = await netAdapter.httpRequest(url, options);
|
|
264
|
+
return JSON.stringify(result);
|
|
265
|
+
}));
|
|
266
|
+
const execAdapter = commandExecutor ?? createCommandExecutorStub();
|
|
267
|
+
let nextSessionId = 1;
|
|
268
|
+
const sessions = new Map();
|
|
269
|
+
const getDispatch = () => globalThis._childProcessDispatch;
|
|
270
|
+
exposeCustomGlobal("_childProcessSpawnStart", makeApplySync((command, argsJson, optionsJson) => {
|
|
271
|
+
const args = JSON.parse(argsJson);
|
|
272
|
+
const options = JSON.parse(optionsJson);
|
|
273
|
+
const sessionId = nextSessionId++;
|
|
274
|
+
const proc = execAdapter.spawn(command, args, {
|
|
275
|
+
cwd: options.cwd,
|
|
276
|
+
env: options.env,
|
|
277
|
+
onStdout: (data) => {
|
|
278
|
+
getDispatch()?.(sessionId, "stdout", data);
|
|
279
|
+
},
|
|
280
|
+
onStderr: (data) => {
|
|
281
|
+
getDispatch()?.(sessionId, "stderr", data);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
proc.wait().then((code) => {
|
|
285
|
+
getDispatch()?.(sessionId, "exit", code);
|
|
286
|
+
sessions.delete(sessionId);
|
|
287
|
+
});
|
|
288
|
+
sessions.set(sessionId, proc);
|
|
289
|
+
return sessionId;
|
|
290
|
+
}));
|
|
291
|
+
exposeCustomGlobal("_childProcessStdinWrite", makeApplySync((sessionId, data) => {
|
|
292
|
+
sessions.get(sessionId)?.writeStdin(data);
|
|
293
|
+
}));
|
|
294
|
+
exposeCustomGlobal("_childProcessStdinClose", makeApplySync((sessionId) => {
|
|
295
|
+
sessions.get(sessionId)?.closeStdin();
|
|
296
|
+
}));
|
|
297
|
+
exposeCustomGlobal("_childProcessKill", makeApplySync((sessionId, signal) => {
|
|
298
|
+
sessions.get(sessionId)?.kill(signal);
|
|
299
|
+
}));
|
|
300
|
+
exposeCustomGlobal("_childProcessSpawnSync", makeApplySyncPromise(async (command, argsJson, optionsJson) => {
|
|
301
|
+
const args = JSON.parse(argsJson);
|
|
302
|
+
const options = JSON.parse(optionsJson);
|
|
303
|
+
const stdoutChunks = [];
|
|
304
|
+
const stderrChunks = [];
|
|
305
|
+
const proc = execAdapter.spawn(command, args, {
|
|
306
|
+
cwd: options.cwd,
|
|
307
|
+
env: options.env,
|
|
308
|
+
onStdout: (data) => stdoutChunks.push(data),
|
|
309
|
+
onStderr: (data) => stderrChunks.push(data),
|
|
310
|
+
});
|
|
311
|
+
const exitCode = await proc.wait();
|
|
312
|
+
const decoder = new TextDecoder();
|
|
313
|
+
const stdout = stdoutChunks.map((c) => decoder.decode(c)).join("");
|
|
314
|
+
const stderr = stderrChunks.map((c) => decoder.decode(c)).join("");
|
|
315
|
+
return JSON.stringify({ stdout, stderr, code: exitCode });
|
|
316
|
+
}));
|
|
317
|
+
if (!("SharedArrayBuffer" in globalThis)) {
|
|
318
|
+
class SharedArrayBufferShim {
|
|
319
|
+
backing;
|
|
320
|
+
constructor(length) {
|
|
321
|
+
this.backing = new ArrayBuffer(length);
|
|
322
|
+
}
|
|
323
|
+
get byteLength() {
|
|
324
|
+
return this.backing.byteLength;
|
|
325
|
+
}
|
|
326
|
+
get growable() {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
get maxByteLength() {
|
|
330
|
+
return this.backing.byteLength;
|
|
331
|
+
}
|
|
332
|
+
slice(start, end) {
|
|
333
|
+
return this.backing.slice(start, end);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
Object.defineProperty(globalThis, "SharedArrayBuffer", {
|
|
337
|
+
value: SharedArrayBufferShim,
|
|
338
|
+
configurable: true,
|
|
339
|
+
writable: true,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
let bridgeModule;
|
|
343
|
+
try {
|
|
344
|
+
bridgeModule = await dynamicImportModule("@secure-exec/core/internal/bridge");
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Vite browser tests may need source fallback.
|
|
348
|
+
try {
|
|
349
|
+
bridgeModule = await dynamicImportModule("@secure-exec/core/internal/bridge");
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
throw new Error("Failed to load bridge module from @secure-exec/core");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
exposeCustomGlobal("_fsModule", bridgeModule.default);
|
|
356
|
+
eval(getIsolateRuntimeSource("globalExposureHelpers"));
|
|
357
|
+
exposeMutableRuntimeStateGlobal("_moduleCache", {});
|
|
358
|
+
exposeMutableRuntimeStateGlobal("_pendingModules", {});
|
|
359
|
+
exposeMutableRuntimeStateGlobal("_currentModule", { dirname: "/" });
|
|
360
|
+
eval(getRequireSetupCode());
|
|
361
|
+
initialized = true;
|
|
362
|
+
}
|
|
363
|
+
function resetModuleState(cwd) {
|
|
364
|
+
exposeMutableRuntimeStateGlobal("_moduleCache", {});
|
|
365
|
+
exposeMutableRuntimeStateGlobal("_pendingModules", {});
|
|
366
|
+
exposeMutableRuntimeStateGlobal("_currentModule", { dirname: cwd });
|
|
367
|
+
}
|
|
368
|
+
function setDynamicImportFallback() {
|
|
369
|
+
exposeMutableRuntimeStateGlobal("__dynamicImport", function (specifier) {
|
|
370
|
+
const cached = dynamicImportCache.get(specifier);
|
|
371
|
+
if (cached)
|
|
372
|
+
return Promise.resolve(cached);
|
|
373
|
+
try {
|
|
374
|
+
const runtimeRequire = globalThis.require;
|
|
375
|
+
if (typeof runtimeRequire !== "function") {
|
|
376
|
+
throw new Error("require is not available in browser runtime");
|
|
377
|
+
}
|
|
378
|
+
const mod = runtimeRequire(specifier);
|
|
379
|
+
return Promise.resolve({ default: mod, ...mod });
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
return Promise.reject(new Error(`Cannot dynamically import '${specifier}': ${String(e)}`));
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
function captureConsole(requestId, captureStdio) {
|
|
387
|
+
const original = console;
|
|
388
|
+
if (!captureStdio) {
|
|
389
|
+
const sandboxConsole = {
|
|
390
|
+
log: () => undefined,
|
|
391
|
+
info: () => undefined,
|
|
392
|
+
warn: () => undefined,
|
|
393
|
+
error: () => undefined,
|
|
394
|
+
};
|
|
395
|
+
globalThis.console = sandboxConsole;
|
|
396
|
+
return {
|
|
397
|
+
restore: () => {
|
|
398
|
+
globalThis.console = original;
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const sandboxConsole = {
|
|
403
|
+
log: (...args) => emitStdio(requestId, "stdout", args),
|
|
404
|
+
info: (...args) => emitStdio(requestId, "stdout", args),
|
|
405
|
+
warn: (...args) => emitStdio(requestId, "stderr", args),
|
|
406
|
+
error: (...args) => emitStdio(requestId, "stderr", args),
|
|
407
|
+
};
|
|
408
|
+
globalThis.console = sandboxConsole;
|
|
409
|
+
return {
|
|
410
|
+
restore: () => {
|
|
411
|
+
globalThis.console = original;
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function updateProcessConfig(options) {
|
|
416
|
+
const proc = globalThis.process;
|
|
417
|
+
if (!proc)
|
|
418
|
+
return;
|
|
419
|
+
if (options?.cwd && typeof proc.chdir === "function") {
|
|
420
|
+
proc.chdir(options.cwd);
|
|
421
|
+
}
|
|
422
|
+
if (options?.env) {
|
|
423
|
+
const filtered = filterEnv(options.env, permissions);
|
|
424
|
+
const currentEnv = proc.env && typeof proc.env === "object"
|
|
425
|
+
? proc.env
|
|
426
|
+
: {};
|
|
427
|
+
proc.env = { ...currentEnv, ...filtered };
|
|
428
|
+
}
|
|
429
|
+
if (options?.stdin !== undefined) {
|
|
430
|
+
exposeMutableRuntimeStateGlobal("_stdinData", options.stdin);
|
|
431
|
+
exposeMutableRuntimeStateGlobal("_stdinPosition", 0);
|
|
432
|
+
exposeMutableRuntimeStateGlobal("_stdinEnded", false);
|
|
433
|
+
exposeMutableRuntimeStateGlobal("_stdinFlowMode", false);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Execute user code as a script (process-style). Transforms ESM/dynamic
|
|
438
|
+
* imports, sets up module/exports globals, and waits for active handles.
|
|
439
|
+
*/
|
|
440
|
+
async function execScript(requestId, code, options, captureStdio = false) {
|
|
441
|
+
resetModuleState(options?.cwd ?? "/");
|
|
442
|
+
updateProcessConfig(options);
|
|
443
|
+
setDynamicImportFallback();
|
|
444
|
+
const { restore } = captureConsole(requestId, captureStdio);
|
|
445
|
+
try {
|
|
446
|
+
let transformed = code;
|
|
447
|
+
if (isESM(code, options?.filePath)) {
|
|
448
|
+
transformed = transform(transformed, { transforms: ["imports"] }).code;
|
|
449
|
+
}
|
|
450
|
+
transformed = transformDynamicImport(transformed);
|
|
451
|
+
exposeMutableRuntimeStateGlobal("module", { exports: {} });
|
|
452
|
+
const moduleRef = globalThis.module;
|
|
453
|
+
exposeMutableRuntimeStateGlobal("exports", moduleRef.exports);
|
|
454
|
+
if (options?.filePath) {
|
|
455
|
+
const dirname = options.filePath.includes("/")
|
|
456
|
+
? options.filePath.substring(0, options.filePath.lastIndexOf("/")) || "/"
|
|
457
|
+
: "/";
|
|
458
|
+
exposeMutableRuntimeStateGlobal("__filename", options.filePath);
|
|
459
|
+
exposeMutableRuntimeStateGlobal("__dirname", dirname);
|
|
460
|
+
exposeMutableRuntimeStateGlobal("_currentModule", {
|
|
461
|
+
dirname,
|
|
462
|
+
filename: options.filePath,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// Await the eval result so async IIFEs / top-level promise expressions
|
|
466
|
+
// resolve before we check for active handles.
|
|
467
|
+
const evalResult = eval(transformed);
|
|
468
|
+
if (evalResult && typeof evalResult === "object" && typeof evalResult.then === "function") {
|
|
469
|
+
await evalResult;
|
|
470
|
+
}
|
|
471
|
+
const waitForActiveHandles = globalThis
|
|
472
|
+
._waitForActiveHandles;
|
|
473
|
+
if (typeof waitForActiveHandles === "function") {
|
|
474
|
+
await waitForActiveHandles();
|
|
475
|
+
}
|
|
476
|
+
const exitCode = globalThis.process
|
|
477
|
+
?.exitCode ?? 0;
|
|
478
|
+
return {
|
|
479
|
+
code: exitCode,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
484
|
+
const exitMatch = message.match(/process\.exit\((\d+)\)/);
|
|
485
|
+
if (exitMatch) {
|
|
486
|
+
const exitCode = Number.parseInt(exitMatch[1], 10);
|
|
487
|
+
return {
|
|
488
|
+
code: exitCode,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
code: 1,
|
|
493
|
+
errorMessage: boundErrorMessage(message),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
restore();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function runScript(requestId, code, filePath, captureStdio = false) {
|
|
501
|
+
const execResult = await execScript(requestId, code, { filePath }, captureStdio);
|
|
502
|
+
const moduleObj = globalThis.module;
|
|
503
|
+
return {
|
|
504
|
+
...execResult,
|
|
505
|
+
exports: moduleObj?.exports,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
self.onmessage = async (event) => {
|
|
509
|
+
const message = event.data;
|
|
510
|
+
try {
|
|
511
|
+
if (message.type === "init") {
|
|
512
|
+
await initRuntime(message.payload);
|
|
513
|
+
postResponse({ type: "response", id: message.id, ok: true, result: true });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!initialized) {
|
|
517
|
+
throw new Error("Sandbox worker not initialized");
|
|
518
|
+
}
|
|
519
|
+
if (message.type === "exec") {
|
|
520
|
+
const result = await execScript(message.id, message.payload.code, message.payload.options, message.payload.captureStdio);
|
|
521
|
+
postResponse({ type: "response", id: message.id, ok: true, result });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (message.type === "run") {
|
|
525
|
+
const result = await runScript(message.id, message.payload.code, message.payload.filePath, message.payload.captureStdio);
|
|
526
|
+
postResponse({ type: "response", id: message.id, ok: true, result });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (message.type === "dispose") {
|
|
530
|
+
postResponse({ type: "response", id: message.id, ok: true, result: true });
|
|
531
|
+
close();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
const error = err;
|
|
536
|
+
postResponse({
|
|
537
|
+
type: "response",
|
|
538
|
+
id: message.id,
|
|
539
|
+
ok: false,
|
|
540
|
+
error: {
|
|
541
|
+
message: error?.message ?? String(err),
|
|
542
|
+
stack: error?.stack,
|
|
543
|
+
code: error?.code,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@secure-exec/browser",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./internal/driver": {
|
|
19
|
+
"types": "./dist/driver.d.ts",
|
|
20
|
+
"import": "./dist/driver.js",
|
|
21
|
+
"default": "./dist/driver.js"
|
|
22
|
+
},
|
|
23
|
+
"./internal/runtime-driver": {
|
|
24
|
+
"types": "./dist/runtime-driver.d.ts",
|
|
25
|
+
"import": "./dist/runtime-driver.js",
|
|
26
|
+
"default": "./dist/runtime-driver.js"
|
|
27
|
+
},
|
|
28
|
+
"./internal/worker": {
|
|
29
|
+
"types": "./dist/worker.d.ts",
|
|
30
|
+
"import": "./dist/worker.js",
|
|
31
|
+
"default": "./dist/worker.js"
|
|
32
|
+
},
|
|
33
|
+
"./internal/worker-protocol": {
|
|
34
|
+
"types": "./dist/worker-protocol.d.ts",
|
|
35
|
+
"import": "./dist/worker-protocol.js",
|
|
36
|
+
"default": "./dist/worker-protocol.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"sucrase": "^3.35.0",
|
|
41
|
+
"@secure-exec/core": "0.1.0-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.10.2",
|
|
45
|
+
"typescript": "^5.7.2"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"check-types": "tsc --noEmit",
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"test": "echo 'no tests yet'"
|
|
51
|
+
}
|
|
52
|
+
}
|