barebrowse 0.14.0 → 0.17.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/CHANGELOG.md +157 -0
- package/README.md +33 -1
- package/barebrowse.context.md +5 -2
- package/cli.js +10 -2
- package/mcp-server.js +14 -2
- package/package.json +1 -1
- package/src/auth.js +27 -13
- package/src/ax-snapshot.js +287 -0
- package/src/bidi.js +143 -0
- package/src/consent-firefox.js +125 -0
- package/src/consent-patterns.js +154 -0
- package/src/consent.js +5 -144
- package/src/daemon.js +162 -51
- package/src/firefox-page.js +445 -0
- package/src/firefox.js +203 -0
- package/src/index.js +88 -6
- package/src/network-idle.js +66 -28
- package/src/readable.js +32 -20
- package/src/stealth-firefox.js +42 -0
- package/src/stealth.js +107 -69
- package/types/auth.d.ts +18 -1
- package/types/ax-snapshot.d.ts +35 -0
- package/types/bidi.d.ts +10 -0
- package/types/consent-firefox.d.ts +9 -0
- package/types/consent-patterns.d.ts +12 -0
- package/types/daemon.d.ts +34 -0
- package/types/firefox-page.d.ts +21 -0
- package/types/firefox.d.ts +41 -0
- package/types/index.d.ts +13 -2
- package/types/network-idle.d.ts +16 -8
- package/types/readable.d.ts +8 -0
- package/types/stealth-firefox.d.ts +9 -0
- package/types/stealth.d.ts +31 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consent-patterns.js — Shared cookie-consent detection data.
|
|
3
|
+
*
|
|
4
|
+
* The engine-agnostic half of consent handling: the multilingual regex sets
|
|
5
|
+
* and dialog roles used to recognise a consent dialog and its "accept" button.
|
|
6
|
+
* Both the CDP walker (consent.js) and the BiDi walker (consent-firefox.js)
|
|
7
|
+
* import these so the language coverage is single-sourced — a new language or
|
|
8
|
+
* pattern is added once and both engines get it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Button text patterns that mean "accept all" / "I agree" across common languages.
|
|
12
|
+
// Order matters: more specific patterns first to avoid false positives.
|
|
13
|
+
export const ACCEPT_PATTERNS = [
|
|
14
|
+
// English
|
|
15
|
+
/\baccept\s*all\b/i,
|
|
16
|
+
/\ballow\s*all\b/i,
|
|
17
|
+
/\bagree\s*to\s*all\b/i,
|
|
18
|
+
/\byes,?\s*i\s*agree\b/i,
|
|
19
|
+
/\bi\s*agree\b/i,
|
|
20
|
+
/\baccept\s*cookies?\b/i,
|
|
21
|
+
/\ballow\s*cookies?\b/i,
|
|
22
|
+
/\bgot\s*it\b/i,
|
|
23
|
+
// Dutch
|
|
24
|
+
/\balles\s*accepteren\b/i,
|
|
25
|
+
/\balles\s*toestaan\b/i,
|
|
26
|
+
/\baccepteren\b/i,
|
|
27
|
+
/\bakkoord\b/i,
|
|
28
|
+
// German
|
|
29
|
+
/\balle\s*akzeptieren\b/i,
|
|
30
|
+
/\ballem\s*zustimmen\b/i,
|
|
31
|
+
/\balle\s*cookies?\s*akzeptieren\b/i,
|
|
32
|
+
// French
|
|
33
|
+
/\btout\s*accepter\b/i,
|
|
34
|
+
/\baccepter\s*tout\b/i,
|
|
35
|
+
/\bj['']accepte\b/i,
|
|
36
|
+
// Spanish
|
|
37
|
+
/\baceptar\s*todo\b/i,
|
|
38
|
+
/\baceptar\s*todas?\b/i,
|
|
39
|
+
// Italian
|
|
40
|
+
/\baccetta\s*tutto\b/i,
|
|
41
|
+
/\baccetto\b/i,
|
|
42
|
+
// Portuguese
|
|
43
|
+
/\baceitar\s*tudo\b/i,
|
|
44
|
+
// Russian
|
|
45
|
+
/принять\s*все/i,
|
|
46
|
+
/принять/i,
|
|
47
|
+
/согласен/i,
|
|
48
|
+
// Ukrainian
|
|
49
|
+
/прийняти\s*все/i,
|
|
50
|
+
/прийняти/i,
|
|
51
|
+
// Polish
|
|
52
|
+
/zaakceptuj\s*wszystk/i,
|
|
53
|
+
/akceptuj\s*wszystk/i,
|
|
54
|
+
/zgadzam\s*się/i,
|
|
55
|
+
// Czech
|
|
56
|
+
/přijmout\s*vše/i,
|
|
57
|
+
/souhlasím/i,
|
|
58
|
+
// Turkish
|
|
59
|
+
/tümünü\s*kabul\s*et/i,
|
|
60
|
+
/kabul\s*et/i,
|
|
61
|
+
/kabul\s*ediyorum/i,
|
|
62
|
+
// Romanian
|
|
63
|
+
/acceptă\s*tot/i,
|
|
64
|
+
/accept\s*toate/i,
|
|
65
|
+
// Hungarian
|
|
66
|
+
/összes\s*elfogadás/i,
|
|
67
|
+
/elfogad/i,
|
|
68
|
+
// Greek
|
|
69
|
+
/αποδοχή\s*όλων/i,
|
|
70
|
+
/αποδέχομαι/i,
|
|
71
|
+
// Swedish
|
|
72
|
+
/acceptera\s*alla/i,
|
|
73
|
+
/godkänn\s*alla/i,
|
|
74
|
+
// Danish
|
|
75
|
+
/accepter\s*alle/i,
|
|
76
|
+
/acceptér\s*alle/i,
|
|
77
|
+
// Norwegian
|
|
78
|
+
/godta\s*alle/i,
|
|
79
|
+
/aksepter\s*alle/i,
|
|
80
|
+
// Finnish
|
|
81
|
+
/hyväksy\s*kaikki/i,
|
|
82
|
+
/hyväksyn/i,
|
|
83
|
+
// Arabic
|
|
84
|
+
/قبول\s*الكل/,
|
|
85
|
+
/قبول\s*الجميع/,
|
|
86
|
+
/موافق/,
|
|
87
|
+
/قبول/,
|
|
88
|
+
// Persian
|
|
89
|
+
/پذیرش\s*همه/,
|
|
90
|
+
/موافقم/,
|
|
91
|
+
/پذیرش/,
|
|
92
|
+
// Chinese (Simplified + Traditional)
|
|
93
|
+
/全部接受/,
|
|
94
|
+
/接受所有/,
|
|
95
|
+
/接受全部/,
|
|
96
|
+
/同意并继续/,
|
|
97
|
+
/全部接受/,
|
|
98
|
+
/接受/,
|
|
99
|
+
/同意/,
|
|
100
|
+
// Japanese
|
|
101
|
+
/すべて受け入れ/,
|
|
102
|
+
/すべて許可/,
|
|
103
|
+
/同意する/,
|
|
104
|
+
/同意します/,
|
|
105
|
+
// Korean
|
|
106
|
+
/모두\s*수락/,
|
|
107
|
+
/모두\s*동의/,
|
|
108
|
+
/동의합니다/,
|
|
109
|
+
/수락/,
|
|
110
|
+
// Vietnamese
|
|
111
|
+
/chấp\s*nhận\s*tất\s*cả/i,
|
|
112
|
+
/đồng\s*ý\s*tất\s*cả/i,
|
|
113
|
+
/đồng\s*ý/i,
|
|
114
|
+
// Thai
|
|
115
|
+
/ยอมรับทั้งหมด/,
|
|
116
|
+
/ยอมรับ/,
|
|
117
|
+
// Hindi
|
|
118
|
+
/सभी\s*स्वीकार/,
|
|
119
|
+
/स्वीकार\s*करें/,
|
|
120
|
+
/सहमत/,
|
|
121
|
+
// Indonesian / Malay
|
|
122
|
+
/terima\s*semua/i,
|
|
123
|
+
/setuju/i,
|
|
124
|
+
// Generic single-word fallbacks (only matched inside dialogs)
|
|
125
|
+
/^accept$/i,
|
|
126
|
+
/^agree$/i,
|
|
127
|
+
/^ok$/i,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// Roles that indicate a consent dialog container.
|
|
131
|
+
export const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
|
|
132
|
+
|
|
133
|
+
// Text patterns in dialog names/headings that confirm it's about consent.
|
|
134
|
+
export const CONSENT_DIALOG_HINTS = [
|
|
135
|
+
/cookie/i,
|
|
136
|
+
/consent/i,
|
|
137
|
+
/privacy/i,
|
|
138
|
+
/before\s*you\s*continue/i,
|
|
139
|
+
/voordat\s*je\s*verdergaat/i, // Dutch
|
|
140
|
+
/bevor\s*du\s*fortf/i, // German
|
|
141
|
+
/avant\s*de\s*continuer/i, // French
|
|
142
|
+
/antes\s*de\s*continuar/i, // Spanish / Portuguese
|
|
143
|
+
/prima\s*di\s*continuare/i, // Italian
|
|
144
|
+
/zanim\s*przejdziesz/i, // Polish
|
|
145
|
+
/прежде\s*чем\s*продолжить/i, // Russian
|
|
146
|
+
/devam\s*etmeden\s*önce/i, // Turkish
|
|
147
|
+
/続行する前に/, // Japanese
|
|
148
|
+
/继续前/, // Chinese Simplified
|
|
149
|
+
/繼續前/, // Chinese Traditional
|
|
150
|
+
/계속하기\s*전에/, // Korean
|
|
151
|
+
/trước\s*khi\s*tiếp\s*tục/i, // Vietnamese
|
|
152
|
+
/ملفات\s*تعريف\s*الارتباط/, // Arabic: cookies
|
|
153
|
+
/คุกกี้/, // Thai: cookies
|
|
154
|
+
];
|
package/src/consent.js
CHANGED
|
@@ -4,152 +4,13 @@
|
|
|
4
4
|
* Scans the page ARIA tree for consent dialogs and clicks the "accept" button.
|
|
5
5
|
* Works across languages by matching common accept/agree patterns.
|
|
6
6
|
* Runs once after page load — no polling, no mutation observers.
|
|
7
|
+
*
|
|
8
|
+
* The multilingual pattern sets live in consent-patterns.js (shared with the
|
|
9
|
+
* Firefox/BiDi walker in consent-firefox.js); this file owns the CDP-specific
|
|
10
|
+
* tree walk + click.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
// Order matters: more specific patterns first to avoid false positives.
|
|
11
|
-
const ACCEPT_PATTERNS = [
|
|
12
|
-
// English
|
|
13
|
-
/\baccept\s*all\b/i,
|
|
14
|
-
/\ballow\s*all\b/i,
|
|
15
|
-
/\bagree\s*to\s*all\b/i,
|
|
16
|
-
/\byes,?\s*i\s*agree\b/i,
|
|
17
|
-
/\bi\s*agree\b/i,
|
|
18
|
-
/\baccept\s*cookies?\b/i,
|
|
19
|
-
/\ballow\s*cookies?\b/i,
|
|
20
|
-
/\bgot\s*it\b/i,
|
|
21
|
-
// Dutch
|
|
22
|
-
/\balles\s*accepteren\b/i,
|
|
23
|
-
/\balles\s*toestaan\b/i,
|
|
24
|
-
/\baccepteren\b/i,
|
|
25
|
-
/\bakkoord\b/i,
|
|
26
|
-
// German
|
|
27
|
-
/\balle\s*akzeptieren\b/i,
|
|
28
|
-
/\ballem\s*zustimmen\b/i,
|
|
29
|
-
/\balle\s*cookies?\s*akzeptieren\b/i,
|
|
30
|
-
// French
|
|
31
|
-
/\btout\s*accepter\b/i,
|
|
32
|
-
/\baccepter\s*tout\b/i,
|
|
33
|
-
/\bj['']accepte\b/i,
|
|
34
|
-
// Spanish
|
|
35
|
-
/\baceptar\s*todo\b/i,
|
|
36
|
-
/\baceptar\s*todas?\b/i,
|
|
37
|
-
// Italian
|
|
38
|
-
/\baccetta\s*tutto\b/i,
|
|
39
|
-
/\baccetto\b/i,
|
|
40
|
-
// Portuguese
|
|
41
|
-
/\baceitar\s*tudo\b/i,
|
|
42
|
-
// Russian
|
|
43
|
-
/принять\s*все/i,
|
|
44
|
-
/принять/i,
|
|
45
|
-
/согласен/i,
|
|
46
|
-
// Ukrainian
|
|
47
|
-
/прийняти\s*все/i,
|
|
48
|
-
/прийняти/i,
|
|
49
|
-
// Polish
|
|
50
|
-
/zaakceptuj\s*wszystk/i,
|
|
51
|
-
/akceptuj\s*wszystk/i,
|
|
52
|
-
/zgadzam\s*się/i,
|
|
53
|
-
// Czech
|
|
54
|
-
/přijmout\s*vše/i,
|
|
55
|
-
/souhlasím/i,
|
|
56
|
-
// Turkish
|
|
57
|
-
/tümünü\s*kabul\s*et/i,
|
|
58
|
-
/kabul\s*et/i,
|
|
59
|
-
/kabul\s*ediyorum/i,
|
|
60
|
-
// Romanian
|
|
61
|
-
/acceptă\s*tot/i,
|
|
62
|
-
/accept\s*toate/i,
|
|
63
|
-
// Hungarian
|
|
64
|
-
/összes\s*elfogadás/i,
|
|
65
|
-
/elfogad/i,
|
|
66
|
-
// Greek
|
|
67
|
-
/αποδοχή\s*όλων/i,
|
|
68
|
-
/αποδέχομαι/i,
|
|
69
|
-
// Swedish
|
|
70
|
-
/acceptera\s*alla/i,
|
|
71
|
-
/godkänn\s*alla/i,
|
|
72
|
-
// Danish
|
|
73
|
-
/accepter\s*alle/i,
|
|
74
|
-
/acceptér\s*alle/i,
|
|
75
|
-
// Norwegian
|
|
76
|
-
/godta\s*alle/i,
|
|
77
|
-
/aksepter\s*alle/i,
|
|
78
|
-
// Finnish
|
|
79
|
-
/hyväksy\s*kaikki/i,
|
|
80
|
-
/hyväksyn/i,
|
|
81
|
-
// Arabic
|
|
82
|
-
/قبول\s*الكل/,
|
|
83
|
-
/قبول\s*الجميع/,
|
|
84
|
-
/موافق/,
|
|
85
|
-
/قبول/,
|
|
86
|
-
// Persian
|
|
87
|
-
/پذیرش\s*همه/,
|
|
88
|
-
/موافقم/,
|
|
89
|
-
/پذیرش/,
|
|
90
|
-
// Chinese (Simplified + Traditional)
|
|
91
|
-
/全部接受/,
|
|
92
|
-
/接受所有/,
|
|
93
|
-
/接受全部/,
|
|
94
|
-
/同意并继续/,
|
|
95
|
-
/全部接受/,
|
|
96
|
-
/接受/,
|
|
97
|
-
/同意/,
|
|
98
|
-
// Japanese
|
|
99
|
-
/すべて受け入れ/,
|
|
100
|
-
/すべて許可/,
|
|
101
|
-
/同意する/,
|
|
102
|
-
/同意します/,
|
|
103
|
-
// Korean
|
|
104
|
-
/모두\s*수락/,
|
|
105
|
-
/모두\s*동의/,
|
|
106
|
-
/동의합니다/,
|
|
107
|
-
/수락/,
|
|
108
|
-
// Vietnamese
|
|
109
|
-
/chấp\s*nhận\s*tất\s*cả/i,
|
|
110
|
-
/đồng\s*ý\s*tất\s*cả/i,
|
|
111
|
-
/đồng\s*ý/i,
|
|
112
|
-
// Thai
|
|
113
|
-
/ยอมรับทั้งหมด/,
|
|
114
|
-
/ยอมรับ/,
|
|
115
|
-
// Hindi
|
|
116
|
-
/सभी\s*स्वीकार/,
|
|
117
|
-
/स्वीकार\s*करें/,
|
|
118
|
-
/सहमत/,
|
|
119
|
-
// Indonesian / Malay
|
|
120
|
-
/terima\s*semua/i,
|
|
121
|
-
/setuju/i,
|
|
122
|
-
// Generic single-word fallbacks (only matched inside dialogs)
|
|
123
|
-
/^accept$/i,
|
|
124
|
-
/^agree$/i,
|
|
125
|
-
/^ok$/i,
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
// Roles that indicate a consent dialog container.
|
|
129
|
-
const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
|
|
130
|
-
|
|
131
|
-
// Text patterns in dialog names/headings that confirm it's about consent.
|
|
132
|
-
const CONSENT_DIALOG_HINTS = [
|
|
133
|
-
/cookie/i,
|
|
134
|
-
/consent/i,
|
|
135
|
-
/privacy/i,
|
|
136
|
-
/before\s*you\s*continue/i,
|
|
137
|
-
/voordat\s*je\s*verdergaat/i, // Dutch
|
|
138
|
-
/bevor\s*du\s*fortf/i, // German
|
|
139
|
-
/avant\s*de\s*continuer/i, // French
|
|
140
|
-
/antes\s*de\s*continuar/i, // Spanish / Portuguese
|
|
141
|
-
/prima\s*di\s*continuare/i, // Italian
|
|
142
|
-
/zanim\s*przejdziesz/i, // Polish
|
|
143
|
-
/прежде\s*чем\s*продолжить/i, // Russian
|
|
144
|
-
/devam\s*etmeden\s*önce/i, // Turkish
|
|
145
|
-
/続行する前に/, // Japanese
|
|
146
|
-
/继续前/, // Chinese Simplified
|
|
147
|
-
/繼續前/, // Chinese Traditional
|
|
148
|
-
/계속하기\s*전에/, // Korean
|
|
149
|
-
/trước\s*khi\s*tiếp\s*tục/i, // Vietnamese
|
|
150
|
-
/ملفات\s*تعريف\s*الارتباط/, // Arabic: cookies
|
|
151
|
-
/คุกกี้/, // Thai: cookies
|
|
152
|
-
];
|
|
13
|
+
import { ACCEPT_PATTERNS, DIALOG_ROLES, CONSENT_DIALOG_HINTS } from './consent-patterns.js';
|
|
153
14
|
|
|
154
15
|
/**
|
|
155
16
|
* Click a node via JavaScript .click() instead of mouse events.
|
package/src/daemon.js
CHANGED
|
@@ -31,24 +31,25 @@ function tokenMatches(expected, got) {
|
|
|
31
31
|
const SESSION_FILE = 'session.json';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Build the argv for the detached daemon child. Pure + exported so the
|
|
35
|
+
* flag-forwarding contract is unit-testable: a forward that's silently
|
|
36
|
+
* dropped here (as `--incognito` was in v0.15.0, turning `open --incognito`
|
|
37
|
+
* into a fully-authenticated session) becomes a failing test, not a leak.
|
|
38
|
+
* @param {object} opts - the session options parsed in cli.js cmdOpen
|
|
39
|
+
* @param {string} absDir - resolved output directory
|
|
40
|
+
* @param {string|undefined} initialUrl - URL to navigate on start (may be undefined)
|
|
41
|
+
* @param {string} cliPath - path to cli.js for the child to run
|
|
42
|
+
* @returns {string[]}
|
|
36
43
|
*/
|
|
37
|
-
export
|
|
38
|
-
const
|
|
39
|
-
mkdirSync(absDir, { recursive: true, mode: 0o700 });
|
|
40
|
-
|
|
41
|
-
// Clean stale session
|
|
42
|
-
const sessionPath = join(absDir, SESSION_FILE);
|
|
43
|
-
if (existsSync(sessionPath)) unlinkSync(sessionPath);
|
|
44
|
-
|
|
45
|
-
// Build child args
|
|
46
|
-
const args = [join(import.meta.dirname, '..', 'cli.js'), '--daemon-internal'];
|
|
44
|
+
export function buildDaemonArgs(opts, absDir, initialUrl, cliPath) {
|
|
45
|
+
const args = [cliPath, '--daemon-internal'];
|
|
47
46
|
args.push('--output-dir', absDir);
|
|
48
47
|
if (initialUrl) args.push('--url', initialUrl);
|
|
48
|
+
if (opts.engine) args.push('--engine', opts.engine);
|
|
49
49
|
if (opts.mode) args.push('--mode', opts.mode);
|
|
50
50
|
if (opts.port) args.push('--port', String(opts.port));
|
|
51
51
|
if (opts.cookies === false) args.push('--no-cookies');
|
|
52
|
+
if (opts.incognito) args.push('--incognito');
|
|
52
53
|
if (opts.browser) args.push('--browser', opts.browser);
|
|
53
54
|
if (opts.timeout) args.push('--timeout', String(opts.timeout));
|
|
54
55
|
if (opts.pruneMode) args.push('--prune-mode', opts.pruneMode);
|
|
@@ -63,6 +64,91 @@ export async function startDaemon(opts, outputDir, initialUrl) {
|
|
|
63
64
|
}
|
|
64
65
|
if (opts.blockPrivateNetwork) args.push('--block-private-network');
|
|
65
66
|
if (opts.uploadDir) args.push('--upload-dir', opts.uploadDir);
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wire Firefox/BiDi console + network capture onto the daemon's log arrays.
|
|
72
|
+
* The BiDi analogue of the CDP `Runtime.consoleAPICalled` / `Network.*`
|
|
73
|
+
* capture — measured event shapes: `log.entryAdded` carries {method, level,
|
|
74
|
+
* args:[{type,value}], text}; the network events key in-flight requests by
|
|
75
|
+
* `request.request` and expose `response.{status,statusText,mimeType}` /
|
|
76
|
+
* top-level `errorText`. Pure + exported so the shape mapping (warn→warning
|
|
77
|
+
* normalization, arg extraction, orphan-safe response/error pairing) is
|
|
78
|
+
* unit-testable against a fake bidi without launching Firefox.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} bidi - BiDi client (async .subscribe(), .on(method, cb))
|
|
81
|
+
* @param {object} sinks
|
|
82
|
+
* @param {Array} sinks.consoleLogs - push-target for console entries
|
|
83
|
+
* @param {Array} sinks.networkLogs - push-target for completed/failed requests
|
|
84
|
+
* @param {Map} sinks.pendingRequests - in-flight request bookkeeping
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
export async function attachBiDiCapture(bidi, { consoleLogs, networkLogs, pendingRequests }) {
|
|
88
|
+
await bidi.subscribe([
|
|
89
|
+
'log.entryAdded',
|
|
90
|
+
'network.beforeRequestSent',
|
|
91
|
+
'network.responseCompleted',
|
|
92
|
+
'network.fetchError',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
bidi.on('log.entryAdded', (e) => {
|
|
96
|
+
consoleLogs.push({
|
|
97
|
+
// Map BiDi's console method (or level for uncaught JS errors) to CDP's
|
|
98
|
+
// `type` vocabulary so `console-logs --level warning` matches on both
|
|
99
|
+
// engines — BiDi says 'warn', CDP says 'warning'.
|
|
100
|
+
type: e.method === 'warn' ? 'warning' : (e.method || e.level),
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
args: Array.isArray(e.args) && e.args.length
|
|
103
|
+
? e.args.map((a) => a.value ?? a.type)
|
|
104
|
+
: [e.text],
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
bidi.on('network.beforeRequestSent', (e) => {
|
|
109
|
+
pendingRequests.set(e.request.request, {
|
|
110
|
+
url: e.request.url,
|
|
111
|
+
method: e.request.method,
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
bidi.on('network.responseCompleted', (e) => {
|
|
117
|
+
const req = pendingRequests.get(e.request.request);
|
|
118
|
+
if (req) {
|
|
119
|
+
networkLogs.push({
|
|
120
|
+
...req,
|
|
121
|
+
status: e.response.status,
|
|
122
|
+
statusText: e.response.statusText,
|
|
123
|
+
mimeType: e.response.mimeType,
|
|
124
|
+
});
|
|
125
|
+
pendingRequests.delete(e.request.request);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
bidi.on('network.fetchError', (e) => {
|
|
130
|
+
const req = pendingRequests.get(e.request.request);
|
|
131
|
+
if (req) {
|
|
132
|
+
networkLogs.push({ ...req, status: 0, error: e.errorText });
|
|
133
|
+
pendingRequests.delete(e.request.request);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Spawn a detached child process that runs the daemon.
|
|
140
|
+
* Parent polls for session.json, then exits.
|
|
141
|
+
*/
|
|
142
|
+
export async function startDaemon(opts, outputDir, initialUrl) {
|
|
143
|
+
const absDir = resolve(outputDir);
|
|
144
|
+
mkdirSync(absDir, { recursive: true, mode: 0o700 });
|
|
145
|
+
|
|
146
|
+
// Clean stale session
|
|
147
|
+
const sessionPath = join(absDir, SESSION_FILE);
|
|
148
|
+
if (existsSync(sessionPath)) unlinkSync(sessionPath);
|
|
149
|
+
|
|
150
|
+
// Build child args (pure + testable — see buildDaemonArgs).
|
|
151
|
+
const args = buildDaemonArgs(opts, absDir, initialUrl, join(import.meta.dirname, '..', 'cli.js'));
|
|
66
152
|
|
|
67
153
|
const child = spawn(process.execPath, args, {
|
|
68
154
|
detached: true,
|
|
@@ -104,6 +190,7 @@ export async function runDaemon(opts, outputDir, initialUrl) {
|
|
|
104
190
|
|
|
105
191
|
// Connect to browser
|
|
106
192
|
const page = await connect({
|
|
193
|
+
engine: opts.engine,
|
|
107
194
|
mode: opts.mode || 'headless',
|
|
108
195
|
port: opts.port ? Number(opts.port) : undefined,
|
|
109
196
|
consent: opts.consent,
|
|
@@ -115,55 +202,65 @@ export async function runDaemon(opts, outputDir, initialUrl) {
|
|
|
115
202
|
blockUrls: opts.blockUrls,
|
|
116
203
|
blockPrivateNetwork: opts.blockPrivateNetwork,
|
|
117
204
|
uploadDir: opts.uploadDir,
|
|
205
|
+
// incognito neuters injectCookies() session-wide; cookies:false (below)
|
|
206
|
+
// is the legacy per-open equivalent. Either yields a logged-out session.
|
|
207
|
+
incognito: opts.incognito || opts.cookies === false,
|
|
118
208
|
});
|
|
119
209
|
|
|
120
|
-
// Console log capture
|
|
210
|
+
// Console + network log capture. CDP uses Runtime/Network events off
|
|
211
|
+
// page.cdp; the Firefox/BiDi engine (page.bidi) captures the same shape over
|
|
212
|
+
// log.entryAdded / network.* events (Phase 2 — attachBiDiCapture). Either
|
|
213
|
+
// way console-logs / network-log / wait-idle behave the same for both.
|
|
121
214
|
const consoleLogs = [];
|
|
122
|
-
await page.cdp.send('Runtime.enable');
|
|
123
|
-
page.cdp.on('Runtime.consoleAPICalled', (params) => {
|
|
124
|
-
consoleLogs.push({
|
|
125
|
-
type: params.type,
|
|
126
|
-
timestamp: new Date().toISOString(),
|
|
127
|
-
args: params.args.map((a) => a.value ?? a.description ?? a.type),
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Network log capture (Network.enable already called by connect)
|
|
132
215
|
const networkLogs = [];
|
|
133
216
|
const pendingRequests = new Map();
|
|
134
217
|
|
|
135
|
-
page.cdp
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
218
|
+
if (page.cdp) {
|
|
219
|
+
await page.cdp.send('Runtime.enable');
|
|
220
|
+
page.cdp.on('Runtime.consoleAPICalled', (params) => {
|
|
221
|
+
consoleLogs.push({
|
|
222
|
+
type: params.type,
|
|
223
|
+
timestamp: new Date().toISOString(),
|
|
224
|
+
args: params.args.map((a) => a.value ?? a.description ?? a.type),
|
|
225
|
+
});
|
|
140
226
|
});
|
|
141
|
-
});
|
|
142
227
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
statusText: params.response.statusText,
|
|
150
|
-
mimeType: params.response.mimeType,
|
|
228
|
+
// Network log capture (Network.enable already called by connect)
|
|
229
|
+
page.cdp.on('Network.requestWillBeSent', (params) => {
|
|
230
|
+
pendingRequests.set(params.requestId, {
|
|
231
|
+
url: params.request.url,
|
|
232
|
+
method: params.request.method,
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
151
234
|
});
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
});
|
|
235
|
+
});
|
|
155
236
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
237
|
+
page.cdp.on('Network.responseReceived', (params) => {
|
|
238
|
+
const req = pendingRequests.get(params.requestId);
|
|
239
|
+
if (req) {
|
|
240
|
+
networkLogs.push({
|
|
241
|
+
...req,
|
|
242
|
+
status: params.response.status,
|
|
243
|
+
statusText: params.response.statusText,
|
|
244
|
+
mimeType: params.response.mimeType,
|
|
245
|
+
});
|
|
246
|
+
pendingRequests.delete(params.requestId);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
page.cdp.on('Network.loadingFailed', (params) => {
|
|
251
|
+
const req = pendingRequests.get(params.requestId);
|
|
252
|
+
if (req) {
|
|
253
|
+
networkLogs.push({
|
|
254
|
+
...req,
|
|
255
|
+
status: 0,
|
|
256
|
+
error: params.errorText,
|
|
257
|
+
});
|
|
258
|
+
pendingRequests.delete(params.requestId);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
} else if (page.bidi) {
|
|
262
|
+
await attachBiDiCapture(page.bidi, { consoleLogs, networkLogs, pendingRequests });
|
|
263
|
+
}
|
|
167
264
|
|
|
168
265
|
// Navigate to initial URL if provided
|
|
169
266
|
if (initialUrl) {
|
|
@@ -316,6 +413,20 @@ export async function runDaemon(opts, outputDir, initialUrl) {
|
|
|
316
413
|
},
|
|
317
414
|
|
|
318
415
|
async eval({ expression }) {
|
|
416
|
+
// Firefox/BiDi has no page.cdp — evaluate over BiDi in the active context.
|
|
417
|
+
if (!page.cdp) {
|
|
418
|
+
try {
|
|
419
|
+
// BiDi's raw result.value serializes objects into a {type,value}
|
|
420
|
+
// entries structure, not a plain object (CDP's returnByValue does).
|
|
421
|
+
// Stringify + parse in-page so an object/array eval matches the CDP
|
|
422
|
+
// shape (same trick readable() uses). `undefined` → JSON `null`.
|
|
423
|
+
const wrapped = `(async () => { const v = await (${expression}); return JSON.stringify(v === undefined ? null : v); })()`;
|
|
424
|
+
const raw = await page.bidi.evaluate(page.context, wrapped, true);
|
|
425
|
+
return { ok: true, value: raw == null ? null : JSON.parse(raw) };
|
|
426
|
+
} catch (err) {
|
|
427
|
+
return { ok: false, error: err.message || 'eval error' };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
319
430
|
const result = await page.cdp.send('Runtime.evaluate', {
|
|
320
431
|
expression,
|
|
321
432
|
returnByValue: true,
|