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.
- 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 +99 -6
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cli.js +14 -12
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +3 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +106 -36
- 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 +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- 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 +104 -6
- package/src/cli.ts +14 -13
- package/src/executor.ts +122 -39
- 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 +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-
|
|
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
|
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":"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"}
|
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,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 {
|
|
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
|
|
372
|
-
//
|
|
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 (!
|
|
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
|
-
|
|
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`;
|