spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1import readline from "node:readline";2import { createLogger, sleep, type Platform } from "@src/shared";3import { SocialBrowserSession } from "./session.ts";45const log = createLogger("login");67export const LOGIN_URLS: Record<Platform, string> = {8 youtube: "https://www.youtube.com/",9 reddit: "https://www.reddit.com/login",10 facebook: "https://www.facebook.com/",11 instagram: "https://www.instagram.com/",12 tiktok: "https://www.tiktok.com/login",13 x: "https://x.com/login",14 linkedin: "https://www.linkedin.com/login",15 threads: "https://www.threads.net/login",16};1718/** Session cookies whose presence means "the human is logged in" (read-only check; values are never logged). */19export const AUTH_COOKIES: Record<Platform, RegExp> = {20 youtube: /^(SAPISID|LOGIN_INFO|__Secure-3PAPISID)$/,21 reddit: /^(reddit_session|token_v2)$/,22 facebook: /^c_user$/,23 instagram: /^(sessionid|ds_user_id)$/,24 tiktok: /^(sessionid|sid_tt)$/,25 x: /^(auth_token)$/,26 linkedin: /^(li_at)$/,27 threads: /^(sessionid|ds_user_id)$/,28};2930export async function isAuthenticated(session: SocialBrowserSession, platform: Platform): Promise<boolean> {31 const cookies = await session.getContext().cookies().catch(() => []);32 return cookies.some((c) => AUTH_COOKIES[platform].test(c.name) && c.value.length > 0);33}3435/**36 * Human-in-the-loop authentication (§7): open a headed browser on the platform and wait until the operator37 * has logged in — detected by the platform's session cookie, or by <Enter> when a TTY is attached.38 * No credentials are read, typed or stored by the crawler; the persistent profile is the only state kept.39 */40export async function interactiveLogin(opts: { platform: Platform; accountAlias: string; profilesDir: string; channel?: string; timeoutMin?: number }): Promise<boolean> {41 const session = new SocialBrowserSession({ ...opts, headless: false });42 await session.start();43 await session.navigate(LOGIN_URLS[opts.platform]);44 if (await isAuthenticated(session, opts.platform)) {45 log.info("profile already authenticated", { platform: opts.platform, alias: opts.accountAlias });46 await sleep(1500);47 await session.stop();48 return true;49 }50 log.info(`Browser is open on ${opts.platform}. Log in manually (and pass any 2FA yourself). I will detect the session automatically.`);51 const deadline = Date.now() + (opts.timeoutMin ?? 15) * 60_000;52 let enterPressed = false;53 const rl = process.stdin.isTTY ? readline.createInterface({ input: process.stdin, output: process.stdout }) : undefined;54 rl?.question("…or press <Enter> once you are logged in. ", () => (enterPressed = true));55 let ok = false;56 while (Date.now() < deadline) {57 if (session.getInfo().health === "crashed") break;58 if (await isAuthenticated(session, opts.platform)) {59 ok = true;60 break;61 }62 if (enterPressed) {63 ok = await isAuthenticated(session, opts.platform);64 break;65 }66 await sleep(2000);67 }68 rl?.close();69 if (ok) {70 // Let the app finish writing its storage, then land on the home page so the profile starts in a clean state.71 await sleep(4000);72 await session.navigate(LOGIN_URLS[opts.platform].replace(/\/login.*$/, "/")).catch(() => {});73 await sleep(2500);74 }75 const page = session.getPage();76 log.info(ok ? "profile saved — authenticated" : "no authenticated session detected (timeout)", { platform: opts.platform, alias: opts.accountAlias, url: page.url(), profile: session.profilePath });77 await session.stop();78 return ok;79}80