Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Convert X (Twitter) tweets, threads, and articles to Markdown with YAML front matter via reverse-engineered API.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/cookies.ts
1import {2CdpConnection,3findChromeExecutable as findChromeExecutableBase,4findExistingChromeDebugPort,5gracefulKillChrome,6getFreePort,7launchChrome as launchChromeBase,8openPageSession,9sleep,10waitForChromeDebugPort,11type PlatformCandidates,12} from "baoyu-chrome-cdp";1314import process from "node:process";1516import { read_cookie_file, write_cookie_file } from "./cookie-file.js";17import { resolveXToMarkdownCookiePath } from "./paths.js";18import { X_COOKIE_NAMES, X_REQUIRED_COOKIES, X_LOGIN_URL, X_USER_DATA_DIR } from "./constants.js";19import type { CookieLike } from "./types.js";2021const CHROME_CANDIDATES_FULL: PlatformCandidates = {22darwin: [23"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",24"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",25"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",26"/Applications/Chromium.app/Contents/MacOS/Chromium",27"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",28],29win32: [30"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",31"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",32"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",33"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",34],35default: [36"/usr/bin/google-chrome",37"/usr/bin/google-chrome-stable",38"/usr/bin/chromium",39"/usr/bin/chromium-browser",40"/snap/bin/chromium",41"/usr/bin/microsoft-edge",42],43};4445function findChromeExecutable(): string | null {46return findChromeExecutableBase({47candidates: CHROME_CANDIDATES_FULL,48envNames: ["X_CHROME_PATH"],49}) ?? null;50}5152async function launchChrome(profileDir: string, port: number) {53const chromePath = findChromeExecutable();54if (!chromePath) throw new Error("Chrome executable not found.");55return await launchChromeBase({56chromePath,57profileDir,58port,59url: X_LOGIN_URL,60extraArgs: ["--disable-popup-blocking"],61});62}6364async function fetchXCookiesViaCdp(65profileDir: string,66timeoutMs: number,67verbose: boolean,68log?: (message: string) => void69): Promise<Record<string, string>> {70const existingPort = await findExistingChromeDebugPort({ profileDir });71const reusing = existingPort !== null;72const port = existingPort ?? await getFreePort("X_DEBUG_PORT");73const chrome = reusing ? null : await launchChrome(profileDir, port);7475let cdp: CdpConnection | null = null;76let targetId: string | null = null;77try {78const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });79cdp = await CdpConnection.connect(wsUrl, 15_000);8081const page = await openPageSession({82cdp,83reusing,84url: X_LOGIN_URL,85matchTarget: (target) => target.type === "page" && (86target.url.includes("x.com") || target.url.includes("twitter.com")87),88enableNetwork: true,89});90const { sessionId } = page;91targetId = page.targetId;9293if (verbose) {94log?.(reusing95? `[x-cookies] Reusing existing Chrome on port ${port}. Waiting for cookies...`96: "[x-cookies] Chrome opened. If needed, complete X login in the window. Waiting for cookies...");97}9899const start = Date.now();100let last: Record<string, string> = {};101102while (Date.now() - start < timeoutMs) {103const { cookies } = await cdp.send<{ cookies: CookieLike[] }>(104"Network.getCookies",105{ urls: ["https://x.com/", "https://twitter.com/"] },106{ sessionId, timeoutMs: 10_000 }107);108109const m = buildXCookieMap((cookies ?? []).filter(Boolean));110last = m;111if (hasRequiredXCookies(m)) {112return m;113}114115await sleep(1000);116}117118throw new Error(`Timed out waiting for X cookies. Last keys: ${Object.keys(last).join(", ")}`);119} finally {120if (cdp) {121if (reusing && targetId) {122try {123await cdp.send("Target.closeTarget", { targetId }, { timeoutMs: 5_000 });124} catch {}125}126cdp.close();127}128129if (chrome) await gracefulKillChrome(chrome, port);130}131}132133function resolveCookieDomain(cookie: CookieLike): string | null {134const rawDomain = cookie.domain?.trim();135if (rawDomain) {136return rawDomain.startsWith(".") ? rawDomain.slice(1) : rawDomain;137}138const rawUrl = cookie.url?.trim();139if (rawUrl) {140try {141return new URL(rawUrl).hostname;142} catch {143return null;144}145}146return null;147}148149function pickCookieValue<T extends CookieLike>(cookies: T[], name: string): string | undefined {150const matches = cookies.filter((cookie) => cookie.name === name && typeof cookie.value === "string");151if (matches.length === 0) return undefined;152153const preferred = matches.find((cookie) => {154const domain = resolveCookieDomain(cookie);155return domain === "x.com" && (cookie.path ?? "/") === "/";156});157const xDomain = matches.find((cookie) => (resolveCookieDomain(cookie) ?? "").endsWith("x.com"));158const twitterDomain = matches.find((cookie) => (resolveCookieDomain(cookie) ?? "").endsWith("twitter.com"));159return (preferred ?? xDomain ?? twitterDomain ?? matches[0])?.value;160}161162function buildXCookieMap<T extends CookieLike>(cookies: T[]): Record<string, string> {163const cookieMap: Record<string, string> = {};164for (const name of X_COOKIE_NAMES) {165const value = pickCookieValue(cookies, name);166if (value) cookieMap[name] = value;167}168return cookieMap;169}170171export function hasRequiredXCookies(cookieMap: Record<string, string>): boolean {172return X_REQUIRED_COOKIES.every((name) => Boolean(cookieMap[name]));173}174175function filterXCookieMap(cookieMap: Record<string, string>): Record<string, string> {176const filtered: Record<string, string> = {};177for (const name of X_COOKIE_NAMES) {178const value = cookieMap[name];179if (value) filtered[name] = value;180}181return filtered;182}183184function buildInlineCookiesFromEnv(): CookieLike[] {185const cookies: CookieLike[] = [];186const authToken = process.env.X_AUTH_TOKEN?.trim();187const ct0 = process.env.X_CT0?.trim();188const gt = process.env.X_GUEST_TOKEN?.trim();189const twid = process.env.X_TWID?.trim();190191if (authToken) {192cookies.push({ name: "auth_token", value: authToken, domain: "x.com", path: "/" });193}194if (ct0) {195cookies.push({ name: "ct0", value: ct0, domain: "x.com", path: "/" });196}197if (gt) {198cookies.push({ name: "gt", value: gt, domain: "x.com", path: "/" });199}200if (twid) {201cookies.push({ name: "twid", value: twid, domain: "x.com", path: "/" });202}203204return cookies;205}206207async function loadXCookiesFromInline(log?: (message: string) => void): Promise<Record<string, string>> {208const inline = buildInlineCookiesFromEnv();209if (inline.length === 0) return {};210211const cookieMap = buildXCookieMap(212inline.filter((cookie): cookie is CookieLike => Boolean(cookie?.name && typeof cookie.value === "string"))213);214215if (Object.keys(cookieMap).length > 0) {216log?.(`[x-cookies] Loaded X cookies from env: ${Object.keys(cookieMap).length} cookie(s).`);217} else {218log?.("[x-cookies] Env cookies provided but no X cookies matched.");219}220221return cookieMap;222}223224async function loadXCookiesFromFile(log?: (message: string) => void): Promise<Record<string, string>> {225const cookiePath = resolveXToMarkdownCookiePath();226const fileMap = filterXCookieMap((await read_cookie_file(cookiePath)) ?? {});227if (Object.keys(fileMap).length > 0) {228log?.(`[x-cookies] Loaded X cookies from file: ${cookiePath} (${Object.keys(fileMap).length} cookie(s))`);229}230return fileMap;231}232233async function loadXCookiesFromCdp(log?: (message: string) => void): Promise<Record<string, string>> {234try {235const cookieMap = await fetchXCookiesViaCdp(X_USER_DATA_DIR, 5 * 60 * 1000, true, log);236if (!hasRequiredXCookies(cookieMap)) return cookieMap;237238const cookiePath = resolveXToMarkdownCookiePath();239try {240await write_cookie_file(cookieMap, cookiePath, "cdp");241log?.(`[x-cookies] Cookies saved to ${cookiePath}`);242} catch (error) {243log?.(244`[x-cookies] Failed to write cookie file (${cookiePath}): ${245error instanceof Error ? error.message : String(error ?? "")246}`247);248}249if (cookieMap.auth_token) log?.(`[x-cookies] auth_token: ${cookieMap.auth_token.slice(0, 20)}...`);250if (cookieMap.ct0) log?.(`[x-cookies] ct0: ${cookieMap.ct0.slice(0, 20)}...`);251return cookieMap;252} catch (error) {253log?.(254`[x-cookies] Failed to load cookies via Chrome DevTools Protocol: ${255error instanceof Error ? error.message : String(error ?? "")256}`257);258return {};259}260}261262export async function loadXCookies(log?: (message: string) => void): Promise<Record<string, string>> {263const inlineMap = await loadXCookiesFromInline(log);264const fileMap = await loadXCookiesFromFile(log);265const combined = { ...fileMap, ...inlineMap };266267if (hasRequiredXCookies(combined)) return combined;268269const cdpMap = await loadXCookiesFromCdp(log);270return { ...fileMap, ...cdpMap, ...inlineMap };271}272273export async function refreshXCookies(log?: (message: string) => void): Promise<Record<string, string>> {274return loadXCookiesFromCdp(log);275}276277export function buildCookieHeader(cookieMap: Record<string, string>): string | undefined {278const entries = Object.entries(cookieMap).filter(([, value]) => value);279if (entries.length === 0) return undefined;280return entries.map(([key, value]) => `${key}=${value}`).join("; ");281}282