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.
- package/dist/bippy.js +1 -1
- package/dist/cdp-log.d.ts +4 -1
- package/dist/cdp-log.d.ts.map +1 -1
- package/dist/cdp-log.js +39 -2
- package/dist/cdp-log.js.map +1 -1
- package/dist/cdp-log.test.d.ts +2 -0
- package/dist/cdp-log.test.d.ts.map +1 -0
- package/dist/cdp-log.test.js +109 -0
- package/dist/cdp-log.test.js.map +1 -0
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +103 -6
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +4 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +102 -32
- package/dist/executor.js.map +1 -1
- package/dist/extension/background.js +23 -12
- package/dist/extension/manifest.json +1 -1
- package/dist/prompt.md +32 -13
- package/dist/readability.js +1 -1
- package/dist/relay-client.d.ts +11 -0
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +46 -1
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.js +10 -6
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-session.test.js +9 -1
- package/dist/relay-session.test.js.map +1 -1
- package/dist/relay-state.test.js +57 -1
- package/dist/relay-state.test.js.map +1 -1
- package/dist/selector-generator.js +1 -1
- package/dist/start-relay-server.d.ts +1 -1
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +23 -1
- package/dist/start-relay-server.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/cdp-log.test.ts +131 -0
- package/src/cdp-log.ts +44 -2
- package/src/cdp-relay.ts +110 -5
- package/src/cli.ts +8 -5
- package/src/executor.ts +119 -35
- package/src/relay-client.ts +62 -5
- package/src/relay-core.test.ts +10 -6
- package/src/relay-session.test.ts +9 -1
- package/src/relay-state.test.ts +67 -1
- package/src/skill.md +32 -13
- package/src/start-relay-server.ts +22 -1
- 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;
|
|
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;
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -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,
|
|
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":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,kCAAkC,EAAE,gCAAgC;IACpE,kCAAkC,EAAE,8CAA8C;CACnF,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe,KAAK;IAC/D,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;QACzB,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAA;QAC9B,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;QAC7D,MAAM,SAAS,GAAG,GAAG,UAAU,KAAK,GAAG,CAAC,IAAI,EAAE,CAAA;QAC9C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAA;IACnC,CAAC;IACD,OAAO;QACL,WAAW,EAAE,UAAU,IAAI,IAAI,IAAI,EAAE;QACrC,SAAS,EAAE,QAAQ,IAAI,IAAI,IAAI,EAAE;KAClC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EACxB,IAAI,GAAG,KAAK,EACZ,IAAI,GAAG,WAAW,EAClB,KAAK,EACL,WAAW,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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(() =>
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|