@cedarjs/api 8.0.0-canary.3450 → 8.0.0-canary.3451
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 +0 -12
- package/dist/index.js +3 -3
- package/package.json +2 -9
- package/dist/sandbox/concurrencyLimiter.d.ts +0 -22
- package/dist/sandbox/concurrencyLimiter.d.ts.map +0 -1
- package/dist/sandbox/concurrencyLimiter.js +0 -33
- package/dist/sandbox/findChildScript.d.ts +0 -29
- package/dist/sandbox/findChildScript.d.ts.map +0 -1
- package/dist/sandbox/findChildScript.js +0 -35
- package/dist/sandbox/index.d.ts +0 -11
- package/dist/sandbox/index.d.ts.map +0 -1
- package/dist/sandbox/index.js +0 -22
- package/dist/sandbox/noNetworkPreload.d.ts +0 -16
- package/dist/sandbox/noNetworkPreload.d.ts.map +0 -1
- package/dist/sandbox/noNetworkPreload.js +0 -109
- package/dist/sandbox/runSandboxedChild.d.ts +0 -186
- package/dist/sandbox/runSandboxedChild.d.ts.map +0 -1
- package/dist/sandbox/runSandboxedChild.js +0 -213
- package/dist/sandbox/sandboxPackages.d.ts +0 -16
- package/dist/sandbox/sandboxPackages.d.ts.map +0 -1
- package/dist/sandbox/sandboxPackages.js +0 -63
package/README.md
CHANGED
|
@@ -39,18 +39,6 @@ Note: CedarJS logging is setup for its api side only. For browser and web side e
|
|
|
39
39
|
|
|
40
40
|
For detailed logger configuration, see the CedarJS logger package [README](./src/logger/README.md).
|
|
41
41
|
|
|
42
|
-
### Sandbox
|
|
43
|
-
|
|
44
|
-
`@cedarjs/api/sandbox` runs a Node script on untrusted input — an uploaded
|
|
45
|
-
photo, a third party's PDF — in a locked-down child process: none of your app's environment
|
|
46
|
-
variables (on Windows a few system ones remain), read access only to its
|
|
47
|
-
script, the packages it imports and the paths you allow, no writes, no
|
|
48
|
-
child processes, no network, a heap cap, a timeout and output limits. Use it
|
|
49
|
-
for parsing, decoding or converting files you didn't create (sharp, pdf.js,
|
|
50
|
-
…). It's defense in depth on top of Node's permission model, not a security
|
|
51
|
-
boundary, and native addons aren't limited by that model. See the sandbox
|
|
52
|
-
[README](./src/sandbox/README.md).
|
|
53
|
-
|
|
54
42
|
## Contributing
|
|
55
43
|
|
|
56
44
|
`@cedarjs/api` uses a few things you should be familiar with:
|
package/dist/index.js
CHANGED
|
@@ -8,9 +8,9 @@ export * from "./cors.js";
|
|
|
8
8
|
export * from "./event.js";
|
|
9
9
|
export * from "./signedTokens/index.js";
|
|
10
10
|
const prismaVersion = "7.10.0";
|
|
11
|
-
const redwoodVersion = "8.0.0-canary.
|
|
12
|
-
const cedarjsVersion = "8.0.0-canary.
|
|
13
|
-
const cedarVersion = "8.0.0-canary.
|
|
11
|
+
const redwoodVersion = "8.0.0-canary.3451";
|
|
12
|
+
const cedarjsVersion = "8.0.0-canary.3451";
|
|
13
|
+
const cedarVersion = "8.0.0-canary.3451";
|
|
14
14
|
export {
|
|
15
15
|
cedarVersion,
|
|
16
16
|
cedarjsVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/api",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3451",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/cedarjs/cedar.git",
|
|
@@ -40,12 +40,6 @@
|
|
|
40
40
|
"default": "./dist/logger/index.js"
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
|
-
"./sandbox": {
|
|
44
|
-
"default": {
|
|
45
|
-
"types": "./dist/sandbox/index.d.ts",
|
|
46
|
-
"default": "./dist/sandbox/index.js"
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
43
|
"./webhooks": {
|
|
50
44
|
"default": {
|
|
51
45
|
"types": "./dist/webhooks/index.d.ts",
|
|
@@ -90,7 +84,7 @@
|
|
|
90
84
|
"title-case": "3.0.3"
|
|
91
85
|
},
|
|
92
86
|
"devDependencies": {
|
|
93
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
87
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3451",
|
|
94
88
|
"@types/aws-lambda": "8.10.164",
|
|
95
89
|
"@types/jsonwebtoken": "9.0.10",
|
|
96
90
|
"@types/memjs": "1",
|
|
@@ -100,7 +94,6 @@
|
|
|
100
94
|
"memjs": "1.3.2",
|
|
101
95
|
"publint": "0.3.25",
|
|
102
96
|
"redis": "6.3.0",
|
|
103
|
-
"sharp": "0.35.5",
|
|
104
97
|
"split2": "4.2.0",
|
|
105
98
|
"ts-toolbelt": "9.6.0",
|
|
106
99
|
"typescript": "5.9.3",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runs `fn` once a slot is free; resolves or rejects with what `fn` does.
|
|
3
|
-
*/
|
|
4
|
-
export type ConcurrencyLimiter = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
5
|
-
/**
|
|
6
|
-
* A limiter that lets at most `max` tasks run at once; the rest wait their
|
|
7
|
-
* turn, first come first served. Use it around `runSandboxedChild()`: each
|
|
8
|
-
* sandboxed child is a whole Node process, and a burst of uploads shouldn't
|
|
9
|
-
* start a hundred of them.
|
|
10
|
-
*
|
|
11
|
-
* The limit is per limiter, in this process. Create one per kind of work
|
|
12
|
-
* at module level, not one per call.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* const limit = createConcurrencyLimiter(2)
|
|
17
|
-
*
|
|
18
|
-
* const result = await limit(() => runSandboxedChild({ ... }))
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export declare const createConcurrencyLimiter: (max: number) => ConcurrencyLimiter;
|
|
22
|
-
//# sourceMappingURL=concurrencyLimiter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"concurrencyLimiter.d.ts","sourceRoot":"","sources":["../../src/sandbox/concurrencyLimiter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;AAExE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,kBAkCtD,CAAA"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const createConcurrencyLimiter = (max) => {
|
|
2
|
-
if (!Number.isInteger(max) || max < 1) {
|
|
3
|
-
throw new Error(`max must be a positive integer, got ${max}`);
|
|
4
|
-
}
|
|
5
|
-
let running = 0;
|
|
6
|
-
const waiting = [];
|
|
7
|
-
const acquire = async () => {
|
|
8
|
-
if (running < max) {
|
|
9
|
-
running += 1;
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
await new Promise((resolve) => waiting.push(resolve));
|
|
13
|
-
};
|
|
14
|
-
const release = () => {
|
|
15
|
-
const next = waiting.shift();
|
|
16
|
-
if (next) {
|
|
17
|
-
next();
|
|
18
|
-
} else {
|
|
19
|
-
running -= 1;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
return async (fn) => {
|
|
23
|
-
await acquire();
|
|
24
|
-
try {
|
|
25
|
-
return await fn();
|
|
26
|
-
} finally {
|
|
27
|
-
release();
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
export {
|
|
32
|
-
createConcurrencyLimiter
|
|
33
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Finds a sandboxed child's script for the module that runs it. Call it
|
|
3
|
-
* with `import.meta.dirname` and the script's name without extension, the
|
|
4
|
-
* same way in every build:
|
|
5
|
-
*
|
|
6
|
-
* - from source (tests, `cedar exec`), `<baseName>.ts` (or `.js`) in
|
|
7
|
-
* `dir`, which Node runs by stripping its types;
|
|
8
|
-
* - in a regular build, `<baseName>.js` in `dir`, where the build puts it
|
|
9
|
-
* next to its caller;
|
|
10
|
-
* - in a bundled build (`cedar build --ud`, and so on Netlify and Vercel),
|
|
11
|
-
* where the caller is part of a bundle,
|
|
12
|
-
* `api/dist/ud/sandbox-children/<baseName>.js` in the app (looked for
|
|
13
|
-
* from `dir` up). A bundled build needs each child's name to be unique
|
|
14
|
-
* in `api/src`.
|
|
15
|
-
*
|
|
16
|
-
* Throws when there's no such script, so a build that lost it fails loudly
|
|
17
|
-
* instead of running without it.
|
|
18
|
-
*
|
|
19
|
-
* Keep the child script plain enough to run as-is: no path aliases like
|
|
20
|
-
* `src/…`, no TypeScript-only syntax such as enums, and imports of packages
|
|
21
|
-
* only, never of your app's own modules.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* const script = findChildScript(import.meta.dirname, 'resize.child')
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export declare const findChildScript: (dir: string, baseName: string) => string;
|
|
29
|
-
//# sourceMappingURL=findChildScript.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"findChildScript.d.ts","sourceRoot":"","sources":["../../src/sandbox/findChildScript.ts"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,EAAE,UAAU,MAAM,KAAG,MAiB/D,CAAA"}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const SANDBOX_CHILDREN_DIR = path.join("api", "dist", "ud", "sandbox-children");
|
|
4
|
-
const findBundledChild = (dir, baseName) => {
|
|
5
|
-
let current = dir;
|
|
6
|
-
for (; ; ) {
|
|
7
|
-
const candidate = path.join(current, SANDBOX_CHILDREN_DIR, `${baseName}.js`);
|
|
8
|
-
if (fs.existsSync(candidate)) {
|
|
9
|
-
return candidate;
|
|
10
|
-
}
|
|
11
|
-
const parent = path.dirname(current);
|
|
12
|
-
if (parent === current) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
current = parent;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
const findChildScript = (dir, baseName) => {
|
|
19
|
-
for (const name of [`${baseName}.js`, `${baseName}.ts`]) {
|
|
20
|
-
const candidate = path.join(dir, name);
|
|
21
|
-
if (fs.existsSync(candidate)) {
|
|
22
|
-
return candidate;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const bundled = findBundledChild(dir, baseName);
|
|
26
|
-
if (bundled) {
|
|
27
|
-
return bundled;
|
|
28
|
-
}
|
|
29
|
-
throw new Error(
|
|
30
|
-
`findChildScript: no ${baseName}.js or ${baseName}.ts in ${dir}, and no ${path.join(SANDBOX_CHILDREN_DIR, `${baseName}.js`)} from it up`
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
export {
|
|
34
|
-
findChildScript
|
|
35
|
-
};
|
package/dist/sandbox/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Run code on untrusted input in a sandboxed, locked-down child process.
|
|
3
|
-
* See `runSandboxedChild()` and ./README.md.
|
|
4
|
-
*/
|
|
5
|
-
export { DEFAULT_SANDBOX_HEAP_MB, DEFAULT_SANDBOX_MAX_INPUT_BYTES, DEFAULT_SANDBOX_MAX_OUTPUT_BYTES, DEFAULT_SANDBOX_TIMEOUT_MS, runSandboxedChild, sandboxedChildArgs, } from './runSandboxedChild.js';
|
|
6
|
-
export type { RunSandboxedChildOptions, SandboxedChildArgsOptions, SandboxedChildFailure, SandboxedChildResult, } from './runSandboxedChild.js';
|
|
7
|
-
export { createConcurrencyLimiter } from './concurrencyLimiter.js';
|
|
8
|
-
export type { ConcurrencyLimiter } from './concurrencyLimiter.js';
|
|
9
|
-
export { permissionModelBlocksNetwork } from './noNetworkPreload.js';
|
|
10
|
-
export { findChildScript } from './findChildScript.js';
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sandbox/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,gCAAgC,EAChC,0BAA0B,EAC1B,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EACV,wBAAwB,EACxB,yBAAyB,EACzB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA"}
|
package/dist/sandbox/index.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_SANDBOX_HEAP_MB,
|
|
3
|
-
DEFAULT_SANDBOX_MAX_INPUT_BYTES,
|
|
4
|
-
DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
|
|
5
|
-
DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
6
|
-
runSandboxedChild,
|
|
7
|
-
sandboxedChildArgs
|
|
8
|
-
} from "./runSandboxedChild.js";
|
|
9
|
-
import { createConcurrencyLimiter } from "./concurrencyLimiter.js";
|
|
10
|
-
import { permissionModelBlocksNetwork } from "./noNetworkPreload.js";
|
|
11
|
-
import { findChildScript } from "./findChildScript.js";
|
|
12
|
-
export {
|
|
13
|
-
DEFAULT_SANDBOX_HEAP_MB,
|
|
14
|
-
DEFAULT_SANDBOX_MAX_INPUT_BYTES,
|
|
15
|
-
DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
|
|
16
|
-
DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
17
|
-
createConcurrencyLimiter,
|
|
18
|
-
findChildScript,
|
|
19
|
-
permissionModelBlocksNetwork,
|
|
20
|
-
runSandboxedChild,
|
|
21
|
-
sandboxedChildArgs
|
|
22
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether this Node's permission model restricts network access. On Node
|
|
3
|
-
* 25 and later it does: under `--permission`, without `--allow-net`, a
|
|
4
|
-
* child can't open sockets, listen, look up names or `fetch`. On Node 24
|
|
5
|
-
* it doesn't, so `runSandboxedChild()` loads a preload that takes the
|
|
6
|
-
* network away from JavaScript instead (see `NO_NETWORK_PRELOAD`).
|
|
7
|
-
*
|
|
8
|
-
* Neither limits native code: an addon makes its own system calls.
|
|
9
|
-
*/
|
|
10
|
-
export declare const permissionModelBlocksNetwork: () => boolean;
|
|
11
|
-
/**
|
|
12
|
-
* The `--import` argument that loads the no-network preload, as a `data:`
|
|
13
|
-
* URL. Loading it needs no file and no read permission.
|
|
14
|
-
*/
|
|
15
|
-
export declare const noNetworkPreloadArg: () => string;
|
|
16
|
-
//# sourceMappingURL=noNetworkPreload.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"noNetworkPreload.d.ts","sourceRoot":"","sources":["../../src/sandbox/noNetworkPreload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,eACe,CAAA;AAiIxD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,cAC2C,CAAA"}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
const permissionModelBlocksNetwork = () => process.allowedNodeEnvironmentFlags.has("--allow-net");
|
|
2
|
-
const NO_NETWORK_PRELOAD = String.raw`
|
|
3
|
-
const BLOCKED = new Set([
|
|
4
|
-
'net',
|
|
5
|
-
'tls',
|
|
6
|
-
'dgram',
|
|
7
|
-
'dns',
|
|
8
|
-
'dns/promises',
|
|
9
|
-
'http',
|
|
10
|
-
'https',
|
|
11
|
-
'http2',
|
|
12
|
-
'_http_agent',
|
|
13
|
-
'_http_client',
|
|
14
|
-
'_http_common',
|
|
15
|
-
'_http_incoming',
|
|
16
|
-
'_http_outgoing',
|
|
17
|
-
'_http_server',
|
|
18
|
-
'_tls_common',
|
|
19
|
-
'_tls_wrap',
|
|
20
|
-
])
|
|
21
|
-
|
|
22
|
-
const isBlocked = (id) =>
|
|
23
|
-
typeof id === 'string' && BLOCKED.has(id.replace(/^node:/, ''))
|
|
24
|
-
|
|
25
|
-
const refuse = (what) =>
|
|
26
|
-
function noNetwork() {
|
|
27
|
-
throw new Error('No network access in a sandboxed child (' + what + ')')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const getBuiltinModule = process.getBuiltinModule.bind(process)
|
|
31
|
-
const { registerHooks, syncBuiltinESMExports } =
|
|
32
|
-
getBuiltinModule('node:module')
|
|
33
|
-
|
|
34
|
-
// The modules themselves first, before the hooks below make them
|
|
35
|
-
// unreachable. Builtin modules are shared, so Node's own users of them
|
|
36
|
-
// (http uses net) get the patched functions too
|
|
37
|
-
const net = getBuiltinModule('node:net')
|
|
38
|
-
net.Socket.prototype.connect = refuse('net.Socket#connect')
|
|
39
|
-
net.Server.prototype.listen = refuse('net.Server#listen')
|
|
40
|
-
net.connect = refuse('net.connect')
|
|
41
|
-
net.createConnection = refuse('net.createConnection')
|
|
42
|
-
|
|
43
|
-
const tls = getBuiltinModule('node:tls')
|
|
44
|
-
tls.connect = refuse('tls.connect')
|
|
45
|
-
|
|
46
|
-
const dgram = getBuiltinModule('node:dgram')
|
|
47
|
-
for (const name of ['bind', 'connect', 'send']) {
|
|
48
|
-
dgram.Socket.prototype[name] = refuse('dgram.Socket#' + name)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const dns = getBuiltinModule('node:dns')
|
|
52
|
-
for (const [label, api] of [
|
|
53
|
-
['dns', dns],
|
|
54
|
-
['dns.promises', dns.promises],
|
|
55
|
-
]) {
|
|
56
|
-
for (const name of Object.keys(api)) {
|
|
57
|
-
if (typeof api[name] === 'function') {
|
|
58
|
-
api[name] = refuse(label + '.' + name)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// The handles under stdin, stdout and stderr (a Pipe or a TTY). Their
|
|
64
|
-
// class can make new handles that connect or bind, e.g. to a Unix socket.
|
|
65
|
-
// Reading process.stdin and friends here also opens the child's own stdio
|
|
66
|
-
// before open() is taken away
|
|
67
|
-
for (const stream of [process.stdin, process.stdout, process.stderr]) {
|
|
68
|
-
const handle = stream._handle
|
|
69
|
-
const proto = handle && Object.getPrototypeOf(handle)
|
|
70
|
-
if (!proto) {
|
|
71
|
-
continue
|
|
72
|
-
}
|
|
73
|
-
for (const name of ['bind', 'bind6', 'connect', 'connect6', 'listen', 'open']) {
|
|
74
|
-
if (typeof proto[name] === 'function') {
|
|
75
|
-
proto[name] = refuse(handle.constructor.name + '#' + name)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
syncBuiltinESMExports()
|
|
81
|
-
|
|
82
|
-
// Then the ways to the modules
|
|
83
|
-
registerHooks({
|
|
84
|
-
resolve(specifier, context, nextResolve) {
|
|
85
|
-
if (isBlocked(specifier)) {
|
|
86
|
-
refuse('import of ' + specifier)()
|
|
87
|
-
}
|
|
88
|
-
return nextResolve(specifier, context)
|
|
89
|
-
},
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
process.getBuiltinModule = (id) => {
|
|
93
|
-
if (isBlocked(id)) {
|
|
94
|
-
refuse('process.getBuiltinModule(' + id + ')')()
|
|
95
|
-
}
|
|
96
|
-
return getBuiltinModule(id)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (const name of ['fetch', 'WebSocket', 'EventSource']) {
|
|
100
|
-
delete globalThis[name]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
//# sourceURL=cedar-sandbox-no-network-preload.js
|
|
104
|
-
`;
|
|
105
|
-
const noNetworkPreloadArg = () => `--import=data:text/javascript,${encodeURIComponent(NO_NETWORK_PRELOAD)}`;
|
|
106
|
-
export {
|
|
107
|
-
noNetworkPreloadArg,
|
|
108
|
-
permissionModelBlocksNetwork
|
|
109
|
-
};
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/** The heap cap for a sandboxed child, in MB, unless `heapMb` is given */
|
|
2
|
-
export declare const DEFAULT_SANDBOX_HEAP_MB = 256;
|
|
3
|
-
/** How long a sandboxed child may run, unless `timeoutMs` is given */
|
|
4
|
-
export declare const DEFAULT_SANDBOX_TIMEOUT_MS = 10000;
|
|
5
|
-
/** The most bytes sent to a sandboxed child, unless `maxInputBytes` is given */
|
|
6
|
-
export declare const DEFAULT_SANDBOX_MAX_INPUT_BYTES: number;
|
|
7
|
-
/** The most bytes read back from a sandboxed child, unless `maxOutputBytes` is given */
|
|
8
|
-
export declare const DEFAULT_SANDBOX_MAX_OUTPUT_BYTES: number;
|
|
9
|
-
export interface SandboxedChildArgsOptions {
|
|
10
|
-
/**
|
|
11
|
-
* The Node script the child runs: a `.js`/`.mjs`/`.cjs` file, or a `.ts`
|
|
12
|
-
* file Node can run by stripping its types. It may read itself.
|
|
13
|
-
*/
|
|
14
|
-
script: string;
|
|
15
|
-
/**
|
|
16
|
-
* The packages the child imports, by name (`['sharp']`). The child may
|
|
17
|
-
* read each one's folder and those of everything it depends on
|
|
18
|
-
* (dependencies, and installed optional and peer dependencies, which is
|
|
19
|
-
* where native packages keep their platform's binaries), resolved from
|
|
20
|
-
* the script's folder the way Node will resolve its imports. Nothing
|
|
21
|
-
* else in `node_modules` is readable. A package that isn't installed
|
|
22
|
-
* there fails the run (`'spawn-error'`) before a child starts.
|
|
23
|
-
*/
|
|
24
|
-
packages?: string[];
|
|
25
|
-
/**
|
|
26
|
-
* Files and directories (and everything under them) the child may read
|
|
27
|
-
* besides its own script and its `packages`. Everything else on disk is
|
|
28
|
-
* unreadable to the child: `.env`, your app's source, uploaded files.
|
|
29
|
-
*/
|
|
30
|
-
readable?: string[];
|
|
31
|
-
/**
|
|
32
|
-
* Whether the child may load native addons (`.node` files), as a library
|
|
33
|
-
* like sharp needs. Off by default. Node's permission model does not
|
|
34
|
-
* apply to native code: an addon, or an attacker who takes over one, can
|
|
35
|
-
* do anything the OS lets the process do. Turn this on only for fixed,
|
|
36
|
-
* known libraries, never for an addon the input could choose.
|
|
37
|
-
*/
|
|
38
|
-
allowAddons?: boolean;
|
|
39
|
-
/** The child's heap cap (`--max-old-space-size`), in MB. Default 256. */
|
|
40
|
-
heapMb?: number;
|
|
41
|
-
/** Arguments for the child script, in `process.argv.slice(2)` */
|
|
42
|
-
args?: string[];
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* The `node` command-line arguments for running `script` as a sandboxed
|
|
46
|
-
* child process: Node's permission model (`--permission`) with read access
|
|
47
|
-
* to `script`, its `packages` and `readable` only, no network, a heap cap, and code generation from
|
|
48
|
-
* strings (`eval`, `new Function`) turned off. No file writes, no child
|
|
49
|
-
* processes, no worker threads, no WASI, no inspector, and native addons
|
|
50
|
-
* only with `allowAddons`.
|
|
51
|
-
*
|
|
52
|
-
* On Node 25 and later the permission model blocks the network. On Node 24
|
|
53
|
-
* it doesn't, so the arguments also load a preload that takes the network
|
|
54
|
-
* away from JavaScript in the child (see `permissionModelBlocksNetwork()`).
|
|
55
|
-
*
|
|
56
|
-
* `runSandboxedChild()` uses this; it's exported for spawning the child
|
|
57
|
-
* yourself (e.g. in a test probing what the child can do). Pass an empty
|
|
58
|
-
* `env` when you do: the arguments restrict what the child may touch, not
|
|
59
|
-
* what it inherits.
|
|
60
|
-
*
|
|
61
|
-
* The permission model checks the path a file is opened by, and Node opens
|
|
62
|
-
* the script and the modules it imports by their real paths, so the
|
|
63
|
-
* script is given by its real path, and other paths by both (an app
|
|
64
|
-
* deployed behind a `current -> releases/…` symlink would otherwise be
|
|
65
|
-
* refused its own files). Throws if a path doesn't exist.
|
|
66
|
-
*/
|
|
67
|
-
export declare const sandboxedChildArgs: ({ script, packages, readable, allowAddons, heapMb, args, }: SandboxedChildArgsOptions) => string[];
|
|
68
|
-
export interface RunSandboxedChildOptions extends SandboxedChildArgsOptions {
|
|
69
|
-
/** The untrusted bytes, written to the child's stdin */
|
|
70
|
-
input: Uint8Array;
|
|
71
|
-
/**
|
|
72
|
-
* The child is killed (SIGKILL) if it hasn't exited after this many
|
|
73
|
-
* milliseconds. Default 10 000. Like the byte limits, it must be a finite
|
|
74
|
-
* number from 0 up; anything else (`NaN` from a missing setting, say) is
|
|
75
|
-
* a `'spawn-error'` and no child is started.
|
|
76
|
-
*/
|
|
77
|
-
timeoutMs?: number;
|
|
78
|
-
/**
|
|
79
|
-
* Larger input is refused without starting the child. Default 25 MiB.
|
|
80
|
-
*/
|
|
81
|
-
maxInputBytes?: number;
|
|
82
|
-
/**
|
|
83
|
-
* The child is killed as soon as it has written more than this many bytes
|
|
84
|
-
* to stdout. Default 25 MiB.
|
|
85
|
-
*/
|
|
86
|
-
maxOutputBytes?: number;
|
|
87
|
-
/** The child's working directory. Default: the script's folder. */
|
|
88
|
-
cwd?: string;
|
|
89
|
-
}
|
|
90
|
-
/** Why a sandboxed child produced no output */
|
|
91
|
-
export type SandboxedChildFailure =
|
|
92
|
-
/** The input was over `maxInputBytes`; no child was started */
|
|
93
|
-
'input-too-large'
|
|
94
|
-
/** The child ran past `timeoutMs` and was killed */
|
|
95
|
-
| 'timeout'
|
|
96
|
-
/** The child wrote more than `maxOutputBytes` and was killed */
|
|
97
|
-
| 'output-too-large'
|
|
98
|
-
/** The child exited non-zero or was killed by a signal */
|
|
99
|
-
| 'exit'
|
|
100
|
-
/** The child couldn't be started, or a limit isn't a valid number */
|
|
101
|
-
| 'spawn-error';
|
|
102
|
-
export type SandboxedChildResult = {
|
|
103
|
-
ok: true;
|
|
104
|
-
/** Everything the child wrote to stdout */
|
|
105
|
-
output: Buffer;
|
|
106
|
-
/** What the child wrote to stderr (the last 4000 characters) */
|
|
107
|
-
stderr: string;
|
|
108
|
-
} | {
|
|
109
|
-
ok: false;
|
|
110
|
-
failure: SandboxedChildFailure;
|
|
111
|
-
/** A one-line description, for a log */
|
|
112
|
-
message: string;
|
|
113
|
-
/** What the child wrote to stderr (the last 4000 characters) */
|
|
114
|
-
stderr: string;
|
|
115
|
-
exitCode: number | null;
|
|
116
|
-
signal: NodeJS.Signals | null;
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* Runs a Node script on untrusted bytes in a locked-down (sandboxed) child
|
|
120
|
-
* process, so a crafted input that exploits a parser bug lands in a process
|
|
121
|
-
* that can't read your secrets or touch your files. Use it for parsing,
|
|
122
|
-
* decoding or converting files your users or third parties send you:
|
|
123
|
-
* resizing a photo with sharp, rendering a PDF page with pdf.js, unpacking
|
|
124
|
-
* an archive, parsing a spreadsheet.
|
|
125
|
-
*
|
|
126
|
-
* The child:
|
|
127
|
-
*
|
|
128
|
-
* - gets an **empty environment** — no `DATABASE_URL`, no API keys, none of
|
|
129
|
-
* your `.env`;
|
|
130
|
-
* - runs under **Node's permission model**: it may read its own script,
|
|
131
|
-
* the `packages` it imports and the `readable` paths,
|
|
132
|
-
* nothing else; it may not write files, spawn processes, start workers,
|
|
133
|
-
* or `eval`; it may load native addons only with `allowAddons`;
|
|
134
|
-
* - has **no network**: no sockets, no `fetch`, no DNS (on Node 24 through
|
|
135
|
-
* a preload, see `permissionModelBlocksNetwork()`);
|
|
136
|
-
* - gets the input on **stdin**, and its **stdout** is the result, used only
|
|
137
|
-
* if it exits with code 0;
|
|
138
|
-
* - is **bounded**: a heap cap, a hard timeout (SIGKILL), and caps on input
|
|
139
|
-
* and output size.
|
|
140
|
-
*
|
|
141
|
-
* It never rejects: every failure — a non-zero exit, a timeout, too much
|
|
142
|
-
* output, a crash — is an `{ ok: false }` result with a `failure` and a
|
|
143
|
-
* `message` for your log. Treat the output as untrusted too, and check it
|
|
144
|
-
* (e.g. that it's really a PNG) before using it.
|
|
145
|
-
*
|
|
146
|
-
* Limit how many run at once with `createConcurrencyLimiter()`: each child
|
|
147
|
-
* is a whole Node process.
|
|
148
|
-
*
|
|
149
|
-
* **This is defense in depth, not a security boundary.** Node documents
|
|
150
|
-
* that its permission model does not protect against malicious code that
|
|
151
|
-
* is already running in the process. None of it constrains native code: a
|
|
152
|
-
* native addon (`allowAddons`), or an exploit in one, makes its own system
|
|
153
|
-
* calls, so for those children what holds is the empty environment, the
|
|
154
|
-
* separate process and its limits. An external binary like ffmpeg needs an
|
|
155
|
-
* OS-level sandbox (a container, seccomp, a separate user).
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```ts
|
|
159
|
-
* import {
|
|
160
|
-
* createConcurrencyLimiter,
|
|
161
|
-
* findChildScript,
|
|
162
|
-
* runSandboxedChild,
|
|
163
|
-
* } from '@cedarjs/api/sandbox'
|
|
164
|
-
*
|
|
165
|
-
* const limit = createConcurrencyLimiter(2)
|
|
166
|
-
*
|
|
167
|
-
* const result = await limit(() =>
|
|
168
|
-
* runSandboxedChild({
|
|
169
|
-
* script: findChildScript(import.meta.dirname, 'resize.child'),
|
|
170
|
-
* packages: ['sharp'],
|
|
171
|
-
* allowAddons: true,
|
|
172
|
-
* input: uploadedBytes,
|
|
173
|
-
* timeoutMs: 10_000,
|
|
174
|
-
* maxOutputBytes: 4 * 1024 * 1024,
|
|
175
|
-
* }),
|
|
176
|
-
* )
|
|
177
|
-
*
|
|
178
|
-
* if (result.ok) {
|
|
179
|
-
* // result.output: Buffer
|
|
180
|
-
* } else {
|
|
181
|
-
* logger.warn({ failure: result.failure }, result.message)
|
|
182
|
-
* }
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
export declare const runSandboxedChild: ({ input, timeoutMs, maxInputBytes, maxOutputBytes, cwd, ...argsOptions }: RunSandboxedChildOptions) => Promise<SandboxedChildResult>;
|
|
186
|
-
//# sourceMappingURL=runSandboxedChild.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runSandboxedChild.d.ts","sourceRoot":"","sources":["../../src/sandbox/runSandboxedChild.ts"],"names":[],"mappings":"AAaA,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,MAAM,CAAA;AAC1C,sEAAsE;AACtE,eAAO,MAAM,0BAA0B,QAAS,CAAA;AAChD,gFAAgF;AAChF,eAAO,MAAM,+BAA+B,QAAW,CAAA;AACvD,wFAAwF;AACxF,eAAO,MAAM,gCAAgC,QAAW,CAAA;AAQxD,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,GAAI,4DAOhC,yBAAyB,KAAG,MAAM,EA6BpC,CAAA;AAED,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB;IACzE,wDAAwD;IACxD,KAAK,EAAE,UAAU,CAAA;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,+CAA+C;AAC/C,MAAM,MAAM,qBAAqB;AAC/B,+DAA+D;AAC7D,iBAAiB;AACnB,oDAAoD;GAClD,SAAS;AACX,gEAAgE;GAC9D,kBAAkB;AACpB,0DAA0D;GACxD,MAAM;AACR,qEAAqE;GACnE,aAAa,CAAA;AAEjB,MAAM,MAAM,oBAAoB,GAC5B;IACE,EAAE,EAAE,IAAI,CAAA;IACR,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;CACf,GACD;IACE,EAAE,EAAE,KAAK,CAAA;IACT,OAAO,EAAE,qBAAqB,CAAA;IAC9B,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CAC9B,CAAA;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,eAAO,MAAM,iBAAiB,GAAI,0EAO/B,wBAAwB,KAAG,OAAO,CAAC,oBAAoB,CA+LzD,CAAA"}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
noNetworkPreloadArg,
|
|
6
|
-
permissionModelBlocksNetwork
|
|
7
|
-
} from "./noNetworkPreload.js";
|
|
8
|
-
import { sandboxPackagePaths } from "./sandboxPackages.js";
|
|
9
|
-
const MIB = 1024 * 1024;
|
|
10
|
-
const DEFAULT_SANDBOX_HEAP_MB = 256;
|
|
11
|
-
const DEFAULT_SANDBOX_TIMEOUT_MS = 1e4;
|
|
12
|
-
const DEFAULT_SANDBOX_MAX_INPUT_BYTES = 25 * MIB;
|
|
13
|
-
const DEFAULT_SANDBOX_MAX_OUTPUT_BYTES = 25 * MIB;
|
|
14
|
-
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
15
|
-
const MAX_STDERR_CHARS = 4e3;
|
|
16
|
-
const sandboxedChildArgs = ({
|
|
17
|
-
script,
|
|
18
|
-
packages = [],
|
|
19
|
-
readable = [],
|
|
20
|
-
allowAddons = false,
|
|
21
|
-
heapMb = DEFAULT_SANDBOX_HEAP_MB,
|
|
22
|
-
args = []
|
|
23
|
-
}) => {
|
|
24
|
-
if (!Number.isInteger(heapMb) || heapMb <= 0) {
|
|
25
|
-
throw new Error(`heapMb must be a positive integer, got ${heapMb}`);
|
|
26
|
-
}
|
|
27
|
-
const realScript = fs.realpathSync(script);
|
|
28
|
-
const readPaths = [
|
|
29
|
-
.../* @__PURE__ */ new Set([
|
|
30
|
-
realScript,
|
|
31
|
-
...sandboxPackagePaths(packages, realScript),
|
|
32
|
-
// Node may read a file through the path it was given or through
|
|
33
|
-
// its real path
|
|
34
|
-
...readable.flatMap((p) => [path.resolve(p), fs.realpathSync(p)])
|
|
35
|
-
])
|
|
36
|
-
];
|
|
37
|
-
return [
|
|
38
|
-
`--max-old-space-size=${heapMb}`,
|
|
39
|
-
"--disallow-code-generation-from-strings",
|
|
40
|
-
"--permission",
|
|
41
|
-
...allowAddons ? ["--allow-addons"] : [],
|
|
42
|
-
// Node warns that the permission model is experimental, and about
|
|
43
|
-
// --allow-addons; the warnings would end up in the failure message
|
|
44
|
-
"--no-warnings",
|
|
45
|
-
...readPaths.map((p) => `--allow-fs-read=${p}`),
|
|
46
|
-
...permissionModelBlocksNetwork() ? [] : [noNetworkPreloadArg()],
|
|
47
|
-
realScript,
|
|
48
|
-
...args
|
|
49
|
-
];
|
|
50
|
-
};
|
|
51
|
-
const runSandboxedChild = ({
|
|
52
|
-
input,
|
|
53
|
-
timeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
54
|
-
maxInputBytes = DEFAULT_SANDBOX_MAX_INPUT_BYTES,
|
|
55
|
-
maxOutputBytes = DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
|
|
56
|
-
cwd,
|
|
57
|
-
...argsOptions
|
|
58
|
-
}) => {
|
|
59
|
-
const fail = (failure, message, rest = {}) => ({
|
|
60
|
-
ok: false,
|
|
61
|
-
failure,
|
|
62
|
-
message,
|
|
63
|
-
stderr: rest.stderr ?? "",
|
|
64
|
-
exitCode: rest.exitCode ?? null,
|
|
65
|
-
signal: rest.signal ?? null
|
|
66
|
-
});
|
|
67
|
-
const invalidLimit = [
|
|
68
|
-
["timeoutMs", timeoutMs, MAX_TIMEOUT_MS],
|
|
69
|
-
["maxInputBytes", maxInputBytes, Number.MAX_SAFE_INTEGER],
|
|
70
|
-
["maxOutputBytes", maxOutputBytes, Number.MAX_SAFE_INTEGER]
|
|
71
|
-
].find(
|
|
72
|
-
([, value, max]) => typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > Number(max)
|
|
73
|
-
);
|
|
74
|
-
if (invalidLimit) {
|
|
75
|
-
const [name, value, max] = invalidLimit;
|
|
76
|
-
return Promise.resolve(
|
|
77
|
-
fail(
|
|
78
|
-
"spawn-error",
|
|
79
|
-
`${name} must be a number from 0 to ${max}, got ${String(value)}`
|
|
80
|
-
)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (input.length > maxInputBytes) {
|
|
84
|
-
return Promise.resolve(
|
|
85
|
-
fail(
|
|
86
|
-
"input-too-large",
|
|
87
|
-
`input is ${input.length} bytes, over the ${maxInputBytes} byte limit`
|
|
88
|
-
)
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
let args;
|
|
92
|
-
try {
|
|
93
|
-
args = sandboxedChildArgs(argsOptions);
|
|
94
|
-
} catch (e) {
|
|
95
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
96
|
-
return Promise.resolve(fail("spawn-error", message));
|
|
97
|
-
}
|
|
98
|
-
return new Promise((resolve) => {
|
|
99
|
-
let settled = false;
|
|
100
|
-
let stderr = "";
|
|
101
|
-
const stderrTail = () => stderr.slice(-MAX_STDERR_CHARS);
|
|
102
|
-
let child;
|
|
103
|
-
const settle = (result) => {
|
|
104
|
-
if (settled) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
settled = true;
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
resolve(result);
|
|
110
|
-
};
|
|
111
|
-
let killedFor = null;
|
|
112
|
-
const kill = (failure, message) => {
|
|
113
|
-
if (killedFor || settled) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
killedFor = { failure, message };
|
|
117
|
-
clearTimeout(timer);
|
|
118
|
-
child?.kill("SIGKILL");
|
|
119
|
-
};
|
|
120
|
-
const timer = setTimeout(
|
|
121
|
-
() => kill("timeout", `timed out after ${timeoutMs} ms`),
|
|
122
|
-
timeoutMs
|
|
123
|
-
);
|
|
124
|
-
try {
|
|
125
|
-
child = spawn(process.execPath, args, {
|
|
126
|
-
// Nothing of the parent's environment: no database URL, no keys
|
|
127
|
-
env: {},
|
|
128
|
-
cwd: cwd ?? path.dirname(fs.realpathSync(argsOptions.script)),
|
|
129
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
130
|
-
windowsHide: true
|
|
131
|
-
});
|
|
132
|
-
} catch (e) {
|
|
133
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
134
|
-
settle(fail("spawn-error", message));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const output = [];
|
|
138
|
-
let outputSize = 0;
|
|
139
|
-
child.stdout?.on("data", (chunk) => {
|
|
140
|
-
if (settled || killedFor) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
outputSize += chunk.length;
|
|
144
|
-
if (outputSize > maxOutputBytes) {
|
|
145
|
-
kill(
|
|
146
|
-
"output-too-large",
|
|
147
|
-
`output is over the ${maxOutputBytes} byte limit`
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
output.push(chunk);
|
|
152
|
-
});
|
|
153
|
-
child.stderr?.setEncoding("utf8");
|
|
154
|
-
child.stderr?.on("data", (chunk) => {
|
|
155
|
-
stderr = (stderr + chunk).slice(-MAX_STDERR_CHARS * 2);
|
|
156
|
-
});
|
|
157
|
-
child.on("error", (e) => {
|
|
158
|
-
if (child?.pid === void 0) {
|
|
159
|
-
settle(fail("spawn-error", e.message, { stderr: stderrTail() }));
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
child.on("exit", (exitCode, signal) => {
|
|
163
|
-
if (killedFor) {
|
|
164
|
-
settle(
|
|
165
|
-
fail(killedFor.failure, killedFor.message, {
|
|
166
|
-
stderr: stderrTail(),
|
|
167
|
-
exitCode,
|
|
168
|
-
signal
|
|
169
|
-
})
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
child.on("close", (exitCode, signal) => {
|
|
174
|
-
if (killedFor) {
|
|
175
|
-
settle(
|
|
176
|
-
fail(killedFor.failure, killedFor.message, {
|
|
177
|
-
stderr: stderrTail(),
|
|
178
|
-
exitCode,
|
|
179
|
-
signal
|
|
180
|
-
})
|
|
181
|
-
);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (exitCode === 0) {
|
|
185
|
-
settle({
|
|
186
|
-
ok: true,
|
|
187
|
-
output: Buffer.concat(output),
|
|
188
|
-
stderr: stderrTail()
|
|
189
|
-
});
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const lastLine = stderrTail().trim().split("\n").filter((line) => line.trim()).pop() ?? "";
|
|
193
|
-
settle(
|
|
194
|
-
fail(
|
|
195
|
-
"exit",
|
|
196
|
-
`exited with ${exitCode ?? signal}${lastLine ? `: ${lastLine}` : ""}`,
|
|
197
|
-
{ stderr: stderrTail(), exitCode, signal }
|
|
198
|
-
)
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
child.stdin?.on("error", () => {
|
|
202
|
-
});
|
|
203
|
-
child.stdin?.end(input);
|
|
204
|
-
});
|
|
205
|
-
};
|
|
206
|
-
export {
|
|
207
|
-
DEFAULT_SANDBOX_HEAP_MB,
|
|
208
|
-
DEFAULT_SANDBOX_MAX_INPUT_BYTES,
|
|
209
|
-
DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
|
|
210
|
-
DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
211
|
-
runSandboxedChild,
|
|
212
|
-
sandboxedChildArgs
|
|
213
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The folders a sandboxed child needs to read to import `packages` from
|
|
3
|
-
* `fromFile` (its script): each package's folder and, recursively, those
|
|
4
|
-
* of its `dependencies` and installed `optionalDependencies` and
|
|
5
|
-
* `peerDependencies`, each resolved from the folder of the package that
|
|
6
|
-
* needs it, as Node will at run time. That covers nested versions, pnpm's
|
|
7
|
-
* layout and native packages' platform binaries (`@img/sharp-<platform>`)
|
|
8
|
-
* without naming a platform. Optional and peer dependencies that aren't
|
|
9
|
-
* installed are skipped; a missing package or regular dependency throws.
|
|
10
|
-
*
|
|
11
|
-
* A package reached through a symlink is granted at both paths: Node reads
|
|
12
|
-
* its `package.json` through the path it looked in, and its modules
|
|
13
|
-
* through their real paths.
|
|
14
|
-
*/
|
|
15
|
-
export declare const sandboxPackagePaths: (packages: string[], fromFile: string) => string[];
|
|
16
|
-
//# sourceMappingURL=sandboxPackages.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxPackages.d.ts","sourceRoot":"","sources":["../../src/sandbox/sandboxPackages.ts"],"names":[],"mappings":"AAoDA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,EAAE,EAAE,UAAU,MAAM,aAuCvE,CAAA"}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
const findPackage = (name, fromFile) => {
|
|
5
|
-
const passed = [];
|
|
6
|
-
for (const dir of createRequire(fromFile).resolve.paths(name) ?? []) {
|
|
7
|
-
const found = path.join(dir, name);
|
|
8
|
-
if (!fs.existsSync(path.join(found, "package.json"))) {
|
|
9
|
-
if (path.basename(dir) === "node_modules" && fs.existsSync(dir)) {
|
|
10
|
-
passed.push(
|
|
11
|
-
path.join(dir, name, "package.json"),
|
|
12
|
-
path.join(fs.realpathSync(dir), name, "package.json")
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
const realDir = fs.realpathSync(found);
|
|
18
|
-
const pkg = JSON.parse(
|
|
19
|
-
fs.readFileSync(path.join(realDir, "package.json"), "utf8")
|
|
20
|
-
);
|
|
21
|
-
return { found, realDir, pkg, passed };
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
};
|
|
25
|
-
const sandboxPackagePaths = (packages, fromFile) => {
|
|
26
|
-
const paths = /* @__PURE__ */ new Set();
|
|
27
|
-
const visited = /* @__PURE__ */ new Set();
|
|
28
|
-
const visit = (name, from, required, by) => {
|
|
29
|
-
const found = findPackage(name, from);
|
|
30
|
-
if (!found) {
|
|
31
|
-
if (required) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`Can't find package "${name}" (needed by ${by}) in any node_modules folder Node looks in from ${path.dirname(from)}`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
paths.add(found.found).add(found.realDir);
|
|
39
|
-
for (const manifest2 of found.passed) {
|
|
40
|
-
paths.add(manifest2);
|
|
41
|
-
}
|
|
42
|
-
if (visited.has(found.realDir)) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
visited.add(found.realDir);
|
|
46
|
-
const manifest = path.join(found.realDir, "package.json");
|
|
47
|
-
const { dependencies = {}, optionalDependencies = {} } = found.pkg;
|
|
48
|
-
const optional = { ...optionalDependencies, ...found.pkg.peerDependencies };
|
|
49
|
-
for (const dep of Object.keys(dependencies)) {
|
|
50
|
-
visit(dep, manifest, !(dep in optional), name);
|
|
51
|
-
}
|
|
52
|
-
for (const dep of Object.keys(optional)) {
|
|
53
|
-
visit(dep, manifest, false, name);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
for (const name of packages) {
|
|
57
|
-
visit(name, fromFile, true, path.basename(fromFile));
|
|
58
|
-
}
|
|
59
|
-
return [...paths];
|
|
60
|
-
};
|
|
61
|
-
export {
|
|
62
|
-
sandboxPackagePaths
|
|
63
|
-
};
|