@scrylog/cli 0.3.0 → 0.3.2
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/dist/assets/highlights-eq9cgrbb.scm +604 -0
- package/dist/assets/highlights-ghv9g403.scm +205 -0
- package/dist/assets/highlights-hk7bwhj4.scm +284 -0
- package/dist/assets/highlights-r812a2qc.scm +150 -0
- package/dist/assets/highlights-x6tmsnaa.scm +115 -0
- package/dist/assets/injections-73j83es3.scm +27 -0
- package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
- package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
- package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/cli/index.js +499 -0
- package/dist/cli/index.js.map +61 -0
- package/dist/daemon/index.js +670 -0
- package/dist/daemon/index.js.map +1475 -0
- package/dist/hooks/claude.js +6 -0
- package/dist/hooks/claude.js.map +11 -0
- package/dist/hooks/codex.js +10 -0
- package/dist/hooks/codex.js.map +11 -0
- package/dist/mcp/index.js +100 -0
- package/dist/mcp/index.js.map +231 -0
- package/dist/oc/index.js +21 -0
- package/dist/oc/index.js.map +10 -0
- package/package.json +16 -13
- package/src/api/docs/sessions.keys.ts +0 -6
- package/src/api/docs/sessions.ts +0 -82
- package/src/api/docs/system.keys.ts +0 -5
- package/src/api/docs/system.ts +0 -69
- package/src/api/docs/usage.keys.ts +0 -3
- package/src/api/docs/usage.ts +0 -24
- package/src/api/docs/webhooks.keys.ts +0 -5
- package/src/api/docs/webhooks.ts +0 -78
- package/src/api/index.ts +0 -18
- package/src/api/lib/auth.ts +0 -43
- package/src/api/lib/errors.ts +0 -28
- package/src/api/lib/route.ts +0 -18
- package/src/api/models/claude.ts +0 -4
- package/src/api/models/codex.ts +0 -4
- package/src/api/models/opencode.ts +0 -11
- package/src/api/routes/sessions.ts +0 -51
- package/src/api/routes/system.ts +0 -106
- package/src/api/routes/usage.ts +0 -15
- package/src/api/routes/webhooks/claude.ts +0 -149
- package/src/api/routes/webhooks/codex.ts +0 -175
- package/src/api/routes/webhooks/opencode.ts +0 -112
- package/src/api/routes.ts +0 -93
- package/src/api/schemas/common.ts +0 -29
- package/src/api/schemas/sessions.ts +0 -86
- package/src/api/schemas/usage.ts +0 -51
- package/src/api/schemas/webhooks/claude.ts +0 -32
- package/src/api/schemas/webhooks/codex.ts +0 -34
- package/src/api/schemas/webhooks/opencode.ts +0 -29
- package/src/api/services/claude-session.ts +0 -9
- package/src/api/services/codex-session.ts +0 -8
- package/src/api/services/opencode-session.ts +0 -73
- package/src/classifier.ts +0 -206
- package/src/cli/commands.ts +0 -714
- package/src/cli/index.ts +0 -18
- package/src/cli/program.ts +0 -175
- package/src/cli/prompt.ts +0 -44
- package/src/daemon/cli.ts +0 -27
- package/src/daemon/collectors/claude/README.md +0 -108
- package/src/daemon/collectors/claude/collector.ts +0 -423
- package/src/daemon/collectors/claude/discovery.ts +0 -127
- package/src/daemon/collectors/claude/hooks.ts +0 -181
- package/src/daemon/collectors/claude/index.ts +0 -8
- package/src/daemon/collectors/claude/jsonl.ts +0 -85
- package/src/daemon/collectors/claude/transcript.ts +0 -246
- package/src/daemon/collectors/codex/README.md +0 -147
- package/src/daemon/collectors/codex/collector.ts +0 -661
- package/src/daemon/collectors/codex/discovery.ts +0 -104
- package/src/daemon/collectors/codex/hooks.ts +0 -179
- package/src/daemon/collectors/codex/index.ts +0 -7
- package/src/daemon/collectors/codex/jsonl.ts +0 -108
- package/src/daemon/collectors/codex/transcript.ts +0 -299
- package/src/daemon/collectors/git.ts +0 -353
- package/src/daemon/collectors/herdr/collector.ts +0 -128
- package/src/daemon/collectors/herdr/index.ts +0 -2
- package/src/daemon/collectors/herdr/normalization.ts +0 -111
- package/src/daemon/collectors/opencode/README.md +0 -106
- package/src/daemon/collectors/opencode/collector.ts +0 -545
- package/src/daemon/collectors/opencode/discovery.ts +0 -257
- package/src/daemon/collectors/opencode/index.ts +0 -5
- package/src/daemon/collectors/opencode/session-builder.ts +0 -59
- package/src/daemon/collectors/opencode-db.ts +0 -294
- package/src/daemon/collectors/process.ts +0 -201
- package/src/daemon/collectors/tmux/README.md +0 -128
- package/src/daemon/collectors/tmux/collector.ts +0 -492
- package/src/daemon/collectors/tmux/enrichment.ts +0 -242
- package/src/daemon/collectors/tmux/index.ts +0 -4
- package/src/daemon/collectors/tmux/parse.ts +0 -128
- package/src/daemon/collectors/usage.ts +0 -259
- package/src/daemon/index.ts +0 -209
- package/src/daemon/scheduler.ts +0 -86
- package/src/daemon/store.ts +0 -630
- package/src/detect.ts +0 -143
- package/src/hooks/claude.ts +0 -596
- package/src/hooks/codex.ts +0 -517
- package/src/legacy/classifier.ts +0 -215
- package/src/legacy/detect.ts +0 -143
- package/src/mcp/client-server.ts +0 -21
- package/src/mcp/http-handler.ts +0 -126
- package/src/mcp/index.ts +0 -61
- package/src/mcp/resources/config-current.ts +0 -19
- package/src/mcp/resources/index.ts +0 -11
- package/src/mcp/resources/sessions-active.ts +0 -19
- package/src/mcp/resources/sessions-all.ts +0 -19
- package/src/mcp/schemas/sessions.ts +0 -3
- package/src/mcp/server.ts +0 -17
- package/src/mcp/services/api-adapter.ts +0 -48
- package/src/mcp/services/data-source.ts +0 -22
- package/src/mcp/services/store-adapter.ts +0 -40
- package/src/mcp/tools/get-active-sessions.ts +0 -16
- package/src/mcp/tools/get-session-children.ts +0 -24
- package/src/mcp/tools/get-session-messages.ts +0 -24
- package/src/mcp/tools/get-session.ts +0 -23
- package/src/mcp/tools/index.ts +0 -15
- package/src/mcp/tools/list-sessions.ts +0 -16
- package/src/oc/index.ts +0 -302
- package/src/service/manager.ts +0 -284
- package/src/shared/config.ts +0 -511
- package/src/shared/ipc.ts +0 -80
- package/src/shared/logger.ts +0 -248
- package/src/shared/paths.ts +0 -73
- package/src/shared/platform.ts +0 -110
- package/src/shared/session-exclusions.ts +0 -5
- package/src/shared/session-view.ts +0 -302
- package/src/shared/terminal-capabilities.ts +0 -130
- package/src/shared/types.ts +0 -330
- package/src/shared/utils.ts +0 -339
- package/src/shared/version.ts +0 -44
- package/src/tui/App.tsx +0 -240
- package/src/tui/client.ts +0 -163
- package/src/tui/components/BindingsModal.tsx +0 -82
- package/src/tui/components/DashboardHeader.tsx +0 -256
- package/src/tui/components/KeyBar.tsx +0 -67
- package/src/tui/components/SessionDetailModal.tsx +0 -169
- package/src/tui/components/SessionsView.tsx +0 -184
- package/src/tui/components/TabBar.tsx +0 -40
- package/src/tui/components/UsageSummaryPane.tsx +0 -272
- package/src/tui/components/UsageView.tsx +0 -242
- package/src/tui/components/tool-icons.ts +0 -24
- package/src/tui/index.tsx +0 -106
- package/src/tui/keybindings.ts +0 -49
- package/src/tui/model.ts +0 -295
- package/src/tui/state.ts +0 -77
- package/src/tui/theme.ts +0 -110
- package/src/types.ts +0 -113
- package/src/usage/README.md +0 -323
- package/src/usage/api.ts +0 -99
- package/src/usage/client.ts +0 -3
- package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
- package/src/usage/clients/anthropic-web-usage.ts +0 -329
- package/src/usage/clients/claude-cli-probe.ts +0 -513
- package/src/usage/clients/codex-app-server.ts +0 -354
- package/src/usage/clients/codex-oauth-usage.ts +0 -240
- package/src/usage/clients/opencode-go-api.ts +0 -100
- package/src/usage/clients/opencode-go-usage.ts +0 -577
- package/src/usage/errors.ts +0 -33
- package/src/usage/index.ts +0 -20
- package/src/usage/local/opencode-go-local.ts +0 -194
- package/src/usage/mappers.ts +0 -282
- package/src/usage/planner.ts +0 -118
- package/src/usage/providers/claude.ts +0 -233
- package/src/usage/providers/codex.ts +0 -115
- package/src/usage/providers/opencode-go.ts +0 -115
- package/src/usage/resolver.ts +0 -4
- package/src/usage/resolvers/browser-cookies.ts +0 -808
- package/src/usage/resolvers/claude-auth.ts +0 -208
- package/src/usage/resolvers/claude-cli.ts +0 -24
- package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
- package/src/usage/resolvers/claude-web.ts +0 -82
- package/src/usage/resolvers/codex-auth.ts +0 -174
- package/src/usage/resolvers/keychain-coordinator.ts +0 -397
- package/src/usage/resolvers/opencode-go-web.ts +0 -55
- package/src/usage/types.ts +0 -225
- package/src/usage/web-session-cache.ts +0 -37
- /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
|
@@ -1,808 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { createDecipheriv, pbkdf2Sync } from "node:crypto";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { Database } from "bun:sqlite";
|
|
6
|
-
import { IS_MAC, IS_WINDOWS } from "../../shared/platform";
|
|
7
|
-
import { keychainCoordinator, type KeychainCoordinator } from "./keychain-coordinator";
|
|
8
|
-
|
|
9
|
-
export interface BrowserCookie {
|
|
10
|
-
name: string;
|
|
11
|
-
value: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface BrowserCookieCandidate {
|
|
15
|
-
browser: string;
|
|
16
|
-
profile: string;
|
|
17
|
-
database: string;
|
|
18
|
-
cookies: BrowserCookie[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Browser catalog types
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export interface ChromiumProfileEntry {
|
|
26
|
-
/** Unique browser ID, e.g. "chrome", "brave", "claude-desktop" */
|
|
27
|
-
id: string;
|
|
28
|
-
/** Relative path under ~/Library/Application Support/ on macOS */
|
|
29
|
-
macOSPath: string;
|
|
30
|
-
/** Relative path under ~/.config/ on Linux */
|
|
31
|
-
linuxPath: string;
|
|
32
|
-
/** Relative path under %APPDATA%/ on Windows */
|
|
33
|
-
windowsPath: string;
|
|
34
|
-
/** macOS Keychain service label, e.g. "Chrome Safe Storage" */
|
|
35
|
-
keychainService?: string;
|
|
36
|
-
/** macOS Keychain account label, e.g. "Chrome" */
|
|
37
|
-
keychainAccount?: string;
|
|
38
|
-
/**
|
|
39
|
-
* Top-level cookie database file (relative to the resolved platform root)
|
|
40
|
-
* attempted when the root has no profile subdirectories. Claude Desktop
|
|
41
|
-
* keeps its Chromium cookie database at the application support root.
|
|
42
|
-
*/
|
|
43
|
-
rootCookiePath?: string;
|
|
44
|
-
/**
|
|
45
|
-
* Apply the Chrome v24+ 32-byte domain-hash strip even when the database
|
|
46
|
-
* lacks version metadata. Required for the Claude Desktop database, whose
|
|
47
|
-
* decrypted values carry the strip but report no meta version.
|
|
48
|
-
*/
|
|
49
|
-
forceDomainHashStrip?: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface GeckoProfileEntry {
|
|
53
|
-
/** Unique browser ID, e.g. "firefox", "zen" */
|
|
54
|
-
id: string;
|
|
55
|
-
/**
|
|
56
|
-
* Profiles parent folder name.
|
|
57
|
-
* macOS: ~/Library/Application Support/<profilesFolder>/Profiles
|
|
58
|
-
* Linux: ~/.mozilla/<profilesFolder>/Profiles
|
|
59
|
-
*/
|
|
60
|
-
profilesFolder: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Browser catalogs
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
export const CHROMIUM_PROFILES: ChromiumProfileEntry[] = [
|
|
68
|
-
{
|
|
69
|
-
id: "chrome",
|
|
70
|
-
macOSPath: "Google/Chrome",
|
|
71
|
-
linuxPath: "google-chrome",
|
|
72
|
-
windowsPath: "Google/Chrome/User Data",
|
|
73
|
-
keychainService: "Chrome Safe Storage",
|
|
74
|
-
keychainAccount: "Chrome",
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: "edge",
|
|
78
|
-
macOSPath: "Microsoft Edge",
|
|
79
|
-
linuxPath: "microsoft-edge",
|
|
80
|
-
windowsPath: "Microsoft/Edge/User Data",
|
|
81
|
-
keychainService: "Microsoft Edge Safe Storage",
|
|
82
|
-
keychainAccount: "Microsoft Edge",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "brave",
|
|
86
|
-
macOSPath: "BraveSoftware/Brave-Browser",
|
|
87
|
-
linuxPath: "BraveSoftware/Brave-Browser",
|
|
88
|
-
windowsPath: "BraveSoftware/Brave-Browser/User Data",
|
|
89
|
-
keychainService: "Brave Safe Storage",
|
|
90
|
-
keychainAccount: "Brave",
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
id: "arc",
|
|
94
|
-
macOSPath: "Arc/User Data",
|
|
95
|
-
linuxPath: "Arc",
|
|
96
|
-
windowsPath: "Arc/User Data",
|
|
97
|
-
keychainService: "Arc Safe Storage",
|
|
98
|
-
keychainAccount: "Arc",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "vivaldi",
|
|
102
|
-
macOSPath: "Vivaldi",
|
|
103
|
-
linuxPath: "vivaldi",
|
|
104
|
-
windowsPath: "Vivaldi/User Data",
|
|
105
|
-
keychainService: "Vivaldi Safe Storage",
|
|
106
|
-
keychainAccount: "Vivaldi",
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: "chromium",
|
|
110
|
-
macOSPath: "Chromium",
|
|
111
|
-
linuxPath: "chromium",
|
|
112
|
-
windowsPath: "Chromium/User Data",
|
|
113
|
-
keychainService: "Chromium Safe Storage",
|
|
114
|
-
keychainAccount: "Chromium",
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
id: "yandex",
|
|
118
|
-
macOSPath: "Yandex/YandexBrowser",
|
|
119
|
-
linuxPath: "yandex-browser",
|
|
120
|
-
windowsPath: "Yandex/YandexBrowser/User Data",
|
|
121
|
-
keychainService: "Yandex Safe Storage",
|
|
122
|
-
keychainAccount: "Yandex",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
// Claude Desktop app (Electron/Chromium) — macOS and Windows only.
|
|
126
|
-
// Keeps its Chromium cookie database at the application support root with
|
|
127
|
-
// no profile subdirectories; its encrypted values carry the Chrome v24+
|
|
128
|
-
// 32-byte domain-hash prefix despite reporting no meta version.
|
|
129
|
-
id: "claude-desktop",
|
|
130
|
-
macOSPath: "Claude",
|
|
131
|
-
linuxPath: "", // not available on Linux
|
|
132
|
-
windowsPath: "Anthropic/Claude",
|
|
133
|
-
keychainService: "Claude Safe Storage",
|
|
134
|
-
keychainAccount: "Claude",
|
|
135
|
-
rootCookiePath: "Cookies",
|
|
136
|
-
forceDomainHashStrip: true,
|
|
137
|
-
},
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
export const GECKO_PROFILES: GeckoProfileEntry[] = [
|
|
141
|
-
{ id: "firefox", profilesFolder: "Firefox" },
|
|
142
|
-
{ id: "zen", profilesFolder: "zen" },
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
const SAFARI_COOKIE_PATH = join(homedir(), "Library", "Cookies", "Cookies.binarycookies");
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Platform root resolution
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
function chromiumRootPath(entry: ChromiumProfileEntry): string {
|
|
152
|
-
const home = homedir();
|
|
153
|
-
if (IS_MAC) {
|
|
154
|
-
if (!entry.macOSPath) return "";
|
|
155
|
-
return join(home, "Library", "Application Support", entry.macOSPath);
|
|
156
|
-
}
|
|
157
|
-
if (IS_WINDOWS) {
|
|
158
|
-
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
159
|
-
if (!entry.windowsPath) return "";
|
|
160
|
-
return join(appData, entry.windowsPath);
|
|
161
|
-
}
|
|
162
|
-
// Linux
|
|
163
|
-
if (!entry.linuxPath) return "";
|
|
164
|
-
return join(home, ".config", entry.linuxPath);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function geckoProfilesRoot(entry: GeckoProfileEntry): string {
|
|
168
|
-
const home = homedir();
|
|
169
|
-
if (IS_MAC) {
|
|
170
|
-
return join(home, "Library", "Application Support", entry.profilesFolder, "Profiles");
|
|
171
|
-
}
|
|
172
|
-
if (IS_WINDOWS) {
|
|
173
|
-
// Firefox on Windows is not in scope; skip
|
|
174
|
-
return "";
|
|
175
|
-
}
|
|
176
|
-
// Linux
|
|
177
|
-
if (entry.id === "firefox") return join(home, ".mozilla", "firefox");
|
|
178
|
-
if (entry.id === "zen") return join(home, ".zen");
|
|
179
|
-
return join(home, ".mozilla", entry.profilesFolder.toLowerCase());
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function cookieDomainMatches(host: string, domain: string): boolean {
|
|
183
|
-
const normalizedHost = host.trim().toLowerCase().replace(/^\./, "");
|
|
184
|
-
const normalizedDomain = domain.trim().toLowerCase().replace(/^\./, "");
|
|
185
|
-
return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function cookieNameAllowed(name: string, allowedNames?: ReadonlySet<string>): boolean {
|
|
189
|
-
return !allowedNames || allowedNames.has(name);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
// Chromium helpers
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Derives the Chromium AES-128-CBC key for a given Keychain service label.
|
|
198
|
-
* macOS: reads through the Keychain coordinator (async, single-flight, cooldown).
|
|
199
|
-
* Linux/Windows: uses the fixed "peanuts" PBKDF2 key (no Keychain).
|
|
200
|
-
*/
|
|
201
|
-
export async function getChromiumSafeStorageKey(
|
|
202
|
-
service: string,
|
|
203
|
-
account: string,
|
|
204
|
-
coordinator: KeychainCoordinator = keychainCoordinator,
|
|
205
|
-
): Promise<Buffer | null> {
|
|
206
|
-
try {
|
|
207
|
-
if (IS_MAC) {
|
|
208
|
-
const password = await coordinator.read(service, account);
|
|
209
|
-
if (!password) return null;
|
|
210
|
-
return pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (IS_WINDOWS) {
|
|
214
|
-
// Chromium uses DPAPI on Windows; plaintext rows can still be imported.
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Linux
|
|
219
|
-
return pbkdf2Sync("peanuts", "saltysalt", 1003, 16, "sha1");
|
|
220
|
-
} catch {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Returns all profile subdirectory paths under rootPath that match the
|
|
227
|
-
* Chromium naming conventions: Default, Profile *, user-*.
|
|
228
|
-
*/
|
|
229
|
-
export function discoverChromiumProfiles(rootPath: string): string[] {
|
|
230
|
-
if (!existsSync(rootPath)) return [];
|
|
231
|
-
let entries: string[];
|
|
232
|
-
try {
|
|
233
|
-
entries = readdirSync(rootPath, { withFileTypes: true })
|
|
234
|
-
.filter((d) => d.isDirectory())
|
|
235
|
-
.map((d) => d.name);
|
|
236
|
-
} catch {
|
|
237
|
-
return [];
|
|
238
|
-
}
|
|
239
|
-
return entries
|
|
240
|
-
.filter((name) => name === "Default" || /^Profile \d+$/.test(name) || /^user-/.test(name))
|
|
241
|
-
.map((name) => join(rootPath, name));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Returns the two candidate cookie DB paths for a Chromium profile directory.
|
|
246
|
-
* The caller is responsible for checking existence before opening.
|
|
247
|
-
*/
|
|
248
|
-
export function chromiumCookieDbPaths(profilePath: string): string[] {
|
|
249
|
-
return [join(profilePath, "Cookies"), join(profilePath, "Network", "Cookies")];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Internal: decrypt AES-128-CBC safeStorage payload, returning raw bytes after
|
|
254
|
-
* PKCS#7 un-padding. Returns `null` on decryption failure. Callers that need
|
|
255
|
-
* the Chrome v24+ 32-byte domain-hash strip must apply it themselves.
|
|
256
|
-
*/
|
|
257
|
-
function decryptSafeStorageRaw(buf: Buffer, key: Buffer): Buffer | null {
|
|
258
|
-
if (buf.length < 3) return null;
|
|
259
|
-
const prefix = buf.slice(0, 3).toString("ascii");
|
|
260
|
-
if (prefix !== "v10" && prefix !== "v11") {
|
|
261
|
-
return buf; // plain text (older versions)
|
|
262
|
-
}
|
|
263
|
-
const iv = Buffer.alloc(16, 32); // space character (0x20) repeated 16 times
|
|
264
|
-
try {
|
|
265
|
-
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
266
|
-
let decrypted = Buffer.concat([decipher.update(buf.slice(3)), decipher.final()]);
|
|
267
|
-
// Remove PKCS#7 padding
|
|
268
|
-
const padLen = decrypted[decrypted.length - 1];
|
|
269
|
-
if (padLen > 0 && padLen <= 16) {
|
|
270
|
-
decrypted = decrypted.slice(0, decrypted.length - padLen);
|
|
271
|
-
}
|
|
272
|
-
return decrypted;
|
|
273
|
-
} catch {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Decrypt a Chromium/Electron safeStorage blob (AES-128-CBC, v10/v11 prefix)
|
|
280
|
-
* and return the result as a UTF-8 string.
|
|
281
|
-
*
|
|
282
|
-
* Used for Electron `safeStorage`-encrypted config fields (e.g. `oauth:tokenCache`
|
|
283
|
-
* in Claude Desktop's `config.json`). Does **not** strip the Chrome v24+ 32-byte
|
|
284
|
-
* domain-hash prefix — use `decryptChromiumCookie` for cookie DB values.
|
|
285
|
-
*/
|
|
286
|
-
export function decryptSafeStorageBlob(buf: Buffer, key: Buffer): string | null {
|
|
287
|
-
const raw = decryptSafeStorageRaw(buf, key);
|
|
288
|
-
return raw !== null ? raw.toString("utf8") : null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function decryptChromiumCookie(encrypted: Buffer, key: Buffer, dbVersion = 0): string | null {
|
|
292
|
-
const raw = decryptSafeStorageRaw(encrypted, key);
|
|
293
|
-
if (raw === null) return null;
|
|
294
|
-
// Chrome v24+ prepends a SHA256 hash of the domain (32 bytes) to encrypted cookie values
|
|
295
|
-
if (encrypted.length >= 3) {
|
|
296
|
-
const prefix = encrypted.slice(0, 3).toString("ascii");
|
|
297
|
-
if ((prefix === "v10" || prefix === "v11") && dbVersion >= 24 && raw.length >= 32) {
|
|
298
|
-
return raw.slice(32).toString("utf8");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return raw.toString("utf8");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export interface ChromiumCookieRows {
|
|
305
|
-
dbVersion: number;
|
|
306
|
-
plaintext: BrowserCookie[];
|
|
307
|
-
encrypted: Array<{ name: string; encrypted: Buffer }>;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Reads and filters target cookie rows from a Chromium database without any
|
|
312
|
-
* Safe Storage key request. Rows are partitioned into plaintext values and
|
|
313
|
-
* encrypted-only rows so callers can decide whether a key read is warranted.
|
|
314
|
-
*/
|
|
315
|
-
export function readChromiumCookieRows(
|
|
316
|
-
dbPath: string,
|
|
317
|
-
domain: string,
|
|
318
|
-
allowedNames?: ReadonlySet<string>,
|
|
319
|
-
options?: { forceDomainHashStrip?: boolean },
|
|
320
|
-
): ChromiumCookieRows {
|
|
321
|
-
const rows: ChromiumCookieRows = { dbVersion: 0, plaintext: [], encrypted: [] };
|
|
322
|
-
let db: Database | undefined;
|
|
323
|
-
try {
|
|
324
|
-
db = new Database(dbPath, { readonly: true });
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
const versionRow = db.query("SELECT value FROM meta WHERE key = 'version'").get() as {
|
|
328
|
-
value: number;
|
|
329
|
-
} | null;
|
|
330
|
-
if (versionRow) rows.dbVersion = Number(versionRow.value);
|
|
331
|
-
} catch {
|
|
332
|
-
// Older databases may not have a meta table.
|
|
333
|
-
}
|
|
334
|
-
if (options?.forceDomainHashStrip && rows.dbVersion < 24) {
|
|
335
|
-
rows.dbVersion = 24;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const columns = db.query("PRAGMA table_info(cookies)").all() as Array<{ name: string }>;
|
|
339
|
-
const hasPlaintextValue = columns.some((column) => column.name === "value");
|
|
340
|
-
const valueColumn = hasPlaintextValue ? "value" : "'' AS value";
|
|
341
|
-
const rawRows = db.query(
|
|
342
|
-
`SELECT name, encrypted_value, ${valueColumn}, host_key, expires_utc FROM cookies WHERE host_key LIKE ?`,
|
|
343
|
-
).all(`%${domain}`) as Array<{
|
|
344
|
-
name: string;
|
|
345
|
-
encrypted_value: Uint8Array | null;
|
|
346
|
-
value: string | null;
|
|
347
|
-
host_key: string;
|
|
348
|
-
expires_utc: number;
|
|
349
|
-
}>;
|
|
350
|
-
// expires_utc is microseconds since the Chrome epoch (1601-01-01).
|
|
351
|
-
const now = (Date.now() / 1000 + 11644473600) * 1_000_000;
|
|
352
|
-
|
|
353
|
-
for (const row of rawRows) {
|
|
354
|
-
if (!cookieDomainMatches(row.host_key, domain) || !cookieNameAllowed(row.name, allowedNames)) {
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const expiresUtc = Number(row.expires_utc);
|
|
358
|
-
if (expiresUtc > 0 && expiresUtc <= now) continue;
|
|
359
|
-
|
|
360
|
-
const plaintext = row.value || null;
|
|
361
|
-
if (plaintext !== null) {
|
|
362
|
-
rows.plaintext.push({ name: row.name, value: plaintext });
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
const encrypted = row.encrypted_value ? Buffer.from(row.encrypted_value) : null;
|
|
366
|
-
if (encrypted) rows.encrypted.push({ name: row.name, encrypted });
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
// Ignore permission errors, locked DB (SQLITE_BUSY), and incompatible schemas.
|
|
370
|
-
} finally {
|
|
371
|
-
db?.close();
|
|
372
|
-
}
|
|
373
|
-
return rows;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function resolveChromiumCookies(rows: ChromiumCookieRows, key: Buffer | null): BrowserCookie[] {
|
|
377
|
-
const cookies = [...rows.plaintext];
|
|
378
|
-
if (!key) return cookies;
|
|
379
|
-
for (const pending of rows.encrypted) {
|
|
380
|
-
const value = decryptChromiumCookie(pending.encrypted, key, rows.dbVersion);
|
|
381
|
-
if (value !== null) cookies.push({ name: pending.name, value });
|
|
382
|
-
}
|
|
383
|
-
return cookies;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function readChromiumCookieDatabase(
|
|
387
|
-
dbPath: string,
|
|
388
|
-
domain: string,
|
|
389
|
-
key: Buffer | null,
|
|
390
|
-
allowedNames?: ReadonlySet<string>,
|
|
391
|
-
): BrowserCookie[] {
|
|
392
|
-
return resolveChromiumCookies(readChromiumCookieRows(dbPath, domain, allowedNames), key);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Reads one Chromium profile database as an isolated candidate. A Safe Storage
|
|
397
|
-
* key is requested through the coordinator only when filtered rows contain an
|
|
398
|
-
* encrypted target cookie; plaintext rows and absent targets never trigger one.
|
|
399
|
-
*/
|
|
400
|
-
export async function readChromiumCookieCandidate(
|
|
401
|
-
profilePath: string,
|
|
402
|
-
dbPath: string,
|
|
403
|
-
entry: ChromiumProfileEntry,
|
|
404
|
-
domain: string,
|
|
405
|
-
allowedNames?: ReadonlySet<string>,
|
|
406
|
-
coordinator: KeychainCoordinator = keychainCoordinator,
|
|
407
|
-
): Promise<BrowserCookieCandidate | null> {
|
|
408
|
-
const rows = readChromiumCookieRows(dbPath, domain, allowedNames, {
|
|
409
|
-
forceDomainHashStrip: entry.forceDomainHashStrip,
|
|
410
|
-
});
|
|
411
|
-
if (rows.plaintext.length === 0 && rows.encrypted.length === 0) return null;
|
|
412
|
-
|
|
413
|
-
let cookies = rows.plaintext;
|
|
414
|
-
if (rows.encrypted.length > 0) {
|
|
415
|
-
const key = await getChromiumSafeStorageKey(entry.keychainService ?? "", entry.keychainAccount ?? "", coordinator);
|
|
416
|
-
if (!key) {
|
|
417
|
-
return rows.plaintext.length > 0
|
|
418
|
-
? { browser: entry.id, profile: profilePath, database: dbPath, cookies: rows.plaintext }
|
|
419
|
-
: null;
|
|
420
|
-
}
|
|
421
|
-
cookies = resolveChromiumCookies(rows, key);
|
|
422
|
-
}
|
|
423
|
-
if (cookies.length === 0) return null;
|
|
424
|
-
return { browser: entry.id, profile: profilePath, database: dbPath, cookies };
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export async function readChromiumCookieCandidates(
|
|
428
|
-
entry: ChromiumProfileEntry,
|
|
429
|
-
domain: string,
|
|
430
|
-
allowedNames?: ReadonlySet<string>,
|
|
431
|
-
coordinator: KeychainCoordinator = keychainCoordinator,
|
|
432
|
-
): Promise<BrowserCookieCandidate[]> {
|
|
433
|
-
const candidates: BrowserCookieCandidate[] = [];
|
|
434
|
-
const rootPath = chromiumRootPath(entry);
|
|
435
|
-
if (!rootPath || !existsSync(rootPath)) return candidates;
|
|
436
|
-
|
|
437
|
-
// No Safe Storage key is requested here: each profile database requests the
|
|
438
|
-
// key lazily through readChromiumCookieCandidate only when an encrypted
|
|
439
|
-
// target cookie survives filtering.
|
|
440
|
-
const profiles = discoverChromiumProfiles(rootPath);
|
|
441
|
-
for (const profilePath of profiles) {
|
|
442
|
-
for (const dbPath of chromiumCookieDbPaths(profilePath)) {
|
|
443
|
-
if (!existsSync(dbPath)) continue;
|
|
444
|
-
const candidate = await readChromiumCookieCandidate(profilePath, dbPath, entry, domain, allowedNames, coordinator);
|
|
445
|
-
if (candidate) candidates.push(candidate);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
// Root-level cookie databases (Claude Desktop): attempt when the root holds
|
|
449
|
-
// no profile subdirectories.
|
|
450
|
-
if (profiles.length === 0 && entry.rootCookiePath) {
|
|
451
|
-
const rootDb = join(rootPath, entry.rootCookiePath);
|
|
452
|
-
if (existsSync(rootDb)) {
|
|
453
|
-
const candidate = await readChromiumCookieCandidate(rootPath, rootDb, entry, domain, allowedNames, coordinator);
|
|
454
|
-
if (candidate) candidates.push(candidate);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return candidates;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export async function readChromiumCookies(entry: ChromiumProfileEntry, domain: string): Promise<BrowserCookie[]> {
|
|
462
|
-
const candidates = await readChromiumCookieCandidates(entry, domain);
|
|
463
|
-
return candidates.flatMap((candidate) => candidate.cookies);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// ---------------------------------------------------------------------------
|
|
467
|
-
// Gecko (Firefox/Zen) support
|
|
468
|
-
// ---------------------------------------------------------------------------
|
|
469
|
-
|
|
470
|
-
export function readGeckoCookieCandidates(
|
|
471
|
-
entry: GeckoProfileEntry,
|
|
472
|
-
domain: string,
|
|
473
|
-
allowedNames?: ReadonlySet<string>,
|
|
474
|
-
): BrowserCookieCandidate[] {
|
|
475
|
-
const candidates: BrowserCookieCandidate[] = [];
|
|
476
|
-
const profilesRoot = geckoProfilesRoot(entry);
|
|
477
|
-
if (!profilesRoot || !existsSync(profilesRoot)) return candidates;
|
|
478
|
-
|
|
479
|
-
let profileNames: string[];
|
|
480
|
-
try {
|
|
481
|
-
profileNames = readdirSync(profilesRoot, { withFileTypes: true })
|
|
482
|
-
.filter((d) => d.isDirectory() && d.name.includes(".default"))
|
|
483
|
-
.map((d) => d.name)
|
|
484
|
-
// Prefer .default-release over .default
|
|
485
|
-
.sort((a, b) => {
|
|
486
|
-
const aScore = a.endsWith(".default-release") ? 1 : 0;
|
|
487
|
-
const bScore = b.endsWith(".default-release") ? 1 : 0;
|
|
488
|
-
return bScore - aScore;
|
|
489
|
-
});
|
|
490
|
-
} catch {
|
|
491
|
-
return candidates;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
for (const profileName of profileNames) {
|
|
495
|
-
const dbPath = join(profilesRoot, profileName, "cookies.sqlite");
|
|
496
|
-
if (!existsSync(dbPath)) continue;
|
|
497
|
-
let db: Database | undefined;
|
|
498
|
-
try {
|
|
499
|
-
db = new Database(dbPath, { readonly: true });
|
|
500
|
-
const stmt = db.query("SELECT name, value, host, expiry FROM moz_cookies WHERE host LIKE ?");
|
|
501
|
-
const rows = stmt.all(`%${domain}`) as Array<{
|
|
502
|
-
name: string;
|
|
503
|
-
value: string;
|
|
504
|
-
host: string;
|
|
505
|
-
expiry: number;
|
|
506
|
-
}>;
|
|
507
|
-
const cookies: BrowserCookie[] = [];
|
|
508
|
-
for (const row of rows) {
|
|
509
|
-
if (!cookieDomainMatches(row.host, domain) || !cookieNameAllowed(row.name, allowedNames)) continue;
|
|
510
|
-
if (Number(row.expiry) > 0 && Number(row.expiry) <= Date.now() / 1000) continue;
|
|
511
|
-
cookies.push({ name: row.name, value: row.value });
|
|
512
|
-
}
|
|
513
|
-
if (cookies.length > 0) {
|
|
514
|
-
candidates.push({ browser: entry.id, profile: join(profilesRoot, profileName), database: dbPath, cookies });
|
|
515
|
-
}
|
|
516
|
-
} catch {
|
|
517
|
-
// Ignore locked or corrupt DB
|
|
518
|
-
} finally {
|
|
519
|
-
db?.close();
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return candidates;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
export function readGeckoCookies(entry: GeckoProfileEntry, domain: string): BrowserCookie[] {
|
|
527
|
-
return readGeckoCookieCandidates(entry, domain).flatMap((candidate) => candidate.cookies);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
// Safari (macOS-only, unchanged)
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
|
|
534
|
-
function readSafariCookies(domain: string): BrowserCookie[] {
|
|
535
|
-
const cookies: BrowserCookie[] = [];
|
|
536
|
-
if (process.platform !== "darwin") return cookies;
|
|
537
|
-
if (!existsSync(SAFARI_COOKIE_PATH)) return cookies;
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
const data = readFileSync(SAFARI_COOKIE_PATH);
|
|
541
|
-
// Simple binarycookies parser
|
|
542
|
-
// Header: 4 bytes magic "cook", 4 bytes page count, then pages
|
|
543
|
-
if (data.length < 8 || data.toString("ascii", 0, 4) !== "cook") return cookies;
|
|
544
|
-
|
|
545
|
-
const numPages = data.readUInt32BE(4);
|
|
546
|
-
let offset = 8;
|
|
547
|
-
const pageSizes: number[] = [];
|
|
548
|
-
for (let i = 0; i < numPages; i++) {
|
|
549
|
-
pageSizes.push(data.readUInt32BE(offset));
|
|
550
|
-
offset += 4;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
for (const pageSize of pageSizes) {
|
|
554
|
-
const pageEnd = offset + pageSize;
|
|
555
|
-
if (pageEnd > data.length) break;
|
|
556
|
-
// Page header: "page" + 4 bytes count
|
|
557
|
-
if (data.toString("ascii", offset, offset + 4) !== "page") {
|
|
558
|
-
offset += pageSize;
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
const numCookies = data.readUInt32BE(offset + 4);
|
|
562
|
-
let cookieOffset = offset + 8;
|
|
563
|
-
for (let i = 0; i < numCookies; i++) {
|
|
564
|
-
// Each cookie record: size (4) + unknown (4) + flags (4) + hasPort (4)
|
|
565
|
-
// + port (4) + urlOffset (4) + nameOffset (4) + pathOffset (4) + valueOffset (4)
|
|
566
|
-
// + tail (8) = 40 bytes header, then strings
|
|
567
|
-
const size = data.readUInt32BE(cookieOffset);
|
|
568
|
-
const urlOffset = data.readUInt32BE(cookieOffset + 20);
|
|
569
|
-
const nameOffset = data.readUInt32BE(cookieOffset + 24);
|
|
570
|
-
const pathOffset = data.readUInt32BE(cookieOffset + 28);
|
|
571
|
-
const valueOffset = data.readUInt32BE(cookieOffset + 32);
|
|
572
|
-
|
|
573
|
-
const url = data.toString("utf8", cookieOffset + urlOffset, cookieOffset + nameOffset - 1);
|
|
574
|
-
const name = data.toString("utf8", cookieOffset + nameOffset, cookieOffset + pathOffset - 1);
|
|
575
|
-
const value = data.toString("utf8", cookieOffset + valueOffset, cookieOffset + size);
|
|
576
|
-
|
|
577
|
-
if (cookieDomainMatches(url, domain)) {
|
|
578
|
-
cookies.push({ name, value });
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
cookieOffset += size;
|
|
582
|
-
if (cookieOffset > pageEnd) break;
|
|
583
|
-
}
|
|
584
|
-
offset += pageSize;
|
|
585
|
-
}
|
|
586
|
-
} catch {
|
|
587
|
-
// Ignore errors
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return cookies;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// ---------------------------------------------------------------------------
|
|
594
|
-
// Public API
|
|
595
|
-
// ---------------------------------------------------------------------------
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Extract cookies for the given domain from all available browser sources.
|
|
599
|
-
* Sources are tried in an order that minimises Keychain prompts:
|
|
600
|
-
* macOS: Gecko (Firefox/Zen) → Safari → Chromium catalog
|
|
601
|
-
* Linux: Gecko → Chromium catalog
|
|
602
|
-
* Windows: Gecko (limited) → Chromium catalog
|
|
603
|
-
*
|
|
604
|
-
* Returns the first source that yields ≥1 matching cookie; subsequent sources
|
|
605
|
-
* are skipped (first-source-wins).
|
|
606
|
-
*/
|
|
607
|
-
export async function extractCookies(domain: string): Promise<BrowserCookie[]> {
|
|
608
|
-
// 1. Gecko sources first — no Keychain prompt, cheap read
|
|
609
|
-
for (const entry of GECKO_PROFILES) {
|
|
610
|
-
const cookies = readGeckoCookies(entry, domain);
|
|
611
|
-
if (cookies.length > 0) return cookies;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// 2. Safari (macOS only) — no Keychain prompt
|
|
615
|
-
if (IS_MAC) {
|
|
616
|
-
const safari = readSafariCookies(domain);
|
|
617
|
-
if (safari.length > 0) return safari;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// 3. Chromium catalog — may trigger Keychain prompts on macOS
|
|
621
|
-
for (const entry of CHROMIUM_PROFILES) {
|
|
622
|
-
const cookies = await readChromiumCookies(entry, domain);
|
|
623
|
-
if (cookies.length > 0) return cookies;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return [];
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
export interface BrowserCookieCursorOptions {
|
|
630
|
-
/** Browser catalog ids to exclude from Chromium scans (e.g. claude-desktop). */
|
|
631
|
-
excludeBrowsers?: ReadonlySet<string>;
|
|
632
|
-
/** Test seam: override the Safari cookie reader. */
|
|
633
|
-
safariReader?: (domain: string) => BrowserCookie[];
|
|
634
|
-
/** Test seam: override the Chromium root path resolution per browser id. */
|
|
635
|
-
chromiumRootResolver?: (entry: ChromiumProfileEntry) => string;
|
|
636
|
-
/** Test seam: use an isolated Keychain coordinator. */
|
|
637
|
-
coordinator?: KeychainCoordinator;
|
|
638
|
-
/**
|
|
639
|
-
* Catalog entry id attempted before Gecko (Claude Desktop for the claude.ai
|
|
640
|
-
* cursor). Other cursors leave this unset and keep the standard order.
|
|
641
|
-
*/
|
|
642
|
-
preferredBrowserId?: string;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Demand-driven browser cookie candidate iterator. Each `next()` call inspects
|
|
647
|
-
* exactly one source (Gecko profile set, Safari, or one Chromium profile
|
|
648
|
-
* database) and never inspects later sources speculatively. A Safe Storage key
|
|
649
|
-
* is requested only when the inspected database holds an encrypted target
|
|
650
|
-
* cookie.
|
|
651
|
-
*/
|
|
652
|
-
export class BrowserCookieSourceCursor {
|
|
653
|
-
private readonly safariReader: (domain: string) => BrowserCookie[];
|
|
654
|
-
private readonly chromiumRootResolver: (entry: ChromiumProfileEntry) => string;
|
|
655
|
-
private readonly coordinator: KeychainCoordinator;
|
|
656
|
-
private readonly preferredEntry?: ChromiumProfileEntry;
|
|
657
|
-
private geckoIndex = 0;
|
|
658
|
-
private safariChecked = false;
|
|
659
|
-
private chromiumIndex = 0;
|
|
660
|
-
private preferredQueue: Array<{ entry: ChromiumProfileEntry; profilePath: string; dbPath?: string }> = [];
|
|
661
|
-
private preferredQueueBuilt = false;
|
|
662
|
-
private chromiumQueue: Array<{ entry: ChromiumProfileEntry; profilePath: string; dbPath?: string }> = [];
|
|
663
|
-
|
|
664
|
-
constructor(
|
|
665
|
-
private readonly domain: string,
|
|
666
|
-
private readonly allowedCookieNames?: readonly string[],
|
|
667
|
-
private readonly options: BrowserCookieCursorOptions = {},
|
|
668
|
-
) {
|
|
669
|
-
this.safariReader = options.safariReader ?? readSafariCookies;
|
|
670
|
-
this.chromiumRootResolver = options.chromiumRootResolver ?? chromiumRootPath;
|
|
671
|
-
this.coordinator = options.coordinator ?? keychainCoordinator;
|
|
672
|
-
if (options.preferredBrowserId) {
|
|
673
|
-
this.preferredEntry = CHROMIUM_PROFILES.find((entry) => entry.id === options.preferredBrowserId);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
private async pushPreferredSources(
|
|
678
|
-
entry: ChromiumProfileEntry,
|
|
679
|
-
): Promise<void> {
|
|
680
|
-
const rootPath = this.chromiumRootResolver(entry);
|
|
681
|
-
if (!rootPath || !existsSync(rootPath)) return;
|
|
682
|
-
for (const profilePath of discoverChromiumProfiles(rootPath)) {
|
|
683
|
-
this.preferredQueue.push({ entry, profilePath });
|
|
684
|
-
}
|
|
685
|
-
if (this.preferredQueue.length === 0 && entry.rootCookiePath) {
|
|
686
|
-
const rootDb = join(rootPath, entry.rootCookiePath);
|
|
687
|
-
if (existsSync(rootDb)) {
|
|
688
|
-
this.preferredQueue.push({ entry, profilePath: rootPath, dbPath: rootDb });
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
private async nextPreferred(): Promise<BrowserCookieCandidate | null> {
|
|
694
|
-
if (!this.preferredEntry) return null;
|
|
695
|
-
if (!this.preferredQueueBuilt) {
|
|
696
|
-
this.preferredQueueBuilt = true;
|
|
697
|
-
await this.pushPreferredSources(this.preferredEntry);
|
|
698
|
-
}
|
|
699
|
-
while (this.preferredQueue.length > 0) {
|
|
700
|
-
const { entry, profilePath, dbPath } = this.preferredQueue.shift()!;
|
|
701
|
-
for (const path of dbPath ? [dbPath] : chromiumCookieDbPaths(profilePath)) {
|
|
702
|
-
if (!existsSync(path)) continue;
|
|
703
|
-
const candidate = await readChromiumCookieCandidate(
|
|
704
|
-
profilePath,
|
|
705
|
-
path,
|
|
706
|
-
entry,
|
|
707
|
-
this.domain,
|
|
708
|
-
this.allowedCookieNames ? new Set(this.allowedCookieNames) : undefined,
|
|
709
|
-
this.coordinator,
|
|
710
|
-
);
|
|
711
|
-
if (candidate) return candidate;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return null;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async next(): Promise<BrowserCookieCandidate | null> {
|
|
718
|
-
const allowedNames = this.allowedCookieNames ? new Set(this.allowedCookieNames) : undefined;
|
|
719
|
-
|
|
720
|
-
const preferred = await this.nextPreferred();
|
|
721
|
-
if (preferred) return preferred;
|
|
722
|
-
|
|
723
|
-
while (this.geckoIndex < GECKO_PROFILES.length) {
|
|
724
|
-
const entry = GECKO_PROFILES[this.geckoIndex++]!;
|
|
725
|
-
const candidates = readGeckoCookieCandidates(entry, this.domain, allowedNames);
|
|
726
|
-
if (candidates.length > 0) return candidates[0]!;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (IS_MAC && !this.safariChecked) {
|
|
730
|
-
this.safariChecked = true;
|
|
731
|
-
const cookies = this.safariReader(this.domain).filter((cookie) => cookieNameAllowed(cookie.name, allowedNames));
|
|
732
|
-
if (cookies.length > 0) {
|
|
733
|
-
return { browser: "safari", profile: "default", database: SAFARI_COOKIE_PATH, cookies };
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
for (;;) {
|
|
738
|
-
if (this.chromiumQueue.length === 0) {
|
|
739
|
-
const entry = CHROMIUM_PROFILES[this.chromiumIndex++];
|
|
740
|
-
if (!entry) return null;
|
|
741
|
-
if (this.options.excludeBrowsers?.has(entry.id)) continue;
|
|
742
|
-
if (this.preferredEntry && entry.id === this.preferredEntry.id) continue;
|
|
743
|
-
const rootPath = this.chromiumRootResolver(entry);
|
|
744
|
-
if (!rootPath || !existsSync(rootPath)) continue;
|
|
745
|
-
for (const profilePath of discoverChromiumProfiles(rootPath)) {
|
|
746
|
-
this.chromiumQueue.push({ entry, profilePath });
|
|
747
|
-
}
|
|
748
|
-
// Root-level cookie databases (Claude Desktop): attempt when the root
|
|
749
|
-
// holds no profile subdirectories at all.
|
|
750
|
-
if (this.chromiumQueue.length === 0 && entry.rootCookiePath) {
|
|
751
|
-
const rootDb = join(rootPath, entry.rootCookiePath);
|
|
752
|
-
if (existsSync(rootDb)) {
|
|
753
|
-
this.chromiumQueue.push({ entry, profilePath: rootPath, dbPath: rootDb });
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
const { entry, profilePath, dbPath } = this.chromiumQueue.shift()!;
|
|
759
|
-
for (const path of dbPath ? [dbPath] : chromiumCookieDbPaths(profilePath)) {
|
|
760
|
-
if (!existsSync(path)) continue;
|
|
761
|
-
const candidate = await readChromiumCookieCandidate(
|
|
762
|
-
profilePath,
|
|
763
|
-
path,
|
|
764
|
-
entry,
|
|
765
|
-
this.domain,
|
|
766
|
-
allowedNames,
|
|
767
|
-
this.coordinator,
|
|
768
|
-
);
|
|
769
|
-
if (candidate) return candidate;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
export async function extractCookieCandidates(
|
|
776
|
-
domain: string,
|
|
777
|
-
allowedCookieNames?: readonly string[],
|
|
778
|
-
options: BrowserCookieCursorOptions = {},
|
|
779
|
-
): Promise<BrowserCookieCandidate[]> {
|
|
780
|
-
const cursor = new BrowserCookieSourceCursor(domain, allowedCookieNames, options);
|
|
781
|
-
const candidates: BrowserCookieCandidate[] = [];
|
|
782
|
-
for (let candidate = await cursor.next(); candidate !== null; candidate = await cursor.next()) {
|
|
783
|
-
candidates.push(candidate);
|
|
784
|
-
}
|
|
785
|
-
return candidates;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Cheap import-source existence check: no SQLite reads and no Keychain
|
|
790
|
-
* requests. Used by planning to decide whether a deferred browser import
|
|
791
|
-
* could yield candidates at all.
|
|
792
|
-
*/
|
|
793
|
-
export function hasBrowserCookieSources(): boolean {
|
|
794
|
-
for (const entry of GECKO_PROFILES) {
|
|
795
|
-
const root = geckoProfilesRoot(entry);
|
|
796
|
-
if (root && existsSync(root)) return true;
|
|
797
|
-
}
|
|
798
|
-
if (IS_MAC && existsSync(SAFARI_COOKIE_PATH)) return true;
|
|
799
|
-
for (const entry of CHROMIUM_PROFILES) {
|
|
800
|
-
const root = chromiumRootPath(entry);
|
|
801
|
-
if (root && existsSync(root)) return true;
|
|
802
|
-
}
|
|
803
|
-
return false;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
export function buildCookieHeader(cookies: BrowserCookie[]): string {
|
|
807
|
-
return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
808
|
-
}
|