playwriter 0.2.0 → 0.3.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.
Files changed (54) hide show
  1. package/dist/bippy.js +1 -1
  2. package/dist/cdp-log.d.ts +4 -1
  3. package/dist/cdp-log.d.ts.map +1 -1
  4. package/dist/cdp-log.js +39 -2
  5. package/dist/cdp-log.js.map +1 -1
  6. package/dist/cdp-log.test.d.ts +2 -0
  7. package/dist/cdp-log.test.d.ts.map +1 -0
  8. package/dist/cdp-log.test.js +109 -0
  9. package/dist/cdp-log.test.js.map +1 -0
  10. package/dist/cdp-relay.d.ts.map +1 -1
  11. package/dist/cdp-relay.js +99 -6
  12. package/dist/cdp-relay.js.map +1 -1
  13. package/dist/cli.js +14 -12
  14. package/dist/cli.js.map +1 -1
  15. package/dist/executor.d.ts +3 -0
  16. package/dist/executor.d.ts.map +1 -1
  17. package/dist/executor.js +106 -36
  18. package/dist/executor.js.map +1 -1
  19. package/dist/extension/background.js +23 -12
  20. package/dist/extension/manifest.json +1 -1
  21. package/dist/prompt.md +32 -13
  22. package/dist/readability.js +1 -1
  23. package/dist/relay-client.d.ts +11 -0
  24. package/dist/relay-client.d.ts.map +1 -1
  25. package/dist/relay-client.js +46 -1
  26. package/dist/relay-client.js.map +1 -1
  27. package/dist/relay-core.test.js +10 -6
  28. package/dist/relay-core.test.js.map +1 -1
  29. package/dist/relay-session.test.js +9 -1
  30. package/dist/relay-session.test.js.map +1 -1
  31. package/dist/relay-state.test.js +57 -1
  32. package/dist/relay-state.test.js.map +1 -1
  33. package/dist/selector-generator.js +1 -1
  34. package/dist/start-relay-server.d.ts +1 -1
  35. package/dist/start-relay-server.d.ts.map +1 -1
  36. package/dist/start-relay-server.js +23 -1
  37. package/dist/start-relay-server.js.map +1 -1
  38. package/dist/utils.d.ts +1 -0
  39. package/dist/utils.d.ts.map +1 -1
  40. package/dist/utils.js +3 -0
  41. package/dist/utils.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/cdp-log.test.ts +131 -0
  44. package/src/cdp-log.ts +44 -2
  45. package/src/cdp-relay.ts +104 -6
  46. package/src/cli.ts +14 -13
  47. package/src/executor.ts +122 -39
  48. package/src/relay-client.ts +62 -5
  49. package/src/relay-core.test.ts +10 -6
  50. package/src/relay-session.test.ts +9 -1
  51. package/src/relay-state.test.ts +67 -1
  52. package/src/skill.md +32 -13
  53. package/src/start-relay-server.ts +22 -1
  54. package/src/utils.ts +4 -0
