fix: allow cookie timeout config

This commit is contained in:
Peter Steinberger
2026-01-01 12:00:02 +01:00
parent 42e5124174
commit 14ad6cf73f
8 changed files with 58 additions and 4 deletions
+1
View File
@@ -12,6 +12,7 @@
- Query ID updater now tracks Following/Followers GraphQL operations.
- Query ID updater now tracks BookmarkFolderTimeline and keeps bookmark query IDs seeded.
- `following`/`followers` JSON user fields are now camelCase (`followersCount`, `followingCount`, `isBlueVerified`, `profileImageUrl`, `createdAt`).
- Cookie extraction timeout is now configurable (default 20s on macOS) via `--cookie-timeout` / `BIRD_COOKIE_TIMEOUT_MS`.
## 0.4.1 — 2025-12-31
### Added
+3
View File
@@ -88,6 +88,7 @@ bird query-ids --fresh
Global options:
- `--timeout <ms>`: abort requests after the given timeout (milliseconds).
- `--cookie-timeout <ms>`: cookie extraction timeout for keychain/OS helpers (milliseconds).
- `--quote-depth <n>`: max quoted tweet depth in JSON output (default: 1; 0 disables).
- `--plain`: stable output (no emoji, no color).
- `--no-emoji`: disable emoji output.
@@ -128,6 +129,7 @@ Example `~/.config/bird/config.json5`:
// Cookie source order for browser extraction (string or array)
cookieSource: ["firefox", "safari"],
firefoxProfile: "default-release",
cookieTimeoutMs: 20000,
timeoutMs: 20000,
quoteDepth: 1
}
@@ -135,6 +137,7 @@ Example `~/.config/bird/config.json5`:
Environment shortcuts:
- `BIRD_TIMEOUT_MS`
- `BIRD_COOKIE_TIMEOUT_MS`
- `BIRD_QUOTE_DEPTH`
## Output
+13
View File
@@ -0,0 +1,13 @@
diff --git a/dist/providers/chromeSqliteMac.js b/dist/providers/chromeSqliteMac.js
index 71c815f77378d03e07061976ac372b3a55a61ac4..3a85a2fce102128d7e7b69145d97bc353f663008 100644
--- a/dist/providers/chromeSqliteMac.js
+++ b/dist/providers/chromeSqliteMac.js
@@ -15,7 +15,7 @@ export async function getCookiesFromChromeSqliteMac(options, origins, allowlistN
const passwordResult = await readKeychainGenericPasswordFirst({
account: 'Chrome',
services: ['Chrome Safe Storage'],
- timeoutMs: 3_000,
+ timeoutMs: options.timeoutMs ?? 3_000,
label: 'Chrome Safe Storage',
});
if (!passwordResult.ok) {
+7 -2
View File
@@ -4,13 +4,18 @@ settings:
autoInstallPeers: false
excludeLinksFromLockfile: false
patchedDependencies:
'@steipete/sweet-cookie':
hash: 05180a0fa74ba5074775f965db7d4561e59ba51978bd5e955c0e4f058a4ed340
path: patches/@steipete__sweet-cookie.patch
importers:
.:
dependencies:
'@steipete/sweet-cookie':
specifier: 0.1.0
version: 0.1.0
version: 0.1.0(patch_hash=05180a0fa74ba5074775f965db7d4561e59ba51978bd5e955c0e4f058a4ed340)
commander:
specifier: ^14.0.2
version: 14.0.2
@@ -1051,7 +1056,7 @@ snapshots:
'@standard-schema/[email protected]': {}
'@steipete/[email protected]': {}
'@steipete/[email protected](patch_hash=05180a0fa74ba5074775f965db7d4561e59ba51978bd5e955c0e4f058a4ed340)': {}
'@types/[email protected]':
dependencies:
+3
View File
@@ -1,2 +1,5 @@
onlyBuiltDependencies:
- esbuild
patchedDependencies:
'@steipete/sweet-cookie': patches/@steipete__sweet-cookie.patch
+8
View File
@@ -108,6 +108,7 @@ type BirdConfig = {
chromeProfile?: string;
firefoxProfile?: string;
cookieSource?: CookieSource | CookieSource[];
cookieTimeoutMs?: number;
timeoutMs?: number;
quoteDepth?: number;
};
@@ -187,6 +188,7 @@ program
.option('--ct0 <token>', 'Twitter ct0 cookie')
.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('--cookie-timeout <ms>', 'Cookie extraction timeout in milliseconds (keychain/OS helpers)')
.option(
'--cookie-source <source>',
'Cookie source for browser cookie extraction (repeatable)',
@@ -207,6 +209,7 @@ type CredentialsOptions = {
chromeProfile?: string;
firefoxProfile?: string;
cookieSource?: CookieSource[];
cookieTimeout?: string | number;
};
function resolveCredentialsFromOptions(opts: CredentialsOptions): ReturnType<typeof resolveCredentials> {
@@ -219,6 +222,7 @@ function resolveCredentialsFromOptions(opts: CredentialsOptions): ReturnType<typ
cookieSource,
chromeProfile: opts.chromeProfile || config.chromeProfile,
firefoxProfile: opts.firefoxProfile || config.firefoxProfile,
cookieTimeoutMs: resolveCookieTimeoutFromOptions(opts),
});
}
@@ -245,6 +249,10 @@ function resolveTimeoutFromOptions(options: { timeout?: string | number }): numb
return resolveTimeoutMs(options.timeout, config.timeoutMs, process.env.BIRD_TIMEOUT_MS);
}
function resolveCookieTimeoutFromOptions(options: { cookieTimeout?: string | number }): number | undefined {
return resolveTimeoutMs(options.cookieTimeout, config.cookieTimeoutMs, process.env.BIRD_COOKIE_TIMEOUT_MS);
}
function resolveQuoteDepth(...values: Array<string | number | undefined | null>): number | undefined {
for (const value of values) {
if (value === undefined || value === null || value === '') {
+13
View File
@@ -22,6 +22,7 @@ export type CookieSource = 'safari' | 'chrome' | 'firefox';
const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0'] as const;
const TWITTER_URL = 'https://x.com/';
const TWITTER_ORIGINS: string[] = ['https://x.com/', 'https://twitter.com/'];
const DEFAULT_COOKIE_TIMEOUT_MS = 20_000;
function normalizeValue(value: unknown): string | null {
if (typeof value !== 'string') {
@@ -102,6 +103,7 @@ async function readTwitterCookiesFromBrowser(options: {
source: CookieSource;
chromeProfile?: string;
firefoxProfile?: string;
cookieTimeoutMs?: number;
}): Promise<CookieExtractionResult> {
const warnings: string[] = [];
const out = buildEmpty();
@@ -114,6 +116,7 @@ async function readTwitterCookiesFromBrowser(options: {
mode: 'merge',
chromeProfile: options.chromeProfile,
firefoxProfile: options.firefoxProfile,
timeoutMs: options.cookieTimeoutMs,
});
warnings.push(...providerWarnings);
@@ -170,9 +173,18 @@ export async function resolveCredentials(options: {
cookieSource?: CookieSource | CookieSource[];
chromeProfile?: string;
firefoxProfile?: string;
cookieTimeoutMs?: number;
}): Promise<CookieExtractionResult> {
const warnings: string[] = [];
const cookies = buildEmpty();
const cookieTimeoutMs =
typeof options.cookieTimeoutMs === 'number' &&
Number.isFinite(options.cookieTimeoutMs) &&
options.cookieTimeoutMs > 0
? options.cookieTimeoutMs
: process.platform === 'darwin'
? DEFAULT_COOKIE_TIMEOUT_MS
: undefined;
if (options.authToken) {
cookies.authToken = options.authToken;
@@ -199,6 +211,7 @@ export async function resolveCredentials(options: {
source,
chromeProfile: options.chromeProfile,
firefoxProfile: options.firefoxProfile,
cookieTimeoutMs,
});
warnings.push(...res.warnings);
if (res.cookies.authToken && res.cookies.ct0) {
+10 -2
View File
@@ -4,11 +4,13 @@ type SweetCookieResult = { cookies: Array<{ name: string; value: string; domain?
const sweet = vi.hoisted(() => ({
results: new Map<string, SweetCookieResult>(),
options: new Map<string, { timeoutMs?: number }>(),
}));
vi.mock('@steipete/sweet-cookie', () => ({
getCookies: vi.fn(async (options: { browsers?: string[] }) => {
getCookies: vi.fn(async (options: { browsers?: string[]; timeoutMs?: number }) => {
const browser = options.browsers?.[0] ?? 'unknown';
sweet.options.set(browser, options);
return (
sweet.results.get(browser) ?? {
cookies: [],
@@ -24,6 +26,7 @@ describe('cookies', () => {
beforeEach(() => {
vi.resetModules();
sweet.results.clear();
sweet.options.clear();
process.env = { ...originalEnv };
process.env.AUTH_TOKEN = undefined;
process.env.TWITTER_AUTH_TOKEN = undefined;
@@ -186,11 +189,16 @@ describe('cookies', () => {
});
const { resolveCredentials } = await import('../src/lib/cookies.js');
const result = await resolveCredentials({ cookieSource: 'chrome', chromeProfile: 'Default' });
const result = await resolveCredentials({
cookieSource: 'chrome',
chromeProfile: 'Default',
cookieTimeoutMs: 15000,
});
expect(result.cookies.authToken).toBe('test_auth');
expect(result.cookies.ct0).toBe('test_ct0');
expect(result.cookies.source).toContain('Chrome');
expect(sweet.options.get('chrome')?.timeoutMs).toBe(15000);
});
it('uses default browser order when cookieSource is omitted', async () => {