From e8e98e641ab284824b8778bf949de533e02a2d78 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 26 Dec 2025 21:28:27 +0100 Subject: [PATCH] refactor(auth)!: replace allow toggles with cookieSource --- CHANGELOG.md | 5 ++++- README.md | 10 ++++----- src/index.ts | 52 +++++++++++++++++++++++++++++++------------ src/lib/cookies.ts | 35 +++++++---------------------- tests/cookies.test.ts | 27 ++++++++-------------- 5 files changed, 63 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5deb08..125470f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,10 @@ ## 0.3.1 — Unreleased ### Added -- Cookie source selection: `--cookie-source auto|safari|chrome|firefox` + `cookieSource` config option. +- Cookie source selection: `--cookie-source safari|chrome|firefox` (repeatable) + `cookieSource` config (string or array). + +### Breaking +- Remove `allowSafari`/`allowChrome`/`allowFirefox` config toggles in favor of `cookieSource` ordering. ## 0.3.0 — 2025-12-26 diff --git a/README.md b/README.md index b5d8d7f..bea4d77 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Global options: - `--plain`: stable output (no emoji, no color). - `--no-emoji`: disable emoji output. - `--no-color`: disable ANSI colors (or set `NO_COLOR=1`). -- `--cookie-source `: choose which browser cookies to use (default `auto`). +- `--cookie-source `: choose browser cookie source (repeatable; order matters). ## Authentication (GraphQL) @@ -73,7 +73,7 @@ X endpoints and authenticates via cookies (`auth_token`, `ct0`). 1. CLI flags: `--auth-token`, `--ct0` 2. Environment variables: `AUTH_TOKEN`, `CT0` (fallback: `TWITTER_AUTH_TOKEN`, `TWITTER_CT0`) -3. Browser cookies (macOS): Safari, Chrome, Firefox (override via `--cookie-source`) +3. Browser cookies (macOS): Safari, Chrome, Firefox (override via `--cookie-source` order) Browser cookie sources: - Safari: `~/Library/Cookies/Cookies.binarycookies` (fallback: `~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies`) @@ -91,11 +91,9 @@ Example `~/.config/bird/config.json5`: ```json5 { - cookieSource: "auto", // or: "firefox" / "chrome" / "safari" + // Cookie source order for browser extraction (string or array) + cookieSource: ["firefox", "safari"], firefoxProfile: "default-release", - allowSafari: true, - allowFirefox: true, - allowChrome: false, timeoutMs: 20000 } ``` diff --git a/src/index.ts b/src/index.ts index e8f24a8..bee7a3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { existsSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; -import { Command, Option } from 'commander'; +import { Command } from 'commander'; import JSON5 from 'json5'; import kleur from 'kleur'; import { resolveCliInvocation } from './lib/cli-args.js'; @@ -45,6 +45,32 @@ const collect = (value: string, previous: string[] = []) => { return previous; }; +const COOKIE_SOURCES: CookieSource[] = ['safari', 'chrome', 'firefox']; + +function parseCookieSource(value: string): CookieSource { + const normalized = value.trim().toLowerCase(); + if (normalized === 'safari' || normalized === 'chrome' || normalized === 'firefox') return normalized; + throw new Error(`Invalid --cookie-source "${value}". Allowed: safari, chrome, firefox.`); +} + +function resolveCookieSourceOrder(input: unknown): CookieSource[] | undefined { + if (typeof input === 'string') return [parseCookieSource(input)]; + if (Array.isArray(input)) { + const result: CookieSource[] = []; + for (const entry of input) { + if (typeof entry !== 'string') continue; + result.push(parseCookieSource(entry)); + } + return result.length > 0 ? result : undefined; + } + return undefined; +} + +const collectCookieSource = (value: string, previous: CookieSource[] = []) => { + previous.push(parseCookieSource(value)); + return previous; +}; + const p = (kind: Parameters[0]) => statusPrefix(kind, output); const l = (kind: Parameters[0]) => labelPrefix(kind, output); @@ -70,10 +96,7 @@ const colors = { type BirdConfig = { chromeProfile?: string; firefoxProfile?: string; - cookieSource?: CookieSource; - allowSafari?: boolean; - allowChrome?: boolean; - allowFirefox?: boolean; + cookieSource?: CookieSource | CookieSource[]; timeoutMs?: number; }; @@ -146,10 +169,11 @@ program .option('--ct0 ', 'Twitter ct0 cookie') .option('--chrome-profile ', 'Chrome profile name for cookie extraction', config.chromeProfile) .option('--firefox-profile ', 'Firefox profile name for cookie extraction', config.firefoxProfile) - .addOption( - new Option('--cookie-source ', 'Cookie source for browser cookie extraction') - .choices(['auto', 'safari', 'chrome', 'firefox']) - .default(config.cookieSource ?? 'auto'), + .option( + '--cookie-source ', + 'Cookie source for browser cookie extraction (repeatable)', + collectCookieSource, + [], ) .option('--media ', 'Attach media file (repeatable, up to 4 images or 1 video)', collect, []) .option('--alt ', 'Alt text for the corresponding --media (repeatable)', collect, []) @@ -163,19 +187,19 @@ type CredentialsOptions = { ct0?: string; chromeProfile?: string; firefoxProfile?: string; - cookieSource?: CookieSource; + cookieSource?: CookieSource[]; }; function resolveCredentialsFromOptions(opts: CredentialsOptions) { + const cookieSource = opts.cookieSource?.length + ? opts.cookieSource + : (resolveCookieSourceOrder(config.cookieSource) ?? COOKIE_SOURCES); return resolveCredentials({ authToken: opts.authToken, ct0: opts.ct0, - cookieSource: opts.cookieSource ?? config.cookieSource ?? 'auto', + cookieSource, chromeProfile: opts.chromeProfile || config.chromeProfile, firefoxProfile: opts.firefoxProfile || config.firefoxProfile, - allowSafari: config.allowSafari ?? true, - allowChrome: config.allowChrome ?? true, - allowFirefox: config.allowFirefox ?? true, }); } diff --git a/src/lib/cookies.ts b/src/lib/cookies.ts index 2d9170d..3274c39 100644 --- a/src/lib/cookies.ts +++ b/src/lib/cookies.ts @@ -20,7 +20,7 @@ export interface CookieExtractionResult { warnings: string[]; } -export type CookieSource = 'auto' | 'safari' | 'chrome' | 'firefox'; +export type CookieSource = 'safari' | 'chrome' | 'firefox'; function normalizeValue(value: unknown): string | null { if (typeof value === 'string') { @@ -446,12 +446,9 @@ export async function extractCookiesFromFirefox(profile?: string): Promise { const warnings: string[] = []; const cookies: TwitterCookies = { @@ -460,7 +457,7 @@ export async function resolveCredentials(options: { source: null, }; - const cookieSource: CookieSource = options.cookieSource ?? 'auto'; + const cookieSource = options.cookieSource; // 1. CLI arguments (highest priority) if (options.authToken) { @@ -498,22 +495,16 @@ export async function resolveCredentials(options: { } } - const allowSafari = options.allowSafari ?? true; - const allowChrome = options.allowChrome ?? true; - const allowFirefox = options.allowFirefox ?? true; - - const sourcesToTry: Array> = - cookieSource === 'auto' ? ['safari', 'chrome', 'firefox'] : [cookieSource]; + const sourcesToTry: CookieSource[] = Array.isArray(cookieSource) + ? cookieSource + : cookieSource + ? [cookieSource] + : ['safari', 'chrome', 'firefox']; for (const source of sourcesToTry) { if (cookies.authToken && cookies.ct0) break; if (source === 'safari') { - if (!allowSafari) { - warnings.push('Safari cookie extraction disabled (allowSafari=false).'); - continue; - } - const safariResult = await extractCookiesFromSafari(); warnings.push(...safariResult.warnings); @@ -529,11 +520,6 @@ export async function resolveCredentials(options: { } if (source === 'chrome') { - if (!allowChrome) { - warnings.push('Chrome cookie extraction disabled (allowChrome=false).'); - continue; - } - const chromeResult = await extractCookiesFromChrome(options.chromeProfile); warnings.push(...chromeResult.warnings); @@ -549,11 +535,6 @@ export async function resolveCredentials(options: { } if (source === 'firefox') { - if (!allowFirefox) { - warnings.push('Firefox cookie extraction disabled (allowFirefox=false).'); - continue; - } - const firefoxResult = await extractCookiesFromFirefox(options.firefoxProfile); warnings.push(...firefoxResult.warnings); diff --git a/tests/cookies.test.ts b/tests/cookies.test.ts index 9bf70c8..9637762 100644 --- a/tests/cookies.test.ts +++ b/tests/cookies.test.ts @@ -135,7 +135,7 @@ describe('cookies', () => { return ''; }); - const result = await resolveCredentials({ cookieSource: 'firefox', allowChrome: false }); + const result = await resolveCredentials({ cookieSource: 'firefox' }); expect(result.cookies.authToken).toBe('firefox_auth'); expect(result.cookies.ct0).toBe('firefox_ct0'); expect(result.cookies.source).toContain('Firefox'); @@ -157,7 +157,7 @@ describe('cookies', () => { ]), ); - const result = await resolveCredentials({ cookieSource: 'safari', allowChrome: false, allowFirefox: false }); + const result = await resolveCredentials({ cookieSource: 'safari' }); expect(result.cookies.authToken).toBe('safari_auth'); expect(result.cookies.ct0).toBe('safari_ct0'); expect(result.cookies.source).toBe('Safari'); @@ -184,11 +184,7 @@ describe('cookies', () => { const { execSync } = await import('node:child_process'); (execSync as unknown as vi.Mock).mockReturnValue('auth_token|firefox_auth\nct0|firefox_ct0'); - const result = await resolveCredentials({ - allowFirefox: true, - allowChrome: false, - firefoxProfile: 'abc.default-release', - }); + const result = await resolveCredentials({ cookieSource: 'firefox', firefoxProfile: 'abc.default-release' }); expect(result.cookies.authToken).toBe('firefox_auth'); expect(result.cookies.ct0).toBe('firefox_ct0'); @@ -215,7 +211,7 @@ describe('cookies', () => { process.env.CT0 = 'test_ct0'; const { resolveCredentials } = await import('../src/lib/cookies.js'); - const result = await resolveCredentials({ allowFirefox: false, allowChrome: false }); + const result = await resolveCredentials({ cookieSource: 'safari' }); expect(result.cookies.authToken).toBe('test_auth_token'); expect(result.cookies.ct0).toBe('test_ct0'); @@ -227,7 +223,7 @@ describe('cookies', () => { process.env.TWITTER_CT0 = 'twitter_ct0'; const { resolveCredentials } = await import('../src/lib/cookies.js'); - const result = await resolveCredentials({ allowFirefox: false, allowChrome: false }); + const result = await resolveCredentials({ cookieSource: 'safari' }); expect(result.cookies.authToken).toBe('twitter_auth'); expect(result.cookies.ct0).toBe('twitter_ct0'); @@ -249,7 +245,7 @@ describe('cookies', () => { process.env.CT0 = ''; const { resolveCredentials } = await import('../src/lib/cookies.js'); - const result = await resolveCredentials({ allowFirefox: false, allowChrome: false }); + const result = await resolveCredentials({ cookieSource: 'safari' }); expect(result.cookies.authToken).toBeNull(); expect(result.cookies.ct0).toBeNull(); @@ -258,7 +254,7 @@ describe('cookies', () => { it('should warn when credentials are missing', async () => { const { resolveCredentials } = await import('../src/lib/cookies.js'); - const result = await resolveCredentials({ allowFirefox: false, allowChrome: false }); + const result = await resolveCredentials({ cookieSource: 'safari' }); expect(result.warnings).toContain( 'Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox', @@ -281,12 +277,7 @@ describe('cookies', () => { }); const { resolveCredentials } = await import('../src/lib/cookies.js'); - const result = await resolveCredentials({ - allowSafari: false, - allowFirefox: false, - allowChrome: true, - chromeProfile: 'Default', - }); + const result = await resolveCredentials({ cookieSource: 'chrome', chromeProfile: 'Default' }); expect(result.cookies.authToken).toBe('test_auth'); expect(result.cookies.ct0).toBe('test_ct0'); @@ -336,7 +327,7 @@ describe('cookies', () => { ); const { resolveCredentials } = await import('../src/lib/cookies.js'); - const result = await resolveCredentials({ allowSafari: true, allowChrome: true, allowFirefox: false }); + const result = await resolveCredentials({ cookieSource: ['safari', 'chrome'] }); expect(result.cookies.authToken).toBe('safari_auth'); expect(result.cookies.ct0).toBe('safari_ct0');