playwriter 0.2.0 → 0.3.0

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 +103 -6
  12. package/dist/cdp-relay.js.map +1 -1
  13. package/dist/cli.js +8 -5
  14. package/dist/cli.js.map +1 -1
  15. package/dist/executor.d.ts +4 -0
  16. package/dist/executor.d.ts.map +1 -1
  17. package/dist/executor.js +102 -32
  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 +2 -1
  39. package/dist/utils.d.ts.map +1 -1
  40. package/dist/utils.js +4 -1
  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 +110 -5
  46. package/src/cli.ts +8 -5
  47. package/src/executor.ts +119 -35
  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 +5 -0
@@ -1 +1 @@
1
- {"version":3,"file":"start-relay-server.js","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,CAAC,KAAK,GAAG,sBAAsB,CAAA;AAEtC,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAA;AAEjC,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAC5C,MAAM,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAA;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA;AAEF,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IAChD,MAAM,MAAM,CAAC,KAAK,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAA;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA;AAEF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAChC,MAAM,MAAM,CAAC,GAAG,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAA;AACxD,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAChC,IAAI,GAAG,KAAK,EACZ,IAAI,GAAG,WAAW,EAClB,KAAK,MAC+C,EAAE;IACtD,MAAM,MAAM,GAAG,MAAM,6BAA6B,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IAEjF,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAA;IAC9D,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;IAC7D,OAAO,CAAC,GAAG,CAAC,gCAAgC,EAAE,iBAAiB,CAAC,CAAA;IAEhE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC;AACD,WAAW,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA"}
1
+ {"version":3,"file":"start-relay-server.js","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"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"}
package/dist/utils.d.ts CHANGED
@@ -11,11 +11,12 @@ export declare function parseRelayHost(host: string, port?: number): {
11
11
  httpBaseUrl: string;
12
12
  wsBaseUrl: string;
13
13
  };
14
- export declare function getCdpUrl({ port, host, token, extensionId, }?: {
14
+ export declare function getCdpUrl({ port, host, token, extensionId, autoEnable, }?: {
15
15
  port?: number;
16
16
  host?: string;
17
17
  token?: string;
18
18
  extensionId?: string | null;
19
+ autoEnable?: boolean;
19
20
  }): string;
20
21
  export declare const LOG_FILE_PATH: string;
21
22
  export declare const LOG_CDP_FILE_PATH: string;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa,UAGzB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY7G;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAY,EACZ,IAAkB,EAClB,KAAK,EACL,WAAW,GACZ,GAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,UAaL;AAID,eAAO,MAAM,aAAa,QAAsF,CAAA;AAChH,eAAO,MAAM,iBAAiB,QACmE,CAAA;AAGjG,eAAO,MAAM,OAAO,EAAoE,MAAM,CAAA;AAE9F,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa,UAGzB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY7G;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAY,EACZ,IAAkB,EAClB,KAAK,EACL,WAAW,EACX,UAAU,GACX,GAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAA;CAChB,UAgBL;AAID,eAAO,MAAM,aAAa,QAAsF,CAAA;AAChH,eAAO,MAAM,iBAAiB,QACmE,CAAA;AAGjG,eAAO,MAAM,OAAO,EAAoE,MAAM,CAAA;AAE9F,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
package/dist/utils.js CHANGED
@@ -28,7 +28,7 @@ export function parseRelayHost(host, port = 19988) {
28
28
  wsBaseUrl: `ws://${host}:${port}`,
29
29
  };
30
30
  }
31
- export function getCdpUrl({ port = 19988, host = '127.0.0.1', token, extensionId, } = {}) {
31
+ export function getCdpUrl({ port = 19988, host = '127.0.0.1', token, extensionId, autoEnable, } = {}) {
32
32
  const id = `${Math.random().toString(36).substring(2, 15)}_${Date.now()}`;
33
33
  const params = new URLSearchParams();
34
34
  if (token) {
@@ -37,6 +37,9 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1', token, extensionId
37
37
  if (extensionId) {
38
38
  params.set('extensionId', extensionId);
39
39
  }
40
+ if (autoEnable) {
41
+ params.set('autoEnable', '1');
42
+ }
40
43
  const queryString = params.toString();
41
44
  const suffix = queryString ? `?${queryString}` : '';
42
45
  const { wsBaseUrl } = parseRelayHost(host, port);
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "playwriter",
3
3
  "description": "",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -0,0 +1,131 @@
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, type CdpLogEntry } from './cdp-log.js'
6
+
7
+ function makeTmpDir() {
8
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'cdp-log-test-'))
9
+ }
10
+
11
+ function makeEntry(i: number): CdpLogEntry {
12
+ return {
13
+ timestamp: new Date().toISOString(),
14
+ direction: 'from-extension',
15
+ message: { method: `Test.method${i}`, id: i },
16
+ }
17
+ }
18
+
19
+ function readIds(logFile: string): number[] {
20
+ return fs
21
+ .readFileSync(logFile, 'utf-8')
22
+ .trim()
23
+ .split('\n')
24
+ .filter((l) => {
25
+ return l.length > 0
26
+ })
27
+ .map((l) => {
28
+ return JSON.parse(l).message.id as number
29
+ })
30
+ }
31
+
32
+ describe('CDP log rotation', () => {
33
+ it('rotates when lineCount exceeds maxEntries, keeping last half', async () => {
34
+ const tmpDir = makeTmpDir()
35
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
36
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 20 })
37
+
38
+ // Write 25 entries to trigger rotation (threshold is 20)
39
+ for (let i = 0; i < 25; i++) {
40
+ logger.log(makeEntry(i))
41
+ }
42
+ await logger.flush()
43
+
44
+ const ids = readIds(logFile)
45
+
46
+ // Rotation triggers after entry 20 is written (lineCount becomes 21 > 20).
47
+ // It keeps last 10 (entries 11-20), then entries 21-24 are appended.
48
+ expect(ids).toMatchInlineSnapshot(`
49
+ [
50
+ 11,
51
+ 12,
52
+ 13,
53
+ 14,
54
+ 15,
55
+ 16,
56
+ 17,
57
+ 18,
58
+ 19,
59
+ 20,
60
+ 21,
61
+ 22,
62
+ 23,
63
+ 24,
64
+ ]
65
+ `)
66
+
67
+ fs.rmSync(tmpDir, { recursive: true })
68
+ })
69
+
70
+ it('does not rotate when under maxEntries', async () => {
71
+ const tmpDir = makeTmpDir()
72
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
73
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 50 })
74
+
75
+ for (let i = 0; i < 30; i++) {
76
+ logger.log(makeEntry(i))
77
+ }
78
+ await logger.flush()
79
+
80
+ const ids = readIds(logFile)
81
+ expect(ids.length).toBe(30)
82
+ expect(ids[0]).toBe(0)
83
+ expect(ids[29]).toBe(29)
84
+
85
+ fs.rmSync(tmpDir, { recursive: true })
86
+ })
87
+
88
+ it('handles multiple rotations', async () => {
89
+ const tmpDir = makeTmpDir()
90
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
91
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 10 })
92
+
93
+ // Write 35 entries, should trigger multiple rotations
94
+ for (let i = 0; i < 35; i++) {
95
+ logger.log(makeEntry(i))
96
+ }
97
+ await logger.flush()
98
+
99
+ const ids = readIds(logFile)
100
+
101
+ // File should never exceed maxEntries
102
+ expect(ids.length).toBeLessThanOrEqual(15)
103
+ expect(ids.length).toBeGreaterThanOrEqual(5)
104
+
105
+ // Last entry should always be the most recent
106
+ expect(ids[ids.length - 1]).toBe(34)
107
+ // No entries from the very beginning should survive multiple rotations
108
+ expect(ids[0]).toBeGreaterThan(10)
109
+
110
+ fs.rmSync(tmpDir, { recursive: true })
111
+ })
112
+
113
+ it('uses atomic rename for rotation', async () => {
114
+ const tmpDir = makeTmpDir()
115
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
116
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 10 })
117
+
118
+ for (let i = 0; i < 15; i++) {
119
+ logger.log(makeEntry(i))
120
+ }
121
+ await logger.flush()
122
+
123
+ // Temp file should not remain after successful rotation
124
+ expect(fs.existsSync(`${logFile}.tmp`)).toBe(false)
125
+
126
+ const ids = readIds(logFile)
127
+ expect(ids[ids.length - 1]).toBe(14)
128
+
129
+ fs.rmSync(tmpDir, { recursive: true })
130
+ })
131
+ })
package/src/cdp-log.ts CHANGED
@@ -12,6 +12,8 @@ export type CdpLogEntry = {
12
12
 
13
13
  export type CdpLogger = {
14
14
  log(entry: CdpLogEntry): void
15
+ /** Wait for all pending writes (and any in-flight rotation) to complete */
16
+ flush(): Promise<void>
15
17
  logFilePath: string
16
18
  }
17
19
 
@@ -41,10 +43,20 @@ function createTruncatingReplacer({ maxStringLength }: { maxStringLength: number
41
43
  }
42
44
  }
43
45
 
46
+ const DEFAULT_MAX_ENTRIES = 10_000
47
+
48
+ function resolvePositiveInt(value: number | undefined, fallback: number): number {
49
+ if (value == null || !Number.isFinite(value) || value < 2) {
50
+ return fallback
51
+ }
52
+ return Math.floor(value)
53
+ }
54
+
44
55
  export function createCdpLogger({
45
56
  logFilePath,
46
57
  maxStringLength,
47
- }: { logFilePath?: string; maxStringLength?: number } = {}): CdpLogger {
58
+ maxEntries,
59
+ }: { logFilePath?: string; maxStringLength?: number; maxEntries?: number } = {}): CdpLogger {
48
60
  const resolvedLogFilePath = logFilePath || LOG_CDP_FILE_PATH
49
61
  const logDir = path.dirname(resolvedLogFilePath)
50
62
  if (!fs.existsSync(logDir)) {
@@ -53,16 +65,46 @@ export function createCdpLogger({
53
65
  fs.writeFileSync(resolvedLogFilePath, '')
54
66
 
55
67
  let queue: Promise<void> = Promise.resolve()
68
+ let lineCount = 0
56
69
  const maxLength = maxStringLength ?? DEFAULT_MAX_STRING_LENGTH
70
+ const envMaxEntries = Number(process.env.PLAYWRITER_CDP_LOG_MAX_ENTRIES)
71
+ const resolvedMaxEntries = resolvePositiveInt(maxEntries, resolvePositiveInt(envMaxEntries, DEFAULT_MAX_ENTRIES))
72
+ // Keep half the entries after rotation so we don't rotate on every write
73
+ const keepAfterRotation = Math.floor(resolvedMaxEntries / 2)
74
+
75
+ // Atomic rotation: write to temp file then rename to avoid corruption on crash
76
+ const rotate = async (): Promise<void> => {
77
+ try {
78
+ const content = await fs.promises.readFile(resolvedLogFilePath, 'utf-8')
79
+ const lines = content.split('\n').filter((l) => {
80
+ return l.length > 0
81
+ })
82
+ const kept = lines.slice(-keepAfterRotation)
83
+ const tmpPath = `${resolvedLogFilePath}.tmp`
84
+ await fs.promises.writeFile(tmpPath, kept.join('\n') + '\n')
85
+ await fs.promises.rename(tmpPath, resolvedLogFilePath)
86
+ lineCount = kept.length
87
+ } catch {
88
+ // If rotation fails (disk error, permissions), keep logging without rotation.
89
+ // lineCount stays high so rotation will be retried on next write.
90
+ }
91
+ }
57
92
 
58
93
  const log = (entry: CdpLogEntry): void => {
59
94
  const replacer = createTruncatingReplacer({ maxStringLength: maxLength })
60
95
  const line = JSON.stringify(entry, replacer)
61
- queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, `${line}\n`))
96
+ queue = queue.then(async () => {
97
+ await fs.promises.appendFile(resolvedLogFilePath, `${line}\n`)
98
+ lineCount++
99
+ if (lineCount > resolvedMaxEntries) {
100
+ await rotate()
101
+ }
102
+ })
62
103
  }
63
104
 
64
105
  return {
65
106
  log,
107
+ flush: () => queue,
66
108
  logFilePath: resolvedLogFilePath,
67
109
  }
68
110
  }
package/src/cdp-relay.ts 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 type { WSContext } from 'hono/ws'
@@ -523,8 +523,9 @@ export async function startPlayWriterCDPRelayServer({
523
523
 
524
524
  // Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
525
525
  // This allows Playwright to connect and immediately have a page to work with.
526
- async function maybeAutoCreateInitialTab(extensionId: string): Promise<void> {
527
- if (!process.env.PLAYWRITER_AUTO_ENABLE) {
526
+ async function maybeAutoCreateInitialTab(options: { extensionId: string; autoEnable: boolean }): Promise<void> {
527
+ const { extensionId, autoEnable } = options
528
+ if (!autoEnable && !process.env.PLAYWRITER_AUTO_ENABLE) {
528
529
  return
529
530
  }
530
531
  const conn = getExtensionConnection(extensionId)
@@ -654,12 +655,14 @@ export async function startPlayWriterCDPRelayServer({
654
655
  params,
655
656
  sessionId,
656
657
  source,
658
+ autoEnable,
657
659
  }: {
658
660
  extensionId: string | null
659
661
  method: CDPCommand['method'] | (string & {})
660
662
  params: CDPCommand['params']
661
663
  sessionId?: CDPCommand['sessionId']
662
664
  source?: CDPCommand['source']
665
+ autoEnable: boolean
663
666
  }) {
664
667
  const conn = getExtensionConnection(extensionId)
665
668
  const connectedTargets = conn?.connectedTargets || new Map<string, relayState.ConnectedTarget>()
@@ -699,7 +702,7 @@ export async function startPlayWriterCDPRelayServer({
699
702
  break
700
703
  }
701
704
  if (conn) {
702
- await maybeAutoCreateInitialTab(conn.id)
705
+ await maybeAutoCreateInitialTab({ extensionId: conn.id, autoEnable })
703
706
  }
704
707
  // Forward auto-attach so Chrome emits iframe Target.attachedToTarget events.
705
708
  // Playwright relies on these (with parentFrameId) when reconnecting over CDP.
@@ -856,6 +859,87 @@ export async function startPlayWriterCDPRelayServer({
856
859
  allowMethods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
857
860
  }),
858
861
  )
862
+ // Host header validation to prevent DNS rebinding attacks.
863
+ // DNS rebinding is worse than a simple cross-origin request: the attacker
864
+ // serves a page from http://evil.com:19988, then rebinds the DNS to
865
+ // 127.0.0.1. The browser now considers requests to our relay as same-origin,
866
+ // so Sec-Fetch-Site is "same-origin", CORS doesn't apply, and JSON POSTs
867
+ // don't need preflight. This bypasses all our other defenses.
868
+ // By rejecting any Host that isn't a known localhost value we kill DNS
869
+ // rebinding at the root. When a valid token is provided (remote access), we
870
+ // allow through regardless of Host since remote clients use real hostnames.
871
+ const ALLOWED_HOSTS = new Set([
872
+ 'localhost',
873
+ '127.0.0.1',
874
+ '[::1]',
875
+ '::1',
876
+ ])
877
+
878
+ // Parse the Host header into just the hostname, handling IPv6 brackets and
879
+ // port suffixes. Returns null for missing or malformed values.
880
+ function parseHostname(hostHeader: string | undefined): string | null {
881
+ const value = hostHeader?.trim().toLowerCase()
882
+ if (!value) {
883
+ return null
884
+ }
885
+ // IPv6 in brackets: [::1] or [::1]:19988
886
+ if (value.startsWith('[')) {
887
+ const closingBracket = value.indexOf(']')
888
+ if (closingBracket === -1) {
889
+ return null
890
+ }
891
+ const host = value.slice(0, closingBracket + 1)
892
+ const rest = value.slice(closingBracket + 1)
893
+ if (rest && !/^:\d+$/.test(rest)) {
894
+ return null
895
+ }
896
+ return host
897
+ }
898
+ // Bare ::1 without brackets (uncommon but possible)
899
+ if (value === '::1') {
900
+ return '::1'
901
+ }
902
+ // hostname or hostname:port
903
+ const colonIndex = value.indexOf(':')
904
+ if (colonIndex === -1) {
905
+ return value
906
+ }
907
+ const host = value.slice(0, colonIndex)
908
+ const portPart = value.slice(colonIndex + 1)
909
+ if (!/^\d+$/.test(portPart)) {
910
+ return null
911
+ }
912
+ return host || null
913
+ }
914
+
915
+ function hasValidToken(c: { req: { header: (name: string) => string | undefined; url: string } }): boolean {
916
+ if (!token) {
917
+ return false
918
+ }
919
+ const authHeader = c.req.header('authorization') || ''
920
+ const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
921
+ const queryToken = new URL(c.req.url, 'http://localhost').searchParams.get('token')
922
+ return bearerToken === token || queryToken === token
923
+ }
924
+
925
+ app.use('*', async (c, next) => {
926
+ const hostname = parseHostname(c.req.header('host'))
927
+ if (hostname && ALLOWED_HOSTS.has(hostname)) {
928
+ return next()
929
+ }
930
+ // Remote clients with a valid token are allowed regardless of Host
931
+ if (hasValidToken(c)) {
932
+ return next()
933
+ }
934
+ // Missing Host header from non-browser clients (curl without Host) is fine
935
+ // in local mode since they're not browser-based DNS rebinding attacks
936
+ if (!hostname && !token) {
937
+ return next()
938
+ }
939
+ logger?.log(pc.red(`Rejecting request with unexpected Host header: ${c.req.header('host')} (DNS rebinding protection)`))
940
+ return c.text('Forbidden - Invalid Host header', 403)
941
+ })
942
+
859
943
  const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
860
944
 
861
945
  const getCdpWsUrl = (c: { req: { header: (name: string) => string | undefined } }) => {
@@ -1028,6 +1112,7 @@ export async function startPlayWriterCDPRelayServer({
1028
1112
  const clientId = c.req.param('clientId') || 'default'
1029
1113
  const url = new URL(c.req.url, 'http://localhost')
1030
1114
  const requestedExtensionId = url.searchParams.get('extensionId')
1115
+ const autoEnable = url.searchParams.get('autoEnable') === '1'
1031
1116
  // When extensionId is explicit, resolve directly. Otherwise use fallback which
1032
1117
  // handles single-extension and uniquely-active-extension cases (#52).
1033
1118
  const resolvedExtension = requestedExtensionId
@@ -1119,6 +1204,7 @@ export async function startPlayWriterCDPRelayServer({
1119
1204
  params,
1120
1205
  sessionId,
1121
1206
  source,
1207
+ autoEnable,
1122
1208
  })
1123
1209
 
1124
1210
  if (method === 'Target.setAutoAttach' && !sessionId) {
@@ -1897,6 +1983,7 @@ export async function startPlayWriterCDPRelayServer({
1897
1983
  app.post('/cli/session/new', async (c) => {
1898
1984
  const body = (await c.req.json().catch(() => ({}))) as {
1899
1985
  extensionId?: string | null
1986
+ autoEnable?: boolean
1900
1987
  cwd?: string
1901
1988
  /** Direct CDP WebSocket URL — bypasses extension, connects straight to Chrome */
1902
1989
  cdpEndpoint?: string
@@ -1950,6 +2037,7 @@ export async function startPlayWriterCDPRelayServer({
1950
2037
  const executor = manager.getExecutor({
1951
2038
  sessionId,
1952
2039
  cwd,
2040
+ cdpConfig: { host: '127.0.0.1', port, token, extensionId: conn.stableKey, autoEnable: body.autoEnable === true },
1953
2041
  sessionMetadata: {
1954
2042
  extensionId: conn.stableKey,
1955
2043
  browser: conn.info.browser || null,
@@ -2065,9 +2153,26 @@ export async function startPlayWriterCDPRelayServer({
2065
2153
  return c.json(result)
2066
2154
  })
2067
2155
 
2068
- const server = serve({ fetch: app.fetch, port, hostname: host })
2156
+ // Use createAdaptorServer instead of serve() so we control the listen()
2157
+ // timing. This lets us inject WebSocket upgrade handlers before binding and
2158
+ // await the bind to surface EADDRINUSE as a catchable error (issue #75).
2159
+ const server = createAdaptorServer({ fetch: app.fetch, hostname: host })
2069
2160
  injectWebSocket(server)
2070
2161
 
2162
+ await new Promise<void>((resolve, reject) => {
2163
+ const onListening = () => {
2164
+ server.off('error', onError)
2165
+ resolve()
2166
+ }
2167
+ const onError = (error: Error) => {
2168
+ server.off('listening', onListening)
2169
+ reject(error)
2170
+ }
2171
+ server.once('listening', onListening)
2172
+ server.once('error', onError)
2173
+ server.listen(port, host)
2174
+ })
2175
+
2071
2176
  const wsHost = `ws://${host}:${port}`
2072
2177
  const cdpEndpoint = `${wsHost}/cdp`
2073
2178
  const extensionEndpoint = `${wsHost}/extension`
package/src/cli.ts CHANGED
@@ -169,15 +169,18 @@ function buildAuthHeaders({ token, json }: { token?: string; json?: boolean }):
169
169
  return headers
170
170
  }
171
171
 
172
- async function fetchExtensionsStatus(host?: string): Promise<ExtensionStatus[]> {
172
+ async function fetchExtensionsStatus({ host, token }: { host?: string; token?: string } = {}): Promise<ExtensionStatus[]> {
173
173
  try {
174
174
  const serverUrl = await getServerUrl(host)
175
+ const headers = buildAuthHeaders({ token })
175
176
  const response = await fetch(`${serverUrl}/extensions/status`, {
176
177
  signal: AbortSignal.timeout(2000),
178
+ headers,
177
179
  })
178
180
  if (!response.ok) {
179
181
  const fallback = await fetch(`${serverUrl}/extension/status`, {
180
182
  signal: AbortSignal.timeout(2000),
183
+ headers,
181
184
  })
182
185
  if (!fallback.ok) {
183
186
  return []
@@ -458,7 +461,7 @@ cli
458
461
  })
459
462
  }
460
463
  } else {
461
- extensions = await fetchExtensionsStatus(options.host)
464
+ extensions = await fetchExtensionsStatus({ host: options.host, token: options.token })
462
465
  }
463
466
 
464
467
  if (extensions.length === 0) {
@@ -489,7 +492,7 @@ cli
489
492
  const response = await fetch(`${serverUrl}/cli/session/new`, {
490
493
  method: 'POST',
491
494
  headers: buildAuthHeaders({ token: options.token, json: true }),
492
- body: JSON.stringify({ extensionId, cwd }),
495
+ body: JSON.stringify({ extensionId, cwd, autoEnable: true }),
493
496
  })
494
497
  if (!response.ok) {
495
498
  const text = await response.text()
@@ -548,7 +551,7 @@ cli
548
551
  const response = await fetch(`${serverUrl}/cli/session/new`, {
549
552
  method: 'POST',
550
553
  headers: buildAuthHeaders({ token: options.token, json: true }),
551
- body: JSON.stringify({ extensionId: selected.extensionId, cwd }),
554
+ body: JSON.stringify({ extensionId: selected.extensionId, cwd, autoEnable: true }),
552
555
  })
553
556
  if (!response.ok) {
554
557
  const text = await response.text()
@@ -929,7 +932,7 @@ cli
929
932
  const [extensions, directInstances] = await Promise.all([
930
933
  isLocal
931
934
  ? waitForConnectedExtensions({ timeoutMs: 2000, pollIntervalMs: 200, logger: console })
932
- : fetchExtensionsStatus(options.host),
935
+ : fetchExtensionsStatus({ host: options.host, token: options.token }),
933
936
  isLocal ? discoverChromeInstances() : Promise.resolve([] as DiscoveredInstance[]),
934
937
  ])
935
938