SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
3.4 KB · 80 lines typescript
Raw Blame History
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