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.
- package/.jscpd-baseline.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +7 -4
- package/package.json +4 -1
- package/src/browser/attach/snapshot.js +4 -28
- package/src/browser/browser-cookie-credentials.js +15 -8
- package/src/browser/browser-cookies.js +36 -30
- package/src/browser/browser-profile-files.js +31 -0
- package/src/browser/browser-profiles.js +11 -13
- package/src/browser/browser-sources.js +39 -9
- package/src/browser/browser-sources.json +610 -25
- package/src/browser/launcher.js +14 -0
- package/src/browser/migration/capabilities.json +117 -0
- package/src/browser/migration/chromium-history.sql +22 -0
- package/src/browser/migration/chromium-writers.js +85 -0
- package/src/browser/migration/cookies.js +4 -1
- package/src/browser/migration/data-classes.js +68 -0
- package/src/browser/migration/domains.js +32 -0
- package/src/browser/migration/firefox-history.js +106 -0
- package/src/browser/migration/firefox.js +39 -25
- package/src/browser/migration/history.js +161 -2
- package/src/browser/migration/index.js +229 -83
- package/src/browser/migration/os-crypt-keys.js +1 -5
- package/src/browser/migration/password-metadata.js +95 -0
- package/src/browser/migration/passwords.js +78 -41
- package/src/browser/migration/safari-import.js +100 -0
- package/src/browser/migration/safari.js +110 -0
- package/src/browser/migration/sqlite-snapshot.js +31 -65
- package/src/browser/migration/validation.js +48 -0
- package/src/browser/real-browser.js +18 -1
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-launch.js +42 -0
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/safari-support.js +98 -0
- package/src/browser/storage-state.js +28 -8
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +64 -19
- package/src/cli/args.js +2 -0
- package/src/cli/commands.js +2 -0
- package/src/cli/high-level-methods.js +2 -0
- package/src/cli/sessions.js +5 -0
- package/src/core/webdriver-page.js +62 -0
- package/src/exports.js +6 -0
- package/src/traces/recorder.js +5 -0
- package/tests/e2e/safari.e2e.test.js +104 -0
- package/tests/unit/browser/attach/snapshot.test.js +30 -0
- package/tests/unit/browser/browser-cookies.test.js +35 -0
- package/tests/unit/browser/catalogue-launch.test.js +93 -0
- package/tests/unit/browser/cookie-sources.test.js +92 -17
- package/tests/unit/browser/credential-catalogue.test.js +50 -0
- package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
- package/tests/unit/browser/migration/data-classes.test.js +89 -0
- package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
- package/tests/unit/browser/migration/firefox-history.test.js +130 -0
- package/tests/unit/browser/migration/history-metadata.test.js +74 -0
- package/tests/unit/browser/migration/index.test.js +16 -1
- package/tests/unit/browser/migration/opera-keys.test.js +22 -0
- package/tests/unit/browser/migration/safari-data.test.js +143 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
- package/tests/unit/browser/migration/validation.test.js +63 -0
- package/tests/unit/browser/migration/yandex.test.js +84 -0
- package/tests/unit/browser/real-browser.test.js +19 -0
- package/tests/unit/browser/safari-cookies.test.js +26 -1
- package/tests/unit/browser/safari-profiles.test.js +111 -0
- package/tests/unit/browser/safari.test.js +192 -0
- package/tests/unit/browser/webdriver.test.js +48 -3
- package/tests/unit/cli/dispatcher.test.js +5 -1
- package/tests/unit/cli/main.test.js +5 -0
- package/tests/unit/scripts/browser-support.test.js +14 -7
- package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
- package/tests/unit/scripts/migration-support.test.js +31 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/** A protected path remains a source, so reads can explain Full Disk Access. */
|
|
4
|
+
export async function findSafariFile(candidates, statFile = stat) {
|
|
5
|
+
for (const candidate of candidates) {
|
|
6
|
+
try {
|
|
7
|
+
if ((await statFile(candidate)).isFile()) {
|
|
8
|
+
return candidate;
|
|
9
|
+
}
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (['EPERM', 'EACCES'].includes(error.code)) {
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Enrich protection failures for every Safari store, preserving the cause. */
|
|
23
|
+
export async function withSafariAccess(
|
|
24
|
+
filename,
|
|
25
|
+
read,
|
|
26
|
+
environment = process.env
|
|
27
|
+
) {
|
|
28
|
+
try {
|
|
29
|
+
return await read();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (!['EPERM', 'EACCES'].includes(error.code)) {
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const app =
|
|
35
|
+
environment.__CFBundleIdentifier ||
|
|
36
|
+
environment.TERM_PROGRAM ||
|
|
37
|
+
process.execPath;
|
|
38
|
+
const diagnostic = new Error(
|
|
39
|
+
`Safari access denied (${error.code}) at ${filename}. Grant Full Disk Access to ${app}, the app running Browser Commander, then retry: x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles`,
|
|
40
|
+
{ cause: error }
|
|
41
|
+
);
|
|
42
|
+
diagnostic.code = error.code;
|
|
43
|
+
throw diagnostic;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFile as readFileBytes, stat } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { findSafariFile, withSafariAccess } from './safari-access.js';
|
|
7
8
|
import { TextDecoder } from 'node:util';
|
|
9
|
+
import { matchesDomains } from './migration/domains.js';
|
|
8
10
|
|
|
9
11
|
const APPLE_EPOCH = 978_307_200;
|
|
10
12
|
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
@@ -121,7 +123,7 @@ export function countSafariCookies(data, domains) {
|
|
|
121
123
|
const host = string(record, 16).toLowerCase();
|
|
122
124
|
total += 1;
|
|
123
125
|
for (const domain of Object.keys(byDomain ?? {})) {
|
|
124
|
-
if (host
|
|
126
|
+
if (matchesDomains(host, [domain])) {
|
|
125
127
|
byDomain[domain] += 1;
|
|
126
128
|
}
|
|
127
129
|
}
|
|
@@ -130,44 +132,45 @@ export function countSafariCookies(data, domains) {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
/** A protected path is still a source: preserve it for an actionable read error. */
|
|
133
|
-
export
|
|
134
|
-
|
|
135
|
-
path.
|
|
136
|
-
path.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
135
|
+
export function findSafariCookieFile(profileDir, statFile = stat) {
|
|
136
|
+
const named =
|
|
137
|
+
path.basename(path.dirname(profileDir)) === 'Profiles' &&
|
|
138
|
+
path.basename(path.dirname(path.dirname(profileDir))) === 'Safari';
|
|
139
|
+
const root = named
|
|
140
|
+
? path.dirname(path.dirname(path.dirname(profileDir)))
|
|
141
|
+
: profileDir;
|
|
142
|
+
const modern = named
|
|
143
|
+
? [
|
|
144
|
+
path.join(
|
|
145
|
+
root,
|
|
146
|
+
'WebKit/WebsiteDataStore',
|
|
147
|
+
path.basename(profileDir).toLowerCase(),
|
|
148
|
+
'Cookies/Cookies.binarycookies'
|
|
149
|
+
),
|
|
150
|
+
]
|
|
151
|
+
: [
|
|
152
|
+
path.join(
|
|
153
|
+
root,
|
|
154
|
+
'WebKit/WebsiteData/Default/Cookies/Cookies.binarycookies'
|
|
155
|
+
),
|
|
156
|
+
path.join(
|
|
157
|
+
root,
|
|
158
|
+
'WebKit/WebsiteDataStore/Default/Cookies/Cookies.binarycookies'
|
|
159
|
+
),
|
|
160
|
+
];
|
|
161
|
+
return findSafariFile(
|
|
162
|
+
[
|
|
163
|
+
...modern,
|
|
164
|
+
path.join(profileDir, 'Cookies', 'Cookies.binarycookies'),
|
|
165
|
+
path.join(profileDir, 'Cookies.binarycookies'),
|
|
166
|
+
],
|
|
167
|
+
statFile
|
|
168
|
+
);
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
export
|
|
171
|
+
export function readSafariCookieFile(
|
|
155
172
|
filePath,
|
|
156
173
|
{ environment = process.env, readFile = readFileBytes } = {}
|
|
157
174
|
) {
|
|
158
|
-
|
|
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
|
-
}
|
|
175
|
+
return withSafariAccess(filePath, () => readFile(filePath), environment);
|
|
173
176
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { launchWebDriver } from './webdriver.js';
|
|
2
|
+
import { findBrowserSource } from './browser-sources.js';
|
|
3
|
+
import { validateSafariOptions } from './safari-support.js';
|
|
4
|
+
import { restoreWebDriverStorageState } from './storage-state.js';
|
|
5
|
+
|
|
6
|
+
/** Launch installed Safari through its native W3C WebDriver server. */
|
|
7
|
+
export async function launchSafari(options = {}, dependencies = {}) {
|
|
8
|
+
validateSafariOptions(options);
|
|
9
|
+
const channel = findBrowserSource(
|
|
10
|
+
options.channel ?? options.browser ?? 'safari'
|
|
11
|
+
).id;
|
|
12
|
+
const launched = await (dependencies.launchWebDriver ?? launchWebDriver)(
|
|
13
|
+
{
|
|
14
|
+
...options,
|
|
15
|
+
browser: channel,
|
|
16
|
+
driverPath: options.driverPath ?? options.executablePath,
|
|
17
|
+
executablePath: undefined,
|
|
18
|
+
},
|
|
19
|
+
dependencies
|
|
20
|
+
);
|
|
21
|
+
try {
|
|
22
|
+
const state = options.storageState;
|
|
23
|
+
await restoreWebDriverStorageState({
|
|
24
|
+
page: launched.page,
|
|
25
|
+
storageState: state,
|
|
26
|
+
seedCookies: options.seedCookies,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
await launched.close();
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...launched,
|
|
34
|
+
browser: launched.driver,
|
|
35
|
+
engine: 'selenium',
|
|
36
|
+
controlProtocol: 'webdriver',
|
|
37
|
+
executablePath: launched.driverPath,
|
|
38
|
+
browserProcess: launched.driverProcess,
|
|
39
|
+
downloads: null,
|
|
40
|
+
launch: options.launch ?? 'real',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { findSafariCookieFile } from './safari-cookies.js';
|
|
4
|
+
import { findSafariFile, withSafariAccess } from './safari-access.js';
|
|
5
|
+
import { openSqliteDatabase } from './browser-cookie-database.js';
|
|
6
|
+
|
|
7
|
+
/** SafariTabs supplies names; each UUID has separate Safari and WebKit stores. */
|
|
8
|
+
export async function listSafariProfiles(browser, root) {
|
|
9
|
+
const profiles = [];
|
|
10
|
+
const defaultFile =
|
|
11
|
+
(await findSafariCookieFile(root)) ??
|
|
12
|
+
(await findSafariFile([
|
|
13
|
+
path.join(root, 'Safari/History.db'),
|
|
14
|
+
path.join(root, 'History.db'),
|
|
15
|
+
path.join(root, 'Safari/Bookmarks.plist'),
|
|
16
|
+
path.join(root, 'Bookmarks.plist'),
|
|
17
|
+
]));
|
|
18
|
+
if (defaultFile) {
|
|
19
|
+
profiles.push({
|
|
20
|
+
browser,
|
|
21
|
+
name: 'Default',
|
|
22
|
+
displayName: 'Default',
|
|
23
|
+
path: root,
|
|
24
|
+
isDefault: true,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const tabs = await findSafariFile([
|
|
28
|
+
path.join(root, 'Safari/SafariTabs.db'),
|
|
29
|
+
path.join(root, 'SafariTabs.db'),
|
|
30
|
+
]);
|
|
31
|
+
if (!tabs) {
|
|
32
|
+
return profiles;
|
|
33
|
+
}
|
|
34
|
+
let rows;
|
|
35
|
+
try {
|
|
36
|
+
rows = await withSafariAccess(tabs, async () => {
|
|
37
|
+
const file = await open(tabs, 'r');
|
|
38
|
+
await file.close();
|
|
39
|
+
const db = await openSqliteDatabase(tabs, {
|
|
40
|
+
readOnly: true,
|
|
41
|
+
fileMustExist: true,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
return db
|
|
45
|
+
.prepare(
|
|
46
|
+
"SELECT DISTINCT external_uuid,title FROM bookmarks WHERE subtype=2 AND external_uuid != 'DefaultProfile' ORDER BY external_uuid"
|
|
47
|
+
)
|
|
48
|
+
.all();
|
|
49
|
+
} finally {
|
|
50
|
+
db.close();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
profiles.push({
|
|
55
|
+
browser,
|
|
56
|
+
name: 'Profiles',
|
|
57
|
+
displayName: 'Safari profile discovery',
|
|
58
|
+
path: root,
|
|
59
|
+
isDefault: false,
|
|
60
|
+
error: error.message,
|
|
61
|
+
});
|
|
62
|
+
return profiles;
|
|
63
|
+
}
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
if (
|
|
66
|
+
!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(
|
|
67
|
+
row.external_uuid ?? ''
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const name = row.external_uuid.toUpperCase();
|
|
73
|
+
const profilePath = path.join(root, 'Safari/Profiles', name);
|
|
74
|
+
profiles.push({
|
|
75
|
+
browser,
|
|
76
|
+
name,
|
|
77
|
+
displayName: row.title ?? name,
|
|
78
|
+
path: profilePath,
|
|
79
|
+
isDefault: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return profiles;
|
|
83
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { findBrowserSource } from './browser-sources.js';
|
|
2
|
+
import { runCommand } from '../utilities/subprocess.js';
|
|
3
|
+
|
|
4
|
+
/** Whether a channel (including aliases) selects an installed Safari. */
|
|
5
|
+
export function isSafariChannel(channel) {
|
|
6
|
+
return findBrowserSource(channel)?.family === 'safari';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A requested operation unavailable in Safari's classic WebDriver. */
|
|
10
|
+
export class SafariUnsupportedError extends Error {
|
|
11
|
+
constructor(feature) {
|
|
12
|
+
super(`${feature} is unsupported on safari`);
|
|
13
|
+
this.name = 'SafariUnsupportedError';
|
|
14
|
+
this.code = 'SAFARI_UNSUPPORTED';
|
|
15
|
+
this.feature = feature;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One-time setup failure with actionable instructions and a settings opener. */
|
|
20
|
+
export class SafariSetupError extends Error {
|
|
21
|
+
constructor(cause, channel = 'safari') {
|
|
22
|
+
const driver = findBrowserSource(channel).executables.darwin[0];
|
|
23
|
+
super(
|
|
24
|
+
`Safari automation setup required: ${cause.message}. In Safari → Settings → Advanced enable "Show features for web developers", then Develop → "Allow Remote Automation". Run "${driver}" --enable once (an admin password may be required). Call error.openSettings() to open Safari's Advanced settings.`,
|
|
25
|
+
{ cause }
|
|
26
|
+
);
|
|
27
|
+
this.name = 'SafariSetupError';
|
|
28
|
+
this.code = 'SAFARI_SETUP_REQUIRED';
|
|
29
|
+
this.channel = channel;
|
|
30
|
+
}
|
|
31
|
+
openSettings() {
|
|
32
|
+
return openSafariSettings({ channel: this.channel });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Translate only recognizable authorization failures; preserve other errors. */
|
|
37
|
+
export function safariLaunchError(error, channel) {
|
|
38
|
+
return /allow\s+remote\s+automation|remote automation.*(?:disabled|enable)|(?:safaridriver|webdriver).*(?:--enable|authoriz|not enabled)|not authorized.*(?:safari|automation)/i.test(
|
|
39
|
+
error.message
|
|
40
|
+
)
|
|
41
|
+
? new SafariSetupError(error, channel)
|
|
42
|
+
: error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Open Advanced settings on explicit request; never change authorization. */
|
|
46
|
+
export async function openSafariSettings({
|
|
47
|
+
channel = 'safari',
|
|
48
|
+
platform = process.platform,
|
|
49
|
+
run = runCommand,
|
|
50
|
+
} = {}) {
|
|
51
|
+
if (platform !== 'darwin') {
|
|
52
|
+
throw new SafariUnsupportedError('Safari settings outside macOS');
|
|
53
|
+
}
|
|
54
|
+
const app =
|
|
55
|
+
findBrowserSource(channel)?.id === 'safari-technology-preview'
|
|
56
|
+
? 'Safari Technology Preview'
|
|
57
|
+
: 'Safari';
|
|
58
|
+
return await run('osascript', [
|
|
59
|
+
'-e',
|
|
60
|
+
`tell application "${app}" to activate`,
|
|
61
|
+
'-e',
|
|
62
|
+
`tell application "${app}" to open location "x-safari-preferences:com.apple.Safari.preferences.Advanced"`,
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Reject settings Safari cannot apply before touching disk or starting it. */
|
|
67
|
+
export function validateSafariOptions(options) {
|
|
68
|
+
for (const name of [
|
|
69
|
+
'headless',
|
|
70
|
+
'userDataDir',
|
|
71
|
+
'attach',
|
|
72
|
+
'migrateFrom',
|
|
73
|
+
'remoteDebuggingPort',
|
|
74
|
+
'cdpEndpoint',
|
|
75
|
+
'wsEndpoint',
|
|
76
|
+
'fingerprint',
|
|
77
|
+
'colorScheme',
|
|
78
|
+
'downloads',
|
|
79
|
+
'bidi',
|
|
80
|
+
'preferences',
|
|
81
|
+
'localState',
|
|
82
|
+
'defaultBrowserCheck',
|
|
83
|
+
'firstRun',
|
|
84
|
+
]) {
|
|
85
|
+
if (
|
|
86
|
+
options[name] !== undefined &&
|
|
87
|
+
options[name] !== null &&
|
|
88
|
+
options[name] !== false
|
|
89
|
+
) {
|
|
90
|
+
throw new SafariUnsupportedError(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const name of ['args', 'extraArgs', 'restrictions']) {
|
|
94
|
+
if (options[name]?.length) {
|
|
95
|
+
throw new SafariUnsupportedError(name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -31,27 +31,47 @@ function restoreOriginLocalStorage(origins) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Restore classic WebDriver cookies; origin preload scripts require BiDi. */
|
|
34
|
-
export async function restoreWebDriverStorageState({
|
|
35
|
-
|
|
34
|
+
export async function restoreWebDriverStorageState({
|
|
35
|
+
page,
|
|
36
|
+
storageState,
|
|
37
|
+
seedCookies = [],
|
|
38
|
+
}) {
|
|
39
|
+
storageState = await loadStorageState(storageState);
|
|
40
|
+
if (!storageState && !seedCookies.length) {
|
|
36
41
|
return;
|
|
37
42
|
}
|
|
38
|
-
const origins = storageState
|
|
43
|
+
const origins = storageState?.origins ?? [];
|
|
39
44
|
if (origins.length) {
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
if (!page.browserName?.toLowerCase().startsWith('safari')) {
|
|
46
|
+
await page.evaluateOnNewDocument(restoreOriginLocalStorage, origins);
|
|
47
|
+
await page.evaluate(restoreOriginLocalStorage, origins);
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
const originalUrl = page.url();
|
|
44
|
-
const cookies = storageState
|
|
51
|
+
const cookies = [...(storageState?.cookies ?? []), ...seedCookies];
|
|
45
52
|
try {
|
|
46
53
|
for (const cookie of cookies) {
|
|
47
54
|
const origin = cookie.url
|
|
48
55
|
? new URL(cookie.url).origin
|
|
49
|
-
:
|
|
56
|
+
: (origins.find(
|
|
57
|
+
({ origin }) =>
|
|
58
|
+
new URL(origin).hostname === cookie.domain?.replace(/^\./u, '')
|
|
59
|
+
)?.origin ??
|
|
60
|
+
`${cookie.secure ? 'https' : 'http'}://${cookie.domain.replace(/^\./u, '')}`);
|
|
50
61
|
await page.goto(origin);
|
|
51
62
|
await page.setCookie(cookie);
|
|
52
63
|
}
|
|
64
|
+
if (page.browserName?.toLowerCase().startsWith('safari')) {
|
|
65
|
+
for (const { origin } of origins) {
|
|
66
|
+
await page.goto(origin);
|
|
67
|
+
await page.evaluate(restoreOriginLocalStorage, origins);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
53
70
|
} finally {
|
|
54
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
cookies.length ||
|
|
73
|
+
(origins.length && page.browserName?.toLowerCase().startsWith('safari'))
|
|
74
|
+
) {
|
|
55
75
|
await page.goto(originalUrl);
|
|
56
76
|
}
|
|
57
77
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
+
import { physicalPath } from './browser-profile-files.js';
|
|
3
|
+
import {
|
|
4
|
+
BROWSER_SOURCES,
|
|
5
|
+
resolveBrowserRoots,
|
|
6
|
+
resolveBrowserExecutables,
|
|
7
|
+
} from './browser-sources.js';
|
|
2
8
|
import { access } from 'node:fs/promises';
|
|
3
9
|
import os from 'node:os';
|
|
4
10
|
import path from 'node:path';
|
|
@@ -9,18 +15,16 @@ import path from 'node:path';
|
|
|
9
15
|
* to refuse a default profile; profile migration uses it to find the source.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
|
-
export const CHANNEL_EXECUTABLE_NAMES =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'msedge-dev': ['microsoft-edge-dev'],
|
|
23
|
-
};
|
|
18
|
+
export const CHANNEL_EXECUTABLE_NAMES = Object.freeze(
|
|
19
|
+
Object.fromEntries(
|
|
20
|
+
BROWSER_SOURCES.flatMap((browser) =>
|
|
21
|
+
[browser.id, ...(browser.aliases ?? [])].map((name) => [
|
|
22
|
+
name,
|
|
23
|
+
browser.executableNames ?? [],
|
|
24
|
+
])
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
);
|
|
24
28
|
|
|
25
29
|
function platformPath(platform) {
|
|
26
30
|
return platform === 'win32' ? path.win32 : path;
|
|
@@ -41,72 +45,13 @@ export function knownDefaultUserDataDirs({
|
|
|
41
45
|
homeDir = os.homedir(),
|
|
42
46
|
environment = process.env,
|
|
43
47
|
} = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
return BROWSER_SOURCES.flatMap((browser) =>
|
|
49
|
+
resolveBrowserRoots(browser.id, {
|
|
50
|
+
platform,
|
|
47
51
|
homeDir,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return [
|
|
52
|
-
pathApi.join(applicationSupport, 'Google', 'Chrome'),
|
|
53
|
-
pathApi.join(applicationSupport, 'Google', 'Chrome Beta'),
|
|
54
|
-
pathApi.join(applicationSupport, 'Google', 'Chrome Canary'),
|
|
55
|
-
pathApi.join(applicationSupport, 'Google', 'Chrome Dev'),
|
|
56
|
-
pathApi.join(applicationSupport, 'Chromium'),
|
|
57
|
-
pathApi.join(applicationSupport, 'BraveSoftware', 'Brave-Browser'),
|
|
58
|
-
pathApi.join(applicationSupport, 'BraveSoftware', 'Brave-Browser-Beta'),
|
|
59
|
-
pathApi.join(
|
|
60
|
-
applicationSupport,
|
|
61
|
-
'BraveSoftware',
|
|
62
|
-
'Brave-Browser-Nightly'
|
|
63
|
-
),
|
|
64
|
-
pathApi.join(applicationSupport, 'Microsoft Edge'),
|
|
65
|
-
pathApi.join(applicationSupport, 'Microsoft Edge Beta'),
|
|
66
|
-
pathApi.join(applicationSupport, 'Microsoft Edge Canary'),
|
|
67
|
-
pathApi.join(applicationSupport, 'Microsoft Edge Dev'),
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
if (platform === 'win32') {
|
|
71
|
-
const localAppData =
|
|
72
|
-
environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
|
|
73
|
-
return [
|
|
74
|
-
pathApi.join(localAppData, 'Google', 'Chrome', 'User Data'),
|
|
75
|
-
pathApi.join(localAppData, 'Google', 'Chrome Beta', 'User Data'),
|
|
76
|
-
pathApi.join(localAppData, 'Google', 'Chrome Dev', 'User Data'),
|
|
77
|
-
pathApi.join(localAppData, 'Google', 'Chrome SxS', 'User Data'),
|
|
78
|
-
pathApi.join(localAppData, 'Chromium', 'User Data'),
|
|
79
|
-
pathApi.join(localAppData, 'BraveSoftware', 'Brave-Browser', 'User Data'),
|
|
80
|
-
pathApi.join(
|
|
81
|
-
localAppData,
|
|
82
|
-
'BraveSoftware',
|
|
83
|
-
'Brave-Browser-Beta',
|
|
84
|
-
'User Data'
|
|
85
|
-
),
|
|
86
|
-
pathApi.join(
|
|
87
|
-
localAppData,
|
|
88
|
-
'BraveSoftware',
|
|
89
|
-
'Brave-Browser-Nightly',
|
|
90
|
-
'User Data'
|
|
91
|
-
),
|
|
92
|
-
pathApi.join(localAppData, 'Microsoft', 'Edge', 'User Data'),
|
|
93
|
-
pathApi.join(localAppData, 'Microsoft', 'Edge Beta', 'User Data'),
|
|
94
|
-
pathApi.join(localAppData, 'Microsoft', 'Edge Dev', 'User Data'),
|
|
95
|
-
pathApi.join(localAppData, 'Microsoft', 'Edge SxS', 'User Data'),
|
|
96
|
-
];
|
|
97
|
-
}
|
|
98
|
-
return [
|
|
99
|
-
pathApi.join(homeDir, '.config', 'google-chrome'),
|
|
100
|
-
pathApi.join(homeDir, '.config', 'google-chrome-beta'),
|
|
101
|
-
pathApi.join(homeDir, '.config', 'google-chrome-unstable'),
|
|
102
|
-
pathApi.join(homeDir, '.config', 'chromium'),
|
|
103
|
-
pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser'),
|
|
104
|
-
pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser-Beta'),
|
|
105
|
-
pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser-Nightly'),
|
|
106
|
-
pathApi.join(homeDir, '.config', 'microsoft-edge'),
|
|
107
|
-
pathApi.join(homeDir, '.config', 'microsoft-edge-beta'),
|
|
108
|
-
pathApi.join(homeDir, '.config', 'microsoft-edge-dev'),
|
|
109
|
-
];
|
|
52
|
+
environment,
|
|
53
|
+
})
|
|
54
|
+
);
|
|
110
55
|
}
|
|
111
56
|
|
|
112
57
|
/** Ensure Chrome is not asked to expose the user's default profile over CDP. */
|
|
@@ -117,12 +62,17 @@ export function assertDedicatedUserDataDir(userDataDir, platformOptions = {}) {
|
|
|
117
62
|
const platform = platformOptions.platform ?? process.platform;
|
|
118
63
|
const pathApi = platformPath(platform);
|
|
119
64
|
const normalize = (value) => {
|
|
120
|
-
|
|
65
|
+
let resolved = pathApi.resolve(value).replace(/[\\/]+$/, '');
|
|
66
|
+
if (platform === process.platform) {
|
|
67
|
+
resolved = physicalPath(resolved);
|
|
68
|
+
}
|
|
121
69
|
return platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
122
70
|
};
|
|
123
71
|
const requested = normalize(userDataDir);
|
|
124
72
|
const isDefault = knownDefaultUserDataDirs(platformOptions).some(
|
|
125
|
-
(directory) =>
|
|
73
|
+
(directory) =>
|
|
74
|
+
requested === normalize(directory) ||
|
|
75
|
+
requested.startsWith(`${normalize(directory)}${pathApi.sep}`)
|
|
126
76
|
);
|
|
127
77
|
if (isDefault) {
|
|
128
78
|
throw new Error(
|
|
@@ -136,74 +86,12 @@ function browserInstallCandidates({
|
|
|
136
86
|
platform = process.platform,
|
|
137
87
|
environment = process.env,
|
|
138
88
|
}) {
|
|
139
|
-
|
|
140
|
-
const names = CHANNEL_EXECUTABLE_NAMES[channel];
|
|
141
|
-
if (!names) {
|
|
89
|
+
if (!CHANNEL_EXECUTABLE_NAMES[channel]?.length) {
|
|
142
90
|
throw new Error(
|
|
143
91
|
`Unknown browser channel: ${channel}. Expected one of ${Object.keys(CHANNEL_EXECUTABLE_NAMES).join(', ')}`
|
|
144
92
|
);
|
|
145
93
|
}
|
|
146
|
-
|
|
147
|
-
if (platform === 'darwin') {
|
|
148
|
-
const applications = {
|
|
149
|
-
brave: 'Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
150
|
-
chrome: 'Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
151
|
-
'chrome-beta': 'Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
|
|
152
|
-
'chrome-canary':
|
|
153
|
-
'Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
154
|
-
'chrome-dev': 'Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev',
|
|
155
|
-
chromium: 'Chromium.app/Contents/MacOS/Chromium',
|
|
156
|
-
msedge: 'Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
157
|
-
'msedge-beta':
|
|
158
|
-
'Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta',
|
|
159
|
-
'msedge-canary':
|
|
160
|
-
'Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary',
|
|
161
|
-
'msedge-dev': 'Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev',
|
|
162
|
-
};
|
|
163
|
-
candidates.push(path.join('/Applications', applications[channel]));
|
|
164
|
-
} else if (platform === 'win32') {
|
|
165
|
-
const roots = [
|
|
166
|
-
environment.PROGRAMFILES,
|
|
167
|
-
environment['PROGRAMFILES(X86)'],
|
|
168
|
-
environment.LOCALAPPDATA,
|
|
169
|
-
].filter(Boolean);
|
|
170
|
-
const relativePaths = {
|
|
171
|
-
brave: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
|
|
172
|
-
chrome: ['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
173
|
-
'chrome-beta': ['Google', 'Chrome Beta', 'Application', 'chrome.exe'],
|
|
174
|
-
'chrome-canary': ['Google', 'Chrome SxS', 'Application', 'chrome.exe'],
|
|
175
|
-
'chrome-dev': ['Google', 'Chrome Dev', 'Application', 'chrome.exe'],
|
|
176
|
-
chromium: ['Chromium', 'Application', 'chrome.exe'],
|
|
177
|
-
msedge: ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
|
|
178
|
-
'msedge-beta': ['Microsoft', 'Edge Beta', 'Application', 'msedge.exe'],
|
|
179
|
-
'msedge-canary': ['Microsoft', 'Edge SxS', 'Application', 'msedge.exe'],
|
|
180
|
-
'msedge-dev': ['Microsoft', 'Edge Dev', 'Application', 'msedge.exe'],
|
|
181
|
-
};
|
|
182
|
-
for (const root of roots) {
|
|
183
|
-
candidates.push(path.win32.join(root, ...relativePaths[channel]));
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
for (const name of names) {
|
|
187
|
-
candidates.push(`/usr/bin/${name}`, `/usr/local/bin/${name}`);
|
|
188
|
-
}
|
|
189
|
-
if (channel === 'chrome') {
|
|
190
|
-
candidates.push('/opt/google/chrome/google-chrome');
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const pathApi = platformPath(platform);
|
|
195
|
-
const delimiter = platform === 'win32' ? ';' : path.delimiter;
|
|
196
|
-
for (const directory of (environment.PATH ?? '').split(delimiter)) {
|
|
197
|
-
if (!directory) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
for (const name of names) {
|
|
201
|
-
candidates.push(
|
|
202
|
-
pathApi.join(directory, platform === 'win32' ? `${name}.exe` : name)
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return [...new Set(candidates)];
|
|
94
|
+
return resolveBrowserExecutables(channel, { platform, environment });
|
|
207
95
|
}
|
|
208
96
|
|
|
209
97
|
/** Resolve a genuine installed Chrome-family browser executable. */
|