// WHERE THE X FETCHERS' LOGIN COMES FROM — one setting, `social.x.cookieSource` // (release 16 slice XL). // // "browser" — the operator's everyday browser, named by `cookiesFromBrowser` // (the same spec yt-dlp reads). gallery-dl is handed // `--cookies-from-browser ` and reads the browser's store // itself on every run; the other X paths read the same store // through xCookiesFromBrowser (xBrowserLogin.ts). Nothing expires // while the operator stays logged in to x.com there. // "profile" — the session broker's persistent profile (xSessionBroker.ts): // "Connect X account" once, then the jar it exports. // // THE DEFAULT IS RESOLVED AT READ TIME, never stored: with no // `social.x.cookieSource` in settings.json, the source is "browser" when // `cookiesFromBrowser` is set and no profile is connected (no exported jar // carrying an auth_token), else "profile". Choosing a source stores it, and a // stored choice always wins. // // Pure and client-safe — no node imports. The settings schema, the fetch // controller and the editor's Settings section all read it. export type XCookieSource = "browser" | "profile"; export const X_COOKIE_SOURCES: readonly XCookieSource[] = ["browser", "profile"]; export function isXCookieSource(v: unknown): v is XCookieSource { return v === "browser" || v === "profile"; } // WHERE X POSTS MAY APPEAR — `social.x.visibility` (release 17 slice XP). // "public" — the default (absent): an X channel's posts are built into every // site that has the channel, as every other channel's are. // "private" — an X channel's posts are left out of every PUBLIC site build and // built only into PRIVATE sites (`site.json` `audience`). Nothing // on disk changes, and fetching does not; flipping back is a // rebuild. The rule itself is lib/postsVisibility.ts. export type XPostsVisibility = "public" | "private"; export const X_POSTS_VISIBILITIES: readonly XPostsVisibility[] = ["public", "private"]; export function isXPostsVisibility(v: unknown): v is XPostsVisibility { return v === "public" || v === "private"; } // The `social` block of settings.json. Only X has settings today; the block is // per platform so a second one does not need a second top-level key. export type XSocialSettings = { // Absent = the read-time default above. cookieSource?: XCookieSource; // Absent = "public". visibility?: XPostsVisibility; }; export type SocialSettings = { x: XSocialSettings; }; // Total over `unknown`, as every settings coercion is: anything that is not a // known source or visibility reads as absent (the default), and unknown keys // are dropped. export function sanitizeSocial(value: unknown): SocialSettings { const r = (value && typeof value === "object" && !Array.isArray(value) ? value : {}) as Record; const x = (r.x && typeof r.x === "object" && !Array.isArray(r.x) ? r.x : {}) as Record; return { x: { ...(isXCookieSource(x.cookieSource) ? { cookieSource: x.cookieSource } : {}), ...(isXPostsVisibility(x.visibility) ? { visibility: x.visibility } : {}), }, }; } export type ResolvedXCookieSource = { source: XCookieSource; // True when settings.json names the source; false when the read-time default // decided it. chosen: boolean; // The browser spec the "browser" source reads (`cookiesFromBrowser`, a // channel's own when it has one), or undefined when none is set. browserSpec?: string; }; export function resolveXCookieSource(input: { stored?: XCookieSource; browserSpec?: string; // The session broker's profile holds a login: its exported jar carries an // auth_token (readXSessionStatus().looksAuthenticated). profileConnected: boolean; }): ResolvedXCookieSource { const browserSpec = input.browserSpec?.trim() || undefined; if (isXCookieSource(input.stored)) { return { source: input.stored, chosen: true, browserSpec }; } return { source: browserSpec && !input.profileConnected ? "browser" : "profile", chosen: false, browserSpec, }; } // A browser cookie spec, as `cookiesFromBrowser` holds it. yt-dlp's syntax is // BROWSER[+KEYRING][:PROFILE][::CONTAINER]; gallery-dl's adds /DOMAIN after the // browser name. Both programs get the spec verbatim — this parse is only for // the readers here, which need the browser, the profile and the container. export type BrowserSpec = { browser: string; domain?: string; keyring?: string; profile?: string; container?: string; }; const SPEC_RE = /^(?[^/+:]+)(?:\/(?[^+:]+))?(?:\s*\+\s*(?[^:]+))?(?:\s*:\s*(?!:)(?.+?))?(?:\s*::\s*(?.+))?$/; export function parseBrowserSpec(spec: string): BrowserSpec | null { const m = SPEC_RE.exec(spec.trim()); if (!m?.groups) return null; const g = m.groups; const out: BrowserSpec = { browser: g.browser.trim().toLowerCase() }; if (g.domain) out.domain = g.domain.trim(); if (g.keyring) out.keyring = g.keyring.trim(); if (g.profile) out.profile = g.profile.trim(); if (g.container) out.container = g.container.trim(); return out; } // The browsers whose cookie store this repo reads itself (xBrowserLogin.ts). // Every other browser gallery-dl and yt-dlp support (the Chromium family keeps // its cookies encrypted with a desktop-keyring key) is read by those programs // alone; the Playwright fallback and the Settings check say so instead. export const SELF_READ_BROWSERS: readonly string[] = ["firefox"]; // The label the Settings section and the logs use for a source. export function xCookieSourceLabel(r: ResolvedXCookieSource): string { if (r.source === "browser") { return r.browserSpec ? `Browser login (${r.browserSpec})` : "Browser login (no cookiesFromBrowser set)"; } return "Connected profile"; } // A resolved source with its label: what the Settings section renders. export type XCookieSourceView = ResolvedXCookieSource & { label: string }; export function xCookieSourceView(r: ResolvedXCookieSource): XCookieSourceView { return { ...r, label: xCookieSourceLabel(r) }; }