refactor(auth)!: replace allow toggles with cookieSource
This commit is contained in:
+4
-1
@@ -3,7 +3,10 @@
|
|||||||
## 0.3.1 — Unreleased
|
## 0.3.1 — Unreleased
|
||||||
|
|
||||||
### Added
|
### 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
|
## 0.3.0 — 2025-12-26
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Global options:
|
|||||||
- `--plain`: stable output (no emoji, no color).
|
- `--plain`: stable output (no emoji, no color).
|
||||||
- `--no-emoji`: disable emoji output.
|
- `--no-emoji`: disable emoji output.
|
||||||
- `--no-color`: disable ANSI colors (or set `NO_COLOR=1`).
|
- `--no-color`: disable ANSI colors (or set `NO_COLOR=1`).
|
||||||
- `--cookie-source <auto|safari|chrome|firefox>`: choose which browser cookies to use (default `auto`).
|
- `--cookie-source <safari|chrome|firefox>`: choose browser cookie source (repeatable; order matters).
|
||||||
|
|
||||||
## Authentication (GraphQL)
|
## Authentication (GraphQL)
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ X endpoints and authenticates via cookies (`auth_token`, `ct0`).
|
|||||||
|
|
||||||
1. CLI flags: `--auth-token`, `--ct0`
|
1. CLI flags: `--auth-token`, `--ct0`
|
||||||
2. Environment variables: `AUTH_TOKEN`, `CT0` (fallback: `TWITTER_AUTH_TOKEN`, `TWITTER_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:
|
Browser cookie sources:
|
||||||
- Safari: `~/Library/Cookies/Cookies.binarycookies` (fallback: `~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies`)
|
- 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
|
```json5
|
||||||
{
|
{
|
||||||
cookieSource: "auto", // or: "firefox" / "chrome" / "safari"
|
// Cookie source order for browser extraction (string or array)
|
||||||
|
cookieSource: ["firefox", "safari"],
|
||||||
firefoxProfile: "default-release",
|
firefoxProfile: "default-release",
|
||||||
allowSafari: true,
|
|
||||||
allowFirefox: true,
|
|
||||||
allowChrome: false,
|
|
||||||
timeoutMs: 20000
|
timeoutMs: 20000
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
+38
-14
@@ -12,7 +12,7 @@
|
|||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { Command, Option } from 'commander';
|
import { Command } from 'commander';
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
import kleur from 'kleur';
|
import kleur from 'kleur';
|
||||||
import { resolveCliInvocation } from './lib/cli-args.js';
|
import { resolveCliInvocation } from './lib/cli-args.js';
|
||||||
@@ -45,6 +45,32 @@ const collect = (value: string, previous: string[] = []) => {
|
|||||||
return previous;
|
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<typeof statusPrefix>[0]) => statusPrefix(kind, output);
|
const p = (kind: Parameters<typeof statusPrefix>[0]) => statusPrefix(kind, output);
|
||||||
const l = (kind: Parameters<typeof labelPrefix>[0]) => labelPrefix(kind, output);
|
const l = (kind: Parameters<typeof labelPrefix>[0]) => labelPrefix(kind, output);
|
||||||
|
|
||||||
@@ -70,10 +96,7 @@ const colors = {
|
|||||||
type BirdConfig = {
|
type BirdConfig = {
|
||||||
chromeProfile?: string;
|
chromeProfile?: string;
|
||||||
firefoxProfile?: string;
|
firefoxProfile?: string;
|
||||||
cookieSource?: CookieSource;
|
cookieSource?: CookieSource | CookieSource[];
|
||||||
allowSafari?: boolean;
|
|
||||||
allowChrome?: boolean;
|
|
||||||
allowFirefox?: boolean;
|
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,10 +169,11 @@ program
|
|||||||
.option('--ct0 <token>', 'Twitter ct0 cookie')
|
.option('--ct0 <token>', 'Twitter ct0 cookie')
|
||||||
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction', config.chromeProfile)
|
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction', config.chromeProfile)
|
||||||
.option('--firefox-profile <name>', 'Firefox profile name for cookie extraction', config.firefoxProfile)
|
.option('--firefox-profile <name>', 'Firefox profile name for cookie extraction', config.firefoxProfile)
|
||||||
.addOption(
|
.option(
|
||||||
new Option('--cookie-source <source>', 'Cookie source for browser cookie extraction')
|
'--cookie-source <source>',
|
||||||
.choices(['auto', 'safari', 'chrome', 'firefox'])
|
'Cookie source for browser cookie extraction (repeatable)',
|
||||||
.default(config.cookieSource ?? 'auto'),
|
collectCookieSource,
|
||||||
|
[],
|
||||||
)
|
)
|
||||||
.option('--media <path>', 'Attach media file (repeatable, up to 4 images or 1 video)', collect, [])
|
.option('--media <path>', 'Attach media file (repeatable, up to 4 images or 1 video)', collect, [])
|
||||||
.option('--alt <text>', 'Alt text for the corresponding --media (repeatable)', collect, [])
|
.option('--alt <text>', 'Alt text for the corresponding --media (repeatable)', collect, [])
|
||||||
@@ -163,19 +187,19 @@ type CredentialsOptions = {
|
|||||||
ct0?: string;
|
ct0?: string;
|
||||||
chromeProfile?: string;
|
chromeProfile?: string;
|
||||||
firefoxProfile?: string;
|
firefoxProfile?: string;
|
||||||
cookieSource?: CookieSource;
|
cookieSource?: CookieSource[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveCredentialsFromOptions(opts: CredentialsOptions) {
|
function resolveCredentialsFromOptions(opts: CredentialsOptions) {
|
||||||
|
const cookieSource = opts.cookieSource?.length
|
||||||
|
? opts.cookieSource
|
||||||
|
: (resolveCookieSourceOrder(config.cookieSource) ?? COOKIE_SOURCES);
|
||||||
return resolveCredentials({
|
return resolveCredentials({
|
||||||
authToken: opts.authToken,
|
authToken: opts.authToken,
|
||||||
ct0: opts.ct0,
|
ct0: opts.ct0,
|
||||||
cookieSource: opts.cookieSource ?? config.cookieSource ?? 'auto',
|
cookieSource,
|
||||||
chromeProfile: opts.chromeProfile || config.chromeProfile,
|
chromeProfile: opts.chromeProfile || config.chromeProfile,
|
||||||
firefoxProfile: opts.firefoxProfile || config.firefoxProfile,
|
firefoxProfile: opts.firefoxProfile || config.firefoxProfile,
|
||||||
allowSafari: config.allowSafari ?? true,
|
|
||||||
allowChrome: config.allowChrome ?? true,
|
|
||||||
allowFirefox: config.allowFirefox ?? true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-27
@@ -20,7 +20,7 @@ export interface CookieExtractionResult {
|
|||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CookieSource = 'auto' | 'safari' | 'chrome' | 'firefox';
|
export type CookieSource = 'safari' | 'chrome' | 'firefox';
|
||||||
|
|
||||||
function normalizeValue(value: unknown): string | null {
|
function normalizeValue(value: unknown): string | null {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
@@ -446,12 +446,9 @@ export async function extractCookiesFromFirefox(profile?: string): Promise<Cooki
|
|||||||
export async function resolveCredentials(options: {
|
export async function resolveCredentials(options: {
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
ct0?: string;
|
ct0?: string;
|
||||||
cookieSource?: CookieSource;
|
cookieSource?: CookieSource | CookieSource[];
|
||||||
allowSafari?: boolean;
|
|
||||||
chromeProfile?: string;
|
chromeProfile?: string;
|
||||||
firefoxProfile?: string;
|
firefoxProfile?: string;
|
||||||
allowChrome?: boolean;
|
|
||||||
allowFirefox?: boolean;
|
|
||||||
}): Promise<CookieExtractionResult> {
|
}): Promise<CookieExtractionResult> {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const cookies: TwitterCookies = {
|
const cookies: TwitterCookies = {
|
||||||
@@ -460,7 +457,7 @@ export async function resolveCredentials(options: {
|
|||||||
source: null,
|
source: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookieSource: CookieSource = options.cookieSource ?? 'auto';
|
const cookieSource = options.cookieSource;
|
||||||
|
|
||||||
// 1. CLI arguments (highest priority)
|
// 1. CLI arguments (highest priority)
|
||||||
if (options.authToken) {
|
if (options.authToken) {
|
||||||
@@ -498,22 +495,16 @@ export async function resolveCredentials(options: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowSafari = options.allowSafari ?? true;
|
const sourcesToTry: CookieSource[] = Array.isArray(cookieSource)
|
||||||
const allowChrome = options.allowChrome ?? true;
|
? cookieSource
|
||||||
const allowFirefox = options.allowFirefox ?? true;
|
: cookieSource
|
||||||
|
? [cookieSource]
|
||||||
const sourcesToTry: Array<Exclude<CookieSource, 'auto'>> =
|
: ['safari', 'chrome', 'firefox'];
|
||||||
cookieSource === 'auto' ? ['safari', 'chrome', 'firefox'] : [cookieSource];
|
|
||||||
|
|
||||||
for (const source of sourcesToTry) {
|
for (const source of sourcesToTry) {
|
||||||
if (cookies.authToken && cookies.ct0) break;
|
if (cookies.authToken && cookies.ct0) break;
|
||||||
|
|
||||||
if (source === 'safari') {
|
if (source === 'safari') {
|
||||||
if (!allowSafari) {
|
|
||||||
warnings.push('Safari cookie extraction disabled (allowSafari=false).');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const safariResult = await extractCookiesFromSafari();
|
const safariResult = await extractCookiesFromSafari();
|
||||||
warnings.push(...safariResult.warnings);
|
warnings.push(...safariResult.warnings);
|
||||||
|
|
||||||
@@ -529,11 +520,6 @@ export async function resolveCredentials(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'chrome') {
|
if (source === 'chrome') {
|
||||||
if (!allowChrome) {
|
|
||||||
warnings.push('Chrome cookie extraction disabled (allowChrome=false).');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chromeResult = await extractCookiesFromChrome(options.chromeProfile);
|
const chromeResult = await extractCookiesFromChrome(options.chromeProfile);
|
||||||
warnings.push(...chromeResult.warnings);
|
warnings.push(...chromeResult.warnings);
|
||||||
|
|
||||||
@@ -549,11 +535,6 @@ export async function resolveCredentials(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'firefox') {
|
if (source === 'firefox') {
|
||||||
if (!allowFirefox) {
|
|
||||||
warnings.push('Firefox cookie extraction disabled (allowFirefox=false).');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firefoxResult = await extractCookiesFromFirefox(options.firefoxProfile);
|
const firefoxResult = await extractCookiesFromFirefox(options.firefoxProfile);
|
||||||
warnings.push(...firefoxResult.warnings);
|
warnings.push(...firefoxResult.warnings);
|
||||||
|
|
||||||
|
|||||||
+9
-18
@@ -135,7 +135,7 @@ describe('cookies', () => {
|
|||||||
return '';
|
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.authToken).toBe('firefox_auth');
|
||||||
expect(result.cookies.ct0).toBe('firefox_ct0');
|
expect(result.cookies.ct0).toBe('firefox_ct0');
|
||||||
expect(result.cookies.source).toContain('Firefox');
|
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.authToken).toBe('safari_auth');
|
||||||
expect(result.cookies.ct0).toBe('safari_ct0');
|
expect(result.cookies.ct0).toBe('safari_ct0');
|
||||||
expect(result.cookies.source).toBe('Safari');
|
expect(result.cookies.source).toBe('Safari');
|
||||||
@@ -184,11 +184,7 @@ describe('cookies', () => {
|
|||||||
const { execSync } = await import('node:child_process');
|
const { execSync } = await import('node:child_process');
|
||||||
(execSync as unknown as vi.Mock).mockReturnValue('auth_token|firefox_auth\nct0|firefox_ct0');
|
(execSync as unknown as vi.Mock).mockReturnValue('auth_token|firefox_auth\nct0|firefox_ct0');
|
||||||
|
|
||||||
const result = await resolveCredentials({
|
const result = await resolveCredentials({ cookieSource: 'firefox', firefoxProfile: 'abc.default-release' });
|
||||||
allowFirefox: true,
|
|
||||||
allowChrome: false,
|
|
||||||
firefoxProfile: 'abc.default-release',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.cookies.authToken).toBe('firefox_auth');
|
expect(result.cookies.authToken).toBe('firefox_auth');
|
||||||
expect(result.cookies.ct0).toBe('firefox_ct0');
|
expect(result.cookies.ct0).toBe('firefox_ct0');
|
||||||
@@ -215,7 +211,7 @@ describe('cookies', () => {
|
|||||||
process.env.CT0 = 'test_ct0';
|
process.env.CT0 = 'test_ct0';
|
||||||
|
|
||||||
const { resolveCredentials } = await import('../src/lib/cookies.js');
|
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.authToken).toBe('test_auth_token');
|
||||||
expect(result.cookies.ct0).toBe('test_ct0');
|
expect(result.cookies.ct0).toBe('test_ct0');
|
||||||
@@ -227,7 +223,7 @@ describe('cookies', () => {
|
|||||||
process.env.TWITTER_CT0 = 'twitter_ct0';
|
process.env.TWITTER_CT0 = 'twitter_ct0';
|
||||||
|
|
||||||
const { resolveCredentials } = await import('../src/lib/cookies.js');
|
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.authToken).toBe('twitter_auth');
|
||||||
expect(result.cookies.ct0).toBe('twitter_ct0');
|
expect(result.cookies.ct0).toBe('twitter_ct0');
|
||||||
@@ -249,7 +245,7 @@ describe('cookies', () => {
|
|||||||
process.env.CT0 = '';
|
process.env.CT0 = '';
|
||||||
|
|
||||||
const { resolveCredentials } = await import('../src/lib/cookies.js');
|
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.authToken).toBeNull();
|
||||||
expect(result.cookies.ct0).toBeNull();
|
expect(result.cookies.ct0).toBeNull();
|
||||||
@@ -258,7 +254,7 @@ describe('cookies', () => {
|
|||||||
|
|
||||||
it('should warn when credentials are missing', async () => {
|
it('should warn when credentials are missing', async () => {
|
||||||
const { resolveCredentials } = await import('../src/lib/cookies.js');
|
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(
|
expect(result.warnings).toContain(
|
||||||
'Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox',
|
'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 { resolveCredentials } = await import('../src/lib/cookies.js');
|
||||||
const result = await resolveCredentials({
|
const result = await resolveCredentials({ cookieSource: 'chrome', chromeProfile: 'Default' });
|
||||||
allowSafari: false,
|
|
||||||
allowFirefox: false,
|
|
||||||
allowChrome: true,
|
|
||||||
chromeProfile: 'Default',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.cookies.authToken).toBe('test_auth');
|
expect(result.cookies.authToken).toBe('test_auth');
|
||||||
expect(result.cookies.ct0).toBe('test_ct0');
|
expect(result.cookies.ct0).toBe('test_ct0');
|
||||||
@@ -336,7 +327,7 @@ describe('cookies', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { resolveCredentials } = await import('../src/lib/cookies.js');
|
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.authToken).toBe('safari_auth');
|
||||||
expect(result.cookies.ct0).toBe('safari_ct0');
|
expect(result.cookies.ct0).toBe('safari_ct0');
|
||||||
|
|||||||
Reference in New Issue
Block a user