@reshotdev/screenshot 0.0.1-beta.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.
Files changed (59) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +388 -0
  3. package/package.json +64 -0
  4. package/src/commands/auth.js +259 -0
  5. package/src/commands/chrome.js +140 -0
  6. package/src/commands/ci-run.js +123 -0
  7. package/src/commands/ci-setup.js +288 -0
  8. package/src/commands/drifts.js +423 -0
  9. package/src/commands/import-tests.js +309 -0
  10. package/src/commands/ingest.js +458 -0
  11. package/src/commands/init.js +633 -0
  12. package/src/commands/publish.js +1721 -0
  13. package/src/commands/pull.js +303 -0
  14. package/src/commands/record.js +94 -0
  15. package/src/commands/run.js +476 -0
  16. package/src/commands/setup-wizard.js +740 -0
  17. package/src/commands/setup.js +137 -0
  18. package/src/commands/status.js +275 -0
  19. package/src/commands/sync.js +621 -0
  20. package/src/commands/ui.js +248 -0
  21. package/src/commands/validate-docs.js +529 -0
  22. package/src/index.js +462 -0
  23. package/src/lib/api-client.js +815 -0
  24. package/src/lib/capture-engine.js +1623 -0
  25. package/src/lib/capture-script-runner.js +3120 -0
  26. package/src/lib/ci-detect.js +137 -0
  27. package/src/lib/config.js +1240 -0
  28. package/src/lib/diff-engine.js +642 -0
  29. package/src/lib/hash.js +74 -0
  30. package/src/lib/image-crop.js +396 -0
  31. package/src/lib/matrix.js +89 -0
  32. package/src/lib/output-path-template.js +318 -0
  33. package/src/lib/playwright-runner.js +252 -0
  34. package/src/lib/polished-clip.js +553 -0
  35. package/src/lib/privacy-engine.js +408 -0
  36. package/src/lib/progress-tracker.js +142 -0
  37. package/src/lib/record-browser-injection.js +654 -0
  38. package/src/lib/record-cdp.js +612 -0
  39. package/src/lib/record-clip.js +343 -0
  40. package/src/lib/record-config.js +623 -0
  41. package/src/lib/record-screenshot.js +360 -0
  42. package/src/lib/record-terminal.js +123 -0
  43. package/src/lib/recorder-service.js +781 -0
  44. package/src/lib/secrets.js +51 -0
  45. package/src/lib/selector-strategies.js +859 -0
  46. package/src/lib/standalone-mode.js +400 -0
  47. package/src/lib/storage-providers.js +569 -0
  48. package/src/lib/style-engine.js +684 -0
  49. package/src/lib/ui-api.js +4677 -0
  50. package/src/lib/ui-assets.js +373 -0
  51. package/src/lib/ui-executor.js +587 -0
  52. package/src/lib/variant-injector.js +591 -0
  53. package/src/lib/viewport-presets.js +454 -0
  54. package/src/lib/worker-pool.js +118 -0
  55. package/web/cropper/index.html +436 -0
  56. package/web/manager/dist/assets/index--ZgioErz.js +507 -0
  57. package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
  58. package/web/manager/dist/index.html +27 -0
  59. package/web/subtitle-editor/index.html +295 -0
