pi-oracle 0.7.21 → 0.7.23
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/CHANGELOG.md +40 -0
- package/README.md +49 -385
- package/docs/ORACLE_DESIGN.md +1 -1
- package/docs/development.md +99 -0
- package/docs/platform-smoke.md +18 -7
- package/docs/reference.md +338 -0
- package/extensions/oracle/index.ts +1 -0
- package/extensions/oracle/lib/poller.ts +3 -8
- package/extensions/oracle/lib/queue.ts +2 -4
- package/extensions/oracle/shared/browser-profile-helpers.d.mts +1 -0
- package/extensions/oracle/shared/browser-profile-helpers.mjs +32 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +35 -6
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +1 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +18 -13
- package/extensions/oracle/worker/chromium-cookie-source.mjs +50 -6
- package/extensions/oracle/worker/linux-chromium-cookie-source.d.mts +6 -0
- package/extensions/oracle/worker/linux-chromium-cookie-source.mjs +98 -0
- package/extensions/oracle/worker/run-job.mjs +34 -34
- package/package.json +12 -11
- package/platform-smoke.config.mjs +1 -22
- package/scripts/oracle-auth.test.mjs +35 -0
- package/scripts/oracle-chatgpt-preset-proof.mjs +50 -21
- package/scripts/oracle-cookie-import.test.mjs +204 -0
- package/scripts/oracle-model.test.mjs +24 -17
- package/scripts/oracle-real-smoke.mjs +30 -9
- package/scripts/platform-smoke/Dockerfile.ubuntu +3 -2
- package/scripts/platform-smoke/crabbox-runner.mjs +18 -6
- package/scripts/platform-smoke/doctor.mjs +58 -41
- package/scripts/platform-smoke/invariants.mjs +254 -26
- package/scripts/platform-smoke/platform-build-windows.ps1 +21 -17
- package/scripts/platform-smoke/targets.mjs +59 -30
- package/scripts/platform-smoke.mjs +47 -14
|
@@ -97,6 +97,38 @@ export function linuxChromiumCookieImportUserDataDirs(options = {}) {
|
|
|
97
97
|
return LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS.map((segments) => join(configHome, ...segments));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Match only native Chromium sources under the same roots as autodetection.
|
|
102
|
+
* Profile names alone remain Chrome selectors, as in Sweet Cookie.
|
|
103
|
+
* @param {string} source
|
|
104
|
+
* @param {BrowserPathOptions} [options]
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
export function isNativeLinuxChromiumCookieSource(source, options = {}) {
|
|
108
|
+
if (!source.includes("/") && !source.includes("\\")) return false;
|
|
109
|
+
if (isAbsolute(source) && !source.includes("chromium")) return false;
|
|
110
|
+
return matchesNativeLinuxChromiumCookieSource(source, options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} source
|
|
115
|
+
* @param {BrowserPathOptions} options
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function matchesNativeLinuxChromiumCookieSource(source, options) {
|
|
119
|
+
const path = normalizedAbsolutePath(source, options);
|
|
120
|
+
let profile = path;
|
|
121
|
+
if (basename(profile) === "Cookies") {
|
|
122
|
+
profile = dirname(profile);
|
|
123
|
+
if (basename(profile) === "Network") profile = dirname(profile);
|
|
124
|
+
}
|
|
125
|
+
const root = dirname(profile);
|
|
126
|
+
if (basename(root) !== "chromium" && basename(root) !== "chromium-browser") return false;
|
|
127
|
+
if (!linuxChromiumCookieImportUserDataDirs(options).includes(root)) return false;
|
|
128
|
+
return basename(path) === "Cookies" ? existsSync(path)
|
|
129
|
+
: existsSync(join(profile, "Cookies")) || existsSync(join(profile, "Network", "Cookies"));
|
|
130
|
+
}
|
|
131
|
+
|
|
100
132
|
/**
|
|
101
133
|
* @param {BrowserPathOptions} [options]
|
|
102
134
|
* @returns {string[]}
|
|
@@ -12,6 +12,7 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
12
12
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
13
13
|
import {
|
|
14
14
|
assertNotKnownBrowserUserDataPath,
|
|
15
|
+
isNativeLinuxChromiumCookieSource,
|
|
15
16
|
sweetCookieSafeStoragePasswordScrubbedEnv,
|
|
16
17
|
} from "../shared/browser-profile-helpers.mjs";
|
|
17
18
|
import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
|
|
@@ -340,10 +341,19 @@ async function launchTargetBrowser() {
|
|
|
340
341
|
await closeTargetBrowser();
|
|
341
342
|
const args = [...targetBrowserBaseArgs({ withLaunchOptions: true, mode: "headed" }), "open", "about:blank"];
|
|
342
343
|
await log(`Launching isolated browser: agent-browser ${JSON.stringify(args)}`);
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
const deadline = Date.now() + AGENT_BROWSER_COMMAND_TIMEOUT_MS;
|
|
345
|
+
while (true) {
|
|
346
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, args, { allowFailure: true, timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
347
|
+
await log(`launch result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
348
|
+
if (result.code === 0) return;
|
|
349
|
+
const message = result.stderr || result.stdout || "Failed to launch isolated oracle browser";
|
|
350
|
+
// agent-browser 0.39 can return from close before its daemon socket disappears.
|
|
351
|
+
// This configure failure happens before open is dispatched; other launch errors are not safe to retry.
|
|
352
|
+
if (!/Could not configure browser: Failed to connect: No such file or directory/i.test(message) || Date.now() >= deadline) {
|
|
353
|
+
throw new Error(message);
|
|
354
|
+
}
|
|
355
|
+
await log("Waiting for the closing agent-browser daemon before retrying launch");
|
|
356
|
+
await sleep(Math.min(250, deadline - Date.now()));
|
|
347
357
|
}
|
|
348
358
|
}
|
|
349
359
|
|
|
@@ -573,6 +583,15 @@ function formatAuthFailureGuidance(error) {
|
|
|
573
583
|
return lines.join("\n");
|
|
574
584
|
}
|
|
575
585
|
|
|
586
|
+
function canUseCalibratedChromeKey(source) {
|
|
587
|
+
if (!source.includes("/") && !source.includes("\\")) return false;
|
|
588
|
+
// Match Sweet Cookie's candidate expansion, not normalized source qualification:
|
|
589
|
+
// a literal absolute Cookies path can retain a Brave selector before "..".
|
|
590
|
+
const candidate = source.startsWith("~/") ? join(homedir(), source.slice(2))
|
|
591
|
+
: isAbsolute(source) ? source : resolve(process.cwd(), source);
|
|
592
|
+
return !/bravesoftware|brave-browser|brave browser/i.test(candidate);
|
|
593
|
+
}
|
|
594
|
+
|
|
576
595
|
async function readRawSourceCookies() {
|
|
577
596
|
if (config.auth.chromeCookiePath && config.auth.chromiumKeychain) {
|
|
578
597
|
return await getCookiesFromConfiguredChromiumSource({
|
|
@@ -584,14 +603,24 @@ async function readRawSourceCookies() {
|
|
|
584
603
|
});
|
|
585
604
|
}
|
|
586
605
|
|
|
587
|
-
|
|
606
|
+
const options = {
|
|
588
607
|
url: providerChatUrl(),
|
|
589
608
|
origins: cookieOrigins(),
|
|
590
609
|
browsers: ["chrome"],
|
|
591
610
|
mode: "merge",
|
|
592
611
|
chromeProfile: cookieSource(),
|
|
593
612
|
timeoutMs: COOKIE_READ_TIMEOUT_MS,
|
|
594
|
-
}
|
|
613
|
+
};
|
|
614
|
+
if (process.platform === "linux") {
|
|
615
|
+
if (process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD?.trim() && canUseCalibratedChromeKey(options.chromeProfile)) {
|
|
616
|
+
return await getCookies(options);
|
|
617
|
+
}
|
|
618
|
+
if (isNativeLinuxChromiumCookieSource(options.chromeProfile)) {
|
|
619
|
+
const { getCookiesFromNativeLinuxChromium } = await import("./linux-chromium-cookie-source.mjs");
|
|
620
|
+
return await getCookiesFromNativeLinuxChromium(options, getCookies);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return await getCookies(options);
|
|
595
624
|
}
|
|
596
625
|
|
|
597
626
|
async function readSourceCookies() {
|
|
@@ -16,6 +16,7 @@ export declare function matchesModelFamilyLabel(label: string | undefined, famil
|
|
|
16
16
|
export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
|
|
17
17
|
export declare function matchesCompactIntelligenceControlLabel(label: string | undefined): boolean;
|
|
18
18
|
export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
|
|
19
|
+
export declare function matchesModelConfigurationOpener(entry: import("./artifact-heuristics.mjs").SnapshotEntry): boolean;
|
|
19
20
|
export declare function requestedEffortLabel(selection: OracleUiSelection): string | undefined;
|
|
20
21
|
export declare function effortSelectionVisible(snapshot: string, effortLabel: string | undefined): boolean;
|
|
21
22
|
export declare function thinkingChipVisible(snapshot: string): boolean;
|
|
@@ -522,24 +522,29 @@ export function snapshotHasUsableComposerControls(snapshot) {
|
|
|
522
522
|
return hasComposer && hasAddFiles;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* @param {SnapshotEntry} entry
|
|
527
|
+
* @returns {boolean}
|
|
528
|
+
*/
|
|
529
|
+
export function matchesModelConfigurationOpener(entry) {
|
|
530
|
+
if (entry.disabled || entry.kind !== "button" || typeof entry.label !== "string") return false;
|
|
531
|
+
const label = normalizeChipLabel(entry.label);
|
|
532
|
+
return label === "Model"
|
|
533
|
+
|| label === "Model selector"
|
|
534
|
+
|| label === "Select ChatGPT model"
|
|
535
|
+
|| matchesCompactIntelligenceOpenerLabel(label)
|
|
536
|
+
|| BARE_EFFORT_PATTERN.test(label)
|
|
537
|
+
|| ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {OracleUiModelFamily} */ (family)))
|
|
538
|
+
|| THINKING_CHIP_PATTERN.test(label)
|
|
539
|
+
|| PRO_CHIP_PATTERN.test(label);
|
|
540
|
+
}
|
|
541
|
+
|
|
525
542
|
/**
|
|
526
543
|
* @param {string} snapshot
|
|
527
544
|
* @returns {boolean}
|
|
528
545
|
*/
|
|
529
546
|
export function snapshotHasModelOpener(snapshot) {
|
|
530
|
-
|
|
531
|
-
const entries = parseSnapshotEntries(snapshot);
|
|
532
|
-
return entries.some((entry) => {
|
|
533
|
-
if (entry.disabled || entry.kind !== "button" || typeof entry.label !== "string") return false;
|
|
534
|
-
const label = normalizeChipLabel(entry.label);
|
|
535
|
-
return label === "Model"
|
|
536
|
-
|| label === "Model selector"
|
|
537
|
-
|| COMPACT_INTELLIGENCE_OPENER_PATTERN.test(label)
|
|
538
|
-
|| EFFORT_LABELS.has(label)
|
|
539
|
-
|| ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {OracleUiModelFamily} */ (family)))
|
|
540
|
-
|| THINKING_CHIP_PATTERN.test(label)
|
|
541
|
-
|| PRO_CHIP_PATTERN.test(label);
|
|
542
|
-
});
|
|
547
|
+
return parseSnapshotEntries(snapshot).some(matchesModelConfigurationOpener);
|
|
543
548
|
}
|
|
544
549
|
|
|
545
550
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Purpose: Read provider cookies from
|
|
2
|
-
// Responsibilities: Snapshot
|
|
3
|
-
// Scope: macOS
|
|
4
|
-
// Usage: auth-bootstrap.mjs
|
|
5
|
-
// Invariants/Assumptions:
|
|
1
|
+
// Purpose: Read provider cookies from configured macOS Chromium stores.
|
|
2
|
+
// Responsibilities: Snapshot and decrypt configured macOS cookie stores.
|
|
3
|
+
// Scope: macOS cookie extraction only; auth policy filtering and browser seeding stay in auth-bootstrap.mjs.
|
|
4
|
+
// Usage: auth-bootstrap.mjs calls the configured macOS API after selecting the source.
|
|
5
|
+
// Invariants/Assumptions: Configured Keychain items identify the browser secret; helper environments are scrubbed.
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
8
8
|
import { createDecipheriv, pbkdf2Sync } from "node:crypto";
|
|
@@ -11,10 +11,20 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { DatabaseSync } from "node:sqlite";
|
|
13
13
|
|
|
14
|
+
/** @typedef {import("./chromium-cookie-source.d.mts").ChromiumKeychainConfig} ChromiumKeychainConfig */
|
|
15
|
+
/** @typedef {import("./chromium-cookie-source.d.mts").ConfiguredChromiumSourceOptions} ConfiguredChromiumSourceOptions */
|
|
16
|
+
/** @typedef {import("./auth-cookie-policy.mjs").ImportedAuthCookie} ImportedAuthCookie */
|
|
17
|
+
|
|
14
18
|
const CHROMIUM_EPOCH_OFFSET_SECONDS = 11_644_473_600n;
|
|
15
19
|
const COOKIE_VALUE_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
16
20
|
const MACOS_CHROMIUM_KEY_ITERATIONS = 1003;
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} command
|
|
24
|
+
* @param {string[]} args
|
|
25
|
+
* @param {{timeoutMs?: number}} [options]
|
|
26
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string, error: string}>}
|
|
27
|
+
*/
|
|
18
28
|
function spawnCapture(command, args, options = {}) {
|
|
19
29
|
return new Promise((resolve) => {
|
|
20
30
|
const child = spawn(command, args, { env: sweetCookieSafeStoragePasswordScrubbedEnv(), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
@@ -46,9 +56,14 @@ function spawnCapture(command, args, options = {}) {
|
|
|
46
56
|
});
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {ChromiumKeychainConfig} keychain
|
|
61
|
+
* @param {number} timeoutMs
|
|
62
|
+
* @returns {Promise<{ok: true, password: string, service: string} | {ok: false, error: string}>}
|
|
63
|
+
*/
|
|
49
64
|
async function readKeychainPassword(keychain, timeoutMs) {
|
|
50
65
|
const services = Array.isArray(keychain.services) && keychain.services.length > 0 ? keychain.services : [keychain.service];
|
|
51
|
-
for (const service of services.filter(Boolean)) {
|
|
66
|
+
for (const service of services.filter(/** @returns {service is string} */ (service) => Boolean(service))) {
|
|
52
67
|
const result = await spawnCapture("security", ["find-generic-password", "-w", "-a", keychain.account, "-s", service], { timeoutMs });
|
|
53
68
|
if (result.ok) {
|
|
54
69
|
const password = result.stdout.trim();
|
|
@@ -59,6 +74,7 @@ async function readKeychainPassword(keychain, timeoutMs) {
|
|
|
59
74
|
return { ok: false, error: `Failed to read macOS Keychain (${keychain.label || keychain.account}): no configured service returned a password.` };
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
/** @param {string} dbPath */
|
|
62
78
|
function snapshotCookieDb(dbPath) {
|
|
63
79
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-oracle-chromium-cookies-"));
|
|
64
80
|
const tempDbPath = join(tempDir, "Cookies");
|
|
@@ -73,6 +89,7 @@ function snapshotCookieDb(dbPath) {
|
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
/** @param {string} sourceDbPath @param {string} targetPath @param {string} suffix */
|
|
76
93
|
function copySidecar(sourceDbPath, targetPath, suffix) {
|
|
77
94
|
const sidecarPath = `${sourceDbPath}${suffix}`;
|
|
78
95
|
try {
|
|
@@ -83,10 +100,12 @@ function copySidecar(sourceDbPath, targetPath, suffix) {
|
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
/** @param {unknown} error */
|
|
86
104
|
function isMissingFileError(error) {
|
|
87
105
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
88
106
|
}
|
|
89
107
|
|
|
108
|
+
/** @param {DatabaseSync} db */
|
|
90
109
|
function readMetaVersion(db) {
|
|
91
110
|
try {
|
|
92
111
|
const row = db.prepare("SELECT value FROM meta WHERE key = 'version'").get();
|
|
@@ -97,6 +116,7 @@ function readMetaVersion(db) {
|
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
/** @param {string} host */
|
|
100
120
|
function parentCookieDomains(host) {
|
|
101
121
|
const labels = host.split(".").filter(Boolean);
|
|
102
122
|
const domains = new Set([host, `.${host}`]);
|
|
@@ -108,11 +128,14 @@ function parentCookieDomains(host) {
|
|
|
108
128
|
return [...domains];
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
/** @param {string} value */
|
|
111
132
|
function sqlStringLiteral(value) {
|
|
112
133
|
return `'${value.replaceAll("'", "''")}'`;
|
|
113
134
|
}
|
|
114
135
|
|
|
136
|
+
/** @param {string[]} origins */
|
|
115
137
|
function buildHostWhereClause(origins) {
|
|
138
|
+
/** @type {Set<string>} */
|
|
116
139
|
const domains = new Set();
|
|
117
140
|
for (const origin of origins) {
|
|
118
141
|
try {
|
|
@@ -125,11 +148,13 @@ function buildHostWhereClause(origins) {
|
|
|
125
148
|
return `host_key IN (${[...domains].map(sqlStringLiteral).join(", ")})`;
|
|
126
149
|
}
|
|
127
150
|
|
|
151
|
+
/** @param {string[]} originHosts @param {string} hostKey */
|
|
128
152
|
function hostMatchesAny(originHosts, hostKey) {
|
|
129
153
|
const cookieDomain = hostKey.startsWith(".") ? hostKey.slice(1) : hostKey;
|
|
130
154
|
return originHosts.some((host) => host === cookieDomain || host.endsWith(`.${cookieDomain}`));
|
|
131
155
|
}
|
|
132
156
|
|
|
157
|
+
/** @param {unknown} value */
|
|
133
158
|
function chromiumExpirationToUnixSeconds(value) {
|
|
134
159
|
if (value === undefined || value === null || String(value) === "0") return undefined;
|
|
135
160
|
try {
|
|
@@ -142,6 +167,7 @@ function chromiumExpirationToUnixSeconds(value) {
|
|
|
142
167
|
}
|
|
143
168
|
}
|
|
144
169
|
|
|
170
|
+
/** @param {unknown} value @returns {ImportedAuthCookie["sameSite"]} */
|
|
145
171
|
function normalizeSameSite(value) {
|
|
146
172
|
const normalized = String(value ?? "").toLowerCase();
|
|
147
173
|
if (normalized === "2" || normalized === "strict") return "Strict";
|
|
@@ -150,10 +176,12 @@ function normalizeSameSite(value) {
|
|
|
150
176
|
return undefined;
|
|
151
177
|
}
|
|
152
178
|
|
|
179
|
+
/** @param {string} password */
|
|
153
180
|
function deriveMacosChromiumKey(password) {
|
|
154
181
|
return pbkdf2Sync(password, "saltysalt", MACOS_CHROMIUM_KEY_ITERATIONS, 16, "sha1");
|
|
155
182
|
}
|
|
156
183
|
|
|
184
|
+
/** @param {Uint8Array} encryptedValue @param {Buffer} key @param {{stripHashPrefix: boolean}} options */
|
|
157
185
|
function decryptCookieValue(encryptedValue, key, options) {
|
|
158
186
|
const buffer = Buffer.from(encryptedValue);
|
|
159
187
|
if (buffer.length < 3) return null;
|
|
@@ -172,6 +200,7 @@ function decryptCookieValue(encryptedValue, key, options) {
|
|
|
172
200
|
}
|
|
173
201
|
}
|
|
174
202
|
|
|
203
|
+
/** @param {Buffer} value */
|
|
175
204
|
function removePkcs7Padding(value) {
|
|
176
205
|
if (!value.length) return value;
|
|
177
206
|
const padding = value[value.length - 1];
|
|
@@ -179,6 +208,7 @@ function removePkcs7Padding(value) {
|
|
|
179
208
|
return value.subarray(0, value.length - padding);
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
/** @param {Uint8Array} value @param {boolean} stripHashPrefix */
|
|
182
212
|
function decodeCookieBytes(value, stripHashPrefix) {
|
|
183
213
|
const bytes = stripHashPrefix && value.length >= 32 ? value.subarray(32) : value;
|
|
184
214
|
try {
|
|
@@ -188,12 +218,19 @@ function decodeCookieBytes(value, stripHashPrefix) {
|
|
|
188
218
|
}
|
|
189
219
|
}
|
|
190
220
|
|
|
221
|
+
/** @param {string} value */
|
|
191
222
|
function stripLeadingControlChars(value) {
|
|
192
223
|
let index = 0;
|
|
193
224
|
while (index < value.length && value.charCodeAt(index) < 0x20) index += 1;
|
|
194
225
|
return value.slice(index);
|
|
195
226
|
}
|
|
196
227
|
|
|
228
|
+
/**
|
|
229
|
+
* @param {Record<string, unknown>[]} rows
|
|
230
|
+
* @param {Pick<ConfiguredChromiumSourceOptions, "origins" | "profile" | "includeExpired"> & {stripHashPrefix: boolean}} options
|
|
231
|
+
* @param {Buffer} key
|
|
232
|
+
* @param {string[]} warnings
|
|
233
|
+
*/
|
|
197
234
|
function collectCookies(rows, options, key, warnings) {
|
|
198
235
|
const cookies = [];
|
|
199
236
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -221,6 +258,7 @@ function collectCookies(rows, options, key, warnings) {
|
|
|
221
258
|
const expires = chromiumExpirationToUnixSeconds(row.expires_utc);
|
|
222
259
|
if (!options.includeExpired && expires !== undefined && expires < now) continue;
|
|
223
260
|
|
|
261
|
+
/** @type {ImportedAuthCookie & {source: {browser: string, profile: string}}} */
|
|
224
262
|
const cookie = {
|
|
225
263
|
name,
|
|
226
264
|
value,
|
|
@@ -239,7 +277,9 @@ function collectCookies(rows, options, key, warnings) {
|
|
|
239
277
|
return dedupeCookies(cookies);
|
|
240
278
|
}
|
|
241
279
|
|
|
280
|
+
/** @param {ImportedAuthCookie[]} cookies */
|
|
242
281
|
function dedupeCookies(cookies) {
|
|
282
|
+
/** @type {Map<string, ImportedAuthCookie>} */
|
|
243
283
|
const seen = new Map();
|
|
244
284
|
for (const cookie of cookies) {
|
|
245
285
|
const key = `${cookie.domain}\t${cookie.path}\t${cookie.name}`;
|
|
@@ -248,6 +288,10 @@ function dedupeCookies(cookies) {
|
|
|
248
288
|
return [...seen.values()];
|
|
249
289
|
}
|
|
250
290
|
|
|
291
|
+
/**
|
|
292
|
+
* @param {ConfiguredChromiumSourceOptions} options
|
|
293
|
+
* @returns {Promise<{cookies: ImportedAuthCookie[], warnings: string[]}>}
|
|
294
|
+
*/
|
|
251
295
|
export async function getCookiesFromConfiguredChromiumSource(options) {
|
|
252
296
|
const warnings = [];
|
|
253
297
|
if (!options.dbPath || !existsSync(options.dbPath)) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Purpose: Select native Linux Chromium's safe-storage key for the public Sweet Cookie parser.
|
|
2
|
+
// Scope: Called only after source qualification in the short-lived auth worker.
|
|
3
|
+
// Invariants: Restore password/backend overrides and scrub keyring helper environments.
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
6
|
+
|
|
7
|
+
// ponytail: Native Linux Chromium key selection only. Keep Sweet Cookie 0.3's
|
|
8
|
+
// public parser/decryptor; remove when an upstream fix passes unchanged-path
|
|
9
|
+
// performance checks. This API runs only in the short-lived auth worker.
|
|
10
|
+
const PASSWORD_ENV = "SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD";
|
|
11
|
+
const BRAVE_PASSWORD_ENV = "SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD";
|
|
12
|
+
const BACKEND_ENV = "SWEET_COOKIE_LINUX_KEYRING";
|
|
13
|
+
|
|
14
|
+
/** @param {string} command @param {string[]} args @returns {Promise<string | undefined>} */
|
|
15
|
+
function readHelper(command, args) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
execFile(command, args, {
|
|
18
|
+
env: sweetCookieSafeStoragePasswordScrubbedEnv(),
|
|
19
|
+
encoding: "utf8", timeout: 3_000, killSignal: "SIGKILL",
|
|
20
|
+
}, (error, stdout) => resolve(error ? undefined : stdout.trim()));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function keyringBackend() {
|
|
25
|
+
const configured = process.env[BACKEND_ENV]?.trim().toLowerCase();
|
|
26
|
+
if (configured === "gnome" || configured === "kwallet" || configured === "basic") return configured;
|
|
27
|
+
const kde = process.env.XDG_CURRENT_DESKTOP?.split(":").some((part) => part.trim().toLowerCase() === "kde")
|
|
28
|
+
|| Boolean(process.env.KDE_FULL_SESSION?.trim());
|
|
29
|
+
return kde ? "kwallet" : "gnome";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readChromiumPassword() {
|
|
33
|
+
const backend = keyringBackend();
|
|
34
|
+
if (backend === "basic") return { password: "", warnings: [] };
|
|
35
|
+
if (backend === "gnome") {
|
|
36
|
+
const password = await readHelper("secret-tool", ["lookup", "service", "Chromium Safe Storage", "account", "Chromium"])
|
|
37
|
+
|| await readHelper("secret-tool", ["lookup", "application", "chromium"]);
|
|
38
|
+
return { password: password || "", warnings: password ? [] : [
|
|
39
|
+
"Failed to read Linux keyring via secret-tool; v11 cookies may be unavailable.",
|
|
40
|
+
] };
|
|
41
|
+
}
|
|
42
|
+
const version = process.env.KDE_SESSION_VERSION?.trim();
|
|
43
|
+
const suffix = version === "5" || version === "6" ? version : "";
|
|
44
|
+
const wallet = await readHelper("dbus-send", [
|
|
45
|
+
"--session", "--print-reply=literal", `--dest=org.kde.kwalletd${suffix}`,
|
|
46
|
+
`/modules/kwalletd${suffix}`, "org.kde.KWallet.networkWallet",
|
|
47
|
+
]);
|
|
48
|
+
const password = await readHelper("kwallet-query", [
|
|
49
|
+
"--read-password", "Chromium Safe Storage", "--folder", "Chromium Keys",
|
|
50
|
+
wallet?.replaceAll('"', "").trim() || "kdewallet",
|
|
51
|
+
]);
|
|
52
|
+
return {
|
|
53
|
+
password: password?.toLowerCase().startsWith("failed to read") ? "" : password || "",
|
|
54
|
+
warnings: password === undefined ? [
|
|
55
|
+
"Failed to read Linux keyring via kwallet-query; v11 cookies may be unavailable.",
|
|
56
|
+
] : [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {import("@steipete/sweet-cookie").GetCookiesOptions} options
|
|
62
|
+
* @param {typeof import("@steipete/sweet-cookie").getCookies} readCookies
|
|
63
|
+
*/
|
|
64
|
+
export async function getCookiesFromNativeLinuxChromium(options, readCookies) {
|
|
65
|
+
const override = process.env[PASSWORD_ENV]?.trim();
|
|
66
|
+
const { password, warnings } = override
|
|
67
|
+
? { password: override, warnings: [] }
|
|
68
|
+
: await readChromiumPassword();
|
|
69
|
+
const previous = {
|
|
70
|
+
[PASSWORD_ENV]: process.env[PASSWORD_ENV],
|
|
71
|
+
[BRAVE_PASSWORD_ENV]: process.env[BRAVE_PASSWORD_ENV],
|
|
72
|
+
[BACKEND_ENV]: process.env[BACKEND_ENV],
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
// Sweet Cookie 0.3 classifies Brave by substrings anywhere in the DB path.
|
|
76
|
+
// Even a Chromium XDG/HOME ancestor can trigger it. Supply the selected
|
|
77
|
+
// Chromium key to either branch so neither can spawn a vendor keyring helper.
|
|
78
|
+
if (password) {
|
|
79
|
+
process.env[PASSWORD_ENV] = password;
|
|
80
|
+
process.env[BRAVE_PASSWORD_ENV] = password;
|
|
81
|
+
}
|
|
82
|
+
// Empty env values are ignored by Sweet Cookie. Its basic backend supplies
|
|
83
|
+
// the empty key without accidentally probing Chrome's keyring instead.
|
|
84
|
+
else {
|
|
85
|
+
delete process.env[PASSWORD_ENV];
|
|
86
|
+
delete process.env[BRAVE_PASSWORD_ENV];
|
|
87
|
+
process.env[BACKEND_ENV] = "basic";
|
|
88
|
+
}
|
|
89
|
+
const result = await readCookies(options);
|
|
90
|
+
result.warnings.unshift(...warnings);
|
|
91
|
+
return result;
|
|
92
|
+
} finally {
|
|
93
|
+
for (const [name, value] of Object.entries(previous)) {
|
|
94
|
+
if (value === undefined) delete process.env[name];
|
|
95
|
+
else process.env[name] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|