Setting the file. One moment.
Cookie Sync · Cookie Sync · browserbase/skills · Skills Docs
ContentsBack to the top of the page Bundled file
scripts/ cookie-sync.mjs
JavaScript · 314 lines · 11 KB
14
//
15 // After syncing, use the browse CLI to open an authenticated session:
16 // SESSION_JSON="$(browse cloud sessions create --context-id <ctx-id> --persist --keep-alive)"
17 // CONNECT_URL="$(echo "$SESSION_JSON" | jq -r .connectUrl)"
18 // browse open https://example.com --cdp "$CONNECT_URL"
19 //
20 // Env vars:
21 // BROWSERBASE_API_KEY — required
22 // BROWSERBASE_CONTEXT_ID — optional, reuse an existing context
23 // CDP_URL — optional, Chrome debugging endpoint or browser WS URL
24 // CDP_PORT_FILE — optional, path to DevToolsActivePort if non-standard
25 // CDP_HOST — optional, host for DevToolsActivePort-based connections
26
27 import { Stagehand } from '@browserbasehq/stagehand' ;
28 import Browserbase from '@browserbasehq/sdk' ;
29 import { readFileSync, existsSync } from 'fs' ;
30 import { execSync } from 'child_process' ;
31 import { homedir } from 'os' ;
32 import { resolve } from 'path' ;
33
34 // ---------------------------------------------------------------------------
35 // CLI args
36 // ---------------------------------------------------------------------------
37
38 function parseArgs () {
39 const args = process.argv. slice ( 2 );
40 const result = { domains: [], contextId: null , verified: false , proxy: null };
41
42 for ( let i = 0 ; i < args. length ; i ++ ) {
43 if (args[i] === '--domains' && args[i + 1 ]) {
44 result.domains = args[ ++ i]. split ( ',' ). map ( d => d. trim (). toLowerCase ()). filter (Boolean);
45 } else if (args[i] === '--context' && args[i + 1 ]) {
46 result.contextId = args[ ++ i];
47 } else if (args[i] === '--verified' ) {
48 result.verified = true ;
49 } else if (args[i] === '--proxy' && args[i + 1 ]) {
50 const parts = args[ ++ i]. split ( ',' ). map ( s => s. trim ());
51 if ( ! parts[ 0 ] || ! parts[ 1 ]) {
52 console. error ( 'Error: --proxy requires "City,State,Country" (e.g. "San Francisco,CA,US")' );
53 process. exit ( 1 );
54 }
55 result.proxy = { city: parts[ 0 ], state: parts[ 1 ], country: parts[ 2 ] || 'US' };
56 }
57 }
58
59 return result;
60 }
61
62 const CLI = parseArgs ();
63
64 // ---------------------------------------------------------------------------
65 // Env validation
66 // ---------------------------------------------------------------------------
67
68 const API_KEY = process.env. BROWSERBASE_API_KEY ;
69
70 if ( ! API_KEY ) {
71 console. error ( 'Error: BROWSERBASE_API_KEY is required' );
72 process. exit ( 1 );
73 }
74
75 // ---------------------------------------------------------------------------
76 // Find local Chrome DevTools WebSocket URL
77 // ---------------------------------------------------------------------------
78
79 async function resolveCdpUrl ( cdpUrl ) {
80 if ( / ^ wss ? : \/\/ . + \/ devtools \/ browser \/ / . test (cdpUrl)) {
81 return cdpUrl;
82 }
83
84 const base = cdpUrl. replace ( / ^ wss ? / i , m => m. length === 3 ? 'https' : 'http' ). replace ( / \/ +$ / , '' );
85 const versionUrl = base. endsWith ( '/json/version' ) ? base : `${ base }/json/version` ;
86 const res = await fetch (versionUrl);
87
88 if ( ! res.ok) {
89 throw new Error ( `Could not resolve CDP_URL via ${ versionUrl } (${ res . status })` );
90 }
91
92 const info = await res. json ();
93 if ( ! info.webSocketDebuggerUrl) {
94 throw new Error ( `CDP_URL did not expose webSocketDebuggerUrl at ${ versionUrl }` );
95 }
96
97 return info.webSocketDebuggerUrl;
98 }
99
100 async function getLocalCdpUrl () {
101 if (process.env. CDP_URL ) {
102 return resolveCdpUrl (process.env. CDP_URL );
103 }
104
105 const home = homedir ();
106 const IS_WINDOWS = process.platform === 'win32' ;
107
108 const macBrowsers = [
109 'Google/Chrome' , 'Google/Chrome Beta' , 'Google/Chrome for Testing' ,
110 'Chromium' , 'BraveSoftware/Brave-Browser' , 'Microsoft Edge' ,
111 ];
112 const linuxBrowsers = [
113 'google-chrome' , 'google-chrome-beta' , 'chromium' ,
114 'vivaldi' , 'vivaldi-snapshot' ,
115 'BraveSoftware/Brave-Browser' , 'microsoft-edge' ,
116 ];
117
118 const candidates = [
119 process.env. CDP_PORT_FILE ,
120 ... macBrowsers. flatMap ( b => [
121 resolve (home, 'Library/Application Support' , b, 'DevToolsActivePort' ),
122 resolve (home, 'Library/Application Support' , b, 'Default/DevToolsActivePort' ),
123 ]),
124 ... linuxBrowsers. flatMap ( b => [
125 resolve (home, '.config' , b, 'DevToolsActivePort' ),
126 resolve (home, '.config' , b, 'Default/DevToolsActivePort' ),
127 ]),
128 ... ( IS_WINDOWS ? [ 'Google/Chrome' , 'BraveSoftware/Brave-Browser' , 'Microsoft/Edge' ]. flatMap ( b => {
129 const base = process.env. LOCALAPPDATA || resolve (home, 'AppData/Local' );
130 return [
131 resolve (base, b, 'User Data/DevToolsActivePort' ),
132 resolve (base, b, 'User Data/Default/DevToolsActivePort' ),
133 ];
134 }) : []),
135 ]. filter (Boolean);
136
137 const portFile = candidates. find ( p => existsSync (p));
138 if ( ! portFile) {
139 throw new Error (
140 'No DevToolsActivePort found. \n ' +
141 'Enable remote debugging: chrome://flags/#allow-remote-debugging (Chrome 146+) \n ' +
142 'Or launch Chrome with --remote-debugging-port=9222 and set CDP_URL=ws://127.0.0.1:9222'
143 );
144 }
145
146 const lines = readFileSync (portFile, 'utf8' ). trim (). split ( / \r ? \n / );
147 if (lines. length < 2 || ! lines[ 0 ] || ! lines[ 1 ]) {
148 throw new Error ( `Invalid DevToolsActivePort file: ${ portFile }` );
149 }
150
151 const host = process.env. CDP_HOST || '127.0.0.1' ;
152 return `ws://${ host }:${ lines [ 0 ] }${ lines [ 1 ] }` ;
153 }
154
155 // ---------------------------------------------------------------------------
156 // Chrome version check
157 // ---------------------------------------------------------------------------
158
159 function checkChromeVersion () {
160 if (process.env. CDP_URL || process.env. CDP_PORT_FILE ) return ;
161
162 const chromePaths = [
163 // macOS
164 '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' ,
165 '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta' ,
166 '/Applications/Chromium.app/Contents/MacOS/Chromium' ,
167 // Linux (resolved via PATH)
168 'google-chrome' ,
169 'google-chrome-stable' ,
170 'chromium-browser' ,
171 'chromium' ,
172 ];
173 for ( const p of chromePaths) {
174 try {
175 const out = execSync ( `"${ p }" --version` , { encoding: 'utf8' , stdio: [ 'pipe' , 'pipe' , 'pipe' ] }). trim ();
176 const match = out. match ( /( \d + ) \. / );
177 if (match) {
178 const major = parseInt (match[ 1 ], 10 );
179 if (major < 146 ) {
180 console. warn ( `Chrome ${ major } detected. Chrome 146+ supports the allow-remote-debugging flag.` );
181 console. warn ( 'For older Chrome, launch with --remote-debugging-port=9222 and set CDP_URL=ws://127.0.0.1:9222.' );
182 return ;
183 }
184 console. log ( `Chrome ${ major } detected` );
185 return ;
186 }
187 } catch { /* try next */ }
188 }
189 }
190
191 // ---------------------------------------------------------------------------
192 // Cookie helpers
193 // ---------------------------------------------------------------------------
194
195 function filterCookies ( cookies , domains ) {
196 if (domains. length === 0 ) return cookies;
197 return cookies. filter ( cookie => {
198 const cookieDomain = cookie.domain. replace ( / ^ \. / , '' ). toLowerCase ();
199 return domains. some ( d => cookieDomain === d || cookieDomain. endsWith ( '.' + d));
200 });
201 }
202
203 function toCookieParams ( cookies ) {
204 return cookies. map ( c => {
205 const param = {
206 name: c.name,
207 value: c.value,
208 domain: c.domain,
209 path: c.path,
210 httpOnly: c.httpOnly,
211 secure: c.secure,
212 };
213 if (c.expires > 0 ) param.expires = c.expires;
214 if (c.sameSite === 'Strict' || c.sameSite === 'Lax' ) {
215 param.sameSite = c.sameSite;
216 } else if (c.sameSite === 'None' && c.secure) {
217 param.sameSite = 'None' ;
218 }
219 return param;
220 });
221 }
222
223 // ---------------------------------------------------------------------------
224 // Main
225 // ---------------------------------------------------------------------------
226
227 async function main () {
228 checkChromeVersion ();
229
230 // Step 1: Connect to local Chrome via Stagehand and export cookies
231 const cdpUrl = await getLocalCdpUrl ();
232 const local = new Stagehand ({
233 env: 'LOCAL' ,
234 localBrowserLaunchOptions: { cdpUrl },
235 verbose: 0 ,
236 disablePino: true ,
237 });
238 await local. init ();
239 console. log ( 'Connected to local Chrome' );
240
241 const allCookies = await local.context. cookies ();
242 console. log ( `Exported ${ allCookies . length } cookies from local Chrome` );
243 await local. close ();
244
245 // Step 2: Filter cookies by domain if requested
246 const cookies = filterCookies (allCookies, CLI .domains);
247 if ( CLI .domains. length > 0 ) {
248 console. log ( `Filtered to ${ cookies . length } cookies matching: ${ CLI . domains . join ( ', ' ) }` );
249 }
250 if (cookies. length === 0 ) {
251 console. warn ( 'Warning: No cookies to sync. Check your domain filters or Chrome login state.' );
252 process. exit ( 0 );
253 }
254
255 // Step 3: Set up context (create new or reuse existing)
256 const bb = new Browserbase ({ apiKey: API_KEY });
257 let contextId = CLI .contextId || process.env. BROWSERBASE_CONTEXT_ID ;
258
259 if ( ! contextId) {
260 const ctx = await bb.contexts. create ({});
261 contextId = ctx.id;
262 console. log ( `Created context: ${ contextId }` );
263 } else {
264 console. log ( `Using existing context: ${ contextId }` );
265 }
266
267 // Step 4: Create a temporary Browserbase session to inject cookies
268 const browserSettings = { context: { id: contextId, persist: true } };
269 if ( CLI .verified) browserSettings.verified = true ;
270
271 const cloud = new Stagehand ({
272 env: 'BROWSERBASE' ,
273 apiKey: API_KEY ,
274 disableAPI: true ,
275 browserbaseSessionCreateParams: {
276 browserSettings,
277 ... ( CLI .proxy && {
278 proxies: [{ type: 'browserbase' , geolocation: CLI .proxy }],
279 }),
280 },
281 verbose: 0 ,
282 disablePino: true ,
283 });
284 await cloud. init ();
285 console. log ( `Injecting cookies via session: ${ cloud . browserbaseSessionID }` );
286
287 // Step 5: Inject cookies and close the session (context persists independently)
288 const cookieParams = toCookieParams (cookies);
289 await cloud.context. addCookies (cookieParams);
290 console. log ( `Injected ${ cookies . length } cookies into context` );
291 await cloud. close ();
292
293 // Step 6: Summary
294 console. log ( '' );
295 console. log ( 'Cookies synced to context.' );
296 console. log ( `Context ID: ${ contextId }` );
297 console. log ( '' );
298 console. log ( 'Browse authenticated sites with:' );
299 console. log ( ` SESSION_JSON="$(browse cloud sessions create --context-id ${ contextId } --persist --keep-alive)"` );
300 console. log ( ' SESSION_ID="$(echo "$SESSION_JSON" | jq -r .id)"' );
301 console. log ( ' CONNECT_URL="$(echo "$SESSION_JSON" | jq -r .connectUrl)"' );
302 console. log ( ' browse open <url> --cdp "$CONNECT_URL"' );
303 console. log ( ' # when done: browse stop && browse cloud sessions update "$SESSION_ID" --status REQUEST_RELEASE' );
304 console. log ( '' );
305 console. log ( 'To refresh cookies later:' );
306 console. log ( ` node cookie-sync.mjs --context ${ contextId }` );
307
308 process. exit ( 0 );
309 }
310
311 main (). catch ( e => {
312 console. error ( `Error: ${ e . message }` );
313 process. exit ( 1 );
314 });