@tiny-fish/cli 0.47.2-next.375 → 0.47.2-next.377
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 +86 -30
- package/dist/lib/auth.d.ts +3 -0
- package/dist/lib/auth.js +15 -2
- package/dist/lib/client.d.ts +3 -1
- package/dist/lib/client.js +15 -0
- package/dist/lib/output.d.ts +1 -1
- package/dist/lib/output.js +3 -2
- package/dist/lib/types.d.ts +4 -0
- package/package.json +1 -1
package/dist/commands/profile.js
CHANGED
|
@@ -3,9 +3,9 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import {
|
|
6
|
+
import { getResolvedApiKey, rejectedKeyHint } from '../lib/auth.js';
|
|
7
7
|
import { deriveChromeKey, listAuthRelevantHostKeys, openCookiesDb, readCookiesForHost, } from '../lib/chrome-cookies.js';
|
|
8
|
-
import { profileCreate, profileUpload } from '../lib/client.js';
|
|
8
|
+
import { profileCreate, profileGet, profileList, profileUpload } from '../lib/client.js';
|
|
9
9
|
import { err, errLine, handleApiError, out, outLine } from '../lib/output.js';
|
|
10
10
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
11
11
|
const chromeLocalStateSchema = z.object({
|
|
@@ -19,7 +19,7 @@ function getChromeRoot() {
|
|
|
19
19
|
if (platform === 'linux') {
|
|
20
20
|
return path.join(os.homedir(), '.config', 'google-chrome');
|
|
21
21
|
}
|
|
22
|
-
// Windows guard — handled in
|
|
22
|
+
// Windows guard — handled in resolveCookiesPath
|
|
23
23
|
return '';
|
|
24
24
|
}
|
|
25
25
|
function sanitizeChromeProfile(profile) {
|
|
@@ -28,14 +28,53 @@ function sanitizeChromeProfile(profile) {
|
|
|
28
28
|
}
|
|
29
29
|
return profile;
|
|
30
30
|
}
|
|
31
|
+
class ChromePathOutsideError extends Error {
|
|
32
|
+
constructor() {
|
|
33
|
+
super('Chrome data path resolves outside the Chrome data directory');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
31
36
|
function assertChromePathContained(targetPath) {
|
|
32
37
|
const chromeRoot = fs.realpathSync(getChromeRoot());
|
|
33
38
|
const resolvedTarget = fs.realpathSync(targetPath);
|
|
34
39
|
const relative = path.relative(chromeRoot, resolvedTarget);
|
|
35
40
|
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
36
|
-
throw new
|
|
41
|
+
throw new ChromePathOutsideError();
|
|
37
42
|
}
|
|
38
43
|
}
|
|
44
|
+
function isContainedCookiesFile(cookiesPath) {
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.lstatSync(cookiesPath).isFile())
|
|
47
|
+
return false;
|
|
48
|
+
assertChromePathContained(cookiesPath);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function listChromeProfilesWithCookies(chromeRoot) {
|
|
56
|
+
try {
|
|
57
|
+
return (fs
|
|
58
|
+
.readdirSync(chromeRoot, { withFileTypes: true })
|
|
59
|
+
.filter((entry) => entry.isDirectory())
|
|
60
|
+
.map((entry) => entry.name)
|
|
61
|
+
// nosemgrep: path-join-resolve-traversal -- readdir names, result containment-checked
|
|
62
|
+
.filter((name) => isContainedCookiesFile(path.join(chromeRoot, name, 'Cookies')))
|
|
63
|
+
.sort());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function availableProfilesHint(chromeRoot) {
|
|
70
|
+
const names = listChromeProfilesWithCookies(chromeRoot);
|
|
71
|
+
return names.length > 0
|
|
72
|
+
? `Available Chrome profiles: ${names.join(', ')}.`
|
|
73
|
+
: 'No Chrome profiles with a Cookies file were found.';
|
|
74
|
+
}
|
|
75
|
+
function chromeProfileError(chromeRoot, reason) {
|
|
76
|
+
return new Error(`${reason} Use --chrome-profile to select a profile. ${availableProfilesHint(chromeRoot)}`);
|
|
77
|
+
}
|
|
39
78
|
function getChromeCookiesPath(requestedProfile) {
|
|
40
79
|
const chromeRoot = getChromeRoot();
|
|
41
80
|
if (requestedProfile !== undefined) {
|
|
@@ -49,26 +88,28 @@ function getChromeCookiesPath(requestedProfile) {
|
|
|
49
88
|
if (error.code === 'ENOENT') {
|
|
50
89
|
return path.join(chromeRoot, 'Default', 'Cookies');
|
|
51
90
|
}
|
|
52
|
-
throw
|
|
91
|
+
throw chromeProfileError(chromeRoot, 'Could not read Chrome profile state.');
|
|
53
92
|
}
|
|
93
|
+
let lastUsed;
|
|
54
94
|
try {
|
|
55
95
|
assertChromePathContained(localStatePath);
|
|
96
|
+
lastUsed = chromeLocalStateSchema.parse(JSON.parse(fs.readFileSync(localStatePath, 'utf8')))
|
|
97
|
+
.profile?.last_used;
|
|
56
98
|
}
|
|
57
99
|
catch (error) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw
|
|
100
|
+
const reason = error instanceof ChromePathOutsideError
|
|
101
|
+
? `${error.message}.`
|
|
102
|
+
: 'Could not read Chrome profile state.';
|
|
103
|
+
throw chromeProfileError(chromeRoot, reason);
|
|
62
104
|
}
|
|
105
|
+
// Chrome writes last_used only after a profile switch.
|
|
106
|
+
if (!lastUsed)
|
|
107
|
+
return path.join(chromeRoot, 'Default', 'Cookies');
|
|
63
108
|
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
109
|
return path.join(chromeRoot, sanitizeChromeProfile(lastUsed), 'Cookies');
|
|
69
110
|
}
|
|
70
111
|
catch {
|
|
71
|
-
throw
|
|
112
|
+
throw chromeProfileError(chromeRoot, 'Chrome profile state names an invalid profile.');
|
|
72
113
|
}
|
|
73
114
|
}
|
|
74
115
|
function validateDomain(domain) {
|
|
@@ -77,16 +118,22 @@ function validateDomain(domain) {
|
|
|
77
118
|
process.exit(1);
|
|
78
119
|
}
|
|
79
120
|
}
|
|
80
|
-
//
|
|
81
|
-
function
|
|
121
|
+
// Local checks only; must not touch the keychain or read cookies.
|
|
122
|
+
function resolveCookiesPath(chromeProfile) {
|
|
82
123
|
if (process.platform === 'win32') {
|
|
83
124
|
throw new Error('Cookie extraction is not supported on Windows yet');
|
|
84
125
|
}
|
|
85
126
|
const cookiesPath = getChromeCookiesPath(chromeProfile);
|
|
86
127
|
if (!fs.existsSync(cookiesPath)) {
|
|
87
|
-
throw
|
|
128
|
+
throw chromeProfileError(getChromeRoot(), `Chrome cookies file not found at ${cookiesPath}.`);
|
|
88
129
|
}
|
|
89
130
|
assertChromePathContained(cookiesPath);
|
|
131
|
+
return cookiesPath;
|
|
132
|
+
}
|
|
133
|
+
// Opens a private copy of Chrome's cookie DB, derives the decryption key, and calls fn.
|
|
134
|
+
function withCookiesDb(cookiesPath, fn) {
|
|
135
|
+
// Recheck: the preflight await lets the path be swapped.
|
|
136
|
+
assertChromePathContained(cookiesPath);
|
|
90
137
|
// Copy the DB out of the profile directory so we avoid the Chrome file lock.
|
|
91
138
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-chrome-cookies-'));
|
|
92
139
|
let db;
|
|
@@ -124,11 +171,11 @@ function withCookiesDb(chromeProfile, fn) {
|
|
|
124
171
|
}
|
|
125
172
|
}
|
|
126
173
|
}
|
|
127
|
-
function extractChromeCookies(domain,
|
|
128
|
-
return withCookiesDb(
|
|
174
|
+
function extractChromeCookies(domain, cookiesPath, stats) {
|
|
175
|
+
return withCookiesDb(cookiesPath, (db, key) => readCookiesForHost(db, key, domain, stats));
|
|
129
176
|
}
|
|
130
|
-
function extractAllChromeCookies(
|
|
131
|
-
return withCookiesDb(
|
|
177
|
+
function extractAllChromeCookies(cookiesPath, stats) {
|
|
178
|
+
return withCookiesDb(cookiesPath, (db, key) => {
|
|
132
179
|
let hostKeys;
|
|
133
180
|
try {
|
|
134
181
|
hostKeys = listAuthRelevantHostKeys(db);
|
|
@@ -214,13 +261,13 @@ function printPrettyProfileUpload(result) {
|
|
|
214
261
|
outLine(`Cookies uploaded: ${result.cookie_count}`);
|
|
215
262
|
outLine(`Domains updated: ${result.domains_updated.join(', ')}`);
|
|
216
263
|
}
|
|
217
|
-
function extractCookiesOrThrow(opts) {
|
|
264
|
+
function extractCookiesOrThrow(opts, cookiesPath) {
|
|
218
265
|
// Extract cookies before creating profile to avoid orphan profiles
|
|
219
266
|
// if extraction fails (Windows, no Chrome, no cookies).
|
|
220
267
|
const stats = { keyringSkipped: 0 };
|
|
221
268
|
const cookies = opts.allDomains
|
|
222
|
-
? extractAllChromeCookies(
|
|
223
|
-
: extractChromeCookies(opts.domain,
|
|
269
|
+
? extractAllChromeCookies(cookiesPath, stats)
|
|
270
|
+
: extractChromeCookies(opts.domain, cookiesPath, stats);
|
|
224
271
|
// Warn even on partial success — surviving plaintext rows can mask
|
|
225
272
|
// that every auth cookie was keyring-encrypted and skipped.
|
|
226
273
|
if (stats.keyringSkipped > 0) {
|
|
@@ -235,6 +282,13 @@ function extractCookiesOrThrow(opts) {
|
|
|
235
282
|
}
|
|
236
283
|
return cookies;
|
|
237
284
|
}
|
|
285
|
+
// Fails a bad key or profile before the keychain prompt.
|
|
286
|
+
async function verifyImportTarget(profileId, apiKey) {
|
|
287
|
+
if (profileId === undefined)
|
|
288
|
+
await profileList(apiKey);
|
|
289
|
+
else
|
|
290
|
+
await profileGet(profileId, apiKey);
|
|
291
|
+
}
|
|
238
292
|
// Exported for testing
|
|
239
293
|
export async function uploadAllDomains(profileId, cookies, apiKey) {
|
|
240
294
|
// Batch upload — never split a canonical domain across chunks
|
|
@@ -270,7 +324,7 @@ export function registerProfile(program) {
|
|
|
270
324
|
.requiredOption('--name <name>', 'Profile name')
|
|
271
325
|
.option('--pretty', 'Human-readable output')
|
|
272
326
|
.action(async (opts) => {
|
|
273
|
-
const apiKey =
|
|
327
|
+
const { key: apiKey, source } = getResolvedApiKey();
|
|
274
328
|
try {
|
|
275
329
|
const response = await profileCreate(opts.name, apiKey);
|
|
276
330
|
if (opts.pretty) {
|
|
@@ -281,7 +335,7 @@ export function registerProfile(program) {
|
|
|
281
335
|
}
|
|
282
336
|
}
|
|
283
337
|
catch (error) {
|
|
284
|
-
handleApiError(error);
|
|
338
|
+
handleApiError(error, rejectedKeyHint(source));
|
|
285
339
|
}
|
|
286
340
|
});
|
|
287
341
|
profileCmd
|
|
@@ -289,7 +343,7 @@ export function registerProfile(program) {
|
|
|
289
343
|
.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.')
|
|
290
344
|
.option('--domain <domain>', 'Domain to import cookies for (e.g. github.com)')
|
|
291
345
|
.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 (
|
|
346
|
+
.option('--chrome-profile <name>', 'Chrome profile directory, e.g. "Profile 1" (default: last-used profile, else Default)')
|
|
293
347
|
.option('--profile-id <id>', 'Profile ID to import cookies into (creates new profile if omitted)')
|
|
294
348
|
.option('--pretty', 'Human-readable output')
|
|
295
349
|
.action(async (opts) => {
|
|
@@ -302,13 +356,15 @@ export function registerProfile(program) {
|
|
|
302
356
|
err({ error: 'Provide exactly one of --domain or --all-domains' });
|
|
303
357
|
process.exit(1);
|
|
304
358
|
}
|
|
305
|
-
const apiKey =
|
|
359
|
+
const { key: apiKey, source } = getResolvedApiKey();
|
|
306
360
|
if (opts.domain) {
|
|
307
361
|
validateDomain(opts.domain);
|
|
308
362
|
}
|
|
309
363
|
let profileId = opts.profileId;
|
|
310
364
|
try {
|
|
311
|
-
const
|
|
365
|
+
const cookiesPath = resolveCookiesPath(opts.chromeProfile);
|
|
366
|
+
await verifyImportTarget(profileId, apiKey);
|
|
367
|
+
const cookies = extractCookiesOrThrow(opts, cookiesPath);
|
|
312
368
|
if (profileId === undefined) {
|
|
313
369
|
const name = opts.allDomains ? 'chrome-import' : opts.domain;
|
|
314
370
|
const created = await profileCreate(name, apiKey);
|
|
@@ -329,7 +385,7 @@ export function registerProfile(program) {
|
|
|
329
385
|
}
|
|
330
386
|
}
|
|
331
387
|
catch (error) {
|
|
332
|
-
handleApiError(error);
|
|
388
|
+
handleApiError(error, rejectedKeyHint(source));
|
|
333
389
|
}
|
|
334
390
|
});
|
|
335
391
|
}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -70,4 +70,7 @@ export declare function validatedApiKey(explicitApiKey?: string): string | undef
|
|
|
70
70
|
* rather than exit like getApiKey does.
|
|
71
71
|
*/
|
|
72
72
|
export declare function apiKeyStatus(explicitApiKey?: string): ResolvedKey | UnresolvedKey;
|
|
73
|
+
export declare function getResolvedApiKey(): ResolvedKey;
|
|
73
74
|
export declare function getApiKey(): string;
|
|
75
|
+
/** 401 repair hint that names where the rejected key came from. */
|
|
76
|
+
export declare function rejectedKeyHint(source: ResolvedKey['source']): string | undefined;
|
package/dist/lib/auth.js
CHANGED
|
@@ -319,10 +319,23 @@ export function apiKeyStatus(explicitApiKey) {
|
|
|
319
319
|
}
|
|
320
320
|
return { key: value, source };
|
|
321
321
|
}
|
|
322
|
-
export function
|
|
322
|
+
export function getResolvedApiKey() {
|
|
323
323
|
const resolved = apiKeyStatus();
|
|
324
324
|
if ('key' in resolved)
|
|
325
|
-
return resolved
|
|
325
|
+
return resolved;
|
|
326
326
|
err({ error: resolved.error });
|
|
327
327
|
process.exit(1);
|
|
328
328
|
}
|
|
329
|
+
export function getApiKey() {
|
|
330
|
+
return getResolvedApiKey().key;
|
|
331
|
+
}
|
|
332
|
+
/** 401 repair hint that names where the rejected key came from. */
|
|
333
|
+
export function rejectedKeyHint(source) {
|
|
334
|
+
if (source === 'env') {
|
|
335
|
+
return `The rejected key came from the ${TINYFISH_API_KEY_VAR} environment variable, which overrides the key saved by \`tinyfish auth login\`. Unset it (check your shell profile) or set it to a valid key, then try again.`;
|
|
336
|
+
}
|
|
337
|
+
if (source === 'config') {
|
|
338
|
+
return 'The rejected key is the one saved by `tinyfish auth login`. Run `tinyfish auth login` to save a new key, then try again.';
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type BrowserSession, type FetchGetContentsParams, type FetchResponse, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse, type WalletResponse } from '@tiny-fish/sdk';
|
|
2
|
-
import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from './types.js';
|
|
2
|
+
import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileSummary, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from './types.js';
|
|
3
3
|
type ConnectMap = Record<string, {
|
|
4
4
|
attempt_id: string;
|
|
5
5
|
}> | undefined;
|
|
@@ -21,6 +21,8 @@ export declare function submitBatch(req: BatchRunRequest, apiKey: string): Promi
|
|
|
21
21
|
export declare function getBatchRuns(runIds: string[], apiKey: string): Promise<BatchGetResponse>;
|
|
22
22
|
export declare function cancelBatchRuns(runIds: string[], apiKey: string): Promise<BatchCancelResponse>;
|
|
23
23
|
export declare function profileCreate(name: string, apiKey: string): Promise<ProfileCreateResponse>;
|
|
24
|
+
export declare function profileGet(profileId: string, apiKey: string): Promise<ProfileSummary>;
|
|
25
|
+
export declare function profileList(apiKey: string): Promise<ProfileSummary[]>;
|
|
24
26
|
export declare function profileUpload(profileId: string, cookies: CookieRecord[], apiKey: string): Promise<ProfileUploadResponse>;
|
|
25
27
|
export declare function vaultConnections(apiKey: string): Promise<VaultConnectionsListResponse>;
|
|
26
28
|
export declare function vaultConnect(req: VaultConnectRequest, apiKey: string): Promise<VaultConnectResponse>;
|
package/dist/lib/client.js
CHANGED
|
@@ -372,6 +372,8 @@ const profileCreateResponseSchema = z.object({
|
|
|
372
372
|
name: z.string(),
|
|
373
373
|
created_at: z.string(),
|
|
374
374
|
});
|
|
375
|
+
const profileSummarySchema = z.object({ id: z.string(), name: z.string() });
|
|
376
|
+
const profileListResponseSchema = z.object({ profiles: z.array(profileSummarySchema) });
|
|
375
377
|
const profileUploadResponseSchema = z.object({
|
|
376
378
|
profile_id: z.string().optional(),
|
|
377
379
|
cookie_count: z.number(),
|
|
@@ -386,6 +388,19 @@ export function profileCreate(name, apiKey) {
|
|
|
386
388
|
return parseWithSchema(profileCreateResponseSchema, response, 'Invalid profile create response');
|
|
387
389
|
});
|
|
388
390
|
}
|
|
391
|
+
export function profileGet(profileId, apiKey) {
|
|
392
|
+
return sdk(apiKey, async (client) => {
|
|
393
|
+
const response = await client.get(`/v1/profiles/${encodeURIComponent(profileId)}`);
|
|
394
|
+
return parseWithSchema(profileSummarySchema, response, 'Invalid profile response');
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
export function profileList(apiKey) {
|
|
398
|
+
return sdk(apiKey, async (client) => {
|
|
399
|
+
const response = await client.get('/v1/profiles');
|
|
400
|
+
return parseWithSchema(profileListResponseSchema, response, 'Invalid profile list response')
|
|
401
|
+
.profiles;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
389
404
|
export function profileUpload(profileId, cookies, apiKey) {
|
|
390
405
|
return sdk(apiKey, async (client) => {
|
|
391
406
|
const response = await client.post(`/v1/profiles/${encodeURIComponent(profileId)}/upload`, { json: { cookies } });
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ export declare function errLine(line: string): void;
|
|
|
15
15
|
/** Advisory stderr line, yellow on a terminal and plain everywhere else so piped output stays clean. */
|
|
16
16
|
export declare function warnLine(line: string): void;
|
|
17
17
|
export declare function err(data: unknown): void;
|
|
18
|
-
export declare function handleApiError(e: unknown): never;
|
|
18
|
+
export declare function handleApiError(e: unknown, unauthorizedHint?: string): never;
|
|
19
19
|
/** Undefined, not a throw: callers hand the value to a Zod safeParse. */
|
|
20
20
|
export declare function parseJson(text: string): unknown;
|
package/dist/lib/output.js
CHANGED
|
@@ -53,7 +53,7 @@ export function err(data) {
|
|
|
53
53
|
const serialized = JSON.stringify(payload) ?? 'undefined';
|
|
54
54
|
process.stderr.write(sanitizeLine(serialized) + '\n');
|
|
55
55
|
}
|
|
56
|
-
export function handleApiError(e) {
|
|
56
|
+
export function handleApiError(e, unauthorizedHint) {
|
|
57
57
|
if (e instanceof ApiError) {
|
|
58
58
|
const payload = { error: e.message, status: e.status };
|
|
59
59
|
if (e.code)
|
|
@@ -64,7 +64,8 @@ export function handleApiError(e) {
|
|
|
64
64
|
}
|
|
65
65
|
else if (e.status === 401) {
|
|
66
66
|
payload.hint =
|
|
67
|
-
|
|
67
|
+
unauthorizedHint ??
|
|
68
|
+
'Clear the TINYFISH_API_KEY environment variable, run `tinyfish auth login`, then try again.';
|
|
68
69
|
}
|
|
69
70
|
err(payload);
|
|
70
71
|
}
|
package/dist/lib/types.d.ts
CHANGED