@secure-exec/browser 0.0.0-main.bccb3a2

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.
@@ -0,0 +1,402 @@
1
+ /**
2
+ * In-memory filesystem for browser environments.
3
+ *
4
+ * In-memory filesystem with POSIX extensions (symlinks, hard links, chmod,
5
+ * chown, utimes, truncate) needed by the kernel VFS interface.
6
+ */
7
+ const S_IFREG = 0o100000;
8
+ const S_IFDIR = 0o040000;
9
+ const S_IFLNK = 0o120000;
10
+ const MAX_SYMLINK_DEPTH = 40;
11
+ function normalizePath(path) {
12
+ if (!path)
13
+ return "/";
14
+ let normalized = path.startsWith("/") ? path : `/${path}`;
15
+ normalized = normalized.replace(/\/+/g, "/");
16
+ if (normalized.length > 1 && normalized.endsWith("/")) {
17
+ normalized = normalized.slice(0, -1);
18
+ }
19
+ // Resolve . and ..
20
+ const parts = normalized.split("/");
21
+ const resolved = [];
22
+ for (const part of parts) {
23
+ if (part === "." || part === "")
24
+ continue;
25
+ if (part === "..") {
26
+ resolved.pop();
27
+ }
28
+ else {
29
+ resolved.push(part);
30
+ }
31
+ }
32
+ return `/${resolved.join("/")}` || "/";
33
+ }
34
+ function dirname(path) {
35
+ const parts = normalizePath(path).split("/").filter(Boolean);
36
+ if (parts.length <= 1)
37
+ return "/";
38
+ return `/${parts.slice(0, -1).join("/")}`;
39
+ }
40
+ let nextIno = 1;
41
+ export class InMemoryFileSystem {
42
+ entries = new Map();
43
+ constructor() {
44
+ // Root directory
45
+ this.entries.set("/", this.newDir());
46
+ }
47
+ // --- Core operations ---
48
+ async readFile(path) {
49
+ const entry = this.resolveEntry(path);
50
+ if (!entry || entry.type !== "file") {
51
+ throw this.enoent("open", path);
52
+ }
53
+ entry.atimeMs = Date.now();
54
+ return entry.data;
55
+ }
56
+ async readTextFile(path) {
57
+ const data = await this.readFile(path);
58
+ return new TextDecoder().decode(data);
59
+ }
60
+ async readDir(path) {
61
+ return (await this.readDirWithTypes(path)).map((e) => e.name);
62
+ }
63
+ async readDirWithTypes(path) {
64
+ const resolved = this.resolvePath(path);
65
+ const dir = this.entries.get(resolved);
66
+ if (!dir || dir.type !== "dir") {
67
+ throw this.enoent("scandir", path);
68
+ }
69
+ const prefix = resolved === "/" ? "/" : `${resolved}/`;
70
+ const names = new Map();
71
+ for (const [entryPath, entry] of this.entries) {
72
+ if (entryPath.startsWith(prefix)) {
73
+ const rest = entryPath.slice(prefix.length);
74
+ if (rest && !rest.includes("/")) {
75
+ names.set(rest, {
76
+ name: rest,
77
+ isDirectory: entry.type === "dir",
78
+ isSymbolicLink: entry.type === "symlink",
79
+ });
80
+ }
81
+ }
82
+ }
83
+ return Array.from(names.values());
84
+ }
85
+ async writeFile(path, content) {
86
+ const normalized = normalizePath(path);
87
+ // Ensure parent exists
88
+ await this.mkdir(dirname(normalized), { recursive: true });
89
+ const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
90
+ const existing = this.entries.get(normalized);
91
+ if (existing && existing.type === "file") {
92
+ existing.data = data;
93
+ existing.mtimeMs = Date.now();
94
+ existing.ctimeMs = Date.now();
95
+ return;
96
+ }
97
+ const now = Date.now();
98
+ this.entries.set(normalized, {
99
+ type: "file",
100
+ data,
101
+ mode: S_IFREG | 0o644,
102
+ uid: 1000,
103
+ gid: 1000,
104
+ nlink: 1,
105
+ ino: nextIno++,
106
+ atimeMs: now,
107
+ mtimeMs: now,
108
+ ctimeMs: now,
109
+ birthtimeMs: now,
110
+ });
111
+ }
112
+ async createDir(path) {
113
+ const normalized = normalizePath(path);
114
+ const parent = dirname(normalized);
115
+ if (!this.entries.has(parent)) {
116
+ throw this.enoent("mkdir", path);
117
+ }
118
+ if (!this.entries.has(normalized)) {
119
+ this.entries.set(normalized, this.newDir());
120
+ }
121
+ }
122
+ async mkdir(path, options) {
123
+ const normalized = normalizePath(path);
124
+ if (options?.recursive !== false) {
125
+ // Recursive: create all missing parents
126
+ const parts = normalized.split("/").filter(Boolean);
127
+ let current = "";
128
+ for (const part of parts) {
129
+ current += `/${part}`;
130
+ if (!this.entries.has(current)) {
131
+ this.entries.set(current, this.newDir());
132
+ }
133
+ }
134
+ }
135
+ else {
136
+ await this.createDir(path);
137
+ }
138
+ }
139
+ async exists(path) {
140
+ try {
141
+ const resolved = this.resolvePath(path);
142
+ return this.entries.has(resolved);
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ async stat(path) {
149
+ const entry = this.resolveEntry(path);
150
+ if (!entry)
151
+ throw this.enoent("stat", path);
152
+ return this.toStat(entry);
153
+ }
154
+ async removeFile(path) {
155
+ const resolved = this.resolvePath(path);
156
+ const entry = this.entries.get(resolved);
157
+ if (!entry || entry.type === "dir") {
158
+ throw this.enoent("unlink", path);
159
+ }
160
+ this.entries.delete(resolved);
161
+ }
162
+ async removeDir(path) {
163
+ const resolved = this.resolvePath(path);
164
+ if (resolved === "/") {
165
+ throw new Error("EPERM: operation not permitted, rmdir '/'");
166
+ }
167
+ const entry = this.entries.get(resolved);
168
+ if (!entry || entry.type !== "dir") {
169
+ throw this.enoent("rmdir", path);
170
+ }
171
+ // Check if empty
172
+ const prefix = `${resolved}/`;
173
+ for (const key of this.entries.keys()) {
174
+ if (key.startsWith(prefix)) {
175
+ throw new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`);
176
+ }
177
+ }
178
+ this.entries.delete(resolved);
179
+ }
180
+ async realpath(path) {
181
+ return this.resolvePath(path);
182
+ }
183
+ async rename(oldPath, newPath) {
184
+ const oldResolved = this.resolvePath(oldPath);
185
+ const newNorm = normalizePath(newPath);
186
+ const entry = this.entries.get(oldResolved);
187
+ if (!entry)
188
+ throw this.enoent("rename", oldPath);
189
+ // Ensure parent of target exists
190
+ if (!this.entries.has(dirname(newNorm))) {
191
+ throw this.enoent("rename", newPath);
192
+ }
193
+ if (entry.type !== "dir") {
194
+ this.entries.set(newNorm, entry);
195
+ this.entries.delete(oldResolved);
196
+ return;
197
+ }
198
+ // Move directory and all children
199
+ const prefix = `${oldResolved}/`;
200
+ const toMove = [];
201
+ for (const [key, val] of this.entries) {
202
+ if (key === oldResolved || key.startsWith(prefix)) {
203
+ toMove.push([key, val]);
204
+ }
205
+ }
206
+ for (const [key] of toMove) {
207
+ this.entries.delete(key);
208
+ }
209
+ for (const [key, val] of toMove) {
210
+ const newKey = key === oldResolved ? newNorm : newNorm + key.slice(oldResolved.length);
211
+ this.entries.set(newKey, val);
212
+ }
213
+ }
214
+ // --- Symlinks ---
215
+ async symlink(target, linkPath) {
216
+ const normalized = normalizePath(linkPath);
217
+ if (this.entries.has(normalized)) {
218
+ throw new Error(`EEXIST: file already exists, symlink '${linkPath}'`);
219
+ }
220
+ const now = Date.now();
221
+ this.entries.set(normalized, {
222
+ type: "symlink",
223
+ target,
224
+ mode: S_IFLNK | 0o777,
225
+ uid: 1000,
226
+ gid: 1000,
227
+ nlink: 1,
228
+ ino: nextIno++,
229
+ atimeMs: now,
230
+ mtimeMs: now,
231
+ ctimeMs: now,
232
+ birthtimeMs: now,
233
+ });
234
+ }
235
+ async readlink(path) {
236
+ const normalized = normalizePath(path);
237
+ const entry = this.entries.get(normalized);
238
+ if (!entry || entry.type !== "symlink") {
239
+ throw this.enoent("readlink", path);
240
+ }
241
+ return entry.target;
242
+ }
243
+ async lstat(path) {
244
+ const normalized = normalizePath(path);
245
+ const entry = this.entries.get(normalized);
246
+ if (!entry)
247
+ throw this.enoent("lstat", path);
248
+ return this.toStat(entry);
249
+ }
250
+ // --- Links ---
251
+ async link(oldPath, newPath) {
252
+ const entry = this.resolveEntry(oldPath);
253
+ if (!entry || entry.type !== "file") {
254
+ throw this.enoent("link", oldPath);
255
+ }
256
+ const newNorm = normalizePath(newPath);
257
+ if (this.entries.has(newNorm)) {
258
+ throw new Error(`EEXIST: file already exists, link '${newPath}'`);
259
+ }
260
+ entry.nlink++;
261
+ this.entries.set(newNorm, entry);
262
+ }
263
+ // --- Permissions & Metadata ---
264
+ async chmod(path, mode) {
265
+ const entry = this.resolveEntry(path);
266
+ if (!entry)
267
+ throw this.enoent("chmod", path);
268
+ const callerTypeBits = mode & 0o170000;
269
+ if (callerTypeBits !== 0) {
270
+ entry.mode = mode;
271
+ }
272
+ else {
273
+ entry.mode = (entry.mode & 0o170000) | (mode & 0o7777);
274
+ }
275
+ entry.ctimeMs = Date.now();
276
+ }
277
+ async chown(path, uid, gid) {
278
+ const entry = this.resolveEntry(path);
279
+ if (!entry)
280
+ throw this.enoent("chown", path);
281
+ entry.uid = uid;
282
+ entry.gid = gid;
283
+ entry.ctimeMs = Date.now();
284
+ }
285
+ async utimes(path, atime, mtime) {
286
+ const entry = this.resolveEntry(path);
287
+ if (!entry)
288
+ throw this.enoent("utimes", path);
289
+ entry.atimeMs = atime;
290
+ entry.mtimeMs = mtime;
291
+ entry.ctimeMs = Date.now();
292
+ }
293
+ async truncate(path, length) {
294
+ const entry = this.resolveEntry(path);
295
+ if (!entry || entry.type !== "file") {
296
+ throw this.enoent("truncate", path);
297
+ }
298
+ if (length < entry.data.length) {
299
+ entry.data = entry.data.slice(0, length);
300
+ }
301
+ else if (length > entry.data.length) {
302
+ const newData = new Uint8Array(length);
303
+ newData.set(entry.data);
304
+ entry.data = newData;
305
+ }
306
+ entry.mtimeMs = Date.now();
307
+ entry.ctimeMs = Date.now();
308
+ }
309
+ async pread(path, offset, length) {
310
+ const entry = this.resolveEntry(path);
311
+ if (!entry || entry.type !== "file") {
312
+ throw this.enoent("open", path);
313
+ }
314
+ entry.atimeMs = Date.now();
315
+ if (offset >= entry.data.length)
316
+ return new Uint8Array(0);
317
+ return entry.data.slice(offset, Math.min(offset + length, entry.data.length));
318
+ }
319
+ async pwrite(path, offset, data) {
320
+ const entry = this.resolveEntry(path);
321
+ if (!entry || entry.type !== "file") {
322
+ throw this.enoent("open", path);
323
+ }
324
+ const endPos = offset + data.length;
325
+ const newContent = new Uint8Array(Math.max(entry.data.length, endPos));
326
+ newContent.set(entry.data);
327
+ newContent.set(data, offset);
328
+ entry.data = newContent;
329
+ const now = Date.now();
330
+ entry.mtimeMs = now;
331
+ entry.ctimeMs = now;
332
+ }
333
+ // --- Helpers ---
334
+ /**
335
+ * Resolve symlinks to get the final path. Returns the normalized path
336
+ * after following all symlinks.
337
+ */
338
+ resolvePath(path, depth = 0) {
339
+ if (depth > MAX_SYMLINK_DEPTH) {
340
+ throw new Error(`ELOOP: too many levels of symbolic links, '${path}'`);
341
+ }
342
+ const normalized = normalizePath(path);
343
+ const entry = this.entries.get(normalized);
344
+ if (!entry)
345
+ return normalized;
346
+ if (entry.type === "symlink") {
347
+ const target = entry.target.startsWith("/")
348
+ ? entry.target
349
+ : `${dirname(normalized)}/${entry.target}`;
350
+ return this.resolvePath(target, depth + 1);
351
+ }
352
+ return normalized;
353
+ }
354
+ /** Resolve a path and return the entry (following symlinks). */
355
+ resolveEntry(path) {
356
+ const resolved = this.resolvePath(path);
357
+ return this.entries.get(resolved);
358
+ }
359
+ newDir() {
360
+ const now = Date.now();
361
+ return {
362
+ type: "dir",
363
+ mode: S_IFDIR | 0o755,
364
+ uid: 1000,
365
+ gid: 1000,
366
+ nlink: 2,
367
+ ino: nextIno++,
368
+ atimeMs: now,
369
+ mtimeMs: now,
370
+ ctimeMs: now,
371
+ birthtimeMs: now,
372
+ };
373
+ }
374
+ toStat(entry) {
375
+ const size = entry.type === "file" ? entry.data.length : 4096;
376
+ return {
377
+ mode: entry.mode,
378
+ size,
379
+ blocks: size === 0 ? 0 : Math.ceil(size / 512),
380
+ dev: 1,
381
+ rdev: 0,
382
+ isDirectory: entry.type === "dir",
383
+ isSymbolicLink: entry.type === "symlink",
384
+ atimeMs: entry.atimeMs,
385
+ mtimeMs: entry.mtimeMs,
386
+ ctimeMs: entry.ctimeMs,
387
+ birthtimeMs: entry.birthtimeMs,
388
+ ino: entry.ino,
389
+ nlink: entry.nlink,
390
+ uid: entry.uid,
391
+ gid: entry.gid,
392
+ };
393
+ }
394
+ enoent(op, path) {
395
+ const err = new Error(`ENOENT: no such file or directory, ${op} '${path}'`);
396
+ err.code = "ENOENT";
397
+ return err;
398
+ }
399
+ }
400
+ export function createInMemoryFileSystem() {
401
+ return new InMemoryFileSystem();
402
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Validate permission callback source strings before revival via new Function().
3
+ *
4
+ * Permission callbacks are serialized with fn.toString() on the host and revived
5
+ * in the Web Worker. Because revival uses new Function(), the source must be
6
+ * validated to prevent code injection.
7
+ */
8
+ /**
9
+ * Validate that a permission callback source string is safe to revive.
10
+ *
11
+ * Returns true if the source appears to be a safe function expression.
12
+ * Returns false if the source contains blocked patterns that could indicate
13
+ * code injection.
14
+ */
15
+ export declare function validatePermissionSource(source: string): boolean;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Validate permission callback source strings before revival via new Function().
3
+ *
4
+ * Permission callbacks are serialized with fn.toString() on the host and revived
5
+ * in the Web Worker. Because revival uses new Function(), the source must be
6
+ * validated to prevent code injection.
7
+ */
8
+ /**
9
+ * Dangerous patterns that should never appear in a permission callback.
10
+ * These could be used to escape the sandbox or access host resources.
11
+ */
12
+ const BLOCKED_PATTERNS = [
13
+ // Code execution / eval
14
+ /\beval\s*\(/,
15
+ /\bFunction\s*\(/,
16
+ /\bnew\s+Function\b/,
17
+ // Module loading
18
+ /\bimport\s*\(/,
19
+ /\bimportScripts\s*\(/,
20
+ /\brequire\s*\(/,
21
+ // Global object access
22
+ /\bglobalThis\b/,
23
+ /\bself\b/,
24
+ /\bwindow\b/,
25
+ // Process/system access
26
+ /\bprocess\s*\.\s*(?:exit|kill|binding|_linkedBinding|env)\b/,
27
+ // Network / IO escape
28
+ /\bXMLHttpRequest\b/,
29
+ /\bWebSocket\b/,
30
+ /\bfetch\s*\(/,
31
+ // Prototype pollution / constructor abuse
32
+ /\bconstructor\s*\[/,
33
+ /\b__proto__\b/,
34
+ /Object\s*\.\s*(?:defineProperty|setPrototypeOf|assign)\b/,
35
+ // Dynamic property access on dangerous objects
36
+ /\bpostMessage\b/,
37
+ ];
38
+ /**
39
+ * Validate that a permission callback source string is safe to revive.
40
+ *
41
+ * Returns true if the source appears to be a safe function expression.
42
+ * Returns false if the source contains blocked patterns that could indicate
43
+ * code injection.
44
+ */
45
+ export function validatePermissionSource(source) {
46
+ if (!source || typeof source !== "string")
47
+ return false;
48
+ const trimmed = source.trim();
49
+ // Must look like a function expression (arrow function or function keyword)
50
+ const startsLikeFunction = trimmed.startsWith("function") ||
51
+ trimmed.startsWith("(") ||
52
+ // Single-param arrow functions: x => ...
53
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>/.test(trimmed);
54
+ if (!startsLikeFunction)
55
+ return false;
56
+ // Check for blocked patterns
57
+ for (const pattern of BLOCKED_PATTERNS) {
58
+ if (pattern.test(source))
59
+ return false;
60
+ }
61
+ return true;
62
+ }
@@ -0,0 +1,34 @@
1
+ import type { ExecOptions, ExecResult, NetworkAdapter, NodeRuntimeDriver, NodeRuntimeDriverFactory, RunResult, RuntimeDriverOptions } from "./runtime.js";
2
+ export interface BrowserRuntimeDriverFactoryOptions {
3
+ workerUrl?: URL | string;
4
+ }
5
+ export declare class BrowserRuntimeDriver implements NodeRuntimeDriver {
6
+ private readonly worker;
7
+ private readonly pending;
8
+ private readonly controlToken;
9
+ private readonly defaultOnStdio?;
10
+ private readonly defaultTimingMitigation;
11
+ private readonly networkAdapter;
12
+ private readonly syncBridge;
13
+ private readonly syncFilesystem;
14
+ private readonly ready;
15
+ private readonly encoder;
16
+ private nextId;
17
+ private disposed;
18
+ constructor(options: RuntimeDriverOptions, factoryOptions?: BrowserRuntimeDriverFactoryOptions);
19
+ get network(): Pick<NetworkAdapter, "fetch" | "dnsLookup" | "httpRequest">;
20
+ private handleWorkerError;
21
+ private handleWorkerMessage;
22
+ private handleSyncRequest;
23
+ private rejectAllPending;
24
+ private clearWorkerHandlers;
25
+ private resetSyncBridgeState;
26
+ private cleanup;
27
+ private callWorker;
28
+ run<T = unknown>(code: string, filePath?: string): Promise<RunResult<T>>;
29
+ exec(code: string, options?: ExecOptions): Promise<ExecResult>;
30
+ dispatchExtensionRequest(namespace: string, payload: Uint8Array): Promise<Uint8Array>;
31
+ dispose(): void;
32
+ terminate(): Promise<void>;
33
+ }
34
+ export declare function createBrowserRuntimeDriverFactory(factoryOptions?: BrowserRuntimeDriverFactoryOptions): NodeRuntimeDriverFactory;