sandboxedjs 0.1.48 → 0.1.49

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,427 @@
1
+ /** The handle a pod's process manager returns. */
2
+ interface ChildHandle {
3
+ pid: number;
4
+ state: "starting" | "running" | "exited";
5
+ exitCode: number | undefined;
6
+ on(event: "stdout" | "stderr" | "exit" | "rawmode", listener: (...args: any[]) => void): unknown;
7
+ exec(): void;
8
+ sendStdin(data: string): void;
9
+ /**
10
+ * Close the child's input.
11
+ *
12
+ * Without this a child that reads stdin to EOF — every filter, and every
13
+ * tool a library pipes into, `xsel` and `base64` alike — waits forever for
14
+ * an end that never comes, and the parent waits on its exit.
15
+ */
16
+ endStdin?(): void;
17
+ kill(signal?: string): void;
18
+ }
19
+ interface ChildSpawnConfig {
20
+ command: string;
21
+ args?: string[];
22
+ cwd?: string;
23
+ env?: Record<string, string>;
24
+ parentPid?: number;
25
+ /**
26
+ * The child was given the parent's streams (`stdio: "inherit"`).
27
+ *
28
+ * Its input is then the parent's terminal rather than a pipe that will end,
29
+ * which is the difference between a program that waits for what the user
30
+ * types and one that reads to end-of-input and stops.
31
+ */
32
+ inheritStdio?: boolean;
33
+ /**
34
+ * The child was told to ignore its input (`stdio: "ignore"`).
35
+ *
36
+ * It then has no input at all, so its stdin is closed at once rather than
37
+ * left open on a parent that will never write to it.
38
+ */
39
+ stdinIgnored?: boolean;
40
+ }
41
+ type SpawnChild = (config: ChildSpawnConfig) => ChildHandle;
42
+ /**
43
+ * Run a child to completion without returning to the event loop.
44
+ *
45
+ * Supplied only by a pod that can actually block — one whose guest runs on its
46
+ * own thread. Where it is absent the synchronous entry points keep reporting
47
+ * that they are unavailable, which is the honest answer for an in-realm pod.
48
+ */
49
+ type SyncSpawn = (request: {
50
+ command: string;
51
+ args: string[];
52
+ cwd: string;
53
+ env?: Record<string, string>;
54
+ input?: string;
55
+ inheritStdio?: boolean;
56
+ }) => {
57
+ status: number | null;
58
+ stdout: string;
59
+ stderr: string;
60
+ signal: string | null;
61
+ error?: {
62
+ code?: string;
63
+ message: string;
64
+ };
65
+ };
66
+ declare function createChildProcessModule(spawnChild: SpawnChild, defaultCwd: () => string, syncSpawn?: SyncSpawn, defaultEnv?: () => Record<string, string>): Record<string, unknown>;
67
+
68
+ /**
69
+ * Clean-room contracts between SandboxedJS and its JavaScript runtime.
70
+ *
71
+ * These deliberately describe only behavior SandboxedJS consumes. Runtime
72
+ * implementations may use Web Workers in browsers or worker_threads on Node.
73
+ */
74
+
75
+ interface VolumeStats {
76
+ totalBytes: number;
77
+ fileCount: number;
78
+ /** New runtime spelling. */
79
+ directoryCount?: number;
80
+ /** Compatibility spelling used by existing volume implementations. */
81
+ dirCount?: number;
82
+ }
83
+ interface VolumeStat {
84
+ mode: number;
85
+ size: number;
86
+ uid: number;
87
+ gid: number;
88
+ ino: number;
89
+ nlink: number;
90
+ atimeMs: number;
91
+ mtimeMs: number;
92
+ ctimeMs: number;
93
+ birthtimeMs: number;
94
+ isFile(): boolean;
95
+ isDirectory(): boolean;
96
+ isSymbolicLink(): boolean;
97
+ }
98
+ interface RuntimeVolume {
99
+ readFileSync(path: string): Uint8Array;
100
+ writeFileSync(path: string, data: string | Uint8Array): void;
101
+ appendFileSync(path: string, data: string | Uint8Array): void;
102
+ readdirSync(path: string): string[];
103
+ lstatSync(path: string): VolumeStat;
104
+ readlinkSync(path: string): string;
105
+ mkdirSync(path: string, options?: {
106
+ mode?: number;
107
+ }): void;
108
+ rmdirSync(path: string): void;
109
+ unlinkSync(path: string): void;
110
+ renameSync(from: string, to: string): void;
111
+ symlinkSync(target: string, path: string): void;
112
+ linkSync(existing: string, path: string): void;
113
+ truncateSync(path: string, length?: number): void;
114
+ chmodSync(path: string, mode: number): void;
115
+ lchmodSync(path: string, mode: number): void;
116
+ chownSync(path: string, uid: number, gid: number): void;
117
+ lchownSync(path: string, uid: number, gid: number): void;
118
+ utimesSync(path: string, atime: Date, mtime: Date): void;
119
+ getStats(): VolumeStats;
120
+ }
121
+ interface RuntimeProcessResult {
122
+ exitCode: number;
123
+ stdout: string;
124
+ stderr: string;
125
+ }
126
+ interface RuntimeProcess {
127
+ readonly completion: Promise<RuntimeProcessResult>;
128
+ /**
129
+ * `output` and `error` carry stdout and stderr; `exit` the code. `rawmode`
130
+ * reports the program turning terminal raw mode on or off, which a terminal
131
+ * needs so that it stops echoing input the program is drawing itself.
132
+ */
133
+ on(event: "output" | "error" | "exit" | "rawmode", listener: (...args: any[]) => void): this;
134
+ write(data: string): void;
135
+ kill(signal?: string): void;
136
+ }
137
+ interface RuntimeHttpResponse {
138
+ statusCode?: number;
139
+ statusMessage?: string;
140
+ headers?: Record<string, string>;
141
+ body?: string | Uint8Array | ArrayBuffer;
142
+ }
143
+ interface RuntimePackageInstaller {
144
+ install(name: string, version?: string, options?: Record<string, unknown>): Promise<unknown>;
145
+ installFromManifest(path: string, options?: Record<string, unknown>): Promise<unknown>;
146
+ /** Create a view that installs into another project root. */
147
+ forCwd?(cwd: string): RuntimePackageInstaller;
148
+ }
149
+ /** Where bytes written by an upgraded server inside the container come out. */
150
+ interface RuntimeSocketPeer {
151
+ data(bytes: Uint8Array): void;
152
+ close(): void;
153
+ }
154
+ /** The caller's end of a connection opened with {@link RuntimePod.connect}. */
155
+ interface RuntimeConnection {
156
+ send(bytes: Uint8Array): void;
157
+ close(): void;
158
+ }
159
+ /** The process-manager protocol a container's kernel bridge substitutes for. */
160
+ interface RuntimeProcessManager {
161
+ spawn(config: ChildSpawnConfig): ChildHandle;
162
+ }
163
+ interface RuntimePod {
164
+ readonly volume: RuntimeVolume;
165
+ readonly packages: RuntimePackageInstaller;
166
+ readonly instanceId: string;
167
+ readonly processManager: RuntimeProcessManager;
168
+ readonly proxy: {
169
+ activePorts(instanceId?: string): number[];
170
+ };
171
+ spawn(command: string, args?: string[], options?: Record<string, unknown>): Promise<RuntimeProcess>;
172
+ request(port: number, init?: Record<string, unknown>): Promise<RuntimeHttpResponse>;
173
+ /**
174
+ * Open a connection that upgrades out of HTTP, or null if nothing takes one.
175
+ *
176
+ * Optional because a pod that only ever answers requests is still a usable
177
+ * pod — a caller treats the absence as "no WebSocket here" rather than as a
178
+ * broken implementation.
179
+ */
180
+ connect?(port: number, init: Record<string, unknown>, peer: RuntimeSocketPeer): RuntimeConnection | null;
181
+ /**
182
+ * Bind a port to a server implemented outside the JavaScript runtime.
183
+ *
184
+ * The pod owns the port table, and a Python process cannot register with it
185
+ * the way a Node server does — it has no `http.createServer`. This is the
186
+ * seam that lets one exist without a second, divergent port table, so
187
+ * `box.request()`, the preview router and `ss` all see the same listeners.
188
+ *
189
+ * Returns the function that unbinds it. Optional: a pod that cannot host
190
+ * foreign servers simply does not offer one.
191
+ */
192
+ serveExternal?(port: number, owner: string, handler: (request: {
193
+ method: string;
194
+ path: string;
195
+ headers: Record<string, string>;
196
+ body: Uint8Array;
197
+ }) => Promise<RuntimeHttpResponse>): () => void;
198
+ snapshot(options?: Record<string, unknown>): unknown;
199
+ restore(snapshot: unknown, options?: Record<string, unknown>): Promise<void>;
200
+ teardown(): void;
201
+ }
202
+
203
+ type FileKind = "file" | "directory" | "symlink" | "chardev" | "blockdev" | "fifo" | "socket";
204
+ /** Render as `drwxr-xr-x`, honouring setuid/setgid/sticky. */
205
+ declare function formatMode(mode: number): string;
206
+ /** Zero-padded octal permissions, as `stat -c %a`/`%04a` would show. */
207
+ declare function octalMode(mode: number, width?: number): string;
208
+ /**
209
+ * Apply a `chmod` spec to an existing mode. Accepts octal (`755`, `0644`) and
210
+ * the symbolic grammar (`u+rwx,go-w`, `a=r`, `+X`, `u+s`, `o+t`).
211
+ *
212
+ * @param isDir whether the target is a directory — needed for the `X` flag.
213
+ */
214
+ declare function applyChmod(spec: string, current: number, isDir: boolean, umask?: number): number;
215
+ /** Parse the `umask` builtin's argument. */
216
+ declare function parseUmask(spec: string): number;
217
+
218
+ /**
219
+ * The `Stats` object handed back by `Vfs.stat`. Shaped like `fs.Stats` so it
220
+ * feels familiar, but with a real `st_mode` that carries the file-type bits
221
+ * (which the underlying volume stores separately).
222
+ */
223
+
224
+ interface StatInit {
225
+ mode: number;
226
+ size: number;
227
+ uid: number;
228
+ gid: number;
229
+ ino: number;
230
+ nlink: number;
231
+ atimeMs: number;
232
+ mtimeMs: number;
233
+ ctimeMs: number;
234
+ birthtimeMs?: number;
235
+ dev?: number;
236
+ rdev?: number;
237
+ blksize?: number;
238
+ }
239
+ declare class Stats {
240
+ readonly mode: number;
241
+ readonly size: number;
242
+ readonly uid: number;
243
+ readonly gid: number;
244
+ readonly ino: number;
245
+ readonly nlink: number;
246
+ readonly dev: number;
247
+ readonly rdev: number;
248
+ readonly blksize: number;
249
+ readonly atimeMs: number;
250
+ readonly mtimeMs: number;
251
+ readonly ctimeMs: number;
252
+ readonly birthtimeMs: number;
253
+ constructor(init: StatInit);
254
+ get blocks(): number;
255
+ get atime(): Date;
256
+ get mtime(): Date;
257
+ get ctime(): Date;
258
+ get birthtime(): Date;
259
+ get kind(): FileKind;
260
+ isFile(): boolean;
261
+ isDirectory(): boolean;
262
+ isSymbolicLink(): boolean;
263
+ isCharacterDevice(): boolean;
264
+ isBlockDevice(): boolean;
265
+ isFIFO(): boolean;
266
+ isSocket(): boolean;
267
+ /** Permission bits only, with the type bits masked off. */
268
+ get perms(): number;
269
+ }
270
+ interface DirEntry {
271
+ name: string;
272
+ kind: FileKind;
273
+ }
274
+
275
+ /**
276
+ * The container's virtual filesystem.
277
+ *
278
+ * Real file content lives in the RuntimePod's `MemoryVolume`, deliberately the
279
+ * *same* volume the Node.js worker processes see — so a file written by `echo`
280
+ * is readable by `require('fs')` inside a spawned script, and vice versa.
281
+ *
282
+ * On top of that volume this layer adds the parts a Linux userland expects and
283
+ * the raw volume does not have: file-type bits in `st_mode`, permission and
284
+ * ownership checks, an `O_*` open/fd table, and pluggable *virtual providers*
285
+ * that synthesise `/proc`, `/sys` and `/dev` on demand.
286
+ */
287
+
288
+ /** The identity a filesystem operation runs as. */
289
+ interface Cred {
290
+ uid: number;
291
+ gid: number;
292
+ groups: number[];
293
+ umask: number;
294
+ }
295
+ declare const ROOT_CRED: Cred;
296
+ declare function makeCred(uid: number, gid: number, groups?: number[], umask?: number): Cred;
297
+ /** A file that does not live in the volume — `/proc/uptime`, `/dev/null`, … */
298
+ interface VirtualNode {
299
+ kind: FileKind;
300
+ /** Permission bits only; the type bits are added from `kind`. */
301
+ mode: number;
302
+ uid?: number;
303
+ gid?: number;
304
+ size?: number;
305
+ mtimeMs?: number;
306
+ /** Symlink target, when `kind === "symlink"`. */
307
+ target?: string;
308
+ read?(): Uint8Array | string;
309
+ write?(data: Uint8Array, append: boolean): void;
310
+ /** Directory listing, when `kind === "directory"`. */
311
+ list?(): string[];
312
+ }
313
+ /**
314
+ * Supplies a subtree of synthetic files. `resolve` receives the path *relative*
315
+ * to `root` ("" means the mount point itself) and returns null for misses.
316
+ */
317
+ interface VirtualProvider {
318
+ root: string;
319
+ resolve(rel: string): VirtualNode | null;
320
+ /**
321
+ * When true (the default) the provider owns its whole subtree and a miss is
322
+ * `ENOENT` — that is what `/proc` wants, so a dead pid does not resolve to a
323
+ * stale on-disk file. `/dev` and `/sys` set this to false so that synthetic
324
+ * nodes overlay a real directory users can still write into.
325
+ */
326
+ exclusive?: boolean;
327
+ }
328
+ interface WriteOptions {
329
+ mode?: number;
330
+ append?: boolean;
331
+ cred?: Cred;
332
+ /** Skip the permission check — used by kernel-internal writes. */
333
+ privileged?: boolean;
334
+ }
335
+ interface ResolveOptions {
336
+ cred?: Cred;
337
+ /** Follow a symlink in the final position. Off for `lstat`, `rm`, `chmod -h`. */
338
+ followFinal?: boolean;
339
+ }
340
+ declare class Vfs {
341
+ readonly volume: RuntimeVolume;
342
+ private readonly providers;
343
+ private nextVirtualIno;
344
+ private readonly virtualInos;
345
+ constructor(volume: RuntimeVolume);
346
+ addProvider(provider: VirtualProvider): void;
347
+ removeProvider(root: string): void;
348
+ /** The mount points currently served synthetically. */
349
+ get virtualRoots(): string[];
350
+ private lookupVirtual;
351
+ /** True when a miss at `abs` must be ENOENT rather than a volume lookup. */
352
+ private isUnderProvider;
353
+ /** Synthetic children a non-exclusive provider contributes to a directory. */
354
+ private virtualChildren;
355
+ private virtualIno;
356
+ /** True when `cred` may perform `mode` (R_OK/W_OK/X_OK) on a stat result. */
357
+ permitted(st: Stats, mode: number, cred: Cred): boolean;
358
+ private require;
359
+ /**
360
+ * Walk `abs` component by component, following symlinks and checking search
361
+ * (`+x`) permission on every directory along the way, exactly like `namei`.
362
+ *
363
+ * Returns the fully resolved absolute path. Does *not* require the final
364
+ * component to exist — callers decide whether a miss is fatal.
365
+ */
366
+ resolvePath(abs: string, opts?: ResolveOptions): string;
367
+ /** lstat that returns null instead of throwing, for internal probing. */
368
+ private tryLstat;
369
+ private readlinkRaw;
370
+ /** stat(2) — follows symlinks. */
371
+ stat(abs: string, opts?: {
372
+ cred?: Cred;
373
+ }): Stats;
374
+ /** lstat(2) — does not follow a symlink in the final position. */
375
+ lstat(abs: string): Stats;
376
+ private statFromVirtual;
377
+ exists(abs: string, cred?: Cred): boolean;
378
+ lexists(abs: string): boolean;
379
+ access(abs: string, mode?: number, cred?: Cred): void;
380
+ realpath(abs: string, cred?: Cred): string;
381
+ readFile(abs: string, cred?: Cred): Uint8Array;
382
+ readText(abs: string, cred?: Cred): string;
383
+ readdir(abs: string, cred?: Cred): string[];
384
+ /** True when the volume itself has a real directory at `abs`. */
385
+ private volumeHasDir;
386
+ readdirWithTypes(abs: string, cred?: Cred): DirEntry[];
387
+ readlink(abs: string, cred?: Cred): string;
388
+ writeFile(abs: string, data: Uint8Array | string, opts?: WriteOptions): void;
389
+ appendFile(abs: string, data: Uint8Array | string, opts?: WriteOptions): void;
390
+ truncate(abs: string, len?: number, cred?: Cred): void;
391
+ mkdir(abs: string, opts?: {
392
+ mode?: number;
393
+ recursive?: boolean;
394
+ cred?: Cred;
395
+ }): void;
396
+ private mkdirOne;
397
+ rmdir(abs: string, cred?: Cred): void;
398
+ unlink(abs: string, cred?: Cred): void;
399
+ /** Recursive delete, the engine behind `rm -r`. */
400
+ rmrf(abs: string, cred?: Cred): void;
401
+ private requireParentWrite;
402
+ rename(from: string, to: string, cred?: Cred): void;
403
+ copyFile(from: string, to: string, cred?: Cred): void;
404
+ symlink(target: string, linkPath: string, cred?: Cred): void;
405
+ link(existing: string, newPath: string, cred?: Cred): void;
406
+ chmod(abs: string, mode: number, cred?: Cred, follow?: boolean): void;
407
+ chown(abs: string, uid: number, gid: number, cred?: Cred, follow?: boolean): void;
408
+ utimes(abs: string, atimeMs: number, mtimeMs: number, cred?: Cred): void;
409
+ /** `touch` semantics: create when missing, otherwise bump the timestamps. */
410
+ touch(abs: string, cred?: Cred, timeMs?: number): void;
411
+ /** Depth-first walk yielding absolute paths. Symlinks are not followed. */
412
+ walk(abs: string, opts?: {
413
+ includeSelf?: boolean;
414
+ cred?: Cred;
415
+ maxDepth?: number;
416
+ }): Generator<string>;
417
+ /** Recursive copy used by `cp -r` and the container's `copyIn` helper. */
418
+ copyTree(from: string, to: string, cred?: Cred): void;
419
+ /** Free/used byte accounting for `df` and `du`. */
420
+ usage(abs?: string): {
421
+ files: number;
422
+ dirs: number;
423
+ bytes: number;
424
+ };
425
+ }
426
+
427
+ export { type Cred as C, type DirEntry as D, type RuntimeVolume as R, type SpawnChild as S, Vfs as V, type WriteOptions as W, type RuntimeHttpResponse as a, type SyncSpawn as b, type VolumeStat as c, type VolumeStats as d, type RuntimePod as e, type RuntimePackageInstaller as f, type ChildSpawnConfig as g, type ChildHandle as h, type RuntimeProcess as i, type RuntimeSocketPeer as j, type RuntimeConnection as k, ROOT_CRED as l, type RuntimeProcessManager as m, type RuntimeProcessResult as n, Stats as o, type VirtualNode as p, type VirtualProvider as q, applyChmod as r, createChildProcessModule as s, formatMode as t, makeCred as u, octalMode as v, parseUmask as w };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.1.48",
4
- "description": "A Linux-like container that runs entirely inside Node.js POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
3
+ "version": "0.1.49",
4
+ "description": "A Linux-like container that runs entirely inside Node.js \u2014 POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "sideEffects": false,
@@ -32,6 +32,11 @@
32
32
  "types": "./dist/browser-host.d.ts",
33
33
  "import": "./dist/browser-host.js",
34
34
  "require": "./dist/browser-host.cjs"
35
+ },
36
+ "./python-abi": {
37
+ "types": "./dist/python-abi.d.ts",
38
+ "import": "./dist/python-abi.js",
39
+ "require": "./dist/python-abi.cjs"
35
40
  }
36
41
  },
37
42
  "files": [
@@ -41,7 +46,7 @@
41
46
  "LICENSE"
42
47
  ],
43
48
  "scripts": {
44
- "build": "tsup",
49
+ "build": "tsup && node python-runtime/scripts/copy_runtime.mjs",
45
50
  "dev": "tsup --watch",
46
51
  "typecheck": "tsc --noEmit",
47
52
  "test": "vitest run",