@tiny-fish/cli 0.46.1-next.362 → 0.46.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/dist/commands/profile.js +74 -15
- package/package.json +1 -1
package/dist/commands/profile.js
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
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'
|
|
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}.
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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
|
}
|