Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 69e9f1b18e0d484986efcd6bf92890dae6cfec2f
parent df945a23403c034c7e1f2a9afada915a414d77ad
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 11:27:44 -0400

editor: the X account session shows the login source in use, checks whether an X login is visible in it, and lets the operator choose it

- XSessionSection: a "Login source" select (Automatic / Browser login /
  Connected profile, accessible name "x cookie source"), the source in use
  ("x cookie source in use"), a Check button ("check x login") whose result is
  one line ("x login status"); the Connect text says the window is the
  operator's own browser, that Google's sign-in may still refuse an embedded
  browser, and that X's password login is the reliable path. Every earlier
  accessible name is unchanged.
- xSessionActions: checkXLoginAction, setXCookieSourceAction (through
  saveSettings); the session actions return the source in use too; Connect
  logs which browser opened.
- e2e x-session.spec: the default source with nothing set; the browser source
  over a fixture Firefox store, Check's line, the select persisting across a
  reload, and back to automatic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/app/settings/components/XSessionSection.tsx | 174++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Meditor/app/settings/page.tsx | 11+++++++++--
Meditor/app/settings/xSessionActions.ts | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Meditor/e2e/x-session.spec.ts | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 305 insertions(+), 39 deletions(-)

diff --git a/editor/app/settings/components/XSessionSection.tsx b/editor/app/settings/components/XSessionSection.tsx @@ -1,44 +1,108 @@ "use client"; -// "Connect X account" — the operator-facing half of the X session broker. +// The X account session — where the X fetchers' login comes from, whether a +// login is visible there, and the operator-facing half of the session broker. // -// Why this exists: gallery-dl is the primary X fetcher and its worst flaw is -// that X cookies expire within days. A persistent, logged-in browser profile -// removes that problem without touching the fetch path at all — the fetcher -// just reads a jar this keeps fresh. +// Two sources (release 16 slice XL, common/social/xCookieSource.ts): +// - the operator's everyday browser (`cookiesFromBrowser`): gallery-dl reads +// its X login on every fetch, so nothing expires while the operator stays +// logged in to x.com there — no window at all; +// - a profile connected here once: a persistent, logged-in browser profile +// the fetcher re-exports fresh cookies from, because X cookies otherwise +// expire within days. -import { useState, useTransition } from "react"; +import { useEffect, useState, useTransition } from "react"; import { + checkXLoginAction, clearXSessionAction, connectXAccountAction, refreshXCookiesAction, + setXCookieSourceAction, + type XSessionActionResult, } from "../xSessionActions"; import type { XSessionStatus } from "yt-dlp-transcript-common/social/xSessionBroker"; +import type { XLoginStatus } from "yt-dlp-transcript-common/social/xBrowserLogin"; +import { + resolveXCookieSource, + xCookieSourceLabel, + type XCookieSourceView, +} from "yt-dlp-transcript-common/social/xCookieSource"; -export function XSessionSection({ initial }: { initial: XSessionStatus }) { +export function XSessionSection({ + initial, + initialSource, +}: { + initial: XSessionStatus; + initialSource: XCookieSourceView; +}) { const [status, setStatus] = useState<XSessionStatus>(initial); + const [source, setSource] = useState<XCookieSourceView>(initialSource); + const [login, setLogin] = useState<XLoginStatus | null>(null); const [error, setError] = useState<string | null>(null); const [note, setNote] = useState<string | null>(null); + // Two transitions: the session actions (Connect waits for the window to + // close) and the source controls (Check, the source select). const [pending, startTransition] = useTransition(); + const [checking, startChecking] = useTransition(); + + // A save of cookiesFromBrowser above (or another tab's choice) re-renders the + // page with a new resolved source; take it. + useEffect(() => { + setSource(initialSource); + // Keyed on the values, not the object: every server render makes a new one. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialSource.source, initialSource.chosen, initialSource.browserSpec]); - const run = ( - fn: () => Promise< - { ok: true; status: XSessionStatus } | { ok: false; error: string } - >, - okNote: string, - ) => + const run = (fn: () => Promise<XSessionActionResult>, okNote: (r: { browser?: string }) => string) => startTransition(async () => { setError(null); setNote(null); const res = await fn(); if (res.ok) { setStatus(res.status); - setNote(okNote); + setSource(res.source); + setLogin(null); + setNote(okNote(res)); + } else { + setError(res.error); + } + }); + + const check = () => + startChecking(async () => { + setError(null); + setNote(null); + const res = await checkXLoginAction(); + if (res.ok) { + setLogin(res.login); + setSource(res.login); } else { setError(res.error); } }); + const choose = (choice: string) => + startChecking(async () => { + setError(null); + setNote(null); + const res = await setXCookieSourceAction(choice); + if (res.ok) { + setSource(res.source); + setLogin(null); + setNote("Login source saved."); + } else { + setError(res.error); + } + }); + + // What "Automatic" resolves to right now, for its option's text. + const automatic = xCookieSourceLabel( + resolveXCookieSource({ + browserSpec: source.browserSpec, + profileConnected: status.looksAuthenticated, + }), + ); + return ( <section data-x-session="" @@ -62,12 +126,68 @@ export function XSessionSection({ initial }: { initial: XSessionStatus }) { </div> <p className="text-xs text-muted-foreground"> - X post archiving needs a logged-in session, and X cookies expire within - days. Connecting an account once stores a browser profile on this host; - the fetcher then re-exports fresh cookies from it automatically, so the - session stops being the thing that breaks. + X post archiving needs a logged-in session, from one of two sources.{" "} + <strong>Browser login</strong>: your everyday browser, named by{" "} + <code>cookiesFromBrowser</code> above — gallery-dl reads its X login on + every fetch, so nothing expires while you stay logged in to x.com there, + and no window is needed. <strong>Connected profile</strong>: a browser + profile connected here once; the fetcher re-exports fresh cookies from + it, because X cookies otherwise expire within days. + </p> + + <div className="flex flex-wrap items-center gap-2"> + <label htmlFor="x-cookie-source" className="text-sm"> + Login source + </label> + <select + id="x-cookie-source" + aria-label="x cookie source" + value={source.chosen ? source.source : "auto"} + onChange={(e) => choose(e.target.value)} + disabled={pending || checking} + className="rounded-md border border-border bg-background px-2 py-1 text-sm disabled:opacity-50" + > + <option value="auto">Automatic — now: {automatic}</option> + <option value="browser"> + Browser login{source.browserSpec ? ` (${source.browserSpec})` : ""} + </option> + <option value="profile">Connected profile</option> + </select> + <button + type="button" + onClick={check} + disabled={pending || checking} + aria-label="check x login" + className="rounded-md border border-border px-3 py-1 text-sm hover:bg-muted disabled:opacity-50" + > + {checking ? "Checking…" : "Check"} + </button> + </div> + + <p aria-label="x cookie source in use" className="text-xs text-muted-foreground"> + In use: {source.label} + {source.chosen ? "" : " (automatic)"} </p> + {login && ( + <p + aria-label="x login status" + className={ + "text-xs " + + (login.authTokenVisible === true + ? "text-success" + : login.authTokenVisible === false + ? "text-destructive" + : "text-muted-foreground") + } + > + {login.summary}{" "} + <span className="text-muted-foreground"> + (checked {new Date(login.checkedAt).toLocaleTimeString()}) + </span> + </p> + )} + {status.cookiesUpdatedAt && ( <p className="text-xs text-muted-foreground"> Cookies last exported:{" "} @@ -79,7 +199,9 @@ export function XSessionSection({ initial }: { initial: XSessionStatus }) { <button type="button" onClick={() => - run(connectXAccountAction, "Connected. Cookies exported.") + run(connectXAccountAction, (r) => + `Connected${r.browser ? ` with ${r.browser}` : ""}. Cookies exported.`, + ) } disabled={pending} aria-label="connect x account" @@ -89,7 +211,7 @@ export function XSessionSection({ initial }: { initial: XSessionStatus }) { </button> <button type="button" - onClick={() => run(refreshXCookiesAction, "Cookies refreshed.")} + onClick={() => run(refreshXCookiesAction, () => "Cookies refreshed.")} disabled={pending || !status.hasProfile} aria-label="refresh x cookies" className="rounded-md border border-border px-3 py-1 text-sm hover:bg-muted disabled:opacity-50" @@ -98,7 +220,7 @@ export function XSessionSection({ initial }: { initial: XSessionStatus }) { </button> <button type="button" - onClick={() => run(clearXSessionAction, "Session cleared.")} + onClick={() => run(clearXSessionAction, () => "Session cleared.")} disabled={pending || !status.hasProfile} aria-label="clear x session" className="rounded-md border border-border px-3 py-1 text-sm hover:bg-muted disabled:opacity-50" @@ -109,9 +231,13 @@ export function XSessionSection({ initial }: { initial: XSessionStatus }) { <p className="text-xs text-muted-foreground"> Connecting opens a browser window <strong>on the machine running the - editor</strong>; complete the login (2FA and captcha included), then - close the window. The login is never automated — that is what gets - accounts flagged. + editor</strong>: your own Chromium or Chrome when one is installed (else + Playwright&apos;s bundled Chromium), without the automation signals. + Complete the login (2FA and captcha included), then close the window. + Google&apos;s sign-in may still refuse an embedded browser (&ldquo;this + browser or app may not be secure&rdquo;) — <strong>X&apos;s own + password login is the reliable path</strong>. The login is never + automated — that is what gets accounts flagged. </p> {note && ( diff --git a/editor/app/settings/page.tsx b/editor/app/settings/page.tsx @@ -3,6 +3,8 @@ import Link from "next/link"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { readXSessionStatus } from "yt-dlp-transcript-common/social/xSessionBroker"; +import { resolveXCookieSourceFor } from "yt-dlp-transcript-common/social/xBrowserLogin"; +import { xCookieSourceView } from "yt-dlp-transcript-common/social/xCookieSource"; import { SettingsForm } from "./components/SettingsForm"; import { XSessionSection } from "./components/XSessionSection"; @@ -29,8 +31,13 @@ export default async function SettingsPage() { ["galleryDlBin", paths.galleryDlBin], ]; - // The X session broker's current state (see xSessionActions.ts). + // The X session broker's current state and the X login source in use (see + // xSessionActions.ts). The source is resolved here, not read: with no stored + // choice it depends on cookiesFromBrowser and the profile. const xSession = await readXSessionStatus(paths); + const xSource = xCookieSourceView( + await resolveXCookieSourceFor(paths, settings), + ); return ( <div className="flex flex-col gap-8"> @@ -86,7 +93,7 @@ export default async function SettingsPage() { </section> <section className="flex flex-col gap-3 border-t border-border pt-6"> - <XSessionSection initial={xSession} /> + <XSessionSection initial={xSession} initialSource={xSource} /> </section> <section className="flex flex-col gap-3 border-t border-border pt-6"> diff --git a/editor/app/settings/xSessionActions.ts b/editor/app/settings/xSessionActions.ts @@ -1,16 +1,21 @@ "use server"; -// Server actions for the X session broker (common/social/xSessionBroker.ts). +// Server actions for the X session broker (common/social/xSessionBroker.ts) +// and the X login source (common/social/xCookieSource.ts, release 16 slice XL). // // "Connect X account" opens a HEADED browser on the editor host so the operator // can log in by hand — password, 2FA and captcha included. That is the whole // point: automating an X login is what gets accounts flagged, and a human doing -// it once produces a profile that stays valid. +// it once produces a profile that stays valid. The window is the operator's own +// Chromium or Chrome when one is installed, without the automation signals +// (common/social/xBrowser.ts). // // Because it opens a window on the SERVER's display, this is deliberately an // explicit operator action and never something a fetch triggers on its own. +import { revalidatePath } from "next/cache"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { clearXSession, connectXAccount, @@ -18,28 +23,49 @@ import { refreshXCookies, type XSessionStatus, } from "yt-dlp-transcript-common/social/xSessionBroker"; +import { + readXLoginStatus, + resolveXCookieSourceFor, + type XLoginStatus, +} from "yt-dlp-transcript-common/social/xBrowserLogin"; +import { + isXCookieSource, + xCookieSourceView, + type XCookieSourceView, +} from "yt-dlp-transcript-common/social/xCookieSource"; +import { saveSettings } from "./saveSettings"; +// Every session action returns the source in use beside the profile's status: +// connecting or forgetting a profile can move the read-time default. export type XSessionActionResult = - | { ok: true; status: XSessionStatus } + | { ok: true; status: XSessionStatus; source: XCookieSourceView; browser?: string } | { ok: false; error: string }; +async function sourceNow(): Promise<XCookieSourceView> { + return xCookieSourceView(await resolveXCookieSourceFor(getPaths(), getSettings())); +} + export async function xSessionStatusAction(): Promise<XSessionStatus> { return readXSessionStatus(getPaths()); } export async function connectXAccountAction(): Promise<XSessionActionResult> { try { - const status = await connectXAccount(getPaths()); + const { browser, ...status } = await connectXAccount(getPaths(), { + // Which browser opened, and any fallback, in the editor's log. + onLog: (line) => console.log(`[x-session] ${line}`), + }); if (!status.looksAuthenticated) { return { ok: false, error: - "The browser closed without a logged-in X session (no auth_token " + - "cookie was exported). Try again and complete the login before " + - "closing the window.", + `The browser (${browser}) closed without a logged-in X session (no ` + + "auth_token cookie was exported). Try again and complete the login " + + "before closing the window — X's own password login is the reliable " + + "path where Google's sign-in refuses.", }; } - return { ok: true, status }; + return { ok: true, status, source: await sourceNow(), browser }; } catch (e) { return { ok: false, error: (e as Error).message }; } @@ -47,7 +73,10 @@ export async function connectXAccountAction(): Promise<XSessionActionResult> { export async function refreshXCookiesAction(): Promise<XSessionActionResult> { try { - return { ok: true, status: await refreshXCookies(getPaths()) }; + const status = await refreshXCookies(getPaths(), { + onLog: (line) => console.log(`[x-session] ${line}`), + }); + return { ok: true, status, source: await sourceNow() }; } catch (e) { return { ok: false, error: (e as Error).message }; } @@ -56,8 +85,51 @@ export async function refreshXCookiesAction(): Promise<XSessionActionResult> { export async function clearXSessionAction(): Promise<XSessionActionResult> { try { await clearXSession(getPaths()); - return { ok: true, status: await readXSessionStatus(getPaths()) }; + return { + ok: true, + status: await readXSessionStatus(getPaths()), + source: await sourceNow(), + }; + } catch (e) { + return { ok: false, error: (e as Error).message }; + } +} + +// THE CHECK: the source in use, whether an X login (an auth_token for x.com) +// is visible in it, and when it was last seen. Reads the browser's cookie store +// read-only (a private copy) or the profile's exported jar. +export type XLoginCheckResult = + | { ok: true; login: XLoginStatus } + | { ok: false; error: string }; + +export async function checkXLoginAction(): Promise<XLoginCheckResult> { + try { + return { ok: true, login: await readXLoginStatus(getPaths(), getSettings()) }; + } catch (e) { + return { ok: false, error: (e as Error).message }; + } +} + +// THE CHOICE: "browser", "profile", or "auto" — no stored choice, the default +// resolved at read time. Writes `social.x.cookieSource` through the one +// settings writer and nothing else. +export type XSourceChoiceResult = + | { ok: true; source: XCookieSourceView } + | { ok: false; error: string }; + +export async function setXCookieSourceAction( + choice: string, +): Promise<XSourceChoiceResult> { + if (choice !== "auto" && !isXCookieSource(choice)) { + return { ok: false, error: `Unknown X login source "${choice}".` }; + } + try { + await saveSettings({ + social: { x: choice === "auto" ? {} : { cookieSource: choice } }, + }); } catch (e) { return { ok: false, error: (e as Error).message }; } + revalidatePath("/settings"); + return { ok: true, source: await sourceNow() }; } diff --git a/editor/e2e/x-session.spec.ts b/editor/e2e/x-session.spec.ts @@ -1,9 +1,15 @@ -// The X session broker UI (common/social/xSessionBroker.ts). The headed login +// The X session broker UI (common/social/xSessionBroker.ts) and the X login +// source (common/social/xCookieSource.ts, release 16 slice XL). The headed login // is never driven here — it would open a real browser and hit x.com, which the -// suite must never do. +// suite must never do. The browser source reads a FIXTURE Firefox store written +// under the test corpus, never a real browser profile. import { test, expect } from "@playwright/test"; -import { resetData } from "./helpers"; +import { readJson, resetData, resolvePath, writeSettings } from "./helpers"; +// By relative path: a runtime import of the package specifier does not resolve +// under playwright's loader (digest.spec.ts says why). +import { writeFirefoxCookieStore } from "../../common/social/__fixtures__/firefoxCookieStore"; + test("settings page renders the X session section", async ({ page }) => { await resetData("empty"); await page.goto("/settings"); @@ -13,4 +19,59 @@ test("settings page renders the X session section", async ({ page }) => { // never click it (it would open a real browser window and hit x.com). await expect(page.getByLabel("connect x account")).toBeVisible(); await expect(page.getByLabel("refresh x cookies")).toBeDisabled(); + // No cookiesFromBrowser in the fixture settings and no profile: the + // read-time default is the profile. + await expect(page.getByLabel("x cookie source", { exact: true })).toHaveValue("auto"); + await expect(page.getByLabel("x cookie source in use")).toHaveText( + "In use: Connected profile (automatic)", + ); +}); + +test("the login source select persists, and Check shows a status line", async ({ page }) => { + await resetData("empty"); + const profileDir = resolvePath("test-transcripts/firefox-fixture/xl.default-release"); + await writeFirefoxCookieStore(profileDir, [ + { + host: ".x.com", + name: "auth_token", + value: "fixture-not-a-real-token", + lastAccessed: Date.UTC(2026, 9, 1, 8, 30, 0) * 1000, + }, + { host: ".example.com", name: "session", value: "not-x" }, + ]); + const spec = `firefox:${profileDir}`; + await writeSettings({ cookiesFromBrowser: spec }); + + await page.goto("/settings"); + const section = page.locator("[data-x-session]"); + const select = page.getByLabel("x cookie source", { exact: true }); + const inUse = page.getByLabel("x cookie source in use"); + + // cookiesFromBrowser set, no profile connected: the browser, by default. + await expect(select).toHaveValue("auto"); + await expect(inUse).toHaveText(`In use: Browser login (${spec}) (automatic)`); + + await page.getByLabel("check x login").click(); + await expect(page.getByLabel("x login status")).toContainText( + "An X login is visible in firefox (its auth_token last used 2026-10-01 08:30:00 UTC).", + ); + + // Choose the profile: stored, shown, and still chosen after a reload. + await select.selectOption("profile"); + await expect(section.getByRole("status")).toHaveText("Login source saved."); + await expect(inUse).toHaveText("In use: Connected profile"); + expect((await readJson<{ social?: unknown }>("test-settings.json")).social).toEqual({ + x: { cookieSource: "profile" }, + }); + + await page.reload(); + await expect(select).toHaveValue("profile"); + await expect(inUse).toHaveText("In use: Connected profile"); + await page.getByLabel("check x login").click(); + await expect(page.getByLabel("x login status")).toContainText("No profile is connected."); + + // Back to automatic: nothing stored, the default decides again. + await select.selectOption("auto"); + await expect(inUse).toHaveText(`In use: Browser login (${spec}) (automatic)`); + expect((await readJson<{ social?: unknown }>("test-settings.json")).social).toEqual({ x: {} }); });