@secure-exec/core 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/bridge/active-handles.d.ts +21 -0
- package/dist/bridge/active-handles.js +60 -0
- package/dist/bridge/child-process.d.ts +90 -0
- package/dist/bridge/child-process.js +606 -0
- package/dist/bridge/fs.d.ts +281 -0
- package/dist/bridge/fs.js +2151 -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 +308 -0
- package/dist/bridge/network.d.ts +249 -0
- package/dist/bridge/network.js +1416 -0
- package/dist/bridge/os.d.ts +13 -0
- package/dist/bridge/os.js +256 -0
- package/dist/bridge/polyfills.d.ts +2 -0
- package/dist/bridge/polyfills.js +11 -0
- package/dist/bridge/process.d.ts +86 -0
- package/dist/bridge/process.js +938 -0
- package/dist/bridge-setup.d.ts +6 -0
- package/dist/bridge-setup.js +9 -0
- package/dist/bridge.js +11538 -0
- package/dist/esm-compiler.d.ts +14 -0
- package/dist/esm-compiler.js +68 -0
- package/dist/fs-helpers.d.ts +23 -0
- package/dist/fs-helpers.js +41 -0
- package/dist/generated/isolate-runtime.d.ts +19 -0
- package/dist/generated/isolate-runtime.js +21 -0
- package/dist/generated/polyfills.d.ts +82 -0
- package/dist/generated/polyfills.js +82 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +25 -0
- package/dist/isolate-runtime/apply-custom-global-policy.js +54 -0
- package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +44 -0
- package/dist/isolate-runtime/apply-timing-mitigation-off.js +14 -0
- package/dist/isolate-runtime/bridge-attach.js +29 -0
- package/dist/isolate-runtime/bridge-initial-globals.js +246 -0
- package/dist/isolate-runtime/eval-script-result.js +8 -0
- package/dist/isolate-runtime/global-exposure-helpers.js +36 -0
- package/dist/isolate-runtime/init-commonjs-module-globals.js +28 -0
- package/dist/isolate-runtime/override-process-cwd.js +8 -0
- package/dist/isolate-runtime/override-process-env.js +8 -0
- package/dist/isolate-runtime/require-setup.js +650 -0
- package/dist/isolate-runtime/set-commonjs-file-globals.js +36 -0
- package/dist/isolate-runtime/set-stdin-data.js +10 -0
- package/dist/isolate-runtime/setup-dynamic-import.js +64 -0
- package/dist/isolate-runtime/setup-fs-facade.js +48 -0
- package/dist/module-resolver.d.ts +25 -0
- package/dist/module-resolver.js +264 -0
- package/dist/package-bundler.d.ts +36 -0
- package/dist/package-bundler.js +497 -0
- package/dist/python-runtime.d.ts +16 -0
- package/dist/python-runtime.js +45 -0
- package/dist/runtime-driver.d.ts +62 -0
- package/dist/runtime-driver.js +1 -0
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +69 -0
- package/dist/shared/api-types.d.ts +71 -0
- package/dist/shared/api-types.js +1 -0
- package/dist/shared/bridge-contract.d.ts +302 -0
- package/dist/shared/bridge-contract.js +82 -0
- package/dist/shared/console-formatter.d.ts +22 -0
- package/dist/shared/console-formatter.js +157 -0
- package/dist/shared/constants.d.ts +3 -0
- package/dist/shared/constants.js +3 -0
- package/dist/shared/errors.d.ts +16 -0
- package/dist/shared/errors.js +21 -0
- package/dist/shared/esm-utils.d.ts +28 -0
- package/dist/shared/esm-utils.js +97 -0
- package/dist/shared/global-exposure.d.ts +38 -0
- package/dist/shared/global-exposure.js +406 -0
- package/dist/shared/in-memory-fs.d.ts +42 -0
- package/dist/shared/in-memory-fs.js +341 -0
- package/dist/shared/permissions.d.ts +38 -0
- package/dist/shared/permissions.js +283 -0
- package/dist/shared/require-setup.d.ts +6 -0
- package/dist/shared/require-setup.js +9 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.js +1 -0
- package/package.json +107 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
const S_IFREG = 0o100000;
|
|
2
|
+
const S_IFDIR = 0o040000;
|
|
3
|
+
const S_IFLNK = 0o120000;
|
|
4
|
+
function normalizePath(path) {
|
|
5
|
+
if (!path)
|
|
6
|
+
return "/";
|
|
7
|
+
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
8
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
9
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
10
|
+
normalized = normalized.slice(0, -1);
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
function splitPath(path) {
|
|
15
|
+
const normalized = normalizePath(path);
|
|
16
|
+
return normalized === "/" ? [] : normalized.slice(1).split("/");
|
|
17
|
+
}
|
|
18
|
+
function dirname(path) {
|
|
19
|
+
const parts = splitPath(path);
|
|
20
|
+
if (parts.length <= 1)
|
|
21
|
+
return "/";
|
|
22
|
+
return `/${parts.slice(0, -1).join("/")}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A fully in-memory VirtualFileSystem backed by Maps.
|
|
26
|
+
* Used as the default filesystem for the browser sandbox and for tests.
|
|
27
|
+
* Paths are always POSIX-style (forward slashes, rooted at "/").
|
|
28
|
+
*/
|
|
29
|
+
export class InMemoryFileSystem {
|
|
30
|
+
files = new Map();
|
|
31
|
+
dirs = new Set(["/"]);
|
|
32
|
+
symlinks = new Map();
|
|
33
|
+
modes = new Map();
|
|
34
|
+
owners = new Map();
|
|
35
|
+
timestamps = new Map();
|
|
36
|
+
hardLinks = new Map(); // newPath → originalPath
|
|
37
|
+
listDirEntries(path) {
|
|
38
|
+
const normalized = normalizePath(path);
|
|
39
|
+
if (!this.dirs.has(normalized)) {
|
|
40
|
+
throw new Error(`ENOENT: no such file or directory, scandir '${normalized}'`);
|
|
41
|
+
}
|
|
42
|
+
const prefix = normalized === "/" ? "/" : `${normalized}/`;
|
|
43
|
+
const entries = new Map();
|
|
44
|
+
for (const filePath of this.files.keys()) {
|
|
45
|
+
if (filePath.startsWith(prefix)) {
|
|
46
|
+
const rest = filePath.slice(prefix.length);
|
|
47
|
+
if (rest && !rest.includes("/")) {
|
|
48
|
+
entries.set(rest, false);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const dirPath of this.dirs.values()) {
|
|
53
|
+
if (dirPath.startsWith(prefix)) {
|
|
54
|
+
const rest = dirPath.slice(prefix.length);
|
|
55
|
+
if (rest && !rest.includes("/")) {
|
|
56
|
+
entries.set(rest, true);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Array.from(entries.entries()).map(([name, isDirectory]) => ({
|
|
61
|
+
name,
|
|
62
|
+
isDirectory,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
async readFile(path) {
|
|
66
|
+
const normalized = normalizePath(path);
|
|
67
|
+
const data = this.files.get(normalized);
|
|
68
|
+
if (!data) {
|
|
69
|
+
throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
async readTextFile(path) {
|
|
74
|
+
const data = await this.readFile(path);
|
|
75
|
+
return new TextDecoder().decode(data);
|
|
76
|
+
}
|
|
77
|
+
async readDir(path) {
|
|
78
|
+
return this.listDirEntries(path).map((entry) => entry.name);
|
|
79
|
+
}
|
|
80
|
+
async readDirWithTypes(path) {
|
|
81
|
+
return this.listDirEntries(path);
|
|
82
|
+
}
|
|
83
|
+
async writeFile(path, content) {
|
|
84
|
+
const normalized = normalizePath(path);
|
|
85
|
+
await this.mkdir(dirname(normalized));
|
|
86
|
+
const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
87
|
+
this.files.set(normalized, data);
|
|
88
|
+
}
|
|
89
|
+
async createDir(path) {
|
|
90
|
+
const normalized = normalizePath(path);
|
|
91
|
+
const parent = dirname(normalized);
|
|
92
|
+
if (!this.dirs.has(parent)) {
|
|
93
|
+
throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
|
|
94
|
+
}
|
|
95
|
+
this.dirs.add(normalized);
|
|
96
|
+
}
|
|
97
|
+
async mkdir(path) {
|
|
98
|
+
const parts = splitPath(path);
|
|
99
|
+
let current = "";
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
current += `/${part}`;
|
|
102
|
+
if (!this.dirs.has(current)) {
|
|
103
|
+
this.dirs.add(current);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
resolveSymlink(normalized, maxDepth = 16) {
|
|
108
|
+
let current = normalized;
|
|
109
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
110
|
+
const target = this.symlinks.get(current);
|
|
111
|
+
if (!target)
|
|
112
|
+
return current;
|
|
113
|
+
current = target.startsWith("/") ? normalizePath(target) : normalizePath(`${dirname(current)}/${target}`);
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
|
|
116
|
+
}
|
|
117
|
+
statEntry(normalized) {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const ts = this.timestamps.get(normalized);
|
|
120
|
+
const owner = this.owners.get(normalized);
|
|
121
|
+
const customMode = this.modes.get(normalized);
|
|
122
|
+
const atimeMs = ts?.atimeMs ?? now;
|
|
123
|
+
const mtimeMs = ts?.mtimeMs ?? now;
|
|
124
|
+
const file = this.files.get(normalized);
|
|
125
|
+
if (file) {
|
|
126
|
+
return {
|
|
127
|
+
mode: customMode ?? (S_IFREG | 0o644),
|
|
128
|
+
size: file.byteLength,
|
|
129
|
+
isDirectory: false,
|
|
130
|
+
isSymbolicLink: false,
|
|
131
|
+
atimeMs,
|
|
132
|
+
mtimeMs,
|
|
133
|
+
ctimeMs: now,
|
|
134
|
+
birthtimeMs: now,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (this.dirs.has(normalized)) {
|
|
138
|
+
return {
|
|
139
|
+
mode: customMode ?? (S_IFDIR | 0o755),
|
|
140
|
+
size: 4096,
|
|
141
|
+
isDirectory: true,
|
|
142
|
+
isSymbolicLink: false,
|
|
143
|
+
atimeMs,
|
|
144
|
+
mtimeMs,
|
|
145
|
+
ctimeMs: now,
|
|
146
|
+
birthtimeMs: now,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
|
|
150
|
+
}
|
|
151
|
+
async exists(path) {
|
|
152
|
+
const normalized = normalizePath(path);
|
|
153
|
+
if (this.symlinks.has(normalized)) {
|
|
154
|
+
try {
|
|
155
|
+
this.resolveSymlink(normalized);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return this.files.has(normalized) || this.dirs.has(normalized);
|
|
163
|
+
}
|
|
164
|
+
async stat(path) {
|
|
165
|
+
const normalized = normalizePath(path);
|
|
166
|
+
const resolved = this.resolveSymlink(normalized);
|
|
167
|
+
return this.statEntry(resolved);
|
|
168
|
+
}
|
|
169
|
+
async removeFile(path) {
|
|
170
|
+
const normalized = normalizePath(path);
|
|
171
|
+
if (!this.files.delete(normalized)) {
|
|
172
|
+
throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async removeDir(path) {
|
|
176
|
+
const normalized = normalizePath(path);
|
|
177
|
+
if (normalized === "/") {
|
|
178
|
+
throw new Error("EPERM: operation not permitted, rmdir '/'");
|
|
179
|
+
}
|
|
180
|
+
if (!this.dirs.has(normalized)) {
|
|
181
|
+
throw new Error(`ENOENT: no such file or directory, rmdir '${normalized}'`);
|
|
182
|
+
}
|
|
183
|
+
const prefix = normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
184
|
+
for (const filePath of this.files.keys()) {
|
|
185
|
+
if (filePath.startsWith(prefix)) {
|
|
186
|
+
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const dirPath of this.dirs.values()) {
|
|
190
|
+
if (dirPath !== normalized && dirPath.startsWith(prefix)) {
|
|
191
|
+
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.dirs.delete(normalized);
|
|
195
|
+
}
|
|
196
|
+
async rename(oldPath, newPath) {
|
|
197
|
+
const oldNormalized = normalizePath(oldPath);
|
|
198
|
+
const newNormalized = normalizePath(newPath);
|
|
199
|
+
if (oldNormalized === newNormalized) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!this.dirs.has(dirname(newNormalized))) {
|
|
203
|
+
throw new Error(`ENOENT: no such file or directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
204
|
+
}
|
|
205
|
+
if (this.files.has(oldNormalized)) {
|
|
206
|
+
if (this.dirs.has(newNormalized)) {
|
|
207
|
+
throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
208
|
+
}
|
|
209
|
+
const content = this.files.get(oldNormalized);
|
|
210
|
+
this.files.set(newNormalized, content);
|
|
211
|
+
this.files.delete(oldNormalized);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!this.dirs.has(oldNormalized)) {
|
|
215
|
+
throw new Error(`ENOENT: no such file or directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
216
|
+
}
|
|
217
|
+
if (oldNormalized === "/") {
|
|
218
|
+
throw new Error(`EPERM: operation not permitted, rename '${oldNormalized}'`);
|
|
219
|
+
}
|
|
220
|
+
if (newNormalized.startsWith(`${oldNormalized}/`)) {
|
|
221
|
+
throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
222
|
+
}
|
|
223
|
+
if (this.dirs.has(newNormalized) || this.files.has(newNormalized)) {
|
|
224
|
+
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
225
|
+
}
|
|
226
|
+
const sourcePrefix = `${oldNormalized}/`;
|
|
227
|
+
const targetPrefix = `${newNormalized}/`;
|
|
228
|
+
const dirPaths = Array.from(this.dirs.values())
|
|
229
|
+
.filter((path) => path === oldNormalized || path.startsWith(sourcePrefix))
|
|
230
|
+
.sort((a, b) => a.length - b.length);
|
|
231
|
+
const filePaths = Array.from(this.files.keys()).filter((path) => path.startsWith(sourcePrefix));
|
|
232
|
+
for (const path of dirPaths) {
|
|
233
|
+
this.dirs.delete(path);
|
|
234
|
+
}
|
|
235
|
+
for (const path of filePaths) {
|
|
236
|
+
const content = this.files.get(path);
|
|
237
|
+
this.files.delete(path);
|
|
238
|
+
this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, content);
|
|
239
|
+
}
|
|
240
|
+
this.dirs.add(newNormalized);
|
|
241
|
+
for (const path of dirPaths) {
|
|
242
|
+
if (path === oldNormalized) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
this.dirs.add(`${targetPrefix}${path.slice(sourcePrefix.length)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async symlink(target, linkPath) {
|
|
249
|
+
const normalized = normalizePath(linkPath);
|
|
250
|
+
if (this.files.has(normalized) || this.dirs.has(normalized) || this.symlinks.has(normalized)) {
|
|
251
|
+
throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
|
|
252
|
+
}
|
|
253
|
+
await this.mkdir(dirname(normalized));
|
|
254
|
+
this.symlinks.set(normalized, target);
|
|
255
|
+
}
|
|
256
|
+
async readlink(path) {
|
|
257
|
+
const normalized = normalizePath(path);
|
|
258
|
+
const target = this.symlinks.get(normalized);
|
|
259
|
+
if (target === undefined) {
|
|
260
|
+
throw new Error(`EINVAL: invalid argument, readlink '${normalized}'`);
|
|
261
|
+
}
|
|
262
|
+
return target;
|
|
263
|
+
}
|
|
264
|
+
async lstat(path) {
|
|
265
|
+
const normalized = normalizePath(path);
|
|
266
|
+
const target = this.symlinks.get(normalized);
|
|
267
|
+
if (target !== undefined) {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
return {
|
|
270
|
+
mode: S_IFLNK | 0o777,
|
|
271
|
+
size: new TextEncoder().encode(target).byteLength,
|
|
272
|
+
isDirectory: false,
|
|
273
|
+
isSymbolicLink: true,
|
|
274
|
+
atimeMs: now,
|
|
275
|
+
mtimeMs: now,
|
|
276
|
+
ctimeMs: now,
|
|
277
|
+
birthtimeMs: now,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return this.statEntry(normalized);
|
|
281
|
+
}
|
|
282
|
+
async link(oldPath, newPath) {
|
|
283
|
+
const oldNormalized = normalizePath(oldPath);
|
|
284
|
+
const newNormalized = normalizePath(newPath);
|
|
285
|
+
const file = this.files.get(oldNormalized);
|
|
286
|
+
if (!file) {
|
|
287
|
+
throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
288
|
+
}
|
|
289
|
+
if (this.files.has(newNormalized) || this.dirs.has(newNormalized)) {
|
|
290
|
+
throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
291
|
+
}
|
|
292
|
+
await this.mkdir(dirname(newNormalized));
|
|
293
|
+
this.files.set(newNormalized, file);
|
|
294
|
+
this.hardLinks.set(newNormalized, oldNormalized);
|
|
295
|
+
}
|
|
296
|
+
async chmod(path, mode) {
|
|
297
|
+
const normalized = normalizePath(path);
|
|
298
|
+
const resolved = this.resolveSymlink(normalized);
|
|
299
|
+
if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
|
|
300
|
+
throw new Error(`ENOENT: no such file or directory, chmod '${normalized}'`);
|
|
301
|
+
}
|
|
302
|
+
const existing = this.modes.get(resolved);
|
|
303
|
+
const typeBits = existing ? (existing & 0o170000) : (this.files.has(resolved) ? S_IFREG : S_IFDIR);
|
|
304
|
+
this.modes.set(resolved, typeBits | (mode & 0o7777));
|
|
305
|
+
}
|
|
306
|
+
async chown(path, uid, gid) {
|
|
307
|
+
const normalized = normalizePath(path);
|
|
308
|
+
const resolved = this.resolveSymlink(normalized);
|
|
309
|
+
if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
|
|
310
|
+
throw new Error(`ENOENT: no such file or directory, chown '${normalized}'`);
|
|
311
|
+
}
|
|
312
|
+
this.owners.set(resolved, { uid, gid });
|
|
313
|
+
}
|
|
314
|
+
async utimes(path, atime, mtime) {
|
|
315
|
+
const normalized = normalizePath(path);
|
|
316
|
+
const resolved = this.resolveSymlink(normalized);
|
|
317
|
+
if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
|
|
318
|
+
throw new Error(`ENOENT: no such file or directory, utimes '${normalized}'`);
|
|
319
|
+
}
|
|
320
|
+
this.timestamps.set(resolved, { atimeMs: atime * 1000, mtimeMs: mtime * 1000 });
|
|
321
|
+
}
|
|
322
|
+
async truncate(path, length) {
|
|
323
|
+
const normalized = normalizePath(path);
|
|
324
|
+
const resolved = this.resolveSymlink(normalized);
|
|
325
|
+
const file = this.files.get(resolved);
|
|
326
|
+
if (!file) {
|
|
327
|
+
throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
|
|
328
|
+
}
|
|
329
|
+
if (length >= file.byteLength) {
|
|
330
|
+
const padded = new Uint8Array(length);
|
|
331
|
+
padded.set(file);
|
|
332
|
+
this.files.set(resolved, padded);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
this.files.set(resolved, file.slice(0, length));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
export function createInMemoryFileSystem() {
|
|
340
|
+
return new InMemoryFileSystem();
|
|
341
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission enforcement layer.
|
|
3
|
+
*
|
|
4
|
+
* Wraps filesystem, network, and command-executor adapters with permission
|
|
5
|
+
* checks that throw EACCES on denial. When no permission callback is provided
|
|
6
|
+
* for a category, guarded operations in that category are denied by default.
|
|
7
|
+
*/
|
|
8
|
+
import type { CommandExecutor, EnvAccessRequest, NetworkAdapter, Permissions, VirtualFileSystem } from "../types.js";
|
|
9
|
+
export declare const allowAllFs: Pick<Permissions, "fs">;
|
|
10
|
+
export declare const allowAllNetwork: Pick<Permissions, "network">;
|
|
11
|
+
export declare const allowAllChildProcess: Pick<Permissions, "childProcess">;
|
|
12
|
+
export declare const allowAllEnv: Pick<Permissions, "env">;
|
|
13
|
+
export declare const allowAll: Permissions;
|
|
14
|
+
/**
|
|
15
|
+
* Wrap a VirtualFileSystem so every operation passes through the fs permission check.
|
|
16
|
+
* Throws EACCES if the permission callback denies or is absent.
|
|
17
|
+
*/
|
|
18
|
+
export declare function wrapFileSystem(fs: VirtualFileSystem, permissions?: Permissions): VirtualFileSystem;
|
|
19
|
+
/**
|
|
20
|
+
* Wrap a NetworkAdapter so externally-originating operations (`listen`, `fetch`,
|
|
21
|
+
* `dns`, `http`) pass through the network permission check.
|
|
22
|
+
* `httpServerClose` is forwarded as-is.
|
|
23
|
+
*/
|
|
24
|
+
export declare function wrapNetworkAdapter(adapter: NetworkAdapter, permissions?: Permissions): NetworkAdapter;
|
|
25
|
+
/** Wrap a CommandExecutor so spawn passes through the childProcess permission check. */
|
|
26
|
+
export declare function wrapCommandExecutor(executor: CommandExecutor, permissions?: Permissions): CommandExecutor;
|
|
27
|
+
export declare function envAccessAllowed(permissions: Permissions | undefined, request: EnvAccessRequest): void;
|
|
28
|
+
/** Create a stub VFS where every operation throws ENOSYS (no filesystem configured). */
|
|
29
|
+
export declare function createFsStub(): VirtualFileSystem;
|
|
30
|
+
/** Create a stub network adapter where every operation throws ENOSYS. */
|
|
31
|
+
export declare function createNetworkStub(): NetworkAdapter;
|
|
32
|
+
/** Create a stub executor where spawn throws ENOSYS. */
|
|
33
|
+
export declare function createCommandExecutorStub(): CommandExecutor;
|
|
34
|
+
/**
|
|
35
|
+
* Filter an env record through the env permission check, returning only
|
|
36
|
+
* allowed key-value pairs. Returns empty object if no permissions configured.
|
|
37
|
+
*/
|
|
38
|
+
export declare function filterEnv(env: Record<string, string> | undefined, permissions?: Permissions): Record<string, string>;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission enforcement layer.
|
|
3
|
+
*
|
|
4
|
+
* Wraps filesystem, network, and command-executor adapters with permission
|
|
5
|
+
* checks that throw EACCES on denial. When no permission callback is provided
|
|
6
|
+
* for a category, guarded operations in that category are denied by default.
|
|
7
|
+
*/
|
|
8
|
+
import { createEaccesError, createEnosysError } from "./errors.js";
|
|
9
|
+
/** Run the permission check; throw the deny error if no checker exists or it denies. */
|
|
10
|
+
function checkPermission(check, request, onDenied) {
|
|
11
|
+
if (!check) {
|
|
12
|
+
throw onDenied(request);
|
|
13
|
+
}
|
|
14
|
+
const decision = check(request);
|
|
15
|
+
if (!decision?.allow) {
|
|
16
|
+
throw onDenied(request, decision?.reason);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Permission callbacks must be self-contained (no closures) because they are
|
|
20
|
+
// serialized via `.toString()` for transfer to the browser Web Worker.
|
|
21
|
+
export const allowAllFs = {
|
|
22
|
+
fs: () => ({ allow: true }),
|
|
23
|
+
};
|
|
24
|
+
export const allowAllNetwork = {
|
|
25
|
+
network: () => ({ allow: true }),
|
|
26
|
+
};
|
|
27
|
+
export const allowAllChildProcess = {
|
|
28
|
+
childProcess: () => ({ allow: true }),
|
|
29
|
+
};
|
|
30
|
+
export const allowAllEnv = {
|
|
31
|
+
env: () => ({ allow: true }),
|
|
32
|
+
};
|
|
33
|
+
export const allowAll = {
|
|
34
|
+
...allowAllFs,
|
|
35
|
+
...allowAllNetwork,
|
|
36
|
+
...allowAllChildProcess,
|
|
37
|
+
...allowAllEnv,
|
|
38
|
+
};
|
|
39
|
+
function fsOpToSyscall(op) {
|
|
40
|
+
switch (op) {
|
|
41
|
+
case "read":
|
|
42
|
+
return "open";
|
|
43
|
+
case "write":
|
|
44
|
+
return "write";
|
|
45
|
+
case "mkdir":
|
|
46
|
+
case "createDir":
|
|
47
|
+
return "mkdir";
|
|
48
|
+
case "readdir":
|
|
49
|
+
return "scandir";
|
|
50
|
+
case "stat":
|
|
51
|
+
return "stat";
|
|
52
|
+
case "rm":
|
|
53
|
+
return "unlink";
|
|
54
|
+
case "rename":
|
|
55
|
+
return "rename";
|
|
56
|
+
case "exists":
|
|
57
|
+
return "access";
|
|
58
|
+
case "chmod":
|
|
59
|
+
return "chmod";
|
|
60
|
+
case "chown":
|
|
61
|
+
return "chown";
|
|
62
|
+
case "link":
|
|
63
|
+
return "link";
|
|
64
|
+
case "symlink":
|
|
65
|
+
return "symlink";
|
|
66
|
+
case "readlink":
|
|
67
|
+
return "readlink";
|
|
68
|
+
case "truncate":
|
|
69
|
+
return "open";
|
|
70
|
+
case "utimes":
|
|
71
|
+
return "utimes";
|
|
72
|
+
default:
|
|
73
|
+
return "open";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Wrap a VirtualFileSystem so every operation passes through the fs permission check.
|
|
78
|
+
* Throws EACCES if the permission callback denies or is absent.
|
|
79
|
+
*/
|
|
80
|
+
export function wrapFileSystem(fs, permissions) {
|
|
81
|
+
return {
|
|
82
|
+
readFile: async (path) => {
|
|
83
|
+
checkPermission(permissions?.fs, { op: "read", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
84
|
+
return fs.readFile(path);
|
|
85
|
+
},
|
|
86
|
+
readTextFile: async (path) => {
|
|
87
|
+
checkPermission(permissions?.fs, { op: "read", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
88
|
+
return fs.readTextFile(path);
|
|
89
|
+
},
|
|
90
|
+
readDir: async (path) => {
|
|
91
|
+
checkPermission(permissions?.fs, { op: "readdir", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
92
|
+
return fs.readDir(path);
|
|
93
|
+
},
|
|
94
|
+
readDirWithTypes: async (path) => {
|
|
95
|
+
checkPermission(permissions?.fs, { op: "readdir", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
96
|
+
return fs.readDirWithTypes(path);
|
|
97
|
+
},
|
|
98
|
+
writeFile: async (path, content) => {
|
|
99
|
+
checkPermission(permissions?.fs, { op: "write", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
100
|
+
return fs.writeFile(path, content);
|
|
101
|
+
},
|
|
102
|
+
createDir: async (path) => {
|
|
103
|
+
checkPermission(permissions?.fs, { op: "createDir", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
104
|
+
return fs.createDir(path);
|
|
105
|
+
},
|
|
106
|
+
mkdir: async (path) => {
|
|
107
|
+
checkPermission(permissions?.fs, { op: "mkdir", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
108
|
+
return fs.mkdir(path);
|
|
109
|
+
},
|
|
110
|
+
exists: async (path) => {
|
|
111
|
+
checkPermission(permissions?.fs, { op: "exists", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
112
|
+
return fs.exists(path);
|
|
113
|
+
},
|
|
114
|
+
stat: async (path) => {
|
|
115
|
+
checkPermission(permissions?.fs, { op: "stat", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
116
|
+
return fs.stat(path);
|
|
117
|
+
},
|
|
118
|
+
removeFile: async (path) => {
|
|
119
|
+
checkPermission(permissions?.fs, { op: "rm", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
120
|
+
return fs.removeFile(path);
|
|
121
|
+
},
|
|
122
|
+
removeDir: async (path) => {
|
|
123
|
+
checkPermission(permissions?.fs, { op: "rm", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
124
|
+
return fs.removeDir(path);
|
|
125
|
+
},
|
|
126
|
+
rename: async (oldPath, newPath) => {
|
|
127
|
+
checkPermission(permissions?.fs, { op: "rename", path: oldPath }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
128
|
+
checkPermission(permissions?.fs, { op: "rename", path: newPath }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
129
|
+
return fs.rename(oldPath, newPath);
|
|
130
|
+
},
|
|
131
|
+
symlink: async (target, linkPath) => {
|
|
132
|
+
checkPermission(permissions?.fs, { op: "symlink", path: linkPath }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
133
|
+
return fs.symlink(target, linkPath);
|
|
134
|
+
},
|
|
135
|
+
readlink: async (path) => {
|
|
136
|
+
checkPermission(permissions?.fs, { op: "readlink", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
137
|
+
return fs.readlink(path);
|
|
138
|
+
},
|
|
139
|
+
lstat: async (path) => {
|
|
140
|
+
checkPermission(permissions?.fs, { op: "stat", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
141
|
+
return fs.lstat(path);
|
|
142
|
+
},
|
|
143
|
+
link: async (oldPath, newPath) => {
|
|
144
|
+
checkPermission(permissions?.fs, { op: "link", path: newPath }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
145
|
+
return fs.link(oldPath, newPath);
|
|
146
|
+
},
|
|
147
|
+
chmod: async (path, mode) => {
|
|
148
|
+
checkPermission(permissions?.fs, { op: "chmod", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
149
|
+
return fs.chmod(path, mode);
|
|
150
|
+
},
|
|
151
|
+
chown: async (path, uid, gid) => {
|
|
152
|
+
checkPermission(permissions?.fs, { op: "chown", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
153
|
+
return fs.chown(path, uid, gid);
|
|
154
|
+
},
|
|
155
|
+
utimes: async (path, atime, mtime) => {
|
|
156
|
+
checkPermission(permissions?.fs, { op: "utimes", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
157
|
+
return fs.utimes(path, atime, mtime);
|
|
158
|
+
},
|
|
159
|
+
truncate: async (path, length) => {
|
|
160
|
+
checkPermission(permissions?.fs, { op: "truncate", path }, (req, reason) => createEaccesError(fsOpToSyscall(req.op), req.path, reason));
|
|
161
|
+
return fs.truncate(path, length);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Wrap a NetworkAdapter so externally-originating operations (`listen`, `fetch`,
|
|
167
|
+
* `dns`, `http`) pass through the network permission check.
|
|
168
|
+
* `httpServerClose` is forwarded as-is.
|
|
169
|
+
*/
|
|
170
|
+
export function wrapNetworkAdapter(adapter, permissions) {
|
|
171
|
+
return {
|
|
172
|
+
httpServerListen: adapter.httpServerListen
|
|
173
|
+
? async (options) => {
|
|
174
|
+
checkPermission(permissions?.network, {
|
|
175
|
+
op: "listen",
|
|
176
|
+
hostname: options.hostname,
|
|
177
|
+
url: options.hostname
|
|
178
|
+
? `http://${options.hostname}:${options.port ?? 3000}`
|
|
179
|
+
: `http://0.0.0.0:${options.port ?? 3000}`,
|
|
180
|
+
method: "LISTEN",
|
|
181
|
+
}, (req, reason) => createEaccesError("listen", req.url, reason));
|
|
182
|
+
return adapter.httpServerListen(options);
|
|
183
|
+
}
|
|
184
|
+
: undefined,
|
|
185
|
+
httpServerClose: adapter.httpServerClose
|
|
186
|
+
? async (serverId) => {
|
|
187
|
+
return adapter.httpServerClose(serverId);
|
|
188
|
+
}
|
|
189
|
+
: undefined,
|
|
190
|
+
fetch: async (url, options) => {
|
|
191
|
+
checkPermission(permissions?.network, { op: "fetch", url, method: options?.method }, (req, reason) => createEaccesError("connect", req.url, reason));
|
|
192
|
+
return adapter.fetch(url, options);
|
|
193
|
+
},
|
|
194
|
+
dnsLookup: async (hostname) => {
|
|
195
|
+
checkPermission(permissions?.network, { op: "dns", hostname }, (req, reason) => createEaccesError("connect", req.hostname, reason));
|
|
196
|
+
return adapter.dnsLookup(hostname);
|
|
197
|
+
},
|
|
198
|
+
httpRequest: async (url, options) => {
|
|
199
|
+
checkPermission(permissions?.network, { op: "http", url, method: options?.method }, (req, reason) => createEaccesError("connect", req.url, reason));
|
|
200
|
+
return adapter.httpRequest(url, options);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** Wrap a CommandExecutor so spawn passes through the childProcess permission check. */
|
|
205
|
+
export function wrapCommandExecutor(executor, permissions) {
|
|
206
|
+
return {
|
|
207
|
+
spawn: (command, args, options) => {
|
|
208
|
+
checkPermission(permissions?.childProcess, { command, args, cwd: options.cwd, env: options.env }, (req, reason) => createEaccesError("spawn", req.command, reason));
|
|
209
|
+
return executor.spawn(command, args, options);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function envAccessAllowed(permissions, request) {
|
|
214
|
+
checkPermission(permissions?.env, request, (req, reason) => createEaccesError("access", req.key, reason));
|
|
215
|
+
}
|
|
216
|
+
/** Create a stub VFS where every operation throws ENOSYS (no filesystem configured). */
|
|
217
|
+
export function createFsStub() {
|
|
218
|
+
const stub = (op, path) => {
|
|
219
|
+
throw createEnosysError(op, path);
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
readFile: async (path) => stub("open", path),
|
|
223
|
+
readTextFile: async (path) => stub("open", path),
|
|
224
|
+
readDir: async (path) => stub("scandir", path),
|
|
225
|
+
readDirWithTypes: async (path) => stub("scandir", path),
|
|
226
|
+
writeFile: async (path) => stub("write", path),
|
|
227
|
+
createDir: async (path) => stub("mkdir", path),
|
|
228
|
+
mkdir: async (path) => stub("mkdir", path),
|
|
229
|
+
exists: async (path) => stub("access", path),
|
|
230
|
+
stat: async (path) => stub("stat", path),
|
|
231
|
+
removeFile: async (path) => stub("unlink", path),
|
|
232
|
+
removeDir: async (path) => stub("rmdir", path),
|
|
233
|
+
rename: async (oldPath, newPath) => stub("rename", `${oldPath}->${newPath}`),
|
|
234
|
+
symlink: async (_target, linkPath) => stub("symlink", linkPath),
|
|
235
|
+
readlink: async (path) => stub("readlink", path),
|
|
236
|
+
lstat: async (path) => stub("stat", path),
|
|
237
|
+
link: async (_oldPath, newPath) => stub("link", newPath),
|
|
238
|
+
chmod: async (path) => stub("chmod", path),
|
|
239
|
+
chown: async (path) => stub("chown", path),
|
|
240
|
+
utimes: async (path) => stub("utimes", path),
|
|
241
|
+
truncate: async (path) => stub("open", path),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Create a stub network adapter where every operation throws ENOSYS. */
|
|
245
|
+
export function createNetworkStub() {
|
|
246
|
+
const stub = (op, path) => {
|
|
247
|
+
throw createEnosysError(op, path);
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
httpServerListen: async () => stub("listen"),
|
|
251
|
+
httpServerClose: async () => stub("close"),
|
|
252
|
+
fetch: async (url) => stub("connect", url),
|
|
253
|
+
dnsLookup: async (hostname) => stub("connect", hostname),
|
|
254
|
+
httpRequest: async (url) => stub("connect", url),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/** Create a stub executor where spawn throws ENOSYS. */
|
|
258
|
+
export function createCommandExecutorStub() {
|
|
259
|
+
return {
|
|
260
|
+
spawn: () => {
|
|
261
|
+
throw createEnosysError("spawn");
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Filter an env record through the env permission check, returning only
|
|
267
|
+
* allowed key-value pairs. Returns empty object if no permissions configured.
|
|
268
|
+
*/
|
|
269
|
+
export function filterEnv(env, permissions) {
|
|
270
|
+
if (!env)
|
|
271
|
+
return {};
|
|
272
|
+
if (!permissions?.env)
|
|
273
|
+
return {};
|
|
274
|
+
const result = {};
|
|
275
|
+
for (const [key, value] of Object.entries(env)) {
|
|
276
|
+
const request = { op: "read", key, value };
|
|
277
|
+
const decision = permissions.env(request);
|
|
278
|
+
if (decision?.allow) {
|
|
279
|
+
result[key] = value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the isolate-side script that installs the global `require()` function,
|
|
3
|
+
* `_requireFrom()`, and require helpers (for example `require.resolve` and
|
|
4
|
+
* `require.cache` wiring to the pre-initialized `_moduleCache`).
|
|
5
|
+
*/
|
|
6
|
+
export declare function getRequireSetupCode(): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getIsolateRuntimeSource } from "../generated/isolate-runtime.js";
|
|
2
|
+
/**
|
|
3
|
+
* Get the isolate-side script that installs the global `require()` function,
|
|
4
|
+
* `_requireFrom()`, and require helpers (for example `require.resolve` and
|
|
5
|
+
* `require.cache` wiring to the pre-initialized `_moduleCache`).
|
|
6
|
+
*/
|
|
7
|
+
export function getRequireSetupCode() {
|
|
8
|
+
return getIsolateRuntimeSource("requireSetup");
|
|
9
|
+
}
|