package/dist/bippy.js CHANGED
@@ -951,7 +951,7 @@ ${e3.stack}` : ``;
951
951
  return !(!t2 || !re2.test(t2) || ie2.test(t2));
952
952
  };
953
953
 
954
- // dist/_bippy-entry-26306-1779191184823.js
954
+ // dist/_bippy-entry-35395-1781365259216.js
955
955
  globalThis.__bippy = {
956
956
  getFiberFromHostInstance: Pe,
957
957
  getDisplayName: Te,
package/dist/cdp-log.d.ts CHANGED
@@ -7,10 +7,13 @@ export type CdpLogEntry = {
7
7
  };
8
8
  export type CdpLogger = {
9
9
  log(entry: CdpLogEntry): void;
10
+ /** Wait for all pending writes (and any in-flight rotation) to complete */
11
+ flush(): Promise<void>;
10
12
  logFilePath: string;
11
13
  };
12
- export declare function createCdpLogger({ logFilePath, maxStringLength, }?: {
14
+ export declare function createCdpLogger({ logFilePath, maxStringLength, maxEntries, }?: {
13
15
  logFilePath?: string;
14
16
  maxStringLength?: number;
17
+ maxEntries?: number;
15
18
  }): CdpLogger;
16
19
  //# sourceMappingURL=cdp-log.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-log.d.ts","sourceRoot":"","sources":["../src/cdp-log.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,iBAAiB,GAAG,eAAe,GAAG,gBAAgB,GAAG,cAAc,CAAA;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAA;IAC/B,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AA4BD,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,eAAe,GAChB,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAqBrE"}
1
+ {"version":3,"file":"cdp-log.d.ts","sourceRoot":"","sources":["../src/cdp-log.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,iBAAiB,GAAG,eAAe,GAAG,gBAAgB,GAAG,cAAc,CAAA;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAA;IAC/B,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,2EAA2E;IAC3E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAqCD,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,eAAe,EACf,UAAU,GACX,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAmD1F"}
package/dist/cdp-log.js CHANGED
@@ -24,7 +24,14 @@ function createTruncatingReplacer({ maxStringLength }) {
24
24
  return value;
25
25
  };
26
26
  }
27
- export function createCdpLogger({ logFilePath, maxStringLength, } = {}) {
27
+ const DEFAULT_MAX_ENTRIES = 10_000;
28
+ function resolvePositiveInt(value, fallback) {
29
+ if (value == null || !Number.isFinite(value) || value < 2) {
30
+ return fallback;
31
+ }
32
+ return Math.floor(value);
33
+ }
34
+ export function createCdpLogger({ logFilePath, maxStringLength, maxEntries, } = {}) {
28
35
  const resolvedLogFilePath = logFilePath || LOG_CDP_FILE_PATH;
29
36
  const logDir = path.dirname(resolvedLogFilePath);
30
37
  if (!fs.existsSync(logDir)) {
@@ -32,14 +39,44 @@ export function createCdpLogger({ logFilePath, maxStringLength, } = {}) {
32
39
  }
33
40
  fs.writeFileSync(resolvedLogFilePath, '');
34
41
  let queue = Promise.resolve();
42
+ let lineCount = 0;
35
43
  const maxLength = maxStringLength ?? DEFAULT_MAX_STRING_LENGTH;
44
+ const envMaxEntries = Number(process.env.PLAYWRITER_CDP_LOG_MAX_ENTRIES);
45
+ const resolvedMaxEntries = resolvePositiveInt(maxEntries, resolvePositiveInt(envMaxEntries, DEFAULT_MAX_ENTRIES));
46
+ // Keep half the entries after rotation so we don't rotate on every write
47
+ const keepAfterRotation = Math.floor(resolvedMaxEntries / 2);
48
+ // Atomic rotation: write to temp file then rename to avoid corruption on crash
49
+ const rotate = async () => {
50
+ try {
51
+ const content = await fs.promises.readFile(resolvedLogFilePath, 'utf-8');
52
+ const lines = content.split('\n').filter((l) => {
53
+ return l.length > 0;
54
+ });
55
+ const kept = lines.slice(-keepAfterRotation);
56
+ const tmpPath = `${resolvedLogFilePath}.tmp`;
57
+ await fs.promises.writeFile(tmpPath, kept.join('\n') + '\n');
58
+ await fs.promises.rename(tmpPath, resolvedLogFilePath);
59
+ lineCount = kept.length;
60
+ }
61
+ catch {
62
+ // If rotation fails (disk error, permissions), keep logging without rotation.
63
+ // lineCount stays high so rotation will be retried on next write.
64
+ }
65
+ };
36
66
  const log = (entry) => {
37
67
  const replacer = createTruncatingReplacer({ maxStringLength: maxLength });
38
68
  const line = JSON.stringify(entry, replacer);
39
- queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, `${line}\n`));
69
+ queue = queue.then(async () => {
70
+ await fs.promises.appendFile(resolvedLogFilePath, `${line}\n`);
71
+ lineCount++;
72
+ if (lineCount > resolvedMaxEntries) {
73
+ await rotate();
74
+ }
75
+ });
40
76
  };
41
77
  return {
42
78
  log,
79
+ flush: () => queue,
43
80
  logFilePath: resolvedLogFilePath,
44
81
  };
45
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-log.js","sourceRoot":"","sources":["../src/cdp-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAe9C,MAAM,yBAAyB,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,IAAI,CAAC,CAAA;AAElG,SAAS,cAAc,CAAC,KAAa,EAAE,SAAiB;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,GAAG,SAAS,CAAA;IAC/C,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,eAAe,cAAc,SAAS,CAAA;AAC3E,CAAC;AAED,SAAS,wBAAwB,CAAC,EAAE,eAAe,EAA+B;IAChF,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAA;IAClC,OAAO,CAAC,IAAY,EAAE,KAAc,EAAE,EAAE;QACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,cAAc,CAAC,KAAK,EAAE,eAAe,CAAC,CAAA;QAC/C,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAChD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpB,OAAO,YAAY,CAAA;YACrB,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,eAAe,MACuC,EAAE;IACxD,MAAM,mBAAmB,GAAG,WAAW,IAAI,iBAAiB,CAAA;IAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IAChD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3C,CAAC;IACD,EAAE,CAAC,aAAa,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAA;IAEzC,IAAI,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAA;IAC5C,MAAM,SAAS,GAAG,eAAe,IAAI,yBAAyB,CAAA;IAE9D,MAAM,GAAG,GAAG,CAAC,KAAkB,EAAQ,EAAE;QACvC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC,CAAA;QACzE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;QAC5C,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,mBAAmB,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAA;IACpF,CAAC,CAAA;IAED,OAAO;QACL,GAAG;QACH,WAAW,EAAE,mBAAmB;KACjC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"cdp-log.js","sourceRoot":"","sources":["../src/cdp-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cdp-log.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp-log.test.d.ts","sourceRoot":"","sources":["../src/cdp-log.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createCdpLogger } from './cdp-log.js';
6
+ function makeTmpDir() {
7
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'cdp-log-test-'));
8
+ }
9
+ function makeEntry(i) {
10
+ return {
11
+ timestamp: new Date().toISOString(),
12
+ direction: 'from-extension',
13
+ message: { method: `Test.method${i}`, id: i },
14
+ };
15
+ }
16
+ function readIds(logFile) {
17
+ return fs
18
+ .readFileSync(logFile, 'utf-8')
19
+ .trim()
20
+ .split('\n')
21
+ .filter((l) => {
22
+ return l.length > 0;
23
+ })
24
+ .map((l) => {
25
+ return JSON.parse(l).message.id;
26
+ });
27
+ }
28
+ describe('CDP log rotation', () => {
29
+ it('rotates when lineCount exceeds maxEntries, keeping last half', async () => {
30
+ const tmpDir = makeTmpDir();
31
+ const logFile = path.join(tmpDir, 'cdp.jsonl');
32
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 20 });
33
+ // Write 25 entries to trigger rotation (threshold is 20)
34
+ for (let i = 0; i < 25; i++) {
35
+ logger.log(makeEntry(i));
36
+ }
37
+ await logger.flush();
38
+ const ids = readIds(logFile);
39
+ // Rotation triggers after entry 20 is written (lineCount becomes 21 > 20).
40
+ // It keeps last 10 (entries 11-20), then entries 21-24 are appended.
41
+ expect(ids).toMatchInlineSnapshot(`
42
+ [
43
+ 11,
44
+ 12,
45
+ 13,
46
+ 14,
47
+ 15,
48
+ 16,
49
+ 17,
50
+ 18,
51
+ 19,
52
+ 20,
53
+ 21,
54
+ 22,
55
+ 23,
56
+ 24,
57
+ ]
58
+ `);
59
+ fs.rmSync(tmpDir, { recursive: true });
60
+ });
61
+ it('does not rotate when under maxEntries', async () => {
62
+ const tmpDir = makeTmpDir();
63
+ const logFile = path.join(tmpDir, 'cdp.jsonl');
64
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 50 });
65
+ for (let i = 0; i < 30; i++) {
66
+ logger.log(makeEntry(i));
67
+ }
68
+ await logger.flush();
69
+ const ids = readIds(logFile);
70
+ expect(ids.length).toBe(30);
71
+ expect(ids[0]).toBe(0);
72
+ expect(ids[29]).toBe(29);
73
+ fs.rmSync(tmpDir, { recursive: true });
74
+ });
75
+ it('handles multiple rotations', async () => {
76
+ const tmpDir = makeTmpDir();
77
+ const logFile = path.join(tmpDir, 'cdp.jsonl');
78
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 10 });
79
+ // Write 35 entries, should trigger multiple rotations
80
+ for (let i = 0; i < 35; i++) {
81
+ logger.log(makeEntry(i));
82
+ }
83
+ await logger.flush();
84
+ const ids = readIds(logFile);
85
+ // File should never exceed maxEntries
86
+ expect(ids.length).toBeLessThanOrEqual(15);
87
+ expect(ids.length).toBeGreaterThanOrEqual(5);
88
+ // Last entry should always be the most recent
89
+ expect(ids[ids.length - 1]).toBe(34);
90
+ // No entries from the very beginning should survive multiple rotations
91
+ expect(ids[0]).toBeGreaterThan(10);
92
+ fs.rmSync(tmpDir, { recursive: true });
93
+ });
94
+ it('uses atomic rename for rotation', async () => {
95
+ const tmpDir = makeTmpDir();
96
+ const logFile = path.join(tmpDir, 'cdp.jsonl');
97
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 10 });
98
+ for (let i = 0; i < 15; i++) {
99
+ logger.log(makeEntry(i));
100
+ }
101
+ await logger.flush();
102
+ // Temp file should not remain after successful rotation
103
+ expect(fs.existsSync(`${logFile}.tmp`)).toBe(false);
104
+ const ids = readIds(logFile);
105
+ expect(ids[ids.length - 1]).toBe(14);
106
+ fs.rmSync(tmpDir, { recursive: true });
107
+ });
108
+ });
109
+ //# sourceMappingURL=cdp-log.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp-log.test.js","sourceRoot":"","sources":["../src/cdp-log.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-relay.d.ts","sourceRoot":"","sources":["../src/cdp-relay.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0D,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAqB/G,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAqChF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,IAAI,IAAI,CAAA;IACb,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CACvF,CAAA;AAED,wBAAsB,6BAA6B,CAAC,EAClD,IAAY,EACZ,IAAkB,EAClB,KAAK,EACL,MAAM,EACN,SAAS,GACV,GAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;KAAE,CAAA;IACnE,SAAS,CAAC,EAAE,SAAS,CAAA;CACjB,GAAG,OAAO,CAAC,WAAW,CAAC,CA2+D5B"}
1
+ {"version":3,"file":"cdp-relay.d.ts","sourceRoot":"","sources":["../src/cdp-relay.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0D,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAqB/G,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAqChF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,IAAI,IAAI,CAAA;IACb,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CACvF,CAAA;AAED,wBAAsB,6BAA6B,CAAC,EAClD,IAAY,EACZ,IAAkB,EAClB,KAAK,EACL,MAAM,EACN,SAAS,GACV,GAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;KAAE,CAAA;IACnE,SAAS,CAAC,EAAE,SAAS,CAAA;CACjB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6kE5B"}
package/dist/cdp-relay.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
- import { serve } from '@hono/node-server';
3
+ import { createAdaptorServer } from '@hono/node-server';
4
4
  import { getConnInfo } from '@hono/node-server/conninfo';
5
5
  import { createNodeWebSocket } from '@hono/node-ws';
6
6
  import pc from 'picocolors';
@@ -10,7 +10,7 @@ Buffer.prototype[util.inspect.custom] = function () {
10
10
  return `<Buffer ${this.length} bytes>`;
11
11
  };
12
12
  import { EventEmitter } from 'node:events';
13
- import { VERSION, EXTENSION_IDS } from './utils.js';
13
+ import { VERSION, EXTENSION_IDS, shouldAutoEnablePlaywriter } from './utils.js';
14
14
  import { createCdpLogger } from './cdp-log.js';
15
15
  import { RecordingRelay } from './recording-relay.js';
16
16
  import { appendSessionToWsUrl } from './chrome-discovery.js';
@@ -368,10 +368,10 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
368
368
  }
369
369
  return recordingRelays.get(connId) || null;
370
370
  };
371
- // Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
372
- // This allows Playwright to connect and immediately have a page to work with.
371
+ // Auto-create an initial blank tab when no targets exist. Set
372
+ // PLAYWRITER_AUTO_ENABLE=false to require manually enabled tabs instead.
373
373
  async function maybeAutoCreateInitialTab(extensionId) {
374
- if (!process.env.PLAYWRITER_AUTO_ENABLE) {
374
+ if (!shouldAutoEnablePlaywriter()) {
375
375
  return;
376
376
  }
377
377
  const conn = getExtensionConnection(extensionId);
@@ -629,6 +629,83 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
629
629
  },
630
630
  allowMethods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
631
631
  }));
632
+ // Host header validation to prevent DNS rebinding attacks.
633
+ // DNS rebinding is worse than a simple cross-origin request: the attacker
634
+ // serves a page from http://evil.com:19988, then rebinds the DNS to
635
+ // 127.0.0.1. The browser now considers requests to our relay as same-origin,
636
+ // so Sec-Fetch-Site is "same-origin", CORS doesn't apply, and JSON POSTs
637
+ // don't need preflight. This bypasses all our other defenses.
638
+ // By rejecting any Host that isn't a known localhost value we kill DNS
639
+ // rebinding at the root. When a valid token is provided (remote access), we
640
+ // allow through regardless of Host since remote clients use real hostnames.
641
+ const ALLOWED_HOSTS = new Set([
642
+ 'localhost',
643
+ '127.0.0.1',
644
+ '[::1]',
645
+ '::1',
646
+ ]);
647
+ // Parse the Host header into just the hostname, handling IPv6 brackets and
648
+ // port suffixes. Returns null for missing or malformed values.
649
+ function parseHostname(hostHeader) {
650
+ const value = hostHeader?.trim().toLowerCase();
651
+ if (!value) {
652
+ return null;
653
+ }
654
+ // IPv6 in brackets: [::1] or [::1]:19988
655
+ if (value.startsWith('[')) {
656
+ const closingBracket = value.indexOf(']');
657
+ if (closingBracket === -1) {
658
+ return null;
659
+ }
660
+ const host = value.slice(0, closingBracket + 1);
661
+ const rest = value.slice(closingBracket + 1);
662
+ if (rest && !/^:\d+$/.test(rest)) {
663
+ return null;
664
+ }
665
+ return host;
666
+ }
667
+ // Bare ::1 without brackets (uncommon but possible)
668
+ if (value === '::1') {
669
+ return '::1';
670
+ }
671
+ // hostname or hostname:port
672
+ const colonIndex = value.indexOf(':');
673
+ if (colonIndex === -1) {
674
+ return value;
675
+ }
676
+ const host = value.slice(0, colonIndex);
677
+ const portPart = value.slice(colonIndex + 1);
678
+ if (!/^\d+$/.test(portPart)) {
679
+ return null;
680
+ }
681
+ return host || null;
682
+ }
683
+ function hasValidToken(c) {
684
+ if (!token) {
685
+ return false;
686
+ }
687
+ const authHeader = c.req.header('authorization') || '';
688
+ const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
689
+ const queryToken = new URL(c.req.url, 'http://localhost').searchParams.get('token');
690
+ return bearerToken === token || queryToken === token;
691
+ }
692
+ app.use('*', async (c, next) => {
693
+ const hostname = parseHostname(c.req.header('host'));
694
+ if (hostname && ALLOWED_HOSTS.has(hostname)) {
695
+ return next();
696
+ }
697
+ // Remote clients with a valid token are allowed regardless of Host
698
+ if (hasValidToken(c)) {
699
+ return next();
700
+ }
701
+ // Missing Host header from non-browser clients (curl without Host) is fine
702
+ // in local mode since they're not browser-based DNS rebinding attacks
703
+ if (!hostname && !token) {
704
+ return next();
705
+ }
706
+ logger?.log(pc.red(`Rejecting request with unexpected Host header: ${c.req.header('host')} (DNS rebinding protection)`));
707
+ return c.text('Forbidden - Invalid Host header', 403);
708
+ });
632
709
  const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
633
710
  const getCdpWsUrl = (c) => {
634
711
  const hostHeader = c.req.header('host') || `${host}:${port}`;
@@ -1647,8 +1724,24 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
1647
1724
  const result = await relay.cancelRecording(cancelParams);
1648
1725
  return c.json(result);
1649
1726
  });
1650
- const server = serve({ fetch: app.fetch, port, hostname: host });
1727
+ // Use createAdaptorServer instead of serve() so we control the listen()
1728
+ // timing. This lets us inject WebSocket upgrade handlers before binding and
1729
+ // await the bind to surface EADDRINUSE as a catchable error (issue #75).
1730
+ const server = createAdaptorServer({ fetch: app.fetch, hostname: host });
1651
1731
  injectWebSocket(server);
1732
+ await new Promise((resolve, reject) => {
1733
+ const onListening = () => {
1734
+ server.off('error', onError);
1735
+ resolve();
1736
+ };
1737
+ const onError = (error) => {
1738
+ server.off('listening', onListening);
1739
+ reject(error);
1740
+ };
1741
+ server.once('listening', onListening);
1742
+ server.once('error', onError);
1743
+ server.listen(port, host);
1744
+ });
1652
1745
  const wsHost = `ws://${host}:${port}`;
1653
1746
  const cdpEndpoint = `${wsHost}/cdp`;
1654
1747
  const extensionEndpoint = `${wsHost}/extension`;