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.
@@ -0,0 +1,35 @@
1
+ import BetterSqlite3 from 'better-sqlite3';
2
+
3
+ let builtInSqlitePromise;
4
+
5
+ function loadBuiltInSqlite() {
6
+ builtInSqlitePromise ??= import('node:sqlite')
7
+ .then(({ DatabaseSync }) => DatabaseSync)
8
+ .catch(() => null);
9
+ return builtInSqlitePromise;
10
+ }
11
+
12
+ /** Open SQLite without requiring a native addon on Node versions that provide it. */
13
+ export async function openSqliteDatabase(
14
+ filename,
15
+ { fileMustExist = false, readOnly = false } = {}
16
+ ) {
17
+ const DatabaseSync = await loadBuiltInSqlite();
18
+ if (DatabaseSync) {
19
+ return new DatabaseSync(filename, { readOnly });
20
+ }
21
+ return new BetterSqlite3(filename, {
22
+ fileMustExist,
23
+ readonly: readOnly,
24
+ });
25
+ }
26
+
27
+ /** Configure a statement to preserve Chromium's 64-bit timestamp values. */
28
+ export function preserveIntegerPrecision(statement) {
29
+ if (typeof statement.setReadBigInts === 'function') {
30
+ statement.setReadBigInts(true);
31
+ } else {
32
+ statement.safeIntegers();
33
+ }
34
+ return statement;
35
+ }
@@ -0,0 +1,343 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+
4
+ import {
5
+ clearBrowserCookieMemoryCache,
6
+ getCachedCredential,
7
+ normalizeCookieCache,
8
+ readCookieResultCache,
9
+ writeCookieResultCache,
10
+ } from './browser-cookie-cache.js';
11
+ import {
12
+ chromiumSameSite,
13
+ decodeChromiumCookiePlaintext,
14
+ decryptChromiumCookie,
15
+ deriveChromiumCookieKey,
16
+ firefoxSameSite,
17
+ } from './browser-cookie-crypto.js';
18
+ import {
19
+ decryptWindowsDpapi,
20
+ readSafeStoragePassword,
21
+ readWindowsEncryptionKey,
22
+ } from './browser-cookie-credentials.js';
23
+ import {
24
+ openSqliteDatabase,
25
+ preserveIntegerPrecision,
26
+ } from './browser-cookie-database.js';
27
+ import {
28
+ findCookieDatabase,
29
+ listBrowserProfiles,
30
+ normalizeCookieBrowser,
31
+ resolveBrowserProfile,
32
+ } from './browser-profiles.js';
33
+
34
+ const CHROME_EPOCH_OFFSET_SECONDS = 11_644_473_600n;
35
+ const MICROSECONDS_PER_SECOND = 1_000_000n;
36
+
37
+ function toNumber(value) {
38
+ return typeof value === 'bigint' ? Number(value) : Number(value ?? 0);
39
+ }
40
+
41
+ function chromiumExpires(value) {
42
+ const microseconds = BigInt(value ?? 0);
43
+ if (microseconds === 0n) {
44
+ return -1;
45
+ }
46
+ return Number(
47
+ microseconds / MICROSECONDS_PER_SECOND - CHROME_EPOCH_OFFSET_SECONDS
48
+ );
49
+ }
50
+
51
+ function firefoxExpires(value) {
52
+ const expires = toNumber(value);
53
+ return expires > 0 ? expires : -1;
54
+ }
55
+
56
+ function queryRows(database, query, domainFilter) {
57
+ const statement = preserveIntegerPrecision(database.prepare(query));
58
+ return domainFilter ? statement.all(`%${domainFilter}%`) : statement.all();
59
+ }
60
+
61
+ function readDatabaseVersion(database) {
62
+ try {
63
+ const row = database
64
+ .prepare("SELECT value FROM meta WHERE key = 'version'")
65
+ .get();
66
+ return Number(row?.value ?? 0);
67
+ } catch {
68
+ return 0;
69
+ }
70
+ }
71
+
72
+ function readFirefoxRows(database, domainFilter) {
73
+ return queryRows(
74
+ database,
75
+ `SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite
76
+ FROM moz_cookies
77
+ ${domainFilter ? 'WHERE host LIKE ?' : ''}
78
+ ORDER BY host, name, path`,
79
+ domainFilter
80
+ );
81
+ }
82
+
83
+ function readChromiumRows(database, domainFilter) {
84
+ return queryRows(
85
+ database,
86
+ `SELECT host_key, name, value, encrypted_value, path, expires_utc,
87
+ is_secure, is_httponly, samesite
88
+ FROM cookies
89
+ ${domainFilter ? 'WHERE host_key LIKE ?' : ''}
90
+ ORDER BY host_key, name, path`,
91
+ domainFilter
92
+ );
93
+ }
94
+
95
+ function mapFirefoxRows(rows) {
96
+ return rows.map((row) => ({
97
+ name: row.name,
98
+ value: row.value,
99
+ domain: row.host,
100
+ path: row.path || '/',
101
+ expires: firefoxExpires(row.expiry),
102
+ httpOnly: Boolean(row.isHttpOnly),
103
+ secure: Boolean(row.isSecure),
104
+ sameSite: firefoxSameSite(row.sameSite),
105
+ }));
106
+ }
107
+
108
+ function encryptionPrefix(encryptedValue) {
109
+ return Buffer.from(encryptedValue).subarray(0, 3).toString('ascii');
110
+ }
111
+
112
+ function chromiumKeyForPrefix(context, prefix) {
113
+ if (context.platform === 'linux' && prefix === 'v10') {
114
+ return deriveChromiumCookieKey('peanuts', 'linux');
115
+ }
116
+ if (context.platform === 'linux' || context.platform === 'darwin') {
117
+ const identity = `${context.browser}:${context.platform}:safe-storage`;
118
+ if (!context.credentialKeys.has(identity)) {
119
+ context.credentialKeys.set(
120
+ identity,
121
+ getCachedCredential({
122
+ cache: context.cache,
123
+ identity,
124
+ refresh: context.refresh,
125
+ now: context.now,
126
+ metadata: {
127
+ browser: context.browser,
128
+ platform: context.platform,
129
+ source: 'safe-storage',
130
+ },
131
+ create: async () =>
132
+ deriveChromiumCookieKey(
133
+ await context.readSafeStoragePassword({
134
+ browser: context.browser,
135
+ platform: context.platform,
136
+ environment: context.environment,
137
+ }),
138
+ context.platform
139
+ ),
140
+ })
141
+ );
142
+ }
143
+ return context.credentialKeys.get(identity);
144
+ }
145
+ if (context.platform === 'win32') {
146
+ const identity = `${context.browser}:win32:legacy-aes-key`;
147
+ if (!context.credentialKeys.has(identity)) {
148
+ context.credentialKeys.set(
149
+ identity,
150
+ getCachedCredential({
151
+ cache: context.cache,
152
+ identity,
153
+ refresh: context.refresh,
154
+ now: context.now,
155
+ metadata: {
156
+ browser: context.browser,
157
+ platform: context.platform,
158
+ source: 'dpapi',
159
+ },
160
+ create: () =>
161
+ context.readWindowsEncryptionKey({
162
+ localStatePath: path.join(
163
+ path.dirname(context.profile.path),
164
+ 'Local State'
165
+ ),
166
+ environment: context.environment,
167
+ decryptDpapi: context.decryptWindowsDpapi,
168
+ }),
169
+ })
170
+ );
171
+ }
172
+ return context.credentialKeys.get(identity);
173
+ }
174
+ throw new Error(
175
+ `Chromium cookie decryption is unsupported on ${context.platform}`
176
+ );
177
+ }
178
+
179
+ async function decryptChromiumRow(row, databaseVersion, context) {
180
+ if (row.value) {
181
+ return row.value;
182
+ }
183
+ const encryptedValue = Buffer.from(row.encrypted_value);
184
+ if (encryptedValue.length === 0) {
185
+ return '';
186
+ }
187
+ const prefix = encryptionPrefix(encryptedValue);
188
+ if (context.platform === 'win32' && prefix !== 'v10' && prefix !== 'v11') {
189
+ if (prefix === 'v20') {
190
+ return decryptChromiumCookie({
191
+ encryptedValue,
192
+ host: row.host_key,
193
+ databaseVersion,
194
+ platform: context.platform,
195
+ key: Buffer.alloc(32),
196
+ });
197
+ }
198
+ const plaintext = await context.decryptWindowsDpapi(encryptedValue, {
199
+ environment: context.environment,
200
+ });
201
+ return decodeChromiumCookiePlaintext({
202
+ plaintext,
203
+ host: row.host_key,
204
+ databaseVersion,
205
+ });
206
+ }
207
+ const key = await chromiumKeyForPrefix(context, prefix);
208
+ return decryptChromiumCookie({
209
+ encryptedValue,
210
+ host: row.host_key,
211
+ databaseVersion,
212
+ platform: context.platform,
213
+ key,
214
+ });
215
+ }
216
+
217
+ async function mapChromiumRows(rows, databaseVersion, context) {
218
+ const cookies = [];
219
+ for (const row of rows) {
220
+ try {
221
+ cookies.push({
222
+ name: row.name,
223
+ value: await decryptChromiumRow(row, databaseVersion, context),
224
+ domain: row.host_key,
225
+ path: row.path || '/',
226
+ expires: chromiumExpires(row.expires_utc),
227
+ httpOnly: Boolean(row.is_httponly),
228
+ secure: Boolean(row.is_secure),
229
+ sameSite: chromiumSameSite(row.samesite),
230
+ });
231
+ } catch (error) {
232
+ if (!context.ignoreDecryptionErrors) {
233
+ throw new Error(
234
+ `Could not decrypt cookie ${row.name} for ${row.host_key}: ${error.message}`
235
+ );
236
+ }
237
+ }
238
+ }
239
+ return cookies;
240
+ }
241
+
242
+ async function openCookieDatabase(cookiePath) {
243
+ try {
244
+ return await openSqliteDatabase(cookiePath, {
245
+ fileMustExist: true,
246
+ readOnly: true,
247
+ });
248
+ } catch (error) {
249
+ throw new Error(`Could not open browser cookie database: ${error.message}`);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Read cookies from an installed browser profile in Playwright/Puppeteer shape.
255
+ * Use dependency injection only for deterministic platform and credential tests.
256
+ */
257
+ export async function readBrowserCookiesWithDependencies(
258
+ options,
259
+ dependencies = {}
260
+ ) {
261
+ if (!options || typeof options !== 'object') {
262
+ throw new TypeError('readBrowserCookies requires an options object');
263
+ }
264
+ const browser = normalizeCookieBrowser(options.browser);
265
+ const platform = dependencies.platform ?? process.platform;
266
+ const homeDir = dependencies.homeDir ?? os.homedir();
267
+ const environment = dependencies.environment ?? process.env;
268
+ const profile = await resolveBrowserProfile({
269
+ browser,
270
+ profile: options.profile,
271
+ platform,
272
+ homeDir,
273
+ environment,
274
+ });
275
+ const cookiePath = await findCookieDatabase(browser, profile.path, platform);
276
+ if (!cookiePath) {
277
+ throw new Error(`No cookie database exists in ${profile.path}`);
278
+ }
279
+ const cache = normalizeCookieCache(
280
+ options.cache,
281
+ homeDir,
282
+ options.ttlMinutes
283
+ );
284
+ const identity = JSON.stringify({
285
+ browser,
286
+ profile: profile.path,
287
+ domainFilter: options.domainFilter ?? null,
288
+ ignoreDecryptionErrors: options.ignoreDecryptionErrors === true,
289
+ });
290
+ const now = dependencies.now ?? Date.now;
291
+ const cachedCookies = await readCookieResultCache({
292
+ cache,
293
+ identity,
294
+ refresh: options.refresh === true,
295
+ now,
296
+ });
297
+ if (cachedCookies) {
298
+ return cachedCookies;
299
+ }
300
+
301
+ const database = await openCookieDatabase(cookiePath);
302
+ let cookies;
303
+ try {
304
+ if (browser === 'firefox') {
305
+ cookies = mapFirefoxRows(readFirefoxRows(database, options.domainFilter));
306
+ } else {
307
+ const databaseVersion = readDatabaseVersion(database);
308
+ const rows = readChromiumRows(database, options.domainFilter);
309
+ cookies = await mapChromiumRows(rows, databaseVersion, {
310
+ browser,
311
+ cache,
312
+ credentialKeys: new Map(),
313
+ decryptWindowsDpapi:
314
+ dependencies.decryptWindowsDpapi ?? decryptWindowsDpapi,
315
+ environment,
316
+ ignoreDecryptionErrors: options.ignoreDecryptionErrors === true,
317
+ now,
318
+ platform,
319
+ profile,
320
+ readSafeStoragePassword:
321
+ dependencies.readSafeStoragePassword ?? readSafeStoragePassword,
322
+ readWindowsEncryptionKey:
323
+ dependencies.readWindowsEncryptionKey ?? readWindowsEncryptionKey,
324
+ refresh: options.refresh === true,
325
+ });
326
+ }
327
+ } finally {
328
+ database.close();
329
+ }
330
+ await writeCookieResultCache({ cache, identity, cookies, now });
331
+ return cookies;
332
+ }
333
+
334
+ /** Read cookies from an installed Chrome, Edge, Brave, Chromium, or Firefox. */
335
+ export function readBrowserCookies(options) {
336
+ return readBrowserCookiesWithDependencies(options);
337
+ }
338
+
339
+ export {
340
+ clearBrowserCookieMemoryCache,
341
+ decryptChromiumCookie,
342
+ listBrowserProfiles,
343
+ };
@@ -0,0 +1,280 @@
1
+ import { access, readdir, readFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const SUPPORTED_COOKIE_BROWSERS = [
6
+ 'chrome',
7
+ 'edge',
8
+ 'brave',
9
+ 'chromium',
10
+ 'firefox',
11
+ ];
12
+
13
+ function platformPath(platform) {
14
+ return platform === 'win32' ? path.win32 : path;
15
+ }
16
+
17
+ export function normalizeCookieBrowser(browser) {
18
+ const normalized = browser === 'msedge' ? 'edge' : browser;
19
+ if (!SUPPORTED_COOKIE_BROWSERS.includes(normalized)) {
20
+ throw new Error(
21
+ `Unsupported browser: ${browser}. Expected one of ${SUPPORTED_COOKIE_BROWSERS.join(', ')}`
22
+ );
23
+ }
24
+ return normalized;
25
+ }
26
+
27
+ export function browserProfileRoot(
28
+ browser,
29
+ {
30
+ platform = process.platform,
31
+ homeDir = os.homedir(),
32
+ environment = process.env,
33
+ } = {}
34
+ ) {
35
+ const normalizedBrowser = normalizeCookieBrowser(browser);
36
+ const pathApi = platformPath(platform);
37
+ if (platform === 'darwin') {
38
+ const support = pathApi.join(homeDir, 'Library', 'Application Support');
39
+ const roots = {
40
+ brave: pathApi.join(support, 'BraveSoftware', 'Brave-Browser'),
41
+ chrome: pathApi.join(support, 'Google', 'Chrome'),
42
+ chromium: pathApi.join(support, 'Chromium'),
43
+ edge: pathApi.join(support, 'Microsoft Edge'),
44
+ firefox: pathApi.join(support, 'Firefox'),
45
+ };
46
+ return roots[normalizedBrowser];
47
+ }
48
+ if (platform === 'win32') {
49
+ const localAppData =
50
+ environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
51
+ const roamingAppData =
52
+ environment.APPDATA ?? pathApi.join(homeDir, 'AppData', 'Roaming');
53
+ const roots = {
54
+ brave: pathApi.join(
55
+ localAppData,
56
+ 'BraveSoftware',
57
+ 'Brave-Browser',
58
+ 'User Data'
59
+ ),
60
+ chrome: pathApi.join(localAppData, 'Google', 'Chrome', 'User Data'),
61
+ chromium: pathApi.join(localAppData, 'Chromium', 'User Data'),
62
+ edge: pathApi.join(localAppData, 'Microsoft', 'Edge', 'User Data'),
63
+ firefox: pathApi.join(roamingAppData, 'Mozilla', 'Firefox'),
64
+ };
65
+ return roots[normalizedBrowser];
66
+ }
67
+ const roots = {
68
+ brave: pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser'),
69
+ chrome: pathApi.join(homeDir, '.config', 'google-chrome'),
70
+ chromium: pathApi.join(homeDir, '.config', 'chromium'),
71
+ edge: pathApi.join(homeDir, '.config', 'microsoft-edge'),
72
+ firefox: pathApi.join(homeDir, '.mozilla', 'firefox'),
73
+ };
74
+ return roots[normalizedBrowser];
75
+ }
76
+
77
+ async function pathExists(filePath) {
78
+ try {
79
+ await access(filePath);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ async function readJson(filePath) {
87
+ try {
88
+ return JSON.parse(await readFile(filePath, 'utf8'));
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ function chromiumCookiePaths(profilePath, pathApi = path) {
95
+ return [
96
+ pathApi.join(profilePath, 'Network', 'Cookies'),
97
+ pathApi.join(profilePath, 'Cookies'),
98
+ ];
99
+ }
100
+
101
+ export async function findCookieDatabase(browser, profilePath, platform) {
102
+ const pathApi = platformPath(platform ?? process.platform);
103
+ if (normalizeCookieBrowser(browser) === 'firefox') {
104
+ const candidate = pathApi.join(profilePath, 'cookies.sqlite');
105
+ return (await pathExists(candidate)) ? candidate : null;
106
+ }
107
+ for (const candidate of chromiumCookiePaths(profilePath, pathApi)) {
108
+ if (await pathExists(candidate)) {
109
+ return candidate;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ async function listChromiumProfiles(browser, root, platform) {
116
+ if (!(await pathExists(root))) {
117
+ return [];
118
+ }
119
+ const pathApi = platformPath(platform);
120
+ const localState = await readJson(pathApi.join(root, 'Local State'));
121
+ const infoCache = localState.profile?.info_cache ?? {};
122
+ const names = new Set(Object.keys(infoCache));
123
+ try {
124
+ const entries = await readdir(root, { withFileTypes: true });
125
+ for (const entry of entries) {
126
+ if (
127
+ entry.isDirectory() &&
128
+ (entry.name === 'Default' || entry.name.startsWith('Profile '))
129
+ ) {
130
+ names.add(entry.name);
131
+ }
132
+ }
133
+ } catch {
134
+ return [];
135
+ }
136
+
137
+ const profiles = [];
138
+ for (const name of names) {
139
+ const profilePath = pathApi.join(root, name);
140
+ if (!(await findCookieDatabase(browser, profilePath, platform))) {
141
+ continue;
142
+ }
143
+ profiles.push({
144
+ browser,
145
+ name,
146
+ displayName: infoCache[name]?.name ?? name,
147
+ path: profilePath,
148
+ isDefault:
149
+ name === (localState.profile?.last_used ?? 'Default') ||
150
+ (names.size === 1 && name === 'Default'),
151
+ });
152
+ }
153
+ return profiles.sort(
154
+ (left, right) =>
155
+ Number(right.isDefault) - Number(left.isDefault) ||
156
+ left.name.localeCompare(right.name)
157
+ );
158
+ }
159
+
160
+ function parseIni(text) {
161
+ const sections = [];
162
+ let current;
163
+ for (const rawLine of text.split(/\r?\n/)) {
164
+ const line = rawLine.trim();
165
+ const sectionMatch = /^\[([^\]]+)]$/.exec(line);
166
+ if (sectionMatch) {
167
+ current = { section: sectionMatch[1] };
168
+ sections.push(current);
169
+ continue;
170
+ }
171
+ const separator = line.indexOf('=');
172
+ if (current && separator > 0 && !line.startsWith(';')) {
173
+ current[line.slice(0, separator)] = line.slice(separator + 1);
174
+ }
175
+ }
176
+ return sections;
177
+ }
178
+
179
+ async function listFirefoxProfiles(root, platform) {
180
+ if (!(await pathExists(root))) {
181
+ return [];
182
+ }
183
+ const pathApi = platformPath(platform);
184
+ let sections = [];
185
+ try {
186
+ sections = parseIni(
187
+ await readFile(pathApi.join(root, 'profiles.ini'), 'utf8')
188
+ );
189
+ } catch {
190
+ const profilesRoot = pathApi.join(root, 'Profiles');
191
+ try {
192
+ const entries = await readdir(profilesRoot, { withFileTypes: true });
193
+ sections = entries
194
+ .filter((entry) => entry.isDirectory())
195
+ .map((entry) => ({
196
+ section: 'Profile',
197
+ Name: entry.name,
198
+ Path: entry.name,
199
+ ProfilesRoot: profilesRoot,
200
+ }));
201
+ } catch {
202
+ return [];
203
+ }
204
+ }
205
+
206
+ const profiles = [];
207
+ for (const section of sections.filter(({ section }) =>
208
+ section.startsWith('Profile')
209
+ )) {
210
+ if (!section.Path) {
211
+ continue;
212
+ }
213
+ const relativeRoot = section.ProfilesRoot ?? root;
214
+ const profilePath =
215
+ section.IsRelative === '0'
216
+ ? section.Path
217
+ : pathApi.resolve(relativeRoot, section.Path);
218
+ if (!(await findCookieDatabase('firefox', profilePath, platform))) {
219
+ continue;
220
+ }
221
+ const displayName = section.Name ?? pathApi.basename(profilePath);
222
+ profiles.push({
223
+ browser: 'firefox',
224
+ name: displayName,
225
+ displayName,
226
+ path: profilePath,
227
+ isDefault: section.Default === '1',
228
+ });
229
+ }
230
+ return profiles.sort(
231
+ (left, right) =>
232
+ Number(right.isDefault) - Number(left.isDefault) ||
233
+ left.name.localeCompare(right.name)
234
+ );
235
+ }
236
+
237
+ /** Discover cookie-bearing profiles from installed browsers. */
238
+ export async function listBrowserProfiles({
239
+ browser,
240
+ platform = process.platform,
241
+ homeDir = os.homedir(),
242
+ environment = process.env,
243
+ } = {}) {
244
+ const browsers = browser
245
+ ? [normalizeCookieBrowser(browser)]
246
+ : SUPPORTED_COOKIE_BROWSERS;
247
+ const profiles = [];
248
+ for (const candidate of browsers) {
249
+ const root = browserProfileRoot(candidate, {
250
+ platform,
251
+ homeDir,
252
+ environment,
253
+ });
254
+ profiles.push(
255
+ ...(candidate === 'firefox'
256
+ ? await listFirefoxProfiles(root, platform)
257
+ : await listChromiumProfiles(candidate, root, platform))
258
+ );
259
+ }
260
+ return profiles;
261
+ }
262
+
263
+ export async function resolveBrowserProfile(options) {
264
+ const browser = normalizeCookieBrowser(options.browser);
265
+ const profiles = await listBrowserProfiles({ ...options, browser });
266
+ const requested = options.profile;
267
+ const selected = requested
268
+ ? profiles.find(
269
+ ({ name, displayName, path: profilePath }) =>
270
+ requested === name ||
271
+ requested === displayName ||
272
+ requested === platformPath(options.platform).basename(profilePath)
273
+ )
274
+ : (profiles.find(({ isDefault }) => isDefault) ?? profiles[0]);
275
+ if (!selected) {
276
+ const detail = requested ? ` profile "${requested}"` : ' profile';
277
+ throw new Error(`Could not find a cookie database for ${browser}${detail}`);
278
+ }
279
+ return selected;
280
+ }
package/src/exports.js CHANGED
@@ -43,6 +43,10 @@ export {
43
43
 
44
44
  // Re-export browser management
45
45
  export { connectBrowser } from './browser/connector.js';
46
+ export {
47
+ listBrowserProfiles,
48
+ readBrowserCookies,
49
+ } from './browser/browser-cookies.js';
46
50
  export {
47
51
  launchAndConnectRealBrowser,
48
52
  launchRealBrowser,
@@ -0,0 +1,21 @@
1
+ import { appendFile } from 'node:fs/promises';
2
+
3
+ import { getCachedCredential } from '../../src/browser/browser-cookie-cache.js';
4
+
5
+ const [, , cacheDir, credentialReads] = process.argv;
6
+
7
+ await getCachedCredential({
8
+ cache: { enabled: true, dir: cacheDir, ttlSeconds: 60 },
9
+ identity: 'chrome:linux:safe-storage',
10
+ refresh: false,
11
+ metadata: {
12
+ browser: 'chrome',
13
+ platform: 'linux',
14
+ source: 'safe-storage',
15
+ },
16
+ create: async () => {
17
+ await appendFile(credentialReads, `${process.pid}\n`, 'utf8');
18
+ await new Promise((resolve) => setTimeout(resolve, 250));
19
+ return Buffer.alloc(16, 7);
20
+ },
21
+ });