@ricsam/isolate-runtime 0.1.4 → 0.1.8
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 +387 -26
- package/dist/cjs/index.cjs +443 -28
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/internal.cjs +97 -0
- package/dist/cjs/internal.cjs.map +10 -0
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +433 -29
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/internal.mjs +53 -0
- package/dist/mjs/internal.mjs.map +10 -0
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +158 -30
- package/dist/types/internal.d.ts +39 -0
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,46 +1,388 @@
|
|
|
1
1
|
# @ricsam/isolate-runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Complete isolated-vm V8 sandbox runtime with all APIs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm add @ricsam/isolate-runtime isolated-vm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
4
12
|
|
|
5
13
|
```typescript
|
|
6
14
|
import { createRuntime } from "@ricsam/isolate-runtime";
|
|
7
15
|
|
|
8
16
|
const runtime = await createRuntime({
|
|
9
|
-
|
|
10
|
-
memoryLimit: 128,
|
|
11
|
-
// Console API
|
|
17
|
+
memoryLimitMB: 128,
|
|
12
18
|
console: {
|
|
13
|
-
|
|
19
|
+
onEntry: (entry) => console.log("[sandbox]", entry),
|
|
20
|
+
},
|
|
21
|
+
fetch: async (request) => fetch(request),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Run code as ES module (supports top-level await)
|
|
25
|
+
await runtime.eval(`
|
|
26
|
+
const response = await fetch("https://api.example.com/data");
|
|
27
|
+
console.log(await response.json());
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
// Set up HTTP server
|
|
31
|
+
await runtime.eval(`
|
|
32
|
+
serve({
|
|
33
|
+
fetch(request) {
|
|
34
|
+
return Response.json({ message: "Hello!" });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
`);
|
|
38
|
+
|
|
39
|
+
// Dispatch requests via fetch handle
|
|
40
|
+
const response = await runtime.fetch.dispatchRequest(
|
|
41
|
+
new Request("http://localhost/api")
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Timers fire automatically with real time
|
|
45
|
+
// Clear all pending timers if needed
|
|
46
|
+
runtime.timers.clearAll();
|
|
47
|
+
|
|
48
|
+
// Console state access
|
|
49
|
+
const counters = runtime.console.getCounters();
|
|
50
|
+
const timers = runtime.console.getTimers();
|
|
51
|
+
runtime.console.reset();
|
|
52
|
+
|
|
53
|
+
// Cleanup
|
|
54
|
+
await runtime.dispose();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Runtime Interface
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
interface RuntimeHandle {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
eval(code: string, filename?: string): Promise<void>;
|
|
63
|
+
dispose(): Promise<void>;
|
|
64
|
+
|
|
65
|
+
// Module handles
|
|
66
|
+
readonly fetch: RuntimeFetchHandle;
|
|
67
|
+
readonly timers: RuntimeTimersHandle;
|
|
68
|
+
readonly console: RuntimeConsoleHandle;
|
|
69
|
+
readonly testEnvironment: RuntimeTestEnvironmentHandle;
|
|
70
|
+
readonly playwright: RuntimePlaywrightHandle;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface RuntimeFetchHandle {
|
|
74
|
+
dispatchRequest(request: Request, options?: DispatchOptions): Promise<Response>;
|
|
75
|
+
hasServeHandler(): boolean;
|
|
76
|
+
hasActiveConnections(): boolean;
|
|
77
|
+
getUpgradeRequest(): UpgradeRequest | null;
|
|
78
|
+
// WebSocket methods...
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface RuntimeTimersHandle {
|
|
82
|
+
clearAll(): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface RuntimeConsoleHandle {
|
|
86
|
+
reset(): void;
|
|
87
|
+
getTimers(): Map<string, number>;
|
|
88
|
+
getCounters(): Map<string, number>;
|
|
89
|
+
getGroupDepth(): number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface RuntimeTestEnvironmentHandle {
|
|
93
|
+
runTests(timeout?: number): Promise<RunResults>;
|
|
94
|
+
hasTests(): boolean;
|
|
95
|
+
getTestCount(): number;
|
|
96
|
+
reset(): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface RuntimePlaywrightHandle {
|
|
100
|
+
getCollectedData(): CollectedData;
|
|
101
|
+
clearCollectedData(): void;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Options
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
interface RuntimeOptions {
|
|
109
|
+
memoryLimitMB?: number;
|
|
110
|
+
console?: ConsoleCallbacks;
|
|
111
|
+
fetch?: FetchCallback;
|
|
112
|
+
fs?: FsOptions;
|
|
113
|
+
moduleLoader?: ModuleLoaderCallback;
|
|
114
|
+
customFunctions?: CustomFunctions;
|
|
115
|
+
cwd?: string;
|
|
116
|
+
/** Enable test environment (describe, it, expect) */
|
|
117
|
+
testEnvironment?: boolean | TestEnvironmentOptions;
|
|
118
|
+
/** Playwright options - user provides page object */
|
|
119
|
+
playwright?: PlaywrightOptions;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface PlaywrightOptions {
|
|
123
|
+
page: import("playwright").Page;
|
|
124
|
+
timeout?: number;
|
|
125
|
+
baseUrl?: string;
|
|
126
|
+
/** Print browser console logs to stdout */
|
|
127
|
+
console?: boolean;
|
|
128
|
+
/** Browser console log callback (from the page, not sandbox) */
|
|
129
|
+
onBrowserConsoleLog?: (entry: { level: string; args: unknown[]; timestamp: number }) => void;
|
|
130
|
+
onNetworkRequest?: (info: { url: string; method: string; headers: Record<string, string>; timestamp: number }) => void;
|
|
131
|
+
onNetworkResponse?: (info: { url: string; status: number; headers: Record<string, string>; timestamp: number }) => void;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Module Loader
|
|
136
|
+
|
|
137
|
+
Provide custom ES modules:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const runtime = await createRuntime({
|
|
141
|
+
moduleLoader: async (moduleName) => {
|
|
142
|
+
if (moduleName === "@/utils") {
|
|
143
|
+
return `
|
|
144
|
+
export function add(a, b) { return a + b; }
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Unknown module: ${moduleName}`);
|
|
14
148
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await runtime.eval(`
|
|
152
|
+
import { add } from "@/utils";
|
|
153
|
+
console.log(add(2, 3)); // 5
|
|
154
|
+
`);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Custom Functions
|
|
158
|
+
|
|
159
|
+
Expose host functions to the isolate. Each function must specify its `type`:
|
|
160
|
+
|
|
161
|
+
- `'sync'` - Synchronous function, returns value directly
|
|
162
|
+
- `'async'` - Asynchronous function, returns a Promise
|
|
163
|
+
- `'asyncIterator'` - Async generator, yields values via `for await...of`
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const runtime = await createRuntime({
|
|
167
|
+
customFunctions: {
|
|
168
|
+
// Async function
|
|
169
|
+
hashPassword: {
|
|
170
|
+
fn: async (password) => bcrypt.hash(password, 10),
|
|
171
|
+
type: 'async',
|
|
172
|
+
},
|
|
173
|
+
// Sync function
|
|
174
|
+
getConfig: {
|
|
175
|
+
fn: () => ({ env: "production" }),
|
|
176
|
+
type: 'sync',
|
|
177
|
+
},
|
|
178
|
+
// Async iterator (generator)
|
|
179
|
+
streamData: {
|
|
180
|
+
fn: async function* (count: number) {
|
|
181
|
+
for (let i = 0; i < count; i++) {
|
|
182
|
+
yield { chunk: i, timestamp: Date.now() };
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
type: 'asyncIterator',
|
|
186
|
+
},
|
|
18
187
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await runtime.eval(`
|
|
191
|
+
const hash = await hashPassword("secret");
|
|
192
|
+
const config = getConfig(); // sync function, no await needed
|
|
193
|
+
|
|
194
|
+
// Consume async iterator
|
|
195
|
+
for await (const data of streamData(5)) {
|
|
196
|
+
console.log(data.chunk); // 0, 1, 2, 3, 4
|
|
197
|
+
}
|
|
198
|
+
`);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Supported Data Types
|
|
202
|
+
|
|
203
|
+
Custom function arguments and return values support the following types:
|
|
204
|
+
|
|
205
|
+
| Category | Types |
|
|
206
|
+
|----------|-------|
|
|
207
|
+
| **Primitives** | `string`, `number`, `boolean`, `null`, `undefined`, `bigint` |
|
|
208
|
+
| **Complex** | `Date`, `RegExp`, `URL`, `Headers` |
|
|
209
|
+
| **Binary** | `Uint8Array`, `ArrayBuffer` |
|
|
210
|
+
| **Web API** | `Request`, `Response`, `File`, `Blob`, `FormData` |
|
|
211
|
+
| **Containers** | Arrays, plain objects (nested) |
|
|
212
|
+
| **Async** | `Promise` (nested), `AsyncIterator` (nested), `Function` (returned) |
|
|
213
|
+
|
|
214
|
+
**Advanced return types:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
const runtime = await createRuntime({
|
|
218
|
+
customFunctions: {
|
|
219
|
+
// Return a function - callable from isolate
|
|
220
|
+
getMultiplier: {
|
|
221
|
+
fn: (factor: number) => (x: number) => x * factor,
|
|
222
|
+
type: 'sync',
|
|
223
|
+
},
|
|
224
|
+
// Return nested promises - awaitable from isolate
|
|
225
|
+
fetchBoth: {
|
|
226
|
+
fn: () => ({
|
|
227
|
+
users: fetch('/api/users').then(r => r.json()),
|
|
228
|
+
posts: fetch('/api/posts').then(r => r.json()),
|
|
229
|
+
}),
|
|
230
|
+
type: 'sync',
|
|
231
|
+
},
|
|
22
232
|
},
|
|
23
233
|
});
|
|
24
234
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// - runtime.tick(): Process pending timers
|
|
29
|
-
// - runtime.dispose(): Clean up all resources
|
|
235
|
+
await runtime.eval(`
|
|
236
|
+
const double = getMultiplier(2);
|
|
237
|
+
console.log(double(5)); // 10
|
|
30
238
|
|
|
31
|
-
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
|
|
239
|
+
const { users, posts } = fetchBoth();
|
|
240
|
+
console.log(await users, await posts);
|
|
241
|
+
`);
|
|
242
|
+
```
|
|
35
243
|
|
|
36
|
-
|
|
37
|
-
|
|
244
|
+
**Unsupported types:**
|
|
245
|
+
- Custom class instances (use plain objects instead)
|
|
246
|
+
- `Symbol`
|
|
247
|
+
- Circular references
|
|
38
248
|
|
|
39
|
-
|
|
40
|
-
|
|
249
|
+
## Test Environment
|
|
250
|
+
|
|
251
|
+
Enable test environment to run tests inside the sandbox:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { createRuntime } from "@ricsam/isolate-runtime";
|
|
255
|
+
|
|
256
|
+
const runtime = await createRuntime({
|
|
257
|
+
testEnvironment: {
|
|
258
|
+
onEvent: (event) => {
|
|
259
|
+
// Receive lifecycle events during test execution
|
|
260
|
+
if (event.type === "testEnd") {
|
|
261
|
+
const icon = event.test.status === "pass" ? "✓" : "✗";
|
|
262
|
+
console.log(`${icon} ${event.test.fullName}`);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await runtime.eval(`
|
|
269
|
+
describe("math", () => {
|
|
270
|
+
it("adds numbers", () => {
|
|
271
|
+
expect(1 + 1).toBe(2);
|
|
272
|
+
});
|
|
273
|
+
it.todo("subtract numbers");
|
|
274
|
+
});
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
// Check if tests exist before running
|
|
278
|
+
if (runtime.testEnvironment.hasTests()) {
|
|
279
|
+
console.log(`Found ${runtime.testEnvironment.getTestCount()} tests`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const results = await runtime.testEnvironment.runTests();
|
|
283
|
+
console.log(`${results.passed}/${results.total} passed, ${results.todo} todo`);
|
|
284
|
+
|
|
285
|
+
// Reset for new tests
|
|
286
|
+
runtime.testEnvironment.reset();
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### TestEnvironmentOptions
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
interface TestEnvironmentOptions {
|
|
293
|
+
onEvent?: (event: TestEvent) => void;
|
|
294
|
+
testTimeout?: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
type TestEvent =
|
|
298
|
+
| { type: "runStart"; testCount: number; suiteCount: number }
|
|
299
|
+
| { type: "suiteStart"; suite: SuiteInfo }
|
|
300
|
+
| { type: "suiteEnd"; suite: SuiteResult }
|
|
301
|
+
| { type: "testStart"; test: TestInfo }
|
|
302
|
+
| { type: "testEnd"; test: TestResult }
|
|
303
|
+
| { type: "runEnd"; results: RunResults };
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Playwright Integration
|
|
307
|
+
|
|
308
|
+
Run browser automation with untrusted code. **You provide the Playwright page object**:
|
|
309
|
+
|
|
310
|
+
### Script Mode (No Tests)
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { createRuntime } from "@ricsam/isolate-runtime";
|
|
314
|
+
import { chromium } from "playwright";
|
|
315
|
+
|
|
316
|
+
const browser = await chromium.launch({ headless: true });
|
|
317
|
+
const page = await browser.newPage();
|
|
318
|
+
|
|
319
|
+
const runtime = await createRuntime({
|
|
320
|
+
playwright: {
|
|
321
|
+
page,
|
|
322
|
+
baseUrl: "https://example.com",
|
|
323
|
+
console: true, // Print browser console to stdout
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Run automation script - no test framework needed
|
|
328
|
+
await runtime.eval(`
|
|
329
|
+
await page.goto("/");
|
|
330
|
+
const title = await page.title();
|
|
331
|
+
console.log("Page title:", title);
|
|
332
|
+
`);
|
|
333
|
+
|
|
334
|
+
// Get collected data
|
|
335
|
+
const data = runtime.playwright.getCollectedData();
|
|
336
|
+
console.log("Network requests:", data.networkRequests);
|
|
337
|
+
|
|
338
|
+
await runtime.dispose();
|
|
339
|
+
await browser.close();
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Test Mode (With Test Framework)
|
|
343
|
+
|
|
344
|
+
Combine `testEnvironment` and `playwright` for browser testing. Playwright extends `expect` with locator matchers:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { createRuntime } from "@ricsam/isolate-runtime";
|
|
348
|
+
import { chromium } from "playwright";
|
|
349
|
+
|
|
350
|
+
const browser = await chromium.launch({ headless: true });
|
|
351
|
+
const page = await browser.newPage();
|
|
352
|
+
|
|
353
|
+
const runtime = await createRuntime({
|
|
354
|
+
testEnvironment: true, // Provides describe, it, expect
|
|
355
|
+
playwright: {
|
|
356
|
+
page,
|
|
357
|
+
baseUrl: "https://example.com",
|
|
358
|
+
onBrowserConsoleLog: (entry) => console.log("[browser]", ...entry.args),
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await runtime.eval(`
|
|
363
|
+
describe("homepage", () => {
|
|
364
|
+
it("loads correctly", async () => {
|
|
365
|
+
await page.goto("/");
|
|
366
|
+
await expect(page.getByText("Example Domain")).toBeVisible(); // Locator matcher
|
|
367
|
+
expect(await page.title()).toBe("Example Domain"); // Primitive matcher
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
`);
|
|
371
|
+
|
|
372
|
+
// Run tests via test-environment
|
|
373
|
+
const results = await runtime.testEnvironment.runTests();
|
|
374
|
+
console.log(`${results.passed}/${results.total} passed`);
|
|
375
|
+
|
|
376
|
+
// Get browser data
|
|
377
|
+
const data = runtime.playwright.getCollectedData();
|
|
378
|
+
console.log("Browser logs:", data.browserConsoleLogs);
|
|
379
|
+
|
|
380
|
+
await runtime.dispose();
|
|
381
|
+
await browser.close();
|
|
41
382
|
```
|
|
42
383
|
|
|
43
|
-
|
|
384
|
+
## Included APIs
|
|
385
|
+
|
|
44
386
|
- Core (Blob, File, streams, URL, TextEncoder/Decoder)
|
|
45
387
|
- Console
|
|
46
388
|
- Encoding (atob/btoa)
|
|
@@ -48,4 +390,23 @@ runtime.dispose();
|
|
|
48
390
|
- Path utilities
|
|
49
391
|
- Crypto (randomUUID, getRandomValues, subtle)
|
|
50
392
|
- Fetch API
|
|
51
|
-
- File System (if handler provided)
|
|
393
|
+
- File System (if handler provided)
|
|
394
|
+
- Test Environment (if enabled)
|
|
395
|
+
- Playwright (if page provided)
|
|
396
|
+
|
|
397
|
+
## Legacy API
|
|
398
|
+
|
|
399
|
+
For backwards compatibility with code that needs direct isolate/context access:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { createLegacyRuntime } from "@ricsam/isolate-runtime";
|
|
403
|
+
|
|
404
|
+
const runtime = await createLegacyRuntime();
|
|
405
|
+
// runtime.isolate and runtime.context are available
|
|
406
|
+
await runtime.context.eval(`console.log("Hello")`);
|
|
407
|
+
runtime.dispose(); // sync
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## License
|
|
411
|
+
|
|
412
|
+
MIT
|