browser-commander 0.21.2 → 0.23.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.
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
 
4
+ import { resolveImportSource } from '../browser-cookies.js';
5
+ import { browserFamily, isSingleProfileBrowser } from '../browser-sources.js';
4
6
  import {
5
7
  browserProfileRoot,
6
- normalizeCookieBrowser,
7
8
  resolveBrowserProfile,
8
9
  } from '../browser-profiles.js';
9
10
  import { migrateBookmarks } from './bookmarks.js';
@@ -48,7 +49,17 @@ export const ALL_DATA_CLASSES = Object.freeze([
48
49
  'extensions',
49
50
  ]);
50
51
 
51
- const CHROMIUM_BROWSERS = new Set(['chrome', 'chromium', 'brave', 'edge']);
52
+ // Classification is driven by the shared catalogue so every Chromium variant
53
+ // (vivaldi, opera, arc, the chrome/edge channels, …) and every Firefox fork
54
+ // (librewolf, waterfox, zen, floorp, the firefox channels, …) is handled the
55
+ // same way as its canonical engine, not just the original four browsers.
56
+ function isChromiumBrowser(browser) {
57
+ return browserFamily(browser) === 'chromium';
58
+ }
59
+
60
+ function isFirefoxBrowser(browser) {
61
+ return browserFamily(browser) === 'firefox';
62
+ }
52
63
 
