@scrylog/cli 0.3.0 → 0.3.1

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.
Files changed (195) hide show
  1. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/assets/highlights-ghv9g403.scm +205 -0
  3. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/assets/highlights-r812a2qc.scm +150 -0
  5. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/assets/injections-73j83es3.scm +27 -0
  7. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  8. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  9. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  10. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  12. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  13. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  14. package/dist/cli/index.js +499 -0
  15. package/dist/cli/index.js.map +61 -0
  16. package/dist/daemon/index.js +670 -0
  17. package/dist/daemon/index.js.map +1475 -0
  18. package/dist/hooks/claude.js +6 -0
  19. package/dist/hooks/claude.js.map +11 -0
  20. package/dist/hooks/codex.js +10 -0
  21. package/dist/hooks/codex.js.map +11 -0
  22. package/dist/mcp/index.js +100 -0
  23. package/dist/mcp/index.js.map +231 -0
  24. package/dist/oc/index.js +21 -0
  25. package/dist/oc/index.js.map +10 -0
  26. package/package.json +16 -13
  27. package/src/api/docs/sessions.keys.ts +0 -6
  28. package/src/api/docs/sessions.ts +0 -82
  29. package/src/api/docs/system.keys.ts +0 -5
  30. package/src/api/docs/system.ts +0 -69
  31. package/src/api/docs/usage.keys.ts +0 -3
  32. package/src/api/docs/usage.ts +0 -24
  33. package/src/api/docs/webhooks.keys.ts +0 -5
  34. package/src/api/docs/webhooks.ts +0 -78
  35. package/src/api/index.ts +0 -18
  36. package/src/api/lib/auth.ts +0 -43
  37. package/src/api/lib/errors.ts +0 -28
  38. package/src/api/lib/route.ts +0 -18
  39. package/src/api/models/claude.ts +0 -4
  40. package/src/api/models/codex.ts +0 -4
  41. package/src/api/models/opencode.ts +0 -11
  42. package/src/api/routes/sessions.ts +0 -51
  43. package/src/api/routes/system.ts +0 -106
  44. package/src/api/routes/usage.ts +0 -15
  45. package/src/api/routes/webhooks/claude.ts +0 -149
  46. package/src/api/routes/webhooks/codex.ts +0 -175
  47. package/src/api/routes/webhooks/opencode.ts +0 -112
  48. package/src/api/routes.ts +0 -93
  49. package/src/api/schemas/common.ts +0 -29
  50. package/src/api/schemas/sessions.ts +0 -86
  51. package/src/api/schemas/usage.ts +0 -51
  52. package/src/api/schemas/webhooks/claude.ts +0 -32
  53. package/src/api/schemas/webhooks/codex.ts +0 -34
  54. package/src/api/schemas/webhooks/opencode.ts +0 -29
  55. package/src/api/services/claude-session.ts +0 -9
  56. package/src/api/services/codex-session.ts +0 -8
  57. package/src/api/services/opencode-session.ts +0 -73
  58. package/src/classifier.ts +0 -206
  59. package/src/cli/commands.ts +0 -714
  60. package/src/cli/index.ts +0 -18
  61. package/src/cli/program.ts +0 -175
  62. package/src/cli/prompt.ts +0 -44
  63. package/src/daemon/cli.ts +0 -27
  64. package/src/daemon/collectors/claude/README.md +0 -108
  65. package/src/daemon/collectors/claude/collector.ts +0 -423
  66. package/src/daemon/collectors/claude/discovery.ts +0 -127
  67. package/src/daemon/collectors/claude/hooks.ts +0 -181
  68. package/src/daemon/collectors/claude/index.ts +0 -8
  69. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  70. package/src/daemon/collectors/claude/transcript.ts +0 -246
  71. package/src/daemon/collectors/codex/README.md +0 -147
  72. package/src/daemon/collectors/codex/collector.ts +0 -661
  73. package/src/daemon/collectors/codex/discovery.ts +0 -104
  74. package/src/daemon/collectors/codex/hooks.ts +0 -179
  75. package/src/daemon/collectors/codex/index.ts +0 -7
  76. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  77. package/src/daemon/collectors/codex/transcript.ts +0 -299
  78. package/src/daemon/collectors/git.ts +0 -353
  79. package/src/daemon/collectors/herdr/collector.ts +0 -128
  80. package/src/daemon/collectors/herdr/index.ts +0 -2
  81. package/src/daemon/collectors/herdr/normalization.ts +0 -111
  82. package/src/daemon/collectors/opencode/README.md +0 -106
  83. package/src/daemon/collectors/opencode/collector.ts +0 -545
  84. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  85. package/src/daemon/collectors/opencode/index.ts +0 -5
  86. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  87. package/src/daemon/collectors/opencode-db.ts +0 -294
  88. package/src/daemon/collectors/process.ts +0 -201
  89. package/src/daemon/collectors/tmux/README.md +0 -128
  90. package/src/daemon/collectors/tmux/collector.ts +0 -492
  91. package/src/daemon/collectors/tmux/enrichment.ts +0 -242
  92. package/src/daemon/collectors/tmux/index.ts +0 -4
  93. package/src/daemon/collectors/tmux/parse.ts +0 -128
  94. package/src/daemon/collectors/usage.ts +0 -259
  95. package/src/daemon/index.ts +0 -209
  96. package/src/daemon/scheduler.ts +0 -86
  97. package/src/daemon/store.ts +0 -630
  98. package/src/detect.ts +0 -143
  99. package/src/hooks/claude.ts +0 -596
  100. package/src/hooks/codex.ts +0 -517
  101. package/src/legacy/classifier.ts +0 -215
  102. package/src/legacy/detect.ts +0 -143
  103. package/src/mcp/client-server.ts +0 -21
  104. package/src/mcp/http-handler.ts +0 -126
  105. package/src/mcp/index.ts +0 -61
  106. package/src/mcp/resources/config-current.ts +0 -19
  107. package/src/mcp/resources/index.ts +0 -11
  108. package/src/mcp/resources/sessions-active.ts +0 -19
  109. package/src/mcp/resources/sessions-all.ts +0 -19
  110. package/src/mcp/schemas/sessions.ts +0 -3
  111. package/src/mcp/server.ts +0 -17
  112. package/src/mcp/services/api-adapter.ts +0 -48
  113. package/src/mcp/services/data-source.ts +0 -22
  114. package/src/mcp/services/store-adapter.ts +0 -40
  115. package/src/mcp/tools/get-active-sessions.ts +0 -16
  116. package/src/mcp/tools/get-session-children.ts +0 -24
  117. package/src/mcp/tools/get-session-messages.ts +0 -24
  118. package/src/mcp/tools/get-session.ts +0 -23
  119. package/src/mcp/tools/index.ts +0 -15
  120. package/src/mcp/tools/list-sessions.ts +0 -16
  121. package/src/oc/index.ts +0 -302
  122. package/src/service/manager.ts +0 -284
  123. package/src/shared/config.ts +0 -511
  124. package/src/shared/ipc.ts +0 -80
  125. package/src/shared/logger.ts +0 -248
  126. package/src/shared/paths.ts +0 -73
  127. package/src/shared/platform.ts +0 -110
  128. package/src/shared/session-exclusions.ts +0 -5
  129. package/src/shared/session-view.ts +0 -302
  130. package/src/shared/terminal-capabilities.ts +0 -130
  131. package/src/shared/types.ts +0 -330
  132. package/src/shared/utils.ts +0 -339
  133. package/src/shared/version.ts +0 -44
  134. package/src/tui/App.tsx +0 -240
  135. package/src/tui/client.ts +0 -163
  136. package/src/tui/components/BindingsModal.tsx +0 -82
  137. package/src/tui/components/DashboardHeader.tsx +0 -256
  138. package/src/tui/components/KeyBar.tsx +0 -67
  139. package/src/tui/components/SessionDetailModal.tsx +0 -169
  140. package/src/tui/components/SessionsView.tsx +0 -184
  141. package/src/tui/components/TabBar.tsx +0 -40
  142. package/src/tui/components/UsageSummaryPane.tsx +0 -272
  143. package/src/tui/components/UsageView.tsx +0 -242
  144. package/src/tui/components/tool-icons.ts +0 -24
  145. package/src/tui/index.tsx +0 -106
  146. package/src/tui/keybindings.ts +0 -49
  147. package/src/tui/model.ts +0 -295
  148. package/src/tui/state.ts +0 -77
  149. package/src/tui/theme.ts +0 -110
  150. package/src/types.ts +0 -113
  151. package/src/usage/README.md +0 -323
  152. package/src/usage/api.ts +0 -99
  153. package/src/usage/client.ts +0 -3
  154. package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
  155. package/src/usage/clients/anthropic-web-usage.ts +0 -329
  156. package/src/usage/clients/claude-cli-probe.ts +0 -513
  157. package/src/usage/clients/codex-app-server.ts +0 -354
  158. package/src/usage/clients/codex-oauth-usage.ts +0 -240
  159. package/src/usage/clients/opencode-go-api.ts +0 -100
  160. package/src/usage/clients/opencode-go-usage.ts +0 -577
  161. package/src/usage/errors.ts +0 -33
  162. package/src/usage/index.ts +0 -20
  163. package/src/usage/local/opencode-go-local.ts +0 -194
  164. package/src/usage/mappers.ts +0 -282
  165. package/src/usage/planner.ts +0 -118
  166. package/src/usage/providers/claude.ts +0 -233
  167. package/src/usage/providers/codex.ts +0 -115
  168. package/src/usage/providers/opencode-go.ts +0 -115
  169. package/src/usage/resolver.ts +0 -4
  170. package/src/usage/resolvers/browser-cookies.ts +0 -808
  171. package/src/usage/resolvers/claude-auth.ts +0 -208
  172. package/src/usage/resolvers/claude-cli.ts +0 -24
  173. package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
  174. package/src/usage/resolvers/claude-web.ts +0 -82
  175. package/src/usage/resolvers/codex-auth.ts +0 -174
  176. package/src/usage/resolvers/keychain-coordinator.ts +0 -397
  177. package/src/usage/resolvers/opencode-go-web.ts +0 -55
  178. package/src/usage/types.ts +0 -225
  179. package/src/usage/web-session-cache.ts +0 -37
  180. /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
  181. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
  182. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
  183. /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
  184. /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
  185. /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
  186. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
  187. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
  188. /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
  189. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
  190. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
  191. /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
  192. /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
  193. /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
  194. /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
  195. /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
- }