last30days: lazy-load vendored Bird cookie support

This commit is contained in:
Pejman Pour-Moezzi
2026-04-08 14:31:17 -07:00
parent 9c203d3595
commit 681d05d7ee
2 changed files with 74 additions and 1 deletions
+23 -1
View File
@@ -2,11 +2,13 @@
* Browser cookie extraction for Twitter authentication. * Browser cookie extraction for Twitter authentication.
* Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads. * Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads.
*/ */
import { getCookies } from '@steipete/sweet-cookie';
const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0']; const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0'];
const TWITTER_URL = 'https://x.com/'; const TWITTER_URL = 'https://x.com/';
const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/']; const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/'];
const DEFAULT_COOKIE_TIMEOUT_MS = 30_000; const DEFAULT_COOKIE_TIMEOUT_MS = 30_000;
async function loadSweetCookie() {
return import('@steipete/sweet-cookie');
}
function normalizeValue(value) { function normalizeValue(value) {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return null; return null;
@@ -79,6 +81,7 @@ function pickCookieValue(cookies, name) {
async function readTwitterCookiesFromBrowser(options) { async function readTwitterCookiesFromBrowser(options) {
const warnings = []; const warnings = [];
const out = buildEmpty(); const out = buildEmpty();
const { getCookies } = options;
const { cookies, warnings: providerWarnings } = await getCookies({ const { cookies, warnings: providerWarnings } = await getCookies({
url: TWITTER_URL, url: TWITTER_URL,
origins: TWITTER_ORIGINS, origins: TWITTER_ORIGINS,
@@ -165,8 +168,27 @@ export async function resolveCredentials(options) {
return { cookies, warnings }; return { cookies, warnings };
} }
const sourcesToTry = resolveSources(options.cookieSource); const sourcesToTry = resolveSources(options.cookieSource);
let getCookies;
try {
({ getCookies } = await loadSweetCookie());
}
catch (error) {
if (error?.code !== 'ERR_MODULE_NOT_FOUND' ||
!String(error.message ?? '').includes('@steipete/sweet-cookie')) {
throw error;
}
warnings.push('Browser cookie lookup unavailable because vendored dependency @steipete/sweet-cookie is not installed.');
if (!cookies.authToken) {
warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox');
}
if (!cookies.ct0) {
warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox');
}
return { cookies, warnings };
}
for (const source of sourcesToTry) { for (const source of sourcesToTry) {
const res = await readTwitterCookiesFromBrowser({ const res = await readTwitterCookiesFromBrowser({
getCookies,
source, source,
chromeProfile: options.chromeProfile, chromeProfile: options.chromeProfile,
firefoxProfile: options.firefoxProfile, firefoxProfile: options.firefoxProfile,
+51
View File
@@ -1,3 +1,7 @@
import json
import os
import shutil
import subprocess
import sys import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -7,6 +11,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from lib.bird_x import parse_bird_response from lib.bird_x import parse_bird_response
REPO_ROOT = Path(__file__).resolve().parents[1]
VENDORED_BIRD = REPO_ROOT / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs"
class TestBirdXEngagementZero(unittest.TestCase): class TestBirdXEngagementZero(unittest.TestCase):
def test_zero_likes_preserved(self): def test_zero_likes_preserved(self):
tweets = [ tweets = [
@@ -22,6 +30,49 @@ class TestBirdXEngagementZero(unittest.TestCase):
self.assertEqual(0, items[0]["engagement"]["likes"]) self.assertEqual(0, items[0]["engagement"]["likes"])
self.assertEqual(5, items[0]["engagement"]["reposts"]) self.assertEqual(5, items[0]["engagement"]["reposts"])
@unittest.skipUnless(shutil.which("node"), "node is required for vendored Bird tests")
class TestVendoredBirdRuntime(unittest.TestCase):
def test_check_uses_env_credentials_without_browser_cookie_dependency(self):
env = os.environ.copy()
env["AUTH_TOKEN"] = "dummy-auth"
env["CT0"] = "dummy-ct0"
result = subprocess.run(
["node", str(VENDORED_BIRD), "--check"],
cwd=REPO_ROOT,
env=env,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(0, result.returncode, result.stderr)
payload = json.loads(result.stdout)
self.assertTrue(payload["authenticated"])
self.assertEqual("env AUTH_TOKEN", payload["source"])
def test_check_with_browser_lookup_disabled_returns_json_warnings(self):
env = os.environ.copy()
env.pop("AUTH_TOKEN", None)
env.pop("CT0", None)
env["BIRD_DISABLE_BROWSER_COOKIES"] = "1"
result = subprocess.run(
["node", str(VENDORED_BIRD), "--check"],
cwd=REPO_ROOT,
env=env,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(1, result.returncode, result.stderr)
payload = json.loads(result.stdout)
self.assertFalse(payload["authenticated"])
self.assertTrue(payload["warnings"])
self.assertIn("Missing auth_token", " ".join(payload["warnings"]))
def test_none_likes_when_missing(self): def test_none_likes_when_missing(self):
tweets = [ tweets = [
{ {