@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createNetworkStub } from "@secure-exec/core";
|
|
2
|
+
import { getBrowserSystemDriverOptions, } from "./driver.js";
|
|
3
|
+
const BROWSER_OPTION_VALIDATORS = [
|
|
4
|
+
{
|
|
5
|
+
label: "memoryLimit",
|
|
6
|
+
hasValue: (options) => options.memoryLimit !== undefined,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
label: "cpuTimeLimitMs",
|
|
10
|
+
hasValue: (options) => options.cpuTimeLimitMs !== undefined,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
label: "timingMitigation",
|
|
14
|
+
hasValue: (options) => options.timingMitigation !== undefined,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
label: "payloadLimits.base64TransferBytes",
|
|
18
|
+
hasValue: (options) => options.payloadLimits?.base64TransferBytes !== undefined,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: "payloadLimits.jsonPayloadBytes",
|
|
22
|
+
hasValue: (options) => options.payloadLimits?.jsonPayloadBytes !== undefined,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
function serializePermissions(permissions) {
|
|
26
|
+
if (!permissions) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const serialize = (fn) => typeof fn === "function" ? fn.toString() : undefined;
|
|
30
|
+
return {
|
|
31
|
+
fs: serialize(permissions.fs),
|
|
32
|
+
network: serialize(permissions.network),
|
|
33
|
+
childProcess: serialize(permissions.childProcess),
|
|
34
|
+
env: serialize(permissions.env),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function resolveWorkerUrl(workerUrl) {
|
|
38
|
+
if (workerUrl instanceof URL) {
|
|
39
|
+
return workerUrl;
|
|
40
|
+
}
|
|
41
|
+
if (workerUrl) {
|
|
42
|
+
return new URL(workerUrl, import.meta.url);
|
|
43
|
+
}
|
|
44
|
+
return new URL("./worker.js", import.meta.url);
|
|
45
|
+
}
|
|
46
|
+
function toBrowserWorkerExecOptions(options) {
|
|
47
|
+
if (!options) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
filePath: options.filePath,
|
|
52
|
+
env: options.env,
|
|
53
|
+
cwd: options.cwd,
|
|
54
|
+
stdin: options.stdin,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function validateBrowserRuntimeOptions(options) {
|
|
58
|
+
const unsupported = BROWSER_OPTION_VALIDATORS
|
|
59
|
+
.filter((validator) => validator.hasValue(options))
|
|
60
|
+
.map((validator) => validator.label);
|
|
61
|
+
if (unsupported.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Browser runtime does not support Node-only options: ${unsupported.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
function validateBrowserExecOptions(options) {
|
|
67
|
+
const unsupported = [];
|
|
68
|
+
if (options?.cpuTimeLimitMs !== undefined) {
|
|
69
|
+
unsupported.push("cpuTimeLimitMs");
|
|
70
|
+
}
|
|
71
|
+
if (options?.timingMitigation !== undefined) {
|
|
72
|
+
unsupported.push("timingMitigation");
|
|
73
|
+
}
|
|
74
|
+
if (unsupported.length === 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Browser runtime does not support Node-only exec options: ${unsupported.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
export class BrowserRuntimeDriver {
|
|
80
|
+
options;
|
|
81
|
+
worker;
|
|
82
|
+
pending = new Map();
|
|
83
|
+
defaultOnStdio;
|
|
84
|
+
networkAdapter;
|
|
85
|
+
ready;
|
|
86
|
+
nextId = 1;
|
|
87
|
+
disposed = false;
|
|
88
|
+
constructor(options, factoryOptions = {}) {
|
|
89
|
+
this.options = options;
|
|
90
|
+
if (typeof Worker === "undefined") {
|
|
91
|
+
throw new Error("Browser runtime requires a global Worker implementation");
|
|
92
|
+
}
|
|
93
|
+
this.defaultOnStdio = options.onStdio;
|
|
94
|
+
this.networkAdapter = options.system.network ?? createNetworkStub();
|
|
95
|
+
this.worker = new Worker(resolveWorkerUrl(factoryOptions.workerUrl), {
|
|
96
|
+
type: "module",
|
|
97
|
+
});
|
|
98
|
+
this.worker.onmessage = this.handleWorkerMessage;
|
|
99
|
+
this.worker.onerror = this.handleWorkerError;
|
|
100
|
+
const browserSystemOptions = getBrowserSystemDriverOptions(options.system);
|
|
101
|
+
const initPayload = {
|
|
102
|
+
processConfig: options.runtime.process,
|
|
103
|
+
osConfig: options.runtime.os,
|
|
104
|
+
permissions: serializePermissions(options.system.permissions),
|
|
105
|
+
filesystem: browserSystemOptions.filesystem,
|
|
106
|
+
networkEnabled: browserSystemOptions.networkEnabled,
|
|
107
|
+
};
|
|
108
|
+
this.ready = this.callWorker("init", initPayload).then(() => undefined);
|
|
109
|
+
this.ready.catch(() => undefined);
|
|
110
|
+
}
|
|
111
|
+
get network() {
|
|
112
|
+
const adapter = this.networkAdapter;
|
|
113
|
+
return {
|
|
114
|
+
fetch: (url, options) => adapter.fetch(url, options),
|
|
115
|
+
dnsLookup: (hostname) => adapter.dnsLookup(hostname),
|
|
116
|
+
httpRequest: (url, options) => adapter.httpRequest(url, options),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
handleWorkerError = (event) => {
|
|
120
|
+
const error = event.error instanceof Error
|
|
121
|
+
? event.error
|
|
122
|
+
: new Error(event.message
|
|
123
|
+
? `Browser runtime worker error: ${event.message} (${event.filename}:${event.lineno}:${event.colno})`
|
|
124
|
+
: "Browser runtime worker error");
|
|
125
|
+
this.rejectAllPending(error);
|
|
126
|
+
};
|
|
127
|
+
handleWorkerMessage = (event) => {
|
|
128
|
+
const message = event.data;
|
|
129
|
+
if (message.type === "stdio") {
|
|
130
|
+
const pending = this.pending.get(message.requestId);
|
|
131
|
+
const hook = pending?.hook ?? this.defaultOnStdio;
|
|
132
|
+
if (!hook) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
hook({ channel: message.channel, message: message.message });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Ignore host hook errors so sandbox execution can continue.
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const pending = this.pending.get(message.id);
|
|
144
|
+
if (!pending) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.pending.delete(message.id);
|
|
148
|
+
if (message.ok) {
|
|
149
|
+
pending.resolve(message.result);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const error = new Error(message.error.message);
|
|
153
|
+
if (message.error.stack) {
|
|
154
|
+
error.stack = message.error.stack;
|
|
155
|
+
}
|
|
156
|
+
error.code = message.error.code;
|
|
157
|
+
pending.reject(error);
|
|
158
|
+
};
|
|
159
|
+
rejectAllPending(error) {
|
|
160
|
+
const entries = Array.from(this.pending.values());
|
|
161
|
+
this.pending.clear();
|
|
162
|
+
for (const pending of entries) {
|
|
163
|
+
pending.reject(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
callWorker(type, payload, hook) {
|
|
167
|
+
if (this.disposed) {
|
|
168
|
+
return Promise.reject(new Error("Browser runtime has been disposed"));
|
|
169
|
+
}
|
|
170
|
+
const id = this.nextId++;
|
|
171
|
+
const message = payload === undefined
|
|
172
|
+
? { id, type }
|
|
173
|
+
: { id, type, payload };
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
this.pending.set(id, { resolve, reject, hook });
|
|
176
|
+
this.worker.postMessage(message);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async run(code, filePath) {
|
|
180
|
+
await this.ready;
|
|
181
|
+
const hook = this.defaultOnStdio;
|
|
182
|
+
return this.callWorker("run", {
|
|
183
|
+
code,
|
|
184
|
+
filePath,
|
|
185
|
+
captureStdio: Boolean(hook),
|
|
186
|
+
}, hook);
|
|
187
|
+
}
|
|
188
|
+
async exec(code, options) {
|
|
189
|
+
validateBrowserExecOptions(options);
|
|
190
|
+
await this.ready;
|
|
191
|
+
const hook = options?.onStdio ?? this.defaultOnStdio;
|
|
192
|
+
return this.callWorker("exec", {
|
|
193
|
+
code,
|
|
194
|
+
options: toBrowserWorkerExecOptions(options),
|
|
195
|
+
captureStdio: Boolean(hook),
|
|
196
|
+
}, hook);
|
|
197
|
+
}
|
|
198
|
+
dispose() {
|
|
199
|
+
if (this.disposed) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.disposed = true;
|
|
203
|
+
this.worker.terminate();
|
|
204
|
+
this.rejectAllPending(new Error("Browser runtime has been disposed"));
|
|
205
|
+
}
|
|
206
|
+
async terminate() {
|
|
207
|
+
this.dispose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export function createBrowserRuntimeDriverFactory(factoryOptions = {}) {
|
|
211
|
+
return {
|
|
212
|
+
createRuntimeDriver(options) {
|
|
213
|
+
validateBrowserRuntimeOptions(options);
|
|
214
|
+
return new BrowserRuntimeDriver(options, factoryOptions);
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { OSConfig, ProcessConfig, ExecResult, RunResult, StdioChannel } from "@secure-exec/core";
|
|
2
|
+
export type SerializedPermissions = {
|
|
3
|
+
fs?: string;
|
|
4
|
+
network?: string;
|
|
5
|
+
childProcess?: string;
|
|
6
|
+
env?: string;
|
|
7
|
+
};
|
|
8
|
+
export type BrowserWorkerExecOptions = {
|
|
9
|
+
filePath?: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
stdin?: string;
|
|
13
|
+
};
|
|
14
|
+
export type BrowserWorkerInitPayload = {
|
|
15
|
+
processConfig?: ProcessConfig;
|
|
16
|
+
osConfig?: OSConfig;
|
|
17
|
+
permissions?: SerializedPermissions;
|
|
18
|
+
filesystem?: "opfs" | "memory";
|
|
19
|
+
networkEnabled?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type BrowserWorkerRequestMessage = {
|
|
22
|
+
id: number;
|
|
23
|
+
type: "init";
|
|
24
|
+
payload: BrowserWorkerInitPayload;
|
|
25
|
+
} | {
|
|
26
|
+
id: number;
|
|
27
|
+
type: "exec";
|
|
28
|
+
payload: {
|
|
29
|
+
code: string;
|
|
30
|
+
options?: BrowserWorkerExecOptions;
|
|
31
|
+
captureStdio?: boolean;
|
|
32
|
+
};
|
|
33
|
+
} | {
|
|
34
|
+
id: number;
|
|
35
|
+
type: "run";
|
|
36
|
+
payload: {
|
|
37
|
+
code: string;
|
|
38
|
+
filePath?: string;
|
|
39
|
+
captureStdio?: boolean;
|
|
40
|
+
};
|
|
41
|
+
} | {
|
|
42
|
+
id: number;
|
|
43
|
+
type: "dispose";
|
|
44
|
+
};
|
|
45
|
+
export type BrowserWorkerResponseMessage = {
|
|
46
|
+
type: "response";
|
|
47
|
+
id: number;
|
|
48
|
+
ok: true;
|
|
49
|
+
result: ExecResult | RunResult | true;
|
|
50
|
+
} | {
|
|
51
|
+
type: "response";
|
|
52
|
+
id: number;
|
|
53
|
+
ok: false;
|
|
54
|
+
error: {
|
|
55
|
+
message: string;
|
|
56
|
+
stack?: string;
|
|
57
|
+
code?: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export type BrowserWorkerStdioMessage = {
|
|
61
|
+
type: "stdio";
|
|
62
|
+
requestId: number;
|
|
63
|
+
channel: StdioChannel;
|
|
64
|
+
message: string;
|
|
65
|
+
};
|
|
66
|
+
export type BrowserWorkerOutboundMessage = BrowserWorkerResponseMessage | BrowserWorkerStdioMessage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|