browser-commander 0.24.0 → 0.26.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 (72) hide show
  1. package/.jscpd-baseline.json +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +7 -4
  4. package/package.json +4 -1
  5. package/src/browser/attach/snapshot.js +4 -28
  6. package/src/browser/browser-cookie-credentials.js +15 -8
  7. package/src/browser/browser-cookies.js +36 -30
  8. package/src/browser/browser-profile-files.js +31 -0
  9. package/src/browser/browser-profiles.js +11 -13
  10. package/src/browser/browser-sources.js +39 -9
  11. package/src/browser/browser-sources.json +610 -25
  12. package/src/browser/launcher.js +14 -0
  13. package/src/browser/migration/capabilities.json +117 -0
  14. package/src/browser/migration/chromium-history.sql +22 -0
  15. package/src/browser/migration/chromium-writers.js +85 -0
  16. package/src/browser/migration/cookies.js +4 -1
  17. package/src/browser/migration/data-classes.js +68 -0
  18. package/src/browser/migration/domains.js +32 -0
  19. package/src/browser/migration/firefox-history.js +106 -0
  20. package/src/browser/migration/firefox.js +39 -25
  21. package/src/browser/migration/history.js +161 -2
  22. package/src/browser/migration/index.js +229 -83
  23. package/src/browser/migration/os-crypt-keys.js +1 -5
  24. package/src/browser/migration/password-metadata.js +95 -0
  25. package/src/browser/migration/passwords.js +78 -41
  26. package/src/browser/migration/safari-import.js +100 -0
  27. package/src/browser/migration/safari.js +110 -0
  28. package/src/browser/migration/sqlite-snapshot.js +31 -65
  29. package/src/browser/migration/validation.js +48 -0
  30. package/src/browser/real-browser.js +18 -1
  31. package/src/browser/safari-access.js +45 -0
  32. package/src/browser/safari-cookies.js +39 -36
  33. package/src/browser/safari-launch.js +42 -0
  34. package/src/browser/safari-profiles.js +83 -0
  35. package/src/browser/safari-support.js +98 -0
  36. package/src/browser/storage-state.js +28 -8
  37. package/src/browser/system-browser.js +31 -143
  38. package/src/browser/webdriver.js +64 -19
  39. package/src/cli/args.js +2 -0
  40. package/src/cli/commands.js +2 -0
  41. package/src/cli/high-level-methods.js +2 -0
  42. package/src/cli/sessions.js +5 -0
  43. package/src/core/webdriver-page.js +62 -0
  44. package/src/exports.js +6 -0
  45. package/src/traces/recorder.js +5 -0
  46. package/tests/e2e/safari.e2e.test.js +104 -0
  47. package/tests/unit/browser/attach/snapshot.test.js +30 -0
  48. package/tests/unit/browser/browser-cookies.test.js +35 -0
  49. package/tests/unit/browser/catalogue-launch.test.js +93 -0
  50. package/tests/unit/browser/cookie-sources.test.js +92 -17
  51. package/tests/unit/browser/credential-catalogue.test.js +50 -0
  52. package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
  53. package/tests/unit/browser/migration/data-classes.test.js +89 -0
  54. package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
  55. package/tests/unit/browser/migration/firefox-history.test.js +130 -0
  56. package/tests/unit/browser/migration/history-metadata.test.js +74 -0
  57. package/tests/unit/browser/migration/index.test.js +16 -1
  58. package/tests/unit/browser/migration/opera-keys.test.js +22 -0
  59. package/tests/unit/browser/migration/safari-data.test.js +143 -0
  60. package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
  61. package/tests/unit/browser/migration/validation.test.js +63 -0
  62. package/tests/unit/browser/migration/yandex.test.js +84 -0
  63. package/tests/unit/browser/real-browser.test.js +19 -0
  64. package/tests/unit/browser/safari-cookies.test.js +26 -1
  65. package/tests/unit/browser/safari-profiles.test.js +111 -0
  66. package/tests/unit/browser/safari.test.js +192 -0
  67. package/tests/unit/browser/webdriver.test.js +48 -3
  68. package/tests/unit/cli/dispatcher.test.js +5 -1
  69. package/tests/unit/cli/main.test.js +5 -0
  70. package/tests/unit/scripts/browser-support.test.js +14 -7
  71. package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
  72. package/tests/unit/scripts/migration-support.test.js +31 -0
