@ricsam/isolate-client 0.1.13 → 0.1.15
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 +237 -8
- package/dist/cjs/connection.cjs +254 -75
- package/dist/cjs/connection.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/connection.mjs +257 -76
- package/dist/mjs/connection.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/types.d.ts +4 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ const runtime = await client.createRuntime({
|
|
|
37
37
|
console: {
|
|
38
38
|
onEntry: (entry) => console.log("[isolate]", entry),
|
|
39
39
|
},
|
|
40
|
-
fetch: async (
|
|
40
|
+
fetch: async (url, init) => fetch(url, init),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
// Execute code (always ES module mode)
|
|
@@ -270,6 +270,93 @@ const runtime = await client.createRuntime({
|
|
|
270
270
|
});
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
+
## WebSocket Client Callback
|
|
274
|
+
|
|
275
|
+
Control outbound WebSocket connections from isolate code. The callback lets you allow, block, or proxy WebSocket connections:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const runtime = await client.createRuntime({
|
|
279
|
+
webSocket: async (url: string, protocols: string[]) => {
|
|
280
|
+
// Block connections to certain hosts
|
|
281
|
+
if (url.includes("blocked.com")) {
|
|
282
|
+
return null; // Connection blocked
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Proxy to a different server
|
|
286
|
+
if (url.includes("internal")) {
|
|
287
|
+
return new WebSocket("wss://proxy.example.com" + new URL(url).pathname);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Allow connection normally
|
|
291
|
+
return new WebSocket(url, protocols.length > 0 ? protocols : undefined);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Isolate code can now use WHATWG WebSocket API
|
|
296
|
+
await runtime.eval(`
|
|
297
|
+
const ws = new WebSocket("wss://api.example.com/stream");
|
|
298
|
+
|
|
299
|
+
ws.onopen = () => {
|
|
300
|
+
console.log("Connected!");
|
|
301
|
+
ws.send("Hello server");
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
ws.onmessage = (event) => {
|
|
305
|
+
console.log("Received:", event.data);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
ws.onclose = (event) => {
|
|
309
|
+
console.log("Closed:", event.code, event.reason);
|
|
310
|
+
};
|
|
311
|
+
`);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### WebSocket Callback Behavior
|
|
315
|
+
|
|
316
|
+
| Return Value | Behavior |
|
|
317
|
+
|--------------|----------|
|
|
318
|
+
| `WebSocket` instance | Use this WebSocket for the connection |
|
|
319
|
+
| `null` | Block the connection (isolate receives error + close events) |
|
|
320
|
+
| `Promise<WebSocket>` | Async - wait for WebSocket |
|
|
321
|
+
| `Promise<null>` | Async - block the connection |
|
|
322
|
+
| Throws/rejects | Block the connection with error |
|
|
323
|
+
|
|
324
|
+
### What "Blocked" Looks Like in the Isolate
|
|
325
|
+
|
|
326
|
+
When a connection is blocked, the isolate sees it as a failed connection (similar to server unreachable):
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const ws = new WebSocket("wss://blocked.com");
|
|
330
|
+
|
|
331
|
+
ws.onerror = (event) => {
|
|
332
|
+
// Fires first
|
|
333
|
+
console.log("Connection failed");
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
ws.onclose = (event) => {
|
|
337
|
+
// Then fires with:
|
|
338
|
+
console.log(event.code); // 1006 (Abnormal Closure)
|
|
339
|
+
console.log(event.reason); // "Connection blocked"
|
|
340
|
+
console.log(event.wasClean); // false
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// ws.onopen never fires
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Default Behavior
|
|
347
|
+
|
|
348
|
+
If no `webSocket` callback is provided, connections are allowed automatically:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// No callback - all WebSocket connections are auto-allowed
|
|
352
|
+
const runtime = await client.createRuntime({});
|
|
353
|
+
|
|
354
|
+
await runtime.eval(`
|
|
355
|
+
// This will connect directly
|
|
356
|
+
const ws = new WebSocket("wss://echo.websocket.org");
|
|
357
|
+
`);
|
|
358
|
+
```
|
|
359
|
+
|
|
273
360
|
## Test Environment
|
|
274
361
|
|
|
275
362
|
Enable test environment to run tests inside the sandbox:
|
|
@@ -327,20 +414,27 @@ type TestEvent =
|
|
|
327
414
|
|
|
328
415
|
## Playwright Integration
|
|
329
416
|
|
|
330
|
-
Run browser automation with untrusted code.
|
|
417
|
+
Run browser automation with untrusted code. Public API is handler-first:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
|
|
421
|
+
|
|
422
|
+
playwright: { handler: defaultPlaywrightHandler(page) }
|
|
423
|
+
```
|
|
331
424
|
|
|
332
425
|
### Script Mode (No Tests)
|
|
333
426
|
|
|
334
427
|
```typescript
|
|
335
428
|
import { chromium } from "playwright";
|
|
429
|
+
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
|
|
336
430
|
|
|
337
431
|
const browser = await chromium.launch({ headless: true });
|
|
338
432
|
const page = await browser.newPage();
|
|
339
433
|
|
|
340
434
|
const runtime = await client.createRuntime({
|
|
341
435
|
playwright: {
|
|
342
|
-
page,
|
|
343
|
-
|
|
436
|
+
handler: defaultPlaywrightHandler(page),
|
|
437
|
+
timeout: 30000, // Default timeout for operations
|
|
344
438
|
onEvent: (event) => {
|
|
345
439
|
// Unified event handler for all playwright events
|
|
346
440
|
if (event.type === "browserConsoleLog") {
|
|
@@ -375,6 +469,7 @@ Combine `testEnvironment` and `playwright` for browser testing. Playwright exten
|
|
|
375
469
|
|
|
376
470
|
```typescript
|
|
377
471
|
import { chromium } from "playwright";
|
|
472
|
+
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
|
|
378
473
|
|
|
379
474
|
const browser = await chromium.launch({ headless: true });
|
|
380
475
|
const page = await browser.newPage();
|
|
@@ -392,8 +487,7 @@ const runtime = await client.createRuntime({
|
|
|
392
487
|
},
|
|
393
488
|
testEnvironment: true, // Provides describe, it, expect
|
|
394
489
|
playwright: {
|
|
395
|
-
page,
|
|
396
|
-
baseUrl: "https://example.com",
|
|
490
|
+
handler: defaultPlaywrightHandler(page),
|
|
397
491
|
console: true, // Routes browser logs through the console handler above
|
|
398
492
|
},
|
|
399
493
|
});
|
|
@@ -420,6 +514,141 @@ await runtime.dispose();
|
|
|
420
514
|
await browser.close();
|
|
421
515
|
```
|
|
422
516
|
|
|
517
|
+
### File Operations (Screenshots, PDFs, File Uploads)
|
|
518
|
+
|
|
519
|
+
For security, file system access requires explicit callbacks. Without these callbacks, operations with file paths will throw errors:
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { chromium } from "playwright";
|
|
523
|
+
import * as fs from "node:fs/promises";
|
|
524
|
+
import * as path from "node:path";
|
|
525
|
+
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
|
|
526
|
+
|
|
527
|
+
const browser = await chromium.launch({ headless: true });
|
|
528
|
+
const page = await browser.newPage();
|
|
529
|
+
|
|
530
|
+
const runtime = await client.createRuntime({
|
|
531
|
+
testEnvironment: true,
|
|
532
|
+
playwright: {
|
|
533
|
+
handler: defaultPlaywrightHandler(page, {
|
|
534
|
+
// Callback for writing screenshots and PDFs to disk
|
|
535
|
+
writeFile: async (filePath: string, data: Buffer) => {
|
|
536
|
+
// Validate path, then write
|
|
537
|
+
if (!filePath.startsWith("/allowed/output/")) {
|
|
538
|
+
throw new Error("Write not allowed to this path");
|
|
539
|
+
}
|
|
540
|
+
await fs.writeFile(filePath, data);
|
|
541
|
+
},
|
|
542
|
+
// Callback for reading files for setInputFiles()
|
|
543
|
+
readFile: async (filePath: string) => {
|
|
544
|
+
// Validate path, then read
|
|
545
|
+
if (!filePath.startsWith("/allowed/uploads/")) {
|
|
546
|
+
throw new Error("Read not allowed from this path");
|
|
547
|
+
}
|
|
548
|
+
const buffer = await fs.readFile(filePath);
|
|
549
|
+
return {
|
|
550
|
+
name: path.basename(filePath),
|
|
551
|
+
mimeType: "application/octet-stream", // Determine from extension
|
|
552
|
+
buffer,
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
}),
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await runtime.eval(`
|
|
560
|
+
test('file operations', async () => {
|
|
561
|
+
await page.goto('data:text/html,<input type="file" id="upload" />');
|
|
562
|
+
|
|
563
|
+
// Screenshot with path - calls writeFile callback
|
|
564
|
+
const base64 = await page.screenshot({ path: '/allowed/output/screenshot.png' });
|
|
565
|
+
// base64 is always returned, writeFile is called additionally
|
|
566
|
+
|
|
567
|
+
// PDF with path - calls writeFile callback
|
|
568
|
+
await page.pdf({ path: '/allowed/output/document.pdf' });
|
|
569
|
+
|
|
570
|
+
// File upload with path - calls readFile callback
|
|
571
|
+
await page.locator('#upload').setInputFiles('/allowed/uploads/test.txt');
|
|
572
|
+
|
|
573
|
+
// File upload with buffer data - no callback needed
|
|
574
|
+
await page.locator('#upload').setInputFiles([{
|
|
575
|
+
name: 'inline.txt',
|
|
576
|
+
mimeType: 'text/plain',
|
|
577
|
+
buffer: new TextEncoder().encode('Hello, World!'),
|
|
578
|
+
}]);
|
|
579
|
+
});
|
|
580
|
+
`);
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Behavior without callbacks:**
|
|
584
|
+
- `screenshot()` / `pdf()` without path: Returns base64 string (works without callback)
|
|
585
|
+
- `screenshot({ path })` / `pdf({ path })` without `writeFile`: Throws error
|
|
586
|
+
- `setInputFiles('/path')` without `readFile`: Throws error
|
|
587
|
+
- `setInputFiles([{ name, mimeType, buffer }])`: Works without callback (inline data)
|
|
588
|
+
|
|
589
|
+
### Multi-Page Testing
|
|
590
|
+
|
|
591
|
+
For tests that need multiple pages or browser contexts, provide `createPage` and/or `createContext` callbacks:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import { chromium } from "playwright";
|
|
595
|
+
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
|
|
596
|
+
|
|
597
|
+
const browser = await chromium.launch({ headless: true });
|
|
598
|
+
const browserContext = await browser.newContext();
|
|
599
|
+
const page = await browserContext.newPage();
|
|
600
|
+
|
|
601
|
+
const runtime = await client.createRuntime({
|
|
602
|
+
testEnvironment: true,
|
|
603
|
+
playwright: {
|
|
604
|
+
handler: defaultPlaywrightHandler(page, {
|
|
605
|
+
// Called when isolate code calls context.newPage(); receive the BrowserContext and call context.newPage()
|
|
606
|
+
createPage: async (context) => context.newPage(),
|
|
607
|
+
// Called when isolate code calls browser.newContext()
|
|
608
|
+
createContext: async (options) => browser.newContext(options),
|
|
609
|
+
}),
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await runtime.eval(`
|
|
614
|
+
test('multi-page test', async () => {
|
|
615
|
+
// Create additional pages
|
|
616
|
+
const page2 = await context.newPage();
|
|
617
|
+
|
|
618
|
+
// Navigate independently
|
|
619
|
+
await page.goto('https://example.com/page1');
|
|
620
|
+
await page2.goto('https://example.com/page2');
|
|
621
|
+
|
|
622
|
+
// Work with multiple pages
|
|
623
|
+
await page.locator('#button').click();
|
|
624
|
+
await page2.locator('#input').fill('text');
|
|
625
|
+
|
|
626
|
+
await page2.close();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test('multi-context test', async () => {
|
|
630
|
+
// Create isolated context (separate cookies, storage)
|
|
631
|
+
const ctx2 = await browser.newContext();
|
|
632
|
+
const page2 = await ctx2.newPage();
|
|
633
|
+
|
|
634
|
+
// Cookies are isolated between contexts
|
|
635
|
+
await context.addCookies([{ name: 'test', value: '1', domain: 'example.com', path: '/' }]);
|
|
636
|
+
const ctx1Cookies = await context.cookies();
|
|
637
|
+
const ctx2Cookies = await ctx2.cookies();
|
|
638
|
+
|
|
639
|
+
expect(ctx1Cookies.some(c => c.name === 'test')).toBe(true);
|
|
640
|
+
expect(ctx2Cookies.some(c => c.name === 'test')).toBe(false);
|
|
641
|
+
|
|
642
|
+
await ctx2.close();
|
|
643
|
+
});
|
|
644
|
+
`);
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Behavior without lifecycle callbacks:**
|
|
648
|
+
- `context.newPage()` without `createPage`: Throws error
|
|
649
|
+
- `browser.newContext()` without `createContext`: Throws error
|
|
650
|
+
- `context.cookies()`, `context.addCookies()`, `context.clearCookies()`: Work without callbacks
|
|
651
|
+
|
|
423
652
|
## Runtime Interface
|
|
424
653
|
|
|
425
654
|
```typescript
|
|
@@ -478,8 +707,8 @@ interface RemoteTestEnvironmentHandle {
|
|
|
478
707
|
}
|
|
479
708
|
|
|
480
709
|
interface RemotePlaywrightHandle {
|
|
481
|
-
getCollectedData():
|
|
482
|
-
clearCollectedData():
|
|
710
|
+
getCollectedData(): CollectedData;
|
|
711
|
+
clearCollectedData(): void;
|
|
483
712
|
}
|
|
484
713
|
```
|
|
485
714
|
|