browser-commander 0.26.2 → 0.27.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 (58) hide show
  1. package/.jscpd-baseline.json +24 -23
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +2 -0
  4. package/package.json +5 -3
  5. package/src/bindings.js +19 -0
  6. package/src/browser/attach/snapshot-launch.js +8 -6
  7. package/src/browser/attach/snapshot.js +39 -2
  8. package/src/browser/browser-cookie-database.js +71 -2
  9. package/src/browser/browser-cookie-session.js +183 -0
  10. package/src/browser/browser-cookies.js +29 -0
  11. package/src/browser/browser-sources.js +13 -0
  12. package/src/browser/browser-sources.json +13 -0
  13. package/src/browser/debugging-port.js +4 -2
  14. package/src/browser/default-browser.js +33 -9
  15. package/src/browser/launch-diagnostics.js +102 -0
  16. package/src/browser/launcher.js +28 -8
  17. package/src/browser/migration/sqlite-snapshot.js +5 -27
  18. package/src/browser/navigation.js +173 -98
  19. package/src/browser/real-browser.js +135 -68
  20. package/src/browser/session-cookies.js +78 -0
  21. package/src/browser/session-persistence.js +72 -0
  22. package/src/browser/storage-state.js +32 -6
  23. package/src/browser/system-browser.js +2 -2
  24. package/src/core/dialog-manager.js +7 -0
  25. package/src/core/managed-navigation.js +101 -0
  26. package/src/core/navigation-manager.js +62 -78
  27. package/src/core/navigation-operation.js +25 -0
  28. package/src/core/navigation-readiness.js +40 -16
  29. package/src/core/network-tracker.js +39 -92
  30. package/src/core/page-session.js +3 -2
  31. package/src/core/page-trigger-manager.js +15 -12
  32. package/src/core/readiness.js +67 -13
  33. package/src/core/subscriptions.js +20 -0
  34. package/src/core/webdriver-page.js +38 -0
  35. package/src/elements/locators.js +35 -5
  36. package/src/elements/reusable.js +92 -0
  37. package/src/elements/visibility.js +6 -11
  38. package/src/exports.js +14 -0
  39. package/src/factory.js +4 -4
  40. package/src/high-level/universal-logic.js +57 -21
  41. package/src/interactions/click.js +3 -0
  42. package/src/interactions/scroll.js +8 -2
  43. package/src/utilities/subprocess.js +15 -4
  44. package/tests/e2e/navigation-budget.e2e.test.js +132 -0
  45. package/tests/e2e/reusable-session.e2e.test.js +231 -0
  46. package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
  47. package/tests/unit/browser/attach/snapshot.test.js +33 -0
  48. package/tests/unit/browser/catalogue-launch.test.js +50 -2
  49. package/tests/unit/browser/default-browser.test.js +18 -0
  50. package/tests/unit/browser/launch-diagnostics.test.js +52 -0
  51. package/tests/unit/browser/real-browser.test.js +1 -1
  52. package/tests/unit/browser/session-workflows.test.js +206 -0
  53. package/tests/unit/browser/storage-state.test.js +7 -2
  54. package/tests/unit/core/navigation-budget.test.js +120 -0
  55. package/tests/unit/core/navigation-manager.test.js +23 -1
  56. package/tests/unit/core/webdriver-page.test.js +33 -0
  57. package/tests/unit/elements/reusable-helpers.test.js +83 -0
  58. package/tests/unit/traces/links.test.js +2 -2
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "version": 1,
3
3
  "fingerprints": {
4
+ "00504cdec3a262ed": 1,
4
5
  "00d381dd611ae8ac": 1,
5
6
  "029a62ef1f48e856": 1,
7
+ "02fc9852e9ba4c49": 1,
6
8
  "038170e72e0af1f5": 1,
7
9
  "0413f601c8911a92": 1,
8
10
  "04b28d01d0bb47dc": 1,
@@ -27,9 +29,7 @@
27
29
  "14beedf656e57fba": 1,
28
30
  "150c8efe188ac225": 1,
29
31
  "16340a16ed52849e": 1,
30
- "16e58ea67d8ef4e8": 1,
31
32
  "17c26c0db2d35ad1": 1,
32
- "193b46c0e7f72053": 1,
33
33
  "19ff70fc80fad524": 1,
34
34
  "1a9f0f0f68ed445c": 1,
35
35
  "1ad4dc7e19a77f68": 1,
@@ -42,10 +42,10 @@
42
42
  "22755b35701c5bb6": 1,
43
43
  "2296c06fa00da254": 1,
44
44
  "229b903386b17538": 1,
45
+ "22ab9a1ca6316d24": 1,
45
46
  "22cbd954358d25f8": 1,
46
47
  "230bfe57cfda164d": 1,
47
48
  "2357ae472525f6bf": 1,
48
- "2394170c8a2a0e9a": 1,
49
49
  "23d2486c0e269b77": 1,
50
50
  "266893ea7a511fe4": 1,
51
51
  "266b9cb95f783cdf": 1,
@@ -66,7 +66,6 @@
66
66
  "35cf2769645f3234": 1,
67
67
  "37bd4114d0089f49": 1,
68
68
  "39cf8134a7fdaa31": 1,
69
- "3a9f1672ecc1d55b": 1,
70
69
  "3adca52a3f04837c": 1,
71
70
  "3c1017def7b3216c": 1,
72
71
  "3c5cb4b62d3739e3": 1,
@@ -75,15 +74,17 @@
75
74
  "406fed8e5b39aafd": 1,
76
75
  "40aa2953f783819c": 1,
77
76
  "418db56376cc23bd": 1,
77
+ "41ba3b00db6d29f5": 1,
78
78
  "421bb16fafde32a7": 1,
79
79
  "441570d51d6f70a4": 1,
80
80
  "457c71f32f53833d": 1,
81
81
  "4715a17d690865ee": 1,
82
+ "47dc143be0bd8be5": 1,
82
83
  "4b0d3cc5c27227e7": 1,
83
84
  "4c3b94caa28045d0": 1,
85
+ "4d1ff99bb19f5685": 1,
84
86
  "4f3c541b3f3e3f23": 1,
85
87
  "4f93b074bc2ac9e8": 1,
86
- "5085765e1f103f3e": 1,
87
88
  "5184c90c9f8406c3": 1,
88
89
  "520a28ee2ced56c2": 1,
89
90
  "53519ec61a6ba319": 1,
@@ -97,13 +98,16 @@
97
98
  "57aa3b487de27c90": 1,
98
99
  "57c410b183671eec": 1,
99
100
  "5825f31d55b82fae": 1,
101
+ "589649758d30b0b4": 1,
100
102
  "58c08b8242c875ec": 1,
101
- "58c1ca12d4d2b50f": 1,
103
+ "591377945ae26575": 1,
104
+ "59b7172dfb646586": 1,
102
105
  "5a09e2677334ba05": 1,
103
106
  "5b5d3f792532356c": 1,
104
107
  "5ba2f3218754f04c": 1,
105
108
  "60052da86e7bb241": 1,
106
109
  "608e1dd27138ec6f": 1,
110
+ "60a3149247760d9b": 1,
107
111
  "60d0c58a01fc8746": 1,
108
112
  "61ae5da6cc003836": 1,
109
113
  "61bcadcd8dc9f63e": 1,
@@ -112,6 +116,7 @@
112
116
  "6479c3604c153ec4": 1,
113
117
  "66fb97eebd7ff0dc": 1,
114
118
  "67a95e428b8fa31c": 1,
119
+ "67c309590c5d69a1": 1,
115
120
  "6880d620e0e67229": 1,
116
121
  "69d50e6f6a5f2ee9": 1,
117
122
  "6a7ed9ad3fc00a39": 1,
@@ -142,35 +147,28 @@
142
147
  "79e705499ddaf44b": 1,
143
148
  "7e6bad380644f9b0": 1,
144
149
  "80cc77491cde09e5": 1,
145
- "80f564fe45f6b315": 1,
146
- "81a56c10bfd42e39": 1,
147
150
  "837ba752ecdaf052": 1,
148
151
  "86646647aab19e39": 1,
149
152
  "8678919f1350f154": 1,
150
153
  "86aae81b1aa0d21b": 1,
151
154
  "87484a2744e09748": 1,
152
- "8b013ac20e436d76": 1,
153
155
  "8c2b1408886c1db1": 1,
154
156
  "8e55579a412bc058": 1,
155
157
  "8ec18cdbd4a96518": 1,
156
158
  "901317bc4d7d30d0": 1,
157
159
  "91049efadc65169b": 1,
158
160
  "9134185980de8a17": 1,
159
- "916833bafca4fa63": 1,
160
161
  "91a538e4a0c28f25": 1,
161
- "92ddafa7e65be7eb": 1,
162
162
  "94821a4d1b745dfd": 1,
163
163
  "961e3cc731bbc9fe": 1,
164
164
  "97c5a112fdde7be8": 1,
165
165
  "990d87f21678fa96": 1,
166
- "9a72b1c2f22156ca": 1,
167
166
  "9b6ef4df04562546": 1,
168
167
  "9e049e3eefca1869": 1,
169
- "9fbb8296259dedbe": 1,
168
+ "9fdc43960bb59bcf": 1,
170
169
  "9fe73b5b918293fe": 1,
171
170
  "a0183465782b0b15": 1,
172
171
  "a11079d40b28716c": 1,
173
- "a133391f90bd1a28": 1,
174
172
  "a1e3a3a13b27dba4": 1,
175
173
  "a1ef39a15701c2b5": 1,
176
174
  "a2a00d268fb2cee4": 1,
@@ -179,17 +177,21 @@
179
177
  "a5298242b5439858": 1,
180
178
  "a6ad7ab6c05656dc": 1,
181
179
  "a6ffab1aacd00af6": 1,
180
+ "a71a4b720d466e32": 1,
181
+ "a71fab6edf38c846": 1,
182
182
  "a78d76908162699e": 1,
183
183
  "a9bd4ffef6da5faf": 1,
184
+ "ac3fe4ef966e0209": 1,
184
185
  "acc95d694eb2f581": 1,
185
186
  "ad4cfa76daa1b6fc": 1,
186
187
  "ad50b48938af8aef": 1,
188
+ "adc4fadd2bfd220c": 1,
187
189
  "add45ff3644fbeeb": 1,
188
190
  "aee1e5c65f783848": 1,
189
191
  "aee45e8b58c2d87f": 1,
190
192
  "af702b20ebd45ef2": 1,
191
- "af9d6a2f4eba146e": 1,
192
193
  "b010686af23efa90": 1,
194
+ "b141049dde51b3e8": 1,
193
195
  "b35ff46c365dcd18": 1,
194
196
  "b41581af204af40e": 1,
195
197
  "b71fa5250da28739": 1,
@@ -224,24 +226,23 @@
224
226
  "cebff7d1cc87f646": 1,
225
227
  "cfc928aa393146b6": 1,
226
228
  "cfd68135b744c27b": 1,
227
- "cfdc0cca50233fcb": 1,
228
229
  "d016035b3bf3340c": 1,
229
230
  "d097f41a6f43e634": 1,
230
- "d0da8714c4b7de30": 1,
231
231
  "d17201496391f6cc": 1,
232
232
  "d17e8db5a2bab966": 1,
233
- "d1a4a8e6a523b752": 1,
234
- "d1e5c2961267399a": 1,
235
233
  "d2ed3d02dd6a2d8c": 1,
236
234
  "d6157a86fea41907": 1,
237
235
  "d6fa9884eee815f0": 1,
238
236
  "d86bafb915488255": 1,
239
237
  "da2c10a66acdb52f": 1,
240
238
  "dabb9fc09638e178": 1,
239
+ "dcc616702e5b0cb4": 1,
241
240
  "de0bf2317c53d427": 1,
242
241
  "de1b481bfe8c3e58": 1,
243
242
  "df2dc922f40485e7": 1,
244
243
  "df3b85f6947db144": 1,
244
+ "e071c1594e47dd0f": 1,
245
+ "e18fb1b06eac2f6a": 1,
245
246
  "e2ef7800deb0f464": 1,
246
247
  "e3111b845acb7a0e": 1,
247
248
  "e59733bafda785e9": 1,
@@ -249,14 +250,14 @@
249
250
  "e67726a913675120": 1,
250
251
  "e686288f2e9b8a6d": 1,
251
252
  "e7d7bb7e163d2327": 1,
253
+ "e902a25d9132eda9": 1,
252
254
  "ea1195048ded896c": 1,
253
255
  "ea88711c5deb1fe5": 1,
254
256
  "eb9a70c2a21094ff": 1,
255
257
  "ebe03c5f8c2f5147": 1,
256
258
  "ee19c8b9b332ee98": 1,
257
- "eeca0ccf8d821f21": 1,
259
+ "ef6b9112e659147b": 1,
258
260
  "f07f979f8c2706e8": 1,
259
- "f186f73c118b8e8a": 1,
260
261
  "f38f72095a67c95e": 1,
261
262
  "f40ba84763547f41": 1,
262
263
  "f46509ed693574df": 1,
@@ -264,15 +265,15 @@
264
265
  "f4bfa7e503443cd1": 1,
265
266
  "f6678d6748e9c902": 1,
266
267
  "f67a07a611f39cb2": 1,
268
+ "f762b61eef0af530": 1,
267
269
  "f7b1f9d699bcd580": 1,
268
270
  "f81e07af5d3a1c3c": 1,
269
271
  "f904844f89cab699": 1,
270
272
  "f911821aa7dda47f": 1,
271
273
  "f97bf515bfd95829": 1,
272
274
  "fae844842fba705b": 1,
273
- "faf76ec43b2dbbaa": 1,
275
+ "fb4a079a529a6285": 1,
274
276
  "fc0d7bde0033411d": 1,
275
- "fc6ed7d760c98f51": 1,
276
277
  "fccfd8b004c12dd4": 1,
277
278
  "fe859df9f97c75d4": 1
278
279
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fab33b3: Honor per-call navigation policies, deadlines and cancellation; expose bounded redacted launch diagnostics. Add reusable indexed controls, ordered selectors/text, removable subscriptions and non-clearing flags. Add cookie-only browser reads, runtime cookies, session discovery and dedicated-profile persistence. Make SQLite optional, align links-notation 0.23, and document Bun CDP compatibility.
8
+
9
+ ## 0.26.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 796ebd8: Accept dedicated macOS application profiles under `~/Library/Application Support` by separating Safari import discovery roots from protected browser data paths. Safari, Technology Preview and Chrome default profiles remain protected.
14
+
3
15
  ## 0.26.2
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -1063,3 +1063,5 @@ See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documen
1063
1063
  ## License
1064
1064
 
1065
1065
  [UNLICENSE](../LICENSE)
1066
+
1067
+ See [navigation budgets, launch diagnostics, reusable helpers and sessions](../docs/navigation-launch-and-sessions.md) for the shared API contracts and engine limitations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -80,11 +80,10 @@
80
80
  }
81
81
  },
82
82
  "dependencies": {
83
- "better-sqlite3": "^13.0.3",
84
83
  "bplist-parser": "^0.5.0",
85
84
  "command-stream": "^1.4.0",
86
85
  "csv-parse": "^7.0.3",
87
- "links-notation": "^0.22.0",
86
+ "links-notation": "^0.23.0",
88
87
  "log-lazy": "^1.0.4",
89
88
  "plist": "^5.0.0",
90
89
  "test-anywhere": "^0.9.1"
@@ -108,5 +107,8 @@
108
107
  "better-sqlite3@13.0.3": true,
109
108
  "node-pty": false,
110
109
  "puppeteer": false
110
+ },
111
+ "optionalDependencies": {
112
+ "better-sqlite3": "^13.0.3"
111
113
  }
112
114
  }
package/src/bindings.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * This module provides helper functions for binding page, engine, and log to library functions.
4
4
  */
5
5
 
6
+ import { findFirst, hasText, isChecked, check } from './elements/reusable.js';
7
+ import { setCookies, clearCookies } from './browser/session-cookies.js';
6
8
  import { wait, evaluate, safeEvaluate } from './utilities/wait.js';
7
9
  import { getUrl, unfocusAddressBar } from './utilities/url.js';
8
10
  import {
@@ -52,6 +54,8 @@ import {
52
54
  fillTextArea,
53
55
  } from './interactions/fill.js';
54
56
  import {
57
+ readFlag,
58
+ uninstallClickListener,
55
59
  waitForUrlCondition,
56
60
  installClickListener,
57
61
  checkAndClearFlag,
@@ -116,6 +120,7 @@ export function createBoundFunctions(options = {}) {
116
120
  page,
117
121
  waitForUrlStabilization: waitForUrlStabilizationBound,
118
122
  navigationManager,
123
+ networkTracker,
119
124
  });
120
125
  const setContentBound = (opts) =>
121
126
  setContent({
@@ -352,6 +357,20 @@ export function createBoundFunctions(options = {}) {
352
357
  installClickListener: installClickListenerBound,
353
358
  checkAndClearFlag: checkAndClearFlagBound,
354
359
  findToggleButton: findToggleButtonBound,
360
+ findFirst: (opts) => findFirst({ ...opts, page, engine }),
361
+ hasText: (opts) => hasText({ ...opts, page, engine }),
362
+ isChecked: (opts) => isChecked({ ...opts, page, engine }),
363
+ check: (opts) => check({ ...opts, page, engine, wait: waitBound }),
364
+ readFlag: (opts) => readFlag({ ...opts, evaluate: evaluateBound }),
365
+ uninstallClickListener: (opts) =>
366
+ uninstallClickListener({ ...opts, evaluate: evaluateBound }),
367
+ setCookies: (options) =>
368
+ setCookies({
369
+ page,
370
+ engine,
371
+ cookies: Array.isArray(options) ? options : options.cookies,
372
+ }),
373
+ clearCookies: (opts = {}) => clearCookies({ ...opts, page, engine }),
355
374
 
356
375
  // PDF generation
357
376
  pdf: pdfBound,
@@ -1,3 +1,4 @@
1
+ import { findBrowserSource } from '../browser-sources.js';
1
2
  import { resolveRestrictions } from '../restrictions.js';
2
3
  import { describeAttachDifferences } from './differences.js';
3
4
  import { snapshotUserDataDir } from './snapshot.js';
@@ -19,13 +20,13 @@ const KEYSTORE_BYPASS_SWITCHES = ['--use-mock-keychain', '--password-store='];
19
20
  * @returns {string}
20
21
  */
21
22
  export function snapshotBrowserForChannel(channel) {
22
- if (channel.startsWith('msedge')) {
23
- return 'edge';
24
- }
25
- if (channel === 'brave' || channel === 'chromium') {
26
- return channel;
23
+ const source = findBrowserSource(channel);
24
+ if (!source || source.family !== 'chromium') {
25
+ throw new TypeError(
26
+ `Snapshot channel must identify a Chromium browser: ${channel}`
27
+ );
27
28
  }
28
- return 'chrome';
29
+ return source.id;
29
30
  }
30
31
 
31
32
  /**
@@ -122,6 +123,7 @@ export async function prepareSnapshotLaunch({
122
123
  browser,
123
124
  profile,
124
125
  userDataDir: attach.userDataDir,
126
+ ...(attach.include ? { include: attach.include } : {}),
125
127
  });
126
128
  const warnings = [
127
129
  ...(report.warnings ?? []),
@@ -5,6 +5,7 @@ import {
5
5
  readdir,
6
6
  readFile,
7
7
  stat,
8
+ lstat,
8
9
  writeFile,
9
10
  } from 'node:fs/promises';
10
11
  import os from 'node:os';
@@ -279,6 +280,7 @@ export async function snapshotUserDataDir({
279
280
  profile = 'Default',
280
281
  userDataDir,
281
282
  to,
283
+ include,
282
284
  platform = process.platform,
283
285
  homeDir = os.homedir(),
284
286
  environment = process.env,
@@ -293,6 +295,16 @@ export async function snapshotUserDataDir({
293
295
  );
294
296
  }
295
297
  assertProfileName(profile);
298
+ if (
299
+ include !== undefined &&
300
+ (!Array.isArray(include) ||
301
+ include.length !== 1 ||
302
+ include[0] !== 'cookies')
303
+ ) {
304
+ throw new TypeError(
305
+ 'snapshot include must be ["cookies"] or omitted for the whole profile'
306
+ );
307
+ }
296
308
  const sourceRoot =
297
309
  userDataDir ??
298
310
  browserProfileRoot(normalizedBrowser, { platform, homeDir, environment });
@@ -310,6 +322,7 @@ export async function snapshotUserDataDir({
310
322
  sourceRoot,
311
323
  profile,
312
324
  target,
325
+ include,
313
326
  source: { browser: normalizedBrowser, profile, userDataDir: sourceRoot },
314
327
  });
315
328
  } catch (error) {
@@ -320,7 +333,7 @@ export async function snapshotUserDataDir({
320
333
  }
321
334
  }
322
335
 
323
- async function copyProfile({ sourceRoot, profile, target, source }) {
336
+ async function copyProfile({ sourceRoot, profile, target, source, include }) {
324
337
  const copier = new SnapshotCopier(sourceRoot, target);
325
338
  const warnings = [];
326
339
  const localState = await stat(path.join(sourceRoot, LOCAL_STATE_FILE)).catch(
@@ -336,7 +349,31 @@ async function copyProfile({ sourceRoot, profile, target, source }) {
336
349
  'The source has no Local State; on Windows it holds the key cookies and passwords are encrypted with, so they cannot be decrypted in the copy.',
337
350
  });
338
351
  }
339
- await copier.copyDirectory(profile);
352
+ if (include) {
353
+ for (const relative of [
354
+ `${profile}/Cookies`,
355
+ `${profile}/Network/Cookies`,
356
+ ]) {
357
+ const parent = await lstat(
358
+ path.dirname(path.join(sourceRoot, relative))
359
+ ).catch(() => null);
360
+ if (parent?.isSymbolicLink()) {
361
+ copier.skipped.push({ item: relative, reason: 'symlink' });
362
+ continue;
363
+ }
364
+ const entry = await lstat(path.join(sourceRoot, relative)).catch(
365
+ () => null
366
+ );
367
+ if (entry?.isFile()) {
368
+ await mkdir(path.dirname(path.join(target, relative)), {
369
+ recursive: true,
370
+ });
371
+ await copier.copyFile(relative);
372
+ }
373
+ }
374
+ } else {
375
+ await copier.copyDirectory(profile);
376
+ }
340
377
  await prepareUserDataDir(target, { profileDirectory: profile });
341
378
  await markExitedCleanly(path.join(target, profile));
342
379
 
@@ -1,5 +1,3 @@
1
- import BetterSqlite3 from 'better-sqlite3';
2
-
3
1
  let builtInSqlitePromise;
4
2
 
5
3
  function loadBuiltInSqlite() {
@@ -18,6 +16,7 @@ export async function openSqliteDatabase(
18
16
  if (DatabaseSync) {
19
17
  return new DatabaseSync(filename, { readOnly });
20
18
  }
19
+ const BetterSqlite3 = await loadOptionalSqlite();
21
20
  return new BetterSqlite3(filename, {
22
21
  fileMustExist,
23
22
  readonly: readOnly,
@@ -33,3 +32,73 @@ export function preserveIntegerPrecision(statement) {
33
32
  }
34
33
  return statement;
35
34
  }
35
+
36
+ /** Lazy native fallback for runtimes without built-in SQLite. */
37
+ export async function loadOptionalSqlite() {
38
+ try {
39
+ return (await import('better-sqlite3')).default;
40
+ } catch (cause) {
41
+ throw new Error(
42
+ 'This SQLite operation needs optional better-sqlite3 on this runtime. Install it or use Node >=22.16 for built-in SQLite backups.',
43
+ { cause }
44
+ );
45
+ }
46
+ }
47
+
48
+ function backupProgress() {
49
+ let remaining = Infinity;
50
+ let lastProgress = Date.now();
51
+ return ({ remainingPages }) => {
52
+ if (remainingPages < remaining) {
53
+ lastProgress = Date.now();
54
+ } else if (Date.now() - lastProgress >= 1000) {
55
+ throw new Error('SQLite backup made no progress');
56
+ }
57
+ remaining = remainingPages;
58
+ return 256;
59
+ };
60
+ }
61
+
62
+ /** Make an online backup, including committed WAL data. */
63
+ export async function backupSqliteDatabase(sourcePath, snapshotPath) {
64
+ let native;
65
+ try {
66
+ native = await loadOptionalSqlite();
67
+ } catch {
68
+ /* built-in fallback */
69
+ }
70
+ let sqlite;
71
+ try {
72
+ sqlite = await import('node:sqlite');
73
+ } catch {
74
+ /* native fallback */
75
+ }
76
+ if (!native && sqlite?.backup) {
77
+ const source = new sqlite.DatabaseSync(sourcePath, {
78
+ readOnly: true,
79
+ timeout: 1000,
80
+ });
81
+ try {
82
+ source.prepare('PRAGMA schema_version').get();
83
+ await sqlite.backup(source, snapshotPath, {
84
+ rate: 256,
85
+ progress: backupProgress(),
86
+ });
87
+ } finally {
88
+ source.close();
89
+ }
90
+ return;
91
+ }
92
+ const BetterSqlite3 = native ?? (await loadOptionalSqlite());
93
+ const source = new BetterSqlite3(sourcePath, {
94
+ readonly: true,
95
+ fileMustExist: true,
96
+ timeout: 1000,
97
+ });
98
+ try {
99
+ source.pragma('schema_version');
100
+ await source.backup(snapshotPath, { progress: backupProgress() });
101
+ } finally {
102
+ source.close();
103
+ }
104
+ }
@@ -0,0 +1,183 @@
1
+ import path from 'node:path';
2
+ import { rm } from 'node:fs/promises';
3
+ import { browserFamily } from './browser-sources.js';
4
+ import { matchesDomains } from './migration/domains.js';
5
+ import {
6
+ resolveBrowserProfile,
7
+ resolveSourceBrowser,
8
+ } from './browser-profiles.js';
9
+ import { saveStorageState } from './storage-state.js';
10
+ import { setCookies } from './session-cookies.js';
11
+
12
+ /** Decrypt through the source browser in a disposable cookie-only snapshot. */
13
+ export async function readBrowserCookieSession(options, dependencies = {}) {
14
+ const browser = await resolveSourceBrowser(options.browser, dependencies);
15
+ if (browserFamily(browser) !== 'chromium') {
16
+ throw new TypeError(
17
+ 'Browser-backed cookie reading requires a Chromium snapshot; use via: database for Firefox or Safari'
18
+ );
19
+ }
20
+ const profile = options.profileDir
21
+ ? { path: options.profileDir }
22
+ : await resolveBrowserProfile({
23
+ ...dependencies,
24
+ browser,
25
+ profile: options.profile,
26
+ });
27
+ const launch =
28
+ dependencies.launch ??
29
+ (await import('./real-browser.js')).launchRealBrowser;
30
+ const result = await launch({
31
+ ...options.launchOptions,
32
+ channel: browser,
33
+ engine: options.engine ?? 'playwright',
34
+ attach: {
35
+ mode: 'snapshot',
36
+ browser,
37
+ profile: path.basename(profile.path),
38
+ userDataDir: path.dirname(profile.path),
39
+ include: ['cookies'],
40
+ },
41
+ });
42
+ try {
43
+ const state = await saveStorageState(result.page);
44
+ return state.cookies.filter((cookie) =>
45
+ matchesDomains(
46
+ cookie.domain,
47
+ options.domains ?? (options.domainFilter ? [options.domainFilter] : [])
48
+ )
49
+ );
50
+ } finally {
51
+ await result.close();
52
+ }
53
+ }
54
+
55
+ async function readDatabaseSession(source, options, dependencies) {
56
+ const read =
57
+ dependencies.readCookies ??
58
+ (await import('./browser-cookies.js')).readBrowserCookies;
59
+ const cookies = (
60
+ await read({
61
+ browser: source.browser,
62
+ profileDir: source.path,
63
+ via: 'database',
64
+ cache: false,
65
+ })
66
+ ).filter((cookie) => matchesDomains(cookie.domain, options.domains));
67
+ let loggedIn = null;
68
+ if (options.isLoggedIn) {
69
+ const launch =
70
+ dependencies.launch ?? (await import('./launcher.js')).launchBrowser;
71
+ const engine = source.engine ?? options.engine ?? 'playwright';
72
+ const result = await launch({
73
+ ...options.launchOptions,
74
+ ...source.launchOptions,
75
+ engine,
76
+ launch: 'engine',
77
+ userDataDir: undefined,
78
+ attach: undefined,
79
+ persistSessionCookies: false,
80
+ });
81
+ try {
82
+ await setCookies({ page: result.page, engine, cookies });
83
+ loggedIn = Boolean(
84
+ await options.isLoggedIn({ ...result, source, cookies })
85
+ );
86
+ } finally {
87
+ await result.close();
88
+ }
89
+ }
90
+ return { ...source, cookies, loggedIn };
91
+ }
92
+
93
+ /** Discover candidate profiles and optionally validate their live sign-in state. */
94
+ export async function findSiteSessions(options = {}, dependencies = {}) {
95
+ if (!Array.isArray(options.domains) || !options.domains.length) {
96
+ throw new TypeError('findSiteSessions requires domains');
97
+ }
98
+ const list =
99
+ dependencies.listSources ??
100
+ (await import('./browser-cookies.js')).listCookieSources;
101
+ const sources = [
102
+ ...(options.sources ?? (await list({ domains: options.domains }))),
103
+ ...(options.profiles ?? []),
104
+ ];
105
+ const sessions = [];
106
+ for (const source of sources) {
107
+ let result;
108
+ let snapshot;
109
+ try {
110
+ if (browserFamily(source.browser) !== 'chromium') {
111
+ sessions.push(await readDatabaseSession(source, options, dependencies));
112
+ continue;
113
+ }
114
+ const attach = {
115
+ mode: 'snapshot',
116
+ browser: source.browser,
117
+ profile: path.basename(source.path),
118
+ userDataDir: path.dirname(source.path),
119
+ include: ['cookies'],
120
+ };
121
+ if (source.launch === 'engine') {
122
+ const copy =
123
+ dependencies.snapshot ??
124
+ (await import('./attach/snapshot.js')).snapshotUserDataDir;
125
+ snapshot = await copy(attach);
126
+ const launch =
127
+ dependencies.launch ?? (await import('./launcher.js')).launchBrowser;
128
+ result = await launch({
129
+ ...options.launchOptions,
130
+ ...source.launchOptions,
131
+ engine: source.engine ?? options.engine ?? 'playwright',
132
+ launch: 'engine',
133
+ userDataDir: snapshot.target,
134
+ args: [
135
+ ...(options.launchOptions?.args ?? []),
136
+ ...(source.launchOptions?.args ?? []),
137
+ `--profile-directory=${attach.profile}`,
138
+ ],
139
+ });
140
+ } else {
141
+ const launch =
142
+ dependencies.launch ??
143
+ (await import('./real-browser.js')).launchRealBrowser;
144
+ result = await launch({
145
+ ...options.launchOptions,
146
+ engine: source.engine ?? options.engine ?? 'playwright',
147
+ channel: source.browser,
148
+ attach,
149
+ });
150
+ }
151
+ const state = await saveStorageState(result.page);
152
+ const cookies = state.cookies.filter((cookie) =>
153
+ matchesDomains(cookie.domain, options.domains)
154
+ );
155
+ const loggedIn = options.isLoggedIn
156
+ ? Boolean(await options.isLoggedIn({ ...result, source, cookies }))
157
+ : null;
158
+ sessions.push({ ...source, cookies, loggedIn });
159
+ } catch (error) {
160
+ sessions.push({
161
+ ...source,
162
+ cookies: [],
163
+ loggedIn: null,
164
+ error: error.message,
165
+ });
166
+ } finally {
167
+ try {
168
+ await result?.close();
169
+ } catch (error) {
170
+ sessions.at(-1).error = error.message;
171
+ } finally {
172
+ if (snapshot) {
173
+ try {
174
+ await rm(snapshot.target, { recursive: true, force: true });
175
+ } catch (error) {
176
+ sessions.at(-1).error = error.message;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ return sessions;
183
+ }