@tiny-fish/cli 0.46.1-next.362 → 0.46.1-next.363

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.
@@ -2,22 +2,75 @@ import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import { createRequire } from 'module';
5
+ import { z } from 'zod';
5
6
  import { getApiKey } from '../lib/auth.js';
6
7
  import { deriveChromeKey, listAuthRelevantHostKeys, openCookiesDb, readCookiesForHost, } from '../lib/chrome-cookies.js';
7
8
  import { profileCreate, profileUpload } from '../lib/client.js';
8
9
  import { err, errLine, handleApiError, out, outLine } from '../lib/output.js';
9
10
  // ── Helpers ───────────────────────────────────────────────────────────────────
10
- function getChromeCookiesPath() {
11
+ const chromeLocalStateSchema = z.object({
12
+ profile: z.object({ last_used: z.string().optional() }).optional(),
13
+ });
14
+ function getChromeRoot() {
11
15
  const platform = process.platform;
12
16
  if (platform === 'darwin') {
13
- return path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'Default', 'Cookies');
17
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome');
14
18
  }
15
19
  if (platform === 'linux') {
16
- return path.join(os.homedir(), '.config', 'google-chrome', 'Default', 'Cookies');
20
+ return path.join(os.homedir(), '.config', 'google-chrome');
17
21
  }
18
22
  // Windows guard — handled in withCookiesDb
19
23
  return '';
20
24
  }