53
64
  function emptyMigrated() {
54
65
  return {
@@ -69,21 +80,23 @@ async function resolveSourceProfileDir({
69
80
  homeDir,
70
81
  environment,
71
82
  }) {
83
+ // A Chromium profile lives in a named subdirectory of the user data dir,
84
+ // except in single-profile browsers (Opera) that keep it in the root; for
85
+ // Firefox the userDataDir already points at the profile.
86
+ const nestsProfiles =
87
+ isChromiumBrowser(browser) && !isSingleProfileBrowser(browser);
72
88
  if (userDataDir) {
73
- // For Chromium a profile lives in a named subdirectory; for Firefox the
74
- // userDataDir already points at the profile.
75
- if (CHROMIUM_BROWSERS.has(browser)) {
76
- return path.join(userDataDir, profile ?? 'Default');
77
- }
78
- return userDataDir;
89
+ return nestsProfiles
90
+ ? path.join(userDataDir, profile ?? 'Default')
91
+ : userDataDir;
79
92
  }
80
- if (CHROMIUM_BROWSERS.has(browser)) {
93
+ if (isChromiumBrowser(browser)) {
81
94
  const root = browserProfileRoot(browser, {
82
95
  platform,
83
96
  homeDir,
84
97
  environment,
85
98
  });
86
- return path.join(root, profile ?? 'Default');
99
+ return nestsProfiles ? path.join(root, profile ?? 'Default') : root;
87
100
  }
88
101
  const resolved = await resolveBrowserProfile({
89
102
  browser,
@@ -101,6 +114,34 @@ function mergeReport(report, className, fragment) {
101
114
  report.warnings.push(...(fragment.warnings ?? []));
102
115
  }
103
116
 
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
+
104
145
  async function resolvePasswordKeys({
105
146
  keys,
106
147
  browser,
@@ -120,7 +161,7 @@ async function resolvePasswordKeys({
120
161
  platform,
121
162
  environment,
122
163
  });
123
- const resolveSourceKey = CHROMIUM_BROWSERS.has(browser)
164
+ const resolveSourceKey = isChromiumBrowser(browser)
124
165
  ? createSourceKeyResolver({
125
166
  browser,
126
167
  platform,
@@ -156,6 +197,7 @@ export async function migrateProfile({
156
197
  keys,
157
198
  homeDir = os.homedir(),
158
199
  environment = process.env,
200
+ runCommand,
159
201
  }) {
160
202
  if (!from?.browser) {
161
203
  throw new TypeError('migrateProfile requires from.browser');
@@ -163,9 +205,19 @@ export async function migrateProfile({
163
205
  if (!to) {
164
206
  throw new TypeError('migrateProfile requires a target directory (to)');
165
207
  }
166
- const browser = normalizeCookieBrowser(from.browser);
167
- const profile = from.profile ?? 'Default';
168
- const isFirefox = browser === 'firefox';
208
+ // An explicit userDataDir names the source, so only an installed-browser
209
+ // import is steered towards the profile holding the requested domains.
210
+ const source = await resolveImportSource({
211
+ browser: from.browser,
212
+ domains: from.userDataDir ? undefined : domains,
213
+ platform,
214
+ homeDir,
215
+ environment,
216
+ runCommand,
217
+ });
218
+ const { browser } = source;
219
+ const profile = from.profile ?? source.profile ?? 'Default';
220
+ const isFirefox = isFirefoxBrowser(browser);
169
221
  const sourceProfileDir = await resolveSourceProfileDir({
170
222
  browser,
171
223
  profile,
@@ -183,6 +235,9 @@ export async function migrateProfile({
183
235
  warnings: [],
184
236
  cookies: [],
185
237
  };
238
+ if (source.warning) {
239
+ report.warnings.push(source.warning);
240
+ }
186
241
 
187
242
  if (selected.has('cookies')) {
188
243
  if (isFirefox) {
@@ -207,6 +262,11 @@ export async function migrateProfile({
207
262
  }
208
263
  }
209
264
 
265
+ if (browserFamily(browser) === 'safari') {
266
+ reportSafariClasses(report, selected);
267
+ return report;
268
+ }
269
+
210
270
  if (selected.has('bookmarks')) {
211
271
  mergeReport(
212
272
  report,
@@ -0,0 +1,173 @@
1
+ /** Read Safari's unencrypted, mixed-endian Cookies.binarycookies store.
2
+ * Format: https://github.com/libyal/dtformats/blob/main/documentation/Safari%20Cookies.asciidoc
3
+ * No SameSite attribute is stored in this format; use Lax and report that loss.
4
+ */
5
+ import { readFile as readFileBytes, stat } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { TextDecoder } from 'node:util';
8
+
9
+ const APPLE_EPOCH = 978_307_200;
10
+ const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
11
+
12
+ function invalid(detail) {
13
+ throw new Error(`Invalid Safari binarycookies: ${detail}`);
14
+ }
15
+
16
+ function bounded(data, offset, size) {
17
+ if (offset < 0 || size < 0 || offset + size > data.length) {
18
+ invalid('truncated or out-of-range data');
19
+ }
20
+ return data.subarray(offset, offset + size);
21
+ }
22
+
23
+ function uint(data, offset, bigEndian = false) {
24
+ const bytes = bounded(data, offset, 4);
25
+ return bigEndian ? bytes.readUInt32BE() : bytes.readUInt32LE();
26
+ }
27
+
28
+ function string(record, field) {
29
+ const offset = uint(record, field);
30
+ if (offset < 56 || offset >= record.length) {
31
+ invalid('string offset');
32
+ }
33
+ const end = record.indexOf(0, offset);
34
+ if (end < 0) {
35
+ invalid('unterminated string');
36
+ }
37
+ try {
38
+ return utf8.decode(record.subarray(offset, end));
39
+ } catch {
40
+ return invalid('invalid UTF-8');
41
+ }
42
+ }
43
+
44
+ /** Validate all structural offsets before walking records; counts are bounded
45
+ * by the file size, so forged counts cannot cause unbounded allocations.
46
+ */
47
+ function* records(data) {
48
+ if (bounded(data, 0, 4).toString('ascii') !== 'cook') {
49
+ invalid('file signature');
50
+ }
51
+ const pages = uint(data, 4, true);
52
+ bounded(data, 8, pages * 4);
53
+ let pageStart = 8 + pages * 4;
54
+ for (let index = 0; index < pages; index += 1) {
55
+ const page = bounded(data, pageStart, uint(data, 8 + index * 4, true));
56
+ pageStart += page.length;
57
+ if (uint(page, 0) !== 0x00010000) {
58
+ invalid('page signature');
59
+ }
60
+ const count = uint(page, 4);
61
+ const headerSize = 8 + count * 4;
62
+ bounded(page, 0, headerSize);
63
+ let previousEnd = headerSize;
64
+ for (let cookie = 0; cookie < count; cookie += 1) {
65
+ const start = uint(page, 8 + cookie * 4);
66
+ if (start < previousEnd) {
67
+ invalid('overlapping record');
68
+ }
69
+ const size = uint(page, start);
70
+ if (size < 56) {
71
+ invalid('short record');
72
+ }
73
+ const record = bounded(page, start, size);
74
+ previousEnd = start + size;
75
+ yield record;
76
+ }
77
+ }
78
+ // Safari may append a checksum and a plist trailer. They are not cookies.
79
+ }
80
+
81
+ /** Decode cookies in automation shape, using the existing substring filter. */
82
+ export function parseSafariCookies(data, domainFilter) {
83
+ const cookies = [];
84
+ for (const record of records(data)) {
85
+ const domain = string(record, 16);
86
+ if (
87
+ domainFilter &&
88
+ !domain.toLowerCase().includes(domainFilter.toLowerCase())
89
+ ) {
90
+ continue;
91
+ }
92
+ const expiry = record.readDoubleLE(40) + APPLE_EPOCH;
93
+ if (
94
+ !Number.isFinite(expiry) ||
95
+ Math.abs(expiry) > Number.MAX_SAFE_INTEGER
96
+ ) {
97
+ invalid('expiry');
98
+ }
99
+ const flags = uint(record, 8);
100
+ cookies.push({
101
+ name: string(record, 20),
102
+ value: string(record, 28),
103
+ domain,
104
+ path: string(record, 24) || '/',
105
+ expires: Math.floor(expiry),
106
+ httpOnly: Boolean(flags & 4),
107
+ secure: Boolean(flags & 1),
108
+ sameSite: 'Lax',
109
+ });
110
+ }
111
+ return cookies;
112
+ }
113
+
114
+ /** Counts decode only domain strings. Names and values are never decoded. */
115
+ export function countSafariCookies(data, domains) {
116
+ const byDomain = domains?.length
117
+ ? Object.fromEntries(domains.map((domain) => [domain, 0]))
118
+ : null;
119
+ let total = 0;
120
+ for (const record of records(data)) {
121
+ const host = string(record, 16).toLowerCase();
122
+ total += 1;
123
+ for (const domain of Object.keys(byDomain ?? {})) {
124
+ if (host.includes(domain.toLowerCase())) {
125
+ byDomain[domain] += 1;
126
+ }
127
+ }
128
+ }
129
+ return { total, byDomain };
130
+ }
131
+
132
+ /** A protected path is still a source: preserve it for an actionable read error. */
133
+ export async function findSafariCookieFile(profileDir, statFile = stat) {
134
+ for (const candidate of [
135
+ path.join(profileDir, 'Cookies', 'Cookies.binarycookies'),
136
+ path.join(profileDir, 'Cookies.binarycookies'),
137
+ ]) {
138
+ try {
139
+ if ((await statFile(candidate)).isFile()) {
140
+ return candidate;
141
+ }
142
+ } catch (error) {
143
+ if (['EPERM', 'EACCES'].includes(error.code)) {
144
+ return candidate;
145
+ }
146
+ if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
147
+ throw error;
148
+ }
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+
154
+ export async function readSafariCookieFile(
155
+ filePath,
156
+ { environment = process.env, readFile = readFileBytes } = {}
157
+ ) {
158
+ try {
159
+ return await readFile(filePath);
160
+ } catch (error) {
161
+ if (!['EPERM', 'EACCES'].includes(error.code)) {
162
+ throw error;
163
+ }
164
+ const app =
165
+ environment.__CFBundleIdentifier ||
166
+ environment.TERM_PROGRAM ||
167
+ process.execPath;
168
+ throw new Error(
169
+ `Safari cookie access denied (${error.code}) at ${filePath}. Grant Full Disk Access to ${app}, the app running Browser Commander, then retry: x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles`,
170
+ { cause: error }
171
+ );
172
+ }
173
+ }
package/src/cli/args.js CHANGED
@@ -78,11 +78,17 @@ export const COMMANDS = Object.freeze({
78
78
  domain: STRINGS,
79
79
  },
80
80
  },
81
+ 'cookies sources': {
82
+ positionals: [],
83
+ options: { domain: STRINGS },
84
+ },
81
85
  'profile migrate': {
82
86
  positionals: [],
83
87
  options: {
84
88
  from: STRING,
85
89
  profile: STRING,
90
+ 'user-data-dir': STRING,
91
+ 'target-browser': STRING,
86
92
  to: STRING,
87
93
  include: STRING,
88
94
  domain: STRINGS,
@@ -92,6 +98,10 @@ export const COMMANDS = Object.freeze({
92
98
  positionals: [],
93
99
  options: { from: STRING, profile: STRING, to: STRING },
94
100
  },
101
+ 'profile sources': {
102
+ positionals: [],
103
+ options: { browser: STRING },
104
+ },
95
105
  attach: {
96
106
  positionals: [],
97
107
  options: { mode: STRING, port: STRING, timeout: STRING },
@@ -210,6 +210,8 @@ async function profileMigrate(parsed, io) {
210
210
  await dispatchOnce(io, 'profile.migrate', {
211
211
  from: requireOption(options, 'from', 'profile migrate'),
212
212
  profile: options.profile,
213
+ userDataDir: options.userDataDir,
214
+ targetBrowser: options.targetBrowser,
213
215
  to: options.to,
214
216
  include: options.include
215
217
  ?.split(',')
@@ -220,6 +222,29 @@ async function profileMigrate(parsed, io) {
220
222
  );
221
223
  }
222
224
 
225
+ /**
226
+ * List the installed browser profiles that hold cookies (names and counts
227
+ * only, never values), optionally filtered and counted by `--domain`.
228
+ */
229
+ async function cookieSources(parsed, io) {
230
+ const { options } = parsed;
231
+ return done(
232
+ await dispatchOnce(io, 'cookies.sources', {
233
+ domains: options.domain,
234
+ })
235
+ );
236
+ }
237
+
238
+ /** List installed browser profiles, optionally for one `--browser`. */
239
+ async function profileSources(parsed, io) {
240
+ const { options } = parsed;
241
+ return done(
242
+ await dispatchOnce(io, 'profile.sources', {
243
+ browser: options.browser,
244
+ })
245
+ );
246
+ }
247
+
223
248
  /** Copy a real profile into `--to` (or a new temporary directory). */
224
249
  async function profileSnapshot(parsed, io) {
225
250
  const { options } = parsed;
@@ -323,8 +348,10 @@ export const COMMAND_HANDLERS = Object.freeze({
323
348
  profile: options.profile,
324
349
  domains: options.domain,
325
350
  })),
351
+ 'cookies sources': cookieSources,
326
352
  'profile migrate': profileMigrate,
327
353
  'profile snapshot': profileSnapshot,
354
+ 'profile sources': profileSources,
328
355
  attach,
329
356
  doctor,
330
357
  run,
@@ -191,12 +191,18 @@ async function importCookies(state, params) {
191
191
  }
192
192
 
193
193
  function migrateProfile(state, params) {
194
+ // The CLI passes the source browser flat (`--from`, `--profile`,
195
+ // `--user-data-dir`); migrateProfile expects them grouped under `from`.
194
196
  return state.dependencies.migrateProfile({
195
- from: params.from,
196
- profile: params.profile,
197
+ from: {
198
+ browser: requireString(params, 'from'),
199
+ profile: params.profile,
200
+ userDataDir: params.userDataDir,
201
+ },
197
202
  to: params.to,
198
203
  include: params.include,
199
204
  domains: params.domains,
205
+ targetBrowser: params.targetBrowser,
200
206
  });
201
207
  }
202
208
 
@@ -209,6 +215,24 @@ function snapshotProfile(state, params) {
209
215
  });
210
216
  }
211
217
 
218
+ /** List browsers/profiles holding cookies, with optional per-domain counts. */
219
+ async function cookieSources(state, params) {
220
+ const domains = params.domains;
221
+ if (domains !== undefined && !Array.isArray(domains)) {
222
+ throw invalidParams('domains must be an array of strings');
223
+ }
224
+ return { sources: await state.dependencies.listCookieSources({ domains }) };
225
+ }
226
+
227
+ /** List the installed browser profiles (never cookie values). */
228
+ async function profileSources(state, params) {
229
+ return {
230
+ profiles: await state.dependencies.listBrowserProfiles({
231
+ browser: params.browser,
232
+ }),
233
+ };
234
+ }
235
+
212
236
  function relayOf(state) {
213
237
  if (!state.relay) {
214
238
  throw invalidParams('No extension is attached; call attach first');
@@ -298,8 +322,10 @@ export const HIGH_LEVEL_METHODS = Object.freeze({
298
322
  'trace.start': traceStart,
299
323
  'trace.stop': traceStop,
300
324
  'cookies.import': importCookies,
325
+ 'cookies.sources': cookieSources,
301
326
  'profile.migrate': migrateProfile,
302
327
  'profile.snapshot': snapshotProfile,
328
+ 'profile.sources': profileSources,
303
329
  attach,
304
330
  'attach.tabs': attachTabs,
305
331
  'attach.send': attachSend,
@@ -64,6 +64,12 @@ export const DEFAULT_DEPENDENCIES = Object.freeze({
64
64
  writeTraceViewer,
65
65
  readBrowserCookies: async (options) =>
66
66
  (await import('../browser/browser-cookies.js')).readBrowserCookies(options),
67
+ listCookieSources: async (options) =>
68
+ (await import('../browser/browser-cookies.js')).listCookieSources(options),
69
+ listBrowserProfiles: async (options) =>
70
+ (await import('../browser/browser-profiles.js')).listBrowserProfiles(
71
+ options
72
+ ),
67
73
  loadEngine: (name) => importOptional(name, `The ${name} engine`),
68
74
  openInUserBrowser: async (url) =>
69
75
  (
package/src/exports.js CHANGED
@@ -60,8 +60,12 @@ export {
60
60
  export { connectBrowser } from './browser/connector.js';
61
61
  export {
62
62
  listBrowserProfiles,
63
+ listCookieSources,
63
64
  readBrowserCookies,
65
+ resolveImportSource,
64
66
  } from './browser/browser-cookies.js';
67
+ export { resolveDefaultBrowser } from './browser/default-browser.js';
68
+ export { BROWSER_IDS, findBrowserSource } from './browser/browser-sources.js';
65
69
  export {
66
70
  launchAndConnectRealBrowser,
67
71
  launchRealBrowser,
@@ -158,6 +158,27 @@ export function createFakeDependencies() {
158
158
  { name: 'sid', value: '1', domain: `.${domainFilter}`, path: '/' },
159
159
  { name: 'bad', value: '2', domain: `.${domainFilter}`, path: '/' },
160
160
  ],
161
+ listCookieSources: async ({ domains } = {}) => [
162
+ {
163
+ browser: 'chrome',
164
+ profile: 'Default',
165
+ path: '/home/user/.config/google-chrome/Default',
166
+ isDefault: true,
167
+ cookies: 7,
168
+ byDomain: domains
169
+ ? Object.fromEntries(domains.map((d) => [d, 1]))
170
+ : null,
171
+ },
172
+ ],
173
+ listBrowserProfiles: async ({ browser } = {}) => [
174
+ {
175
+ browser: browser ?? 'chrome',
176
+ name: 'Default',
177
+ displayName: 'Default',
178
+ path: `/home/user/.config/${browser ?? 'google-chrome'}/Default`,
179
+ isDefault: true,
180
+ },
181
+ ],
161
182
  loadEngine: async (name) => ({ default: new FakeEngine(name) }),
162
183
  openInUserBrowser: async (url) => ({ opened: url, command: ['open', url] }),
163
184
  migrateProfile: async (options) => ({ migrated: {}, options }),
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import assert from 'node:assert';
11
- import { readFile, stat, writeFile } from 'node:fs/promises';
11
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
12
12
  import path from 'node:path';
13
13
 
14
14
  import BetterSqlite3 from 'better-sqlite3';
@@ -148,6 +148,35 @@ export async function writeFirefoxCookies(profileDir, rows) {
148
148
  return cookiePath;
149
149
  }
150
150
 
151
+ /**
152
+ * Write a Linux Firefox-family install under `home` with one default profile,
153
+ * listed in `profiles.ini`, whose `cookies.sqlite` holds `rows`.
154
+ *
155
+ * @param {string} home - Home directory the install lives under
156
+ * @param {Array<Object>} rows - Cookie rows, as for `writeFirefoxCookies`
157
+ * @param {Object} [options]
158
+ * @param {string} [options.root='.mozilla/firefox'] - Install root, relative
159
+ * to `home` (a fork such as LibreWolf uses its own)
160
+ * @param {string} [options.name='default-release'] - Profile name
161
+ * @returns {Promise<string>} The profile directory
162
+ */
163
+ export async function writeFirefoxProfile(
164
+ home,
165
+ rows,
166
+ { root = '.mozilla/firefox', name = 'default-release' } = {}
167
+ ) {
168
+ const rootPath = path.join(home, ...root.split('/'));
169
+ const profileName = `xyz.${name}`;
170
+ const profilePath = path.join(rootPath, profileName);
171
+ await mkdir(profilePath, { recursive: true });
172
+ await writeFile(
173
+ path.join(rootPath, 'profiles.ini'),
174
+ `[Profile0]\nName=${name}\nIsRelative=1\nPath=${profileName}\nDefault=1\n`
175
+ );
176
+ await writeFirefoxCookies(profilePath, rows);
177
+ return profilePath;
178
+ }
179
+
151
180
  /**
152
181
  * Write a Firefox `places.sqlite` with a toolbar bookmark and, unless
153
182
  * `withMenuBookmark` is false, a bookmarks-menu one. Each bookmark has its own
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { describe, it } from 'node:test';
5
+
6
+ import { readBrowserCookiesWithDependencies } from '../../../src/browser/browser-cookies.js';
7
+ import { writeFirefoxCookies } from '../../helpers/migration-fixtures.js';
8
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
9
+
10
+ const makeDirectory = useTempDirectories();
11
+
12
+ describe('readBrowserCookies honours an explicit profileDir', () => {
13
+ it('reads cookies from the given directory, not the default profile root', async () => {
14
+ const temporaryDirectory = await makeDirectory('bc-profiledir-');
15
+ // A custom userDataDir that is NOT under the default profile root.
16
+ const customProfile = path.join(temporaryDirectory, 'custom', 'profile');
17
+ await mkdir(customProfile, { recursive: true });
18
+ await writeFirefoxCookies(customProfile, [
19
+ { name: 'sid', value: 'abc', host: '.example.com' },
20
+ ]);
21
+
22
+ // homeDir points somewhere with no browser data, so a reader that ignored
23
+ // profileDir and re-resolved the default profile would find nothing.
24
+ const emptyHome = path.join(temporaryDirectory, 'empty-home');
25
+ await mkdir(emptyHome, { recursive: true });
26
+
27
+ const cookies = await readBrowserCookiesWithDependencies(
28
+ { browser: 'firefox', profileDir: customProfile },
29
+ { platform: 'linux', homeDir: emptyHome }
30
+ );
31
+
32
+ assert.equal(cookies.length, 1);
33
+ assert.equal(cookies[0].name, 'sid');
34
+ assert.equal(cookies[0].domain, '.example.com');
35
+ });
36
+ });
@@ -37,7 +37,7 @@ import {
37
37
  listBrowserProfiles as publicListBrowserProfiles,
38
38
  readBrowserCookies as publicReadBrowserCookies,
39
39
  } from '../../../src/index.js';
40
- import { writeFirefoxCookies } from '../../helpers/migration-fixtures.js';
40
+ import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
41
41
 
42
42
  const CHROME_EPOCH_OFFSET_SECONDS = 11_644_473_600;
43
43
  const execFile = promisify(execFileCallback);
@@ -134,16 +134,7 @@ async function createChromiumProfile({ homeDir, rows, profile = 'Default' }) {
134
134
  }
135
135
 
136
136
  async function createFirefoxProfile({ homeDir, rows }) {
137
- const root = path.join(homeDir, '.mozilla', 'firefox');
138
- const profileName = 'fixture.default-release';
139
- const profilePath = path.join(root, profileName);
140
- await mkdir(profilePath, { recursive: true });
141
- await writeFile(
142
- path.join(root, 'profiles.ini'),
143
- `[Profile0]\nName=default-release\nIsRelative=1\nPath=${profileName}\nDefault=1\n`
144
- );
145
- const cookiePath = await writeFirefoxCookies(profilePath, rows);
146
- return { cookiePath, profilePath, root };
137
+ return { profilePath: await writeFirefoxProfile(homeDir, rows) };
147
138
  }
148
139
 
149
140
  describe('installed browser cookie import', () => {