@@ -3,6 +3,156 @@ import path from 'node:path';
3
3
  import { pathExists } from './fs-utils.js';
4
4
 
5
5
  import { withDatabaseSnapshot } from './sqlite-snapshot.js';
6
+ import { matchesDomains } from './domains.js';
7
+ import { openSqliteDatabase } from '../browser-cookie-database.js';
8
+
9
+ async function filterHistory(targetPath, domains, warnings) {
10
+ if (!domains?.length) {
11
+ return;
12
+ }
13
+ const db = await openSqliteDatabase(targetPath);
14
+ try {
15
+ const tables = new Set(
16
+ db
17
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
18
+ .all()
19
+ .map((row) => row.name)
20
+ );
21
+ if (tables.has('urls')) {
22
+ const remove = db.prepare('DELETE FROM urls WHERE id = ?');
23
+ for (const row of db.prepare('SELECT id,url FROM urls').all()) {
24
+ if (!matchesDomains(row.url, domains)) {
25
+ remove.run(row.id);
26
+ }
27
+ }
28
+ for (const [table, column] of [
29
+ ['visits', 'url'],
30
+ ['segments', 'url_id'],
31
+ ['keyword_search_terms', 'url_id'],
32
+ ]) {
33
+ if (tables.has(table)) {
34
+ db.exec(
35
+ `DELETE FROM ${table} WHERE ${column} NOT IN (SELECT id FROM urls)`
36
+ );
37
+ }
38
+ }
39
+ if (tables.has('visits')) {
40
+ for (const [table, column] of [
41
+ ['visit_source', 'id'],
42
+ ['content_annotations', 'visit_id'],
43
+ ['context_annotations', 'visit_id'],
44
+ ]) {
45
+ if (tables.has(table)) {
46
+ db.exec(
47
+ `DELETE FROM ${table} WHERE ${column} NOT IN (SELECT id FROM visits)`
48
+ );
49
+ }
50
+ }
51
+ }
52
+ if (tables.has('segments') && tables.has('segment_usage')) {
53
+ db.exec(
54
+ 'DELETE FROM segment_usage WHERE segment_id NOT IN (SELECT id FROM segments)'
55
+ );
56
+ }
57
+ }
58
+ filterDownloads(db, tables, domains);
59
+ if (tables.has('top_sites')) {
60
+ const remove = db.prepare('DELETE FROM top_sites WHERE url = ?');
61
+ for (const row of db.prepare('SELECT url FROM top_sites').all()) {
62
+ if (!matchesDomains(row.url, domains)) {
63
+ remove.run(row.url);
64
+ }
65
+ }
66
+ }
67
+ omitUnfilteredMetadata(db, tables, warnings);
68
+ db.exec('VACUUM');
69
+ } finally {
70
+ db.close();
71
+ }
72
+ }
73
+
74
+ function omitUnfilteredMetadata(db, tables, warnings) {
75
+ const filtered = new Set([
76
+ 'urls',
77
+ 'downloads',
78
+ 'top_sites',
79
+ 'meta',
80
+ 'sqlite_sequence',
81
+ ]);
82
+ if (tables.has('urls')) {
83
+ for (const table of ['visits', 'segments', 'keyword_search_terms']) {
84
+ filtered.add(table);
85
+ }
86
+ if (tables.has('visits')) {
87
+ for (const table of [
88
+ 'visit_source',
89
+ 'content_annotations',
90
+ 'context_annotations',
91
+ ]) {
92
+ filtered.add(table);
93
+ }
94
+ }
95
+ if (tables.has('segments')) {
96
+ filtered.add('segment_usage');
97
+ }
98
+ }
99
+ if (tables.has('downloads')) {
100
+ filtered.add('downloads_url_chains');
101
+ filtered.add('downloads_slices');
102
+ }
103
+ // Cluster labels and keywords can describe excluded visits even when a
104
+ // cluster also contains selected visits. Unknown schemas have no safe join.
105
+ for (const table of [...tables].sort()) {
106
+ if (filtered.has(table)) {
107
+ continue;
108
+ }
109
+ const identifier = `"${table.replaceAll('"', '""')}"`;
110
+ const count = db.prepare(`SELECT COUNT(*) AS c FROM ${identifier}`).get().c;
111
+ db.exec(`DELETE FROM ${identifier}`);
112
+ if (count > 0) {
113
+ warnings.push({
114
+ type: 'history',
115
+ item: table,
116
+ reason: 'unsupported-history-metadata',
117
+ detail: `${count} copied metadata rows removed`,
118
+ });
119
+ }
120
+ }
121
+ }
122
+
123
+ function filterDownloads(db, tables, domains) {
124
+ if (!tables.has('downloads')) {
125
+ return;
126
+ }
127
+ const remove = db.prepare('DELETE FROM downloads WHERE id = ?');
128
+ for (const row of db.prepare('SELECT * FROM downloads').all()) {
129
+ const urls = ['url', 'site_url', 'tab_url', 'referrer', 'tab_referrer_url']
130
+ .map((column) => row[column])
131
+ .filter(Boolean);
132
+ if (tables.has('downloads_url_chains')) {
133
+ urls.push(
134
+ ...db
135
+ .prepare('SELECT url FROM downloads_url_chains WHERE id = ?')
136
+ .all(row.id)
137
+ .map((chain) => chain.url)
138
+ );
139
+ }
140
+ // A redirect outside the selected sites is still unrelated browsing data.
141
+ if (!urls.length || urls.some((url) => !matchesDomains(url, domains))) {
142
+ remove.run(row.id);
143
+ }
144
+ }
145
+ for (const [table, column] of [
146
+ ['downloads_url_chains', 'id'],
147
+ ['downloads_slices', 'download_id'],
148
+ ]) {
149
+ if (tables.has(table)) {
150
+ db.exec(
151
+ `DELETE FROM ${table} WHERE ${column} NOT IN (SELECT id FROM downloads)`
152
+ );
153
+ }
154
+ }
155
+ }
6
156
 
