feat(browse): add extractCookiesViaCdpPipe using CDP pipe transport

Pipe-transport equivalent of extractCookiesViaCdp. Uses Target.getTargets
+ Target.attachToTarget(flatten: true) to acquire a page session over a
single CDP pipe connection, then Network.getAllCookies. Still not wired
into importCookiesViaCdp; next commit swaps the TCP path over.

Refs #1136.
This commit is contained in:
Nicolas Gomes Ferreira Dos Santos 2026-04-23 20:52:29 -07:00
parent 3bc13adabc
commit c3f26cf7b5
2 changed files with 175 additions and 1 deletions

View File

@ -1151,6 +1151,72 @@ export class CdpPipeTransport {
}
}
// Minimal transport surface needed by extractCookiesViaCdpPipe. Lets tests
// pass a scripted mock without constructing a real CdpPipeTransport.
export interface CdpCallable {
send(method: string, params?: object, sessionId?: string): Promise<any>;
}
/**
* Extract cookies over a CDP-over-pipe transport. The transport must already
* be wired to a running Chrome process with --remote-debugging-pipe.
*
* Flow:
* 1. Target.getTargets find a page target
* 2. Target.attachToTarget flatten session, get sessionId
* 3. Network.enable on the page session
* 4. Network.getAllCookies returns every cookie the browser has
* 5. Filter + map to PlaywrightCookie[]
*/
export async function extractCookiesViaCdpPipe(
transport: CdpCallable,
domains: string[],
): Promise<PlaywrightCookie[]> {
if (domains.length === 0) return [];
const targets = await transport.send('Target.getTargets');
const pageTarget = (targets.targetInfos as Array<{ targetId: string; type: string }>)
.find(t => t.type === 'page');
if (!pageTarget) {
throw new CookieImportError(
'No page target found in Chrome for CDP cookie extraction',
'cdp_error',
);
}
const attached = await transport.send('Target.attachToTarget', {
targetId: pageTarget.targetId,
flatten: true,
});
const sessionId = attached.sessionId as string;
await transport.send('Network.enable', undefined, sessionId);
const cookiesResp = await transport.send('Network.getAllCookies', undefined, sessionId);
// Normalize domain matching: `.example.com` should match `example.com` and vice versa.
const domainSet = new Set<string>();
for (const d of domains) {
domainSet.add(d);
domainSet.add(d.startsWith('.') ? d.slice(1) : '.' + d);
}
const result: PlaywrightCookie[] = [];
for (const c of cookiesResp.cookies as CdpCookie[]) {
if (!domainSet.has(c.domain)) continue;
result.push({
name: c.name,
value: c.value,
domain: c.domain,
path: c.path || '/',
expires: c.expires === -1 ? -1 : c.expires,
secure: c.secure,
httpOnly: c.httpOnly,
sameSite: cdpSameSite(c.sameSite),
});
}
return result;
}
/**
* Check if a browser's cookie DB contains v20 (App-Bound) encrypted cookies.
* Quick check reads a small sample, no decryption attempted.

View File

@ -20,7 +20,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { PassThrough } from 'node:stream';
import { CdpPipeTransport } from '../src/cookie-import-browser';
import { CdpPipeTransport, extractCookiesViaCdpPipe } from '../src/cookie-import-browser';
// ─── Test Constants ─────────────────────────────────────────────
@ -677,3 +677,111 @@ describe('CdpPipeTransport', () => {
t.close();
});
});
// A mock transport that replays scripted responses per-method.
// The test writes scripts keyed by method name; send() returns the next
// scripted response for that method.
class ScriptedTransport {
private scripts: Record<string, unknown[]> = {};
sent: Array<{ method: string; params?: unknown; sessionId?: string }> = [];
script(method: string, ...responses: unknown[]) {
this.scripts[method] = (this.scripts[method] ?? []).concat(responses);
}
async send(method: string, params?: object, sessionId?: string): Promise<any> {
this.sent.push({ method, params, sessionId });
const queue = this.scripts[method];
if (!queue || queue.length === 0) {
throw new Error(`No scripted response for ${method}`);
}
const next = queue.shift();
if (next instanceof Error) throw next;
return next;
}
}
describe('extractCookiesViaCdpPipe', () => {
test('happy path: attaches to first page target, requests cookies, filters by domain', async () => {
const transport = new ScriptedTransport();
transport.script('Target.getTargets', {
targetInfos: [
{ targetId: 'BROWSER1', type: 'browser' },
{ targetId: 'PAGE1', type: 'page', url: 'about:blank' },
],
});
transport.script('Target.attachToTarget', { sessionId: 'S1' });
transport.script('Network.enable', {});
transport.script('Network.getAllCookies', {
cookies: [
{ name: 'sid', value: 'abc', domain: 'example.com', path: '/', expires: 1800000000, size: 4, httpOnly: true, secure: true, session: false, sameSite: 'Lax' },
{ name: 'other', value: 'xyz', domain: 'other.com', path: '/', expires: -1, size: 3, httpOnly: false, secure: false, session: true, sameSite: 'None' },
],
});
const cookies = await extractCookiesViaCdpPipe(transport as any, ['example.com']);
expect(cookies).toHaveLength(1);
expect(cookies[0]).toEqual({
name: 'sid',
value: 'abc',
domain: 'example.com',
path: '/',
expires: 1800000000,
secure: true,
httpOnly: true,
sameSite: 'Lax',
});
expect(transport.sent.map(s => s.method)).toEqual([
'Target.getTargets',
'Target.attachToTarget',
'Network.enable',
'Network.getAllCookies',
]);
expect(transport.sent[1].params).toEqual({ targetId: 'PAGE1', flatten: true });
expect(transport.sent[2].sessionId).toBe('S1');
expect(transport.sent[3].sessionId).toBe('S1');
});
test('matches cookies with leading-dot domain normalization', async () => {
const transport = new ScriptedTransport();
transport.script('Target.getTargets', { targetInfos: [{ targetId: 'P', type: 'page' }] });
transport.script('Target.attachToTarget', { sessionId: 'S1' });
transport.script('Network.enable', {});
transport.script('Network.getAllCookies', {
cookies: [
{ name: 'a', value: '1', domain: '.example.com', path: '/', expires: -1, size: 1, httpOnly: false, secure: false, session: true, sameSite: 'Lax' },
],
});
const cookies = await extractCookiesViaCdpPipe(transport as any, ['example.com']);
expect(cookies).toHaveLength(1);
expect(cookies[0].domain).toBe('.example.com');
});
test('throws cdp_error if no page target exists', async () => {
const transport = new ScriptedTransport();
transport.script('Target.getTargets', {
targetInfos: [{ targetId: 'BROWSER1', type: 'browser' }],
});
let caught: any = null;
try {
await extractCookiesViaCdpPipe(transport as any, ['example.com']);
} catch (e) {
caught = e;
}
expect(caught).toBeInstanceOf(CookieImportError);
expect(caught!.code).toBe('cdp_error');
expect(caught!.message.toLowerCase()).toContain('page target');
});
test('empty domains list short-circuits to empty result', async () => {
const transport = new ScriptedTransport();
const cookies = await extractCookiesViaCdpPipe(transport as any, []);
expect(cookies).toEqual([]);
expect(transport.sent).toHaveLength(0);
});
});