opensteer 0.6.2 → 0.6.4
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/bin/opensteer.mjs +94 -8
- package/dist/{browser-profile-client-DK9qa_Dj.d.cts → browser-profile-client-D6PuRefA.d.cts} +1 -1
- package/dist/{browser-profile-client-CaL-mwqs.d.ts → browser-profile-client-OUHaODro.d.ts} +1 -1
- package/dist/{chunk-7RMY26CM.js → chunk-54KNQTOL.js} +172 -55
- package/dist/{chunk-WJI7TGBQ.js → chunk-6B6LOYU3.js} +1 -1
- package/dist/{chunk-F2VDVOJO.js → chunk-G6V2DJRN.js} +451 -609
- package/dist/chunk-K5CL76MG.js +81 -0
- package/dist/{chunk-WDRMHPWL.js → chunk-KPPOTU3D.js} +159 -180
- package/dist/cli/auth.cjs +186 -95
- package/dist/cli/auth.d.cts +1 -1
- package/dist/cli/auth.d.ts +1 -1
- package/dist/cli/auth.js +2 -2
- package/dist/cli/local-profile.cjs +197 -0
- package/dist/cli/local-profile.d.cts +18 -0
- package/dist/cli/local-profile.d.ts +18 -0
- package/dist/cli/local-profile.js +97 -0
- package/dist/cli/profile.cjs +1747 -1279
- package/dist/cli/profile.d.cts +2 -2
- package/dist/cli/profile.d.ts +2 -2
- package/dist/cli/profile.js +469 -7
- package/dist/cli/server.cjs +759 -257
- package/dist/cli/server.js +69 -16
- package/dist/index.cjs +688 -238
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/{types-BxiRblC7.d.cts → types-BWItZPl_.d.cts} +31 -13
- package/dist/{types-BxiRblC7.d.ts → types-BWItZPl_.d.ts} +31 -13
- package/package.json +2 -2
- package/skills/opensteer/SKILL.md +34 -14
- package/skills/opensteer/references/cli-reference.md +1 -1
- package/skills/opensteer/references/examples.md +5 -3
- package/skills/opensteer/references/sdk-reference.md +16 -14
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
cloudNotLaunchedError,
|
|
4
4
|
cloudUnsupportedMethodError,
|
|
5
5
|
createCloudRuntimeState,
|
|
6
|
-
createKeychainStore,
|
|
7
6
|
extractErrorMessage,
|
|
8
7
|
normalizeError,
|
|
9
8
|
normalizeNamespace,
|
|
@@ -11,507 +10,18 @@ import {
|
|
|
11
10
|
resolveCloudSelection,
|
|
12
11
|
resolveConfigWithEnv,
|
|
13
12
|
resolveNamespace,
|
|
14
|
-
resolveNamespaceDir
|
|
15
|
-
|
|
13
|
+
resolveNamespaceDir,
|
|
14
|
+
selectCloudCredential
|
|
15
|
+
} from "./chunk-KPPOTU3D.js";
|
|
16
|
+
import {
|
|
17
|
+
detectChromePaths,
|
|
18
|
+
expandHome,
|
|
19
|
+
listLocalChromeProfiles
|
|
20
|
+
} from "./chunk-K5CL76MG.js";
|
|
16
21
|
import {
|
|
17
22
|
flattenExtractionDataToFieldPlan
|
|
18
23
|
} from "./chunk-3H5RRIMZ.js";
|
|
19
24
|
|
|
20
|
-
// src/browser/chrome.ts
|
|
21
|
-
import { homedir, platform } from "os";
|
|
22
|
-
import { join } from "path";
|
|
23
|
-
function expandHome(p) {
|
|
24
|
-
if (p.startsWith("~/") || p === "~") {
|
|
25
|
-
return join(homedir(), p.slice(1));
|
|
26
|
-
}
|
|
27
|
-
return p;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// src/browser/chromium-profile.ts
|
|
31
|
-
import { promisify } from "util";
|
|
32
|
-
import { execFile } from "child_process";
|
|
33
|
-
import { createDecipheriv, createHash, pbkdf2Sync } from "crypto";
|
|
34
|
-
import {
|
|
35
|
-
cp,
|
|
36
|
-
copyFile,
|
|
37
|
-
mkdtemp,
|
|
38
|
-
readdir,
|
|
39
|
-
readFile,
|
|
40
|
-
rm
|
|
41
|
-
} from "fs/promises";
|
|
42
|
-
import { existsSync, statSync } from "fs";
|
|
43
|
-
import { basename, dirname, join as join2 } from "path";
|
|
44
|
-
import { tmpdir } from "os";
|
|
45
|
-
import { chromium } from "playwright";
|
|
46
|
-
var execFileAsync = promisify(execFile);
|
|
47
|
-
var CHROMIUM_EPOCH_MICROS = 11644473600000000n;
|
|
48
|
-
var AES_BLOCK_BYTES = 16;
|
|
49
|
-
var MAC_KEY_ITERATIONS = 1003;
|
|
50
|
-
var LINUX_KEY_ITERATIONS = 1;
|
|
51
|
-
var KEY_LENGTH = 16;
|
|
52
|
-
var KEY_SALT = "saltysalt";
|
|
53
|
-
var DEFAULT_CHROMIUM_BRAND = {
|
|
54
|
-
macService: "Chrome Safe Storage",
|
|
55
|
-
macAccount: "Chrome",
|
|
56
|
-
linuxApplications: ["chrome", "google-chrome"]
|
|
57
|
-
};
|
|
58
|
-
var CHROMIUM_BRANDS = [
|
|
59
|
-
{
|
|
60
|
-
match: ["bravesoftware", "brave-browser"],
|
|
61
|
-
brand: {
|
|
62
|
-
macService: "Brave Safe Storage",
|
|
63
|
-
macAccount: "Brave",
|
|
64
|
-
linuxApplications: ["brave-browser", "brave"]
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
match: ["microsoft", "edge"],
|
|
69
|
-
brand: {
|
|
70
|
-
macService: "Microsoft Edge Safe Storage",
|
|
71
|
-
macAccount: "Microsoft Edge",
|
|
72
|
-
linuxApplications: ["microsoft-edge"],
|
|
73
|
-
playwrightChannel: "msedge"
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
match: ["google", "chrome beta"],
|
|
78
|
-
brand: {
|
|
79
|
-
macService: "Chrome Beta Safe Storage",
|
|
80
|
-
macAccount: "Chrome Beta",
|
|
81
|
-
linuxApplications: ["chrome-beta"],
|
|
82
|
-
playwrightChannel: "chrome-beta"
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
match: ["google", "chrome"],
|
|
87
|
-
brand: {
|
|
88
|
-
macService: "Chrome Safe Storage",
|
|
89
|
-
macAccount: "Chrome",
|
|
90
|
-
linuxApplications: ["chrome", "google-chrome"],
|
|
91
|
-
playwrightChannel: "chrome"
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
match: ["chromium"],
|
|
96
|
-
brand: {
|
|
97
|
-
macService: "Chromium Safe Storage",
|
|
98
|
-
macAccount: "Chromium",
|
|
99
|
-
linuxApplications: ["chromium"]
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
];
|
|
103
|
-
function directoryExists(filePath) {
|
|
104
|
-
try {
|
|
105
|
-
return statSync(filePath).isDirectory();
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function fileExists(filePath) {
|
|
111
|
-
try {
|
|
112
|
-
return statSync(filePath).isFile();
|
|
113
|
-
} catch {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function resolveCookieDbPath(profileDir) {
|
|
118
|
-
const candidates = [join2(profileDir, "Network", "Cookies"), join2(profileDir, "Cookies")];
|
|
119
|
-
for (const candidate of candidates) {
|
|
120
|
-
if (fileExists(candidate)) {
|
|
121
|
-
return candidate;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
async function selectProfileDirFromUserDataDir(userDataDir) {
|
|
127
|
-
const entries = await readdir(userDataDir, {
|
|
128
|
-
withFileTypes: true
|
|
129
|
-
}).catch(() => []);
|
|
130
|
-
const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => join2(userDataDir, entry.name)).filter((entryPath) => resolveCookieDbPath(entryPath));
|
|
131
|
-
return candidates;
|
|
132
|
-
}
|
|
133
|
-
async function resolveChromiumProfileLocation(inputPath) {
|
|
134
|
-
const expandedPath = expandHome(inputPath.trim());
|
|
135
|
-
if (!expandedPath) {
|
|
136
|
-
throw new Error("Profile path cannot be empty.");
|
|
137
|
-
}
|
|
138
|
-
if (fileExists(expandedPath) && basename(expandedPath) === "Cookies") {
|
|
139
|
-
const directParent = dirname(expandedPath);
|
|
140
|
-
const profileDir = basename(directParent) === "Network" ? dirname(directParent) : directParent;
|
|
141
|
-
const userDataDir = dirname(profileDir);
|
|
142
|
-
return {
|
|
143
|
-
userDataDir,
|
|
144
|
-
profileDir,
|
|
145
|
-
profileDirectory: basename(profileDir),
|
|
146
|
-
cookieDbPath: expandedPath,
|
|
147
|
-
localStatePath: fileExists(join2(userDataDir, "Local State")) ? join2(userDataDir, "Local State") : null
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
if (fileExists(expandedPath)) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
if (!directoryExists(expandedPath)) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Could not find a Chromium profile at "${inputPath}".`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
const directCookieDb = resolveCookieDbPath(expandedPath);
|
|
161
|
-
if (directCookieDb) {
|
|
162
|
-
const userDataDir = dirname(expandedPath);
|
|
163
|
-
return {
|
|
164
|
-
userDataDir,
|
|
165
|
-
profileDir: expandedPath,
|
|
166
|
-
profileDirectory: basename(expandedPath),
|
|
167
|
-
cookieDbPath: directCookieDb,
|
|
168
|
-
localStatePath: fileExists(join2(userDataDir, "Local State")) ? join2(userDataDir, "Local State") : null
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const localStatePath = join2(expandedPath, "Local State");
|
|
172
|
-
if (!fileExists(localStatePath)) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
const profileDirs = await selectProfileDirFromUserDataDir(expandedPath);
|
|
178
|
-
if (profileDirs.length === 0) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`No Chromium profile with a Cookies database was found under "${inputPath}".`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (profileDirs.length > 1) {
|
|
184
|
-
const candidates = profileDirs.map((entry) => basename(entry)).join(", ");
|
|
185
|
-
throw new Error(
|
|
186
|
-
`"${inputPath}" contains multiple Chromium profiles (${candidates}). Pass a specific profile directory such as "${profileDirs[0]}".`
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
const selectedProfileDir = profileDirs[0];
|
|
190
|
-
const cookieDbPath = resolveCookieDbPath(selectedProfileDir);
|
|
191
|
-
if (!cookieDbPath) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`No Chromium Cookies database was found for "${inputPath}".`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
userDataDir: expandedPath,
|
|
198
|
-
profileDir: selectedProfileDir,
|
|
199
|
-
profileDirectory: basename(selectedProfileDir),
|
|
200
|
-
cookieDbPath,
|
|
201
|
-
localStatePath
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
function resolvePersistentChromiumLaunchProfile(inputPath) {
|
|
205
|
-
const expandedPath = expandHome(inputPath.trim());
|
|
206
|
-
if (!expandedPath) {
|
|
207
|
-
return {
|
|
208
|
-
userDataDir: inputPath
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
if (fileExists(expandedPath) && basename(expandedPath) === "Cookies") {
|
|
212
|
-
const directParent = dirname(expandedPath);
|
|
213
|
-
const profileDir = basename(directParent) === "Network" ? dirname(directParent) : directParent;
|
|
214
|
-
return {
|
|
215
|
-
userDataDir: dirname(profileDir),
|
|
216
|
-
profileDirectory: basename(profileDir)
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
if (directoryExists(expandedPath) && resolveCookieDbPath(expandedPath) && fileExists(join2(dirname(expandedPath), "Local State"))) {
|
|
220
|
-
return {
|
|
221
|
-
userDataDir: dirname(expandedPath),
|
|
222
|
-
profileDirectory: basename(expandedPath)
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
return {
|
|
226
|
-
userDataDir: expandedPath
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
function detectChromiumBrand(location) {
|
|
230
|
-
const normalizedPath = location.userDataDir.toLowerCase();
|
|
231
|
-
for (const candidate of CHROMIUM_BRANDS) {
|
|
232
|
-
if (candidate.match.every((fragment) => normalizedPath.includes(fragment))) {
|
|
233
|
-
return candidate.brand;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return DEFAULT_CHROMIUM_BRAND;
|
|
237
|
-
}
|
|
238
|
-
async function createSqliteSnapshot(dbPath) {
|
|
239
|
-
const snapshotDir = await mkdtemp(join2(tmpdir(), "opensteer-cookie-db-"));
|
|
240
|
-
const snapshotPath = join2(snapshotDir, "Cookies");
|
|
241
|
-
await copyFile(dbPath, snapshotPath);
|
|
242
|
-
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
243
|
-
const source = `${dbPath}${suffix}`;
|
|
244
|
-
if (!existsSync(source)) {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
await copyFile(source, `${snapshotPath}${suffix}`);
|
|
248
|
-
}
|
|
249
|
-
return {
|
|
250
|
-
snapshotPath,
|
|
251
|
-
cleanup: async () => {
|
|
252
|
-
await rm(snapshotDir, { recursive: true, force: true });
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
async function querySqliteJson(dbPath, query) {
|
|
257
|
-
const result = await execFileAsync("sqlite3", ["-json", dbPath, query], {
|
|
258
|
-
encoding: "utf8",
|
|
259
|
-
maxBuffer: 64 * 1024 * 1024
|
|
260
|
-
});
|
|
261
|
-
const stdout = result.stdout;
|
|
262
|
-
const trimmed = stdout.trim();
|
|
263
|
-
if (!trimmed) {
|
|
264
|
-
return [];
|
|
265
|
-
}
|
|
266
|
-
return JSON.parse(trimmed);
|
|
267
|
-
}
|
|
268
|
-
function convertChromiumTimestampToUnixSeconds(value) {
|
|
269
|
-
if (!value || value === "0") {
|
|
270
|
-
return -1;
|
|
271
|
-
}
|
|
272
|
-
const micros = BigInt(value);
|
|
273
|
-
if (micros <= CHROMIUM_EPOCH_MICROS) {
|
|
274
|
-
return -1;
|
|
275
|
-
}
|
|
276
|
-
return Number((micros - CHROMIUM_EPOCH_MICROS) / 1000000n);
|
|
277
|
-
}
|
|
278
|
-
function mapChromiumSameSite(value) {
|
|
279
|
-
if (value === 2) {
|
|
280
|
-
return "Strict";
|
|
281
|
-
}
|
|
282
|
-
if (value === 0) {
|
|
283
|
-
return "None";
|
|
284
|
-
}
|
|
285
|
-
return "Lax";
|
|
286
|
-
}
|
|
287
|
-
function stripChromiumPadding(buffer) {
|
|
288
|
-
const paddingLength = buffer[buffer.length - 1];
|
|
289
|
-
if (paddingLength <= 0 || paddingLength > AES_BLOCK_BYTES) {
|
|
290
|
-
return buffer;
|
|
291
|
-
}
|
|
292
|
-
return buffer.subarray(0, buffer.length - paddingLength);
|
|
293
|
-
}
|
|
294
|
-
function stripDomainHashPrefix(buffer, hostKey) {
|
|
295
|
-
if (buffer.length < 32) {
|
|
296
|
-
return buffer;
|
|
297
|
-
}
|
|
298
|
-
const domainHash = createHash("sha256").update(hostKey, "utf8").digest();
|
|
299
|
-
if (buffer.subarray(0, 32).equals(domainHash)) {
|
|
300
|
-
return buffer.subarray(32);
|
|
301
|
-
}
|
|
302
|
-
return buffer;
|
|
303
|
-
}
|
|
304
|
-
function decryptChromiumAes128CbcValue(encryptedValue, key, hostKey) {
|
|
305
|
-
const ciphertext = encryptedValue.length > 3 && encryptedValue[0] === 118 && encryptedValue[1] === 49 && (encryptedValue[2] === 48 || encryptedValue[2] === 49) ? encryptedValue.subarray(3) : encryptedValue;
|
|
306
|
-
const iv = Buffer.alloc(AES_BLOCK_BYTES, " ");
|
|
307
|
-
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
308
|
-
const plaintext = Buffer.concat([
|
|
309
|
-
decipher.update(ciphertext),
|
|
310
|
-
decipher.final()
|
|
311
|
-
]);
|
|
312
|
-
return stripDomainHashPrefix(stripChromiumPadding(plaintext), hostKey).toString(
|
|
313
|
-
"utf8"
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
function decryptChromiumAes256GcmValue(encryptedValue, key) {
|
|
317
|
-
const nonce = encryptedValue.subarray(3, 15);
|
|
318
|
-
const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16);
|
|
319
|
-
const authTag = encryptedValue.subarray(encryptedValue.length - 16);
|
|
320
|
-
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
321
|
-
decipher.setAuthTag(authTag);
|
|
322
|
-
return Buffer.concat([
|
|
323
|
-
decipher.update(ciphertext),
|
|
324
|
-
decipher.final()
|
|
325
|
-
]).toString("utf8");
|
|
326
|
-
}
|
|
327
|
-
async function dpapiUnprotect(buffer) {
|
|
328
|
-
const script = [
|
|
329
|
-
`$inputBytes = [Convert]::FromBase64String('${buffer.toString("base64")}')`,
|
|
330
|
-
"$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(",
|
|
331
|
-
" $inputBytes,",
|
|
332
|
-
" $null,",
|
|
333
|
-
" [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
334
|
-
")",
|
|
335
|
-
"[Convert]::ToBase64String($plainBytes)"
|
|
336
|
-
].join("\n");
|
|
337
|
-
const { stdout } = await execFileAsync(
|
|
338
|
-
"powershell.exe",
|
|
339
|
-
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
340
|
-
{
|
|
341
|
-
encoding: "utf8",
|
|
342
|
-
maxBuffer: 8 * 1024 * 1024
|
|
343
|
-
}
|
|
344
|
-
);
|
|
345
|
-
return Buffer.from(stdout.trim(), "base64");
|
|
346
|
-
}
|
|
347
|
-
async function buildChromiumDecryptor(location) {
|
|
348
|
-
if (process.platform === "darwin") {
|
|
349
|
-
const brand = detectChromiumBrand(location);
|
|
350
|
-
const keychainStore = createKeychainStore();
|
|
351
|
-
const password = keychainStore?.get(brand.macService, brand.macAccount) ?? null;
|
|
352
|
-
if (!password) {
|
|
353
|
-
throw new Error(
|
|
354
|
-
`Unable to read ${brand.macService} from macOS Keychain.`
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
const key = pbkdf2Sync(password, KEY_SALT, MAC_KEY_ITERATIONS, KEY_LENGTH, "sha1");
|
|
358
|
-
return async (row) => decryptChromiumAes128CbcValue(
|
|
359
|
-
Buffer.from(row.encrypted_value || "", "hex"),
|
|
360
|
-
key,
|
|
361
|
-
row.host_key
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
if (process.platform === "linux") {
|
|
365
|
-
const brand = detectChromiumBrand(location);
|
|
366
|
-
const keychainStore = createKeychainStore();
|
|
367
|
-
const password = keychainStore?.get(brand.macService, brand.macAccount) ?? brand.linuxApplications.map((application) => keychainStore?.get(application, application) ?? null).find(Boolean) ?? null;
|
|
368
|
-
const key = pbkdf2Sync(
|
|
369
|
-
password || "peanuts",
|
|
370
|
-
KEY_SALT,
|
|
371
|
-
LINUX_KEY_ITERATIONS,
|
|
372
|
-
KEY_LENGTH,
|
|
373
|
-
"sha1"
|
|
374
|
-
);
|
|
375
|
-
return async (row) => decryptChromiumAes128CbcValue(
|
|
376
|
-
Buffer.from(row.encrypted_value || "", "hex"),
|
|
377
|
-
key,
|
|
378
|
-
row.host_key
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
if (process.platform === "win32") {
|
|
382
|
-
if (!location.localStatePath) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
`Unable to locate Chromium Local State for profile: ${location.profileDir}`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
const localState = JSON.parse(
|
|
388
|
-
await readFile(location.localStatePath, "utf8")
|
|
389
|
-
);
|
|
390
|
-
const encryptedKeyBase64 = localState.os_crypt?.encrypted_key;
|
|
391
|
-
if (!encryptedKeyBase64) {
|
|
392
|
-
throw new Error(
|
|
393
|
-
`Local State did not include os_crypt.encrypted_key for ${location.userDataDir}`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
const encryptedKey = Buffer.from(encryptedKeyBase64, "base64");
|
|
397
|
-
const masterKey = await dpapiUnprotect(encryptedKey.subarray(5));
|
|
398
|
-
return async (row) => {
|
|
399
|
-
const encryptedValue = Buffer.from(row.encrypted_value || "", "hex");
|
|
400
|
-
if (encryptedValue.length > 4 && encryptedValue[0] === 1 && encryptedValue[1] === 0 && encryptedValue[2] === 0 && encryptedValue[3] === 0) {
|
|
401
|
-
const decrypted = await dpapiUnprotect(encryptedValue);
|
|
402
|
-
return decrypted.toString("utf8");
|
|
403
|
-
}
|
|
404
|
-
return decryptChromiumAes256GcmValue(encryptedValue, masterKey);
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
throw new Error(
|
|
408
|
-
`Local Chromium cookie sync is not supported on ${process.platform}.`
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
function buildPlaywrightCookie(row, value) {
|
|
412
|
-
if (!row.name.trim()) {
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
if (!row.host_key.trim()) {
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
const expires = row.has_expires === 1 ? convertChromiumTimestampToUnixSeconds(row.expires_utc) : -1;
|
|
419
|
-
if (expires !== -1 && expires <= Math.floor(Date.now() / 1e3)) {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
return {
|
|
423
|
-
name: row.name,
|
|
424
|
-
value,
|
|
425
|
-
domain: row.host_key,
|
|
426
|
-
path: row.path || "/",
|
|
427
|
-
expires,
|
|
428
|
-
httpOnly: row.is_httponly === 1,
|
|
429
|
-
secure: row.is_secure === 1,
|
|
430
|
-
sameSite: mapChromiumSameSite(row.samesite)
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
async function loadCookiesFromLocalProfileDir(inputPath, options = {}) {
|
|
434
|
-
const location = await resolveChromiumProfileLocation(inputPath);
|
|
435
|
-
try {
|
|
436
|
-
return await loadCookiesFromSqlite(location);
|
|
437
|
-
} catch (error) {
|
|
438
|
-
if (!isMissingSqliteBinary(error)) {
|
|
439
|
-
throw error;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return await loadCookiesFromBrowserSnapshot(location, options);
|
|
443
|
-
}
|
|
444
|
-
async function loadCookiesFromSqlite(location) {
|
|
445
|
-
const snapshot = await createSqliteSnapshot(location.cookieDbPath);
|
|
446
|
-
try {
|
|
447
|
-
const rows = await querySqliteJson(
|
|
448
|
-
snapshot.snapshotPath,
|
|
449
|
-
[
|
|
450
|
-
"SELECT",
|
|
451
|
-
" host_key,",
|
|
452
|
-
" name,",
|
|
453
|
-
" value,",
|
|
454
|
-
" hex(encrypted_value) AS encrypted_value,",
|
|
455
|
-
" path,",
|
|
456
|
-
" CAST(expires_utc AS TEXT) AS expires_utc,",
|
|
457
|
-
" is_secure,",
|
|
458
|
-
" is_httponly,",
|
|
459
|
-
" has_expires,",
|
|
460
|
-
" samesite",
|
|
461
|
-
"FROM cookies"
|
|
462
|
-
].join(" ")
|
|
463
|
-
);
|
|
464
|
-
const decryptValue = await buildChromiumDecryptor(location);
|
|
465
|
-
const cookies = [];
|
|
466
|
-
for (const row of rows) {
|
|
467
|
-
let value = row.value || "";
|
|
468
|
-
if (!value && row.encrypted_value) {
|
|
469
|
-
value = await decryptValue(row);
|
|
470
|
-
}
|
|
471
|
-
const cookie = buildPlaywrightCookie(row, value);
|
|
472
|
-
if (cookie) {
|
|
473
|
-
cookies.push(cookie);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return cookies;
|
|
477
|
-
} finally {
|
|
478
|
-
await snapshot.cleanup();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
async function loadCookiesFromBrowserSnapshot(location, options) {
|
|
482
|
-
const snapshotRootDir = await mkdtemp(join2(tmpdir(), "opensteer-profile-"));
|
|
483
|
-
const snapshotProfileDir = join2(
|
|
484
|
-
snapshotRootDir,
|
|
485
|
-
basename(location.profileDir)
|
|
486
|
-
);
|
|
487
|
-
let context = null;
|
|
488
|
-
try {
|
|
489
|
-
await cp(location.profileDir, snapshotProfileDir, {
|
|
490
|
-
recursive: true
|
|
491
|
-
});
|
|
492
|
-
if (location.localStatePath) {
|
|
493
|
-
await copyFile(location.localStatePath, join2(snapshotRootDir, "Local State"));
|
|
494
|
-
}
|
|
495
|
-
const brand = detectChromiumBrand(location);
|
|
496
|
-
const args = [`--profile-directory=${basename(snapshotProfileDir)}`];
|
|
497
|
-
context = await chromium.launchPersistentContext(snapshotRootDir, {
|
|
498
|
-
channel: brand.playwrightChannel,
|
|
499
|
-
headless: options.headless ?? true,
|
|
500
|
-
timeout: options.timeout ?? 12e4,
|
|
501
|
-
args
|
|
502
|
-
});
|
|
503
|
-
return await context.cookies();
|
|
504
|
-
} finally {
|
|
505
|
-
await context?.close().catch(() => void 0);
|
|
506
|
-
await rm(snapshotRootDir, { recursive: true, force: true });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
function isMissingSqliteBinary(error) {
|
|
510
|
-
return Boolean(
|
|
511
|
-
error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
25
|
// src/navigation.ts
|
|
516
26
|
var DEFAULT_TIMEOUT = 3e4;
|
|
517
27
|
var DEFAULT_SETTLE_MS = 750;
|
|
@@ -5293,7 +4803,7 @@ async function closeTab(context, activePage, index) {
|
|
|
5293
4803
|
}
|
|
5294
4804
|
|
|
5295
4805
|
// src/actions/cookies.ts
|
|
5296
|
-
import { readFile
|
|
4806
|
+
import { readFile, writeFile } from "fs/promises";
|
|
5297
4807
|
async function getCookies(context, url) {
|
|
5298
4808
|
return context.cookies(url ? [url] : void 0);
|
|
5299
4809
|
}
|
|
@@ -5308,7 +4818,7 @@ async function exportCookies(context, filePath, url) {
|
|
|
5308
4818
|
await writeFile(filePath, JSON.stringify(cookies, null, 2), "utf-8");
|
|
5309
4819
|
}
|
|
5310
4820
|
async function importCookies(context, filePath) {
|
|
5311
|
-
const raw = await
|
|
4821
|
+
const raw = await readFile(filePath, "utf-8");
|
|
5312
4822
|
const cookies = JSON.parse(raw);
|
|
5313
4823
|
await context.addCookies(cookies);
|
|
5314
4824
|
}
|
|
@@ -8001,11 +7511,17 @@ function sleep4(ms) {
|
|
|
8001
7511
|
}
|
|
8002
7512
|
|
|
8003
7513
|
// src/opensteer.ts
|
|
8004
|
-
import { createHash
|
|
7514
|
+
import { createHash, randomUUID } from "crypto";
|
|
8005
7515
|
|
|
8006
7516
|
// src/browser/pool.ts
|
|
7517
|
+
import { spawn } from "child_process";
|
|
7518
|
+
import { existsSync } from "fs";
|
|
7519
|
+
import { copyFile, cp, mkdir, mkdtemp, rm } from "fs/promises";
|
|
7520
|
+
import { createServer } from "net";
|
|
7521
|
+
import { tmpdir } from "os";
|
|
7522
|
+
import { join } from "path";
|
|
8007
7523
|
import {
|
|
8008
|
-
chromium
|
|
7524
|
+
chromium
|
|
8009
7525
|
} from "playwright";
|
|
8010
7526
|
|
|
8011
7527
|
// src/browser/cdp-proxy.ts
|
|
@@ -8348,132 +7864,184 @@ function errorMessage(error) {
|
|
|
8348
7864
|
// src/browser/pool.ts
|
|
8349
7865
|
var BrowserPool = class {
|
|
8350
7866
|
browser = null;
|
|
8351
|
-
persistentContext = null;
|
|
8352
7867
|
cdpProxy = null;
|
|
7868
|
+
launchedProcess = null;
|
|
7869
|
+
tempUserDataDir = null;
|
|
8353
7870
|
defaults;
|
|
8354
7871
|
constructor(defaults = {}) {
|
|
8355
7872
|
this.defaults = defaults;
|
|
8356
7873
|
}
|
|
8357
7874
|
async launch(options = {}) {
|
|
8358
|
-
if (this.browser || this.cdpProxy) {
|
|
7875
|
+
if (this.browser || this.cdpProxy || this.launchedProcess || this.tempUserDataDir) {
|
|
8359
7876
|
await this.close();
|
|
8360
7877
|
}
|
|
8361
|
-
const
|
|
8362
|
-
const
|
|
8363
|
-
const
|
|
8364
|
-
|
|
8365
|
-
|
|
7878
|
+
const mode = options.mode ?? this.defaults.mode ?? "chromium";
|
|
7879
|
+
const cdpUrl = options.cdpUrl ?? this.defaults.cdpUrl;
|
|
7880
|
+
const userDataDir = options.userDataDir ?? this.defaults.userDataDir;
|
|
7881
|
+
const profileDirectory = options.profileDirectory ?? this.defaults.profileDirectory;
|
|
7882
|
+
const executablePath = options.executablePath ?? this.defaults.executablePath;
|
|
7883
|
+
if (cdpUrl) {
|
|
7884
|
+
if (mode === "real") {
|
|
7885
|
+
throw new Error(
|
|
7886
|
+
'cdpUrl cannot be combined with mode "real". Use one browser launch path at a time.'
|
|
7887
|
+
);
|
|
7888
|
+
}
|
|
7889
|
+
if (userDataDir || profileDirectory) {
|
|
7890
|
+
throw new Error(
|
|
7891
|
+
"userDataDir/profileDirectory cannot be combined with cdpUrl."
|
|
7892
|
+
);
|
|
7893
|
+
}
|
|
7894
|
+
if (options.context && Object.keys(options.context).length > 0) {
|
|
7895
|
+
throw new Error(
|
|
7896
|
+
"context launch options are not supported when attaching over CDP."
|
|
7897
|
+
);
|
|
7898
|
+
}
|
|
7899
|
+
return this.connectToRunning(cdpUrl, options.timeout);
|
|
8366
7900
|
}
|
|
8367
|
-
if (
|
|
8368
|
-
|
|
7901
|
+
if (mode !== "real" && (userDataDir || profileDirectory)) {
|
|
7902
|
+
throw new Error(
|
|
7903
|
+
'userDataDir/profileDirectory require mode "real".'
|
|
7904
|
+
);
|
|
8369
7905
|
}
|
|
8370
|
-
if (
|
|
8371
|
-
|
|
7906
|
+
if (mode === "real") {
|
|
7907
|
+
if (options.context && Object.keys(options.context).length > 0) {
|
|
7908
|
+
throw new Error(
|
|
7909
|
+
"context launch options are not supported for real-browser mode."
|
|
7910
|
+
);
|
|
7911
|
+
}
|
|
7912
|
+
return this.launchOwnedRealBrowser({
|
|
7913
|
+
...options,
|
|
7914
|
+
executablePath,
|
|
7915
|
+
userDataDir,
|
|
7916
|
+
profileDirectory
|
|
7917
|
+
});
|
|
8372
7918
|
}
|
|
8373
7919
|
return this.launchSandbox(options);
|
|
8374
7920
|
}
|
|
8375
7921
|
async close() {
|
|
8376
7922
|
const browser = this.browser;
|
|
8377
|
-
const
|
|
7923
|
+
const cdpProxy = this.cdpProxy;
|
|
7924
|
+
const launchedProcess = this.launchedProcess;
|
|
7925
|
+
const tempUserDataDir = this.tempUserDataDir;
|
|
8378
7926
|
this.browser = null;
|
|
8379
|
-
this.
|
|
7927
|
+
this.cdpProxy = null;
|
|
7928
|
+
this.launchedProcess = null;
|
|
7929
|
+
this.tempUserDataDir = null;
|
|
8380
7930
|
try {
|
|
8381
|
-
if (
|
|
8382
|
-
await
|
|
8383
|
-
} else if (browser) {
|
|
8384
|
-
await browser.close();
|
|
7931
|
+
if (browser) {
|
|
7932
|
+
await browser.close().catch(() => void 0);
|
|
8385
7933
|
}
|
|
8386
7934
|
} finally {
|
|
8387
|
-
|
|
8388
|
-
|
|
7935
|
+
cdpProxy?.close();
|
|
7936
|
+
await killProcessTree(launchedProcess);
|
|
7937
|
+
if (tempUserDataDir) {
|
|
7938
|
+
await rm(tempUserDataDir, {
|
|
7939
|
+
recursive: true,
|
|
7940
|
+
force: true
|
|
7941
|
+
}).catch(() => void 0);
|
|
7942
|
+
}
|
|
8389
7943
|
}
|
|
8390
7944
|
}
|
|
8391
|
-
async connectToRunning(
|
|
8392
|
-
this.cdpProxy?.close();
|
|
8393
|
-
this.cdpProxy = null;
|
|
7945
|
+
async connectToRunning(cdpUrl, timeout) {
|
|
8394
7946
|
let browser = null;
|
|
7947
|
+
let cdpProxy = null;
|
|
8395
7948
|
try {
|
|
8396
|
-
const { browserWsUrl, targets } = await discoverTargets(
|
|
7949
|
+
const { browserWsUrl, targets } = await discoverTargets(cdpUrl);
|
|
8397
7950
|
if (targets.length === 0) {
|
|
8398
7951
|
throw new Error(
|
|
8399
7952
|
"No page targets found. Is the browser running with an open window?"
|
|
8400
7953
|
);
|
|
8401
7954
|
}
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
browser = await chromium2.connectOverCDP(proxyWsUrl, {
|
|
7955
|
+
cdpProxy = new CDPProxy(browserWsUrl, targets[0].id);
|
|
7956
|
+
const proxyWsUrl = await cdpProxy.start();
|
|
7957
|
+
browser = await chromium.connectOverCDP(proxyWsUrl, {
|
|
8406
7958
|
timeout: timeout ?? 3e4
|
|
8407
7959
|
});
|
|
8408
7960
|
this.browser = browser;
|
|
8409
|
-
this.
|
|
8410
|
-
const
|
|
8411
|
-
if (contexts.length === 0) {
|
|
8412
|
-
throw new Error(
|
|
8413
|
-
"Connection succeeded but no browser contexts found. Is the browser running with an open window?"
|
|
8414
|
-
);
|
|
8415
|
-
}
|
|
8416
|
-
const context = contexts[0];
|
|
8417
|
-
const pages = context.pages();
|
|
8418
|
-
const page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
7961
|
+
this.cdpProxy = cdpProxy;
|
|
7962
|
+
const { context, page } = await pickBrowserContextAndPage(browser);
|
|
8419
7963
|
return { browser, context, page, isExternal: true };
|
|
8420
7964
|
} catch (error) {
|
|
8421
7965
|
if (browser) {
|
|
8422
7966
|
await browser.close().catch(() => void 0);
|
|
8423
7967
|
}
|
|
7968
|
+
cdpProxy?.close();
|
|
8424
7969
|
this.browser = null;
|
|
8425
|
-
this.persistentContext = null;
|
|
8426
|
-
this.cdpProxy?.close();
|
|
8427
7970
|
this.cdpProxy = null;
|
|
8428
7971
|
throw error;
|
|
8429
7972
|
}
|
|
8430
7973
|
}
|
|
8431
|
-
async
|
|
8432
|
-
const
|
|
8433
|
-
const
|
|
8434
|
-
if (
|
|
8435
|
-
|
|
7974
|
+
async launchOwnedRealBrowser(options) {
|
|
7975
|
+
const chromePaths = detectChromePaths();
|
|
7976
|
+
const executablePath = options.executablePath ?? chromePaths.executable ?? void 0;
|
|
7977
|
+
if (!executablePath) {
|
|
7978
|
+
throw new Error(
|
|
7979
|
+
"Chrome was not found. Set browser.executablePath or install Chrome in a supported location."
|
|
7980
|
+
);
|
|
8436
7981
|
}
|
|
8437
|
-
const
|
|
8438
|
-
|
|
8439
|
-
{
|
|
8440
|
-
channel,
|
|
8441
|
-
headless: options.headless ?? this.defaults.headless,
|
|
8442
|
-
executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
|
|
8443
|
-
slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
|
|
8444
|
-
timeout: options.timeout,
|
|
8445
|
-
...options.context || {},
|
|
8446
|
-
args
|
|
8447
|
-
}
|
|
7982
|
+
const sourceUserDataDir = expandHome(
|
|
7983
|
+
options.userDataDir ?? chromePaths.defaultUserDataDir
|
|
8448
7984
|
);
|
|
8449
|
-
const
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
|
|
8466
|
-
timeout: options.timeout
|
|
7985
|
+
const profileDirectory = options.profileDirectory ?? "Default";
|
|
7986
|
+
const tempUserDataDir = await cloneProfileToTempDir(
|
|
7987
|
+
sourceUserDataDir,
|
|
7988
|
+
profileDirectory
|
|
7989
|
+
);
|
|
7990
|
+
const debugPort = await reserveDebugPort();
|
|
7991
|
+
const headless = resolveLaunchHeadless(
|
|
7992
|
+
"real",
|
|
7993
|
+
options.headless,
|
|
7994
|
+
this.defaults.headless
|
|
7995
|
+
);
|
|
7996
|
+
const launchArgs = buildRealBrowserLaunchArgs({
|
|
7997
|
+
userDataDir: tempUserDataDir,
|
|
7998
|
+
profileDirectory,
|
|
7999
|
+
debugPort,
|
|
8000
|
+
headless
|
|
8467
8001
|
});
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8002
|
+
const processHandle = spawn(executablePath, launchArgs, {
|
|
8003
|
+
detached: process.platform !== "win32",
|
|
8004
|
+
stdio: "ignore"
|
|
8005
|
+
});
|
|
8006
|
+
processHandle.unref();
|
|
8007
|
+
let browser = null;
|
|
8008
|
+
try {
|
|
8009
|
+
const wsUrl = await resolveCdpWebSocketUrl(
|
|
8010
|
+
`http://127.0.0.1:${debugPort}`,
|
|
8011
|
+
options.timeout ?? 3e4
|
|
8012
|
+
);
|
|
8013
|
+
browser = await chromium.connectOverCDP(wsUrl, {
|
|
8014
|
+
timeout: options.timeout ?? 3e4
|
|
8015
|
+
});
|
|
8016
|
+
const { context, page } = await createOwnedBrowserContextAndPage(
|
|
8017
|
+
browser,
|
|
8018
|
+
{
|
|
8019
|
+
headless,
|
|
8020
|
+
initialUrl: options.initialUrl,
|
|
8021
|
+
timeoutMs: options.timeout ?? 3e4
|
|
8022
|
+
}
|
|
8023
|
+
);
|
|
8024
|
+
this.browser = browser;
|
|
8025
|
+
this.launchedProcess = processHandle;
|
|
8026
|
+
this.tempUserDataDir = tempUserDataDir;
|
|
8027
|
+
return { browser, context, page, isExternal: false };
|
|
8028
|
+
} catch (error) {
|
|
8029
|
+
await browser?.close().catch(() => void 0);
|
|
8030
|
+
await killProcessTree(processHandle);
|
|
8031
|
+
await rm(tempUserDataDir, {
|
|
8032
|
+
recursive: true,
|
|
8033
|
+
force: true
|
|
8034
|
+
}).catch(() => void 0);
|
|
8035
|
+
throw error;
|
|
8036
|
+
}
|
|
8473
8037
|
}
|
|
8474
8038
|
async launchSandbox(options) {
|
|
8475
|
-
const browser = await
|
|
8476
|
-
headless:
|
|
8039
|
+
const browser = await chromium.launch({
|
|
8040
|
+
headless: resolveLaunchHeadless(
|
|
8041
|
+
"chromium",
|
|
8042
|
+
options.headless,
|
|
8043
|
+
this.defaults.headless
|
|
8044
|
+
),
|
|
8477
8045
|
executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
|
|
8478
8046
|
slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
|
|
8479
8047
|
timeout: options.timeout
|
|
@@ -8481,10 +8049,268 @@ var BrowserPool = class {
|
|
|
8481
8049
|
const context = await browser.newContext(options.context || {});
|
|
8482
8050
|
const page = await context.newPage();
|
|
8483
8051
|
this.browser = browser;
|
|
8484
|
-
this.persistentContext = null;
|
|
8485
8052
|
return { browser, context, page, isExternal: false };
|
|
8486
8053
|
}
|
|
8487
8054
|
};
|
|
8055
|
+
async function pickBrowserContextAndPage(browser) {
|
|
8056
|
+
const context = getPrimaryBrowserContext(browser);
|
|
8057
|
+
const pages = context.pages();
|
|
8058
|
+
const page = pages.find((candidate) => isInspectablePageUrl2(candidate.url())) || pages[0] || await context.newPage();
|
|
8059
|
+
return { context, page };
|
|
8060
|
+
}
|
|
8061
|
+
function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
|
|
8062
|
+
if (requestedHeadless !== void 0) {
|
|
8063
|
+
return requestedHeadless;
|
|
8064
|
+
}
|
|
8065
|
+
if (defaultHeadless !== void 0) {
|
|
8066
|
+
return defaultHeadless;
|
|
8067
|
+
}
|
|
8068
|
+
return mode === "real";
|
|
8069
|
+
}
|
|
8070
|
+
async function createOwnedBrowserContextAndPage(browser, options) {
|
|
8071
|
+
const context = getPrimaryBrowserContext(browser);
|
|
8072
|
+
const page = await createOwnedBrowserPage(browser, context, options);
|
|
8073
|
+
return { context, page };
|
|
8074
|
+
}
|
|
8075
|
+
async function createOwnedBrowserPage(browser, context, options) {
|
|
8076
|
+
const targetUrl = options.initialUrl ?? "about:blank";
|
|
8077
|
+
const existingPages = new Set(context.pages());
|
|
8078
|
+
const browserSession = await browser.newBrowserCDPSession();
|
|
8079
|
+
try {
|
|
8080
|
+
const { targetId } = await browserSession.send("Target.createTarget", {
|
|
8081
|
+
url: targetUrl,
|
|
8082
|
+
newWindow: !options.headless
|
|
8083
|
+
});
|
|
8084
|
+
await browserSession.send("Target.activateTarget", { targetId }).catch(() => void 0);
|
|
8085
|
+
const page = await waitForOwnedBrowserPage(context, {
|
|
8086
|
+
existingPages,
|
|
8087
|
+
targetUrl,
|
|
8088
|
+
timeoutMs: options.timeoutMs
|
|
8089
|
+
});
|
|
8090
|
+
if (targetUrl !== "about:blank") {
|
|
8091
|
+
await page.waitForLoadState("domcontentloaded", {
|
|
8092
|
+
timeout: options.timeoutMs
|
|
8093
|
+
});
|
|
8094
|
+
}
|
|
8095
|
+
await closeDisposableStartupTargets(browserSession, targetId);
|
|
8096
|
+
return page;
|
|
8097
|
+
} finally {
|
|
8098
|
+
await browserSession.detach().catch(() => void 0);
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
async function closeDisposableStartupTargets(browserSession, preservedTargetId) {
|
|
8102
|
+
const response = await browserSession.send("Target.getTargets").catch(() => null);
|
|
8103
|
+
if (!response) {
|
|
8104
|
+
return;
|
|
8105
|
+
}
|
|
8106
|
+
for (const targetInfo of response.targetInfos) {
|
|
8107
|
+
if (targetInfo.targetId === preservedTargetId || targetInfo.type !== "page" || !isDisposableStartupPageUrl(targetInfo.url)) {
|
|
8108
|
+
continue;
|
|
8109
|
+
}
|
|
8110
|
+
await browserSession.send("Target.closeTarget", { targetId: targetInfo.targetId }).catch(() => void 0);
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
8113
|
+
async function waitForOwnedBrowserPage(context, options) {
|
|
8114
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
8115
|
+
let fallbackPage = null;
|
|
8116
|
+
while (Date.now() < deadline) {
|
|
8117
|
+
for (const candidate of context.pages()) {
|
|
8118
|
+
if (options.existingPages.has(candidate)) {
|
|
8119
|
+
continue;
|
|
8120
|
+
}
|
|
8121
|
+
const url = candidate.url();
|
|
8122
|
+
if (!isInspectablePageUrl2(url)) {
|
|
8123
|
+
continue;
|
|
8124
|
+
}
|
|
8125
|
+
fallbackPage ??= candidate;
|
|
8126
|
+
if (options.targetUrl === "about:blank") {
|
|
8127
|
+
return candidate;
|
|
8128
|
+
}
|
|
8129
|
+
if (pageLooselyMatchesUrl(url, options.targetUrl)) {
|
|
8130
|
+
return candidate;
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
await sleep5(100);
|
|
8134
|
+
}
|
|
8135
|
+
if (fallbackPage) {
|
|
8136
|
+
return fallbackPage;
|
|
8137
|
+
}
|
|
8138
|
+
throw new Error(
|
|
8139
|
+
`Chrome created a target for ${options.targetUrl}, but Playwright did not expose the page in time.`
|
|
8140
|
+
);
|
|
8141
|
+
}
|
|
8142
|
+
function getPrimaryBrowserContext(browser) {
|
|
8143
|
+
const contexts = browser.contexts();
|
|
8144
|
+
if (contexts.length === 0) {
|
|
8145
|
+
throw new Error(
|
|
8146
|
+
"Connection succeeded but no browser contexts were exposed."
|
|
8147
|
+
);
|
|
8148
|
+
}
|
|
8149
|
+
return contexts[0];
|
|
8150
|
+
}
|
|
8151
|
+
function isInspectablePageUrl2(url) {
|
|
8152
|
+
return url === "about:blank" || url.startsWith("http://") || url.startsWith("https://");
|
|
8153
|
+
}
|
|
8154
|
+
function isDisposableStartupPageUrl(url) {
|
|
8155
|
+
return url === "about:blank" || url === "chrome://newtab/" || url === "chrome://new-tab-page/";
|
|
8156
|
+
}
|
|
8157
|
+
function pageLooselyMatchesUrl(currentUrl, initialUrl) {
|
|
8158
|
+
try {
|
|
8159
|
+
const current = new URL(currentUrl);
|
|
8160
|
+
const requested = new URL(initialUrl);
|
|
8161
|
+
if (current.href === requested.href) {
|
|
8162
|
+
return true;
|
|
8163
|
+
}
|
|
8164
|
+
return current.hostname === requested.hostname && current.pathname === requested.pathname;
|
|
8165
|
+
} catch {
|
|
8166
|
+
return currentUrl === initialUrl;
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
function normalizeDiscoveryUrl(cdpUrl) {
|
|
8170
|
+
let parsed;
|
|
8171
|
+
try {
|
|
8172
|
+
parsed = new URL(cdpUrl);
|
|
8173
|
+
} catch {
|
|
8174
|
+
throw new Error(
|
|
8175
|
+
`Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
|
|
8176
|
+
);
|
|
8177
|
+
}
|
|
8178
|
+
if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
|
|
8179
|
+
return parsed;
|
|
8180
|
+
}
|
|
8181
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
8182
|
+
throw new Error(
|
|
8183
|
+
`Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
|
|
8184
|
+
);
|
|
8185
|
+
}
|
|
8186
|
+
const normalized = new URL(parsed.toString());
|
|
8187
|
+
normalized.pathname = "/json/version";
|
|
8188
|
+
normalized.search = "";
|
|
8189
|
+
normalized.hash = "";
|
|
8190
|
+
return normalized;
|
|
8191
|
+
}
|
|
8192
|
+
async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
|
|
8193
|
+
if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
|
|
8194
|
+
return cdpUrl;
|
|
8195
|
+
}
|
|
8196
|
+
const versionUrl = normalizeDiscoveryUrl(cdpUrl);
|
|
8197
|
+
const deadline = Date.now() + timeoutMs;
|
|
8198
|
+
let lastError = "CDP discovery did not respond.";
|
|
8199
|
+
while (Date.now() < deadline) {
|
|
8200
|
+
const remaining = Math.max(deadline - Date.now(), 1e3);
|
|
8201
|
+
try {
|
|
8202
|
+
const response = await fetch(versionUrl, {
|
|
8203
|
+
signal: AbortSignal.timeout(Math.min(remaining, 5e3))
|
|
8204
|
+
});
|
|
8205
|
+
if (!response.ok) {
|
|
8206
|
+
lastError = `${response.status} ${response.statusText}`;
|
|
8207
|
+
} else {
|
|
8208
|
+
const payload = await response.json();
|
|
8209
|
+
const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
|
|
8210
|
+
if (wsUrl && wsUrl.trim()) {
|
|
8211
|
+
return wsUrl;
|
|
8212
|
+
}
|
|
8213
|
+
lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
|
|
8214
|
+
}
|
|
8215
|
+
} catch (error) {
|
|
8216
|
+
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
8217
|
+
}
|
|
8218
|
+
await sleep5(100);
|
|
8219
|
+
}
|
|
8220
|
+
throw new Error(
|
|
8221
|
+
`Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
|
|
8222
|
+
);
|
|
8223
|
+
}
|
|
8224
|
+
async function reserveDebugPort() {
|
|
8225
|
+
return await new Promise((resolve, reject) => {
|
|
8226
|
+
const server = createServer();
|
|
8227
|
+
server.unref();
|
|
8228
|
+
server.on("error", reject);
|
|
8229
|
+
server.listen(0, "127.0.0.1", () => {
|
|
8230
|
+
const address = server.address();
|
|
8231
|
+
if (!address || typeof address === "string") {
|
|
8232
|
+
server.close();
|
|
8233
|
+
reject(new Error("Failed to reserve a local debug port."));
|
|
8234
|
+
return;
|
|
8235
|
+
}
|
|
8236
|
+
server.close((error) => {
|
|
8237
|
+
if (error) {
|
|
8238
|
+
reject(error);
|
|
8239
|
+
return;
|
|
8240
|
+
}
|
|
8241
|
+
resolve(address.port);
|
|
8242
|
+
});
|
|
8243
|
+
});
|
|
8244
|
+
});
|
|
8245
|
+
}
|
|
8246
|
+
async function cloneProfileToTempDir(userDataDir, profileDirectory) {
|
|
8247
|
+
const resolvedUserDataDir = expandHome(userDataDir);
|
|
8248
|
+
const tempUserDataDir = await mkdtemp(
|
|
8249
|
+
join(tmpdir(), "opensteer-real-browser-")
|
|
8250
|
+
);
|
|
8251
|
+
const sourceProfileDir = join(resolvedUserDataDir, profileDirectory);
|
|
8252
|
+
const targetProfileDir = join(tempUserDataDir, profileDirectory);
|
|
8253
|
+
if (existsSync(sourceProfileDir)) {
|
|
8254
|
+
await cp(sourceProfileDir, targetProfileDir, {
|
|
8255
|
+
recursive: true
|
|
8256
|
+
});
|
|
8257
|
+
} else {
|
|
8258
|
+
await mkdir(targetProfileDir, {
|
|
8259
|
+
recursive: true
|
|
8260
|
+
});
|
|
8261
|
+
}
|
|
8262
|
+
const localStatePath = join(resolvedUserDataDir, "Local State");
|
|
8263
|
+
if (existsSync(localStatePath)) {
|
|
8264
|
+
await copyFile(localStatePath, join(tempUserDataDir, "Local State"));
|
|
8265
|
+
}
|
|
8266
|
+
return tempUserDataDir;
|
|
8267
|
+
}
|
|
8268
|
+
function buildRealBrowserLaunchArgs(options) {
|
|
8269
|
+
const args = [
|
|
8270
|
+
`--user-data-dir=${options.userDataDir}`,
|
|
8271
|
+
`--profile-directory=${options.profileDirectory}`,
|
|
8272
|
+
`--remote-debugging-port=${options.debugPort}`,
|
|
8273
|
+
"--no-first-run",
|
|
8274
|
+
"--no-default-browser-check",
|
|
8275
|
+
"--disable-background-networking",
|
|
8276
|
+
"--disable-sync",
|
|
8277
|
+
"--disable-popup-blocking"
|
|
8278
|
+
];
|
|
8279
|
+
if (options.headless) {
|
|
8280
|
+
args.push("--headless=new");
|
|
8281
|
+
}
|
|
8282
|
+
return args;
|
|
8283
|
+
}
|
|
8284
|
+
async function killProcessTree(processHandle) {
|
|
8285
|
+
if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
|
|
8286
|
+
return;
|
|
8287
|
+
}
|
|
8288
|
+
if (process.platform === "win32") {
|
|
8289
|
+
await new Promise((resolve) => {
|
|
8290
|
+
const killer = spawn(
|
|
8291
|
+
"taskkill",
|
|
8292
|
+
["/pid", String(processHandle.pid), "/t", "/f"],
|
|
8293
|
+
{
|
|
8294
|
+
stdio: "ignore"
|
|
8295
|
+
}
|
|
8296
|
+
);
|
|
8297
|
+
killer.on("error", () => resolve());
|
|
8298
|
+
killer.on("exit", () => resolve());
|
|
8299
|
+
});
|
|
8300
|
+
return;
|
|
8301
|
+
}
|
|
8302
|
+
try {
|
|
8303
|
+
process.kill(-processHandle.pid, "SIGKILL");
|
|
8304
|
+
} catch {
|
|
8305
|
+
try {
|
|
8306
|
+
processHandle.kill("SIGKILL");
|
|
8307
|
+
} catch {
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
async function sleep5(ms) {
|
|
8312
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
8313
|
+
}
|
|
8488
8314
|
|
|
8489
8315
|
// src/action-wait.ts
|
|
8490
8316
|
var ROBUST_PROFILE = {
|
|
@@ -8679,7 +8505,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
8679
8505
|
this.idleSince = 0;
|
|
8680
8506
|
}
|
|
8681
8507
|
const remaining = Math.max(1, options.deadline - now);
|
|
8682
|
-
await
|
|
8508
|
+
await sleep6(Math.min(NETWORK_POLL_MS, remaining));
|
|
8683
8509
|
}
|
|
8684
8510
|
}
|
|
8685
8511
|
handleRequestStarted = (request) => {
|
|
@@ -8724,7 +8550,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
8724
8550
|
return false;
|
|
8725
8551
|
}
|
|
8726
8552
|
};
|
|
8727
|
-
async function
|
|
8553
|
+
async function sleep6(ms) {
|
|
8728
8554
|
await new Promise((resolve) => {
|
|
8729
8555
|
setTimeout(resolve, ms);
|
|
8730
8556
|
});
|
|
@@ -10081,30 +9907,20 @@ var Opensteer = class _Opensteer {
|
|
|
10081
9907
|
this.pool = new BrowserPool(resolved.browser || {});
|
|
10082
9908
|
if (cloudSelection.cloud) {
|
|
10083
9909
|
const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
|
|
10084
|
-
const
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
);
|
|
10090
|
-
}
|
|
10091
|
-
let credential = "";
|
|
10092
|
-
let authScheme = cloudConfig?.authScheme ?? "api-key";
|
|
10093
|
-
if (accessToken) {
|
|
10094
|
-
credential = accessToken;
|
|
10095
|
-
authScheme = "bearer";
|
|
10096
|
-
} else if (apiKey) {
|
|
10097
|
-
credential = apiKey;
|
|
10098
|
-
}
|
|
9910
|
+
const credential = selectCloudCredential({
|
|
9911
|
+
apiKey: cloudConfig?.apiKey,
|
|
9912
|
+
accessToken: cloudConfig?.accessToken,
|
|
9913
|
+
authScheme: cloudConfig?.authScheme
|
|
9914
|
+
});
|
|
10099
9915
|
if (!credential) {
|
|
10100
9916
|
throw new Error(
|
|
10101
9917
|
"Cloud mode requires credentials via cloud.apiKey/cloud.accessToken or OPENSTEER_API_KEY/OPENSTEER_ACCESS_TOKEN."
|
|
10102
9918
|
);
|
|
10103
9919
|
}
|
|
10104
9920
|
this.cloud = createCloudRuntimeState(
|
|
10105
|
-
credential,
|
|
9921
|
+
credential.token,
|
|
10106
9922
|
cloudConfig?.baseUrl,
|
|
10107
|
-
authScheme
|
|
9923
|
+
credential.authScheme
|
|
10108
9924
|
);
|
|
10109
9925
|
} else {
|
|
10110
9926
|
this.cloud = null;
|
|
@@ -10425,9 +10241,11 @@ var Opensteer = class _Opensteer {
|
|
|
10425
10241
|
}
|
|
10426
10242
|
const session = await this.pool.launch({
|
|
10427
10243
|
...options,
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10244
|
+
mode: options.mode ?? this.config.browser?.mode,
|
|
10245
|
+
cdpUrl: options.cdpUrl ?? this.config.browser?.cdpUrl,
|
|
10246
|
+
userDataDir: options.userDataDir ?? this.config.browser?.userDataDir,
|
|
10247
|
+
profileDirectory: options.profileDirectory ?? this.config.browser?.profileDirectory,
|
|
10248
|
+
executablePath: options.executablePath ?? this.config.browser?.executablePath
|
|
10431
10249
|
});
|
|
10432
10250
|
this.browser = session.browser;
|
|
10433
10251
|
this.contextRef = session.context;
|
|
@@ -10458,6 +10276,32 @@ var Opensteer = class _Opensteer {
|
|
|
10458
10276
|
instance.snapshotCache = null;
|
|
10459
10277
|
return instance;
|
|
10460
10278
|
}
|
|
10279
|
+
static listLocalProfiles(userDataDir) {
|
|
10280
|
+
return listLocalChromeProfiles(userDataDir);
|
|
10281
|
+
}
|
|
10282
|
+
static fromSystemChrome(browser = {}, config = {}) {
|
|
10283
|
+
const chromePaths = detectChromePaths();
|
|
10284
|
+
const executablePath = browser.executablePath ?? config.browser?.executablePath ?? chromePaths.executable ?? void 0;
|
|
10285
|
+
if (!executablePath) {
|
|
10286
|
+
throw new Error(
|
|
10287
|
+
"Chrome was not found. Pass executablePath explicitly or install Chrome in a supported location."
|
|
10288
|
+
);
|
|
10289
|
+
}
|
|
10290
|
+
const userDataDir = browser.userDataDir ?? config.browser?.userDataDir ?? chromePaths.defaultUserDataDir;
|
|
10291
|
+
const autoDetectedProfiles = listLocalChromeProfiles(userDataDir);
|
|
10292
|
+
const profileDirectory = browser.profileDirectory ?? config.browser?.profileDirectory ?? autoDetectedProfiles[0]?.directory ?? "Default";
|
|
10293
|
+
return new _Opensteer({
|
|
10294
|
+
...config,
|
|
10295
|
+
browser: {
|
|
10296
|
+
...config.browser || {},
|
|
10297
|
+
mode: "real",
|
|
10298
|
+
headless: browser.headless ?? config.browser?.headless ?? true,
|
|
10299
|
+
executablePath,
|
|
10300
|
+
userDataDir,
|
|
10301
|
+
profileDirectory
|
|
10302
|
+
}
|
|
10303
|
+
});
|
|
10304
|
+
}
|
|
10461
10305
|
async close() {
|
|
10462
10306
|
this.snapshotCache = null;
|
|
10463
10307
|
if (this.cloud) {
|
|
@@ -12490,7 +12334,7 @@ var Opensteer = class _Opensteer {
|
|
|
12490
12334
|
}
|
|
12491
12335
|
resolveStorageKey(description) {
|
|
12492
12336
|
if (!description) return null;
|
|
12493
|
-
return
|
|
12337
|
+
return createHash("sha256").update(description).digest("hex").slice(0, 16);
|
|
12494
12338
|
}
|
|
12495
12339
|
normalizePath(path3) {
|
|
12496
12340
|
return sanitizeElementPath(path3);
|
|
@@ -12562,7 +12406,7 @@ function normalizeExtractSource(source) {
|
|
|
12562
12406
|
}
|
|
12563
12407
|
function computeSchemaHash(schema) {
|
|
12564
12408
|
const stable = stableStringify(schema);
|
|
12565
|
-
return
|
|
12409
|
+
return createHash("sha256").update(stable).digest("hex");
|
|
12566
12410
|
}
|
|
12567
12411
|
function buildPathMap(fields) {
|
|
12568
12412
|
const out = {};
|
|
@@ -12822,8 +12666,6 @@ function buildLocalRunId(namespace) {
|
|
|
12822
12666
|
}
|
|
12823
12667
|
|
|
12824
12668
|
export {
|
|
12825
|
-
expandHome,
|
|
12826
|
-
loadCookiesFromLocalProfileDir,
|
|
12827
12669
|
waitForVisualStability,
|
|
12828
12670
|
createEmptyRegistry,
|
|
12829
12671
|
LocalSelectorStorage,
|