claude-chrome-parallel 3.4.0 → 3.4.2
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/README.md +114 -48
- package/dist/cdp/client.d.ts +17 -4
- package/dist/cdp/client.d.ts.map +1 -1
- package/dist/cdp/client.js +144 -100
- package/dist/cdp/client.js.map +1 -1
- package/dist/cdp/connection-pool.d.ts +5 -1
- package/dist/cdp/connection-pool.d.ts.map +1 -1
- package/dist/cdp/connection-pool.js +40 -38
- package/dist/cdp/connection-pool.js.map +1 -1
- package/dist/chrome/launcher.js +2 -2
- package/dist/chrome/launcher.js.map +1 -1
- package/dist/dashboard/activity-tracker.d.ts +16 -0
- package/dist/dashboard/activity-tracker.d.ts.map +1 -1
- package/dist/dashboard/activity-tracker.js +49 -8
- package/dist/dashboard/activity-tracker.js.map +1 -1
- package/dist/hints/hint-engine.d.ts +13 -1
- package/dist/hints/hint-engine.d.ts.map +1 -1
- package/dist/hints/hint-engine.js +41 -6
- package/dist/hints/hint-engine.js.map +1 -1
- package/dist/hints/rules/error-recovery.d.ts.map +1 -1
- package/dist/hints/rules/error-recovery.js +4 -0
- package/dist/hints/rules/error-recovery.js.map +1 -1
- package/dist/hints/rules/sequence-detection.d.ts.map +1 -1
- package/dist/hints/rules/sequence-detection.js +28 -0
- package/dist/hints/rules/sequence-detection.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +18 -4
- package/dist/mcp-server.js.map +1 -1
- package/dist/orchestration/state-manager.d.ts +12 -2
- package/dist/orchestration/state-manager.d.ts.map +1 -1
- package/dist/orchestration/state-manager.js +24 -21
- package/dist/orchestration/state-manager.js.map +1 -1
- package/dist/orchestration/workflow-engine.d.ts +34 -4
- package/dist/orchestration/workflow-engine.d.ts.map +1 -1
- package/dist/orchestration/workflow-engine.js +235 -54
- package/dist/orchestration/workflow-engine.js.map +1 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +67 -7
- package/dist/session-manager.js.map +1 -1
- package/dist/tools/click-element.d.ts.map +1 -1
- package/dist/tools/click-element.js +112 -40
- package/dist/tools/click-element.js.map +1 -1
- package/dist/tools/computer.d.ts.map +1 -1
- package/dist/tools/computer.js +7 -7
- package/dist/tools/computer.js.map +1 -1
- package/dist/tools/javascript.d.ts.map +1 -1
- package/dist/tools/javascript.js +6 -2
- package/dist/tools/javascript.js.map +1 -1
- package/dist/tools/orchestration.d.ts.map +1 -1
- package/dist/tools/orchestration.js +51 -0
- package/dist/tools/orchestration.js.map +1 -1
- package/dist/tools/read-page.d.ts.map +1 -1
- package/dist/tools/read-page.js +52 -8
- package/dist/tools/read-page.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Claude Chrome Parallel
|
|
1
|
+
# Claude Chrome Parallel (CCP)
|
|
2
2
|
|
|
3
3
|
> **Ultrafast parallel browser MCP.**
|
|
4
4
|
|
|
@@ -37,7 +37,16 @@ CCP (parallel, zero auth):
|
|
|
37
37
|
|
|
38
38
|
## Why CCP Is Fast
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
This is not a speed optimization. It's a **structural change**.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Playwright MCP: [blank browser] → login → task → close (repeat per site)
|
|
44
|
+
CCP: [your Chrome] → task (already logged in)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Playwright creates a new browser per site. Each one needs: navigate → type email → type password → solve 2FA → wait for redirect. That's 30-120s per site, and it's sequential. **You're spending 95% of the time on authentication, not the actual task.**
|
|
48
|
+
|
|
49
|
+
CCP connects to your existing Chrome via CDP. You're already logged in to everything. Workers run in parallel. The speed advantage **compounds** with every site:
|
|
41
50
|
|
|
42
51
|
| Sites | Playwright MCP | CCP | Speedup |
|
|
43
52
|
|:-----:|:--------------:|:---:|:-------:|
|
|
@@ -46,16 +55,18 @@ The speed advantage **compounds** with every site. Playwright MCP has two bottle
|
|
|
46
55
|
| 5 | ~250s | ~3s | **80x** |
|
|
47
56
|
| 10 | ~500s | ~3s | **160x** |
|
|
48
57
|
|
|
49
|
-
**Why?** Playwright launches a blank browser per site. Each needs: navigate → type email → type password → solve 2FA → wait for redirect. That's 30-120s **per site**, and it's **sequential**.
|
|
50
|
-
|
|
51
|
-
CCP connects to your existing Chrome. You're already logged into everything. Workers run in parallel. The task that takes 250s with Playwright takes 3s with CCP.
|
|
52
|
-
|
|
53
58
|
### Memory
|
|
54
59
|
|
|
55
60
|
Playwright spawns a **separate browser process** per session (~500MB each). Five sites = 2.5GB.
|
|
56
61
|
|
|
57
62
|
CCP uses **one Chrome** with lightweight browser contexts (like incognito windows sharing the same process). Five Workers = ~300MB total. That's **8x less memory** — and it stays flat whether you run 5 or 20 Workers.
|
|
58
63
|
|
|
64
|
+
### Bot Detection Immunity
|
|
65
|
+
|
|
66
|
+
Playwright runs headless browsers with detectable fingerprints. Cloudflare, reCAPTCHA, and anti-bot systems can flag them.
|
|
67
|
+
|
|
68
|
+
CCP uses **your actual Chrome** — real fingerprint, real cookies, real browsing history. It's indistinguishable from you clicking around manually, because it literally is your browser.
|
|
69
|
+
|
|
59
70
|
---
|
|
60
71
|
|
|
61
72
|
## Core Features
|
|
@@ -168,54 +179,84 @@ Claude: [3 Workers, 3 sites, simultaneously]
|
|
|
168
179
|
|
|
169
180
|
## What You Can Do
|
|
170
181
|
|
|
171
|
-
###
|
|
182
|
+
### 20-Site Parallel Crawling
|
|
172
183
|
|
|
173
184
|
```
|
|
174
|
-
You: ccp
|
|
175
|
-
|
|
176
|
-
Claude: [
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
You: ccp crawl these 20 competitor sites and extract their pricing
|
|
186
|
+
|
|
187
|
+
Claude: [20 Workers, 20 sites, simultaneously — all in your logged-in Chrome]
|
|
188
|
+
site-01: $49/mo ✓ (1.2s)
|
|
189
|
+
site-02: $59/mo ✓ (0.9s)
|
|
190
|
+
...
|
|
191
|
+
site-20: $39/mo ✓ (1.4s)
|
|
192
|
+
Total: 2.8s | Sequential: ~60s | Speedup: 21x
|
|
180
193
|
```
|
|
181
194
|
|
|
182
|
-
|
|
195
|
+
Each Worker runs in an isolated browser context. One Chrome process, 20 parallel sessions, ~300MB total. Not 20 separate browsers eating 10GB of RAM.
|
|
183
196
|
|
|
184
|
-
|
|
185
|
-
You: ccp find the cheapest iPhone 15 on Amazon, eBay, and Walmart
|
|
197
|
+
### Multi-Cloud Dashboard Monitoring
|
|
186
198
|
|
|
187
|
-
Claude: [3 Workers, 3 sites, simultaneously]
|
|
188
|
-
Amazon: $999
|
|
189
|
-
eBay: $945 ← lowest
|
|
190
|
-
Walmart: $979
|
|
191
|
-
Time: 1.3s total (not 3.9s)
|
|
192
199
|
```
|
|
200
|
+
You: ccp screenshot my AWS billing, GCP console, Azure portal, Stripe,
|
|
201
|
+
and Datadog — all at once
|
|
202
|
+
|
|
203
|
+
Claude: [5 Workers — already logged into every cloud provider]
|
|
204
|
+
aws-billing.png $12,847/mo ✓
|
|
205
|
+
gcp-console.png $8,291/mo ✓
|
|
206
|
+
azure-portal.png $3,104/mo ✓
|
|
207
|
+
stripe-revenue.png $47,230 MRR ✓
|
|
208
|
+
datadog-metrics.png 99.7% uptime ✓
|
|
209
|
+
Time: 3.1s (not 10+ minutes of login screens)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
No OAuth tokens. No service accounts. No API keys to rotate. You're already logged in.
|
|
193
213
|
|
|
194
|
-
###
|
|
214
|
+
### Automated Regression Testing
|
|
195
215
|
|
|
216
|
+
```bash
|
|
217
|
+
# Full regression suite — 10 flows, 10 Workers, one command
|
|
218
|
+
claude -p "ccp test these 10 critical flows on staging.myapp.com:
|
|
219
|
+
login, signup, checkout, search, profile-edit,
|
|
220
|
+
password-reset, file-upload, notifications, admin-panel, API-docs"
|
|
221
|
+
|
|
222
|
+
# Sequential: ~15 minutes. CCP: ~90 seconds.
|
|
223
|
+
# Run this before every deploy.
|
|
196
224
|
```
|
|
197
|
-
You: ccp screenshot my AWS billing, Stripe dashboard, and Vercel usage
|
|
198
225
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
Each Worker gets an isolated session — no cookie contamination between test flows. Test multi-user scenarios (admin + regular user) simultaneously.
|
|
227
|
+
|
|
228
|
+
### Competitive Intelligence at Scale
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
You: ccp monitor pricing on Amazon, Walmart, Target, Best Buy, and Costco
|
|
232
|
+
for "Sony WH-1000XM5" — compare and find the lowest
|
|
233
|
+
|
|
234
|
+
Claude: [5 Workers, 5 retailers, parallel]
|
|
235
|
+
Amazon: $278
|
|
236
|
+
Walmart: $298
|
|
237
|
+
Target: $279
|
|
238
|
+
Best Buy: $249 ← lowest (sale)
|
|
239
|
+
Costco: $269 (members only)
|
|
240
|
+
Time: 2.4s | All prices from live pages, not cached APIs
|
|
203
241
|
```
|
|
204
242
|
|
|
205
|
-
|
|
243
|
+
Works on sites with bot detection because it's your real Chrome — real cookies, real fingerprint, real browsing history.
|
|
206
244
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
245
|
+
### Multi-Account Operations
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
You: ccp check order status on my personal and business Amazon accounts,
|
|
249
|
+
plus my eBay seller dashboard — all at the same time
|
|
250
|
+
|
|
251
|
+
Claude: [3 Workers, 3 isolated sessions]
|
|
252
|
+
Amazon Personal: 2 packages arriving tomorrow
|
|
253
|
+
Amazon Business: Purchase order #4521 approved
|
|
254
|
+
eBay Seller: 3 new orders, $847 revenue today
|
|
255
|
+
Time: 2.1s
|
|
217
256
|
```
|
|
218
257
|
|
|
258
|
+
Same site, different accounts, simultaneously. Each Worker has its own cookies and session state.
|
|
259
|
+
|
|
219
260
|
---
|
|
220
261
|
|
|
221
262
|
## Comparison
|
|
@@ -240,7 +281,9 @@ claude -p "ccp test form validation on myapp.com" # Worker 5
|
|
|
240
281
|
|
|
241
282
|
## Adaptive Guidance
|
|
242
283
|
|
|
243
|
-
|
|
284
|
+
The biggest time sink in LLM browser automation isn't execution speed — it's **wrong tool choices, missed page state, and pointless retries**. Each mistake costs 3-10 seconds of LLM inference. Three mistakes and you've wasted 30 seconds before anything useful happens.
|
|
285
|
+
|
|
286
|
+
CCP injects contextual `_hint` fields into every tool response to prevent this:
|
|
244
287
|
|
|
245
288
|
```
|
|
246
289
|
click_element → Error: "ref not found"
|
|
@@ -250,9 +293,13 @@ click_element → Error: "ref not found"
|
|
|
250
293
|
navigate → title contains "Login"
|
|
251
294
|
_hint: "Login page detected. Use fill_form for credentials."
|
|
252
295
|
→ LLM skips straight to form filling.
|
|
296
|
+
|
|
297
|
+
find → computer(click) pattern detected
|
|
298
|
+
_hint: "Use click_element to find+click in one call."
|
|
299
|
+
→ Eliminates unnecessary intermediate steps.
|
|
253
300
|
```
|
|
254
301
|
|
|
255
|
-
|
|
302
|
+
21 static rules across 6 priority tiers + an **adaptive memory** system that learns from your usage. When the same error→recovery pattern appears 3 times, it's promoted to a permanent hint — persisted across sessions in `.chrome-parallel/hints/learned-patterns.json`.
|
|
256
303
|
|
|
257
304
|
<details>
|
|
258
305
|
<summary>Rule priority tiers</summary>
|
|
@@ -356,16 +403,18 @@ navigate → title contains "Login"
|
|
|
356
403
|
## CLI
|
|
357
404
|
|
|
358
405
|
```bash
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
406
|
+
ccp setup # Auto-configure (global)
|
|
407
|
+
ccp setup --scope project # Auto-configure (project only)
|
|
408
|
+
ccp serve --auto-launch # Start with auto Chrome launch
|
|
409
|
+
ccp serve --headless-shell # Headless mode (15-30% less memory)
|
|
410
|
+
ccp serve -p <port> # Custom debugging port (default: 9222)
|
|
411
|
+
ccp doctor # Diagnose installation
|
|
412
|
+
ccp status # View sessions
|
|
413
|
+
ccp cleanup # Clean up old sessions
|
|
367
414
|
```
|
|
368
415
|
|
|
416
|
+
> `ccp` requires global install (`npm i -g claude-chrome-parallel`). All commands also work via `npx claude-chrome-parallel <command>`.
|
|
417
|
+
|
|
369
418
|
---
|
|
370
419
|
|
|
371
420
|
<details>
|
|
@@ -389,6 +438,23 @@ cd claude-chrome-parallel
|
|
|
389
438
|
npm install && npm run build && npm test
|
|
390
439
|
```
|
|
391
440
|
|
|
441
|
+
## Compatibility
|
|
442
|
+
|
|
443
|
+
CCP is a standard **MCP server** (stdio JSON-RPC). While optimized for Claude Code, it works with any MCP-compatible client:
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"mcpServers": {
|
|
448
|
+
"chrome-parallel": {
|
|
449
|
+
"command": "npx",
|
|
450
|
+
"args": ["-y", "claude-chrome-parallel", "serve", "--auto-launch"]
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Cursor, Windsurf, Codex CLI, or any editor that supports MCP can use CCP with the config above. See [Contributing](CONTRIBUTING.md) for multi-client testing status.
|
|
457
|
+
|
|
392
458
|
## License
|
|
393
459
|
|
|
394
460
|
MIT — [LICENSE](LICENSE)
|
package/dist/cdp/client.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export declare class CDPClient {
|
|
|
32
32
|
private autoLaunch;
|
|
33
33
|
private cookieSourceCache;
|
|
34
34
|
private cookieDataCache;
|
|
35
|
+
private targetIdIndex;
|
|
36
|
+
private inFlightCookieScans;
|
|
35
37
|
private static readonly COOKIE_CACHE_TTL;
|
|
36
38
|
constructor(options?: CDPClientOptions);
|
|
37
39
|
/**
|
|
@@ -117,14 +119,25 @@ export declare class CDPClient {
|
|
|
117
119
|
*/
|
|
118
120
|
private domainMatchScore;
|
|
119
121
|
/**
|
|
120
|
-
* Find an authenticated page with cookies to copy from
|
|
121
|
-
* Returns the targetId of a page that has cookies in Chrome's default context
|
|
122
|
+
* Find an authenticated page with cookies to copy from.
|
|
123
|
+
* Returns the targetId of a page that has cookies in Chrome's default context.
|
|
124
|
+
*
|
|
125
|
+
* Promise coalescing: concurrent callers for the same domain share one probe
|
|
126
|
+
* instead of independently hammering Chrome with 20 simultaneous scans.
|
|
127
|
+
*
|
|
122
128
|
* @param targetDomain Optional domain to prioritize when selecting cookie source
|
|
123
129
|
*/
|
|
124
130
|
findAuthenticatedPageTargetId(targetDomain?: string): Promise<string | null>;
|
|
125
131
|
/**
|
|
126
|
-
*
|
|
127
|
-
* Uses
|
|
132
|
+
* Internal implementation of the authenticated-page probe.
|
|
133
|
+
* Uses Target.attachToTarget (multiplexed CDP) instead of raw WebSocket connections.
|
|
134
|
+
* Uses Target.getTargets result directly instead of /json/list HTTP calls.
|
|
135
|
+
*/
|
|
136
|
+
private _doFindAuthenticatedPageTargetId;
|
|
137
|
+
/**
|
|
138
|
+
* Copy all cookies from authenticated page to destination page.
|
|
139
|
+
* Uses Target.attachToTarget (multiplexed CDP) to bypass Puppeteer's context isolation —
|
|
140
|
+
* no raw WebSocket connections, no /json/list HTTP calls.
|
|
128
141
|
*/
|
|
129
142
|
copyCookiesViaCDP(sourceTargetId: string, destPage: Page): Promise<number>;
|
|
130
143
|
/**
|
package/dist/cdp/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/cdp/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAkB,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/cdp/client.ts"],"names":[],"mappings":"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"}
|
package/dist/cdp/client.js
CHANGED
|
@@ -32,7 +32,9 @@ class CDPClient {
|
|
|
32
32
|
autoLaunch;
|
|
33
33
|
cookieSourceCache = new Map();
|
|
34
34
|
cookieDataCache = new Map();
|
|
35
|
-
|
|
35
|
+
targetIdIndex = new Map();
|
|
36
|
+
inFlightCookieScans = new Map();
|
|
37
|
+
static COOKIE_CACHE_TTL = 300000; // 5 minutes
|
|
36
38
|
constructor(options = {}) {
|
|
37
39
|
const globalConfig = (0, global_1.getGlobalConfig)();
|
|
38
40
|
this.port = options.port || globalConfig.port;
|
|
@@ -82,6 +84,7 @@ class CDPClient {
|
|
|
82
84
|
}
|
|
83
85
|
// Clean up cookie data cache for this target
|
|
84
86
|
this.cookieDataCache.delete(targetId);
|
|
87
|
+
this.targetIdIndex.delete(targetId);
|
|
85
88
|
for (const listener of this.targetDestroyedListeners) {
|
|
86
89
|
try {
|
|
87
90
|
listener(targetId);
|
|
@@ -158,6 +161,8 @@ class CDPClient {
|
|
|
158
161
|
});
|
|
159
162
|
// Clear existing sessions
|
|
160
163
|
this.sessions.clear();
|
|
164
|
+
this.targetIdIndex.clear();
|
|
165
|
+
this.inFlightCookieScans.clear();
|
|
161
166
|
this.browser = null;
|
|
162
167
|
// Attempt reconnection
|
|
163
168
|
while (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
@@ -212,6 +217,20 @@ class CDPClient {
|
|
|
212
217
|
console.error(`[CDPClient] Target destroyed: ${targetId}`);
|
|
213
218
|
this.onTargetDestroyed(targetId);
|
|
214
219
|
});
|
|
220
|
+
// Maintain target-to-page index for O(1) lookups
|
|
221
|
+
this.browser.on('targetcreated', async (target) => {
|
|
222
|
+
if (target.type() === 'page') {
|
|
223
|
+
try {
|
|
224
|
+
const page = await target.page();
|
|
225
|
+
if (page) {
|
|
226
|
+
this.targetIdIndex.set(getTargetId(target), page);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Target may have been destroyed before we could index it
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
215
234
|
this.connectionState = 'connected';
|
|
216
235
|
this.emitConnectionEvent({
|
|
217
236
|
type: 'connected',
|
|
@@ -371,8 +390,12 @@ class CDPClient {
|
|
|
371
390
|
}
|
|
372
391
|
}
|
|
373
392
|
/**
|
|
374
|
-
* Find an authenticated page with cookies to copy from
|
|
375
|
-
* Returns the targetId of a page that has cookies in Chrome's default context
|
|
393
|
+
* Find an authenticated page with cookies to copy from.
|
|
394
|
+
* Returns the targetId of a page that has cookies in Chrome's default context.
|
|
395
|
+
*
|
|
396
|
+
* Promise coalescing: concurrent callers for the same domain share one probe
|
|
397
|
+
* instead of independently hammering Chrome with 20 simultaneous scans.
|
|
398
|
+
*
|
|
376
399
|
* @param targetDomain Optional domain to prioritize when selecting cookie source
|
|
377
400
|
*/
|
|
378
401
|
async findAuthenticatedPageTargetId(targetDomain) {
|
|
@@ -383,7 +406,28 @@ class CDPClient {
|
|
|
383
406
|
console.error(`[CDPClient] Cache hit for cookie source (domain: ${cacheKey}): ${cached.targetId.slice(0, 8)}`);
|
|
384
407
|
return cached.targetId;
|
|
385
408
|
}
|
|
386
|
-
|
|
409
|
+
// Promise coalescing: if a scan for this domain is already in-flight, reuse it
|
|
410
|
+
const existing = this.inFlightCookieScans.get(cacheKey);
|
|
411
|
+
if (existing) {
|
|
412
|
+
console.error(`[CDPClient] Coalescing cookie scan for domain: ${cacheKey}`);
|
|
413
|
+
return existing;
|
|
414
|
+
}
|
|
415
|
+
// Start the scan and register it so concurrent callers share this promise
|
|
416
|
+
const scanPromise = this._doFindAuthenticatedPageTargetId(targetDomain, cacheKey);
|
|
417
|
+
this.inFlightCookieScans.set(cacheKey, scanPromise);
|
|
418
|
+
try {
|
|
419
|
+
return await scanPromise;
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
this.inFlightCookieScans.delete(cacheKey);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Internal implementation of the authenticated-page probe.
|
|
427
|
+
* Uses Target.attachToTarget (multiplexed CDP) instead of raw WebSocket connections.
|
|
428
|
+
* Uses Target.getTargets result directly instead of /json/list HTTP calls.
|
|
429
|
+
*/
|
|
430
|
+
async _doFindAuthenticatedPageTargetId(targetDomain, cacheKey) {
|
|
387
431
|
const browser = this.getBrowser();
|
|
388
432
|
const session = await browser.target().createCDPSession();
|
|
389
433
|
try {
|
|
@@ -417,42 +461,34 @@ class CDPClient {
|
|
|
417
461
|
});
|
|
418
462
|
console.error(`[CDPClient] Sorted ${candidates.length} candidates by domain match to ${targetDomain}`);
|
|
419
463
|
}
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// Check each candidate to find one with actual cookies (in priority order)
|
|
464
|
+
// Check each candidate to find one with actual cookies (in priority order).
|
|
465
|
+
// Uses Target.attachToTarget over the existing multiplexed session — no raw WebSocket,
|
|
466
|
+
// no /json/list HTTP round-trip.
|
|
424
467
|
for (const candidate of candidates) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// while Puppeteer-created pages have empty cookies)
|
|
431
|
-
const cookieCount = await new Promise((resolve) => {
|
|
432
|
-
const ws = new WebSocket(targetInfo.webSocketDebuggerUrl);
|
|
433
|
-
const timeout = setTimeout(() => { ws.close(); resolve(0); }, 2000);
|
|
434
|
-
ws.on('open', () => {
|
|
435
|
-
ws.send(JSON.stringify({ id: 1, method: 'Network.getAllCookies' }));
|
|
436
|
-
});
|
|
437
|
-
ws.on('message', (data) => {
|
|
438
|
-
const msg = JSON.parse(data.toString());
|
|
439
|
-
if (msg.id === 1) {
|
|
440
|
-
clearTimeout(timeout);
|
|
441
|
-
ws.close();
|
|
442
|
-
resolve(msg.result?.cookies?.length || 0);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
ws.on('error', () => {
|
|
446
|
-
clearTimeout(timeout);
|
|
447
|
-
resolve(0);
|
|
468
|
+
let attachedSessionId = null;
|
|
469
|
+
try {
|
|
470
|
+
const { sessionId } = await session.send('Target.attachToTarget', {
|
|
471
|
+
targetId: candidate.targetId,
|
|
472
|
+
flatten: true,
|
|
448
473
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
474
|
+
attachedSessionId = sessionId;
|
|
475
|
+
// Send Network.getAllCookies through the flat CDP session
|
|
476
|
+
const result = await session.send('Network.getAllCookies', undefined, { sessionId });
|
|
477
|
+
const cookieCount = result?.cookies?.length || 0;
|
|
478
|
+
if (cookieCount > 0) {
|
|
479
|
+
const domainScore = targetDomain ? this.domainMatchScore(candidate.url, targetDomain) : 0;
|
|
480
|
+
console.error(`[CDPClient] Found authenticated page ${candidate.targetId.slice(0, 8)} at ${candidate.url.slice(0, 50)} (${cookieCount} cookies, domain score: ${domainScore})`);
|
|
481
|
+
this.cookieSourceCache.set(cacheKey, { targetId: candidate.targetId, timestamp: Date.now() });
|
|
482
|
+
return candidate.targetId;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// Target may be unresponsive or already detached — skip
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
if (attachedSessionId) {
|
|
490
|
+
await session.send('Target.detachFromTarget', { sessionId: attachedSessionId }).catch(() => { });
|
|
491
|
+
}
|
|
456
492
|
}
|
|
457
493
|
}
|
|
458
494
|
console.error('[CDPClient] No pages with cookies found');
|
|
@@ -463,17 +499,17 @@ class CDPClient {
|
|
|
463
499
|
}
|
|
464
500
|
}
|
|
465
501
|
/**
|
|
466
|
-
* Copy all cookies from authenticated page to destination page
|
|
467
|
-
* Uses
|
|
502
|
+
* Copy all cookies from authenticated page to destination page.
|
|
503
|
+
* Uses Target.attachToTarget (multiplexed CDP) to bypass Puppeteer's context isolation —
|
|
504
|
+
* no raw WebSocket connections, no /json/list HTTP calls.
|
|
468
505
|
*/
|
|
469
506
|
async copyCookiesViaCDP(sourceTargetId, destPage) {
|
|
470
|
-
const WebSocket = require('ws');
|
|
471
507
|
console.error(`[CDPClient] copyCookiesViaCDP called with sourceTargetId: ${sourceTargetId.slice(0, 8)}`);
|
|
472
508
|
try {
|
|
473
|
-
// Check cookie data cache first
|
|
509
|
+
// Check cookie data cache first — avoids re-probing Chrome entirely
|
|
474
510
|
const cachedData = this.cookieDataCache.get(sourceTargetId);
|
|
475
511
|
if (cachedData && Date.now() - cachedData.timestamp < CDPClient.COOKIE_CACHE_TTL) {
|
|
476
|
-
console.error(`[CDPClient] Cache hit for cookie data (${cachedData.cookies.length} cookies), skipping
|
|
512
|
+
console.error(`[CDPClient] Cache hit for cookie data (${cachedData.cookies.length} cookies), skipping CDP attach`);
|
|
477
513
|
const destSession = await destPage.createCDPSession();
|
|
478
514
|
try {
|
|
479
515
|
const cookiesToSet = cachedData.cookies.map(c => ({
|
|
@@ -494,69 +530,65 @@ class CDPClient {
|
|
|
494
530
|
await destSession.detach().catch(() => { });
|
|
495
531
|
}
|
|
496
532
|
}
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
if (!sourceTarget || !sourceTarget.webSocketDebuggerUrl) {
|
|
502
|
-
console.error(`[CDPClient] Source target not found in /json/list. Looking for: ${sourceTargetId}`);
|
|
503
|
-
console.error(`[CDPClient] Available targets: ${targets.map(t => t.id.slice(0, 8) + ' ' + t.url.slice(0, 40)).join(', ')}`);
|
|
504
|
-
return 0;
|
|
505
|
-
}
|
|
506
|
-
console.error(`[CDPClient] Connecting to source target at ${sourceTarget.url.slice(0, 50)}`);
|
|
507
|
-
// Get cookies via WebSocket
|
|
508
|
-
const cookies = await new Promise((resolve, reject) => {
|
|
509
|
-
const ws = new WebSocket(sourceTarget.webSocketDebuggerUrl);
|
|
510
|
-
const timeout = setTimeout(() => {
|
|
511
|
-
ws.close();
|
|
512
|
-
reject(new Error('WebSocket timeout'));
|
|
513
|
-
}, 5000);
|
|
514
|
-
ws.on('open', () => {
|
|
515
|
-
ws.send(JSON.stringify({ id: 1, method: 'Network.getAllCookies' }));
|
|
516
|
-
});
|
|
517
|
-
ws.on('message', (data) => {
|
|
518
|
-
const msg = JSON.parse(data.toString());
|
|
519
|
-
if (msg.id === 1) {
|
|
520
|
-
clearTimeout(timeout);
|
|
521
|
-
ws.close();
|
|
522
|
-
resolve(msg.result?.cookies || []);
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
ws.on('error', (err) => {
|
|
526
|
-
clearTimeout(timeout);
|
|
527
|
-
reject(err);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
// Store in cookie data cache
|
|
531
|
-
this.cookieDataCache.set(sourceTargetId, { cookies, timestamp: Date.now() });
|
|
532
|
-
if (cookies.length === 0) {
|
|
533
|
-
console.error(`[CDPClient] No cookies found in source page`);
|
|
534
|
-
return 0;
|
|
535
|
-
}
|
|
536
|
-
console.error(`[CDPClient] Found ${cookies.length} cookies, setting on destination page`);
|
|
537
|
-
// Set cookies on destination page
|
|
538
|
-
const destSession = await destPage.createCDPSession();
|
|
533
|
+
// Attach to the source target via the multiplexed browser CDP session
|
|
534
|
+
const browser = this.getBrowser();
|
|
535
|
+
const browserSession = await browser.target().createCDPSession();
|
|
536
|
+
let attachedSessionId = null;
|
|
539
537
|
try {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
538
|
+
// Verify the target exists before attaching
|
|
539
|
+
const { targetInfos } = await browserSession.send('Target.getTargets');
|
|
540
|
+
const sourceInfo = targetInfos.find(t => t.targetId === sourceTargetId);
|
|
541
|
+
if (!sourceInfo) {
|
|
542
|
+
console.error(`[CDPClient] Source target not found: ${sourceTargetId.slice(0, 8)}`);
|
|
543
|
+
console.error(`[CDPClient] Available targets: ${targetInfos.map(t => t.targetId.slice(0, 8) + ' ' + t.url.slice(0, 40)).join(', ')}`);
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
546
|
+
console.error(`[CDPClient] Attaching to source target at ${sourceInfo.url.slice(0, 50)}`);
|
|
547
|
+
const { sessionId } = await browserSession.send('Target.attachToTarget', {
|
|
548
|
+
targetId: sourceTargetId,
|
|
549
|
+
flatten: true,
|
|
550
|
+
});
|
|
551
|
+
attachedSessionId = sessionId;
|
|
552
|
+
// Fetch cookies through the flat session (no raw WebSocket, no /json/list)
|
|
553
|
+
const result = await browserSession.send('Network.getAllCookies', undefined, { sessionId });
|
|
554
|
+
const cookies = result?.cookies || [];
|
|
555
|
+
// Store in cookie data cache
|
|
556
|
+
this.cookieDataCache.set(sourceTargetId, { cookies, timestamp: Date.now() });
|
|
557
|
+
if (cookies.length === 0) {
|
|
558
|
+
console.error('[CDPClient] No cookies found in source page');
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
console.error(`[CDPClient] Found ${cookies.length} cookies, setting on destination page`);
|
|
562
|
+
// Set cookies on destination page via its own CDPSession
|
|
563
|
+
const destSession = await destPage.createCDPSession();
|
|
564
|
+
try {
|
|
565
|
+
const cookiesToSet = cookies.map(c => ({
|
|
566
|
+
name: c.name,
|
|
567
|
+
value: c.value,
|
|
568
|
+
domain: c.domain,
|
|
569
|
+
path: c.path,
|
|
570
|
+
expires: c.expires,
|
|
571
|
+
httpOnly: c.httpOnly,
|
|
572
|
+
secure: c.secure,
|
|
573
|
+
sameSite: c.sameSite,
|
|
574
|
+
}));
|
|
575
|
+
await destSession.send('Network.setCookies', { cookies: cookiesToSet });
|
|
576
|
+
console.error(`[CDPClient] Successfully copied ${cookies.length} cookies`);
|
|
577
|
+
return cookies.length;
|
|
578
|
+
}
|
|
579
|
+
finally {
|
|
580
|
+
await destSession.detach().catch(() => { });
|
|
581
|
+
}
|
|
553
582
|
}
|
|
554
583
|
finally {
|
|
555
|
-
|
|
584
|
+
if (attachedSessionId) {
|
|
585
|
+
await browserSession.send('Target.detachFromTarget', { sessionId: attachedSessionId }).catch(() => { });
|
|
586
|
+
}
|
|
587
|
+
await browserSession.detach().catch(() => { });
|
|
556
588
|
}
|
|
557
589
|
}
|
|
558
590
|
catch (error) {
|
|
559
|
-
console.error(
|
|
591
|
+
console.error('[CDPClient] Error in copyCookiesViaCDP:', error);
|
|
560
592
|
return 0;
|
|
561
593
|
}
|
|
562
594
|
}
|
|
@@ -611,14 +643,26 @@ class CDPClient {
|
|
|
611
643
|
* Get page by target ID
|
|
612
644
|
*/
|
|
613
645
|
async getPageByTargetId(targetId) {
|
|
646
|
+
// Fast path: check index first (O(1))
|
|
647
|
+
const indexed = this.targetIdIndex.get(targetId);
|
|
648
|
+
if (indexed && !indexed.isClosed()) {
|
|
649
|
+
return indexed;
|
|
650
|
+
}
|
|
651
|
+
// Fallback: linear scan (for pages created before indexing started)
|
|
614
652
|
const browser = this.getBrowser();
|
|
615
653
|
const targets = browser.targets();
|
|
616
654
|
for (const target of targets) {
|
|
617
655
|
if (getTargetId(target) === targetId && target.type() === 'page') {
|
|
618
656
|
const page = await target.page();
|
|
657
|
+
if (page) {
|
|
658
|
+
// Populate index for future lookups
|
|
659
|
+
this.targetIdIndex.set(targetId, page);
|
|
660
|
+
}
|
|
619
661
|
return page;
|
|
620
662
|
}
|
|
621
663
|
}
|
|
664
|
+
// Clean stale index entry
|
|
665
|
+
this.targetIdIndex.delete(targetId);
|
|
622
666
|
return null;
|
|
623
667
|
}
|
|
624
668
|
/**
|