opensteer 0.6.3 → 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-SCNX4NN3.js → chunk-54KNQTOL.js} +141 -2
- package/dist/{chunk-FTKWQ6X3.js → chunk-6B6LOYU3.js} +1 -1
- package/dist/{chunk-3OMXCBPD.js → chunk-G6V2DJRN.js} +442 -591
- package/dist/chunk-K5CL76MG.js +81 -0
- package/dist/{chunk-KE35RQOJ.js → chunk-KPPOTU3D.js} +53 -144
- package/dist/cli/auth.cjs +53 -6
- 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 +2844 -2412
- 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 +649 -204
- package/dist/cli/server.js +69 -16
- package/dist/index.cjs +578 -185
- 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
package/dist/cli/profile.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Cookie } from 'playwright';
|
|
2
|
-
import { O as OpensteerAuthScheme, a as OpensteerConfig, C as CookieParam } from '../types-
|
|
3
|
-
import { B as BrowserProfileStatus, a as BrowserProfileListRequest, b as BrowserProfileListResponse, c as BrowserProfileCreateRequest, d as BrowserProfileDescriptor } from '../browser-profile-client-
|
|
2
|
+
import { O as OpensteerAuthScheme, a as OpensteerConfig, C as CookieParam } from '../types-BWItZPl_.cjs';
|
|
3
|
+
import { B as BrowserProfileStatus, a as BrowserProfileListRequest, b as BrowserProfileListResponse, c as BrowserProfileCreateRequest, d as BrowserProfileDescriptor } from '../browser-profile-client-D6PuRefA.cjs';
|
|
4
4
|
|
|
5
5
|
type ParsedProfileArgs = {
|
|
6
6
|
mode: 'help';
|
package/dist/cli/profile.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Cookie } from 'playwright';
|
|
2
|
-
import { O as OpensteerAuthScheme, a as OpensteerConfig, C as CookieParam } from '../types-
|
|
3
|
-
import { B as BrowserProfileStatus, a as BrowserProfileListRequest, b as BrowserProfileListResponse, c as BrowserProfileCreateRequest, d as BrowserProfileDescriptor } from '../browser-profile-client-
|
|
2
|
+
import { O as OpensteerAuthScheme, a as OpensteerConfig, C as CookieParam } from '../types-BWItZPl_.js';
|
|
3
|
+
import { B as BrowserProfileStatus, a as BrowserProfileListRequest, b as BrowserProfileListResponse, c as BrowserProfileCreateRequest, d as BrowserProfileDescriptor } from '../browser-profile-client-OUHaODro.js';
|
|
4
4
|
|
|
5
5
|
type ParsedProfileArgs = {
|
|
6
6
|
mode: 'help';
|
package/dist/cli/profile.js
CHANGED
|
@@ -1,23 +1,485 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BrowserProfileClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-6B6LOYU3.js";
|
|
4
4
|
import {
|
|
5
|
+
createKeychainStore,
|
|
5
6
|
ensureCloudCredentialsForCommand
|
|
6
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-54KNQTOL.js";
|
|
7
8
|
import {
|
|
8
|
-
Opensteer
|
|
9
|
-
|
|
10
|
-
loadCookiesFromLocalProfileDir
|
|
11
|
-
} from "../chunk-3OMXCBPD.js";
|
|
9
|
+
Opensteer
|
|
10
|
+
} from "../chunk-G6V2DJRN.js";
|
|
12
11
|
import {
|
|
13
12
|
resolveConfigWithEnv
|
|
14
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-KPPOTU3D.js";
|
|
14
|
+
import {
|
|
15
|
+
expandHome
|
|
16
|
+
} from "../chunk-K5CL76MG.js";
|
|
15
17
|
import "../chunk-3H5RRIMZ.js";
|
|
16
18
|
|
|
17
19
|
// src/cli/profile.ts
|
|
18
20
|
import path from "path";
|
|
19
21
|
import { createInterface } from "readline/promises";
|
|
20
22
|
|
|
23
|
+
// src/browser/chromium-profile.ts
|
|
24
|
+
import { promisify } from "util";
|
|
25
|
+
import { execFile } from "child_process";
|
|
26
|
+
import { createDecipheriv, createHash, pbkdf2Sync } from "crypto";
|
|
27
|
+
import {
|
|
28
|
+
cp,
|
|
29
|
+
copyFile,
|
|
30
|
+
mkdtemp,
|
|
31
|
+
readdir,
|
|
32
|
+
readFile,
|
|
33
|
+
rm
|
|
34
|
+
} from "fs/promises";
|
|
35
|
+
import { existsSync, statSync } from "fs";
|
|
36
|
+
import { basename, dirname, join } from "path";
|
|
37
|
+
import { tmpdir } from "os";
|
|
38
|
+
import { chromium } from "playwright";
|
|
39
|
+
var execFileAsync = promisify(execFile);
|
|
40
|
+
var CHROMIUM_EPOCH_MICROS = 11644473600000000n;
|
|
41
|
+
var AES_BLOCK_BYTES = 16;
|
|
42
|
+
var MAC_KEY_ITERATIONS = 1003;
|
|
43
|
+
var LINUX_KEY_ITERATIONS = 1;
|
|
44
|
+
var KEY_LENGTH = 16;
|
|
45
|
+
var KEY_SALT = "saltysalt";
|
|
46
|
+
var DEFAULT_CHROMIUM_BRAND = {
|
|
47
|
+
macService: "Chrome Safe Storage",
|
|
48
|
+
macAccount: "Chrome",
|
|
49
|
+
linuxApplications: ["chrome", "google-chrome"]
|
|
50
|
+
};
|
|
51
|
+
var CHROMIUM_BRANDS = [
|
|
52
|
+
{
|
|
53
|
+
match: ["bravesoftware", "brave-browser"],
|
|
54
|
+
brand: {
|
|
55
|
+
macService: "Brave Safe Storage",
|
|
56
|
+
macAccount: "Brave",
|
|
57
|
+
linuxApplications: ["brave-browser", "brave"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
match: ["microsoft", "edge"],
|
|
62
|
+
brand: {
|
|
63
|
+
macService: "Microsoft Edge Safe Storage",
|
|
64
|
+
macAccount: "Microsoft Edge",
|
|
65
|
+
linuxApplications: ["microsoft-edge"],
|
|
66
|
+
playwrightChannel: "msedge"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
match: ["google", "chrome beta"],
|
|
71
|
+
brand: {
|
|
72
|
+
macService: "Chrome Beta Safe Storage",
|
|
73
|
+
macAccount: "Chrome Beta",
|
|
74
|
+
linuxApplications: ["chrome-beta"],
|
|
75
|
+
playwrightChannel: "chrome-beta"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
match: ["google", "chrome"],
|
|
80
|
+
brand: {
|
|
81
|
+
macService: "Chrome Safe Storage",
|
|
82
|
+
macAccount: "Chrome",
|
|
83
|
+
linuxApplications: ["chrome", "google-chrome"],
|
|
84
|
+
playwrightChannel: "chrome"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
match: ["chromium"],
|
|
89
|
+
brand: {
|
|
90
|
+
macService: "Chromium Safe Storage",
|
|
91
|
+
macAccount: "Chromium",
|
|
92
|
+
linuxApplications: ["chromium"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
function directoryExists(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
return statSync(filePath).isDirectory();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function fileExists(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
return statSync(filePath).isFile();
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resolveCookieDbPath(profileDir) {
|
|
111
|
+
const candidates = [join(profileDir, "Network", "Cookies"), join(profileDir, "Cookies")];
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
if (fileExists(candidate)) {
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
async function selectProfileDirFromUserDataDir(userDataDir) {
|
|
120
|
+
const entries = await readdir(userDataDir, {
|
|
121
|
+
withFileTypes: true
|
|
122
|
+
}).catch(() => []);
|
|
123
|
+
const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => join(userDataDir, entry.name)).filter((entryPath) => resolveCookieDbPath(entryPath));
|
|
124
|
+
return candidates;
|
|
125
|
+
}
|
|
126
|
+
async function resolveChromiumProfileLocation(inputPath) {
|
|
127
|
+
const expandedPath = expandHome(inputPath.trim());
|
|
128
|
+
if (!expandedPath) {
|
|
129
|
+
throw new Error("Profile path cannot be empty.");
|
|
130
|
+
}
|
|
131
|
+
if (fileExists(expandedPath) && basename(expandedPath) === "Cookies") {
|
|
132
|
+
const directParent = dirname(expandedPath);
|
|
133
|
+
const profileDir = basename(directParent) === "Network" ? dirname(directParent) : directParent;
|
|
134
|
+
const userDataDir = dirname(profileDir);
|
|
135
|
+
return {
|
|
136
|
+
userDataDir,
|
|
137
|
+
profileDir,
|
|
138
|
+
profileDirectory: basename(profileDir),
|
|
139
|
+
cookieDbPath: expandedPath,
|
|
140
|
+
localStatePath: fileExists(join(userDataDir, "Local State")) ? join(userDataDir, "Local State") : null
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (fileExists(expandedPath)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!directoryExists(expandedPath)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Could not find a Chromium profile at "${inputPath}".`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const directCookieDb = resolveCookieDbPath(expandedPath);
|
|
154
|
+
if (directCookieDb) {
|
|
155
|
+
const userDataDir = dirname(expandedPath);
|
|
156
|
+
return {
|
|
157
|
+
userDataDir,
|
|
158
|
+
profileDir: expandedPath,
|
|
159
|
+
profileDirectory: basename(expandedPath),
|
|
160
|
+
cookieDbPath: directCookieDb,
|
|
161
|
+
localStatePath: fileExists(join(userDataDir, "Local State")) ? join(userDataDir, "Local State") : null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const localStatePath = join(expandedPath, "Local State");
|
|
165
|
+
if (!fileExists(localStatePath)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const profileDirs = await selectProfileDirFromUserDataDir(expandedPath);
|
|
171
|
+
if (profileDirs.length === 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`No Chromium profile with a Cookies database was found under "${inputPath}".`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (profileDirs.length > 1) {
|
|
177
|
+
const candidates = profileDirs.map((entry) => basename(entry)).join(", ");
|
|
178
|
+
throw new Error(
|
|
179
|
+
`"${inputPath}" contains multiple Chromium profiles (${candidates}). Pass a specific profile directory such as "${profileDirs[0]}".`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const selectedProfileDir = profileDirs[0];
|
|
183
|
+
const cookieDbPath = resolveCookieDbPath(selectedProfileDir);
|
|
184
|
+
if (!cookieDbPath) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`No Chromium Cookies database was found for "${inputPath}".`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
userDataDir: expandedPath,
|
|
191
|
+
profileDir: selectedProfileDir,
|
|
192
|
+
profileDirectory: basename(selectedProfileDir),
|
|
193
|
+
cookieDbPath,
|
|
194
|
+
localStatePath
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function detectChromiumBrand(location) {
|
|
198
|
+
const normalizedPath = location.userDataDir.toLowerCase();
|
|
199
|
+
for (const candidate of CHROMIUM_BRANDS) {
|
|
200
|
+
if (candidate.match.every((fragment) => normalizedPath.includes(fragment))) {
|
|
201
|
+
return candidate.brand;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return DEFAULT_CHROMIUM_BRAND;
|
|
205
|
+
}
|
|
206
|
+
async function createSqliteSnapshot(dbPath) {
|
|
207
|
+
const snapshotDir = await mkdtemp(join(tmpdir(), "opensteer-cookie-db-"));
|
|
208
|
+
const snapshotPath = join(snapshotDir, "Cookies");
|
|
209
|
+
await copyFile(dbPath, snapshotPath);
|
|
210
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
211
|
+
const source = `${dbPath}${suffix}`;
|
|
212
|
+
if (!existsSync(source)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
await copyFile(source, `${snapshotPath}${suffix}`);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
snapshotPath,
|
|
219
|
+
cleanup: async () => {
|
|
220
|
+
await rm(snapshotDir, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function querySqliteJson(dbPath, query) {
|
|
225
|
+
const result = await execFileAsync("sqlite3", ["-json", dbPath, query], {
|
|
226
|
+
encoding: "utf8",
|
|
227
|
+
maxBuffer: 64 * 1024 * 1024
|
|
228
|
+
});
|
|
229
|
+
const stdout = result.stdout;
|
|
230
|
+
const trimmed = stdout.trim();
|
|
231
|
+
if (!trimmed) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
return JSON.parse(trimmed);
|
|
235
|
+
}
|
|
236
|
+
function convertChromiumTimestampToUnixSeconds(value) {
|
|
237
|
+
if (!value || value === "0") {
|
|
238
|
+
return -1;
|
|
239
|
+
}
|
|
240
|
+
const micros = BigInt(value);
|
|
241
|
+
if (micros <= CHROMIUM_EPOCH_MICROS) {
|
|
242
|
+
return -1;
|
|
243
|
+
}
|
|
244
|
+
return Number((micros - CHROMIUM_EPOCH_MICROS) / 1000000n);
|
|
245
|
+
}
|
|
246
|
+
function mapChromiumSameSite(value) {
|
|
247
|
+
if (value === 2) {
|
|
248
|
+
return "Strict";
|
|
249
|
+
}
|
|
250
|
+
if (value === 0) {
|
|
251
|
+
return "None";
|
|
252
|
+
}
|
|
253
|
+
return "Lax";
|
|
254
|
+
}
|
|
255
|
+
function stripChromiumPadding(buffer) {
|
|
256
|
+
const paddingLength = buffer[buffer.length - 1];
|
|
257
|
+
if (paddingLength <= 0 || paddingLength > AES_BLOCK_BYTES) {
|
|
258
|
+
return buffer;
|
|
259
|
+
}
|
|
260
|
+
return buffer.subarray(0, buffer.length - paddingLength);
|
|
261
|
+
}
|
|
262
|
+
function stripDomainHashPrefix(buffer, hostKey) {
|
|
263
|
+
if (buffer.length < 32) {
|
|
264
|
+
return buffer;
|
|
265
|
+
}
|
|
266
|
+
const domainHash = createHash("sha256").update(hostKey, "utf8").digest();
|
|
267
|
+
if (buffer.subarray(0, 32).equals(domainHash)) {
|
|
268
|
+
return buffer.subarray(32);
|
|
269
|
+
}
|
|
270
|
+
return buffer;
|
|
271
|
+
}
|
|
272
|
+
function decryptChromiumAes128CbcValue(encryptedValue, key, hostKey) {
|
|
273
|
+
const ciphertext = encryptedValue.length > 3 && encryptedValue[0] === 118 && encryptedValue[1] === 49 && (encryptedValue[2] === 48 || encryptedValue[2] === 49) ? encryptedValue.subarray(3) : encryptedValue;
|
|
274
|
+
const iv = Buffer.alloc(AES_BLOCK_BYTES, " ");
|
|
275
|
+
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
276
|
+
const plaintext = Buffer.concat([
|
|
277
|
+
decipher.update(ciphertext),
|
|
278
|
+
decipher.final()
|
|
279
|
+
]);
|
|
280
|
+
return stripDomainHashPrefix(stripChromiumPadding(plaintext), hostKey).toString(
|
|
281
|
+
"utf8"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function decryptChromiumAes256GcmValue(encryptedValue, key) {
|
|
285
|
+
const nonce = encryptedValue.subarray(3, 15);
|
|
286
|
+
const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16);
|
|
287
|
+
const authTag = encryptedValue.subarray(encryptedValue.length - 16);
|
|
288
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
289
|
+
decipher.setAuthTag(authTag);
|
|
290
|
+
return Buffer.concat([
|
|
291
|
+
decipher.update(ciphertext),
|
|
292
|
+
decipher.final()
|
|
293
|
+
]).toString("utf8");
|
|
294
|
+
}
|
|
295
|
+
async function dpapiUnprotect(buffer) {
|
|
296
|
+
const script = [
|
|
297
|
+
`$inputBytes = [Convert]::FromBase64String('${buffer.toString("base64")}')`,
|
|
298
|
+
"$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(",
|
|
299
|
+
" $inputBytes,",
|
|
300
|
+
" $null,",
|
|
301
|
+
" [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
302
|
+
")",
|
|
303
|
+
"[Convert]::ToBase64String($plainBytes)"
|
|
304
|
+
].join("\n");
|
|
305
|
+
const { stdout } = await execFileAsync(
|
|
306
|
+
"powershell.exe",
|
|
307
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
308
|
+
{
|
|
309
|
+
encoding: "utf8",
|
|
310
|
+
maxBuffer: 8 * 1024 * 1024
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
return Buffer.from(stdout.trim(), "base64");
|
|
314
|
+
}
|
|
315
|
+
async function buildChromiumDecryptor(location) {
|
|
316
|
+
if (process.platform === "darwin") {
|
|
317
|
+
const brand = detectChromiumBrand(location);
|
|
318
|
+
const keychainStore = createKeychainStore();
|
|
319
|
+
const password = keychainStore?.get(brand.macService, brand.macAccount) ?? null;
|
|
320
|
+
if (!password) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Unable to read ${brand.macService} from macOS Keychain.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const key = pbkdf2Sync(password, KEY_SALT, MAC_KEY_ITERATIONS, KEY_LENGTH, "sha1");
|
|
326
|
+
return async (row) => decryptChromiumAes128CbcValue(
|
|
327
|
+
Buffer.from(row.encrypted_value || "", "hex"),
|
|
328
|
+
key,
|
|
329
|
+
row.host_key
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (process.platform === "linux") {
|
|
333
|
+
const brand = detectChromiumBrand(location);
|
|
334
|
+
const keychainStore = createKeychainStore();
|
|
335
|
+
const password = keychainStore?.get(brand.macService, brand.macAccount) ?? brand.linuxApplications.map((application) => keychainStore?.get(application, application) ?? null).find(Boolean) ?? null;
|
|
336
|
+
const key = pbkdf2Sync(
|
|
337
|
+
password || "peanuts",
|
|
338
|
+
KEY_SALT,
|
|
339
|
+
LINUX_KEY_ITERATIONS,
|
|
340
|
+
KEY_LENGTH,
|
|
341
|
+
"sha1"
|
|
342
|
+
);
|
|
343
|
+
return async (row) => decryptChromiumAes128CbcValue(
|
|
344
|
+
Buffer.from(row.encrypted_value || "", "hex"),
|
|
345
|
+
key,
|
|
346
|
+
row.host_key
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (process.platform === "win32") {
|
|
350
|
+
if (!location.localStatePath) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Unable to locate Chromium Local State for profile: ${location.profileDir}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const localState = JSON.parse(
|
|
356
|
+
await readFile(location.localStatePath, "utf8")
|
|
357
|
+
);
|
|
358
|
+
const encryptedKeyBase64 = localState.os_crypt?.encrypted_key;
|
|
359
|
+
if (!encryptedKeyBase64) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Local State did not include os_crypt.encrypted_key for ${location.userDataDir}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const encryptedKey = Buffer.from(encryptedKeyBase64, "base64");
|
|
365
|
+
const masterKey = await dpapiUnprotect(encryptedKey.subarray(5));
|
|
366
|
+
return async (row) => {
|
|
367
|
+
const encryptedValue = Buffer.from(row.encrypted_value || "", "hex");
|
|
368
|
+
if (encryptedValue.length > 4 && encryptedValue[0] === 1 && encryptedValue[1] === 0 && encryptedValue[2] === 0 && encryptedValue[3] === 0) {
|
|
369
|
+
const decrypted = await dpapiUnprotect(encryptedValue);
|
|
370
|
+
return decrypted.toString("utf8");
|
|
371
|
+
}
|
|
372
|
+
return decryptChromiumAes256GcmValue(encryptedValue, masterKey);
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Local Chromium cookie sync is not supported on ${process.platform}.`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
function buildPlaywrightCookie(row, value) {
|
|
380
|
+
if (!row.name.trim()) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
if (!row.host_key.trim()) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const expires = row.has_expires === 1 ? convertChromiumTimestampToUnixSeconds(row.expires_utc) : -1;
|
|
387
|
+
if (expires !== -1 && expires <= Math.floor(Date.now() / 1e3)) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
name: row.name,
|
|
392
|
+
value,
|
|
393
|
+
domain: row.host_key,
|
|
394
|
+
path: row.path || "/",
|
|
395
|
+
expires,
|
|
396
|
+
httpOnly: row.is_httponly === 1,
|
|
397
|
+
secure: row.is_secure === 1,
|
|
398
|
+
sameSite: mapChromiumSameSite(row.samesite)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function loadCookiesFromLocalProfileDir(inputPath, options = {}) {
|
|
402
|
+
const location = await resolveChromiumProfileLocation(inputPath);
|
|
403
|
+
try {
|
|
404
|
+
return await loadCookiesFromSqlite(location);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (!isMissingSqliteBinary(error)) {
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return await loadCookiesFromBrowserSnapshot(location, options);
|
|
411
|
+
}
|
|
412
|
+
async function loadCookiesFromSqlite(location) {
|
|
413
|
+
const snapshot = await createSqliteSnapshot(location.cookieDbPath);
|
|
414
|
+
try {
|
|
415
|
+
const rows = await querySqliteJson(
|
|
416
|
+
snapshot.snapshotPath,
|
|
417
|
+
[
|
|
418
|
+
"SELECT",
|
|
419
|
+
" host_key,",
|
|
420
|
+
" name,",
|
|
421
|
+
" value,",
|
|
422
|
+
" hex(encrypted_value) AS encrypted_value,",
|
|
423
|
+
" path,",
|
|
424
|
+
" CAST(expires_utc AS TEXT) AS expires_utc,",
|
|
425
|
+
" is_secure,",
|
|
426
|
+
" is_httponly,",
|
|
427
|
+
" has_expires,",
|
|
428
|
+
" samesite",
|
|
429
|
+
"FROM cookies"
|
|
430
|
+
].join(" ")
|
|
431
|
+
);
|
|
432
|
+
const decryptValue = await buildChromiumDecryptor(location);
|
|
433
|
+
const cookies = [];
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
let value = row.value || "";
|
|
436
|
+
if (!value && row.encrypted_value) {
|
|
437
|
+
value = await decryptValue(row);
|
|
438
|
+
}
|
|
439
|
+
const cookie = buildPlaywrightCookie(row, value);
|
|
440
|
+
if (cookie) {
|
|
441
|
+
cookies.push(cookie);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return cookies;
|
|
445
|
+
} finally {
|
|
446
|
+
await snapshot.cleanup();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function loadCookiesFromBrowserSnapshot(location, options) {
|
|
450
|
+
const snapshotRootDir = await mkdtemp(join(tmpdir(), "opensteer-profile-"));
|
|
451
|
+
const snapshotProfileDir = join(
|
|
452
|
+
snapshotRootDir,
|
|
453
|
+
basename(location.profileDir)
|
|
454
|
+
);
|
|
455
|
+
let context = null;
|
|
456
|
+
try {
|
|
457
|
+
await cp(location.profileDir, snapshotProfileDir, {
|
|
458
|
+
recursive: true
|
|
459
|
+
});
|
|
460
|
+
if (location.localStatePath) {
|
|
461
|
+
await copyFile(location.localStatePath, join(snapshotRootDir, "Local State"));
|
|
462
|
+
}
|
|
463
|
+
const brand = detectChromiumBrand(location);
|
|
464
|
+
const args = [`--profile-directory=${basename(snapshotProfileDir)}`];
|
|
465
|
+
context = await chromium.launchPersistentContext(snapshotRootDir, {
|
|
466
|
+
channel: brand.playwrightChannel,
|
|
467
|
+
headless: options.headless ?? true,
|
|
468
|
+
timeout: options.timeout ?? 12e4,
|
|
469
|
+
args
|
|
470
|
+
});
|
|
471
|
+
return await context.cookies();
|
|
472
|
+
} finally {
|
|
473
|
+
await context?.close().catch(() => void 0);
|
|
474
|
+
await rm(snapshotRootDir, { recursive: true, force: true });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function isMissingSqliteBinary(error) {
|
|
478
|
+
return Boolean(
|
|
479
|
+
error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
21
483
|
// src/cli/profile-sync.ts
|
|
22
484
|
function normalizeCookieDomain(value) {
|
|
23
485
|
const trimmed = value.trim().toLowerCase();
|