playwriter 0.1.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 +5 -5
- 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 +120 -11
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cli-help.test.js +22 -0
- package/dist/cli-help.test.js.map +1 -1
- package/dist/cli.js +69 -25
- 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 +140 -33
- package/dist/executor.js.map +1 -1
- package/dist/extension/background.js +343 -62
- package/dist/extension/manifest.json +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +6 -1
- package/dist/mcp.js.map +1 -1
- package/dist/performance-examples.d.ts +5 -0
- package/dist/performance-examples.d.ts.map +1 -0
- package/dist/performance-examples.js +112 -0
- package/dist/performance-examples.js.map +1 -0
- package/dist/performance-profiling.md +417 -0
- package/dist/prompt.md +51 -18
- package/dist/react-source.d.ts +44 -0
- package/dist/react-source.d.ts.map +1 -1
- package/dist/react-source.js +207 -20
- package/dist/react-source.js.map +1 -1
- 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 +43 -7
- 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/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +19 -4
- package/dist/screen-recording.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 +3 -3
- package/src/cdp-log.test.ts +131 -0
- package/src/cdp-log.ts +44 -2
- package/src/cdp-relay.ts +127 -10
- package/src/cli-help.test.ts +22 -0
- package/src/cli.ts +74 -24
- package/src/executor.ts +166 -39
- package/src/mcp.ts +6 -1
- package/src/performance-examples.ts +186 -0
- package/src/react-source.ts +310 -24
- package/src/relay-client.ts +62 -5
- package/src/relay-core.test.ts +10 -6
- package/src/relay-session.test.ts +45 -11
- package/src/relay-state.test.ts +67 -1
- package/src/screen-recording.ts +20 -4
- package/src/skill.md +62 -19
- package/src/start-relay-server.ts +22 -1
- package/src/utils.ts +5 -0
package/dist/bippy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.
|
|
2
|
+
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.6/node_modules/bippy/dist/rdt-hook-BZMdLD7S.js
|
|
3
3
|
var e = `0.5.27`;
|
|
4
4
|
var t = `bippy-${e}`;
|
|
5
5
|
var n = Object.defineProperty;
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
} catch {}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.
|
|
87
|
+
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.6/node_modules/bippy/dist/install-hook-only-BOBPiBkc.js
|
|
88
88
|
_();
|
|
89
89
|
|
|
90
|
-
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.
|
|
90
|
+
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.6/node_modules/bippy/dist/core-coQbWNwP.js
|
|
91
91
|
var a2 = 0;
|
|
92
92
|
var o2 = 1;
|
|
93
93
|
var c2 = 5;
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
var Fe = Error();
|
|
226
226
|
var $ = new Set;
|
|
227
227
|
|
|
228
|
-
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.
|
|
228
|
+
// ../node_modules/.pnpm/bippy@0.5.27_@types+react@19.2.7_react@19.2.6/node_modules/bippy/dist/source.js
|
|
229
229
|
var g3 = Object.create;
|
|
230
230
|
var _3 = Object.defineProperty;
|
|
231
231
|
var v2 = Object.getOwnPropertyDescriptor;
|
|
@@ -951,7 +951,7 @@ ${e3.stack}` : ``;
|
|
|
951
951
|
return !(!t2 || !re2.test(t2) || ie2.test(t2));
|
|
952
952
|
};
|
|
953
953
|
|
|
954
|
-
// dist/_bippy-entry-
|
|
954
|
+
// dist/_bippy-entry-28588-1781364082573.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
|
package/dist/cdp-log.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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(() =>
|
|
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
|
}
|
package/dist/cdp-log.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"cdp-log.js","sourceRoot":"","sources":["../src/cdp-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,eAAe,EAAoB,MAAM,cAAc,CAAA;AAEhE,SAAS,UAAU;IACjB,OAAO,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC,CAAA;AAChE,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,gBAAgB;QAC3B,OAAO,EAAE,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;KAC9C,CAAA;AACH,CAAC;AAED,SAAS,OAAO,CAAC,OAAe;IAC9B,OAAO,EAAE;SACN,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC;SAC9B,IAAI,EAAE;SACN,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACZ,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;IACrB,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAY,CAAA;IAC3C,CAAC,CAAC,CAAA;AACN,CAAC;AAED,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QAC9C,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;QAExE,yDAAyD;QACzD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QAEpB,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAE5B,2EAA2E;QAC3E,qEAAqE;QACrE,MAAM,CAAC,GAAG,CAAC,CAAC,qBAAqB,CAAC;;;;;;;;;;;;;;;;;KAiBjC,CAAC,CAAA;QAEF,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QAC9C,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;QAExE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QAEpB,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAC5B,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACtB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAExB,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QAC9C,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;QAExE,sDAAsD;QACtD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QAEpB,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAE5B,sCAAsC;QACtC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAA;QAC1C,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAA;QAE5C,8CAA8C;QAC9C,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACpC,uEAAuE;QACvE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,CAAA;QAElC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QAC9C,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;QAExE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QAEpB,wDAAwD;QACxD,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAEnD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAEpC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
package/dist/cdp-relay.d.ts.map
CHANGED
|
@@ -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,
|
|
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,CAolE5B"}
|
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 {
|
|
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';
|
|
@@ -370,8 +370,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
370
370
|
};
|
|
371
371
|
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
|
372
372
|
// This allows Playwright to connect and immediately have a page to work with.
|
|
373
|
-
async function maybeAutoCreateInitialTab(
|
|
374
|
-
|
|
373
|
+
async function maybeAutoCreateInitialTab(options) {
|
|
374
|
+
const { extensionId, autoEnable } = options;
|
|
375
|
+
if (!autoEnable && !process.env.PLAYWRITER_AUTO_ENABLE) {
|
|
375
376
|
return;
|
|
376
377
|
}
|
|
377
378
|
const conn = getExtensionConnection(extensionId);
|
|
@@ -461,7 +462,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
461
462
|
}
|
|
462
463
|
}));
|
|
463
464
|
}
|
|
464
|
-
async function routeCdpCommand({ extensionId, method, params, sessionId, source, }) {
|
|
465
|
+
async function routeCdpCommand({ extensionId, method, params, sessionId, source, autoEnable, }) {
|
|
465
466
|
const conn = getExtensionConnection(extensionId);
|
|
466
467
|
const connectedTargets = conn?.connectedTargets || new Map();
|
|
467
468
|
const resolvedExtensionId = conn?.id || extensionId;
|
|
@@ -498,7 +499,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
498
499
|
break;
|
|
499
500
|
}
|
|
500
501
|
if (conn) {
|
|
501
|
-
await maybeAutoCreateInitialTab(conn.id);
|
|
502
|
+
await maybeAutoCreateInitialTab({ extensionId: conn.id, autoEnable });
|
|
502
503
|
}
|
|
503
504
|
// Forward auto-attach so Chrome emits iframe Target.attachedToTarget events.
|
|
504
505
|
// Playwright relies on these (with parentFrameId) when reconnecting over CDP.
|
|
@@ -629,6 +630,83 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
629
630
|
},
|
|
630
631
|
allowMethods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
|
|
631
632
|
}));
|
|
633
|
+
// Host header validation to prevent DNS rebinding attacks.
|
|
634
|
+
// DNS rebinding is worse than a simple cross-origin request: the attacker
|
|
635
|
+
// serves a page from http://evil.com:19988, then rebinds the DNS to
|
|
636
|
+
// 127.0.0.1. The browser now considers requests to our relay as same-origin,
|
|
637
|
+
// so Sec-Fetch-Site is "same-origin", CORS doesn't apply, and JSON POSTs
|
|
638
|
+
// don't need preflight. This bypasses all our other defenses.
|
|
639
|
+
// By rejecting any Host that isn't a known localhost value we kill DNS
|
|
640
|
+
// rebinding at the root. When a valid token is provided (remote access), we
|
|
641
|
+
// allow through regardless of Host since remote clients use real hostnames.
|
|
642
|
+
const ALLOWED_HOSTS = new Set([
|
|
643
|
+
'localhost',
|
|
644
|
+
'127.0.0.1',
|
|
645
|
+
'[::1]',
|
|
646
|
+
'::1',
|
|
647
|
+
]);
|
|
648
|
+
// Parse the Host header into just the hostname, handling IPv6 brackets and
|
|
649
|
+
// port suffixes. Returns null for missing or malformed values.
|
|
650
|
+
function parseHostname(hostHeader) {
|
|
651
|
+
const value = hostHeader?.trim().toLowerCase();
|
|
652
|
+
if (!value) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
// IPv6 in brackets: [::1] or [::1]:19988
|
|
656
|
+
if (value.startsWith('[')) {
|
|
657
|
+
const closingBracket = value.indexOf(']');
|
|
658
|
+
if (closingBracket === -1) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
const host = value.slice(0, closingBracket + 1);
|
|
662
|
+
const rest = value.slice(closingBracket + 1);
|
|
663
|
+
if (rest && !/^:\d+$/.test(rest)) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
return host;
|
|
667
|
+
}
|
|
668
|
+
// Bare ::1 without brackets (uncommon but possible)
|
|
669
|
+
if (value === '::1') {
|
|
670
|
+
return '::1';
|
|
671
|
+
}
|
|
672
|
+
// hostname or hostname:port
|
|
673
|
+
const colonIndex = value.indexOf(':');
|
|
674
|
+
if (colonIndex === -1) {
|
|
675
|
+
return value;
|
|
676
|
+
}
|
|
677
|
+
const host = value.slice(0, colonIndex);
|
|
678
|
+
const portPart = value.slice(colonIndex + 1);
|
|
679
|
+
if (!/^\d+$/.test(portPart)) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
return host || null;
|
|
683
|
+
}
|
|
684
|
+
function hasValidToken(c) {
|
|
685
|
+
if (!token) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
const authHeader = c.req.header('authorization') || '';
|
|
689
|
+
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
690
|
+
const queryToken = new URL(c.req.url, 'http://localhost').searchParams.get('token');
|
|
691
|
+
return bearerToken === token || queryToken === token;
|
|
692
|
+
}
|
|
693
|
+
app.use('*', async (c, next) => {
|
|
694
|
+
const hostname = parseHostname(c.req.header('host'));
|
|
695
|
+
if (hostname && ALLOWED_HOSTS.has(hostname)) {
|
|
696
|
+
return next();
|
|
697
|
+
}
|
|
698
|
+
// Remote clients with a valid token are allowed regardless of Host
|
|
699
|
+
if (hasValidToken(c)) {
|
|
700
|
+
return next();
|
|
701
|
+
}
|
|
702
|
+
// Missing Host header from non-browser clients (curl without Host) is fine
|
|
703
|
+
// in local mode since they're not browser-based DNS rebinding attacks
|
|
704
|
+
if (!hostname && !token) {
|
|
705
|
+
return next();
|
|
706
|
+
}
|
|
707
|
+
logger?.log(pc.red(`Rejecting request with unexpected Host header: ${c.req.header('host')} (DNS rebinding protection)`));
|
|
708
|
+
return c.text('Forbidden - Invalid Host header', 403);
|
|
709
|
+
});
|
|
632
710
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
633
711
|
const getCdpWsUrl = (c) => {
|
|
634
712
|
const hostHeader = c.req.header('host') || `${host}:${port}`;
|
|
@@ -780,6 +858,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
780
858
|
const clientId = c.req.param('clientId') || 'default';
|
|
781
859
|
const url = new URL(c.req.url, 'http://localhost');
|
|
782
860
|
const requestedExtensionId = url.searchParams.get('extensionId');
|
|
861
|
+
const autoEnable = url.searchParams.get('autoEnable') === '1';
|
|
783
862
|
// When extensionId is explicit, resolve directly. Otherwise use fallback which
|
|
784
863
|
// handles single-extension and uniquely-active-extension cases (#52).
|
|
785
864
|
const resolvedExtension = requestedExtensionId
|
|
@@ -856,6 +935,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
856
935
|
params,
|
|
857
936
|
sessionId,
|
|
858
937
|
source,
|
|
938
|
+
autoEnable,
|
|
859
939
|
});
|
|
860
940
|
if (method === 'Target.setAutoAttach' && !sessionId) {
|
|
861
941
|
// Re-read state after async routeCdpCommand — targets may have changed
|
|
@@ -1385,27 +1465,31 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1385
1465
|
const { ExecutorManager } = await import('./executor.js');
|
|
1386
1466
|
// Pass config instead of URL so executor can generate unique client IDs for each connection
|
|
1387
1467
|
executorManager = new ExecutorManager({
|
|
1388
|
-
cdpConfig: { host: '127.0.0.1', port },
|
|
1468
|
+
cdpConfig: { host: '127.0.0.1', port, token },
|
|
1389
1469
|
logger: logger || { log: console.error, error: console.error },
|
|
1390
1470
|
});
|
|
1391
1471
|
}
|
|
1392
1472
|
return executorManager;
|
|
1393
1473
|
};
|
|
1394
1474
|
// ============================================================================
|
|
1395
|
-
// Security middleware for privileged HTTP routes (/cli/*, /recording
|
|
1475
|
+
// Security middleware for privileged HTTP routes (/cli/*, /recording/*, /mcp-log)
|
|
1396
1476
|
//
|
|
1397
1477
|
// CORS alone does NOT prevent cross-origin POST attacks. Browsers skip the
|
|
1398
1478
|
// preflight for "simple" requests (POST + Content-Type: text/plain), so a
|
|
1399
1479
|
// malicious website can fire-and-forget a POST to localhost:19988/cli/execute
|
|
1400
1480
|
// and the code executes before CORS even enters the picture.
|
|
1401
1481
|
//
|
|
1402
|
-
//
|
|
1482
|
+
// Three layers of defense:
|
|
1403
1483
|
// 1. Sec-Fetch-Site: browsers set this forbidden header on every request.
|
|
1404
1484
|
// If present and not "same-origin"/"none", it's a cross-origin browser
|
|
1405
1485
|
// request → reject. Node.js clients don't send it → unaffected.
|
|
1406
1486
|
// 2. Content-Type must be application/json on POST. This forces a CORS
|
|
1407
1487
|
// preflight as a fallback, which our CORS policy already blocks.
|
|
1408
|
-
// 3. When token mode is enabled (remote access), require the token
|
|
1488
|
+
// 3. When token mode is enabled (remote access), require the token on EVERY
|
|
1489
|
+
// request, including loopback. Tunnel agents (traforo, ngrok, cloudflared)
|
|
1490
|
+
// forward public traffic from 127.0.0.1, so a loopback bypass would be
|
|
1491
|
+
// a full auth bypass. In-process callers attach the token themselves
|
|
1492
|
+
// via PLAYWRITER_TOKEN env (set by the `serve` command at startup).
|
|
1409
1493
|
// ============================================================================
|
|
1410
1494
|
const privilegedRouteMiddleware = async (c, next) => {
|
|
1411
1495
|
// Block cross-origin browser requests via Sec-Fetch-Site header.
|
|
@@ -1425,7 +1509,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1425
1509
|
return c.text('Content-Type must be application/json', 415);
|
|
1426
1510
|
}
|
|
1427
1511
|
}
|
|
1428
|
-
// When token mode is enabled (remote/serve mode), require authentication
|
|
1512
|
+
// When token mode is enabled (remote/serve mode), require authentication
|
|
1513
|
+
// on EVERY request, including loopback. Earlier versions bypassed the
|
|
1514
|
+
// check for 127.0.0.1/::1 to spare in-process callers, but that's unsafe:
|
|
1515
|
+
// when the relay is fronted by a tunnel agent (traforo, ngrok, cloudflared,
|
|
1516
|
+
// etc.) running as a local process, every public request reaches the relay
|
|
1517
|
+
// from 127.0.0.1 and would skip auth. In-process callers must instead
|
|
1518
|
+
// attach the token themselves — they read PLAYWRITER_TOKEN from env, which
|
|
1519
|
+
// the `serve` command sets at startup.
|
|
1429
1520
|
if (token) {
|
|
1430
1521
|
const authHeader = c.req.header('authorization') || '';
|
|
1431
1522
|
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
@@ -1440,6 +1531,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1440
1531
|
};
|
|
1441
1532
|
app.use('/cli/*', privilegedRouteMiddleware);
|
|
1442
1533
|
app.use('/recording/*', privilegedRouteMiddleware);
|
|
1534
|
+
app.use('/mcp-log', privilegedRouteMiddleware);
|
|
1443
1535
|
app.post('/cli/execute', async (c) => {
|
|
1444
1536
|
try {
|
|
1445
1537
|
const body = (await c.req.json());
|
|
@@ -1537,6 +1629,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1537
1629
|
const executor = manager.getExecutor({
|
|
1538
1630
|
sessionId,
|
|
1539
1631
|
cwd,
|
|
1632
|
+
cdpConfig: { host: '127.0.0.1', port, token, extensionId: conn.stableKey, autoEnable: body.autoEnable === true },
|
|
1540
1633
|
sessionMetadata: {
|
|
1541
1634
|
extensionId: conn.stableKey,
|
|
1542
1635
|
browser: conn.info.browser || null,
|
|
@@ -1635,8 +1728,24 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1635
1728
|
const result = await relay.cancelRecording(cancelParams);
|
|
1636
1729
|
return c.json(result);
|
|
1637
1730
|
});
|
|
1638
|
-
|
|
1731
|
+
// Use createAdaptorServer instead of serve() so we control the listen()
|
|
1732
|
+
// timing. This lets us inject WebSocket upgrade handlers before binding and
|
|
1733
|
+
// await the bind to surface EADDRINUSE as a catchable error (issue #75).
|
|
1734
|
+
const server = createAdaptorServer({ fetch: app.fetch, hostname: host });
|
|
1639
1735
|
injectWebSocket(server);
|
|
1736
|
+
await new Promise((resolve, reject) => {
|
|
1737
|
+
const onListening = () => {
|
|
1738
|
+
server.off('error', onError);
|
|
1739
|
+
resolve();
|
|
1740
|
+
};
|
|
1741
|
+
const onError = (error) => {
|
|
1742
|
+
server.off('listening', onListening);
|
|
1743
|
+
reject(error);
|
|
1744
|
+
};
|
|
1745
|
+
server.once('listening', onListening);
|
|
1746
|
+
server.once('error', onError);
|
|
1747
|
+
server.listen(port, host);
|
|
1748
|
+
});
|
|
1640
1749
|
const wsHost = `ws://${host}:${port}`;
|
|
1641
1750
|
const cdpEndpoint = `${wsHost}/cdp`;
|
|
1642
1751
|
const extensionEndpoint = `${wsHost}/extension`;
|