@@ -0,0 +1,612 @@
1
+ // record-cdp.js - CDP connection utilities for record command
2
+ const { chromium } = require("playwright");
3
+ const chalk = require("chalk");
4
+ const http = require("http");
5
+
6
+ /**
7
+ * Check if Chrome CDP endpoint is available
8
+ * @param {string} host
9
+ * @param {number} port
10
+ * @returns {Promise<{available: boolean, targets?: any[], error?: string}>}
11
+ */
12
+ async function checkCdpEndpoint(host = "localhost", port = 9222) {
13
+ return new Promise((resolve) => {
14
+ const url = `http://${host}:${port}/json/version`;
15
+
16
+ const req = http.get(url, { timeout: 3000 }, (res) => {
17
+ let data = "";
18
+ res.on("data", (chunk) => (data += chunk));
19
+ res.on("end", () => {
20
+ try {
21
+ const info = JSON.parse(data);
22
+ resolve({ available: true, info });
23
+ } catch (e) {
24
+ resolve({
25
+ available: false,
26
+ error: "Invalid response from CDP endpoint",
27
+ });
28
+ }
29
+ });
30
+ });
31
+
32
+ req.on("error", (err) => {
33
+ resolve({ available: false, error: err.message });
34
+ });
35
+
36
+ req.on("timeout", () => {
37
+ req.destroy();
38
+ resolve({ available: false, error: "Connection timeout" });
39
+ });
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Get list of available pages from CDP
45
+ * @param {string} host
46
+ * @param {number} port
47
+ * @returns {Promise<any[]>}
48
+ */
49
+ async function getCdpTargets(host = "localhost", port = 9222) {
50
+ return new Promise((resolve, reject) => {
51
+ const url = `http://${host}:${port}/json/list`;
52
+
53
+ const req = http.get(url, { timeout: 3000 }, (res) => {
54
+ let data = "";
55
+ res.on("data", (chunk) => (data += chunk));
56
+ res.on("end", () => {
57
+ try {
58
+ const targets = JSON.parse(data);
59
+ resolve(targets);
60
+ } catch (e) {
61
+ reject(new Error("Failed to parse CDP targets"));
62
+ }
63
+ });
64
+ });
65
+
66
+ req.on("error", reject);
67
+ req.on("timeout", () => {
68
+ req.destroy();
69
+ reject(new Error("Timeout getting CDP targets"));
70
+ });
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Connect to an active Chrome instance via CDP with enhanced error handling
76
+ * @param {Object} options - Connection options
77
+ * @param {boolean} options.autoLaunch - Whether to auto-launch Chrome if not running (not implemented yet)
78
+ * @param {boolean} options.uiMode - Whether we're in UI mode (affects error messages)
79
+ * @param {string} options.targetUrl - Specific URL to connect to (optional)
80
+ * @param {string} options.targetId - Specific target ID to connect to (optional)
81
+ * @returns {Promise<{browser, context, page}>} Connected browser objects
82
+ */
83
+ async function connectToActivePage(options = {}) {
84
+ const { uiMode = false, targetUrl = null, targetId = null } = options;
85
+ const host = "localhost";
86
+ const port = 9222;
87
+ const cdpUrl = `http://${host}:${port}`;
88
+
89
+ // Step 1: Check if CDP endpoint is reachable
90
+ console.log(
91
+ chalk.gray(`[CDP] Checking if Chrome is available at ${cdpUrl}...`)
92
+ );
93
+
94
+ const endpointCheck = await checkCdpEndpoint(host, port);
95
+
96
+ if (!endpointCheck.available) {
97
+ console.error(
98
+ chalk.red(
99
+ `\n❌ Chrome CDP endpoint not reachable: ${endpointCheck.error}`
100
+ )
101
+ );
102
+ printChromeInstructions();
103
+ throw new Error(
104
+ `Chrome is not running with remote debugging enabled. ${endpointCheck.error}`
105
+ );
106
+ }
107
+
108
+ console.log(
109
+ chalk.gray(
110
+ `[CDP] Chrome found: ${endpointCheck.info?.Browser || "Unknown browser"}`
111
+ )
112
+ );
113
+
114
+ // Step 2: Get available targets
115
+ let targets;
116
+ try {
117
+ targets = await getCdpTargets(host, port);
118
+ console.log(chalk.gray(`[CDP] Found ${targets.length} browser targets`));
119
+ } catch (error) {
120
+ console.error(
121
+ chalk.red(`\n❌ Failed to get browser targets: ${error.message}`)
122
+ );
123
+ throw new Error(`Failed to enumerate browser tabs: ${error.message}`);
124
+ }
125
+
126
+ // Step 3: Find a suitable page target
127
+ const pageTargets = targets.filter((t) => t.type === "page");
128
+
129
+ if (pageTargets.length === 0) {
130
+ console.error(chalk.red("\n❌ No open tabs found in Chrome"));
131
+ console.log(
132
+ chalk.yellow("Please open at least one tab in Chrome and try again.\n")
133
+ );
134
+ throw new Error("No open tabs found in Chrome. Please open a tab first.");
135
+ }
136
+
137
+ // Find the target based on options
138
+ let bestTarget;
139
+
140
+ if (targetId) {
141
+ // Find by target ID
142
+ bestTarget = pageTargets.find((t) => t.id === targetId);
143
+ if (!bestTarget) {
144
+ console.error(chalk.red(`\n❌ Target with ID ${targetId} not found`));
145
+ throw new Error(
146
+ `Target with ID ${targetId} not found. It may have been closed.`
147
+ );
148
+ }
149
+ console.log(
150
+ chalk.gray(
151
+ `[CDP] Using specified target: ${bestTarget.title || bestTarget.url}`
152
+ )
153
+ );
154
+ } else if (targetUrl) {
155
+ // Find by URL (partial match)
156
+ bestTarget = pageTargets.find((t) => t.url.includes(targetUrl));
157
+ if (!bestTarget) {
158
+ // Try more flexible matching
159
+ const targetUrlLower = targetUrl.toLowerCase();
160
+ bestTarget = pageTargets.find((t) =>
161
+ t.url.toLowerCase().includes(targetUrlLower)
162
+ );
163
+ }
164
+ if (!bestTarget) {
165
+ console.error(chalk.red(`\n❌ No tab found matching URL: ${targetUrl}`));
166
+ console.log(chalk.yellow("Available tabs:"));
167
+ pageTargets.forEach((t) => console.log(chalk.gray(` - ${t.url}`)));
168
+ throw new Error(`No tab found matching URL: ${targetUrl}`);
169
+ }
170
+ console.log(
171
+ chalk.gray(
172
+ `[CDP] Found target matching URL: ${bestTarget.title || bestTarget.url}`
173
+ )
174
+ );
175
+ } else {
176
+ // Default: Sort and pick best target (prefer non-chrome:// URLs, exclude localhost:4300 which is our UI)
177
+ const sortedTargets = [...pageTargets].sort((a, b) => {
178
+ // Exclude our own UI
179
+ const aIsOurUI =
180
+ a.url.includes("localhost:4300") || a.url.includes("127.0.0.1:4300");
181
+ const bIsOurUI =
182
+ b.url.includes("localhost:4300") || b.url.includes("127.0.0.1:4300");
183
+ if (aIsOurUI && !bIsOurUI) return 1;
184
+ if (!aIsOurUI && bIsOurUI) return -1;
185
+
186
+ // Prefer real pages over chrome:// pages
187
+ const aIsChrome =
188
+ a.url.startsWith("chrome://") ||
189
+ a.url.startsWith("chrome-error://") ||
190
+ a.url === "about:blank";
191
+ const bIsChrome =
192
+ b.url.startsWith("chrome://") ||
193
+ b.url.startsWith("chrome-error://") ||
194
+ b.url === "about:blank";
195
+ if (aIsChrome && !bIsChrome) return 1;
196
+ if (!aIsChrome && bIsChrome) return -1;
197
+ return 0;
198
+ });
199
+
200
+ bestTarget = sortedTargets[0];
201
+ console.log(
202
+ chalk.gray(`[CDP] Best target: ${bestTarget.title || bestTarget.url}`)
203
+ );
204
+ }
205
+
206
+ // Warn if best target is still a chrome:// page or our UI
207
+ if (
208
+ bestTarget.url.startsWith("chrome://") ||
209
+ bestTarget.url.startsWith("chrome-error://") ||
210
+ bestTarget.url === "about:blank"
211
+ ) {
212
+ console.log(
213
+ chalk.yellow("\n⚠ Warning: Chrome is not on a regular webpage.")
214
+ );
215
+ console.log(
216
+ chalk.yellow(
217
+ " Please navigate to your application in Chrome before starting recording.\n"
218
+ )
219
+ );
220
+ }
221
+
222
+ if (
223
+ bestTarget.url.includes("localhost:4300") ||
224
+ bestTarget.url.includes("127.0.0.1:4300")
225
+ ) {
226
+ console.log(
227
+ chalk.yellow("\n⚠ Warning: Selected tab is the Reshot UI itself!")
228
+ );
229
+ console.log(
230
+ chalk.yellow(
231
+ " Please select a different tab with your target application.\n"
232
+ )
233
+ );
234
+ }
235
+
236
+ // Step 4: Connect via Playwright CDP
237
+ try {
238
+ console.log(chalk.gray(`[CDP] Connecting via Playwright...`));
239
+
240
+ const browser = await chromium.connectOverCDP(cdpUrl, {
241
+ timeout: 10000, // 10 second timeout
242
+ });
243
+
244
+ const contexts = browser.contexts();
245
+
246
+ if (contexts.length === 0) {
247
+ // This shouldn't happen if we found targets above, but handle it anyway
248
+ console.error(
249
+ chalk.red("\n❌ Connected but no browser contexts available")
250
+ );
251
+ await browser.close();
252
+ throw new Error("No browser contexts found after connecting");
253
+ }
254
+
255
+ const context = contexts[0];
256
+ const pages = context.pages();
257
+
258
+ if (pages.length === 0) {
259
+ console.error(chalk.red("\n❌ Connected but no pages available"));
260
+ await browser.close();
261
+ throw new Error("No pages found in browser context");
262
+ }
263
+
264
+ // Find the page matching our best target
265
+ let page = null;
266
+
267
+ // First, try exact URL match
268
+ for (const p of pages) {
269
+ if (p.url() === bestTarget.url) {
270
+ page = p;
271
+ break;
272
+ }
273
+ }
274
+
275
+ // If no exact match, try partial match
276
+ if (!page && (targetUrl || targetId)) {
277
+ for (const p of pages) {
278
+ const pUrl = p.url();
279
+ if (targetUrl && pUrl.includes(targetUrl)) {
280
+ page = p;
281
+ break;
282
+ }
283
+ }
284
+ }
285
+
286
+ // Fallback: find any non-chrome:// page that's not our UI
287
+ if (!page) {
288
+ for (const p of pages) {
289
+ const pUrl = p.url();
290
+ const isOurUI =
291
+ pUrl.includes("localhost:4300") || pUrl.includes("127.0.0.1:4300");
292
+ const isChrome =
293
+ pUrl.startsWith("chrome://") ||
294
+ pUrl.startsWith("chrome-error://") ||
295
+ pUrl === "about:blank";
296
+ if (!isOurUI && !isChrome) {
297
+ page = p;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+
303
+ // Last resort: just use the last page
304
+ if (!page) {
305
+ page = pages[pages.length - 1];
306
+ }
307
+
308
+ console.log(chalk.green(`✔ Connected to Chrome`));
309
+ console.log(chalk.gray(` Active page: ${page.url()}\n`));
310
+
311
+ return { browser, context, page };
312
+ } catch (error) {
313
+ // Enhanced error handling for Playwright connection errors
314
+ const errorMsg = error.message || String(error);
315
+
316
+ if (
317
+ errorMsg.includes("Target closed") ||
318
+ errorMsg.includes("Target page, context or browser has been closed")
319
+ ) {
320
+ console.error(chalk.red("\n❌ Browser tab was closed during connection"));
321
+ throw new Error(
322
+ "Browser tab was closed. Please keep a tab open and try again."
323
+ );
324
+ }
325
+
326
+ if (errorMsg.includes("timeout") || errorMsg.includes("Timeout")) {
327
+ console.error(chalk.red("\n❌ Connection timeout"));
328
+ console.log(
329
+ chalk.yellow(
330
+ "Chrome may be unresponsive. Try restarting Chrome with the debug flag.\n"
331
+ )
332
+ );
333
+ printChromeInstructions();
334
+ throw new Error("Connection timeout. Chrome may be unresponsive.");
335
+ }
336
+
337
+ console.error(chalk.red(`\n❌ Failed to connect: ${errorMsg}`));
338
+ printChromeInstructions();
339
+ throw new Error(`Failed to connect to Chrome: ${errorMsg}`);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Print instructions for starting Chrome with remote debugging
345
+ */
346
+ function printChromeInstructions() {
347
+ const platform = process.platform;
348
+
349
+ console.log(
350
+ chalk.yellow(
351
+ "\nTo enable recording, Chrome needs to be started with remote debugging:"
352
+ )
353
+ );
354
+ console.log(
355
+ chalk.yellow(
356
+ "1. Quit Chrome completely (check that no Chrome processes are running)"
357
+ )
358
+ );
359
+ console.log(
360
+ chalk.yellow("2. Start Chrome from terminal with the debug flag:\n")
361
+ );
362
+
363
+ if (platform === "darwin") {
364
+ console.log(chalk.cyan(" macOS:"));
365
+ console.log(
366
+ chalk.white(
367
+ ' /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"\n'
368
+ )
369
+ );
370
+ } else if (platform === "win32") {
371
+ console.log(chalk.cyan(" Windows:"));
372
+ console.log(
373
+ chalk.white(
374
+ ' "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\\.reshot\\chrome-debug"\n'
375
+ )
376
+ );
377
+ } else {
378
+ console.log(chalk.cyan(" Linux:"));
379
+ console.log(
380
+ chalk.white(
381
+ ' google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"'
382
+ )
383
+ );
384
+ console.log(chalk.gray(" or"));
385
+ console.log(
386
+ chalk.white(
387
+ ' chromium --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"\n'
388
+ )
389
+ );
390
+ }
391
+
392
+ console.log(
393
+ chalk.gray("3. Navigate to your application in the opened Chrome window")
394
+ );
395
+ console.log(chalk.gray('4. Come back here and click "Start Recording"\n'));
396
+ }
397
+
398
+ /**
399
+ * Save the current browser session state (cookies, localStorage) to a file
400
+ * This can be used later by capture-engine to maintain authenticated sessions
401
+ * @param {string} outputPath - Path to save the storage state JSON
402
+ * @returns {Promise<{success: boolean, path?: string, error?: string}>}
403
+ */
404
+ async function saveSessionState(outputPath) {
405
+ try {
406
+ const { browser, context, page } = await connectToActivePage({ uiMode: true });
407
+
408
+ if (!context) {
409
+ return { success: false, error: "Could not connect to browser context" };
410
+ }
411
+
412
+ // Playwright's storageState includes cookies and localStorage
413
+ const rawState = await context.storageState();
414
+
415
+ // Sanitize cookies (CDP returns expires: -1 for session cookies, which Playwright rejects)
416
+ const { sanitized: storageState, stats } = sanitizeStorageState(rawState);
417
+ if (stats.fixed > 0 || stats.removed > 0 || stats.stripped > 0) {
418
+ console.log(chalk.gray(` Sanitized cookies: ${stats.fixed} fixed, ${stats.removed} removed, ${stats.stripped} stripped`));
419
+ }
420
+
421
+ // Ensure directory exists
422
+ const fs = require("fs-extra");
423
+ const path = require("path");
424
+ fs.ensureDirSync(path.dirname(outputPath));
425
+
426
+ // Write storage state to file
427
+ fs.writeJsonSync(outputPath, storageState, { spaces: 2 });
428
+
429
+ console.log(chalk.green(` ✔ Session saved to: ${outputPath}`));
430
+ console.log(chalk.gray(` Cookies: ${storageState.cookies?.length || 0}`));
431
+ console.log(chalk.gray(` Origins with localStorage: ${storageState.origins?.length || 0}`));
432
+
433
+ // Don't close the browser - user might still be using it
434
+ // await browser.close();
435
+
436
+ return { success: true, path: outputPath };
437
+ } catch (error) {
438
+ console.error(chalk.red(` ✖ Failed to save session: ${error.message}`));
439
+ return { success: false, error: error.message };
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Get the default session state file path
445
+ * @returns {string}
446
+ */
447
+ function getDefaultSessionPath() {
448
+ const path = require("path");
449
+ const os = require("os");
450
+ return path.join(os.homedir(), ".reshot", "session-state.json");
451
+ }
452
+
453
+ /**
454
+ * Quietly check if CDP is available and sync the session if so.
455
+ * This is called automatically before captures to use the live browser session.
456
+ * @param {string} outputPath - Path to save the storage state JSON (optional, uses default)
457
+ * @param {Function} logger - Optional logging function (uses console.log if not provided)
458
+ * @returns {Promise<{synced: boolean, path?: string, error?: string}>}
459
+ */
460
+ async function autoSyncSessionFromCDP(outputPath = null, logger = null) {
461
+ const log = logger || (() => {}); // Quiet by default
462
+ const fs = require("fs-extra");
463
+ const path = require("path");
464
+ const os = require("os");
465
+
466
+ const sessionPath = outputPath || path.join(os.homedir(), ".reshot", "session-state.json");
467
+
468
+ try {
469
+ // Step 1: Check if CDP endpoint is available (quietly)
470
+ const endpointCheck = await checkCdpEndpoint("localhost", 9222);
471
+
472
+ if (!endpointCheck.available) {
473
+ // No CDP browser - that's fine, just return
474
+ log(chalk.gray(" → No CDP browser detected, skipping session sync"));
475
+ return { synced: false, reason: "no_cdp" };
476
+ }
477
+
478
+ log(chalk.gray(" → CDP browser detected, syncing session..."));
479
+
480
+ // Step 2: Connect to the browser
481
+ const browser = await chromium.connectOverCDP("http://localhost:9222", {
482
+ timeout: 5000,
483
+ });
484
+
485
+ try {
486
+ const contexts = browser.contexts();
487
+ if (contexts.length === 0) {
488
+ log(chalk.gray(" → No browser contexts found"));
489
+ await browser.close().catch(() => {}); // Disconnect (won't close Chrome since we used connectOverCDP)
490
+ return { synced: false, reason: "no_context" };
491
+ }
492
+
493
+ const context = contexts[0];
494
+
495
+ // Step 3: Get storage state
496
+ const rawState = await context.storageState();
497
+
498
+ // Sanitize cookies (CDP returns expires: -1 for session cookies, which Playwright rejects)
499
+ const { sanitized: storageState, stats } = sanitizeStorageState(rawState);
500
+ if (stats.fixed > 0 || stats.removed > 0 || stats.stripped > 0) {
501
+ log(chalk.gray(` Sanitized cookies: ${stats.fixed} fixed, ${stats.removed} removed, ${stats.stripped} stripped`));
502
+ }
503
+
504
+ // Check if there's actually any meaningful session data
505
+ const hasCookies = storageState.cookies && storageState.cookies.length > 0;
506
+ const hasStorage = storageState.origins && storageState.origins.length > 0;
507
+
508
+ // Disconnect from CDP (browser.close() on CDP connection just disconnects, doesn't close Chrome)
509
+ await browser.close().catch(() => {});
510
+
511
+ if (!hasCookies && !hasStorage) {
512
+ log(chalk.gray(" → No session data found in CDP browser"));
513
+ return { synced: false, reason: "empty_session" };
514
+ }
515
+
516
+ // Step 4: Save to file
517
+ fs.ensureDirSync(path.dirname(sessionPath));
518
+ fs.writeJsonSync(sessionPath, storageState, { spaces: 2 });
519
+
520
+ log(chalk.green(` ✔ Auto-synced session from CDP browser`));
521
+ log(chalk.gray(` Cookies: ${storageState.cookies?.length || 0}, localStorage origins: ${storageState.origins?.length || 0}`));
522
+
523
+ return { synced: true, path: sessionPath };
524
+ } catch (innerError) {
525
+ // Make sure to disconnect even on error
526
+ await browser.close().catch(() => {});
527
+ throw innerError;
528
+ }
529
+ } catch (error) {
530
+ // Silently fail - this is a convenience feature
531
+ log(chalk.gray(` → Session sync skipped: ${error.message}`));
532
+ return { synced: false, reason: "error", error: error.message };
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Fields that Playwright's `Storage.setCookies` accepts.
538
+ * CDP returns extra metadata (e.g. `partitionKey`, `_crHasCrossSiteAncestor`)
539
+ * that Playwright rejects as "Invalid cookie fields".
540
+ */
541
+ const VALID_COOKIE_FIELDS = new Set([
542
+ 'name', 'value', 'domain', 'path', 'expires', 'httpOnly', 'secure', 'sameSite',
543
+ ]);
544
+
545
+ /**
546
+ * Sanitize a Playwright storage state object so it can be safely passed
547
+ * to `browser.newContext({ storageState })`.
548
+ *
549
+ * - Strips any cookie field not in the Playwright whitelist (catches CDP-only
550
+ * metadata like `partitionKey`, `_crHasCrossSiteAncestor`, etc.)
551
+ * - Removes `expires` when ≤ 0 (CDP session cookies)
552
+ * - Drops cookies missing `name` or `domain`
553
+ *
554
+ * @param {Object} storageState - Raw Playwright storage state (`{ cookies, origins }`)
555
+ * @returns {{ sanitized: Object, stats: { fixed: number, removed: number, stripped: number } }}
556
+ */
557
+ function sanitizeStorageState(storageState) {
558
+ if (!storageState || !storageState.cookies) {
559
+ return { sanitized: storageState, stats: { fixed: 0, removed: 0, stripped: 0 } };
560
+ }
561
+
562
+ let fixed = 0;
563
+ let removed = 0;
564
+ let stripped = 0;
565
+
566
+ const cleanCookies = [];
567
+ for (const cookie of storageState.cookies) {
568
+ // Remove cookies missing required fields (value can be empty string)
569
+ if (!cookie.name || !cookie.domain) {
570
+ removed++;
571
+ continue;
572
+ }
573
+
574
+ // Strip unknown fields (CDP metadata Playwright doesn't recognize)
575
+ const clean = {};
576
+ let hadUnknown = false;
577
+ for (const [key, val] of Object.entries(cookie)) {
578
+ if (VALID_COOKIE_FIELDS.has(key)) {
579
+ clean[key] = val;
580
+ } else {
581
+ hadUnknown = true;
582
+ }
583
+ }
584
+ if (hadUnknown) {
585
+ stripped++;
586
+ }
587
+
588
+ // Fix invalid expires values (session cookies from CDP)
589
+ if (clean.expires !== undefined && clean.expires <= 0) {
590
+ fixed++;
591
+ delete clean.expires;
592
+ }
593
+
594
+ cleanCookies.push(clean);
595
+ }
596
+
597
+ return {
598
+ sanitized: { ...storageState, cookies: cleanCookies },
599
+ stats: { fixed, removed, stripped },
600
+ };
601
+ }
602
+
603
+ module.exports = {
604
+ connectToActivePage,
605
+ checkCdpEndpoint,
606
+ getCdpTargets,
607
+ printChromeInstructions,
608
+ saveSessionState,
609
+ getDefaultSessionPath,
610
+ autoSyncSessionFromCDP,
611
+ sanitizeStorageState,
612
+ };