| 1 | /** |
| 2 | * Browser cookie extraction for Twitter authentication. |
| 3 | * Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads. |
| 4 | */ |
| 5 | const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0']; |
| 6 | const TWITTER_URL = 'https://x.com/'; |
| 7 | const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/']; |
| 8 | const DEFAULT_COOKIE_TIMEOUT_MS = 30_000; |
| 9 | async function loadSweetCookie() { |
| 10 | return import('@steipete/sweet-cookie'); |
| 11 | } |
| 12 | function normalizeValue(value) { |
| 13 | if (typeof value !== 'string') { |
| 14 | return null; |
| 15 | } |
| 16 | const trimmed = value.trim(); |
| 17 | return trimmed.length > 0 ? trimmed : null; |
| 18 | } |
| 19 | function envFlagEnabled(name) { |
| 20 | const value = normalizeValue(process.env[name]); |
| 21 | if (!value) { |
| 22 | return false; |
| 23 | } |
| 24 | return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase()); |
| 25 | } |
| 26 | function cookieHeader(authToken, ct0) { |
| 27 | return `auth_token=${authToken}; ct0=${ct0}`; |
| 28 | } |
| 29 | function buildEmpty() { |
| 30 | return { authToken: null, ct0: null, cookieHeader: null, source: null }; |
| 31 | } |
| 32 | function readEnvCookie(cookies, keys, field) { |
| 33 | if (cookies[field]) { |
| 34 | return; |
| 35 | } |
| 36 | for (const key of keys) { |
| 37 | const value = normalizeValue(process.env[key]); |
| 38 | if (!value) { |
| 39 | continue; |
| 40 | } |
| 41 | cookies[field] = value; |
| 42 | if (!cookies.source) { |
| 43 | cookies.source = `env ${key}`; |
| 44 | } |
| 45 | break; |
| 46 | } |
| 47 | } |
| 48 | function resolveSources(cookieSource) { |
| 49 | if (Array.isArray(cookieSource)) { |
| 50 | return cookieSource; |
| 51 | } |
| 52 | if (cookieSource) { |
| 53 | return [cookieSource]; |
| 54 | } |
| 55 | return ['safari', 'chrome', 'firefox']; |
| 56 | } |
| 57 | function labelForSource(source, profile) { |
| 58 | if (source === 'safari') { |
| 59 | return 'Safari'; |
| 60 | } |
| 61 | if (source === 'chrome') { |
| 62 | return profile ? `Chrome profile "${profile}"` : 'Chrome default profile'; |
| 63 | } |
| 64 | return profile ? `Firefox profile "${profile}"` : 'Firefox default profile'; |
| 65 | } |
| 66 | function pickCookieValue(cookies, name) { |
| 67 | const matches = cookies.filter((c) => c?.name === name && typeof c.value === 'string'); |
| 68 | if (matches.length === 0) { |
| 69 | return null; |
| 70 | } |
| 71 | const preferred = matches.find((c) => (c.domain ?? '').endsWith('x.com')); |
| 72 | if (preferred?.value) { |
| 73 | return preferred.value; |
| 74 | } |
| 75 | const twitter = matches.find((c) => (c.domain ?? '').endsWith('twitter.com')); |
| 76 | if (twitter?.value) { |
| 77 | return twitter.value; |
| 78 | } |
| 79 | return matches[0]?.value ?? null; |
| 80 | } |
| 81 | async function readTwitterCookiesFromBrowser(options) { |
| 82 | const warnings = []; |
| 83 | const out = buildEmpty(); |
| 84 | const { getCookies } = options; |
| 85 | const { cookies, warnings: providerWarnings } = await getCookies({ |
| 86 | url: TWITTER_URL, |
| 87 | origins: TWITTER_ORIGINS, |
| 88 | names: [...TWITTER_COOKIE_NAMES], |
| 89 | browsers: [options.source], |
| 90 | mode: 'merge', |
| 91 | chromeProfile: options.chromeProfile, |
| 92 | firefoxProfile: options.firefoxProfile, |
| 93 | timeoutMs: options.cookieTimeoutMs, |
| 94 | }); |
| 95 | warnings.push(...providerWarnings); |
| 96 | const authToken = pickCookieValue(cookies, 'auth_token'); |
| 97 | const ct0 = pickCookieValue(cookies, 'ct0'); |
| 98 | if (authToken) { |
| 99 | out.authToken = authToken; |
| 100 | } |
| 101 | if (ct0) { |
| 102 | out.ct0 = ct0; |
| 103 | } |
| 104 | if (out.authToken && out.ct0) { |
| 105 | out.cookieHeader = cookieHeader(out.authToken, out.ct0); |
| 106 | out.source = labelForSource(options.source, options.source === 'chrome' ? options.chromeProfile : options.firefoxProfile); |
| 107 | return { cookies: out, warnings }; |
| 108 | } |
| 109 | if (options.source === 'safari') { |
| 110 | warnings.push('No Twitter cookies found in Safari. Make sure you are logged into x.com in Safari.'); |
| 111 | } |
| 112 | else if (options.source === 'chrome') { |
| 113 | warnings.push('No Twitter cookies found in Chrome. Make sure you are logged into x.com in Chrome.'); |
| 114 | } |
| 115 | else { |
| 116 | warnings.push('No Twitter cookies found in Firefox. Make sure you are logged into x.com in Firefox and the profile exists.'); |
| 117 | } |
| 118 | return { cookies: out, warnings }; |
| 119 | } |
| 120 | async function extractCookiesFromBrowser(options) { |
| 121 | const { getCookies } = await loadSweetCookie(); |
| 122 | return readTwitterCookiesFromBrowser({ |
| 123 | getCookies, |
| 124 | ...options, |
| 125 | }); |
| 126 | } |
| 127 | export async function extractCookiesFromSafari() { |
| 128 | return extractCookiesFromBrowser({ source: 'safari' }); |
| 129 | } |
| 130 | export async function extractCookiesFromChrome(profile) { |
| 131 | return extractCookiesFromBrowser({ source: 'chrome', chromeProfile: profile }); |
| 132 | } |
| 133 | export async function extractCookiesFromFirefox(profile) { |
| 134 | return extractCookiesFromBrowser({ source: 'firefox', firefoxProfile: profile }); |
| 135 | } |
| 136 | /** |
| 137 | * Resolve Twitter credentials from multiple sources. |
| 138 | * Priority: CLI args > environment variables > browsers (ordered). |
| 139 | */ |
| 140 | export async function resolveCredentials(options) { |
| 141 | const warnings = []; |
| 142 | const cookies = buildEmpty(); |
| 143 | const disableBrowserCookies = envFlagEnabled('BIRD_DISABLE_BROWSER_COOKIES') || |
| 144 | envFlagEnabled('LAST30DAYS_DISABLE_BROWSER_COOKIES'); |
| 145 | const cookieTimeoutMs = typeof options.cookieTimeoutMs === 'number' && |
| 146 | Number.isFinite(options.cookieTimeoutMs) && |
| 147 | options.cookieTimeoutMs > 0 |
| 148 | ? options.cookieTimeoutMs |
| 149 | : process.platform === 'darwin' |
| 150 | ? DEFAULT_COOKIE_TIMEOUT_MS |
| 151 | : undefined; |
| 152 | if (options.authToken) { |
| 153 | cookies.authToken = options.authToken; |
| 154 | cookies.source = 'CLI argument'; |
| 155 | } |
| 156 | if (options.ct0) { |
| 157 | cookies.ct0 = options.ct0; |
| 158 | if (!cookies.source) { |
| 159 | cookies.source = 'CLI argument'; |
| 160 | } |
| 161 | } |
| 162 | readEnvCookie(cookies, ['AUTH_TOKEN', 'TWITTER_AUTH_TOKEN'], 'authToken'); |
| 163 | readEnvCookie(cookies, ['CT0', 'TWITTER_CT0'], 'ct0'); |
| 164 | if (cookies.authToken && cookies.ct0) { |
| 165 | cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0); |
| 166 | return { cookies, warnings }; |
| 167 | } |
| 168 | if (disableBrowserCookies) { |
| 169 | if (!cookies.authToken) { |
| 170 | warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or disable BIRD_DISABLE_BROWSER_COOKIES to allow browser cookie lookup'); |
| 171 | } |
| 172 | if (!cookies.ct0) { |
| 173 | warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or disable BIRD_DISABLE_BROWSER_COOKIES to allow browser cookie lookup'); |
| 174 | } |
| 175 | return { cookies, warnings }; |
| 176 | } |
| 177 | const sourcesToTry = resolveSources(options.cookieSource); |
| 178 | let getCookies; |
| 179 | try { |
| 180 | ({ getCookies } = await loadSweetCookie()); |
| 181 | } |
| 182 | catch (error) { |
| 183 | if (error?.code !== 'ERR_MODULE_NOT_FOUND' || |
| 184 | !String(error.message ?? '').includes('@steipete/sweet-cookie')) { |
| 185 | throw error; |
| 186 | } |
| 187 | warnings.push('Browser cookie lookup unavailable because vendored dependency @steipete/sweet-cookie is not installed.'); |
| 188 | if (!cookies.authToken) { |
| 189 | warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox'); |
| 190 | } |
| 191 | if (!cookies.ct0) { |
| 192 | warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox'); |
| 193 | } |
| 194 | return { cookies, warnings }; |
| 195 | } |
| 196 | for (const source of sourcesToTry) { |
| 197 | const res = await readTwitterCookiesFromBrowser({ |
| 198 | getCookies, |
| 199 | source, |
| 200 | chromeProfile: options.chromeProfile, |
| 201 | firefoxProfile: options.firefoxProfile, |
| 202 | cookieTimeoutMs, |
| 203 | }); |
| 204 | warnings.push(...res.warnings); |
| 205 | if (res.cookies.authToken && res.cookies.ct0) { |
| 206 | return { cookies: res.cookies, warnings }; |
| 207 | } |
| 208 | } |
| 209 | if (!cookies.authToken) { |
| 210 | warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox'); |
| 211 | } |
| 212 | if (!cookies.ct0) { |
| 213 | warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox'); |
| 214 | } |
| 215 | if (cookies.authToken && cookies.ct0) { |
| 216 | cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0); |
| 217 | } |
| 218 | return { cookies, warnings }; |
| 219 | } |
| 220 | //# sourceMappingURL=cookies.js.map |
| 221 |