cloak22 2.2.0

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.
@@ -0,0 +1,681 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { createDecipheriv, createHash, pbkdf2Sync } from "node:crypto"
3
+ import fs from "node:fs"
4
+ import os from "node:os"
5
+ import path from "node:path"
6
+ import { normalizeCookie, type Cookie } from "./cookies.js"
7
+ import { resolveChromeCookiesDatabasePath } from "./chrome-profile-sites.js"
8
+ import { defaultChromeUserDataDir } from "./chrome-profiles.js"
9
+
10
+ export const CHROME_COOKIE_LIMITATION_WARNING =
11
+ "Chrome cookie import is best-effort and only reuses cookies, not the rest of the browser profile. If a login still fails after injection, the site may depend on additional browser state."
12
+ export const CHROME_COOKIE_SUPPORT_MISSING_ERROR =
13
+ "Chrome cookie support is not available in this environment. cloak could not access the selected profile's cookie database or the platform secret storage needed to decrypt it."
14
+ export const CHROME_COOKIE_APP_BOUND_UNSUPPORTED_ERROR =
15
+ "Chrome app-bound cookie encryption on Windows is not supported by cloak yet."
16
+
17
+ type ChromePuppeteerCookie = {
18
+ name: string
19
+ value: string
20
+ domain: string
21
+ path: string
22
+ expires: number | bigint
23
+ HttpOnly?: boolean
24
+ Secure?: boolean
25
+ sameSite?: string
26
+ }
27
+
28
+ type ChromeCookieReader = (
29
+ url: string,
30
+ format: "puppeteer",
31
+ profile?: string
32
+ ) => Promise<ChromePuppeteerCookie[]>
33
+
34
+ type ChromeCookieDatabaseRow = {
35
+ host_key: string
36
+ path: string
37
+ is_secure: number | bigint
38
+ expires_utc: number | bigint
39
+ name: string
40
+ value: string
41
+ encrypted_value: Uint8Array | null
42
+ creation_utc: number | bigint
43
+ is_httponly: number | bigint
44
+ samesite: number | bigint | null
45
+ }
46
+
47
+ type StatementLike = {
48
+ all(...parameters: string[]): Array<Record<string, unknown>>
49
+ setReadBigInts?(enabled: boolean): StatementLike
50
+ }
51
+
52
+ type DatabaseLike = {
53
+ prepare(sql: string): StatementLike
54
+ close(): void
55
+ }
56
+
57
+ type DatabaseConstructor = new (
58
+ path: string,
59
+ options?: {
60
+ readOnly?: boolean
61
+ }
62
+ ) => DatabaseLike
63
+
64
+ type ChromeCookieReaderDependencies = {
65
+ chromeUserDataDir?: string
66
+ platform?: NodeJS.Platform
67
+ pathExists?: (targetPath: string) => boolean
68
+ makeTempDir?: (prefix: string) => string
69
+ copyFile?: (sourcePath: string, targetPath: string) => void
70
+ removeDir?: (targetPath: string, options: { recursive: true; force: true }) => void
71
+ queryRows?: (databasePath: string, hostKeys: string[]) => ChromeCookieDatabaseRow[]
72
+ readFile?: (targetPath: string) => string
73
+ runCommand?: (command: string, args: string[]) => string
74
+ }
75
+
76
+ type ChromeCookieCryptoCache = {
77
+ macKey?: Buffer
78
+ linuxV11Key?: Buffer | null
79
+ windowsKey?: Buffer
80
+ }
81
+
82
+ const CHROMIUM_EPOCH_MICROSECONDS = 11644473600000000n
83
+ const POSIX_IV = Buffer.from(" ".repeat(16), "utf8")
84
+ const POSIX_SALT = "saltysalt"
85
+ const MAC_SAFE_STORAGE_SERVICE = "Chrome Safe Storage"
86
+ const MAC_SAFE_STORAGE_ACCOUNT = "Chrome"
87
+ const LINUX_V10_PASSWORD = "peanuts"
88
+ const WINDOWS_DPAPI_KEY_PREFIX = Buffer.from("DPAPI", "utf8")
89
+ const WINDOWS_APP_BOUND_KEY_PREFIX = Buffer.from("APPB", "utf8")
90
+
91
+ function loadDatabaseConstructor(): DatabaseConstructor {
92
+ const sqlite = require("node:sqlite") as {
93
+ DatabaseSync: DatabaseConstructor
94
+ }
95
+
96
+ return sqlite.DatabaseSync
97
+ }
98
+
99
+ function chromiumTimestampToUnixSeconds(timestamp: number | bigint): number {
100
+ if (typeof timestamp === "bigint") {
101
+ return Number((timestamp - CHROMIUM_EPOCH_MICROSECONDS) / 1000000n)
102
+ }
103
+
104
+ return Math.trunc(
105
+ (timestamp - Number(CHROMIUM_EPOCH_MICROSECONDS)) / 1000000
106
+ )
107
+ }
108
+
109
+ function normalizeChromeCookie(raw: ChromePuppeteerCookie): Cookie {
110
+ const normalized = {
111
+ name: raw.name,
112
+ value: raw.value,
113
+ domain: raw.domain,
114
+ path: raw.path,
115
+ httpOnly: raw.HttpOnly,
116
+ secure: raw.Secure,
117
+ sameSite: raw.sameSite,
118
+ } as Parameters<typeof normalizeCookie>[0] & { expires?: number }
119
+
120
+ if (!isZeroChromiumTimestamp(raw.expires)) {
121
+ normalized.expires = chromiumTimestampToUnixSeconds(raw.expires)
122
+ }
123
+
124
+ return normalizeCookie(normalized)
125
+ }
126
+
127
+ function isZeroChromiumTimestamp(timestamp: number | bigint): boolean {
128
+ return typeof timestamp === "bigint" ? timestamp === 0n : timestamp === 0
129
+ }
130
+
131
+ export async function readChromeCookies(
132
+ options: { url: string; profile?: string },
133
+ getCookies: ChromeCookieReader = createChromeCookieReader()
134
+ ): Promise<Cookie[]> {
135
+ const cookies = await getCookies(options.url, "puppeteer", options.profile)
136
+ return cookies.map(normalizeChromeCookie)
137
+ }
138
+
139
+ export function createChromeCookieReader(
140
+ dependencies: ChromeCookieReaderDependencies = {}
141
+ ): ChromeCookieReader {
142
+ return async (url: string, format: "puppeteer", profile?: string) => {
143
+ if (format !== "puppeteer") {
144
+ throw new Error("cloak only supports Chrome cookie export in puppeteer format")
145
+ }
146
+
147
+ const parsedUrl = parseCookieUrl(url)
148
+ const chromeUserDataDir =
149
+ dependencies.chromeUserDataDir ?? defaultChromeUserDataDir()
150
+ const pathExists = dependencies.pathExists ?? fs.existsSync
151
+ const makeTempDir = dependencies.makeTempDir ?? fs.mkdtempSync
152
+ const copyFile = dependencies.copyFile ?? fs.copyFileSync
153
+ const removeDir = dependencies.removeDir ?? fs.rmSync
154
+ const queryRows = dependencies.queryRows ?? queryChromeCookieRows
155
+ const sourcePath = resolveChromeCookiesDatabasePath(
156
+ {
157
+ chromeUserDataDir,
158
+ profileDirectory: profile ?? "Default",
159
+ },
160
+ {
161
+ pathExists,
162
+ }
163
+ )
164
+
165
+ if (!sourcePath) {
166
+ return []
167
+ }
168
+
169
+ const tempRoot = makeTempDir(path.join(os.tmpdir(), "cloak-cookie-db-"))
170
+ const stagedPath = path.join(tempRoot, path.basename(sourcePath))
171
+
172
+ try {
173
+ copyFile(sourcePath, stagedPath)
174
+ copyOptionalSidecar(`${sourcePath}-wal`, `${stagedPath}-wal`, pathExists, copyFile)
175
+ copyOptionalSidecar(`${sourcePath}-shm`, `${stagedPath}-shm`, pathExists, copyFile)
176
+
177
+ const rows = queryRows(stagedPath, candidateChromeCookieHosts(parsedUrl.hostname))
178
+ const cryptoCache: ChromeCookieCryptoCache = {}
179
+ const cookies: ChromePuppeteerCookie[] = []
180
+
181
+ for (const row of rows) {
182
+ if (!chromeCookieMatchesUrl(row, parsedUrl)) {
183
+ continue
184
+ }
185
+
186
+ cookies.push(
187
+ rowToPuppeteerCookie(row, {
188
+ ...dependencies,
189
+ chromeUserDataDir,
190
+ }, cryptoCache)
191
+ )
192
+ }
193
+
194
+ return cookies
195
+ } finally {
196
+ removeDir(tempRoot, { recursive: true, force: true })
197
+ }
198
+ }
199
+ }
200
+
201
+ export function candidateChromeCookieHosts(hostname: string): string[] {
202
+ const normalizedHost = hostname.trim().toLowerCase().replace(/\.$/, "")
203
+
204
+ if (!normalizedHost) {
205
+ return []
206
+ }
207
+
208
+ const labels = normalizedHost.split(".")
209
+ const hosts = new Set<string>()
210
+
211
+ for (let index = 0; index < labels.length; index += 1) {
212
+ const candidate = labels.slice(index).join(".")
213
+ hosts.add(candidate)
214
+ hosts.add(`.${candidate}`)
215
+ }
216
+
217
+ return [...hosts]
218
+ }
219
+
220
+ export function chromeCookieMatchesUrl(
221
+ row: Pick<ChromeCookieDatabaseRow, "host_key" | "path" | "is_secure">,
222
+ url: URL
223
+ ): boolean {
224
+ if (toBoolean(row.is_secure) && url.protocol !== "https:") {
225
+ return false
226
+ }
227
+
228
+ if (!hostMatchesCookieDomain(url.hostname, row.host_key)) {
229
+ return false
230
+ }
231
+
232
+ return pathMatchesCookiePath(url.pathname || "/", row.path || "/")
233
+ }
234
+
235
+ function parseCookieUrl(url: string): URL {
236
+ let parsedUrl: URL
237
+
238
+ try {
239
+ parsedUrl = new URL(url)
240
+ } catch {
241
+ throw new Error(
242
+ "Could not parse URI, format should be http://www.example.com/path/"
243
+ )
244
+ }
245
+
246
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
247
+ throw new Error(
248
+ "Could not parse URI, format should be http://www.example.com/path/"
249
+ )
250
+ }
251
+
252
+ return parsedUrl
253
+ }
254
+
255
+ function rowToPuppeteerCookie(
256
+ row: ChromeCookieDatabaseRow,
257
+ dependencies: ChromeCookieReaderDependencies,
258
+ cryptoCache: ChromeCookieCryptoCache
259
+ ): ChromePuppeteerCookie {
260
+ const resolvedValue = resolveChromeCookieValue(row, dependencies, cryptoCache)
261
+ const cookie: ChromePuppeteerCookie = {
262
+ name: row.name,
263
+ value: resolvedValue,
264
+ domain: row.host_key,
265
+ path: row.path,
266
+ expires: row.expires_utc,
267
+ }
268
+
269
+ if (toBoolean(row.is_secure)) {
270
+ cookie.Secure = true
271
+ }
272
+
273
+ if (toBoolean(row.is_httponly)) {
274
+ cookie.HttpOnly = true
275
+ }
276
+
277
+ const sameSite = databaseSameSiteToChromeSameSite(row.samesite)
278
+ if (sameSite) {
279
+ cookie.sameSite = sameSite
280
+ }
281
+
282
+ return cookie
283
+ }
284
+
285
+ function resolveChromeCookieValue(
286
+ row: ChromeCookieDatabaseRow,
287
+ dependencies: ChromeCookieReaderDependencies,
288
+ cryptoCache: ChromeCookieCryptoCache
289
+ ): string {
290
+ const encryptedValue = toBuffer(row.encrypted_value)
291
+
292
+ if (encryptedValue.length === 0) {
293
+ return row.value
294
+ }
295
+
296
+ const decrypted = decryptChromeCookieValue(
297
+ encryptedValue,
298
+ row.host_key,
299
+ dependencies,
300
+ cryptoCache
301
+ )
302
+
303
+ return stripEncryptedCookieDomainHash(decrypted, row.host_key).toString("utf8")
304
+ }
305
+
306
+ function decryptChromeCookieValue(
307
+ encryptedValue: Buffer,
308
+ hostKey: string,
309
+ dependencies: ChromeCookieReaderDependencies,
310
+ cryptoCache: ChromeCookieCryptoCache
311
+ ): Buffer {
312
+ const platform = dependencies.platform ?? process.platform
313
+
314
+ if (platform === "darwin") {
315
+ return decryptPosixCookieValue(
316
+ encryptedValue,
317
+ getMacEncryptionKey(dependencies, cryptoCache)
318
+ )
319
+ }
320
+
321
+ if (platform === "linux") {
322
+ return decryptLinuxCookieValue(encryptedValue, dependencies, cryptoCache)
323
+ }
324
+
325
+ if (platform === "win32") {
326
+ return decryptWindowsCookieValue(
327
+ encryptedValue,
328
+ hostKey,
329
+ dependencies,
330
+ cryptoCache
331
+ )
332
+ }
333
+
334
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
335
+ }
336
+
337
+ function decryptPosixCookieValue(encryptedValue: Buffer, key: Buffer): Buffer {
338
+ const version = encryptedValue.subarray(0, 3).toString("utf8")
339
+
340
+ if (version !== "v10" && version !== "v11") {
341
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
342
+ }
343
+
344
+ const decipher = createDecipheriv("aes-128-cbc", key, POSIX_IV)
345
+
346
+ return Buffer.concat([
347
+ decipher.update(encryptedValue.subarray(3)),
348
+ decipher.final(),
349
+ ])
350
+ }
351
+
352
+ function decryptLinuxCookieValue(
353
+ encryptedValue: Buffer,
354
+ dependencies: ChromeCookieReaderDependencies,
355
+ cryptoCache: ChromeCookieCryptoCache
356
+ ): Buffer {
357
+ const version = encryptedValue.subarray(0, 3).toString("utf8")
358
+
359
+ if (version === "v10") {
360
+ return decryptPosixCookieValue(encryptedValue, derivePosixKey(LINUX_V10_PASSWORD, 1))
361
+ }
362
+
363
+ if (version === "v11") {
364
+ const key = getLinuxV11EncryptionKey(dependencies, cryptoCache)
365
+
366
+ if (key) {
367
+ return decryptPosixCookieValue(encryptedValue, key)
368
+ }
369
+
370
+ return decryptPosixCookieValue(encryptedValue, derivePosixKey("", 1))
371
+ }
372
+
373
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
374
+ }
375
+
376
+ function decryptWindowsCookieValue(
377
+ encryptedValue: Buffer,
378
+ _hostKey: string,
379
+ dependencies: ChromeCookieReaderDependencies,
380
+ cryptoCache: ChromeCookieCryptoCache
381
+ ): Buffer {
382
+ const version = encryptedValue.subarray(0, 3).toString("utf8")
383
+
384
+ if (version === "v20") {
385
+ throw new Error(CHROME_COOKIE_APP_BOUND_UNSUPPORTED_ERROR)
386
+ }
387
+
388
+ if (version !== "v10") {
389
+ return decryptWindowsDpapi(encryptedValue, dependencies)
390
+ }
391
+
392
+ const key = getWindowsEncryptionKey(dependencies, cryptoCache)
393
+ const nonce = encryptedValue.subarray(3, 15)
394
+ const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16)
395
+ const authTag = encryptedValue.subarray(encryptedValue.length - 16)
396
+ const decipher = createDecipheriv("aes-256-gcm", key, nonce)
397
+ decipher.setAuthTag(authTag)
398
+
399
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()])
400
+ }
401
+
402
+ function getMacEncryptionKey(
403
+ dependencies: ChromeCookieReaderDependencies,
404
+ cryptoCache: ChromeCookieCryptoCache
405
+ ): Buffer {
406
+ if (cryptoCache.macKey) {
407
+ return cryptoCache.macKey
408
+ }
409
+
410
+ const password = runCommand(dependencies, "security", [
411
+ "find-generic-password",
412
+ "-w",
413
+ "-s",
414
+ MAC_SAFE_STORAGE_SERVICE,
415
+ "-a",
416
+ MAC_SAFE_STORAGE_ACCOUNT,
417
+ ]).trimEnd()
418
+
419
+ if (!password) {
420
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
421
+ }
422
+
423
+ cryptoCache.macKey = derivePosixKey(password, 1003)
424
+ return cryptoCache.macKey
425
+ }
426
+
427
+ function getLinuxV11EncryptionKey(
428
+ dependencies: ChromeCookieReaderDependencies,
429
+ cryptoCache: ChromeCookieCryptoCache
430
+ ): Buffer | null {
431
+ if (cryptoCache.linuxV11Key !== undefined) {
432
+ return cryptoCache.linuxV11Key
433
+ }
434
+
435
+ try {
436
+ const password = runCommand(dependencies, "secret-tool", [
437
+ "lookup",
438
+ "xdg:schema",
439
+ "chrome_libsecret_os_crypt_password",
440
+ ]).trimEnd()
441
+
442
+ cryptoCache.linuxV11Key =
443
+ password.length > 0 ? derivePosixKey(password, 1) : null
444
+ } catch {
445
+ cryptoCache.linuxV11Key = null
446
+ }
447
+
448
+ return cryptoCache.linuxV11Key
449
+ }
450
+
451
+ function getWindowsEncryptionKey(
452
+ dependencies: ChromeCookieReaderDependencies,
453
+ cryptoCache: ChromeCookieCryptoCache
454
+ ): Buffer {
455
+ if (cryptoCache.windowsKey) {
456
+ return cryptoCache.windowsKey
457
+ }
458
+
459
+ const chromeUserDataDir =
460
+ dependencies.chromeUserDataDir ?? defaultChromeUserDataDir()
461
+ const readFile = dependencies.readFile ?? ((targetPath: string) => fs.readFileSync(targetPath, "utf8"))
462
+ const localStatePath = path.join(chromeUserDataDir, "Local State")
463
+ const localState = JSON.parse(readFile(localStatePath)) as {
464
+ os_crypt?: {
465
+ encrypted_key?: string
466
+ }
467
+ }
468
+ const encodedKey = localState.os_crypt?.encrypted_key
469
+
470
+ if (!encodedKey) {
471
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
472
+ }
473
+
474
+ const encryptedKey = Buffer.from(encodedKey, "base64")
475
+
476
+ if (encryptedKey.subarray(0, WINDOWS_APP_BOUND_KEY_PREFIX.length).equals(WINDOWS_APP_BOUND_KEY_PREFIX)) {
477
+ throw new Error(CHROME_COOKIE_APP_BOUND_UNSUPPORTED_ERROR)
478
+ }
479
+
480
+ if (!encryptedKey.subarray(0, WINDOWS_DPAPI_KEY_PREFIX.length).equals(WINDOWS_DPAPI_KEY_PREFIX)) {
481
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
482
+ }
483
+
484
+ cryptoCache.windowsKey = decryptWindowsDpapi(
485
+ encryptedKey.subarray(WINDOWS_DPAPI_KEY_PREFIX.length),
486
+ dependencies
487
+ )
488
+
489
+ return cryptoCache.windowsKey
490
+ }
491
+
492
+ function decryptWindowsDpapi(
493
+ encryptedValue: Buffer,
494
+ dependencies: ChromeCookieReaderDependencies
495
+ ): Buffer {
496
+ const script = [
497
+ "$inputBase64 = $args[0]",
498
+ "$bytes = [Convert]::FromBase64String($inputBase64)",
499
+ "$plain = [System.Security.Cryptography.ProtectedData]::Unprotect(",
500
+ " $bytes,",
501
+ " $null,",
502
+ " [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
503
+ ")",
504
+ "[Console]::Out.Write([Convert]::ToBase64String($plain))",
505
+ ].join("; ")
506
+ const base64Value = encryptedValue.toString("base64")
507
+ const binaries = ["powershell.exe", "powershell", "pwsh"]
508
+
509
+ for (const binary of binaries) {
510
+ try {
511
+ const output = runCommand(dependencies, binary, [
512
+ "-NoProfile",
513
+ "-NonInteractive",
514
+ "-Command",
515
+ script,
516
+ base64Value,
517
+ ]).trim()
518
+
519
+ return Buffer.from(output, "base64")
520
+ } catch {
521
+ continue
522
+ }
523
+ }
524
+
525
+ throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
526
+ }
527
+
528
+ function derivePosixKey(password: string, iterations: number): Buffer {
529
+ return pbkdf2Sync(password, POSIX_SALT, iterations, 16, "sha1")
530
+ }
531
+
532
+ function stripEncryptedCookieDomainHash(
533
+ decryptedValue: Buffer,
534
+ hostKey: string
535
+ ): Buffer {
536
+ const domainHash = createHash("sha256").update(hostKey).digest()
537
+
538
+ if (decryptedValue.length < domainHash.length) {
539
+ return decryptedValue
540
+ }
541
+
542
+ if (decryptedValue.subarray(0, domainHash.length).equals(domainHash)) {
543
+ return decryptedValue.subarray(domainHash.length)
544
+ }
545
+
546
+ return decryptedValue
547
+ }
548
+
549
+ function hostMatchesCookieDomain(hostname: string, cookieDomain: string): boolean {
550
+ const normalizedHost = hostname.toLowerCase().replace(/\.$/, "")
551
+ const normalizedDomain = cookieDomain.toLowerCase().replace(/^\./, "").replace(/\.$/, "")
552
+ const isDomainCookie = cookieDomain.startsWith(".")
553
+
554
+ if (!isDomainCookie) {
555
+ return normalizedHost === normalizedDomain
556
+ }
557
+
558
+ return (
559
+ normalizedHost === normalizedDomain ||
560
+ normalizedHost.endsWith(`.${normalizedDomain}`)
561
+ )
562
+ }
563
+
564
+ function pathMatchesCookiePath(requestPath: string, cookiePath: string): boolean {
565
+ const normalizedRequestPath = requestPath || "/"
566
+ const normalizedCookiePath = cookiePath || "/"
567
+
568
+ if (normalizedRequestPath === normalizedCookiePath) {
569
+ return true
570
+ }
571
+
572
+ if (!normalizedRequestPath.startsWith(normalizedCookiePath)) {
573
+ return false
574
+ }
575
+
576
+ if (normalizedCookiePath.endsWith("/")) {
577
+ return true
578
+ }
579
+
580
+ return normalizedRequestPath.charAt(normalizedCookiePath.length) === "/"
581
+ }
582
+
583
+ function databaseSameSiteToChromeSameSite(
584
+ sameSite: number | bigint | null
585
+ ): string | undefined {
586
+ const normalizedSameSite =
587
+ typeof sameSite === "bigint" ? Number(sameSite) : sameSite
588
+
589
+ switch (normalizedSameSite) {
590
+ case 0:
591
+ return "no_restriction"
592
+ case 1:
593
+ return "lax"
594
+ case 2:
595
+ return "strict"
596
+ default:
597
+ return undefined
598
+ }
599
+ }
600
+
601
+ function queryChromeCookieRows(
602
+ databasePath: string,
603
+ hostKeys: string[]
604
+ ): ChromeCookieDatabaseRow[] {
605
+ if (hostKeys.length === 0) {
606
+ return []
607
+ }
608
+
609
+ const DatabaseSync = loadDatabaseConstructor()
610
+ const database = new DatabaseSync(databasePath, {
611
+ readOnly: true,
612
+ })
613
+
614
+ try {
615
+ const placeholders = hostKeys.map(() => "?").join(", ")
616
+ const statement = database.prepare(
617
+ [
618
+ "SELECT",
619
+ "host_key,",
620
+ "path,",
621
+ "is_secure,",
622
+ "expires_utc,",
623
+ "name,",
624
+ "value,",
625
+ "encrypted_value,",
626
+ "creation_utc,",
627
+ "is_httponly,",
628
+ "samesite",
629
+ "FROM cookies",
630
+ `WHERE host_key IN (${placeholders})`,
631
+ "ORDER BY LENGTH(path) DESC, creation_utc ASC",
632
+ ].join(" ")
633
+ )
634
+ statement.setReadBigInts?.(true)
635
+
636
+ return statement.all(...hostKeys) as ChromeCookieDatabaseRow[]
637
+ } finally {
638
+ database.close()
639
+ }
640
+ }
641
+
642
+ function copyOptionalSidecar(
643
+ sourcePath: string,
644
+ targetPath: string,
645
+ pathExists: (targetPath: string) => boolean,
646
+ copyFile: (sourcePath: string, targetPath: string) => void
647
+ ) {
648
+ if (!pathExists(sourcePath)) {
649
+ return
650
+ }
651
+
652
+ copyFile(sourcePath, targetPath)
653
+ }
654
+
655
+ function runCommand(
656
+ dependencies: ChromeCookieReaderDependencies,
657
+ command: string,
658
+ args: string[]
659
+ ): string {
660
+ const invoke =
661
+ dependencies.runCommand ??
662
+ ((binary: string, binaryArgs: string[]) =>
663
+ execFileSync(binary, binaryArgs, {
664
+ encoding: "utf8",
665
+ windowsHide: true,
666
+ }))
667
+
668
+ return invoke(command, args)
669
+ }
670
+
671
+ function toBoolean(value: number | bigint): boolean {
672
+ if (typeof value === "bigint") {
673
+ return value !== 0n
674
+ }
675
+
676
+ return value !== 0
677
+ }
678
+
679
+ function toBuffer(value: Uint8Array | null): Buffer {
680
+ return value ? Buffer.from(value) : Buffer.alloc(0)
681
+ }