@johnhenry/andbox 0.0.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 +21 -0
- package/README.md +207 -0
- package/package.json +40 -0
- package/src/capability-gate.mjs +98 -0
- package/src/constants.mjs +24 -0
- package/src/deferred.mjs +23 -0
- package/src/import-map-resolver.mjs +71 -0
- package/src/index.d.ts +268 -0
- package/src/index.mjs +14 -0
- package/src/network-policy.mjs +48 -0
- package/src/sandbox.mjs +394 -0
- package/src/stdio.mjs +58 -0
- package/src/worker-source.mjs +216 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Henry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# andbox
|
|
2
|
+
|
|
3
|
+
A separate-context JavaScript runtime with Worker isolation, RPC capabilities, import maps, and timeouts.
|
|
4
|
+
|
|
5
|
+
andbox runs JavaScript in a dedicated Web Worker with a structured bridge back to the host. Code in that Worker can call host-provided "capabilities" via RPC, use import-mapped packages, and define virtual modules -- all with configurable rate limits, timeouts, and hard-kill semantics.
|
|
6
|
+
|
|
7
|
+
**andbox's job is running code in its own context with a clean RPC surface, not containing adversarial code.** The Worker boundary keeps well-behaved code from touching the DOM or host globals by accident, and gives you rate limits, timeouts, and a kill switch for code you trust but don't want to block on or grant unrestricted access to. It is **not** a security sandbox: code that specifically tries to escape can reach `fetch`, `WebSocket`, `Worker`, and other Worker-global APIs directly, regardless of what capabilities you grant. See [Security model](#security-model) before using andbox to run code you don't trust.
|
|
8
|
+
|
|
9
|
+
Zero dependencies. Uses only Web Workers and standard browser APIs.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @johnhenry/andbox
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or via CDN (no bundler needed):
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { createSandbox } from 'https://esm.sh/@johnhenry/andbox';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> **Provenance:** previously published as `andbox@0.1.1`. This package restarts
|
|
24
|
+
> versioning at `0.0.0` under the `@johnhenry` scope.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createSandbox } from '@johnhenry/andbox';
|
|
30
|
+
|
|
31
|
+
const sandbox = await createSandbox({
|
|
32
|
+
capabilities: {
|
|
33
|
+
readFile: async (path) => { /* host-side file read */ },
|
|
34
|
+
writeFile: async (path, content) => { /* host-side file write */ },
|
|
35
|
+
},
|
|
36
|
+
importMap: {
|
|
37
|
+
imports: {
|
|
38
|
+
'lodash': 'https://esm.sh/lodash',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
onConsole: (level, ...args) => console.log(`[sandbox:${level}]`, ...args),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Evaluate code in the sandbox
|
|
45
|
+
const result = await sandbox.evaluate(`
|
|
46
|
+
const greeting = 'Hello from the sandbox!';
|
|
47
|
+
console.log(greeting);
|
|
48
|
+
|
|
49
|
+
// Call a host capability
|
|
50
|
+
const content = await host.call('readFile', '/etc/hostname');
|
|
51
|
+
return content;
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
// Define a virtual module
|
|
55
|
+
await sandbox.defineModule('utils', `
|
|
56
|
+
export function add(a, b) { return a + b; }
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
// Import the virtual module from sandbox code
|
|
60
|
+
await sandbox.evaluate(`
|
|
61
|
+
const { add } = await sandboxImport('utils');
|
|
62
|
+
return add(2, 3); // 5
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
// Clean up
|
|
66
|
+
await sandbox.dispose();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Sandbox Modes
|
|
70
|
+
|
|
71
|
+
andbox supports three execution modes:
|
|
72
|
+
|
|
73
|
+
- **`worker`** (default) -- Runs in a dedicated Worker with an RPC bridge, import maps, virtual modules, and hard-kill timeout semantics. See [Security model](#security-model) for what this does and doesn't protect against.
|
|
74
|
+
- **`inline`** -- Same-thread execution via AsyncFunction. Lighter weight, no Worker overhead, no isolation at all -- code runs with full access to the calling context. Only for code you already trust.
|
|
75
|
+
- **`data-uri`** -- Dynamic `import()` via Blob URL. Module-level separation without a Worker. Supports globals injection.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
// Inline mode (no Worker)
|
|
79
|
+
const inline = createSandbox({ mode: 'inline', globals: { math: Math } });
|
|
80
|
+
const result = await inline.execute('return math.sqrt(16)');
|
|
81
|
+
|
|
82
|
+
// Data-URI mode
|
|
83
|
+
const dataUri = createSandbox({ mode: 'data-uri', globals: { x: 42 } });
|
|
84
|
+
const result = await dataUri.execute('print(x)');
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
### `createSandbox(options?)`
|
|
90
|
+
|
|
91
|
+
Creates a new sandboxed runtime. Returns a promise (Worker mode) or object (inline/data-uri mode).
|
|
92
|
+
|
|
93
|
+
**Options:**
|
|
94
|
+
|
|
95
|
+
| Option | Type | Default | Description |
|
|
96
|
+
|--------|------|---------|-------------|
|
|
97
|
+
| `mode` | `'worker' \| 'inline' \| 'data-uri'` | `'worker'` | Execution mode |
|
|
98
|
+
| `importMap` | `{ imports?, scopes? }` | `{}` | Import map for package resolution (Worker mode) |
|
|
99
|
+
| `capabilities` | `Record<string, Function>` | `{}` | Host functions callable via `host.call()` (Worker mode) |
|
|
100
|
+
| `defaultTimeoutMs` | `number` | `30000` | Default timeout for `evaluate()` |
|
|
101
|
+
| `baseURL` | `string` | `location.href` | Base URL for relative imports |
|
|
102
|
+
| `policy` | `GatePolicy` | -- | Rate limiting policy |
|
|
103
|
+
| `onConsole` | `(level, ...args) => void` | -- | Console output handler |
|
|
104
|
+
| `globals` | `Record<string, any>` | `{}` | Global variables (inline/data-uri modes) |
|
|
105
|
+
|
|
106
|
+
**Returns (Worker mode):** `Promise<{ evaluate, defineModule, dispose, stats, isDisposed }>`
|
|
107
|
+
|
|
108
|
+
### `sandbox.evaluate(code, opts?)`
|
|
109
|
+
|
|
110
|
+
Evaluates JavaScript code in the sandbox. The code is wrapped in an async IIFE -- use `return` to produce a result.
|
|
111
|
+
|
|
112
|
+
| Option | Type | Description |
|
|
113
|
+
|--------|------|-------------|
|
|
114
|
+
| `timeoutMs` | `number` | Override default timeout |
|
|
115
|
+
| `signal` | `AbortSignal` | Abort evaluation |
|
|
116
|
+
| `onConsole` | `(level, ...args) => void` | Per-call console handler |
|
|
117
|
+
|
|
118
|
+
**Inside sandbox code (Worker mode):**
|
|
119
|
+
|
|
120
|
+
- `host.call(name, ...args)` -- Call a host capability by name
|
|
121
|
+
- `sandboxImport(name)` -- Import a virtual module
|
|
122
|
+
- `console.log/warn/error/info` -- Forwarded to host `onConsole`
|
|
123
|
+
|
|
124
|
+
### `sandbox.defineModule(name, source)`
|
|
125
|
+
|
|
126
|
+
Defines a virtual module that sandbox code can import via `sandboxImport(name)`.
|
|
127
|
+
|
|
128
|
+
### `sandbox.dispose()`
|
|
129
|
+
|
|
130
|
+
Terminates the Worker and rejects all pending evaluations.
|
|
131
|
+
|
|
132
|
+
### `sandbox.stats()`
|
|
133
|
+
|
|
134
|
+
Returns runtime statistics including pending evaluations, virtual modules, and gate stats.
|
|
135
|
+
|
|
136
|
+
### `gateCapabilities(capabilities, policy?)`
|
|
137
|
+
|
|
138
|
+
Wraps host functions with rate limiting and payload caps.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { gateCapabilities } from '@johnhenry/andbox';
|
|
142
|
+
|
|
143
|
+
const { gated, stats } = gateCapabilities(
|
|
144
|
+
{ fetch: async (url) => (await fetch(url)).text() },
|
|
145
|
+
{
|
|
146
|
+
limits: { maxCalls: 100, maxArgBytes: 1_000_000, maxConcurrent: 8 },
|
|
147
|
+
capabilities: { fetch: { maxCalls: 50 } },
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `resolveWithImportMap(specifier, importMap, parentURL?)`
|
|
153
|
+
|
|
154
|
+
Resolves a module specifier against an import map, following the browser import map algorithm.
|
|
155
|
+
|
|
156
|
+
### `createNetworkFetch(allowedHosts?, fetchFn?)`
|
|
157
|
+
|
|
158
|
+
Creates a fetch function that checks the request hostname against an allowlist before calling through. Useful for keeping cooperative code pointed at the hosts you intend -- **not redirect-safe** (see [Security model](#security-model)): an allowlisted host that responds with a redirect is followed without re-checking the final URL.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import { createNetworkFetch } from '@johnhenry/andbox';
|
|
162
|
+
|
|
163
|
+
const gatedFetch = createNetworkFetch(['api.example.com']);
|
|
164
|
+
await gatedFetch('https://api.example.com/data'); // OK
|
|
165
|
+
await gatedFetch('https://evil.com/steal'); // throws
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `createStdio()`
|
|
169
|
+
|
|
170
|
+
Creates an async iterable stream for console output capture.
|
|
171
|
+
|
|
172
|
+
### `makeDeferred()`, `makeAbortError()`, `makeTimeoutError(ms)`
|
|
173
|
+
|
|
174
|
+
Promise and error utilities used internally, also available for consumers.
|
|
175
|
+
|
|
176
|
+
### `makeWorkerSource()`
|
|
177
|
+
|
|
178
|
+
Returns the Worker script source code as a string (useful for custom Worker setups).
|
|
179
|
+
|
|
180
|
+
## Execution model
|
|
181
|
+
|
|
182
|
+
Code runs inside a Web Worker created from a Blob URL. This gets you, for free, against code that isn't specifically trying to defeat it:
|
|
183
|
+
|
|
184
|
+
- **No DOM access** -- Workers are inherently isolated from the document
|
|
185
|
+
- **No direct host object references** -- only what's explicitly passed in (capabilities, globals, import map entries) is reachable, so ordinary code can't accidentally touch host-side state
|
|
186
|
+
- **Hard kill** -- on timeout, the Worker is `terminate()`d and a fresh one is created for the next call
|
|
187
|
+
- **Virtual modules** -- modules defined via `defineModule()` are available via `sandboxImport()`
|
|
188
|
+
- **Capability rate limits** -- `gateCapabilities()` caps calls/concurrency/payload size per capability, for cooperative callers
|
|
189
|
+
|
|
190
|
+
## Security model
|
|
191
|
+
|
|
192
|
+
andbox is **not** a boundary against code that is actively trying to escape it. If you're running code you don't fully trust, read this section -- every item below is a confirmed way sandboxed code can act outside what `capabilities`/`policy`/`createNetworkFetch` appear to allow:
|
|
193
|
+
|
|
194
|
+
- **Worker-global APIs are directly reachable, regardless of `capabilities`.** Sandboxed code executes in a real Worker global scope, so `fetch`, `WebSocket`, `Worker` (nested workers), `importScripts`, `indexedDB`, and `self.postMessage` are all callable directly -- omitting a `fetch` capability does not block network access. This is fundamental to how `Function`-based evaluation works and isn't fixable without a different execution strategy (e.g. a cross-origin iframe with a strict CSP, or a Realms/Compartments-based approach). See [andbox#10](https://github.com/johnhenry/andbox/issues/10).
|
|
195
|
+
- **`sandboxImport()` will load and execute an arbitrary remote URL.** Any `http(s)://` specifier is passed straight to `import()` with no allowlist, independent of any network policy configured for capabilities. See [andbox#7](https://github.com/johnhenry/andbox/issues/7).
|
|
196
|
+
- **A timeout stops message delivery, not in-flight host-side effects.** If a capability call with a real side effect (a write, an API call) is in flight when the timeout fires, that side effect still completes on the host even though the Worker is killed. Capabilities with real side effects should be designed to be idempotent and/or cancellable via `AbortSignal`. See [andbox#8](https://github.com/johnhenry/andbox/issues/8).
|
|
197
|
+
|
|
198
|
+
Fixed as of this audit (kept here for history -- see the linked issues for details):
|
|
199
|
+
|
|
200
|
+
- ~~`gateCapabilities()`'s check could be bypassed via the prototype chain~~ (`host.call('constructor', ...)` resolved through `Object.prototype` to the real global `Object` constructor). Fixed by building the gated object with `Object.create(null)`. See [andbox#5](https://github.com/johnhenry/andbox/issues/5).
|
|
201
|
+
- ~~`createNetworkFetch()`'s allowlist was not redirect-safe~~ (it checked the request hostname before the fetch, not the final response URL). Fixed by fetching with `redirect: 'manual'` and rejecting any redirect response outright. See [andbox#6](https://github.com/johnhenry/andbox/issues/6).
|
|
202
|
+
|
|
203
|
+
If you need to run untrusted/adversarial code safely, andbox alone is not sufficient -- pair it with OS-level isolation (a separate process/container with its own network and filesystem restrictions) or use a purpose-built sandboxing runtime. Capability gating and rate limits here are for organizing and throttling code you already trust, not for containing code you don't.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@johnhenry/andbox",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Sandboxed JavaScript runtime with Worker isolation, RPC, import maps, and timeouts",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/*.test.mjs",
|
|
17
|
+
"example:01": "node examples/01-untrusted-code-runs-isolated.mjs",
|
|
18
|
+
"example:02": "node examples/02-capability-limits-cut-off-abuse.mjs",
|
|
19
|
+
"example:03": "node examples/03-network-allowlist-blocks-unapproved-hosts.mjs",
|
|
20
|
+
"example:04": "node examples/04-runaway-code-gets-timed-out.mjs",
|
|
21
|
+
"examples": "npm run example:01 && npm run example:02 && npm run example:03 && npm run example:04"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"sandbox",
|
|
25
|
+
"worker",
|
|
26
|
+
"isolation",
|
|
27
|
+
"rpc",
|
|
28
|
+
"browser",
|
|
29
|
+
"javascript"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/johnhenry/andbox"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://opensource.johnhenry.me/andbox/",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=24.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gate — rate-limits and validates host.call() invocations.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a capabilities object with a Proxy that enforces:
|
|
5
|
+
* - Global call count limits
|
|
6
|
+
* - Global argument byte limits
|
|
7
|
+
* - Per-capability call count and argument byte limits
|
|
8
|
+
* - Concurrent call limits
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DEFAULT_LIMITS, DEFAULT_CAPABILITY_LIMITS } from './constants.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} GatePolicy
|
|
15
|
+
* @property {{ maxCalls?: number, maxArgBytes?: number, maxConcurrent?: number }} [limits]
|
|
16
|
+
* @property {Record<string, { maxArgBytes?: number, maxCalls?: number }>} [capabilities]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gate capabilities with rate limiting and payload caps.
|
|
21
|
+
*
|
|
22
|
+
* @param {Record<string, Function>} capabilities - Raw capability functions.
|
|
23
|
+
* @param {GatePolicy} [policy] - Rate limit policy.
|
|
24
|
+
* @returns {{ gated: Record<string, Function>, stats: () => object }}
|
|
25
|
+
*/
|
|
26
|
+
export function gateCapabilities(capabilities, policy = {}) {
|
|
27
|
+
const limits = { ...DEFAULT_LIMITS, ...policy.limits };
|
|
28
|
+
const capPolicies = policy.capabilities || {};
|
|
29
|
+
|
|
30
|
+
let totalCalls = 0;
|
|
31
|
+
let totalArgBytes = 0;
|
|
32
|
+
let concurrent = 0;
|
|
33
|
+
const perCap = new Map(); // name -> { calls, argBytes }
|
|
34
|
+
|
|
35
|
+
function getCapStats(name) {
|
|
36
|
+
if (!perCap.has(name)) perCap.set(name, { calls: 0, argBytes: 0 });
|
|
37
|
+
return perCap.get(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Object.create(null) — no Object.prototype chain, so lookups for names
|
|
41
|
+
// like 'constructor' or 'toString' cannot resolve through the prototype
|
|
42
|
+
// to real global functions and bypass the allowlist/rate limiting below.
|
|
43
|
+
const gated = Object.create(null);
|
|
44
|
+
|
|
45
|
+
for (const [name, fn] of Object.entries(capabilities)) {
|
|
46
|
+
const capLimits = { ...DEFAULT_CAPABILITY_LIMITS, ...capPolicies[name] };
|
|
47
|
+
|
|
48
|
+
gated[name] = async (...args) => {
|
|
49
|
+
// Measure argument bytes
|
|
50
|
+
const argStr = JSON.stringify(args);
|
|
51
|
+
const argBytes = new TextEncoder().encode(argStr).byteLength;
|
|
52
|
+
|
|
53
|
+
// Global limits
|
|
54
|
+
if (limits.maxCalls > 0 && totalCalls >= limits.maxCalls) {
|
|
55
|
+
throw new Error(`Global call limit exceeded (${limits.maxCalls})`);
|
|
56
|
+
}
|
|
57
|
+
if (limits.maxArgBytes > 0 && totalArgBytes + argBytes > limits.maxArgBytes) {
|
|
58
|
+
throw new Error(`Global argument byte limit exceeded (${limits.maxArgBytes})`);
|
|
59
|
+
}
|
|
60
|
+
if (limits.maxConcurrent > 0 && concurrent >= limits.maxConcurrent) {
|
|
61
|
+
throw new Error(`Concurrent call limit exceeded (${limits.maxConcurrent})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Per-capability limits
|
|
65
|
+
const stats = getCapStats(name);
|
|
66
|
+
if (capLimits.maxCalls > 0 && stats.calls >= capLimits.maxCalls) {
|
|
67
|
+
throw new Error(`Capability '${name}' call limit exceeded (${capLimits.maxCalls})`);
|
|
68
|
+
}
|
|
69
|
+
if (capLimits.maxArgBytes > 0 && argBytes > capLimits.maxArgBytes) {
|
|
70
|
+
throw new Error(`Capability '${name}' argument size exceeded (${capLimits.maxArgBytes} bytes)`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Track
|
|
74
|
+
totalCalls++;
|
|
75
|
+
totalArgBytes += argBytes;
|
|
76
|
+
stats.calls++;
|
|
77
|
+
stats.argBytes += argBytes;
|
|
78
|
+
concurrent++;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return await fn(...args);
|
|
82
|
+
} finally {
|
|
83
|
+
concurrent--;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stats() {
|
|
89
|
+
return {
|
|
90
|
+
totalCalls,
|
|
91
|
+
totalArgBytes,
|
|
92
|
+
concurrent,
|
|
93
|
+
perCapability: Object.fromEntries(perCap),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { gated, stats };
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default constants for andbox sandboxes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Default timeout for evaluate() in milliseconds. */
|
|
6
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
|
|
8
|
+
/** Default resource limits. */
|
|
9
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
10
|
+
/** Max capability calls per sandbox lifetime (0 = unlimited). */
|
|
11
|
+
maxCalls: 0,
|
|
12
|
+
/** Max total argument bytes across all calls (0 = unlimited). */
|
|
13
|
+
maxArgBytes: 0,
|
|
14
|
+
/** Max concurrent pending capability calls. */
|
|
15
|
+
maxConcurrent: 16,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Default per-capability limits. */
|
|
19
|
+
export const DEFAULT_CAPABILITY_LIMITS = Object.freeze({
|
|
20
|
+
/** Max argument bytes for a single call to this capability. */
|
|
21
|
+
maxArgBytes: 0,
|
|
22
|
+
/** Max calls to this specific capability (0 = unlimited). */
|
|
23
|
+
maxCalls: 0,
|
|
24
|
+
});
|
package/src/deferred.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred promise utilities for andbox.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Create a deferred promise with external resolve/reject. */
|
|
6
|
+
export function makeDeferred() {
|
|
7
|
+
let resolve, reject;
|
|
8
|
+
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
9
|
+
return { promise, resolve, reject };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Create an AbortError. */
|
|
13
|
+
export function makeAbortError(message = 'Operation aborted') {
|
|
14
|
+
const err = new DOMException(message, 'AbortError');
|
|
15
|
+
return err;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Create a timeout Error. */
|
|
19
|
+
export function makeTimeoutError(ms) {
|
|
20
|
+
const err = new Error(`Sandbox execution timed out after ${ms}ms`);
|
|
21
|
+
err.name = 'TimeoutError';
|
|
22
|
+
return err;
|
|
23
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import map resolution following the WICG import maps spec (subset).
|
|
3
|
+
*
|
|
4
|
+
* Supports `imports` (bare specifier → URL) and `scopes` (per-prefix overrides).
|
|
5
|
+
* Used by the Worker to resolve `sandboxImport()` specifiers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a specifier using an import map.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} specifier - The import specifier (bare or relative).
|
|
12
|
+
* @param {{ imports?: Record<string,string>, scopes?: Record<string,Record<string,string>> }} importMap
|
|
13
|
+
* @param {string} [parentURL] - The URL of the importing module (for scopes).
|
|
14
|
+
* @returns {string|null} Resolved URL or null if no match.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveWithImportMap(specifier, importMap, parentURL) {
|
|
17
|
+
if (!importMap) return null;
|
|
18
|
+
|
|
19
|
+
// Check scopes first (more specific wins)
|
|
20
|
+
if (parentURL && importMap.scopes) {
|
|
21
|
+
// Sort scope keys by length descending — most specific first
|
|
22
|
+
const scopeKeys = Object.keys(importMap.scopes)
|
|
23
|
+
.filter(scope => parentURL.startsWith(scope))
|
|
24
|
+
.sort((a, b) => b.length - a.length);
|
|
25
|
+
|
|
26
|
+
for (const scope of scopeKeys) {
|
|
27
|
+
const result = matchSpecifier(specifier, importMap.scopes[scope]);
|
|
28
|
+
if (result !== null) return result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check top-level imports
|
|
33
|
+
if (importMap.imports) {
|
|
34
|
+
const result = matchSpecifier(specifier, importMap.imports);
|
|
35
|
+
if (result !== null) return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Match a specifier against a mapping object.
|
|
43
|
+
* Handles both exact matches and prefix matches (keys ending with '/').
|
|
44
|
+
*
|
|
45
|
+
* @param {string} specifier
|
|
46
|
+
* @param {Record<string,string>} mapping
|
|
47
|
+
* @returns {string|null}
|
|
48
|
+
*/
|
|
49
|
+
function matchSpecifier(specifier, mapping) {
|
|
50
|
+
// Exact match
|
|
51
|
+
if (mapping[specifier] !== undefined) {
|
|
52
|
+
return mapping[specifier];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Prefix match: find the longest key ending with '/' that matches
|
|
56
|
+
let bestKey = null;
|
|
57
|
+
for (const key of Object.keys(mapping)) {
|
|
58
|
+
if (!key.endsWith('/')) continue;
|
|
59
|
+
if (!specifier.startsWith(key)) continue;
|
|
60
|
+
if (bestKey === null || key.length > bestKey.length) {
|
|
61
|
+
bestKey = key;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (bestKey !== null) {
|
|
66
|
+
const suffix = specifier.slice(bestKey.length);
|
|
67
|
+
return mapping[bestKey] + suffix;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|