browser-commander 0.15.0 → 0.16.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 542bf75: Fix CI/CD false positives, false negatives and warnings across all workflows
8
+ - Repair `if:` conditions that started with `!`, which a YAML plain scalar cannot do
9
+ - Replace `always()` with `!cancelled()` so cancelled runs stop propagating
10
+ - Gate `instant-release` and `changeset-pr` on lint and test, so a manual release can no longer publish unvalidated code
11
+ - Make the changelog fragment checks fail instead of only warning
12
+ - Pass `github.base_ref` and free-form `workflow_dispatch` inputs through environment variables instead of interpolating them into shell bodies
13
+ - Update `actions/setup-python` to v6, `codecov/codecov-action` to v7 and `peter-evans/create-pull-request` to v8
14
+ - Extend `scripts/check-ci-workflows.mjs` so each of the above becomes a permanent policy check
15
+ - Add repository-wide secrets scanning and a 1500-line file limit gate
16
+
17
+ ## 0.16.0
18
+
19
+ ### Minor Changes
20
+
21
+ - a23c0fa: Add installed-browser profile discovery and privacy-preserving cookie import for Chrome, Edge, Brave, Chromium, and Firefox, including OS credential caching and Playwright/Puppeteer-compatible output.
22
+
3
23
  ## 0.15.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -152,8 +152,41 @@ const connection = await launchRealBrowser({
152
152
  await connection.browser.close();
