Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273// login — keep Iris signed in to Fuser, with no human in the room.//// await ensureSignedIn(cdp, { email: "iris@fuser.studio" })//// Runs in `setup`, OFF CAMERA, before every take. It is a no-op in the normal// case (already signed in) and a full email-OTP login when it is not — so// "a video is requested" now implies "make sure we are logged in first", which// is the only version of this that survives being left alone.//// WHY THIS HAS TO EXIST. Fuser's session cookie is not written to disk: panda's// Chrome `Cookies` DB is months stale, so the session lives only in the running// browser's memory. Quit Chrome — a crash, a reboot, a `pkill` from// prep-chrome — and Iris is logged out. Before this, a person had to walk over// and type a code out of her inbox, which is not a thing an unattended renderer// can do at 4am.//// THE ROUTE IS EMAIL OTP, not Google. `emailOTP()` is enabled server-side// (services/auth/auth.ts) and the login dialog exposes it: email → Continue →// a six-digit code is mailed → type it → in. Google's OAuth screen is a// third-party page with its own bot defences; the OTP path is Fuser's own front// door, and Iris owns the mailbox it mails.//// SELECTORS ARE LOCALE-PROOF ON PURPOSE. This runs before the screenplay's// setLocale, so the dialog may still be in whatever language the last take used.// Nothing here matches on words — the two inputs are found by their placeholders// (`hello@fuser.studio` and `123456`, both hardcoded in LoginDialog.tsx and// never translated), and the submit button is found by where it sits (the last// button in the dialog), not by whether it reads "Continue" or "继续".//// The mailbox half is lib/otp-mail.py — read its header for why a UIDNEXT mark,// and not a timestamp, is what proves a code is ours.
import { execFile } from "node:child_process";import { existsSync, readFileSync } from "node:fs";import { homedir } from "node:os";import { dirname, join } from "node:path";import { fileURLToPath } from "node:url";import { promisify } from "node:util";
const run = promisify(execFile);const HERE = dirname(fileURLToPath(import.meta.url));const OTP_MAIL = join(HERE, "otp-mail.py");const PYTHON = process.env.CAPTUTOR_PYTHON || "/usr/bin/python3";
export const WORKSPACE = "https://app.fuser.studio/w/me";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// ── selectors ──────────────────────────────────────────────────────────────// (See the header: placeholders, not labels — they are the same in every locale.)const EMAIL_FIELD = 'input[placeholder^="hello@"]';const OTP_FIELD = 'input[placeholder="123456"]';const SIGNED_IN = '[aria-label="Create blank project"]';
// The gradient submit button. It is "Continue", then "Login", then in Chinese// something else again — but it is always the last visible button in the dialog,// and in the OTP step it is the only one (the Google button unmounts).const SUBMIT = `js=(() => { const bs = [...document.querySelectorAll('button')] .filter((b) => b.offsetParent !== null && b.getBoundingClientRect().width > 0); return bs[bs.length - 1] || null;})()`;
const SUBMIT_ENABLED = `(() => { const bs = [...document.querySelectorAll('button')] .filter((b) => b.offsetParent !== null && b.getBoundingClientRect().width > 0); const b = bs[bs.length - 1]; return b && !b.disabled && b.getAttribute('aria-disabled') !== 'true';})()`;
/// Where the login dialog has landed. One eval, no guessing.const PAGE_STATE = `(() => { if (document.querySelector(${JSON.stringify(SIGNED_IN)})) return "in"; if (document.querySelector(${JSON.stringify(OTP_FIELD)})) return "otp"; if (document.querySelector(${JSON.stringify(EMAIL_FIELD)})) return "email"; const txt = document.body ? document.body.innerText : ""; // Turnstile expires if the dialog is left sitting, and the dialog then shows // an "Oops!" that no amount of typing gets you out of. It is a RELOAD state, // not a failure — this is exactly how we found panda: parked on // "We were unable to verify your humanity" with nobody there to press Refresh. if (/Oops!|verify your humanity|Try again|Refresh/i.test(txt)) return "stuck"; if (/Login to Fuser|Sign in|Sign up to Fuser/i.test(txt)) return "email"; return "unknown";})()`;
/// The app password, from panda's ~/.config/captutor/iris.json (chmod 600) or/// the environment. Never from the repo: this directory is inside the vault, and/// a secret in a file that gets rsynced to two office minis is a secret twice.export function irisCredentials() { const path = process.env.CAPTUTOR_IRIS_CONFIG || join(homedir(), ".config", "captutor", "iris.json"); const file = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {}; const appPassword = process.env.CAPTUTOR_IRIS_APP_PASSWORD || file.appPassword; if (!appPassword) { throw new Error( `no Gmail app password for Iris.\n` + ` put it in ${path} (chmod 600):\n` + ` { "email": "iris@fuser.studio", "appPassword": "…" }\n` + ` or set CAPTUTOR_IRIS_APP_PASSWORD.\n` + ` the source of truth is vault/fuser/iris-credentials.md`); } const email = file.email || "iris@fuser.studio"; return { email, appPassword, // never logged, never printed imapHost: file.imapHost || "imap.gmail.com", imapPort: String(file.imapPort || 993), imapUser: file.imapUser || email, };}
/// Talk to the mailbox. The password goes in through the ENVIRONMENT, not argv:/// argv is readable by every process on the machine (`ps`), and panda is shared.async function mail(creds, args, { timeoutMs = 180000 } = {}) { const { stdout } = await run(PYTHON, [OTP_MAIL, ...args], { timeout: timeoutMs, encoding: "utf8", env: { ...process.env, IMAP_HOST: creds.imapHost, IMAP_PORT: creds.imapPort, IMAP_USER: creds.imapUser, IMAP_PASSWORD: creds.appPassword, }, }); return JSON.parse(stdout.trim().split("\n").pop());}
/// A trusted click, with no drawn cursor.////// cursor.mjs's clickOn glides a visible pointer across the screen because the/// viewer has to see where the click landed. Nobody is watching this one — it/// happens before `reel` rolls — so it skips the choreography and just presses.async function tap(cdp, selector) { const { x, y } = await cdp.center(selector); await cdp.mouse("mouseMoved", x, y); await cdp.mouse("mousePressed", x, y); await cdp.mouse("mouseReleased", x, y); await sleep(120);}
async function fill(cdp, selector, text) { await tap(cdp, selector); await cdp.type(text);}
/// Is the workspace on screen? Cheap, and the answer in the normal case.async function signedIn(cdp) { return !!(await cdp.eval(`!!document.querySelector(${JSON.stringify(SIGNED_IN)})`));}
/// Wait for the app to commit to a state. A React app that is still mounting is/// neither signed in nor signed out, and treating that moment as "logged out"/// would mail Iris a code every single run.async function settle(cdp, { timeoutMs = 15000 } = {}) { const deadline = Date.now() + timeoutMs; let last = "unknown"; while (Date.now() < deadline) { last = await cdp.eval(PAGE_STATE).catch(() => "unknown"); if (last !== "unknown") return last; await sleep(300); } return last;}
/** * Make sure the filming browser is signed in as Iris. Returns * `{ signedIn: true, already: bool }`, or throws with what it saw. * * Safe to call before every take: when the session is alive this costs one eval. */export async function ensureSignedIn(cdp, { email, timeoutMs = 240000, workspace = WORKSPACE, log = (m) => console.log(` ${m}`),} = {}) { if (await signedIn(cdp)) return { signedIn: true, already: true };
const creds = irisCredentials(); const who = email || creds.email;
// Two attempts. The first can lose to an expired Turnstile — the widget dies // quietly on a page that has been open for hours, and the only cure is a fresh // load. A second attempt on a reloaded page is not a retry-until-it-works loop; // it is the one failure mode we know the shape of. for (let attempt = 1; attempt <= 2; attempt++) { // Always start from a fresh page: it clears a stuck "Oops!", and Turnstile // begins solving the moment the dialog mounts. await cdp.nav(workspace); let state = await settle(cdp); if (state === "in") return { signedIn: true, already: true };
if (state === "stuck" || state === "unknown") { log(`login dialog was ${state} — reloading`); await cdp.nav(workspace); state = await settle(cdp); if (state === "in") return { signedIn: true, already: true }; }
log(`signing in as ${who} (attempt ${attempt}/2)`);
if (state === "email") { await cdp.waitFor(`document.querySelector(${JSON.stringify(EMAIL_FIELD)})`); await fill(cdp, EMAIL_FIELD, who);
// Turnstile gates the button, not the field: `isDisabled` includes // `!turnstileToken`. So the wait below is really "wait for Cloudflare to // decide we are human" — it is usually a couple of seconds, and if it // never comes the dialog will say so and we reload. try { await cdp.waitFor(SUBMIT_ENABLED, { timeoutMs: 45000 }); } catch { log("Turnstile never released the button — retrying on a fresh page"); continue; }
// MARK THE MAILBOX FIRST. Everything after this instant is ours; anything // already in there is somebody else's code, or an old one of ours. const after = await mail(creds, ["mark"], { timeoutMs: 60000 });
await tap(cdp, SUBMIT);
// The OTP field appearing IS the receipt that the mail was sent — the // dialog only swaps to it when `account.login` resolved true. try { await cdp.waitFor( `document.querySelector(${JSON.stringify(OTP_FIELD)})`, { timeoutMs: 30000 }); } catch { const txt = await cdp.eval("document.body.innerText").catch(() => ""); log(`Continue did not open the code field — page says: ${JSON.stringify(txt.slice(0, 160))}`); continue; } log("code requested — watching the mailbox");
const hit = await mail(creds, [ "wait", "--after", JSON.stringify(after), "--wait", "150", ], { timeoutMs: 170000 }); if (hit.error) throw new Error(`OTP mail: ${hit.error}`); log(`code arrived in ${hit.box} — ${JSON.stringify(hit.subject.replace(/\d{6}/, "······"))}`);
await fill(cdp, OTP_FIELD, hit.code); await cdp.waitFor(SUBMIT_ENABLED, { timeoutMs: 10000 }); await tap(cdp, SUBMIT); } else if (state === "otp") { // A half-finished dialog from an earlier run. We have no code for it // (the one that was mailed is gone), so start the whole thing over. log("dialog was waiting on a code we never got — starting over"); await cdp.nav(workspace); continue; }
// handleTokenSubmit reloads the page on success, so the workspace mounting // is the proof — not the dialog closing. try { await cdp.waitFor( `document.querySelector(${JSON.stringify(SIGNED_IN)})`, { timeoutMs: Math.min(60000, timeoutMs) }); log(`signed in as ${who}`); return { signedIn: true, already: false }; } catch { const txt = await cdp.eval("document.body.innerText").catch(() => ""); if (/incorrect|Failed to verify/i.test(txt)) { log("Fuser rejected the code — asking for a new one"); continue; // a stale code slipped through, or it expired mid-type } throw new Error(`login did not land on the workspace. page says: ${txt.slice(0, 200)}`); } }
throw new Error("could not sign in to Fuser after 2 attempts");}