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
@@ -17,6 +17,8 @@ import { resolveSystemBrowserExecutable } from './system-browser.js';
17
17
  import { applyFingerprint } from '../fingerprint/apply.js';
18
18
  import { attachDownloads } from '../downloads/attach.js';
19
19
  import { launchWebDriver } from './webdriver.js';
20
+ import { launchSafari } from './safari-launch.js';
21
+ import { isSafariChannel } from './safari-support.js';
20
22
  import {
21
23
  loadStorageState,
22
24
  restorePlaywrightStorageState,
@@ -300,6 +302,18 @@ export async function launchBrowserWithDependencies(
300
302
  options = {},
301
303
  dependencies = {}
302
304
  ) {
305
+ if (isSafariChannel(options.channel ?? options.browser)) {
306
+ validateLaunchMode({
307
+ engine: options.engine ?? 'playwright',
308
+ launch: options.launch ?? 'real',
309
+ attach: options.attach,
310
+ });
311
+ return await launchSafari(options, dependencies);
312
+ }
313
+ return await launchNonSafariBrowser(options, dependencies);
314
+ }
315
+
316
+ async function launchNonSafariBrowser(options, dependencies) {
303
317
  const {
304
318
  engine = 'playwright',
305
319
  launch = 'real',
@@ -0,0 +1,117 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "classes": [
4
+ "cookies",
5
+ "bookmarks",
6
+ "history",
7
+ "passwords",
8
+ "preferences",
9
+ "extensions",
10
+ "localStorage",
11
+ "indexedDB",
12
+ "sessionStorage",
13
+ "autofill",
14
+ "paymentCards",
15
+ "searchEngines",
16
+ "siteSettings",
17
+ "openTabs",
18
+ "downloads",
19
+ "readingList",
20
+ "clientCertificates",
21
+ "passkeys"
22
+ ],
23
+ "sourceFamilies": ["chromium", "firefox", "safari"],
24
+ "targetFamilies": ["chromium", "firefox", "webkit"],
25
+ "imports": {
26
+ "chromium": {
27
+ "chromium": [
28
+ "seed",
29
+ "copy",
30
+ "snapshot",
31
+ "rekey",
32
+ "copy",
33
+ "copy",
34
+ "unsupported",
35
+ "unsupported",
36
+ "unsupported",
37
+ "unsupported",
38
+ "consent/unsupported",
39
+ "unsupported",
40
+ "unsupported",
41
+ "unsupported",
42
+ "unsupported",
43
+ "unsupported",
44
+ "unsupported",
45
+ "non-exportable"
46
+ ]
47
+ },
48
+ "firefox": {
49
+ "chromium": [
50
+ "seed",
51
+ "translate",
52
+ "translate",
53
+ "NSS/rekey",
54
+ "unsupported",
55
+ "unsupported",
56
+ "unsupported",
57
+ "unsupported",
58
+ "unsupported",
59
+ "unsupported",
60
+ "consent/unsupported",
61
+ "unsupported",
62
+ "unsupported",
63
+ "unsupported",
64
+ "unsupported",
65
+ "unsupported",
66
+ "unsupported",
67
+ "non-exportable"
68
+ ]
69
+ },
70
+ "safari": {
71
+ "chromium": [
72
+ "seed",
73
+ "translate",
74
+ "translate",
75
+ "CSV/rekey",
76
+ "unsupported",
77
+ "unsupported",
78
+ "unsupported",
79
+ "unsupported",
80
+ "unsupported",
81
+ "unsupported",
82
+ "consent/unsupported",
83
+ "unsupported",
84
+ "unsupported",
85
+ "unsupported",
86
+ "unsupported",
87
+ "unsupported",
88
+ "unsupported",
89
+ "non-exportable"
90
+ ]
91
+ }
92
+ },
93
+ "wholeProfileMigration": {
94
+ "chromium": "not implemented",
95
+ "firefox": "not implemented",
96
+ "webkit": "not implemented"
97
+ },
98
+ "pendingClasses": [
99
+ "localStorage",
100
+ "indexedDB",
101
+ "sessionStorage",
102
+ "autofill",
103
+ "paymentCards",
104
+ "searchEngines",
105
+ "siteSettings",
106
+ "openTabs",
107
+ "downloads",
108
+ "readingList",
109
+ "clientCertificates"
110
+ ],
111
+ "installedBrowserLaunch": {
112
+ "chromium": "CDP",
113
+ "firefox": "explicit WebDriver setup",
114
+ "safari": "explicit safaridriver setup",
115
+ "detection": "unsupported"
116
+ }
117
+ }
@@ -0,0 +1,22 @@
1
+ -- Core tables match Chromium History schema 70 (compatible version 16).
2
+ -- Chromium initializes remaining ancillary tables when it opens the profile.
3
+ CREATE TABLE meta (key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
4
+ INSERT INTO meta VALUES ('version', '70'), ('last_compatible_version', '16');
5
+ CREATE TABLE urls (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT, url LONGVARCHAR, title LONGVARCHAR,
7
+ visit_count INTEGER DEFAULT 0 NOT NULL, typed_count INTEGER DEFAULT 0 NOT NULL,
8
+ last_visit_time INTEGER DEFAULT 0 NOT NULL, hidden INTEGER DEFAULT 0 NOT NULL
9
+ );
10
+ CREATE TABLE visits (
11
+ id INTEGER PRIMARY KEY AUTOINCREMENT, url INTEGER NOT NULL,
12
+ visit_time INTEGER NOT NULL, from_visit INTEGER DEFAULT 0,
13
+ external_referrer_url TEXT, transition INTEGER DEFAULT 0 NOT NULL,
14
+ segment_id INTEGER DEFAULT 0, visit_duration INTEGER DEFAULT 0 NOT NULL,
15
+ incremented_omnibox_typed_score BOOLEAN DEFAULT FALSE NOT NULL,
16
+ opener_visit INTEGER DEFAULT 0, originator_cache_guid TEXT DEFAULT '',
17
+ originator_visit_id INTEGER DEFAULT 0, originator_from_visit INTEGER DEFAULT 0,
18
+ originator_opener_visit INTEGER DEFAULT 0, is_known_to_sync BOOLEAN DEFAULT FALSE NOT NULL,
19
+ consider_for_ntp_most_visited BOOLEAN DEFAULT FALSE NOT NULL,
20
+ visited_link_id INTEGER DEFAULT 0 NOT NULL, app_id TEXT
21
+ );
22
+ CREATE TABLE visit_source (id INTEGER PRIMARY KEY, source INTEGER NOT NULL);
@@ -0,0 +1,85 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { openSqliteDatabase } from '../browser-cookie-database.js';
4
+
5
+ export async function writeChromiumBookmarks(targetProfileDir, entries) {
6
+ let id = 3;
7
+ let count = 0;
8
+ function convert(node) {
9
+ const result = {
10
+ id: String(++id),
11
+ date_added: '13300000000000000',
12
+ name: node.name,
13
+ type: node.type,
14
+ };
15
+ if (node.type === 'url') {
16
+ count += 1;
17
+ result.url = node.url;
18
+ } else {
19
+ result.date_modified = result.date_added;
20
+ result.children = (node.children ?? []).map(convert);
21
+ }
22
+ return result;
23
+ }
24
+ const roots = Object.fromEntries(
25
+ ['bookmark_bar', 'other', 'synced'].map((name, index) => [
26
+ name,
27
+ {
28
+ id: String(index + 1),
29
+ name,
30
+ type: 'folder',
31
+ date_added: '13300000000000000',
32
+ date_modified: '13300000000000000',
33
+ children: name === 'other' ? entries.map(convert) : [],
34
+ },
35
+ ])
36
+ );
37
+ await mkdir(targetProfileDir, { recursive: true });
38
+ await writeFile(
39
+ path.join(targetProfileDir, 'Bookmarks'),
40
+ JSON.stringify({ version: 1, roots })
41
+ );
42
+ return count;
43
+ }
44
+
45
+ export async function writeChromiumHistory(targetProfileDir, entries) {
46
+ await mkdir(targetProfileDir, { recursive: true });
47
+ const db = await openSqliteDatabase(path.join(targetProfileDir, 'History'));
48
+ try {
49
+ db.exec(
50
+ await readFile(new URL('./chromium-history.sql', import.meta.url), 'utf8')
51
+ );
52
+ const urls = new Map();
53
+ const insertUrl = db.prepare(
54
+ 'INSERT INTO urls (url,title,visit_count,last_visit_time) VALUES (?,?,0,0)'
55
+ );
56
+ const updateUrl = db.prepare(
57
+ 'UPDATE urls SET title=?, visit_count=visit_count+1,last_visit_time=max(last_visit_time,?) WHERE id=?'
58
+ );
59
+ const insertVisit = db.prepare(
60
+ 'INSERT INTO visits (url,visit_time) VALUES (?,?)'
61
+ );
62
+ db.exec('BEGIN');
63
+ try {
64
+ for (const entry of entries) {
65
+ if (!urls.has(entry.url)) {
66
+ urls.set(
67
+ entry.url,
68
+ insertUrl.run(entry.url, entry.title).lastInsertRowid
69
+ );
70
+ }
71
+ const id = urls.get(entry.url);
72
+ const time = BigInt(entry.time) + 11644473600000000n;
73
+ updateUrl.run(entry.title, time, id);
74
+ insertVisit.run(id, time);
75
+ }
76
+ db.exec('COMMIT');
77
+ } catch (error) {
78
+ db.exec('ROLLBACK');
79
+ throw error;
80
+ }
81
+ } finally {
82
+ db.close();
83
+ }
84
+ return entries.length;
85
+ }
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import { pathExists } from './fs-utils.js';
4
+ import { matchesDomains } from './domains.js';
4
5
 
5
6
  import { readBrowserCookies } from '../browser-cookies.js';
6
7
 
@@ -130,7 +131,9 @@ export async function migrateCookies({
130
131
  );
131
132
  }
132
133
  }
133
- const cookies = [...seen.values()];
134
+ const cookies = [...seen.values()].filter((cookie) =>
135
+ matchesDomains(cookie.domain, domains)
136
+ );
134
137
 
135
138
  const skipped = [];
136
139
  for (const cookie of cookies) {
@@ -0,0 +1,68 @@
1
+ /** Explicit reports for selected classes without a native target writer. */
2
+ export const ADDITIONAL_DATA_CLASSES = Object.freeze([
3
+ 'localStorage',
4
+ 'indexedDB',
5
+ 'sessionStorage',
6
+ 'autofill',
7
+ 'paymentCards',
8
+ 'searchEngines',
9
+ 'siteSettings',
10
+ 'openTabs',
11
+ 'downloads',
12
+ 'readingList',
13
+ 'clientCertificates',
14
+ 'passkeys',
15
+ ]);
16
+
17
+ export function reportAdditionalClasses({
18
+ selected,
19
+ profileDir,
20
+ includePaymentCards,
21
+ }) {
22
+ return ADDITIONAL_DATA_CLASSES.filter((type) => selected.has(type)).flatMap(
23
+ (type) => reportAdditionalClass({ type, profileDir, includePaymentCards })
24
+ );
25
+ }
26
+
27
+ export function reportAdditionalClass({
28
+ type,
29
+ profileDir,
30
+ includePaymentCards,
31
+ }) {
32
+ const entry = (item, reason, detail) => ({ type, item, reason, detail });
33
+ if (type === 'passkeys') {
34
+ return ['iCloud Keychain', 'Google Password Manager', 'Windows Hello'].map(
35
+ (provider) =>
36
+ entry(
37
+ provider,
38
+ 'passkey-not-exportable',
39
+ `${provider} private keys cannot be exported by copying browser profile files. Provider-authorized transfer may be available separately. Sign in once with the platform passkey in a dedicated persistent profile and retain its session.`
40
+ )
41
+ );
42
+ }
43
+ if (type === 'paymentCards' && !includePaymentCards) {
44
+ return [
45
+ entry(
46
+ profileDir,
47
+ 'payment-card-consent-required',
48
+ 'Payment cards require includePaymentCards=true (Python/Rust: include_payment_cards; CLI: --include-payment-cards). Selecting the class alone does not grant consent.'
49
+ ),
50
+ ];
51
+ }
52
+ if (type === 'clientCertificates') {
53
+ return [
54
+ entry(
55
+ profileDir,
56
+ 'client-certificates-os-export-required',
57
+ 'Client certificate private keys require an authorized export from the OS or NSS store. Hardware-backed or non-exportable keys cannot be copied from browser profile files.'
58
+ ),
59
+ ];
60
+ }
61
+ return [
62
+ entry(
63
+ profileDir,
64
+ 'data-class-not-supported',
65
+ `A native source reader and compatible target writer for ${type} are not implemented; no data was copied.`
66
+ ),
67
+ ];
68
+ }
@@ -0,0 +1,32 @@
1
+ /** Site filters match a host or its subdomains, never an unrelated suffix. */
2
+ export function matchesDomains(value, domains) {
3
+ if (!domains?.length) {
4
+ return true;
5
+ }
6
+ let host;
7
+ try {
8
+ host = value.includes('://') ? new URL(value).hostname : value;
9
+ } catch {
10
+ return false;
11
+ }
12
+ host = host.replace(/^\./, '').toLowerCase().replace(/\.$/, '');
13
+ return domains.some((domain) => {
14
+ const filter = domain.replace(/^\./, '').toLowerCase().replace(/\.$/, '');
15
+ return host === filter || host.endsWith(`.${filter}`);
16
+ });
17
+ }
18
+
19
+ export function validateDomains(domains) {
20
+ if (domains === undefined) {
21
+ return;
22
+ }
23
+ if (
24
+ !Array.isArray(domains) ||
25
+ domains.some(
26
+ (domain) =>
27
+ typeof domain !== 'string' || !domain || /[\s/:]/u.test(domain)
28
+ )
29
+ ) {
30
+ throw new TypeError('domains must be an array of host names');
31
+ }
32
+ }
@@ -0,0 +1,106 @@
1
+ import { matchesDomains } from './domains.js';
2
+ import { profileFileIfPresent } from './fs-utils.js';
3
+ import { readDatabaseSnapshot } from './sqlite-snapshot.js';
4
+ import { writeChromiumHistory } from './chromium-writers.js';
5
+ import { preserveIntegerPrecision } from '../browser-cookie-database.js';
6
+
7
+ /** Translate individual Firefox visits using a consistent read-only snapshot. */
8
+ export async function migrateFirefoxHistory({
9
+ profileDir,
10
+ targetProfileDir,
11
+ domains,
12
+ }) {
13
+ const sourcePath = await profileFileIfPresent(profileDir, 'places.sqlite');
14
+ const entry = (reason, detail, item = 'places.sqlite') => ({
15
+ type: 'history',
16
+ item,
17
+ reason,
18
+ detail,
19
+ });
20
+ if (!sourcePath) {
21
+ return { migrated: 0, skipped: [entry('source-missing')], warnings: [] };
22
+ }
23
+ const rows = await readDatabaseSnapshot({
24
+ sourcePath,
25
+ read: (db) => {
26
+ for (const [table, required] of [
27
+ ['moz_places', ['id', 'url', 'title']],
28
+ ['moz_historyvisits', ['id', 'place_id', 'visit_date']],
29
+ ]) {
30
+ const columns = new Set(
31
+ db
32
+ .prepare(`PRAGMA table_info(${table})`)
33
+ .all()
34
+ .map(({ name }) => name)
35
+ );
36
+ if (!required.every((name) => columns.has(name))) {
37
+ return null;
38
+ }
39
+ }
40
+ return preserveIntegerPrecision(
41
+ db.prepare(
42
+ 'SELECT p.url,p.title,v.id,v.visit_date AS time FROM moz_historyvisits v JOIN moz_places p ON v.place_id=p.id ORDER BY v.visit_date,v.id'
43
+ )
44
+ ).all();
45
+ },
46
+ });
47
+ if (rows === null) {
48
+ return {
49
+ migrated: 0,
50
+ skipped: [
51
+ entry(
52
+ 'source-format-unsupported',
53
+ 'Firefox history requires moz_places and moz_historyvisits with URL, title and integer visit dates; no target was written.'
54
+ ),
55
+ ],
56
+ warnings: [],
57
+ };
58
+ }
59
+ const skipped = [];
60
+ const visits = [];
61
+ for (const row of rows) {
62
+ if (typeof row.url !== 'string' || !row.url) {
63
+ skipped.push(
64
+ entry(
65
+ 'invalid-history-url',
66
+ 'The visit URL is not a nonempty string.',
67
+ `places.sqlite visit ${row.id}`
68
+ )
69
+ );
70
+ continue;
71
+ }
72
+ if (!matchesDomains(row.url, domains)) {
73
+ continue;
74
+ }
75
+ if (
76
+ typeof row.time !== 'bigint' ||
77
+ row.time < -11644473600000000n ||
78
+ row.time > 9211727563254775807n
79
+ ) {
80
+ skipped.push(
81
+ entry(
82
+ 'invalid-history-timestamp',
83
+ 'The visit date is not an integer representable in the target history format.',
84
+ `places.sqlite visit ${row.id}`
85
+ )
86
+ );
87
+ continue;
88
+ }
89
+ visits.push({ url: row.url, title: row.title ?? '', time: row.time });
90
+ }
91
+ const migrated = visits.length
92
+ ? await writeChromiumHistory(targetProfileDir, visits)
93
+ : 0;
94
+ return {
95
+ migrated,
96
+ skipped,
97
+ warnings: migrated
98
+ ? [
99
+ entry(
100
+ 'firefox-history-metadata-not-translated',
101
+ 'URL/title and visit dates were translated; Firefox transition types, referring visits and sync metadata were not copied.'
102
+ ),
103
+ ]
104
+ : [],
105
+ };
106
+ }
@@ -1,9 +1,10 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { matchesDomains } from './domains.js';
3
4
  import { profileFileIfPresent } from './fs-utils.js';
4
5
 
5
6
  import { openSqliteDatabase } from '../browser-cookie-database.js';
6
- import { mapFirefoxCookieRows } from '../browser-cookies.js';
7
+ import { mapFirefoxCookieRows, readFirefoxRows } from '../browser-cookies.js';
7
8
  import { readDatabaseSnapshot } from './sqlite-snapshot.js';
8
9
  import { encryptChromiumValue } from './chromium-crypto.js';
9
10
  import { firefoxBookmarksToChrome } from './firefox-bookmarks.js';
@@ -24,8 +25,8 @@ import {
24
25
  * them over CDP.
25
26
  * - **bookmarks:** read from `places.sqlite` (`moz_bookmarks` + `moz_places`)
26
27
  * and converted to Chrome's `Bookmarks` JSON.
27
- * - **history:** counted from `places.sqlite` and reported; it is not written,
28
- * because Chrome's `History` schema is incompatible with Firefox's.
28
+ * - **history:** translated from `moz_historyvisits` into Chrome's `History`,
29
+ * preserving individual visit dates and reporting untranslated metadata.
29
30
  * - **passwords:** decrypted from `logins.json` with the NSS key in `key4.db`
30
31
  * and re-encrypted into a Chrome `Login Data`. When a primary password is set
31
32
  * and not supplied, they are reported as `primary-password-set`.
@@ -102,20 +103,11 @@ export async function readFirefoxCookies({ profileDir, domains }) {
102
103
  return readDatabaseSnapshot({
103
104
  sourcePath: cookiePath,
104
105
  read: (db) => {
105
- const rows = db
106
- .prepare(
107
- `SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite
108
- FROM moz_cookies ORDER BY host, name, path`
109
- )
110
- .all();
106
+ const rows = readFirefoxRows(db);
111
107
  // The installed-browser cookie reader owns the row → cookie mapping, so
112
108
  // a migrated cookie and an imported one always have the same shape.
113
109
  return mapFirefoxCookieRows(
114
- rows.filter((row) =>
115
- Array.isArray(domains) && domains.length > 0
116
- ? domains.some((domain) => (row.host ?? '').includes(domain))
117
- : true
118
- )
110
+ rows.filter((row) => matchesDomains(row.host ?? '', domains))
119
111
  );
120
112
  },
121
113
  });
@@ -228,6 +220,7 @@ export async function migrateFirefoxPasswords({
228
220
  targetKey,
229
221
  targetPrefix = platform === 'win32' ? 'v10' : 'v11',
230
222
  primaryPassword = Buffer.alloc(0),
223
+ domains,
231
224
  }) {
232
225
  const loginsPath = await profileFileIfPresent(profileDir, 'logins.json');
233
226
  const key4Path = await profileFileIfPresent(profileDir, 'key4.db');
@@ -271,6 +264,9 @@ export async function migrateFirefoxPasswords({
271
264
  const decrypted = [];
272
265
  const skipped = [];
273
266
  for (const login of logins) {
267
+ if (!matchesDomains(login.hostname ?? '', domains)) {
268
+ continue;
269
+ }
274
270
  try {
275
271
  decrypted.push({
276
272
  origin: login.hostname,
@@ -287,6 +283,33 @@ export async function migrateFirefoxPasswords({
287
283
  }
288
284
  }
289
285
 
286
+ const migrated = await writeChromiumPasswords({
287
+ entries: decrypted,
288
+ targetProfileDir,
289
+ platform,
290
+ targetKey,
291
+ targetPrefix,
292
+ });
293
+
294
+ const warnings = [];
295
+ if (migrated > 0) {
296
+ warnings.push({
297
+ type: 'passwords',
298
+ item: 'Login Data',
299
+ reason: 'reencrypted-for-chrome',
300
+ detail: `${migrated} Firefox logins were decrypted and re-encrypted into a Chrome Login Data; Chrome may re-key the store on first launch.`,
301
+ });
302
+ }
303
+ return { migrated, skipped, warnings };
304
+ }
305
+
306
+ export async function writeChromiumPasswords({
307
+ entries,
308
+ targetProfileDir,
309
+ platform,
310
+ targetKey,
311
+ targetPrefix,
312
+ }) {
290
313
  await mkdir(targetProfileDir, { recursive: true });
291
314
  const targetPath = path.join(targetProfileDir, 'Login Data');
292
315
  const db = await openSqliteDatabase(targetPath);
@@ -301,7 +324,7 @@ export async function migrateFirefoxPasswords({
301
324
  date_last_used, date_password_modified
302
325
  ) VALUES (?, ?, '', ?, '', ?, '', ?, 0, 0, 0, 0, 0, 0, 0)`
303
326
  );
304
- for (const entry of decrypted) {
327
+ for (const entry of entries) {
305
328
  const encrypted = encryptChromiumValue({
306
329
  plaintext: entry.password,
307
330
  key: targetKey,
@@ -321,14 +344,5 @@ export async function migrateFirefoxPasswords({
321
344
  db.close();
322
345
  }
323
346
 
324
- const warnings = [];
325
- if (migrated > 0) {
326
- warnings.push({
327
- type: 'passwords',
328
- item: 'Login Data',
329
- reason: 'reencrypted-for-chrome',
330
- detail: `${migrated} Firefox logins were decrypted and re-encrypted into a Chrome Login Data; Chrome may re-key the store on first launch.`,
331
- });
332
- }
333
- return { migrated, skipped, warnings };
347
+ return migrated;
334
348
  }