153
153
  ```
154
154
 
155
- Cookie seeding copies only cookies you explicitly provide; the helper does not
156
- read, decrypt, or expose cookies from a browser's default profile.
155
+ Cookie seeding copies only cookies you explicitly provide. To seed a dedicated
156
+ profile from one of your installed browser profiles, use the explicit local
157
+ cookie-import helper:
158
+
159
+ ```javascript
160
+ import {
161
+ launchAndConnectRealBrowser,
162
+ listBrowserProfiles,
163
+ readBrowserCookies,
164
+ } from 'browser-commander';
165
+
166
+ console.log(await listBrowserProfiles({ browser: 'chrome' }));
167
+
168
+ const cookies = await readBrowserCookies({
169
+ browser: 'chrome', // chrome, edge, brave, chromium, or firefox
170
+ profile: 'Default',
171
+ domainFilter: 'example.com',
172
+ cache: { ttlMinutes: 60 },
173
+ });
174
+
175
+ const connection = await launchAndConnectRealBrowser({
176
+ engine: 'playwright',
177
+ channel: 'chrome',
178
+ userDataDir: '/tmp/my-dedicated-profile',
179
+ seedCookies: cookies,
180
+ });
181
+ ```
182
+
183
+ The import stays on the local machine and runs only when called. It never sends
184
+ cookie data anywhere. Decrypted result and derived-key cache files are stored
185
+ under `~/.browser-commander/cookie-cache/` with owner-only permissions. The
186
+ default 60-minute TTL and a cross-process lock ensure that concurrent or repeated
187
+ scripts touch Keychain, libsecret/KWallet, or DPAPI at most once per TTL window.
188
+ Set `refresh: true` to force a new read, customize `cache.dir`/`ttlMinutes`, or
189
+ set `cache: false` to opt out of disk caching.
157
190
  `launchAndConnectRealBrowser()` remains available as a descriptive alias.
158
191
  Remote-debugging, loopback, and profile arguments remain managed even when all
159
192
  optional defaults are ignored; `headless: true` adds `--headless=new`.
@@ -391,6 +424,48 @@ and profile path alongside `{ browser, page }`.
391
424
  of defaults to omit (or `true` to omit all optional defaults). The older
392
425
  `args` append option remains supported.
393
426
 
427
+ ### listBrowserProfiles(options)
428
+
429
+ Discover cookie-bearing profiles for Chrome, Edge, Brave, Chromium, and Firefox.
430
+ Pass an optional `browser` to narrow discovery. Each result contains
431
+ `{ browser, name, displayName, path, isDefault }`.
432
+
433
+ ### readBrowserCookies(options)
434
+
435
+ Read cookies from an installed browser profile and return the exact
436
+ `{ name, value, domain, path, expires, httpOnly, secure, sameSite }` shape used by
437
+ Playwright and Puppeteer:
438
+
439
+ ```javascript
440
+ const cookies = await readBrowserCookies({
441
+ browser: 'firefox',
442
+ profile: 'default-release', // optional; the default profile is selected first
443
+ domainFilter: 'example.com', // optional substring match
444
+ cache: { dir: './private-cookie-cache', ttlMinutes: 30 },
445
+ refresh: false,
446
+ });
447
+ ```
448
+
449
+ Platform support:
450
+
451
+ | Browser family | macOS | Linux | Windows |
452
+ | ----------------------------- | ------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------- |
453
+ | Chrome, Edge, Brave, Chromium | Keychain + AES-128-CBC (`v10`/`v11`) | libsecret/KWallet + AES-128-CBC (`v11`), or the Chromium `v10` fallback key | DPAPI-protected AES-256-GCM key (`v10`/`v11`) |
454
+ | Firefox | `cookies.sqlite` | `cookies.sqlite` | `cookies.sqlite` |
455
+
456
+ Firefox cookie values are stored directly in its local cookie database. Chromium
457
+ database version 24 domain hashes and Chrome's 1601-based timestamps are handled
458
+ automatically. Current Windows Chromium can use app-bound `v20` encryption,
459
+ which intentionally requires the browser's privileged elevation service and
460
+ cannot be decrypted by an ordinary external process. The helper reports that
461
+ boundary instead of bypassing it; use a browser-supported export or an existing
462
+ Browser Commander storage-state file for those cookies. Set
463
+ `ignoreDecryptionErrors: true` only when returning the remaining decryptable
464
+ cookies is acceptable.
465
+
466
+ Treat imported cookies like passwords: keep cache directories private, use a
467
+ short TTL, never commit them, and seed only a dedicated automation profile.
468
+
394
469
  ### saveStorageState(page, filePath)
395
470
 
396
471
  Save the current cookies and localStorage in Playwright's portable storage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -63,6 +63,7 @@
63
63
  }
64
64
  },
65
65
  "dependencies": {
66
+ "better-sqlite3": "^12.11.1",
66
67
  "log-lazy": "^1.0.4",
67
68
  "test-anywhere": "^0.9.1"
68
69
  },
@@ -72,8 +73,8 @@
72
73
  "eslint-config-prettier": "^10.1.8",
73
74
  "eslint-plugin-prettier": "^5.5.4",
74
75
  "husky": "^9.1.7",
75
- "jsdoc": "^4.0.4",
76
76
  "jscpd": "^4.0.5",
77
+ "jsdoc": "^4.0.4",
77
78
  "lint-staged": "^16.2.6",
78
79
  "playwright": "^1.56.1",
79
80
  "prettier": "^3.6.2",
@@ -0,0 +1,278 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { execFile as execFileCallback } from 'node:child_process';
3
+ import {
4
+ chmod,
5
+ mkdir,
6
+ open,
7
+ readFile,
8
+ rename,
9
+ rm,
10
+ stat,
11
+ } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { promisify } from 'node:util';
14
+
15
+ const DEFAULT_TTL_MINUTES = 60;
16
+ const LOCK_STALE_MILLISECONDS = 30_000;
17
+ const LOCK_WAIT_MILLISECONDS = 30_000;
18
+ const credentialPromises = new Map();
19
+ const execFile = promisify(execFileCallback);
20
+ let windowsPrincipalPromise;
21
+
22
+ function hash(value) {
23
+ return createHash('sha256').update(value).digest('hex');
24
+ }
25
+
26
+ export function clearBrowserCookieMemoryCache() {
27
+ credentialPromises.clear();
28
+ }
29
+
30
+ export function normalizeCookieCache(cache, homeDir, ttlMinutes) {
31
+ const selectedTtl = ttlMinutes ?? cache?.ttlMinutes ?? DEFAULT_TTL_MINUTES;
32
+ if (!Number.isFinite(selectedTtl) || selectedTtl < 0) {
33
+ throw new RangeError(
34
+ 'cookie cache ttlMinutes must be a non-negative number'
35
+ );
36
+ }
37
+ if (cache === false) {
38
+ return { enabled: false, ttlSeconds: selectedTtl * 60 };
39
+ }
40
+ return {
41
+ enabled: true,
42
+ dir: path.resolve(
43
+ cache?.dir ?? path.join(homeDir, '.browser-commander', 'cookie-cache')
44
+ ),
45
+ ttlSeconds: selectedTtl * 60,
46
+ };
47
+ }
48
+
49
+ function windowsPrincipal() {
50
+ windowsPrincipalPromise ??= execFile('whoami', [], {
51
+ windowsHide: true,
52
+ }).then(({ stdout }) => stdout.trim());
53
+ return windowsPrincipalPromise;
54
+ }
55
+
56
+ async function restrictOwnerOnly(targetPath, directory) {
57
+ if (process.platform === 'win32') {
58
+ const principal = await windowsPrincipal();
59
+ if (!principal) {
60
+ throw new Error('Could not identify the current Windows user');
61
+ }
62
+ const permission = directory ? '(OI)(CI)F' : 'F';
63
+ await execFile(
64
+ 'icacls',
65
+ [
66
+ targetPath,
67
+ '/inheritance:r',
68
+ '/grant:r',
69
+ `${principal}:${permission}`,
70
+ '/q',
71
+ ],
72
+ { windowsHide: true }
73
+ );
74
+ return;
75
+ }
76
+ await chmod(targetPath, directory ? 0o700 : 0o600);
77
+ }
78
+
79
+ async function ensureCacheDirectory(cacheDir) {
80
+ await mkdir(cacheDir, { recursive: true, mode: 0o700 });
81
+ await restrictOwnerOnly(cacheDir, true);
82
+ }
83
+
84
+ async function readFreshJson(filePath, ttlSeconds, now) {
85
+ try {
86
+ const value = JSON.parse(await readFile(filePath, 'utf8'));
87
+ const age = now() / 1000 - value.savedAt;
88
+ return age >= 0 && age <= ttlSeconds ? value : null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ async function writeOwnerOnlyJson(filePath, value) {
95
+ const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
96
+ const handle = await open(temporaryPath, 'wx', 0o600);
97
+ try {
98
+ await handle.writeFile(`${JSON.stringify(value)}\n`, 'utf8');
99
+ await handle.sync();
100
+ } finally {
101
+ await handle.close();
102
+ }
103
+ try {
104
+ await rename(temporaryPath, filePath);
105
+ } catch (error) {
106
+ await rm(temporaryPath, { force: true });
107
+ throw error;
108
+ }
109
+ await restrictOwnerOnly(filePath, false);
110
+ }
111
+
112
+ function cachePath(cacheDir, kind, identity) {
113
+ return path.join(cacheDir, `${kind}-${hash(identity)}.json`);
114
+ }
115
+
116
+ export async function readCookieResultCache({
117
+ cache,
118
+ identity,
119
+ refresh,
120
+ now = Date.now,
121
+ }) {
122
+ if (!cache.enabled || refresh) {
123
+ return null;
124
+ }
125
+ const cached = await readFreshJson(
126
+ cachePath(cache.dir, 'cookies', identity),
127
+ cache.ttlSeconds,
128
+ now
129
+ );
130
+ return cached?.kind === 'cookies' && Array.isArray(cached.cookies)
131
+ ? cached.cookies
132
+ : null;
133
+ }
134
+
135
+ export async function writeCookieResultCache({
136
+ cache,
137
+ identity,
138
+ cookies,
139
+ now = Date.now,
140
+ }) {
141
+ if (!cache.enabled) {
142
+ return;
143
+ }
144
+ await ensureCacheDirectory(cache.dir);
145
+ await writeOwnerOnlyJson(cachePath(cache.dir, 'cookies', identity), {
146
+ version: 1,
147
+ kind: 'cookies',
148
+ savedAt: now() / 1000,
149
+ cookies,
150
+ });
151
+ }
152
+
153
+ async function removeStaleLock(lockPath, now) {
154
+ try {
155
+ const lockStat = await stat(lockPath);
156
+ if (now() - lockStat.mtimeMs > LOCK_STALE_MILLISECONDS) {
157
+ await rm(lockPath, { force: true });
158
+ }
159
+ } catch {
160
+ // Another process may have released it already.
161
+ }
162
+ }
163
+
164
+ async function wait(milliseconds) {
165
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
166
+ }
167
+
168
+ async function acquireCredentialLock(lockPath, cachedPath, options) {
169
+ const startedAt = options.now();
170
+ while (options.now() - startedAt <= LOCK_WAIT_MILLISECONDS) {
171
+ try {
172
+ return await open(lockPath, 'wx', 0o600);
173
+ } catch (error) {
174
+ if (error.code !== 'EEXIST') {
175
+ throw error;
176
+ }
177
+ const cached = await readFreshJson(
178
+ cachedPath,
179
+ options.ttlSeconds,
180
+ options.now
181
+ );
182
+ if (
183
+ cached?.kind === 'derived-key' &&
184
+ (!options.refresh || cached.savedAt !== options.initialSavedAt)
185
+ ) {
186
+ return { cached };
187
+ }
188
+ await removeStaleLock(lockPath, options.now);
189
+ await wait(50);
190
+ }
191
+ }
192
+ throw new Error('timed out waiting for another cookie credential reader');
193
+ }
194
+
195
+ async function loadOrCreateCredential({
196
+ cache,
197
+ identity,
198
+ refresh,
199
+ create,
200
+ now,
201
+ metadata,
202
+ }) {
203
+ if (!cache.enabled) {
204
+ return { key: Buffer.from(await create()), savedAt: now() / 1000 };
205
+ }
206
+ await ensureCacheDirectory(cache.dir);
207
+ const cachedPath = cachePath(cache.dir, 'credential', identity);
208
+ const lockPath = `${cachedPath}.lock`;
209
+ const initial = await readFreshJson(cachedPath, cache.ttlSeconds, now);
210
+ if (!refresh && initial?.kind === 'derived-key') {
211
+ return {
212
+ key: Buffer.from(initial.key, 'base64'),
213
+ savedAt: initial.savedAt,
214
+ };
215
+ }
216
+
217
+ const acquired = await acquireCredentialLock(lockPath, cachedPath, {
218
+ initialSavedAt: initial?.savedAt,
219
+ now,
220
+ refresh,
221
+ ttlSeconds: cache.ttlSeconds,
222
+ });
223
+ if (acquired.cached) {
224
+ return {
225
+ key: Buffer.from(acquired.cached.key, 'base64'),
226
+ savedAt: acquired.cached.savedAt,
227
+ };
228
+ }
229
+
230
+ try {
231
+ const afterLock = await readFreshJson(cachedPath, cache.ttlSeconds, now);
232
+ if (
233
+ afterLock?.kind === 'derived-key' &&
234
+ (!refresh || afterLock.savedAt !== initial?.savedAt)
235
+ ) {
236
+ return {
237
+ key: Buffer.from(afterLock.key, 'base64'),
238
+ savedAt: afterLock.savedAt,
239
+ };
240
+ }
241
+ const key = Buffer.from(await create());
242
+ const savedAt = now() / 1000;
243
+ await writeOwnerOnlyJson(cachedPath, {
244
+ version: 1,
245
+ kind: 'derived-key',
246
+ savedAt,
247
+ key: key.toString('base64'),
248
+ ...metadata,
249
+ });
250
+ return { key, savedAt };
251
+ } finally {
252
+ await acquired.close();
253
+ await rm(lockPath, { force: true });
254
+ }
255
+ }
256
+
257
+ export async function getCachedCredential(options) {
258
+ const memoryIdentity = `${options.cache.dir ?? 'disabled'}:${options.identity}`;
259
+ const now = options.now ?? Date.now;
260
+ if (!options.refresh && credentialPromises.has(memoryIdentity)) {
261
+ const cached = await credentialPromises.get(memoryIdentity);
262
+ const age = now() / 1000 - cached.savedAt;
263
+ if (age >= 0 && age <= options.cache.ttlSeconds) {
264
+ return cached.key;
265
+ }
266
+ credentialPromises.delete(memoryIdentity);
267
+ }
268
+ const promise = loadOrCreateCredential({ ...options, now });
269
+ credentialPromises.set(memoryIdentity, promise);
270
+ try {
271
+ return (await promise).key;
272
+ } catch (error) {
273
+ if (credentialPromises.get(memoryIdentity) === promise) {
274
+ credentialPromises.delete(memoryIdentity);
275
+ }
276
+ throw error;
277
+ }
278
+ }
@@ -0,0 +1,148 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { promisify } from 'node:util';
4
+
5
+ const execFile = promisify(execFileCallback);
6
+
7
+ const SAFE_STORAGE = {
8
+ brave: {
9
+ application: 'brave',
10
+ folder: 'Brave Keys',
11
+ service: 'Brave Safe Storage',
12
+ },
13
+ chrome: {
14
+ application: 'chrome',
15
+ folder: 'Chrome Keys',
16
+ service: 'Chrome Safe Storage',
17
+ },
18
+ chromium: {
19
+ application: 'chromium',
20
+ folder: 'Chromium Keys',
21
+ service: 'Chromium Safe Storage',
22
+ },
23
+ edge: {
24
+ application: 'microsoft-edge',
25
+ folder: 'Microsoft Edge Keys',
26
+ service: 'Microsoft Edge Safe Storage',
27
+ },
28
+ };
29
+
30
+ async function runCredentialCommand(command, args, environment) {
31
+ const { stdout } = await execFile(command, args, {
32
+ encoding: 'utf8',
33
+ env: environment,
34
+ maxBuffer: 1024 * 1024,
35
+ windowsHide: true,
36
+ });
37
+ return stdout.trim();
38
+ }
39
+
40
+ async function readLinuxSafeStoragePassword(browser, environment, runCommand) {
41
+ const identity = SAFE_STORAGE[browser];
42
+ try {
43
+ const password = await runCommand(
44
+ 'secret-tool',
45
+ ['lookup', 'application', identity.application],
46
+ environment
47
+ );
48
+ if (password) {
49
+ return password;
50
+ }
51
+ } catch {
52
+ // Try KWallet before reporting that v11 key storage is unavailable.
53
+ }
54
+ try {
55
+ const password = await runCommand(
56
+ 'kwallet-query',
57
+ ['-r', identity.service, '-f', identity.folder, 'kdewallet'],
58
+ environment
59
+ );
60
+ if (password) {
61
+ return password;
62
+ }
63
+ } catch {
64
+ // Fall through to the actionable error below.
65
+ }
66
+ throw new Error(
67
+ `Could not read ${identity.service} from libsecret or KWallet; install secret-tool or unlock the browser key store`
68
+ );
69
+ }
70
+
71
+ /** Read a Chromium Safe Storage password from the platform credential store. */
72
+ export async function readSafeStoragePassword({
73
+ browser,
74
+ platform = process.platform,
75
+ environment = process.env,
76
+ runCredentialCommand: runCommand = runCredentialCommand,
77
+ }) {
78
+ const identity = SAFE_STORAGE[browser];
79
+ if (!identity) {
80
+ throw new Error(`No Safe Storage identity is known for ${browser}`);
81
+ }
82
+ if (platform === 'darwin') {
83
+ const password = await runCommand(
84
+ 'security',
85
+ ['find-generic-password', '-w', '-s', identity.service],
86
+ environment
87
+ );
88
+ if (!password) {
89
+ throw new Error(`${identity.service} returned an empty password`);
90
+ }
91
+ return password;
92
+ }
93
+ if (platform === 'linux') {
94
+ return readLinuxSafeStoragePassword(browser, environment, runCommand);
95
+ }
96
+ throw new Error(`Safe Storage passwords are not used on ${platform}`);
97
+ }
98
+
99
+ const DPAPI_SCRIPT = [
100
+ '$inputBytes=[Convert]::FromBase64String($args[0])',
101
+ '$outputBytes=[Security.Cryptography.ProtectedData]::Unprotect(',
102
+ ' $inputBytes,$null,',
103
+ ' [Security.Cryptography.DataProtectionScope]::CurrentUser)',
104
+ '[Convert]::ToBase64String($outputBytes)',
105
+ ].join(';');
106
+
107
+ /** Decrypt bytes with Windows DPAPI in the current user's security context. */
108
+ export async function decryptWindowsDpapi(
109
+ encryptedValue,
110
+ { environment = process.env } = {}
111
+ ) {
112
+ const output = await runCredentialCommand(
113
+ 'powershell.exe',
114
+ [
115
+ '-NoLogo',
116
+ '-NoProfile',
117
+ '-NonInteractive',
118
+ '-Command',
119
+ DPAPI_SCRIPT,
120
+ Buffer.from(encryptedValue).toString('base64'),
121
+ ],
122
+ environment
123
+ );
124
+ return Buffer.from(output, 'base64');
125
+ }
126
+
127
+ /** Recover the legacy AES-GCM key from a Chromium Local State file. */
128
+ export async function readWindowsEncryptionKey({
129
+ localStatePath,
130
+ environment = process.env,
131
+ decryptDpapi = decryptWindowsDpapi,
132
+ }) {
133
+ let state;
134
+ try {
135
+ state = JSON.parse(await readFile(localStatePath, 'utf8'));
136
+ } catch (error) {
137
+ throw new Error(`Could not read Chromium Local State: ${error.message}`);
138
+ }
139
+ const encoded = state.os_crypt?.encrypted_key;
140
+ if (!encoded) {
141
+ throw new Error('Chromium Local State has no os_crypt.encrypted_key');
142
+ }
143
+ const encryptedKey = Buffer.from(encoded, 'base64');
144
+ if (encryptedKey.subarray(0, 5).toString('ascii') !== 'DPAPI') {
145
+ throw new Error('Chromium Local State key does not have a DPAPI prefix');
146
+ }
147
+ return decryptDpapi(encryptedKey.subarray(5), { environment });
148
+ }
@@ -0,0 +1,128 @@
1
+ import {
2
+ createDecipheriv,
3
+ createHash,
4
+ pbkdf2Sync,
5
+ timingSafeEqual,
6
+ } from 'node:crypto';
7
+ import { TextDecoder } from 'node:util';
8
+
9
+ const CHROMIUM_CBC_IV = Buffer.alloc(16, 0x20);
10
+ const DOMAIN_HASH_BYTES = 32;
11
+
12
+ export function deriveChromiumCookieKey(password, platform) {
13
+ if (platform !== 'darwin' && platform !== 'linux') {
14
+ throw new Error(`CBC cookie keys are not used on ${platform}`);
15
+ }
16
+ return pbkdf2Sync(
17
+ password,
18
+ 'saltysalt',
19
+ platform === 'darwin' ? 1003 : 1,
20
+ 16,
21
+ 'sha1'
22
+ );
23
+ }
24
+
25
+ function removeDomainHash(plaintext, host, databaseVersion) {
26
+ if (databaseVersion < 24) {
27
+ return plaintext;
28
+ }
29
+ if (plaintext.length < DOMAIN_HASH_BYTES) {
30
+ throw new Error('decrypted cookie is missing its domain hash');
31
+ }
32
+ const expectedHash = createHash('sha256').update(host).digest();
33
+ const actualHash = plaintext.subarray(0, DOMAIN_HASH_BYTES);
34
+ if (!timingSafeEqual(actualHash, expectedHash)) {
35
+ throw new Error('decrypted cookie domain hash does not match its host');
36
+ }
37
+ return plaintext.subarray(DOMAIN_HASH_BYTES);
38
+ }
39
+
40
+ function decodeCookieValue(plaintext) {
41
+ return new TextDecoder('utf-8', { fatal: true }).decode(plaintext);
42
+ }
43
+
44
+ /** Validate and decode plaintext returned by a Chromium platform decryptor. */
45
+ export function decodeChromiumCookiePlaintext({
46
+ plaintext,
47
+ host,
48
+ databaseVersion = 0,
49
+ }) {
50
+ return decodeCookieValue(
51
+ removeDomainHash(Buffer.from(plaintext), host, databaseVersion)
52
+ );
53
+ }
54
+
55
+ function decryptCbcCookie(encryptedValue, key) {
56
+ const decipher = createDecipheriv('aes-128-cbc', key, CHROMIUM_CBC_IV);
57
+ return Buffer.concat([
58
+ decipher.update(encryptedValue.subarray(3)),
59
+ decipher.final(),
60
+ ]);
61
+ }
62
+
63
+ function decryptGcmCookie(encryptedValue, key) {
64
+ const payload = encryptedValue.subarray(3);
65
+ if (payload.length < 12 + 16) {
66
+ throw new Error('encrypted AES-GCM cookie is truncated');
67
+ }
68
+ const nonce = payload.subarray(0, 12);
69
+ const authenticationTag = payload.subarray(-16);
70
+ const ciphertext = payload.subarray(12, -16);
71
+ const decipher = createDecipheriv('aes-256-gcm', key, nonce);
72
+ decipher.setAuthTag(authenticationTag);
73
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
74
+ }
75
+
76
+ /** Decrypt a Chromium cookie encrypted_value using an already recovered key. */
77
+ export function decryptChromiumCookie({
78
+ encryptedValue,
79
+ host,
80
+ databaseVersion = 0,
81
+ platform = process.platform,
82
+ key,
83
+ }) {
84
+ const encrypted = Buffer.from(encryptedValue);
85
+ const prefix = encrypted.subarray(0, 3).toString('ascii');
86
+ if (prefix === 'v20') {
87
+ throw new Error(
88
+ 'Windows app-bound v20 cookies cannot be decrypted outside the browser; use a browser-supported export or a previously saved storage state'
89
+ );
90
+ }
91
+ if (prefix !== 'v10' && prefix !== 'v11') {
92
+ throw new Error(
93
+ 'cookie does not use a supported Chromium encryption prefix'
94
+ );
95
+ }
96
+ if (!Buffer.isBuffer(key)) {
97
+ throw new TypeError('a recovered cookie encryption key is required');
98
+ }
99
+ const plaintext =
100
+ platform === 'win32'
101
+ ? decryptGcmCookie(encrypted, key)
102
+ : decryptCbcCookie(encrypted, key);
103
+ return decodeChromiumCookiePlaintext({
104
+ plaintext,
105
+ host,
106
+ databaseVersion,
107
+ });
108
+ }
109
+
110
+ export function chromiumSameSite(value) {
111
+ if (Number(value) === 2) {
112
+ return 'Strict';
113
+ }
114
+ if (Number(value) === 1) {
115
+ return 'Lax';
116
+ }
117
+ return 'None';
118
+ }
119
+
120
+ export function firefoxSameSite(value) {
121
+ if (Number(value) === 2) {
122
+ return 'Strict';
123
+ }
124
+ if (Number(value) === 1) {
125
+ return 'Lax';
126
+ }
127
+ return 'None';
128
+ }