7
157
  /**
8
158
  * History and Top Sites migration.
@@ -20,12 +170,13 @@ import { withDatabaseSnapshot } from './sqlite-snapshot.js';
20
170
  * already guarantees a point-in-time consistent copy.
21
171
  */
22
172
 
23
- function snapshotInto({ sourcePath, targetPath }) {
173
+ function snapshotInto({ sourcePath, targetPath, domains, warnings }) {
24
174
  return withDatabaseSnapshot({
25
175
  sourcePath,
26
176
  read: async (snapshotPath) => {
27
177
  const { copyFile } = await import('node:fs/promises');
28
178
  await copyFile(snapshotPath, targetPath);
179
+ await filterHistory(targetPath, domains, warnings);
29
180
  // Best-effort URL count for the report.
30
181
  try {
31
182
  const { openSqliteDatabase } =
@@ -52,7 +203,11 @@ function snapshotInto({ sourcePath, targetPath }) {
52
203
  * @param {string} options.targetProfileDir
53
204
  * @returns {Promise<{migrated: number, skipped: Array, warnings: Array}>}
54
205
  */
55
- export async function migrateHistory({ sourceProfileDir, targetProfileDir }) {
206
+ export async function migrateHistory({
207
+ sourceProfileDir,
208
+ targetProfileDir,
209
+ domains,
210
+ }) {
56
211
  const skipped = [];
57
212
  const warnings = [];
58
213
  await mkdir(targetProfileDir, { recursive: true });
@@ -64,6 +219,8 @@ export async function migrateHistory({ sourceProfileDir, targetProfileDir }) {
64
219
  const count = await snapshotInto({
65
220
  sourcePath: historySource,
66
221
  targetPath: path.join(targetProfileDir, 'History'),
222
+ domains,
223
+ warnings,
67
224
  });
68
225
  urlCount = count ?? 0;
69
226
  migratedDatabases += 1;
@@ -80,6 +237,8 @@ export async function migrateHistory({ sourceProfileDir, targetProfileDir }) {
80
237
  await snapshotInto({
81
238
  sourcePath: topSitesSource,
82
239
  targetPath: path.join(targetProfileDir, 'Top Sites'),
240
+ domains,
241
+ warnings,
83
242
  });
84
243
  migratedDatabases += 1;
85
244
  }
@@ -7,15 +7,25 @@ import {
7
7
  browserProfileRoot,
8
8
  resolveBrowserProfile,
9
9
  } from '../browser-profiles.js';
10
+ import { migrateSafariClass } from './safari-import.js';
11
+ import {
12
+ validateMigrationOptions,
13
+ validateMigrationPaths,
14
+ } from './validation.js';
10
15
  import { migrateBookmarks } from './bookmarks.js';
16
+ import { pathExists } from './fs-utils.js';
11
17
  import { migrateCookies } from './cookies.js';
18
+ import {
19
+ ADDITIONAL_DATA_CLASSES,
20
+ reportAdditionalClasses,
21
+ } from './data-classes.js';
12
22
  import { migrateExtensions } from './extensions.js';
13
23
  import {
14
24
  migrateFirefoxBookmarks,
15
25
  migrateFirefoxPasswords,
16
26
  readFirefoxCookies,
17
- reportFirefoxHistory,
18
27
  } from './firefox.js';
28
+ import { migrateFirefoxHistory } from './firefox-history.js';
19
29
  import { migrateHistory } from './history.js';
20
30
  import { migratePasswords } from './passwords.js';
21
31
  import { migratePreferences } from './preferences.js';
@@ -36,8 +46,8 @@ import {
36
46
  *
37
47
  * Cookies are returned (not written): a running Chromium re-derives its own
38
48
  * cookie encryption, so the launcher seeds them over CDP with the existing
39
- * `seedCookies` path. Every other data class is written into the target profile
40
- * directory.
49
+ * `seedCookies` path. Supported file imports are written into the target profile;
50
+ * classes without a native writer receive explicit skipped reports.
41
51
  */
42
52
 
43
53
  export const ALL_DATA_CLASSES = Object.freeze([
@@ -47,6 +57,7 @@ export const ALL_DATA_CLASSES = Object.freeze([
47
57
  'passwords',
48
58
  'preferences',
49
59
  'extensions',
60
+ ...ADDITIONAL_DATA_CLASSES,
50
61
  ]);
51
62
 
52
63
  // Classification is driven by the shared catalogue so every Chromium variant
@@ -62,14 +73,7 @@ function isFirefoxBrowser(browser) {
62
73
  }
63
74
 
64
75
  function emptyMigrated() {
65
- return {
66
- cookies: 0,
67
- bookmarks: 0,
68
- history: 0,
69
- passwords: 0,
70
- preferences: 0,
71
- extensions: 0,
72
- };
76
+ return Object.fromEntries(ALL_DATA_CLASSES.map((type) => [type, 0]));
73
77
  }
74
78
 
75
79
  async function resolveSourceProfileDir({
@@ -114,34 +118,6 @@ function mergeReport(report, className, fragment) {
114
118
  report.warnings.push(...(fragment.warnings ?? []));
115
119
  }
116
120
 
117
- function reportSafariClasses(report, selected) {
118
- for (const type of ALL_DATA_CLASSES.filter(
119
- (name) => name !== 'cookies' && selected.has(name)
120
- )) {
121
- report.skipped.push({
122
- type,
123
- item: report.source.browser,
124
- reason:
125
- type === 'passwords'
126
- ? 'safari-password-export-required'
127
- : 'safari-class-not-supported',
128
- detail:
129
- type === 'passwords'
130
- ? 'Safari passwords live in the Keychain. Export Passwords from Safari or the Passwords app to CSV; CSV import is tracked separately and is not supported yet.'
131
- : 'Safari currently supports cookie import only; this data class has not been translated.',
132
- });
133
- }
134
- if (report.cookies.length > 0) {
135
- report.warnings.push({
136
- type: 'cookies',
137
- item: report.source.browser,
138
- reason: 'safari-samesite-unavailable',
139
- detail:
140
- 'Cookies.binarycookies does not store SameSite; imported cookies use Lax.',
141
- });
142
- }
143
- }
144
-
145
121
  async function resolvePasswordKeys({
146
122
  keys,
147
123
  browser,
@@ -179,7 +155,8 @@ async function resolvePasswordKeys({
179
155
  * @param {{browser: string, profile: (string|undefined), userDataDir: (string|undefined)}} options.from
180
156
  * @param {string} options.to - Target profile directory
181
157
  * @param {string[]} [options.include=ALL_DATA_CLASSES]
182
- * @param {string[]} [options.domains] - Cookie domain filter
158
+ * @param {string[]} [options.domains] - Per-site host/subdomain filter
159
+ * @param {boolean} [options.includePaymentCards=false] - Separate explicit payment-card consent
183
160
  * @param {string} [options.platform=process.platform]
184
161
  * @param {string} [options.targetBrowser] - Launching browser channel (key derivation)
185
162
  * @param {Object} [options.keys] - Injected password keys {resolveSourceKey, targetKey, targetPrefix, primaryPassword}
@@ -198,6 +175,8 @@ export async function migrateProfile({
198
175
  homeDir = os.homedir(),
199
176
  environment = process.env,
200
177
  runCommand,
178
+ passwordCsv,
179
+ includePaymentCards = false,
201
180
  }) {
202
181
  if (!from?.browser) {
203
182
  throw new TypeError('migrateProfile requires from.browser');
@@ -205,6 +184,17 @@ export async function migrateProfile({
205
184
  if (!to) {
206
185
  throw new TypeError('migrateProfile requires a target directory (to)');
207
186
  }
187
+ validateMigrationOptions({
188
+ include,
189
+ domains,
190
+ targetBrowser,
191
+ to,
192
+ platform,
193
+ homeDir,
194
+ environment,
195
+ classes: ALL_DATA_CLASSES,
196
+ includePaymentCards,
197
+ });
208
198
  // An explicit userDataDir names the source, so only an installed-browser
209
199
  // import is steered towards the profile holding the requested domains.
210
200
  const source = await resolveImportSource({
@@ -216,6 +206,11 @@ export async function migrateProfile({
216
206
  runCommand,
217
207
  });
218
208
  const { browser } = source;
209
+ if (!['chromium', 'firefox', 'safari'].includes(browserFamily(browser))) {
210
+ throw new TypeError(
211
+ `Browser ${browser} supports detection only; its profile format is not supported for migration`
212
+ );
213
+ }
219
214
  const profile = from.profile ?? source.profile ?? 'Default';
220
215
  const isFirefox = isFirefoxBrowser(browser);
221
216
  const sourceProfileDir = await resolveSourceProfileDir({
@@ -227,6 +222,7 @@ export async function migrateProfile({
227
222
  environment,
228
223
  });
229
224
  const selected = new Set(include);
225
+ validateMigrationPaths(sourceProfileDir, to);
230
226
  const report = {
231
227
  source: { browser, profile, userDataDir: from.userDataDir ?? null },
232
228
  target: to,
@@ -238,6 +234,13 @@ export async function migrateProfile({
238
234
  if (source.warning) {
239
235
  report.warnings.push(source.warning);
240
236
  }
237
+ report.skipped.push(
238
+ ...reportAdditionalClasses({
239
+ selected,
240
+ profileDir: sourceProfileDir,
241
+ includePaymentCards,
242
+ })
243
+ );
241
244
 
242
245
  if (selected.has('cookies')) {
243
246
  if (isFirefox) {
@@ -263,8 +266,21 @@ export async function migrateProfile({
263
266
  }
264
267
 
265
268
  if (browserFamily(browser) === 'safari') {
266
- reportSafariClasses(report, selected);
267
- return report;
269
+ return migrateSafariProfile({
270
+ selected,
271
+ passwordCsv,
272
+ keys,
273
+ browser,
274
+ targetBrowser,
275
+ platform,
276
+ sourceProfileDir,
277
+ environment,
278
+ from,
279
+ to,
280
+ domains,
281
+ homeDir,
282
+ report,
283
+ });
268
284
  }
269
285
 
270
286
  if (selected.has('bookmarks')) {
@@ -288,10 +304,15 @@ export async function migrateProfile({
288
304
  report,
289
305
  'history',
290
306
  isFirefox
291
- ? await reportFirefoxHistory({ profileDir: sourceProfileDir })
307
+ ? await migrateFirefoxHistory({
308
+ profileDir: sourceProfileDir,
309
+ targetProfileDir: to,
310
+ domains,
311
+ })
292
312
  : await migrateHistory({
293
313
  sourceProfileDir,
294
314
  targetProfileDir: to,
315
+ domains,
295
316
  })
296
317
  );
297
318
  }
@@ -312,56 +333,181 @@ export async function migrateProfile({
312
333
  );
313
334
  }
314
335
 
336
+ if (isFirefox) {
337
+ for (const type of ['preferences', 'extensions']) {
338
+ if (selected.has(type)) {
339
+ report.skipped.push({
340
+ type,
341
+ item: sourceProfileDir,
342
+ reason: 'firefox-class-not-supported',
343
+ detail:
344
+ 'Firefox preferences and extensions cannot be copied into a Chromium profile.',
345
+ });
346
+ }
347
+ }
348
+ }
349
+
315
350
  if (selected.has('passwords')) {
316
- const passwordKeys = await resolvePasswordKeys({
351
+ await migratePasswordClass(report, {
317
352
  keys,
318
353
  browser,
319
- targetBrowser: targetBrowser ?? (isFirefox ? 'chrome' : browser),
354
+ targetBrowser,
320
355
  platform,
321
356
  sourceProfileDir,
322
357
  environment,
358
+ to,
359
+ isFirefox,
360
+ domains,
323
361
  });
324
- if (!passwordKeys?.targetKey) {
325
- report.skipped.push({
326
- type: 'passwords',
327
- item: 'Login Data',
328
- reason: 'target-key-unavailable',
329
- });
330
- report.warnings.push({
331
- type: 'passwords',
332
- item: 'Login Data',
333
- reason: 'target-key-unavailable',
334
- detail:
335
- 'A target encryption key was not available (on Windows the launcher must generate one and write it into the target Local State); passwords were not migrated.',
336
- });
337
- } else if (isFirefox) {
338
- mergeReport(
339
- report,
340
- 'passwords',
341
- await migrateFirefoxPasswords({
342
- profileDir: sourceProfileDir,
343
- targetProfileDir: to,
362
+ }
363
+
364
+ return report;
365
+ }
366
+
367
+ async function migrateSafariProfile({
368
+ report,
369
+ browser,
370
+ domains,
371
+ environment,
372
+ from,
373
+ homeDir,
374
+ keys,
375
+ passwordCsv,
376
+ platform,
377
+ selected,
378
+ sourceProfileDir,
379
+ targetBrowser,
380
+ to,
381
+ }) {
382
+ const passwordKeys =
383
+ selected.has('passwords') && passwordCsv
384
+ ? await resolvePasswordKeys({
385
+ keys,
386
+ browser,
387
+ targetBrowser: targetBrowser ?? 'chrome',
344
388
  platform,
345
- targetKey: passwordKeys.targetKey,
346
- targetPrefix: passwordKeys.targetPrefix,
347
- primaryPassword: passwordKeys.primaryPassword,
348
- })
349
- );
350
- } else {
351
- mergeReport(
352
- report,
353
- 'passwords',
354
- await migratePasswords({
355
389
  sourceProfileDir,
356
- targetProfileDir: to,
357
- platform,
358
- resolveSourceKey: passwordKeys.resolveSourceKey,
359
- targetKey: passwordKeys.targetKey,
360
- targetPrefix: passwordKeys.targetPrefix,
390
+ environment,
361
391
  })
362
- );
363
- }
392
+ : null;
393
+ for (const type of ALL_DATA_CLASSES.filter(
394
+ (type) =>
395
+ type !== 'cookies' &&
396
+ !ADDITIONAL_DATA_CLASSES.includes(type) &&
397
+ selected.has(type)
398
+ )) {
399
+ mergeReport(
400
+ report,
401
+ type,
402
+ await migrateSafariClass({
403
+ type,
404
+ profileDir: sourceProfileDir,
405
+ targetProfileDir: to,
406
+ domains,
407
+ passwordCsv,
408
+ passwordKeys,
409
+ platform,
410
+ legacyDir:
411
+ from.userDataDir ||
412
+ path.basename(path.dirname(sourceProfileDir)) === 'Profiles'
413
+ ? undefined
414
+ : path.join(
415
+ homeDir,
416
+ 'Library',
417
+ browser === 'safari' ? 'Safari' : 'Safari Technology Preview'
418
+ ),
419
+ })
420
+ );
421
+ }
422
+ if (report.cookies.length) {
423
+ report.warnings.push({
424
+ type: 'cookies',
425
+ item: browser,
426
+ reason: 'safari-samesite-unavailable',
427
+ detail:
428
+ 'Cookies.binarycookies does not store SameSite; imported cookies use Lax.',
429
+ });
364
430
  }
365
-
366
431
  return report;
367
432
  }
433
+
434
+ async function migratePasswordClass(
435
+ report,
436
+ {
437
+ browser,
438
+ domains,
439
+ environment,
440
+ isFirefox,
441
+ keys,
442
+ platform,
443
+ sourceProfileDir,
444
+ targetBrowser,
445
+ to,
446
+ }
447
+ ) {
448
+ if (
449
+ browser === 'yandex' &&
450
+ (await pathExists(path.join(sourceProfileDir, 'Ya Passman Data')))
451
+ ) {
452
+ report.skipped.push({
453
+ type: 'passwords',
454
+ item: 'Ya Passman Data',
455
+ reason: 'yandex-passman-encryption-unsupported',
456
+ detail:
457
+ 'Ya Passman Data uses local_encryptor_data and may require a Yandex master password; this extra encryption layer is not supported. Export passwords to a supported format instead.',
458
+ });
459
+ if (!(await pathExists(path.join(sourceProfileDir, 'Login Data')))) {
460
+ return report;
461
+ }
462
+ }
463
+ const passwordKeys = await resolvePasswordKeys({
464
+ keys,
465
+ browser,
466
+ targetBrowser: targetBrowser ?? (isFirefox ? 'chrome' : browser),
467
+ platform,
468
+ sourceProfileDir,
469
+ environment,
470
+ });
471
+ if (!passwordKeys?.targetKey) {
472
+ report.skipped.push({
473
+ type: 'passwords',
474
+ item: 'Login Data',
475
+ reason: 'target-key-unavailable',
476
+ });
477
+ report.warnings.push({
478
+ type: 'passwords',
479
+ item: 'Login Data',
480
+ reason: 'target-key-unavailable',
481
+ detail:
482
+ 'A target encryption key was not available (on Windows the launcher must generate one and write it into the target Local State); passwords were not migrated.',
483
+ });
484
+ } else if (isFirefox) {
485
+ mergeReport(
486
+ report,
487
+ 'passwords',
488
+ await migrateFirefoxPasswords({
489
+ profileDir: sourceProfileDir,
490
+ targetProfileDir: to,
491
+ platform,
492
+ targetKey: passwordKeys.targetKey,
493
+ targetPrefix: passwordKeys.targetPrefix,
494
+ primaryPassword: passwordKeys.primaryPassword,
495
+ domains,
496
+ })
497
+ );
498
+ } else {
499
+ mergeReport(
500
+ report,
501
+ 'passwords',
502
+ await migratePasswords({
503
+ sourceProfileDir,
504
+ targetProfileDir: to,
505
+ platform,
506
+ resolveSourceKey: passwordKeys.resolveSourceKey,
507
+ targetKey: passwordKeys.targetKey,
508
+ targetPrefix: passwordKeys.targetPrefix,
509
+ domains,
510
+ })
511
+ );
512
+ }
513
+ }
@@ -1,5 +1,3 @@
1
- import path from 'node:path';
2
-
3
1
  import { deriveChromiumCookieKey } from '../browser-cookie-crypto.js';
4
2
  import {
5
3
  decryptWindowsDpapi,
@@ -101,6 +99,4 @@ export async function resolveTargetKey({
101
99
  }
102
100
 
103
101
  /** Convenience: the Local State path next to a profile directory. */
104
- export function localStatePathForProfile(profileDir) {
105
- return path.join(path.dirname(profileDir), 'Local State');
106
- }
102
+ export { localStatePathForProfile } from '../browser-profile-files.js';