@secure-exec/browser 0.2.0-rc.2 → 0.3.0-rc.2
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/README.md +4 -5
- package/dist/driver.d.ts +10 -4
- package/dist/driver.js +18 -3
- package/dist/index.d.ts +6 -5
- package/dist/index.js +2 -2
- package/dist/os-filesystem.d.ts +4 -4
- package/dist/os-filesystem.js +28 -11
- package/dist/runtime-driver.d.ts +11 -3
- package/dist/runtime-driver.js +271 -22
- package/dist/runtime.d.ts +222 -0
- package/dist/runtime.js +377 -0
- package/dist/sync-bridge.d.ts +46 -0
- package/dist/sync-bridge.js +49 -0
- package/dist/worker-adapter.js +1 -0
- package/dist/worker-protocol.d.ts +33 -10
- package/dist/worker.js +897 -116
- package/package.json +79 -66
- package/LICENSE +0 -191
package/dist/runtime-driver.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getBrowserSystemDriverOptions } from "./driver.js";
|
|
2
|
+
import { createFsStub, createNetworkStub, loadFile, mkdir, resolveModule, } from "./runtime.js";
|
|
3
|
+
import { assertBrowserSyncBridgeSupport, createBrowserSyncBridgePayload, SYNC_BRIDGE_KIND_BINARY, SYNC_BRIDGE_KIND_JSON, SYNC_BRIDGE_KIND_NONE, SYNC_BRIDGE_KIND_TEXT, SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE, SYNC_BRIDGE_SIGNAL_KIND_INDEX, SYNC_BRIDGE_SIGNAL_LENGTH_INDEX, SYNC_BRIDGE_SIGNAL_STATE_INDEX, SYNC_BRIDGE_SIGNAL_STATE_READY, SYNC_BRIDGE_SIGNAL_STATUS_INDEX, SYNC_BRIDGE_STATUS_ERROR, SYNC_BRIDGE_STATUS_OK, toBrowserSyncBridgeError, } from "./sync-bridge.js";
|
|
4
|
+
const DEFAULT_BROWSER_TIMING_MITIGATION = "freeze";
|
|
3
5
|
const BROWSER_OPTION_VALIDATORS = [
|
|
4
6
|
{
|
|
5
7
|
label: "memoryLimit",
|
|
@@ -9,10 +11,6 @@ const BROWSER_OPTION_VALIDATORS = [
|
|
|
9
11
|
label: "cpuTimeLimitMs",
|
|
10
12
|
hasValue: (options) => options.cpuTimeLimitMs !== undefined,
|
|
11
13
|
},
|
|
12
|
-
{
|
|
13
|
-
label: "timingMitigation",
|
|
14
|
-
hasValue: (options) => options.timingMitigation !== undefined,
|
|
15
|
-
},
|
|
16
14
|
];
|
|
17
15
|
function serializePermissions(permissions) {
|
|
18
16
|
if (!permissions) {
|
|
@@ -35,6 +33,12 @@ function resolveWorkerUrl(workerUrl) {
|
|
|
35
33
|
}
|
|
36
34
|
return new URL("./worker.js", import.meta.url);
|
|
37
35
|
}
|
|
36
|
+
function createWorkerControlToken() {
|
|
37
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
38
|
+
return globalThis.crypto.randomUUID();
|
|
39
|
+
}
|
|
40
|
+
return `browser-runtime-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
41
|
+
}
|
|
38
42
|
function toBrowserWorkerExecOptions(options) {
|
|
39
43
|
if (!options) {
|
|
40
44
|
return undefined;
|
|
@@ -44,12 +48,11 @@ function toBrowserWorkerExecOptions(options) {
|
|
|
44
48
|
env: options.env,
|
|
45
49
|
cwd: options.cwd,
|
|
46
50
|
stdin: options.stdin,
|
|
51
|
+
timingMitigation: options.timingMitigation,
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
54
|
function validateBrowserRuntimeOptions(options) {
|
|
50
|
-
const unsupported = BROWSER_OPTION_VALIDATORS
|
|
51
|
-
.filter((validator) => validator.hasValue(options))
|
|
52
|
-
.map((validator) => validator.label);
|
|
55
|
+
const unsupported = BROWSER_OPTION_VALIDATORS.filter((validator) => validator.hasValue(options)).map((validator) => validator.label);
|
|
53
56
|
if (unsupported.length === 0) {
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
@@ -60,30 +63,171 @@ function validateBrowserExecOptions(options) {
|
|
|
60
63
|
if (options?.cpuTimeLimitMs !== undefined) {
|
|
61
64
|
unsupported.push("cpuTimeLimitMs");
|
|
62
65
|
}
|
|
63
|
-
if (options?.timingMitigation !== undefined) {
|
|
64
|
-
unsupported.push("timingMitigation");
|
|
65
|
-
}
|
|
66
66
|
if (unsupported.length === 0) {
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
throw new Error(`Browser runtime does not support Node-only exec options: ${unsupported.join(", ")}`);
|
|
70
70
|
}
|
|
71
|
+
function isStdioMessage(message) {
|
|
72
|
+
return message.type === "stdio";
|
|
73
|
+
}
|
|
74
|
+
function isResponseMessage(message) {
|
|
75
|
+
return message.type === "response";
|
|
76
|
+
}
|
|
77
|
+
function isSyncRequestMessage(message) {
|
|
78
|
+
return message.type === "sync-request";
|
|
79
|
+
}
|
|
80
|
+
function createSyncBridgeFilesystem(options) {
|
|
81
|
+
return options.system.filesystem ?? createFsStub();
|
|
82
|
+
}
|
|
83
|
+
function throwBridgePayloadTooLarge(label, actualBytes, maxBytes) {
|
|
84
|
+
const error = new Error(`[${SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE}] ${label}: payload is ${actualBytes} bytes, limit is ${maxBytes} bytes`);
|
|
85
|
+
error.code = SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
function toUint8Array(value) {
|
|
89
|
+
if (value instanceof Uint8Array) {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (ArrayBuffer.isView(value)) {
|
|
93
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
94
|
+
}
|
|
95
|
+
if (value instanceof ArrayBuffer) {
|
|
96
|
+
return new Uint8Array(value);
|
|
97
|
+
}
|
|
98
|
+
return new TextEncoder().encode(String(value));
|
|
99
|
+
}
|
|
100
|
+
function createSyncBridgeResponseBytes(response, encoder) {
|
|
101
|
+
switch (response.kind) {
|
|
102
|
+
case SYNC_BRIDGE_KIND_NONE:
|
|
103
|
+
return new Uint8Array(0);
|
|
104
|
+
case SYNC_BRIDGE_KIND_TEXT:
|
|
105
|
+
return encoder.encode(response.value);
|
|
106
|
+
case SYNC_BRIDGE_KIND_BINARY:
|
|
107
|
+
return response.value;
|
|
108
|
+
case SYNC_BRIDGE_KIND_JSON:
|
|
109
|
+
return encoder.encode(JSON.stringify(response.value));
|
|
110
|
+
default:
|
|
111
|
+
return new Uint8Array(0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function handleSyncBridgeOperation(filesystem, message) {
|
|
115
|
+
switch (message.operation) {
|
|
116
|
+
case "fs.readFile":
|
|
117
|
+
return {
|
|
118
|
+
kind: SYNC_BRIDGE_KIND_TEXT,
|
|
119
|
+
value: await filesystem.readTextFile(String(message.args[0])),
|
|
120
|
+
};
|
|
121
|
+
case "fs.writeFile":
|
|
122
|
+
await filesystem.writeFile(String(message.args[0]), String(message.args[1] ?? ""));
|
|
123
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
124
|
+
case "fs.readFileBinary":
|
|
125
|
+
return {
|
|
126
|
+
kind: SYNC_BRIDGE_KIND_BINARY,
|
|
127
|
+
value: await filesystem.readFile(String(message.args[0])),
|
|
128
|
+
};
|
|
129
|
+
case "fs.writeFileBinary":
|
|
130
|
+
await filesystem.writeFile(String(message.args[0]), toUint8Array(message.args[1]));
|
|
131
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
132
|
+
case "fs.readDir":
|
|
133
|
+
return {
|
|
134
|
+
kind: SYNC_BRIDGE_KIND_JSON,
|
|
135
|
+
value: await filesystem.readDirWithTypes(String(message.args[0])),
|
|
136
|
+
};
|
|
137
|
+
case "fs.createDir":
|
|
138
|
+
await filesystem.createDir(String(message.args[0]));
|
|
139
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
140
|
+
case "fs.mkdir":
|
|
141
|
+
await mkdir(filesystem, String(message.args[0]));
|
|
142
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
143
|
+
case "fs.rmdir":
|
|
144
|
+
await filesystem.removeDir(String(message.args[0]));
|
|
145
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
146
|
+
case "fs.exists":
|
|
147
|
+
return {
|
|
148
|
+
kind: SYNC_BRIDGE_KIND_JSON,
|
|
149
|
+
value: await filesystem.exists(String(message.args[0])),
|
|
150
|
+
};
|
|
151
|
+
case "fs.stat":
|
|
152
|
+
return {
|
|
153
|
+
kind: SYNC_BRIDGE_KIND_JSON,
|
|
154
|
+
value: await filesystem.stat(String(message.args[0])),
|
|
155
|
+
};
|
|
156
|
+
case "fs.lstat":
|
|
157
|
+
return {
|
|
158
|
+
kind: SYNC_BRIDGE_KIND_JSON,
|
|
159
|
+
value: await filesystem.lstat(String(message.args[0])),
|
|
160
|
+
};
|
|
161
|
+
case "fs.unlink":
|
|
162
|
+
await filesystem.removeFile(String(message.args[0]));
|
|
163
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
164
|
+
case "fs.rename":
|
|
165
|
+
await filesystem.rename(String(message.args[0]), String(message.args[1]));
|
|
166
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
167
|
+
case "fs.realpath":
|
|
168
|
+
return {
|
|
169
|
+
kind: SYNC_BRIDGE_KIND_TEXT,
|
|
170
|
+
value: await filesystem.realpath(String(message.args[0])),
|
|
171
|
+
};
|
|
172
|
+
case "fs.readlink":
|
|
173
|
+
return {
|
|
174
|
+
kind: SYNC_BRIDGE_KIND_TEXT,
|
|
175
|
+
value: await filesystem.readlink(String(message.args[0])),
|
|
176
|
+
};
|
|
177
|
+
case "fs.symlink":
|
|
178
|
+
await filesystem.symlink(String(message.args[0]), String(message.args[1]));
|
|
179
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
180
|
+
case "fs.link":
|
|
181
|
+
await filesystem.link(String(message.args[0]), String(message.args[1]));
|
|
182
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
183
|
+
case "fs.chmod":
|
|
184
|
+
await filesystem.chmod(String(message.args[0]), Number(message.args[1]));
|
|
185
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
186
|
+
case "fs.truncate":
|
|
187
|
+
await filesystem.truncate(String(message.args[0]), Number(message.args[1]));
|
|
188
|
+
return { kind: SYNC_BRIDGE_KIND_NONE };
|
|
189
|
+
case "module.resolve": {
|
|
190
|
+
const resolved = await resolveModule(String(message.args[0]), String(message.args[1]), filesystem);
|
|
191
|
+
return resolved === null
|
|
192
|
+
? { kind: SYNC_BRIDGE_KIND_NONE }
|
|
193
|
+
: { kind: SYNC_BRIDGE_KIND_TEXT, value: resolved };
|
|
194
|
+
}
|
|
195
|
+
case "module.loadFile": {
|
|
196
|
+
const source = await loadFile(String(message.args[0]), filesystem);
|
|
197
|
+
return source === null
|
|
198
|
+
? { kind: SYNC_BRIDGE_KIND_NONE }
|
|
199
|
+
: { kind: SYNC_BRIDGE_KIND_TEXT, value: source };
|
|
200
|
+
}
|
|
201
|
+
default:
|
|
202
|
+
throw new Error(`Unsupported browser sync bridge operation: ${String(message.operation)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
71
205
|
export class BrowserRuntimeDriver {
|
|
72
|
-
options;
|
|
73
206
|
worker;
|
|
74
207
|
pending = new Map();
|
|
208
|
+
controlToken = createWorkerControlToken();
|
|
75
209
|
defaultOnStdio;
|
|
210
|
+
defaultTimingMitigation;
|
|
76
211
|
networkAdapter;
|
|
212
|
+
syncBridge;
|
|
213
|
+
syncFilesystem;
|
|
77
214
|
ready;
|
|
215
|
+
encoder = new TextEncoder();
|
|
78
216
|
nextId = 1;
|
|
79
217
|
disposed = false;
|
|
80
218
|
constructor(options, factoryOptions = {}) {
|
|
81
|
-
this.options = options;
|
|
82
219
|
if (typeof Worker === "undefined") {
|
|
83
220
|
throw new Error("Browser runtime requires a global Worker implementation");
|
|
84
221
|
}
|
|
222
|
+
assertBrowserSyncBridgeSupport();
|
|
85
223
|
this.defaultOnStdio = options.onStdio;
|
|
224
|
+
this.defaultTimingMitigation =
|
|
225
|
+
options.timingMitigation ??
|
|
226
|
+
options.runtime.process.timingMitigation ??
|
|
227
|
+
DEFAULT_BROWSER_TIMING_MITIGATION;
|
|
86
228
|
this.networkAdapter = options.system.network ?? createNetworkStub();
|
|
229
|
+
this.syncBridge = createBrowserSyncBridgePayload(options.payloadLimits);
|
|
230
|
+
this.syncFilesystem = createSyncBridgeFilesystem(options);
|
|
87
231
|
this.worker = new Worker(resolveWorkerUrl(factoryOptions.workerUrl), {
|
|
88
232
|
type: "module",
|
|
89
233
|
});
|
|
@@ -96,7 +240,9 @@ export class BrowserRuntimeDriver {
|
|
|
96
240
|
permissions: serializePermissions(options.system.permissions),
|
|
97
241
|
filesystem: browserSystemOptions.filesystem,
|
|
98
242
|
networkEnabled: browserSystemOptions.networkEnabled,
|
|
243
|
+
timingMitigation: this.defaultTimingMitigation,
|
|
99
244
|
payloadLimits: options.payloadLimits,
|
|
245
|
+
syncBridge: this.syncBridge,
|
|
100
246
|
};
|
|
101
247
|
this.ready = this.callWorker("init", initPayload).then(() => undefined);
|
|
102
248
|
this.ready.catch(() => undefined);
|
|
@@ -110,16 +256,29 @@ export class BrowserRuntimeDriver {
|
|
|
110
256
|
};
|
|
111
257
|
}
|
|
112
258
|
handleWorkerError = (event) => {
|
|
259
|
+
if (this.disposed) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
113
262
|
const error = event.error instanceof Error
|
|
114
263
|
? event.error
|
|
115
264
|
: new Error(event.message
|
|
116
265
|
? `Browser runtime worker error: ${event.message} (${event.filename}:${event.lineno}:${event.colno})`
|
|
117
266
|
: "Browser runtime worker error");
|
|
118
|
-
this.
|
|
267
|
+
this.cleanup(error, { terminateWorker: true });
|
|
119
268
|
};
|
|
120
269
|
handleWorkerMessage = (event) => {
|
|
270
|
+
if (this.disposed) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
121
273
|
const message = event.data;
|
|
122
|
-
if (message.
|
|
274
|
+
if (message.controlToken !== this.controlToken) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (isSyncRequestMessage(message)) {
|
|
278
|
+
void this.handleSyncRequest(message);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (isStdioMessage(message)) {
|
|
123
282
|
const pending = this.pending.get(message.requestId);
|
|
124
283
|
const hook = pending?.hook ?? this.defaultOnStdio;
|
|
125
284
|
if (!hook) {
|
|
@@ -133,6 +292,9 @@ export class BrowserRuntimeDriver {
|
|
|
133
292
|
}
|
|
134
293
|
return;
|
|
135
294
|
}
|
|
295
|
+
if (!isResponseMessage(message)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
136
298
|
const pending = this.pending.get(message.id);
|
|
137
299
|
if (!pending) {
|
|
138
300
|
return;
|
|
@@ -149,6 +311,37 @@ export class BrowserRuntimeDriver {
|
|
|
149
311
|
error.code = message.error.code;
|
|
150
312
|
pending.reject(error);
|
|
151
313
|
};
|
|
314
|
+
async handleSyncRequest(message) {
|
|
315
|
+
const signal = new Int32Array(this.syncBridge.signalBuffer);
|
|
316
|
+
const data = new Uint8Array(this.syncBridge.dataBuffer);
|
|
317
|
+
try {
|
|
318
|
+
const response = await handleSyncBridgeOperation(this.syncFilesystem, message);
|
|
319
|
+
const bytes = createSyncBridgeResponseBytes(response, this.encoder);
|
|
320
|
+
if (bytes.byteLength > data.byteLength) {
|
|
321
|
+
const suffix = typeof message.args[0] === "string" ? ` ${message.args[0]}` : "";
|
|
322
|
+
throwBridgePayloadTooLarge(`${message.operation}${suffix}`, bytes.byteLength, data.byteLength);
|
|
323
|
+
}
|
|
324
|
+
data.set(bytes, 0);
|
|
325
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_STATUS_INDEX, SYNC_BRIDGE_STATUS_OK);
|
|
326
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_KIND_INDEX, response.kind);
|
|
327
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_LENGTH_INDEX, bytes.byteLength);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
let bytes = this.encoder.encode(JSON.stringify(toBrowserSyncBridgeError(error)));
|
|
331
|
+
if (bytes.byteLength > data.byteLength) {
|
|
332
|
+
bytes = this.encoder.encode(JSON.stringify({
|
|
333
|
+
message: "Browser runtime sync bridge error exceeded shared buffer capacity",
|
|
334
|
+
code: SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
data.set(bytes, 0);
|
|
338
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_STATUS_INDEX, SYNC_BRIDGE_STATUS_ERROR);
|
|
339
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_KIND_INDEX, SYNC_BRIDGE_KIND_JSON);
|
|
340
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_LENGTH_INDEX, bytes.byteLength);
|
|
341
|
+
}
|
|
342
|
+
Atomics.store(signal, SYNC_BRIDGE_SIGNAL_STATE_INDEX, SYNC_BRIDGE_SIGNAL_STATE_READY);
|
|
343
|
+
Atomics.notify(signal, SYNC_BRIDGE_SIGNAL_STATE_INDEX, 1);
|
|
344
|
+
}
|
|
152
345
|
rejectAllPending(error) {
|
|
153
346
|
const entries = Array.from(this.pending.values());
|
|
154
347
|
this.pending.clear();
|
|
@@ -156,17 +349,68 @@ export class BrowserRuntimeDriver {
|
|
|
156
349
|
pending.reject(error);
|
|
157
350
|
}
|
|
158
351
|
}
|
|
352
|
+
clearWorkerHandlers() {
|
|
353
|
+
try {
|
|
354
|
+
this.worker.onmessage = null;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Ignore host Worker implementations with non-writable event hooks.
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
this.worker.onerror = null;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Ignore host Worker implementations with non-writable event hooks.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
resetSyncBridgeState() {
|
|
367
|
+
new Int32Array(this.syncBridge.signalBuffer).fill(0);
|
|
368
|
+
new Uint8Array(this.syncBridge.dataBuffer).fill(0);
|
|
369
|
+
}
|
|
370
|
+
cleanup(error, options = {}) {
|
|
371
|
+
if (this.disposed) {
|
|
372
|
+
this.rejectAllPending(error);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
this.disposed = true;
|
|
376
|
+
this.clearWorkerHandlers();
|
|
377
|
+
if (options.terminateWorker) {
|
|
378
|
+
try {
|
|
379
|
+
this.worker.terminate();
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Ignore termination errors while tearing down a broken worker.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.resetSyncBridgeState();
|
|
386
|
+
this.rejectAllPending(error);
|
|
387
|
+
}
|
|
159
388
|
callWorker(type, payload, hook) {
|
|
160
389
|
if (this.disposed) {
|
|
161
390
|
return Promise.reject(new Error("Browser runtime has been disposed"));
|
|
162
391
|
}
|
|
163
392
|
const id = this.nextId++;
|
|
164
393
|
const message = payload === undefined
|
|
165
|
-
? {
|
|
166
|
-
|
|
394
|
+
? {
|
|
395
|
+
controlToken: this.controlToken,
|
|
396
|
+
id,
|
|
397
|
+
type,
|
|
398
|
+
}
|
|
399
|
+
: {
|
|
400
|
+
controlToken: this.controlToken,
|
|
401
|
+
id,
|
|
402
|
+
type,
|
|
403
|
+
payload,
|
|
404
|
+
};
|
|
167
405
|
return new Promise((resolve, reject) => {
|
|
168
406
|
this.pending.set(id, { resolve, reject, hook });
|
|
169
|
-
|
|
407
|
+
try {
|
|
408
|
+
this.worker.postMessage(message);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
this.pending.delete(id);
|
|
412
|
+
reject(error);
|
|
413
|
+
}
|
|
170
414
|
});
|
|
171
415
|
}
|
|
172
416
|
async run(code, filePath) {
|
|
@@ -188,13 +432,18 @@ export class BrowserRuntimeDriver {
|
|
|
188
432
|
captureStdio: Boolean(hook),
|
|
189
433
|
}, hook);
|
|
190
434
|
}
|
|
435
|
+
async dispatchExtensionRequest(namespace, payload) {
|
|
436
|
+
await this.ready;
|
|
437
|
+
const response = await this.callWorker("extension", { namespace, payload });
|
|
438
|
+
return response.payload;
|
|
439
|
+
}
|
|
191
440
|
dispose() {
|
|
192
441
|
if (this.disposed) {
|
|
193
442
|
return;
|
|
194
443
|
}
|
|
195
|
-
this.disposed
|
|
196
|
-
|
|
197
|
-
|
|
444
|
+
this.cleanup(new Error("Browser runtime has been disposed"), {
|
|
445
|
+
terminateWorker: true,
|
|
446
|
+
});
|
|
198
447
|
}
|
|
199
448
|
async terminate() {
|
|
200
449
|
this.dispose();
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { createInMemoryFileSystem, InMemoryFileSystem } from "./os-filesystem.js";
|
|
2
|
+
export type StdioChannel = "stdout" | "stderr";
|
|
3
|
+
export type TimingMitigation = "off" | "freeze";
|
|
4
|
+
type BodyLike = unknown;
|
|
5
|
+
export interface VirtualDirEntry {
|
|
6
|
+
name: string;
|
|
7
|
+
isDirectory: boolean;
|
|
8
|
+
isSymbolicLink?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface VirtualStat {
|
|
11
|
+
mode: number;
|
|
12
|
+
size: number;
|
|
13
|
+
blocks: number;
|
|
14
|
+
dev: number;
|
|
15
|
+
rdev: number;
|
|
16
|
+
isDirectory: boolean;
|
|
17
|
+
isSymbolicLink: boolean;
|
|
18
|
+
atimeMs: number;
|
|
19
|
+
mtimeMs: number;
|
|
20
|
+
ctimeMs: number;
|
|
21
|
+
birthtimeMs: number;
|
|
22
|
+
ino: number;
|
|
23
|
+
nlink: number;
|
|
24
|
+
uid: number;
|
|
25
|
+
gid: number;
|
|
26
|
+
}
|
|
27
|
+
export interface VirtualFileSystem {
|
|
28
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
29
|
+
readTextFile(path: string): Promise<string>;
|
|
30
|
+
readDir(path: string): Promise<string[]>;
|
|
31
|
+
readDirWithTypes(path: string): Promise<VirtualDirEntry[]>;
|
|
32
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
33
|
+
createDir(path: string): Promise<void>;
|
|
34
|
+
mkdir(path: string, options?: {
|
|
35
|
+
recursive?: boolean;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
exists(path: string): Promise<boolean>;
|
|
38
|
+
stat(path: string): Promise<VirtualStat>;
|
|
39
|
+
removeFile(path: string): Promise<void>;
|
|
40
|
+
removeDir(path: string): Promise<void>;
|
|
41
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
42
|
+
realpath(path: string): Promise<string>;
|
|
43
|
+
symlink(target: string, linkPath: string): Promise<void>;
|
|
44
|
+
readlink(path: string): Promise<string>;
|
|
45
|
+
lstat(path: string): Promise<VirtualStat>;
|
|
46
|
+
link(oldPath: string, newPath: string): Promise<void>;
|
|
47
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
48
|
+
chown(path: string, uid: number, gid: number): Promise<void>;
|
|
49
|
+
utimes(path: string, atime: number, mtime: number): Promise<void>;
|
|
50
|
+
truncate(path: string, length: number): Promise<void>;
|
|
51
|
+
pread(path: string, offset: number, length: number): Promise<Uint8Array>;
|
|
52
|
+
pwrite(path: string, offset: number, data: Uint8Array): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export type PermissionDecision = boolean | {
|
|
55
|
+
allowed: boolean;
|
|
56
|
+
reason?: string;
|
|
57
|
+
} | {
|
|
58
|
+
allow: boolean;
|
|
59
|
+
reason?: string;
|
|
60
|
+
};
|
|
61
|
+
export type PermissionCheck<T = unknown> = (request: T) => PermissionDecision;
|
|
62
|
+
export interface Permissions {
|
|
63
|
+
fs?: PermissionCheck<{
|
|
64
|
+
path: string;
|
|
65
|
+
operation: string;
|
|
66
|
+
}>;
|
|
67
|
+
network?: PermissionCheck<{
|
|
68
|
+
url?: string;
|
|
69
|
+
host?: string;
|
|
70
|
+
port?: number;
|
|
71
|
+
}>;
|
|
72
|
+
childProcess?: PermissionCheck<{
|
|
73
|
+
command: string;
|
|
74
|
+
args: string[];
|
|
75
|
+
}>;
|
|
76
|
+
env?: PermissionCheck<{
|
|
77
|
+
name: string;
|
|
78
|
+
value: string;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
export declare const allowAllFs: PermissionCheck;
|
|
82
|
+
export declare const allowAllNetwork: PermissionCheck;
|
|
83
|
+
export declare const allowAllChildProcess: PermissionCheck;
|
|
84
|
+
export declare const allowAllEnv: PermissionCheck;
|
|
85
|
+
export declare const allowAll: Permissions;
|
|
86
|
+
export interface ExecOptions {
|
|
87
|
+
filePath?: string;
|
|
88
|
+
env?: Record<string, string>;
|
|
89
|
+
cwd?: string;
|
|
90
|
+
stdin?: string;
|
|
91
|
+
cpuTimeLimitMs?: number;
|
|
92
|
+
timingMitigation?: TimingMitigation;
|
|
93
|
+
onStdio?: StdioHook;
|
|
94
|
+
}
|
|
95
|
+
export interface ExecResult {
|
|
96
|
+
code: number;
|
|
97
|
+
exitCode?: number;
|
|
98
|
+
stdout?: string;
|
|
99
|
+
stderr?: string;
|
|
100
|
+
errorMessage?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface RunResult<T = unknown> {
|
|
103
|
+
value?: T;
|
|
104
|
+
code: number;
|
|
105
|
+
errorMessage?: string;
|
|
106
|
+
exports?: T;
|
|
107
|
+
}
|
|
108
|
+
export interface OSConfig {
|
|
109
|
+
homedir?: string;
|
|
110
|
+
tmpdir?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface ProcessConfig {
|
|
113
|
+
cwd?: string;
|
|
114
|
+
env?: Record<string, string>;
|
|
115
|
+
argv?: string[];
|
|
116
|
+
timingMitigation?: TimingMitigation;
|
|
117
|
+
frozenTimeMs?: number;
|
|
118
|
+
}
|
|
119
|
+
export type StdioEvent = {
|
|
120
|
+
channel: StdioChannel;
|
|
121
|
+
message: string;
|
|
122
|
+
};
|
|
123
|
+
export type StdioHook = (event: StdioEvent) => void;
|
|
124
|
+
export interface CommandExecutor {
|
|
125
|
+
spawn(command: string, args: string[], options?: {
|
|
126
|
+
cwd?: string;
|
|
127
|
+
env?: Record<string, string>;
|
|
128
|
+
onStdout?: (data: Uint8Array) => void;
|
|
129
|
+
onStderr?: (data: Uint8Array) => void;
|
|
130
|
+
}): {
|
|
131
|
+
wait(): Promise<number>;
|
|
132
|
+
writeStdin(data: Uint8Array | string): void;
|
|
133
|
+
closeStdin(): void;
|
|
134
|
+
kill(signal?: number): void;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export interface NetworkAdapter {
|
|
138
|
+
fetch(url: string, options?: {
|
|
139
|
+
method?: string;
|
|
140
|
+
headers?: Record<string, string>;
|
|
141
|
+
body?: BodyLike | null;
|
|
142
|
+
}): Promise<{
|
|
143
|
+
ok: boolean;
|
|
144
|
+
status: number;
|
|
145
|
+
statusText: string;
|
|
146
|
+
headers: Record<string, string>;
|
|
147
|
+
body: string;
|
|
148
|
+
url: string;
|
|
149
|
+
redirected: boolean;
|
|
150
|
+
}>;
|
|
151
|
+
dnsLookup(hostname: string): Promise<{
|
|
152
|
+
address?: string;
|
|
153
|
+
family?: number;
|
|
154
|
+
error?: string;
|
|
155
|
+
code?: string;
|
|
156
|
+
}>;
|
|
157
|
+
httpRequest(url: string, options?: {
|
|
158
|
+
method?: string;
|
|
159
|
+
headers?: Record<string, string>;
|
|
160
|
+
body?: BodyLike | null;
|
|
161
|
+
}): Promise<{
|
|
162
|
+
status: number;
|
|
163
|
+
statusText: string;
|
|
164
|
+
headers: Record<string, string>;
|
|
165
|
+
body: string;
|
|
166
|
+
url: string;
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
169
|
+
export interface SystemDriver {
|
|
170
|
+
filesystem?: VirtualFileSystem;
|
|
171
|
+
network?: NetworkAdapter;
|
|
172
|
+
commandExecutor?: CommandExecutor;
|
|
173
|
+
permissions?: Permissions;
|
|
174
|
+
runtime: {
|
|
175
|
+
process: ProcessConfig;
|
|
176
|
+
os: OSConfig;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export interface RuntimeDriverOptions {
|
|
180
|
+
system: SystemDriver;
|
|
181
|
+
runtime: {
|
|
182
|
+
process: ProcessConfig;
|
|
183
|
+
os: OSConfig;
|
|
184
|
+
};
|
|
185
|
+
memoryLimit?: number;
|
|
186
|
+
cpuTimeLimitMs?: number;
|
|
187
|
+
timingMitigation?: TimingMitigation;
|
|
188
|
+
onStdio?: StdioHook;
|
|
189
|
+
payloadLimits?: {
|
|
190
|
+
base64TransferBytes?: number;
|
|
191
|
+
jsonPayloadBytes?: number;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export interface NodeRuntimeDriver {
|
|
195
|
+
exec(code: string, options?: ExecOptions): Promise<ExecResult>;
|
|
196
|
+
run<T = unknown>(code: string, filePath?: string): Promise<RunResult<T>>;
|
|
197
|
+
dispose(): void;
|
|
198
|
+
terminate?(): Promise<void>;
|
|
199
|
+
}
|
|
200
|
+
export interface NodeRuntimeDriverFactory {
|
|
201
|
+
createRuntimeDriver(options: RuntimeDriverOptions): NodeRuntimeDriver;
|
|
202
|
+
}
|
|
203
|
+
export declare function filterEnv(env: Record<string, string> | undefined, permissions?: Permissions): Record<string, string>;
|
|
204
|
+
export declare function createEnosysError(operation: string): Error;
|
|
205
|
+
export declare function createFsStub(): VirtualFileSystem;
|
|
206
|
+
export declare function createNetworkStub(): NetworkAdapter;
|
|
207
|
+
export declare function createCommandExecutorStub(): CommandExecutor;
|
|
208
|
+
export declare function wrapFileSystem(filesystem: VirtualFileSystem, permissions?: Permissions): VirtualFileSystem;
|
|
209
|
+
export declare function wrapNetworkAdapter(adapter: NetworkAdapter, permissions?: Permissions): NetworkAdapter;
|
|
210
|
+
export declare function mkdir(filesystem: VirtualFileSystem, path: string, options?: {
|
|
211
|
+
recursive?: boolean;
|
|
212
|
+
} | boolean): Promise<void>;
|
|
213
|
+
export declare function loadFile(path: string, filesystem: VirtualFileSystem): Promise<string | null>;
|
|
214
|
+
export declare function resolveModule(specifier: string, fromPath: string, filesystem: VirtualFileSystem, _mode?: "require" | "import"): Promise<string | null>;
|
|
215
|
+
export declare function isESM(code: string, filePath?: string): boolean;
|
|
216
|
+
export declare function transformDynamicImport(code: string): string;
|
|
217
|
+
export declare const POLYFILL_CODE_MAP: Record<string, string>;
|
|
218
|
+
export declare function exposeCustomGlobal(name: string, value: unknown): void;
|
|
219
|
+
export declare function exposeMutableRuntimeStateGlobal(name: string, value: unknown): void;
|
|
220
|
+
export declare function getIsolateRuntimeSource(id: string): string;
|
|
221
|
+
export declare function getRequireSetupCode(): string;
|
|
222
|
+
export { createInMemoryFileSystem, InMemoryFileSystem };
|