browser-commander 0.21.2 → 0.22.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.
- package/.jscpd-baseline.json +5 -6
- package/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/src/browser/browser-cookie-credentials.js +15 -24
- package/src/browser/browser-cookies.js +219 -11
- package/src/browser/browser-profiles.js +145 -79
- package/src/browser/browser-sources.js +184 -0
- package/src/browser/browser-sources.json +399 -0
- package/src/browser/default-browser.js +176 -0
- package/src/browser/migration/cookies.js +3 -0
- package/src/browser/migration/index.js +41 -14
- package/src/cli/args.js +10 -0
- package/src/cli/commands.js +27 -0
- package/src/cli/high-level-methods.js +28 -2
- package/src/cli/modules.js +6 -0
- package/src/exports.js +4 -0
- package/tests/helpers/cli-fakes.js +21 -0
- package/tests/helpers/migration-fixtures.js +30 -1
- package/tests/unit/browser/browser-cookies-profiledir.test.js +36 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -11
- package/tests/unit/browser/browser-profiles-sources.test.js +150 -0
- package/tests/unit/browser/browser-sources.test.js +154 -0
- package/tests/unit/browser/cookie-sources.test.js +146 -0
- package/tests/unit/browser/default-browser.test.js +121 -0
- package/tests/unit/browser/migration/index.test.js +62 -1
- package/tests/unit/cli/main.test.js +32 -5
- package/tests/unit/scripts/browser-support.test.js +56 -0
|
@@ -2,28 +2,74 @@ import { access, readdir, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import {
|
|
6
|
+
BROWSER_IDS,
|
|
7
|
+
browserFamily,
|
|
8
|
+
isSingleProfileBrowser,
|
|
9
|
+
normalizeBrowserId,
|
|
10
|
+
resolveBrowserRoots,
|
|
11
|
+
} from './browser-sources.js';
|
|
12
|
+
import { resolveDefaultBrowser } from './default-browser.js';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Keywords that select the operating-system default browser instead of a named
|
|
16
|
+
* one, so `browser: 'default'` (or `'auto'`) imports from whatever a person
|
|
17
|
+
* actually uses. Importing stays opt-in: callers pass this explicitly.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_BROWSER_KEYWORDS = new Set(['default', 'auto']);
|
|
20
|
+
|
|
21
|
+
/** Whether `browser` asks for the system default rather than a named browser. */
|
|
22
|
+
export function isDefaultBrowserKeyword(browser) {
|
|
23
|
+
return (
|
|
24
|
+
typeof browser === 'string' &&
|
|
25
|
+
DEFAULT_BROWSER_KEYWORDS.has(browser.trim().toLowerCase())
|
|
26
|
+
);
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a requested browser to a canonical catalogue id, expanding the
|
|
31
|
+
* `default`/`auto` keywords to the system default browser. Named browsers are
|
|
32
|
+
* normalized through the catalogue as before.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveSourceBrowser(
|
|
35
|
+
browser,
|
|
36
|
+
{ platform = process.platform, environment = process.env, runCommand } = {}
|
|
37
|
+
) {
|
|
38
|
+
if (!isDefaultBrowserKeyword(browser)) {
|
|
39
|
+
return normalizeCookieBrowser(browser);
|
|
40
|
+
}
|
|
41
|
+
const resolved = await resolveDefaultBrowser({
|
|
42
|
+
platform,
|
|
43
|
+
environment,
|
|
44
|
+
runCommand,
|
|
45
|
+
});
|
|
46
|
+
if (!resolved) {
|
|
20
47
|
throw new Error(
|
|
21
|
-
|
|
48
|
+
'Could not determine the system default browser; pass an explicit browser instead of "default".'
|
|
22
49
|
);
|
|
23
50
|
}
|
|
24
|
-
return
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every browser profile discovery can read from. Driven by the shared
|
|
56
|
+
* `browser-sources.json` catalogue, so adding a browser there adds it here.
|
|
57
|
+
*/
|
|
58
|
+
export const SUPPORTED_COOKIE_BROWSERS = BROWSER_IDS;
|
|
59
|
+
|
|
60
|
+
function platformPath(platform) {
|
|
61
|
+
return platform === 'win32' ? path.win32 : path;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolve a browser name (id or alias) to its canonical id. */
|
|
65
|
+
export function normalizeCookieBrowser(browser) {
|
|
66
|
+
return normalizeBrowserId(browser);
|
|
25
67
|
}
|
|
26
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The primary profile root a browser uses on a platform, or `undefined` when
|
|
71
|
+
* the browser does not run there.
|
|
72
|
+
*/
|
|
27
73
|
export function browserProfileRoot(
|
|
28
74
|
browser,
|
|
29
75
|
{
|
|
@@ -32,46 +78,12 @@ export function browserProfileRoot(
|
|
|
32
78
|
environment = process.env,
|
|
33
79
|
} = {}
|
|
34
80
|
) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
chrome: pathApi.join(support, 'Google', 'Chrome'),
|
|
42
|
-
chromium: pathApi.join(support, 'Chromium'),
|
|
43
|
-
edge: pathApi.join(support, 'Microsoft Edge'),
|
|
44
|
-
firefox: pathApi.join(support, 'Firefox'),
|
|
45
|
-
};
|
|
46
|
-
return roots[normalizedBrowser];
|
|
47
|
-
}
|
|
48
|
-
if (platform === 'win32') {
|
|
49
|
-
const localAppData =
|
|
50
|
-
environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
|
|
51
|
-
const roamingAppData =
|
|
52
|
-
environment.APPDATA ?? pathApi.join(homeDir, 'AppData', 'Roaming');
|
|
53
|
-
const roots = {
|
|
54
|
-
brave: pathApi.join(
|
|
55
|
-
localAppData,
|
|
56
|
-
'BraveSoftware',
|
|
57
|
-
'Brave-Browser',
|
|
58
|
-
'User Data'
|
|
59
|
-
),
|
|
60
|
-
chrome: pathApi.join(localAppData, 'Google', 'Chrome', 'User Data'),
|
|
61
|
-
chromium: pathApi.join(localAppData, 'Chromium', 'User Data'),
|
|
62
|
-
edge: pathApi.join(localAppData, 'Microsoft', 'Edge', 'User Data'),
|
|
63
|
-
firefox: pathApi.join(roamingAppData, 'Mozilla', 'Firefox'),
|
|
64
|
-
};
|
|
65
|
-
return roots[normalizedBrowser];
|
|
66
|
-
}
|
|
67
|
-
const roots = {
|
|
68
|
-
brave: pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser'),
|
|
69
|
-
chrome: pathApi.join(homeDir, '.config', 'google-chrome'),
|
|
70
|
-
chromium: pathApi.join(homeDir, '.config', 'chromium'),
|
|
71
|
-
edge: pathApi.join(homeDir, '.config', 'microsoft-edge'),
|
|
72
|
-
firefox: pathApi.join(homeDir, '.mozilla', 'firefox'),
|
|
73
|
-
};
|
|
74
|
-
return roots[normalizedBrowser];
|
|
81
|
+
const [root] = resolveBrowserRoots(browser, {
|
|
82
|
+
platform,
|
|
83
|
+
homeDir,
|
|
84
|
+
environment,
|
|
85
|
+
});
|
|
86
|
+
return root;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
async function pathExists(filePath) {
|
|
@@ -100,7 +112,7 @@ function chromiumCookiePaths(profilePath, pathApi = path) {
|
|
|
100
112
|
|
|
101
113
|
export async function findCookieDatabase(browser, profilePath, platform) {
|
|
102
114
|
const pathApi = platformPath(platform ?? process.platform);
|
|
103
|
-
if (
|
|
115
|
+
if (browserFamily(browser) === 'firefox') {
|
|
104
116
|
const candidate = pathApi.join(profilePath, 'cookies.sqlite');
|
|
105
117
|
return (await pathExists(candidate)) ? candidate : null;
|
|
106
118
|
}
|
|
@@ -112,11 +124,38 @@ export async function findCookieDatabase(browser, profilePath, platform) {
|
|
|
112
124
|
return null;
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
/** Default profile first, then by name. */
|
|
128
|
+
function sortProfiles(profiles) {
|
|
129
|
+
return profiles.sort(
|
|
130
|
+
(left, right) =>
|
|
131
|
+
Number(right.isDefault) - Number(left.isDefault) ||
|
|
132
|
+
left.name.localeCompare(right.name)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
async function listChromiumProfiles(browser, root, platform) {
|
|
116
137
|
if (!(await pathExists(root))) {
|
|
117
138
|
return [];
|
|
118
139
|
}
|
|
119
140
|
const pathApi = platformPath(platform);
|
|
141
|
+
|
|
142
|
+
// Opera-style browsers keep one profile in the root itself rather than in
|
|
143
|
+
// Default/Profile N subdirectories.
|
|
144
|
+
if (isSingleProfileBrowser(browser)) {
|
|
145
|
+
if (!(await findCookieDatabase(browser, root, platform))) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
browser,
|
|
151
|
+
name: 'Default',
|
|
152
|
+
displayName: 'Default',
|
|
153
|
+
path: root,
|
|
154
|
+
isDefault: true,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
120
159
|
const localState = await readJson(pathApi.join(root, 'Local State'));
|
|
121
160
|
const infoCache = localState.profile?.info_cache ?? {};
|
|
122
161
|
const names = new Set(Object.keys(infoCache));
|
|
@@ -150,11 +189,7 @@ async function listChromiumProfiles(browser, root, platform) {
|
|
|
150
189
|
(names.size === 1 && name === 'Default'),
|
|
151
190
|
});
|
|
152
191
|
}
|
|
153
|
-
return profiles
|
|
154
|
-
(left, right) =>
|
|
155
|
-
Number(right.isDefault) - Number(left.isDefault) ||
|
|
156
|
-
left.name.localeCompare(right.name)
|
|
157
|
-
);
|
|
192
|
+
return sortProfiles(profiles);
|
|
158
193
|
}
|
|
159
194
|
|
|
160
195
|
function parseIni(text) {
|
|
@@ -176,7 +211,7 @@ function parseIni(text) {
|
|
|
176
211
|
return sections;
|
|
177
212
|
}
|
|
178
213
|
|
|
179
|
-
async function listFirefoxProfiles(root, platform) {
|
|
214
|
+
async function listFirefoxProfiles(browser, root, platform) {
|
|
180
215
|
if (!(await pathExists(root))) {
|
|
181
216
|
return [];
|
|
182
217
|
}
|
|
@@ -215,23 +250,37 @@ async function listFirefoxProfiles(root, platform) {
|
|
|
215
250
|
section.IsRelative === '0'
|
|
216
251
|
? section.Path
|
|
217
252
|
: pathApi.resolve(relativeRoot, section.Path);
|
|
218
|
-
if (!(await findCookieDatabase(
|
|
253
|
+
if (!(await findCookieDatabase(browser, profilePath, platform))) {
|
|
219
254
|
continue;
|
|
220
255
|
}
|
|
221
256
|
const displayName = section.Name ?? pathApi.basename(profilePath);
|
|
222
257
|
profiles.push({
|
|
223
|
-
browser
|
|
258
|
+
browser,
|
|
224
259
|
name: displayName,
|
|
225
260
|
displayName,
|
|
226
261
|
path: profilePath,
|
|
227
262
|
isDefault: section.Default === '1',
|
|
228
263
|
});
|
|
229
264
|
}
|
|
230
|
-
return profiles
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
265
|
+
return sortProfiles(profiles);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function listProfilesForBrowser(browser, platform, homeDir, environment) {
|
|
269
|
+
const roots = resolveBrowserRoots(browser, {
|
|
270
|
+
platform,
|
|
271
|
+
homeDir,
|
|
272
|
+
environment,
|
|
273
|
+
});
|
|
274
|
+
const family = browserFamily(browser);
|
|
275
|
+
const profiles = [];
|
|
276
|
+
for (const root of roots) {
|
|
277
|
+
profiles.push(
|
|
278
|
+
...(family === 'firefox'
|
|
279
|
+
? await listFirefoxProfiles(browser, root, platform)
|
|
280
|
+
: await listChromiumProfiles(browser, root, platform))
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return profiles;
|
|
235
284
|
}
|
|
236
285
|
|
|
237
286
|
/** Discover cookie-bearing profiles from installed browsers. */
|
|
@@ -240,28 +289,45 @@ export async function listBrowserProfiles({
|
|
|
240
289
|
platform = process.platform,
|
|
241
290
|
homeDir = os.homedir(),
|
|
242
291
|
environment = process.env,
|
|
292
|
+
runCommand,
|
|
243
293
|
} = {}) {
|
|
244
294
|
const browsers = browser
|
|
245
|
-
? [
|
|
246
|
-
|
|
295
|
+
? [
|
|
296
|
+
await resolveSourceBrowser(browser, {
|
|
297
|
+
platform,
|
|
298
|
+
environment,
|
|
299
|
+
runCommand,
|
|
300
|
+
}),
|
|
301
|
+
]
|
|
302
|
+
: BROWSER_IDS;
|
|
247
303
|
const profiles = [];
|
|
304
|
+
// Several Firefox channels (firefox, firefox-developer, firefox-nightly)
|
|
305
|
+
// share one profile root, so a catalogue-wide scan would otherwise report
|
|
306
|
+
// the same profile under each id. Keep the first (canonical) browser.
|
|
307
|
+
const seen = new Set();
|
|
248
308
|
for (const candidate of browsers) {
|
|
249
|
-
const
|
|
309
|
+
for (const profile of await listProfilesForBrowser(
|
|
310
|
+
candidate,
|
|
250
311
|
platform,
|
|
251
312
|
homeDir,
|
|
252
|
-
environment
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
313
|
+
environment
|
|
314
|
+
)) {
|
|
315
|
+
if (seen.has(profile.path)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
seen.add(profile.path);
|
|
319
|
+
profiles.push(profile);
|
|
320
|
+
}
|
|
259
321
|
}
|
|
260
322
|
return profiles;
|
|
261
323
|
}
|
|
262
324
|
|
|
263
325
|
export async function resolveBrowserProfile(options) {
|
|
264
|
-
const browser =
|
|
326
|
+
const browser = await resolveSourceBrowser(options.browser, {
|
|
327
|
+
platform: options.platform,
|
|
328
|
+
environment: options.environment,
|
|
329
|
+
runCommand: options.runCommand,
|
|
330
|
+
});
|
|
265
331
|
const profiles = await listBrowserProfiles({ ...options, browser });
|
|
266
332
|
const requested = options.profile;
|
|
267
333
|
const selected = requested
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalogue of installed browsers Browser Commander can import from (#114).
|
|
3
|
+
*
|
|
4
|
+
* The data lives in `browser-sources.json`, a shared asset Python and Rust
|
|
5
|
+
* duplicate byte-for-byte (checked by
|
|
6
|
+
* `scripts/check-shared-fingerprint-assets.sh`). This module turns that data
|
|
7
|
+
* into the lookups the rest of the browser code needs: canonical ids with
|
|
8
|
+
* their aliases, the per-platform profile roots, the Chromium Safe Storage
|
|
9
|
+
* identity, and the operating-system identifiers that mark a browser as the
|
|
10
|
+
* system default.
|
|
11
|
+
*
|
|
12
|
+
* Keeping it data-driven is what lets a single JSON edit add Opera, Vivaldi,
|
|
13
|
+
* Arc, a Firefox fork, or a Chrome channel to all three implementations at
|
|
14
|
+
* once, rather than touching hand-written per-platform maps in each language.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
const registry = JSON.parse(
|
|
21
|
+
readFileSync(new URL('./browser-sources.json', import.meta.url), 'utf8')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every known browser, in catalogue order.
|
|
26
|
+
*
|
|
27
|
+
* @type {ReadonlyArray<object>}
|
|
28
|
+
*/
|
|
29
|
+
export const BROWSER_SOURCES = Object.freeze(
|
|
30
|
+
registry.browsers.map((browser) => Object.freeze(browser))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Canonical ids, in catalogue order. */
|
|
34
|
+
export const BROWSER_IDS = Object.freeze(
|
|
35
|
+
BROWSER_SOURCES.map((browser) => browser.id)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const BY_NAME = new Map();
|
|
39
|
+
for (const browser of BROWSER_SOURCES) {
|
|
40
|
+
BY_NAME.set(browser.id, browser);
|
|
41
|
+
for (const alias of browser.aliases ?? []) {
|
|
42
|
+
BY_NAME.set(alias, browser);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function platformPath(platform) {
|
|
47
|
+
return platform === 'win32' ? path.win32 : path;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a name (canonical id or alias) to its catalogue entry.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @returns {object|undefined}
|
|
55
|
+
*/
|
|
56
|
+
export function findBrowserSource(name) {
|
|
57
|
+
if (typeof name !== 'string') {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return BY_NAME.get(name) ?? BY_NAME.get(name.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a name to its canonical id, throwing a readable error otherwise.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} name
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeBrowserId(name) {
|
|
70
|
+
return normalizeBrowserSource(name).id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The family ('chromium' or 'firefox') of a browser name. */
|
|
74
|
+
export function browserFamily(name) {
|
|
75
|
+
return normalizeBrowserSource(name).family;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeBrowserSource(name) {
|
|
79
|
+
const source = findBrowserSource(name);
|
|
80
|
+
if (!source) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Unsupported browser: ${name}. Expected one of ${BROWSER_IDS.join(', ')}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return source;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function templateVariables(platform, homeDir, environment) {
|
|
89
|
+
const pathApi = platformPath(platform);
|
|
90
|
+
if (platform === 'darwin') {
|
|
91
|
+
return {
|
|
92
|
+
home: homeDir,
|
|
93
|
+
appSupport: pathApi.join(homeDir, 'Library', 'Application Support'),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (platform === 'win32') {
|
|
97
|
+
return {
|
|
98
|
+
home: homeDir,
|
|
99
|
+
localAppData:
|
|
100
|
+
environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local'),
|
|
101
|
+
appData:
|
|
102
|
+
environment.APPDATA ?? pathApi.join(homeDir, 'AppData', 'Roaming'),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
home: homeDir,
|
|
107
|
+
config: environment.XDG_CONFIG_HOME ?? pathApi.join(homeDir, '.config'),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function expandTemplate(template, variables, pathApi) {
|
|
112
|
+
const match = /^\{(\w+)\}(.*)$/.exec(template);
|
|
113
|
+
if (!match) {
|
|
114
|
+
return template;
|
|
115
|
+
}
|
|
116
|
+
const base = variables[match[1]];
|
|
117
|
+
if (base === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const rest = match[2].split('/').filter(Boolean);
|
|
121
|
+
return pathApi.join(base, ...rest);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The absolute profile roots a browser uses on a platform. Returns an empty
|
|
126
|
+
* array when the browser does not run on that platform (for example Chrome
|
|
127
|
+
* Canary on Linux).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} name
|
|
130
|
+
* @param {Object} [options]
|
|
131
|
+
* @param {string} [options.platform=process.platform]
|
|
132
|
+
* @param {string} [options.homeDir=os.homedir()]
|
|
133
|
+
* @param {Object} [options.environment=process.env]
|
|
134
|
+
* @returns {string[]}
|
|
135
|
+
*/
|
|
136
|
+
export function resolveBrowserRoots(
|
|
137
|
+
name,
|
|
138
|
+
{
|
|
139
|
+
platform = process.platform,
|
|
140
|
+
homeDir = os.homedir(),
|
|
141
|
+
environment = process.env,
|
|
142
|
+
} = {}
|
|
143
|
+
) {
|
|
144
|
+
const source = normalizeBrowserSource(name);
|
|
145
|
+
const templates = source.roots?.[platform] ?? [];
|
|
146
|
+
const pathApi = platformPath(platform);
|
|
147
|
+
const variables = templateVariables(platform, homeDir, environment);
|
|
148
|
+
const roots = [];
|
|
149
|
+
for (const template of templates) {
|
|
150
|
+
const resolved = expandTemplate(template, variables, pathApi);
|
|
151
|
+
if (resolved !== undefined) {
|
|
152
|
+
roots.push(resolved);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return roots;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** True when a browser stores one profile in the root itself (Opera-style). */
|
|
159
|
+
export function isSingleProfileBrowser(name) {
|
|
160
|
+
return normalizeBrowserSource(name).singleProfile === true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The Chromium Safe Storage identity for a browser, or undefined for a
|
|
165
|
+
* Firefox-family browser (which does not use OSCrypt).
|
|
166
|
+
*
|
|
167
|
+
* @param {string} name
|
|
168
|
+
* @returns {{service: string, application: string, folder: string}|undefined}
|
|
169
|
+
*/
|
|
170
|
+
export function safeStorageIdentity(name) {
|
|
171
|
+
return normalizeBrowserSource(name).safeStorage;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The operating-system identifiers that mark a browser as the system default:
|
|
176
|
+
* macOS bundle ids, Linux `.desktop` file names, or Windows ProgIds.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} name
|
|
179
|
+
* @param {string} platform
|
|
180
|
+
* @returns {string[]}
|
|
181
|
+
*/
|
|
182
|
+
export function defaultBrowserIdentifiers(name, platform) {
|
|
183
|
+
return normalizeBrowserSource(name).default?.[platform] ?? [];
|
|
184
|
+
}
|