@showrun/core 0.1.10 → 0.2.0-rc.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=playwrightJsExecutor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwrightJsExecutor.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/playwrightJsExecutor.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { executePlaywrightJs, extractFunctionBody } from '../dsl/playwrightJsExecutor.js';
3
+ // Minimal mock page/context/frame for testing
4
+ function createMockScope(overrides) {
5
+ const mockPage = {
6
+ goto: vi.fn().mockResolvedValue(undefined),
7
+ title: vi.fn().mockResolvedValue('Test Title'),
8
+ url: vi.fn().mockReturnValue('https://example.com'),
9
+ locator: vi.fn().mockReturnValue({
10
+ evaluateAll: vi.fn().mockResolvedValue([]),
11
+ }),
12
+ screenshot: vi.fn().mockResolvedValue(Buffer.from('')),
13
+ mouse: { click: vi.fn().mockResolvedValue(undefined) },
14
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
15
+ };
16
+ return {
17
+ page: mockPage,
18
+ context: {},
19
+ frame: {},
20
+ inputs: { query: 'hello' },
21
+ secrets: { apiKey: 'secret123' },
22
+ showrun: {
23
+ network: {
24
+ list: vi.fn().mockResolvedValue([]),
25
+ find: vi.fn().mockResolvedValue(null),
26
+ get: vi.fn().mockResolvedValue(null),
27
+ replay: vi.fn().mockResolvedValue({ status: 200, contentType: 'application/json', body: '{}', bodySize: 2 }),
28
+ },
29
+ },
30
+ util: {
31
+ detectCloudflareTurnstile: vi.fn().mockResolvedValue({ found: false }),
32
+ solveCloudflareTurnstile: vi.fn().mockResolvedValue({ success: false, detected: false }),
33
+ },
34
+ ...overrides,
35
+ };
36
+ }
37
+ describe('extractFunctionBody', () => {
38
+ it('extracts body from async function export', () => {
39
+ const code = `module.exports = async function({ page }) {
40
+ await page.goto('https://example.com');
41
+ return { title: 'test' };
42
+ };`;
43
+ const body = extractFunctionBody(code);
44
+ expect(body).toContain("await page.goto('https://example.com')");
45
+ expect(body).toContain("return { title: 'test' }");
46
+ });
47
+ it('extracts body from async arrow export', () => {
48
+ const code = `module.exports = async ({ page }) => {
49
+ return { title: await page.title() };
50
+ };`;
51
+ const body = extractFunctionBody(code);
52
+ expect(body).toContain('return { title: await page.title() }');
53
+ });
54
+ it('throws on invalid format', () => {
55
+ expect(() => extractFunctionBody('const x = 1;')).toThrow('Could not parse flow.playwright.js');
56
+ });
57
+ });
58
+ describe('executePlaywrightJs', () => {
59
+ it('returns collectibles from user code', async () => {
60
+ const code = `module.exports = async function({ page, inputs }) {
61
+ return { result: inputs.query + '_processed' };
62
+ };`;
63
+ const scope = createMockScope();
64
+ const result = await executePlaywrightJs(code, scope);
65
+ expect(result.collectibles).toEqual({ result: 'hello_processed' });
66
+ expect(result.logs).toEqual([]);
67
+ });
68
+ it('can call page methods', async () => {
69
+ const code = `module.exports = async function({ page }) {
70
+ const t = await page.title();
71
+ return { title: t };
72
+ };`;
73
+ const scope = createMockScope();
74
+ const result = await executePlaywrightJs(code, scope);
75
+ expect(result.collectibles).toEqual({ title: 'Test Title' });
76
+ expect(scope.page.title).toHaveBeenCalled();
77
+ });
78
+ it('blocks dangerous globals', async () => {
79
+ const code = `module.exports = async function({ page }) {
80
+ return {
81
+ hasProcess: typeof process !== 'undefined',
82
+ hasRequire: typeof require !== 'undefined',
83
+ hasBuffer: typeof Buffer !== 'undefined',
84
+ hasGlobal: typeof global !== 'undefined',
85
+ hasGlobalThis: typeof globalThis !== 'undefined',
86
+ hasFetch: typeof fetch !== 'undefined',
87
+ hasEval: typeof eval !== 'undefined',
88
+ hasFunction: typeof Function !== 'undefined',
89
+ hasSetTimeout: typeof setTimeout !== 'undefined',
90
+ };
91
+ };`;
92
+ const scope = createMockScope();
93
+ const result = await executePlaywrightJs(code, scope);
94
+ expect(result.collectibles.hasProcess).toBe(false);
95
+ expect(result.collectibles.hasRequire).toBe(false);
96
+ expect(result.collectibles.hasBuffer).toBe(false);
97
+ expect(result.collectibles.hasGlobal).toBe(false);
98
+ expect(result.collectibles.hasGlobalThis).toBe(false);
99
+ expect(result.collectibles.hasFetch).toBe(false);
100
+ expect(result.collectibles.hasEval).toBe(false);
101
+ expect(result.collectibles.hasFunction).toBe(false);
102
+ expect(result.collectibles.hasSetTimeout).toBe(false);
103
+ });
104
+ it('freezes inputs (mutations do not propagate)', async () => {
105
+ const code = `module.exports = async function({ inputs }) {
106
+ inputs.query = 'modified';
107
+ return { value: inputs.query };
108
+ };`;
109
+ const scope = createMockScope();
110
+ const result = await executePlaywrightJs(code, scope);
111
+ // Frozen object silently ignores assignment in sloppy mode
112
+ expect(result.collectibles.value).toBe('hello');
113
+ // Original scope inputs are also unmodified
114
+ expect(scope.inputs.query).toBe('hello');
115
+ });
116
+ it('freezes secrets (mutations do not propagate)', async () => {
117
+ const code = `module.exports = async function({ secrets }) {
118
+ secrets.apiKey = 'hacked';
119
+ return { value: secrets.apiKey };
120
+ };`;
121
+ const scope = createMockScope();
122
+ const result = await executePlaywrightJs(code, scope);
123
+ expect(result.collectibles.value).toBe('secret123');
124
+ expect(scope.secrets.apiKey).toBe('secret123');
125
+ });
126
+ it('returns empty object when code returns undefined', async () => {
127
+ const code = `module.exports = async function({ page }) {
128
+ // no return
129
+ };`;
130
+ const scope = createMockScope();
131
+ const result = await executePlaywrightJs(code, scope);
132
+ expect(result.collectibles).toEqual({});
133
+ });
134
+ it('can access showrun.network.replay', async () => {
135
+ const code = `module.exports = async function({ showrun }) {
136
+ const res = await showrun.network.replay('req-1');
137
+ return { status: res.status };
138
+ };`;
139
+ const scope = createMockScope();
140
+ const result = await executePlaywrightJs(code, scope);
141
+ expect(result.collectibles).toEqual({ status: 200 });
142
+ expect(scope.showrun.network.replay).toHaveBeenCalledWith('req-1');
143
+ });
144
+ it('can access showrun.network.list', async () => {
145
+ const code = `module.exports = async function({ showrun }) {
146
+ const entries = await showrun.network.list();
147
+ return { count: entries.length };
148
+ };`;
149
+ const scope = createMockScope();
150
+ const result = await executePlaywrightJs(code, scope);
151
+ expect(result.collectibles).toEqual({ count: 0 });
152
+ expect(scope.showrun.network.list).toHaveBeenCalled();
153
+ });
154
+ it('can access showrun.network.find', async () => {
155
+ const code = `module.exports = async function({ showrun }) {
156
+ const entry = await showrun.network.find({ urlIncludes: '/api' });
157
+ return { found: entry !== null };
158
+ };`;
159
+ const scope = createMockScope();
160
+ const result = await executePlaywrightJs(code, scope);
161
+ expect(result.collectibles).toEqual({ found: false });
162
+ expect(scope.showrun.network.find).toHaveBeenCalledWith({ urlIncludes: '/api' });
163
+ });
164
+ it('captures console.log output', async () => {
165
+ const code = `module.exports = async function({ page }) {
166
+ console.log('hello', 'world');
167
+ console.warn('a warning');
168
+ console.error('an error');
169
+ console.info('info message');
170
+ return { done: true };
171
+ };`;
172
+ const scope = createMockScope();
173
+ const result = await executePlaywrightJs(code, scope);
174
+ expect(result.collectibles).toEqual({ done: true });
175
+ expect(result.logs).toEqual([
176
+ 'hello world',
177
+ '[warn] a warning',
178
+ '[error] an error',
179
+ 'info message',
180
+ ]);
181
+ });
182
+ it('times out on long-running code', async () => {
183
+ const code = `module.exports = async function({ page }) {
184
+ await new Promise(() => {}); // never resolves
185
+ return {};
186
+ };`;
187
+ const scope = createMockScope();
188
+ await expect(executePlaywrightJs(code, scope, 100) // 100ms timeout
189
+ ).rejects.toThrow('timed out');
190
+ });
191
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { mkdirSync, rmSync, statSync } from 'fs';
2
+ import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, statSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir, platform } from 'os';
5
5
  import { randomBytes } from 'crypto';
@@ -90,11 +90,18 @@ describe('tokenStore', () => {
90
90
  });
91
91
  // ── RegistryClient tests ──────────────────────────────────────────────────
92
92
  describe('RegistryClient', () => {
93
+ let tempDir;
93
94
  let originalEnv;
94
95
  beforeEach(() => {
96
+ tempDir = join(tmpdir(), `showrun-registry-client-${randomBytes(6).toString('hex')}`);
97
+ mkdirSync(tempDir, { recursive: true });
95
98
  originalEnv = {
96
99
  SHOWRUN_REGISTRY_URL: process.env.SHOWRUN_REGISTRY_URL,
100
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
101
+ APPDATA: process.env.APPDATA,
102
+ HOME: process.env.HOME,
97
103
  };
104
+ process.env.XDG_CONFIG_HOME = tempDir;
98
105
  });
99
106
  afterEach(() => {
100
107
  for (const [key, value] of Object.entries(originalEnv)) {
@@ -105,6 +112,7 @@ describe('RegistryClient', () => {
105
112
  process.env[key] = value;
106
113
  }
107
114
  }
115
+ rmSync(tempDir, { recursive: true, force: true });
108
116
  vi.restoreAllMocks();
109
117
  });
110
118
  it('falls back to default registry URL when not configured', async () => {
@@ -225,4 +233,164 @@ describe('RegistryClient', () => {
225
233
  return client.searchPacks({ q: 'fail' });
226
234
  })()).rejects.toThrow(RegistryError);
227
235
  });
236
+ it('publishPack updates showrunVersions with the current ShowRun version', async () => {
237
+ const tempDir = join(tmpdir(), `showrun-registry-publish-${randomBytes(6).toString('hex')}`);
238
+ mkdirSync(tempDir, { recursive: true });
239
+ const packDir = join(tempDir, 'example-pack');
240
+ mkdirSync(packDir, { recursive: true });
241
+ writeFileSync(join(packDir, 'taskpack.json'), JSON.stringify({
242
+ id: 'example-pack',
243
+ name: 'Example Pack',
244
+ version: '1.0.0',
245
+ kind: 'playwright-js',
246
+ description: 'Example pack',
247
+ showrunVersions: ['0.1.0'],
248
+ inputs: {},
249
+ collectibles: [],
250
+ }, null, 2));
251
+ writeFileSync(join(packDir, 'flow.playwright.js'), 'module.exports = async function() { return {}; };\n');
252
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
253
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
254
+ fetchSpy
255
+ .mockResolvedValueOnce(new Response(JSON.stringify({ message: 'Not Found' }), {
256
+ status: 404,
257
+ headers: { 'Content-Type': 'application/json' },
258
+ }))
259
+ .mockResolvedValueOnce(new Response(JSON.stringify({ id: 'pack-1' }), {
260
+ status: 200,
261
+ headers: { 'Content-Type': 'application/json' },
262
+ }))
263
+ .mockResolvedValueOnce(new Response(JSON.stringify({ version: '1.0.0' }), {
264
+ status: 200,
265
+ headers: { 'Content-Type': 'application/json' },
266
+ }));
267
+ try {
268
+ const { RegistryClient } = await import('../registry/client.js');
269
+ const { saveTokens } = await import('../registry/tokenStore.js');
270
+ saveTokens({
271
+ accessToken: 'header.eyJleHAiOjk5OTk5OTk5OTl9.signature',
272
+ refreshToken: 'rt-mock',
273
+ user: { id: '1', username: 'alice', email: 'alice@example.com' },
274
+ registryUrl: 'https://registry.example.com',
275
+ savedAt: new Date().toISOString(),
276
+ });
277
+ const client = new RegistryClient('https://registry.example.com');
278
+ await client.publishPack({ packPath: packDir, slug: 'example-pack' });
279
+ const writtenManifest = JSON.parse(readFileSync(join(packDir, 'taskpack.json'), 'utf-8'));
280
+ expect(writtenManifest.showrunVersions).toContain('0.2.0-rc.0');
281
+ expect(writtenManifest.showrunVersions).toContain('0.1.0');
282
+ const publishCall = fetchSpy.mock.calls.at(-1);
283
+ const publishBody = JSON.parse(String(publishCall[1].body));
284
+ expect(publishBody.manifest.showrunVersions).toContain('0.2.0-rc.0');
285
+ expect(warnSpy).not.toHaveBeenCalled();
286
+ }
287
+ finally {
288
+ rmSync(tempDir, { recursive: true, force: true });
289
+ }
290
+ });
291
+ it('installs playwright-js packs using flow.playwright.js', async () => {
292
+ const tempDir = join(tmpdir(), `showrun-registry-install-${randomBytes(6).toString('hex')}`);
293
+ mkdirSync(tempDir, { recursive: true });
294
+ const manifest = {
295
+ id: 'example-pack',
296
+ name: 'Example Pack',
297
+ version: '1.2.3',
298
+ kind: 'playwright-js',
299
+ description: 'Example playwright-js pack',
300
+ inputs: {
301
+ query: { type: 'string', required: true },
302
+ },
303
+ collectibles: [
304
+ { name: 'items', type: 'array' },
305
+ ],
306
+ };
307
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
308
+ fetchSpy
309
+ .mockResolvedValueOnce(new Response(JSON.stringify({
310
+ id: 'pack-1',
311
+ slug: '@alice/example-pack',
312
+ name: 'Example Pack',
313
+ description: 'Example playwright-js pack',
314
+ visibility: 'public',
315
+ latestVersion: '1.2.3',
316
+ owner: { id: 'user-1', username: 'alice' },
317
+ createdAt: '2026-03-12T00:00:00.000Z',
318
+ updatedAt: '2026-03-12T00:00:00.000Z',
319
+ versions: [{ version: '1.2.3', createdAt: '2026-03-12T00:00:00.000Z' }],
320
+ }), {
321
+ status: 200,
322
+ headers: { 'Content-Type': 'application/json' },
323
+ }))
324
+ .mockResolvedValueOnce(new Response(JSON.stringify({
325
+ manifest,
326
+ playwrightJsSource: 'module.exports = async function() { return { items: [] }; };\n',
327
+ }), {
328
+ status: 200,
329
+ headers: { 'Content-Type': 'application/json' },
330
+ }));
331
+ try {
332
+ const { RegistryClient } = await import('../registry/client.js');
333
+ const client = new RegistryClient('https://registry.example.com');
334
+ await client.installPack('@alice/example-pack', tempDir);
335
+ const packDir = join(tempDir, '@alice/example-pack');
336
+ expect(existsSync(join(packDir, 'taskpack.json'))).toBe(true);
337
+ expect(existsSync(join(packDir, 'flow.playwright.js'))).toBe(true);
338
+ expect(existsSync(join(packDir, 'flow.json'))).toBe(false);
339
+ const installedManifest = JSON.parse(readFileSync(join(packDir, 'taskpack.json'), 'utf-8'));
340
+ expect(installedManifest.kind).toBe('playwright-js');
341
+ expect(readFileSync(join(packDir, 'flow.playwright.js'), 'utf-8')).toContain('return { items: [] }');
342
+ }
343
+ finally {
344
+ rmSync(tempDir, { recursive: true, force: true });
345
+ }
346
+ });
347
+ it('warns when installed pack showrunVersions do not include the current version', async () => {
348
+ const tempDir = join(tmpdir(), `showrun-registry-warn-${randomBytes(6).toString('hex')}`);
349
+ mkdirSync(tempDir, { recursive: true });
350
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
351
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
352
+ fetchSpy
353
+ .mockResolvedValueOnce(new Response(JSON.stringify({
354
+ id: 'pack-1',
355
+ slug: '@alice/example-pack',
356
+ name: 'Example Pack',
357
+ description: 'Example playwright-js pack',
358
+ visibility: 'public',
359
+ latestVersion: '1.2.3',
360
+ owner: { id: 'user-1', username: 'alice' },
361
+ createdAt: '2026-03-12T00:00:00.000Z',
362
+ updatedAt: '2026-03-12T00:00:00.000Z',
363
+ versions: [{ version: '1.2.3', createdAt: '2026-03-12T00:00:00.000Z' }],
364
+ }), {
365
+ status: 200,
366
+ headers: { 'Content-Type': 'application/json' },
367
+ }))
368
+ .mockResolvedValueOnce(new Response(JSON.stringify({
369
+ manifest: {
370
+ id: 'example-pack',
371
+ name: 'Example Pack',
372
+ version: '1.2.3',
373
+ kind: 'playwright-js',
374
+ description: 'Example playwright-js pack',
375
+ showrunVersions: ['0.1.0'],
376
+ inputs: {},
377
+ collectibles: [],
378
+ },
379
+ playwrightJsSource: 'module.exports = async function() { return {}; };\n',
380
+ }), {
381
+ status: 200,
382
+ headers: { 'Content-Type': 'application/json' },
383
+ }));
384
+ try {
385
+ const { RegistryClient } = await import('../registry/client.js');
386
+ const client = new RegistryClient('https://registry.example.com');
387
+ await client.installPack('@alice/example-pack', tempDir);
388
+ expect(warnSpy).toHaveBeenCalledOnce();
389
+ expect(warnSpy.mock.calls[0][0]).toContain('declares compatibility with ShowRun 0.1.0');
390
+ expect(warnSpy.mock.calls[0][0]).toContain('current version is 0.2.0-rc.0');
391
+ }
392
+ finally {
393
+ rmSync(tempDir, { recursive: true, force: true });
394
+ }
395
+ });
228
396
  });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Sandboxed executor for playwright-js task packs.
3
+ *
4
+ * Runs user-provided Playwright JavaScript in a best-effort sandbox:
5
+ * - Dangerous Node.js globals are shadowed via AsyncFunction parameters
6
+ * - inputs and secrets are passed as frozen copies (read-only)
7
+ * - page, context, frame have full Playwright access
8
+ *
9
+ * NOTE: This is NOT a hard security boundary. A determined attacker could
10
+ * escape via prototype chain tricks. Trust is managed at the registry level.
11
+ */
12
+ import type { Page, BrowserContext, Frame } from 'playwright';
13
+ import type { NetworkCaptureApi } from '../networkCapture.js';
14
+ import { type PlaywrightJsUtil } from '../util/index.js';
15
+ /**
16
+ * Scope provided to user code.
17
+ */
18
+ export interface PlaywrightJsScope {
19
+ page: Page;
20
+ context: BrowserContext;
21
+ frame: Frame;
22
+ inputs: Record<string, unknown>;
23
+ secrets: Record<string, string>;
24
+ showrun: {
25
+ network: {
26
+ list: NetworkCaptureApi['list'];
27
+ find: NetworkCaptureApi['find'];
28
+ get: NetworkCaptureApi['get'];
29
+ replay: NetworkCaptureApi['replay'];
30
+ };
31
+ };
32
+ util: PlaywrightJsUtil;
33
+ }
34
+ /**
35
+ * Result from executing a playwright-js flow.
36
+ */
37
+ export interface PlaywrightJsResult {
38
+ collectibles: Record<string, unknown>;
39
+ logs: string[];
40
+ error?: string;
41
+ }
42
+ /**
43
+ * Extract the function body from a `module.exports = async function(...) { BODY }` pattern.
44
+ * Also supports `module.exports = async (...) => { BODY }` arrow functions.
45
+ */
46
+ export declare function extractFunctionBody(code: string): string;
47
+ /**
48
+ * Execute a playwright-js flow in a sandboxed context.
49
+ *
50
+ * @param code Raw source of flow.playwright.js
51
+ * @param scope Playwright objects + inputs/secrets
52
+ * @param timeoutMs Maximum execution time (default 5 minutes)
53
+ * @returns The return value from the user function (collectibles object)
54
+ */
55
+ export declare function executePlaywrightJs(code: string, scope: PlaywrightJsScope, timeoutMs?: number): Promise<PlaywrightJsResult>;
56
+ //# sourceMappingURL=playwrightJsExecutor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwrightJsExecutor.d.ts","sourceRoot":"","sources":["../../src/dsl/playwrightJsExecutor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAajF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE;QACP,OAAO,EAAE;YACP,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;YAChC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;YAChC,GAAG,EAAE,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC9B,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC;SACrC,CAAC;KACH,CAAC;IACF,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+BxD;AAMD;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,iBAAiB,EACxB,SAAS,SAAgB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CA4E7B"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Sandboxed executor for playwright-js task packs.
3
+ *
4
+ * Runs user-provided Playwright JavaScript in a best-effort sandbox:
5
+ * - Dangerous Node.js globals are shadowed via AsyncFunction parameters
6
+ * - inputs and secrets are passed as frozen copies (read-only)
7
+ * - page, context, frame have full Playwright access
8
+ *
9
+ * NOTE: This is NOT a hard security boundary. A determined attacker could
10
+ * escape via prototype chain tricks. Trust is managed at the registry level.
11
+ */
12
+ /**
13
+ * Globals shadowed inside user code (passed as undefined).
14
+ */
15
+ const BLOCKED_GLOBALS = [
16
+ 'process', 'require', 'module', 'exports',
17
+ '__dirname', '__filename', 'global', 'globalThis',
18
+ 'Buffer', 'setTimeout', 'setInterval', 'setImmediate',
19
+ 'clearTimeout', 'clearInterval', 'clearImmediate',
20
+ 'fetch', 'XMLHttpRequest', 'eval', 'Function', 'Deno', 'Bun',
21
+ ];
22
+ /**
23
+ * Extract the function body from a `module.exports = async function(...) { BODY }` pattern.
24
+ * Also supports `module.exports = async (...) => { BODY }` arrow functions.
25
+ */
26
+ export function extractFunctionBody(code) {
27
+ // Match module.exports = async function(...) { BODY }
28
+ // or module.exports = async (...) => { BODY }
29
+ const patterns = [
30
+ // async function with destructuring or params
31
+ /module\.exports\s*=\s*async\s+function\s*\([^)]*\)\s*\{/,
32
+ // async arrow with destructuring or params
33
+ /module\.exports\s*=\s*async\s*\([^)]*\)\s*=>\s*\{/,
34
+ ];
35
+ for (const pattern of patterns) {
36
+ const match = code.match(pattern);
37
+ if (match) {
38
+ const startIdx = match.index + match[0].length;
39
+ // Find matching closing brace
40
+ let depth = 1;
41
+ let i = startIdx;
42
+ while (i < code.length && depth > 0) {
43
+ if (code[i] === '{')
44
+ depth++;
45
+ else if (code[i] === '}')
46
+ depth--;
47
+ i++;
48
+ }
49
+ if (depth === 0) {
50
+ return code.slice(startIdx, i - 1).trim();
51
+ }
52
+ }
53
+ }
54
+ throw new Error('Could not parse flow.playwright.js: expected `module.exports = async function({ page, context, frame, inputs, secrets }) { ... }` pattern.');
55
+ }
56
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
57
+ const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
58
+ /**
59
+ * Execute a playwright-js flow in a sandboxed context.
60
+ *
61
+ * @param code Raw source of flow.playwright.js
62
+ * @param scope Playwright objects + inputs/secrets
63
+ * @param timeoutMs Maximum execution time (default 5 minutes)
64
+ * @returns The return value from the user function (collectibles object)
65
+ */
66
+ export async function executePlaywrightJs(code, scope, timeoutMs = 5 * 60 * 1000) {
67
+ const functionBody = extractFunctionBody(code);
68
+ // Capture console.log output from user code
69
+ const logs = [];
70
+ const customConsole = {
71
+ log: (...args) => logs.push(args.map(String).join(' ')),
72
+ warn: (...args) => logs.push(`[warn] ${args.map(String).join(' ')}`),
73
+ error: (...args) => logs.push(`[error] ${args.map(String).join(' ')}`),
74
+ info: (...args) => logs.push(args.map(String).join(' ')),
75
+ };
76
+ // Build the sandboxed function:
77
+ // Blocked globals are function parameters, passed as undefined to shadow them.
78
+ // Scope variables (page, context, etc.) are also parameters.
79
+ const fn = new AsyncFunction(...BLOCKED_GLOBALS, 'page', 'context', 'frame', 'inputs', 'secrets', 'showrun', 'util', 'console', functionBody);
80
+ // Freeze inputs and secrets so user code cannot mutate them
81
+ const frozenInputs = Object.freeze({ ...scope.inputs });
82
+ const frozenSecrets = Object.freeze({ ...scope.secrets });
83
+ // Build args: undefined for each blocked global, then scope values
84
+ const blockedArgs = BLOCKED_GLOBALS.map(() => undefined);
85
+ const args = [
86
+ ...blockedArgs,
87
+ scope.page,
88
+ scope.context,
89
+ scope.frame,
90
+ frozenInputs,
91
+ frozenSecrets,
92
+ scope.showrun,
93
+ scope.util,
94
+ customConsole,
95
+ ];
96
+ // Execute with timeout, wrapped in try-catch to handle browser/page closure errors
97
+ let result;
98
+ try {
99
+ result = await Promise.race([
100
+ fn(...args),
101
+ new Promise((_, reject) => {
102
+ const timer = globalThis.setTimeout(() => {
103
+ reject(new Error(`playwright-js execution timed out after ${timeoutMs}ms`));
104
+ }, timeoutMs);
105
+ // Allow Node.js to exit even if timer is pending
106
+ if (typeof timer === 'object' && 'unref' in timer) {
107
+ timer.unref();
108
+ }
109
+ }),
110
+ ]);
111
+ }
112
+ catch (error) {
113
+ // Handle flow execution errors gracefully instead of crashing
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ logs.push(`[error] Flow execution failed: ${errorMessage}`);
116
+ return {
117
+ collectibles: {},
118
+ logs,
119
+ error: errorMessage,
120
+ };
121
+ }
122
+ // Normalize result: if null/undefined, return empty object
123
+ let collectibles;
124
+ if (result == null) {
125
+ collectibles = {};
126
+ }
127
+ else if (typeof result !== 'object' || Array.isArray(result)) {
128
+ collectibles = { _result: result };
129
+ }
130
+ else {
131
+ collectibles = result;
132
+ }
133
+ return { collectibles, logs };
134
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from './packVersioning.js';
13
13
  export * from './config.js';
14
14
  export * from './requestSnapshot.js';
15
15
  export * from './httpReplay.js';
16
+ export * from './version.js';
16
17
  export * from './registry/index.js';
17
18
  export * from './proxy/index.js';
18
19
  export * from './transport/index.js';
@@ -24,4 +25,6 @@ export * from './dsl/validation.js';
24
25
  export * from './dsl/templating.js';
25
26
  export * from './dsl/target.js';
26
27
  export * from './dsl/conditions.js';
28
+ export * from './dsl/playwrightJsExecutor.js';
29
+ export * from './util/index.js';
27
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAGhC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAG7B,cAAc,qBAAqB,CAAC;AAGpC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * from './packVersioning.js';
13
13
  export * from './config.js';
14
14
  export * from './requestSnapshot.js';
15
15
  export * from './httpReplay.js';
16
+ export * from './version.js';
16
17
  // Registry exports
17
18
  export * from './registry/index.js';
18
19
  // Proxy exports
@@ -29,3 +30,5 @@ export * from './dsl/validation.js';
29
30
  export * from './dsl/templating.js';
30
31
  export * from './dsl/target.js';
31
32
  export * from './dsl/conditions.js';
33
+ export * from './dsl/playwrightJsExecutor.js';
34
+ export * from './util/index.js';
package/dist/loader.d.ts CHANGED
@@ -19,9 +19,17 @@ export declare class TaskPackLoader {
19
19
  */
20
20
  static loadManifest(packPath: string): TaskPackManifest;
21
21
  /**
22
- * Load task pack from directory (json-dsl format only)
22
+ * Load task pack from directory
23
23
  */
24
24
  static loadTaskPack(packPath: string): Promise<TaskPack>;
25
+ /**
26
+ * Load a json-dsl task pack
27
+ */
28
+ private static loadJsonDslPack;
29
+ /**
30
+ * Load a playwright-js task pack
31
+ */
32
+ private static loadPlaywrightJsPack;
25
33
  /**
26
34
  * Load secrets from .secrets.json file in pack directory
27
35
  * Returns empty object if file doesn't exist
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAsC,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB;IA4BvD;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4C9D;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAwB5D;;OAEG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAQlE"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAsC,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB;IA6BvD;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAU9D;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IA0C9B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAyBnC;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAwB5D;;OAEG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAQlE"}