Disable browser cookie fallback for local X auth
Prefer injected AUTH_TOKEN/CT0 for bundled Bird, disable browser-cookie probing in repo-invoked subprocesses, and keep repo-invoked yt-dlp from inheriting browser-cookie settings. Validation: uv run python -m unittest tests.test_bird_x tests.test_youtube_yt
This commit is contained in:
+13
-1
@@ -36,10 +36,19 @@ def set_credentials(auth_token: Optional[str], ct0: Optional[str]):
|
|||||||
_credentials['CT0'] = ct0
|
_credentials['CT0'] = ct0
|
||||||
|
|
||||||
|
|
||||||
|
def _has_injected_credentials() -> bool:
|
||||||
|
"""Return True when both X session cookies were injected from config."""
|
||||||
|
return bool(_credentials.get('AUTH_TOKEN') and _credentials.get('CT0'))
|
||||||
|
|
||||||
|
|
||||||
def _subprocess_env() -> Dict[str, str]:
|
def _subprocess_env() -> Dict[str, str]:
|
||||||
"""Build env dict for Node subprocesses, merging injected credentials."""
|
"""Build env dict for Node subprocesses, merging injected credentials."""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(_credentials)
|
env.update(_credentials)
|
||||||
|
# When repo config already provides cookies, disable browser-cookie fallback
|
||||||
|
# so vendored Bird never hits Safari/Chrome keychain during automation.
|
||||||
|
if _has_injected_credentials():
|
||||||
|
env.setdefault("BIRD_DISABLE_BROWSER_COOKIES", "1")
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
@@ -126,6 +135,9 @@ def is_bird_authenticated() -> Optional[str]:
|
|||||||
if not is_bird_installed():
|
if not is_bird_installed():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if _has_injected_credentials():
|
||||||
|
return "env AUTH_TOKEN"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["node", str(_BIRD_SEARCH_MJS), "--whoami"],
|
["node", str(_BIRD_SEARCH_MJS), "--whoami"],
|
||||||
@@ -473,4 +485,4 @@ def parse_bird_response(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|||||||
+19
-1
@@ -14,6 +14,13 @@ function normalizeValue(value) {
|
|||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
return trimmed.length > 0 ? trimmed : null;
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
function envFlagEnabled(name) {
|
||||||
|
const value = normalizeValue(process.env[name]);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
|
||||||
|
}
|
||||||
function cookieHeader(authToken, ct0) {
|
function cookieHeader(authToken, ct0) {
|
||||||
return `auth_token=${authToken}; ct0=${ct0}`;
|
return `auth_token=${authToken}; ct0=${ct0}`;
|
||||||
}
|
}
|
||||||
@@ -123,6 +130,8 @@ export async function extractCookiesFromFirefox(profile) {
|
|||||||
export async function resolveCredentials(options) {
|
export async function resolveCredentials(options) {
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
const cookies = buildEmpty();
|
const cookies = buildEmpty();
|
||||||
|
const disableBrowserCookies = envFlagEnabled('BIRD_DISABLE_BROWSER_COOKIES') ||
|
||||||
|
envFlagEnabled('LAST30DAYS_DISABLE_BROWSER_COOKIES');
|
||||||
const cookieTimeoutMs = typeof options.cookieTimeoutMs === 'number' &&
|
const cookieTimeoutMs = typeof options.cookieTimeoutMs === 'number' &&
|
||||||
Number.isFinite(options.cookieTimeoutMs) &&
|
Number.isFinite(options.cookieTimeoutMs) &&
|
||||||
options.cookieTimeoutMs > 0
|
options.cookieTimeoutMs > 0
|
||||||
@@ -146,6 +155,15 @@ export async function resolveCredentials(options) {
|
|||||||
cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0);
|
cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0);
|
||||||
return { cookies, warnings };
|
return { cookies, warnings };
|
||||||
}
|
}
|
||||||
|
if (disableBrowserCookies) {
|
||||||
|
if (!cookies.authToken) {
|
||||||
|
warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or disable BIRD_DISABLE_BROWSER_COOKIES to allow browser cookie lookup');
|
||||||
|
}
|
||||||
|
if (!cookies.ct0) {
|
||||||
|
warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or disable BIRD_DISABLE_BROWSER_COOKIES to allow browser cookie lookup');
|
||||||
|
}
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
const sourcesToTry = resolveSources(options.cookieSource);
|
const sourcesToTry = resolveSources(options.cookieSource);
|
||||||
for (const source of sourcesToTry) {
|
for (const source of sourcesToTry) {
|
||||||
const res = await readTwitterCookiesFromBrowser({
|
const res = await readTwitterCookiesFromBrowser({
|
||||||
@@ -170,4 +188,4 @@ export async function resolveCredentials(options) {
|
|||||||
}
|
}
|
||||||
return { cookies, warnings };
|
return { cookies, warnings };
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=cookies.js.map
|
//# sourceMappingURL=cookies.js.map
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ def search_youtube(
|
|||||||
# filtering returns 0 for evergreen topics like "thumbnail tips".
|
# filtering returns 0 for evergreen topics like "thumbnail tips".
|
||||||
cmd = [
|
cmd = [
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
|
"--ignore-config",
|
||||||
|
"--no-cookies-from-browser",
|
||||||
f"ytsearch{count}:{core_topic}",
|
f"ytsearch{count}:{core_topic}",
|
||||||
"--dump-json",
|
"--dump-json",
|
||||||
"--no-warnings",
|
"--no-warnings",
|
||||||
@@ -295,6 +297,8 @@ def fetch_transcript(video_id: str, temp_dir: str) -> Optional[str]:
|
|||||||
"""
|
"""
|
||||||
cmd = [
|
cmd = [
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
|
"--ignore-config",
|
||||||
|
"--no-cookies-from-browser",
|
||||||
"--write-auto-subs",
|
"--write-auto-subs",
|
||||||
"--sub-lang", "en",
|
"--sub-lang", "en",
|
||||||
"--sub-format", "vtt",
|
"--sub-format", "vtt",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ from lib import bird_x
|
|||||||
|
|
||||||
|
|
||||||
class TestExtractCoreSubject(unittest.TestCase):
|
class TestExtractCoreSubject(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
bird_x._credentials.clear()
|
||||||
|
|
||||||
def test_strips_trending_noise(self):
|
def test_strips_trending_noise(self):
|
||||||
result = bird_x._extract_core_subject("trendiest Claude Code skills")
|
result = bird_x._extract_core_subject("trendiest Claude Code skills")
|
||||||
self.assertNotIn("trendiest", result)
|
self.assertNotIn("trendiest", result)
|
||||||
@@ -28,6 +31,9 @@ class TestExtractCoreSubject(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestBirdSearchRetries(unittest.TestCase):
|
class TestBirdSearchRetries(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
bird_x._credentials.clear()
|
||||||
|
|
||||||
def test_last_chance_retry_uses_strongest_token(self):
|
def test_last_chance_retry_uses_strongest_token(self):
|
||||||
"""When shorter retry also returns 0, uses longest non-noise token."""
|
"""When shorter retry also returns 0, uses longest non-noise token."""
|
||||||
empty = {"items": []}
|
empty = {"items": []}
|
||||||
@@ -53,5 +59,29 @@ class TestBirdSearchRetries(unittest.TestCase):
|
|||||||
self.assertEqual(run_mock.call_count, 1)
|
self.assertEqual(run_mock.call_count, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBirdAuthEnvironment(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
bird_x._credentials.clear()
|
||||||
|
|
||||||
|
def test_subprocess_env_disables_browser_cookie_fallback_when_injected(self):
|
||||||
|
bird_x.set_credentials("auth-token", "ct0-token")
|
||||||
|
|
||||||
|
env = bird_x._subprocess_env()
|
||||||
|
|
||||||
|
self.assertEqual(env["AUTH_TOKEN"], "auth-token")
|
||||||
|
self.assertEqual(env["CT0"], "ct0-token")
|
||||||
|
self.assertEqual(env["BIRD_DISABLE_BROWSER_COOKIES"], "1")
|
||||||
|
|
||||||
|
def test_is_bird_authenticated_short_circuits_when_credentials_injected(self):
|
||||||
|
bird_x.set_credentials("auth-token", "ct0-token")
|
||||||
|
|
||||||
|
with mock.patch.object(bird_x, "is_bird_installed", return_value=True), \
|
||||||
|
mock.patch.object(bird_x.subprocess, "run") as run_mock:
|
||||||
|
result = bird_x.is_bird_authenticated()
|
||||||
|
|
||||||
|
self.assertEqual(result, "env AUTH_TOKEN")
|
||||||
|
run_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Tests for yt-dlp invocation safety flags."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||||
|
|
||||||
|
from lib import youtube_yt
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProc:
|
||||||
|
def __init__(self):
|
||||||
|
self.pid = 12345
|
||||||
|
self.returncode = 0
|
||||||
|
|
||||||
|
def communicate(self, timeout=None):
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestYtDlpFlags(unittest.TestCase):
|
||||||
|
def test_search_ignores_global_config_and_browser_cookies(self):
|
||||||
|
proc = _DummyProc()
|
||||||
|
with mock.patch.object(youtube_yt, "is_ytdlp_installed", return_value=True), \
|
||||||
|
mock.patch.object(youtube_yt.subprocess, "Popen", return_value=proc) as popen_mock:
|
||||||
|
youtube_yt.search_youtube("Claude Code", "2026-02-01", "2026-03-01")
|
||||||
|
|
||||||
|
cmd = popen_mock.call_args.args[0]
|
||||||
|
self.assertIn("--ignore-config", cmd)
|
||||||
|
self.assertIn("--no-cookies-from-browser", cmd)
|
||||||
|
|
||||||
|
def test_transcript_fetch_ignores_global_config_and_browser_cookies(self):
|
||||||
|
proc = _DummyProc()
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir, \
|
||||||
|
mock.patch.object(youtube_yt.subprocess, "Popen", return_value=proc) as popen_mock:
|
||||||
|
youtube_yt.fetch_transcript("abc123", temp_dir)
|
||||||
|
|
||||||
|
cmd = popen_mock.call_args.args[0]
|
||||||
|
self.assertIn("--ignore-config", cmd)
|
||||||
|
self.assertIn("--no-cookies-from-browser", cmd)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user