25
+ function sanitizeChromeProfile(profile) {
26
+ if (!profile || profile === '.' || profile === '..' || path.basename(profile) !== profile) {
27
+ throw new Error('Chrome profile must be a directory name such as "Default" or "Profile 1"');
28
+ }
29
+ return profile;
30
+ }
31
+ function assertChromePathContained(targetPath) {
32
+ const chromeRoot = fs.realpathSync(getChromeRoot());
33
+ const resolvedTarget = fs.realpathSync(targetPath);
34
+ const relative = path.relative(chromeRoot, resolvedTarget);
35
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
36
+ throw new Error('Chrome data path resolves outside the Chrome data directory');
37
+ }
38
+ }
39
+ function getChromeCookiesPath(requestedProfile) {
40
+ const chromeRoot = getChromeRoot();
41
+ if (requestedProfile !== undefined) {
42
+ return path.join(chromeRoot, sanitizeChromeProfile(requestedProfile), 'Cookies');
43
+ }
44
+ const localStatePath = path.join(chromeRoot, 'Local State');
45
+ try {
46
+ fs.lstatSync(localStatePath);
47
+ }
48
+ catch (error) {
49
+ if (error.code === 'ENOENT') {
50
+ return path.join(chromeRoot, 'Default', 'Cookies');
51
+ }
52
+ throw error;
53
+ }
54
+ try {
55
+ assertChromePathContained(localStatePath);
56
+ }
57
+ catch (error) {
58
+ if (error.code === 'ENOENT') {
59
+ throw new Error('Could not read Chrome profile state. Use --chrome-profile to select a profile.');
60
+ }
61
+ throw error;
62
+ }
63
+ try {
64
+ const localState = chromeLocalStateSchema.parse(JSON.parse(fs.readFileSync(localStatePath, 'utf8')));
65
+ const lastUsed = localState.profile?.last_used;
66
+ if (!lastUsed)
67
+ throw new Error('missing last-used profile');
68
+ return path.join(chromeRoot, sanitizeChromeProfile(lastUsed), 'Cookies');
69
+ }
70
+ catch {
71
+ throw new Error('Could not read Chrome profile state. Use --chrome-profile to select a profile.');
72
+ }
73
+ }
21
74
  function validateDomain(domain) {
22
75
  if (domain.includes('/') || domain.includes('://') || /\s/.test(domain) || /:\d/.test(domain)) {
23
76
  err({ error: 'domain must be a plain hostname (e.g. github.com)' });
@@ -25,14 +78,15 @@ function validateDomain(domain) {
25
78
  }
26
79
  }
27
80
  // Opens a private copy of Chrome's cookie DB, derives the decryption key, and calls fn.
28
- function withCookiesDb(fn) {
81
+ function withCookiesDb(chromeProfile, fn) {
29
82
  if (process.platform === 'win32') {
30
83
  throw new Error('Cookie extraction is not supported on Windows yet');
31
84
  }
32
- const cookiesPath = getChromeCookiesPath();
85
+ const cookiesPath = getChromeCookiesPath(chromeProfile);
33
86
  if (!fs.existsSync(cookiesPath)) {
34
- throw new Error(`Chrome cookies file not found at ${cookiesPath}. Is Google Chrome installed?`);
87
+ throw new Error(`Chrome cookies file not found at ${cookiesPath}. Use --chrome-profile to select another Chrome profile.`);
35
88
  }
89
+ assertChromePathContained(cookiesPath);
36
90
  // Copy the DB out of the profile directory so we avoid the Chrome file lock.
37
91
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-chrome-cookies-'));
38
92
  let db;
@@ -41,8 +95,11 @@ function withCookiesDb(fn) {
41
95
  fs.copyFileSync(cookiesPath, dbPath);
42
96
  // Committed rows can sit in Cookies-wal until Chrome checkpoints.
43
97
  for (const ext of ['-wal', '-shm']) {
44
- if (fs.existsSync(cookiesPath + ext))
45
- fs.copyFileSync(cookiesPath + ext, dbPath + ext);
98
+ const source = cookiesPath + ext;
99
+ if (fs.existsSync(source)) {
100
+ assertChromePathContained(source);
101
+ fs.copyFileSync(source, dbPath + ext);
102
+ }
46
103
  }
47
104
  try {
48
105
  db = openCookiesDb(dbPath);
@@ -67,11 +124,11 @@ function withCookiesDb(fn) {
67
124
  }
68
125
  }
69
126
  }
70
- function extractChromeCookies(domain, stats) {
71
- return withCookiesDb((db, key) => readCookiesForHost(db, key, domain, stats));
127
+ function extractChromeCookies(domain, chromeProfile, stats) {
128
+ return withCookiesDb(chromeProfile, (db, key) => readCookiesForHost(db, key, domain, stats));
72
129
  }
73
- function extractAllChromeCookies(stats) {
74
- return withCookiesDb((db, key) => {
130
+ function extractAllChromeCookies(chromeProfile, stats) {
131
+ return withCookiesDb(chromeProfile, (db, key) => {
75
132
  let hostKeys;
76
133
  try {
77
134
  hostKeys = listAuthRelevantHostKeys(db);
@@ -162,8 +219,8 @@ function extractCookiesOrThrow(opts) {
162
219
  // if extraction fails (Windows, no Chrome, no cookies).
163
220
  const stats = { keyringSkipped: 0 };
164
221
  const cookies = opts.allDomains
165
- ? extractAllChromeCookies(stats)
166
- : extractChromeCookies(opts.domain, stats);
222
+ ? extractAllChromeCookies(opts.chromeProfile, stats)
223
+ : extractChromeCookies(opts.domain, opts.chromeProfile, stats);
167
224
  // Warn even on partial success — surviving plaintext rows can mask
168
225
  // that every auth cookie was keyring-encrypted and skipped.
169
226
  if (stats.keyringSkipped > 0) {
@@ -232,6 +289,7 @@ export function registerProfile(program) {
232
289
  .description('Import Chrome cookies into a profile. Use --domain for a single domain or --all-domains for every domain in your Chrome profile. Exactly one must be provided.')
233
290
  .option('--domain <domain>', 'Domain to import cookies for (e.g. github.com)')
234
291
  .option('--all-domains', 'Import cookies for all registrable domains (e.g. github.com) found in your Chrome profile. IPs and non-resolvable hostnames (e.g. localhost) are skipped.')
292
+ .option('--chrome-profile <name>', 'Chrome profile directory (auto-detects the last-used profile by default)')
235
293
  .option('--profile-id <id>', 'Profile ID to import cookies into (creates new profile if omitted)')
236
294
  .option('--pretty', 'Human-readable output')
237
295
  .action(async (opts) => {
@@ -256,9 +314,10 @@ export function registerProfile(program) {
256
314
  const created = await profileCreate(name, apiKey);
257
315
  profileId = created.id;
258
316
  }
259
- const result = opts.allDomains
317
+ const uploadResult = opts.allDomains
260
318
  ? await uploadAllDomains(profileId, cookies, apiKey)
261
319
  : await profileUpload(profileId, cookies, apiKey);
320
+ const result = { ...uploadResult, profile_id: profileId };
262
321
  if (result.domains_failed && result.domains_failed.length > 0) {
263
322
  errLine(`Warning: failed to update cookies for domains: ${result.domains_failed.join(', ')}`);
264
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.46.1-next.362",
3
+ "version": "0.46.1-next.363",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {