l / server.js
KiWA001's picture
Handle proxy TLS MITM during validation
4ba3d15
Raw
History Blame Contribute Delete
127 kB
import express from "express";
import { WebSocketServer } from "ws";
import { chromium } from "playwright";
import { v4 as uuidv4 } from "uuid";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import dotenv from "dotenv";
import { createClient } from "@supabase/supabase-js";
import crypto from "crypto";
import { createProxyManager } from "./proxy-manager.js";
dotenv.config();
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
});
process.on("unhandledRejection", (err) => {
console.error("Unhandled rejection:", err);
});
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || "127.0.0.1";
const SESSIONS_DIR = path.join(__dirname, "sessions");
const STREAM_JPEG_QUALITY = Math.min(100, Math.max(10, Number(process.env.STREAM_JPEG_QUALITY || 90)));
const STREAM_INTERVAL_MS = Math.min(1000, Math.max(60, Number(process.env.STREAM_INTERVAL_MS || 120)));
const ADMIN_STREAM_JPEG_QUALITY = Math.min(
100,
Math.max(10, Number(process.env.ADMIN_STREAM_JPEG_QUALITY || 60))
);
const ADMIN_EVERY_NTH_FRAME = Math.min(10, Math.max(1, Number(process.env.ADMIN_EVERY_NTH_FRAME || 2)));
const STREAM_MODE = "screencast";
const DISABLE_EMAIL_NEXT_FREEZE = ["1", "true", "yes"].includes(
String(process.env.DISABLE_EMAIL_NEXT_FREEZE || "").toLowerCase()
);
const BLANK_GUARD_INTERVAL_MS = Math.min(10000, Math.max(500, Number(process.env.BLANK_GUARD_INTERVAL_MS || 1500)));
const SIGNIN_COPY_INTERVAL_MS = Math.min(10000, Math.max(500, Number(process.env.SIGNIN_COPY_INTERVAL_MS || 1500)));
const SIGNIN_COPY_OVERRIDE_TEXT = process.env.SIGNIN_COPY_OVERRIDE_TEXT || "Continue";
const MAX_CONCURRENT_SESSIONS = Math.max(1, Number(process.env.MAX_CONCURRENT_SESSIONS || 1));
const IDLE_TIMEOUT_MS = Math.min(3600000, Math.max(60000, Number(process.env.IDLE_TIMEOUT_MS || 300000)));
const HEARTBEAT_INTERVAL_MS = Math.min(60000, Math.max(2000, Number(process.env.HEARTBEAT_INTERVAL_MS || 8000)));
const HEARTBEAT_GRACE_MS = Math.min(
300000,
Math.max(5000, Number(process.env.HEARTBEAT_GRACE_MS || HEARTBEAT_INTERVAL_MS * 4))
);
const SELF_HEAL_ENABLED = process.env.SELF_HEAL_ENABLED !== "false";
const SELF_HEAL_INTERVAL_MS = Math.min(60000, Math.max(3000, Number(process.env.SELF_HEAL_INTERVAL_MS || 10000)));
const SELF_HEAL_STALE_FRAME_MS = Math.min(
300000,
Math.max(5000, Number(process.env.SELF_HEAL_STALE_FRAME_MS || 20000))
);
const SELF_HEAL_LOG_TABLE = process.env.SELF_HEAL_LOG_TABLE || "self_heal_logs";
const SELF_HEAL_BRAIN_ENABLED = process.env.SELF_HEAL_BRAIN_ENABLED !== "false";
const SELF_HEAL_BRAIN_COOLDOWN_MS = Math.min(
300000,
Math.max(5000, Number(process.env.SELF_HEAL_BRAIN_COOLDOWN_MS || 20000))
);
const MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions";
const MISTRAL_MODEL = "mistral-tiny";
const MISTRAL_API_KEY = "lzWRBwuWxwTGkihMcR4jCicHNpFGiKmA";
const BACKUPS_DIR = path.join(SESSIONS_DIR, "backups");
const PROFILES_DIR = path.join(SESSIONS_DIR, "profiles");
const MAX_IDB_RECORDS = Number(process.env.MAX_IDB_RECORDS || 0);
const DETACH_TTL_MS = Math.min(
3600000,
Math.max(IDLE_TIMEOUT_MS, Math.max(5000, Number(process.env.DETACH_TTL_MS || IDLE_TIMEOUT_MS)))
);
const SNAPSHOT_INTERVAL_MS = Math.min(3600000, Math.max(30000, Number(process.env.SNAPSHOT_INTERVAL_MS || 300000)));
const PERSISTENT_PROFILE = process.env.PERSISTENT_PROFILE !== "false";
const ADMIN_LOCK_VIEWPORT = process.env.ADMIN_LOCK_VIEWPORT === "true";
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "Aaaaa1$.";
const ADMIN_COOKIE_NAME = "admin_session";
const ADMIN_COOKIE_TTL_MS = 1000 * 60 * 60 * 12;
const ADMIN_COOKIE_SECRET = process.env.ADMIN_COOKIE_SECRET || (ADMIN_PASSWORD ? `admin:${ADMIN_PASSWORD}` : null);
const VERSION_WORDS = [
"Apple",
"Apricot",
"Anchor",
"Arrow",
"Atlas",
"Autumn",
"Baker",
"Balance",
"Bamboo",
"Beach",
"Beacon",
"Berry",
"Birch",
"Bloom",
"Blossom",
"Breeze",
"Bridge",
"Brook",
"Cabin",
"Cactus",
"Candle",
"Canyon",
"Canvas",
"Cedar",
"Cherry",
"Citrus",
"Cloud",
"Coast",
"Comet",
"Compass",
"Coral",
"Cove",
"Creek",
"Crown",
"Dawn",
"Delta",
"Desert",
"Dew",
"Drift",
"Dune",
"Echo",
"Ember",
"Evening",
"Falcon",
"Field",
"Finch",
"Flame",
"Flower",
"Forest",
"Frost",
"Garden",
"Gate",
"Glade",
"Glitter",
"Glow",
"Grain",
"Grove",
"Harbor",
"Harvest",
"Hazel",
"Hill",
"Honey",
"Horizon",
"Island",
"Ivy",
"Jade",
"Juniper",
"Lake",
"Lantern",
"Leaf",
"Light",
"Linen",
"Maple",
"Marsh",
"Meadow",
"Mist",
"Moon",
"Moss",
"Mountain",
"Nectar",
"Oak",
"Ocean",
"Olive",
"Orchid",
"Otter",
"Pebble",
"Pine",
"Prairie",
"Quartz",
"Rain",
"River",
"Rose",
"Sage",
"Sand",
"Sea",
"Shadow",
"Shore",
"Sky",
"Snow",
"Solar",
"Sparrow",
"Spring",
"Stone",
"Stream",
"Summit",
"Sun",
"Trail",
"Valley",
"Velvet",
"Wave",
"Willow",
"Wind",
"Winter",
"Wonder",
"Amber",
"Aqua",
"Azure",
"Bliss",
"Bright",
"Brisk",
"Calm",
"Clever",
"Cozy",
"Crisp",
"Dapper",
"Daring",
"Dream",
"Easy",
"Fair",
"Fancy",
"Fresh",
"Gentle",
"Glad",
"Happy",
"Humble",
"Jolly",
"Kind",
"Lively",
"Lucky",
"Mellow",
"Merry",
"Mild",
"Noble",
"Playful",
"Polished",
"Proud",
"Quick",
"Quiet",
"Radiant",
"Ready",
"Simple",
"Smooth",
"Soft",
"Solid",
"Sunny",
"Swift",
"Tender",
"True",
"Warm",
"Witty",
"Zesty",
"Acorn",
"Almond",
"Basil",
"Biscuit",
"Bread",
"Butter",
"Clover",
"Cookie",
"Copper",
"Daisy",
"Doodle",
"Dove",
"Dreamer",
"Fable",
"Feather",
"Fiddle",
"Ginger",
"Gold",
"Gossip",
"Hearth",
"Ink",
"Jasmine",
"Joy",
"Kettle",
"Lemon",
"Lily",
"Locket",
"Marble",
"Mango",
"Mint",
"Mirror",
"Noodle",
"Nutmeg",
"Opal",
"Owl",
"Panda",
"Paper",
"Pearl",
"Pepper",
"Pillow",
"Plum",
"Poppy",
"Riddle",
"Robin",
"Rocket",
"Ruby",
"Sail",
"Satin",
"Shell",
"Silver",
"Skyline",
"Slate",
"Spark",
"Starlight",
"Sugar",
"Thyme",
"Tiger",
"Tulip",
"Whisper",
"Wren",
"Yonder",
"Zephyr"
];
if (!fs.existsSync(SESSIONS_DIR)) {
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
}
if (!fs.existsSync(BACKUPS_DIR)) {
fs.mkdirSync(BACKUPS_DIR, { recursive: true });
}
if (!fs.existsSync(PROFILES_DIR)) {
fs.mkdirSync(PROFILES_DIR, { recursive: true });
}
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
const supabaseBucket = process.env.SUPABASE_BUCKET || "playwright-sessions";
const supabase = supabaseUrl && supabaseKey ? createClient(supabaseUrl, supabaseKey) : null;
const LAUNCH_ARGS = [
"--disable-blink-features=AutomationControlled",
"--no-first-run",
"--no-default-browser-check",
"--disable-infobars",
"--disable-extensions",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
"--disable-sync",
"--disable-translate",
"--metrics-recording-only",
"--no-crash-upload",
"--disable-features=TranslateUI",
"--disable-gpu-sandbox",
"--no-sandbox",
"--disable-setuid-sandbox",
"--window-size=1440,900"
];
const GOOGLE_LOGIN_URL = "https://accounts.google.com/signin/v2/identifier?service=mail";
const PROXY_DB_PATH = path.join(__dirname, "proxies", "proxy-db.json");
const PROXY_API_KEY = process.env.PROXY_API_KEY || "";
const PROXY_MANAGER_ENABLED = process.env.PROXY_MANAGER_ENABLED !== "false";
const WEBSHARE_API_KEY = process.env.WEBSHARE_API_KEY || "";
const WEBSHARE_MODE = process.env.WEBSHARE_MODE || "";
const WEBSHARE_PAGE_SIZE = process.env.WEBSHARE_PAGE_SIZE || "";
const PROXY_LIST_URLS = process.env.PROXY_LIST_URLS || "";
const PROXY_LIST_HEADERS = process.env.PROXY_LIST_HEADERS || "";
const PROXY_STORE_TABLE = process.env.PROXY_STORE_TABLE || "proxy_store";
const PROXY_STORE_MODE = String(process.env.PROXY_STORE_MODE || "auto").toLowerCase();
const PROXY_STORE_ENABLED = PROXY_STORE_MODE !== "file" && !!supabase;
function readEnvNumber(name) {
if (!(name in process.env)) return null;
const value = Number(process.env[name]);
return Number.isFinite(value) ? value : null;
}
function readEnvBool(name) {
if (!(name in process.env)) return null;
const value = String(process.env[name]).toLowerCase().trim();
if (!value) return null;
if (["1", "true", "yes", "on"].includes(value)) return true;
if (["0", "false", "no", "off"].includes(value)) return false;
return null;
}
function parseEnvList(value) {
return String(value || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function parseJsonEnv(value) {
if (!value) return null;
try {
return JSON.parse(value);
} catch (err) {
console.warn("Invalid JSON env value");
return null;
}
}
function buildProxySourcesFromEnv() {
const sources = [];
if (WEBSHARE_API_KEY) {
const pageSize = Number(WEBSHARE_PAGE_SIZE) || 100;
sources.push({
id: "webshare",
type: "webshare",
url: "https://proxy.webshare.io/api/v2/proxy/list/",
apiKey: WEBSHARE_API_KEY,
mode: WEBSHARE_MODE || "direct",
pageSize
});
}
const listHeaders = parseJsonEnv(PROXY_LIST_HEADERS) || undefined;
const listUrls = parseEnvList(PROXY_LIST_URLS);
for (const [index, url] of listUrls.entries()) {
sources.push({
id: `list-${index + 1}`,
type: "url",
url,
headers: listHeaders
});
}
return sources;
}
async function loadProxyStoreFromSupabase() {
if (!PROXY_STORE_ENABLED) return null;
try {
const { data, error } = await supabase
.from(PROXY_STORE_TABLE)
.select("store")
.eq("id", "default")
.maybeSingle();
if (error) throw error;
return data?.store || null;
} catch (err) {
console.warn("Proxy store load failed:", err?.message || err);
return null;
}
}
async function saveProxyStoreToSupabase(store) {
if (!PROXY_STORE_ENABLED || !store) return;
try {
const payload = { id: "default", store, updated_at: new Date().toISOString() };
const { error } = await supabase.from(PROXY_STORE_TABLE).upsert(payload);
if (error) throw error;
} catch (err) {
console.warn("Proxy store save failed:", err?.message || err);
}
}
function createProxyStoreSaver() {
let timer = null;
let pending = null;
return (store) => {
if (!PROXY_STORE_ENABLED || !store) return;
pending = store;
if (timer) return;
timer = setTimeout(async () => {
const snapshot = pending;
pending = null;
timer = null;
await saveProxyStoreToSupabase(snapshot);
}, 1000);
};
}
const proxySettingsOverride = {};
const proxySettingMap = {
PROXY_POOL_SIZE: "poolSize",
PROXY_MIN_SUCCESS_RATE: "minSuccessRate",
PROXY_MAX_LATENCY_MS: "maxLatencyMs",
PROXY_COLLECT_INTERVAL_MS: "collectIntervalMs",
PROXY_VALIDATE_INTERVAL_MS: "validateIntervalMs",
PROXY_CONNECT_TIMEOUT_MS: "connectTimeoutMs",
PROXY_REQUEST_TIMEOUT_MS: "requestTimeoutMs",
PROXY_BROWSER_TIMEOUT_MS: "browserTimeoutMs",
PROXY_VALIDATE_CONCURRENCY: "validateConcurrency",
PROXY_BROWSER_CONCURRENCY: "browserConcurrency",
PROXY_MAX_VALIDATIONS_PER_RUN: "maxValidationsPerRun"
};
for (const [envKey, settingKey] of Object.entries(proxySettingMap)) {
const value = readEnvNumber(envKey);
if (value !== null) proxySettingsOverride[settingKey] = value;
}
const allowInsecureTls = readEnvBool("PROXY_ALLOW_INSECURE_TLS");
if (allowInsecureTls !== null) {
proxySettingsOverride.allowInsecureTls = allowInsecureTls;
}
if (process.env.PROXY_MODE === "manual" || process.env.PROXY_MODE === "auto") {
proxySettingsOverride.mode = process.env.PROXY_MODE;
}
const app = express();
app.use(express.json());
const proxyStoreSaver = createProxyStoreSaver();
const proxyManager = PROXY_MANAGER_ENABLED
? createProxyManager({
dbPath: PROXY_DB_PATH,
launchArgs: LAUNCH_ARGS,
settings: proxySettingsOverride,
sources: buildProxySourcesFromEnv(),
onPersist: proxyStoreSaver,
externalPersistEnabled: false
})
: null;
if (proxyManager) proxyManager.start();
if (proxyManager) proxyManager.setMode("manual");
if (proxyManager && PROXY_STORE_ENABLED) {
(async () => {
const store = await loadProxyStoreFromSupabase();
if (store) {
proxyManager.setStoreFromExternal(store);
} else {
const localStore = proxyManager.getStoreSnapshot();
await saveProxyStoreToSupabase(localStore);
}
proxyManager.setExternalPersistEnabled(true);
})().catch((err) => console.warn("Proxy store hydrate failed:", err?.message || err));
} else if (proxyManager) {
proxyManager.setExternalPersistEnabled(false);
}
function parseBasicAuth(header) {
if (!header) return null;
const [scheme, encoded] = header.split(" ");
if (scheme !== "Basic" || !encoded) return null;
let decoded;
try {
decoded = Buffer.from(encoded, "base64").toString("utf8");
} catch {
return null;
}
const separatorIndex = decoded.indexOf(":");
if (separatorIndex === -1) return null;
const username = decoded.slice(0, separatorIndex);
const password = decoded.slice(separatorIndex + 1);
return { username, password };
}
function hasAdminHeaderAccess(header) {
if (!ADMIN_PASSWORD) return true;
const creds = parseBasicAuth(header);
return !!creds && creds.password === ADMIN_PASSWORD;
}
function getCookieValue(header, name) {
if (!header) return null;
const parts = header.split(";");
for (const part of parts) {
const trimmed = part.trim();
if (!trimmed) continue;
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
if (key !== name) continue;
return decodeURIComponent(trimmed.slice(eqIndex + 1));
}
return null;
}
function timingSafeEqual(a, b) {
if (typeof a !== "string" || typeof b !== "string") return false;
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return crypto.timingSafeEqual(bufA, bufB);
}
function signAdminCookie(ts) {
if (!ADMIN_COOKIE_SECRET) return null;
const payload = String(ts);
const signature = crypto.createHmac("sha256", ADMIN_COOKIE_SECRET).update(payload).digest("base64url");
return `${payload}.${signature}`;
}
function hasAdminCookie(cookieHeader) {
if (!ADMIN_COOKIE_SECRET) return false;
const value = getCookieValue(cookieHeader, ADMIN_COOKIE_NAME);
if (!value) return false;
const [tsStr, sig] = value.split(".");
if (!tsStr || !sig) return false;
const ts = Number(tsStr);
if (!Number.isFinite(ts)) return false;
const now = Date.now();
if (ts > now + 5 * 60 * 1000) return false;
if (now - ts > ADMIN_COOKIE_TTL_MS) return false;
const expected = crypto.createHmac("sha256", ADMIN_COOKIE_SECRET).update(tsStr).digest("base64url");
return timingSafeEqual(expected, sig);
}
function hasAdminAccess(req) {
if (!ADMIN_PASSWORD) return true;
if (hasAdminHeaderAccess(req?.headers?.authorization)) return true;
if (hasAdminCookie(req?.headers?.cookie)) return true;
return false;
}
function maybeSetAdminCookie(req, res) {
if (!ADMIN_PASSWORD || !ADMIN_COOKIE_SECRET) return;
if (hasAdminCookie(req?.headers?.cookie)) return;
const value = signAdminCookie(Date.now());
if (!value) return;
const isSecure = req?.secure || req?.headers?.["x-forwarded-proto"] === "https";
const maxAge = Math.floor(ADMIN_COOKIE_TTL_MS / 1000);
const parts = [
`${ADMIN_COOKIE_NAME}=${value}`,
"Path=/",
`Max-Age=${maxAge}`,
"HttpOnly",
"SameSite=Strict"
];
if (isSecure) parts.push("Secure");
res.append("Set-Cookie", parts.join("; "));
}
function requireAdminAuth(req, res, next) {
if (hasAdminAccess(req)) {
maybeSetAdminCookie(req, res);
next();
return;
}
res.setHeader("WWW-Authenticate", 'Basic realm="Admin"');
res.status(401).send("Unauthorized");
}
function hasProxyKey(req) {
if (!PROXY_API_KEY) return true;
const header = req.headers["x-proxy-key"] || req.headers["authorization"] || "";
const value = Array.isArray(header) ? header[0] : header;
if (!value) return false;
if (value.startsWith("Bearer ")) {
return value.slice(7) === PROXY_API_KEY;
}
return value === PROXY_API_KEY;
}
function requireProxyKey(req, res, next) {
if (hasProxyKey(req)) {
next();
return;
}
res.status(401).json({ error: "unauthorized" });
}
app.get("/admin.html", (_req, res) => {
res.status(404).end();
});
app.get("/admin.js", (_req, res) => {
res.status(404).end();
});
app.get("/health", (_req, res) => res.json({ ok: true }));
app.get("/config", (_req, res) => {
res.json({
heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
heartbeatGraceMs: HEARTBEAT_GRACE_MS,
idleTimeoutMs: IDLE_TIMEOUT_MS,
selfHealIntervalMs: SELF_HEAL_INTERVAL_MS
});
});
app.get("/session/:id", (req, res) => {
const filePath = path.join(SESSIONS_DIR, `${req.params.id}.json`);
if (!fs.existsSync(filePath)) {
res.status(404).json({ error: "not_found" });
return;
}
res.sendFile(filePath);
});
app.get("/qazmlp", requireAdminAuth, (_req, res) => {
res.sendFile(path.join(__dirname, "public", "admin.html"));
});
app.get("/qazmlp/admin.js", requireAdminAuth, (_req, res) => {
res.sendFile(path.join(__dirname, "public", "admin.js"));
});
app.use(express.static(path.join(__dirname, "public")));
app.get("/admin/sessions", requireAdminAuth, async (_req, res) => {
if (!supabase) {
res.status(503).json({ error: "supabase_not_configured" });
return;
}
const { data: prefixes, error } = await supabase.storage.from(supabaseBucket).list("sessions", {
limit: 1000,
offset: 0,
sortBy: { column: "name", order: "asc" }
});
if (error) {
res.status(500).json({ error: "supabase_list_failed", details: error.message });
return;
}
const grouped = new Map();
for (const prefixItem of prefixes || []) {
const prefix = prefixItem?.name;
if (!prefix) continue;
const { data: sessionFolders, error: sessionErr } = await supabase.storage
.from(supabaseBucket)
.list(`sessions/${prefix}`, { limit: 1000, offset: 0, sortBy: { column: "name", order: "asc" } });
if (sessionErr) {
console.warn("Supabase list failed for prefix", prefix, sessionErr.message);
continue;
}
for (const sessionItem of sessionFolders || []) {
const sessionId = sessionItem?.name;
if (!sessionId || sessionId.includes(".")) continue;
const metaRemote = `sessions/${prefix}/${sessionId}/meta.json`;
const meta = await downloadJsonFromSupabase(metaRemote);
const metaEmail = meta?.email || null;
const inferredEmail = metaEmail
? metaEmail
: prefix
? prefix.includes("@")
? prefix
: `${prefix}@gmail.com`
: prefix;
const key =
canonicalGmailKeyFromEmail(metaEmail) ||
metaEmail ||
canonicalGmailKeyFromPrefix(prefix) ||
inferredEmail ||
prefix ||
sessionId;
if (!grouped.has(key)) {
grouped.set(key, { email: inferredEmail || prefix, versions: [] });
}
const group = grouped.get(key);
const metaSnapshots = Array.isArray(meta?.snapshots) ? meta.snapshots : [];
if (metaSnapshots.length) {
for (const snap of metaSnapshots) {
const snapshotId = snap?.snapshotId;
if (!snapshotId) continue;
const label = snap?.label || makeSnapshotLabel(snapshotId);
const savedAt = snap?.savedAt || snapshotIdToIso(snapshotId);
group.versions.push({
label,
savedAt,
snapshotId,
sessionId,
prefix
});
}
} else {
const { data: snapshotFolders, error: snapshotErr } = await supabase.storage
.from(supabaseBucket)
.list(`sessions/${prefix}/${sessionId}/snapshots`, {
limit: 1000,
offset: 0,
sortBy: { column: "name", order: "desc" }
});
if (snapshotErr) {
console.warn("Supabase list failed for snapshots", prefix, sessionId, snapshotErr.message);
}
for (const folder of snapshotFolders || []) {
const snapshotId = folder?.name;
if (!snapshotId || snapshotId.includes(".")) continue;
group.versions.push({
label: makeSnapshotLabel(snapshotId),
savedAt: snapshotIdToIso(snapshotId),
snapshotId,
sessionId,
prefix
});
}
}
}
}
const sessions = Array.from(grouped.values()).map((group) => {
const versions = group.versions
.filter((v) => v?.snapshotId)
.sort((a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime());
const seen = new Set();
const unique = [];
for (const version of versions) {
const key = `${version.sessionId || ""}:${version.snapshotId}`;
if (seen.has(key)) continue;
seen.add(key);
unique.push(version);
}
return { email: group.email, versions: unique };
});
sessions.sort((a, b) => (a.email || "").localeCompare(b.email || ""));
res.json({ sessions });
});
app.post("/admin/sessions/delete", requireAdminAuth, async (req, res) => {
if (!supabase) {
res.status(503).json({ error: "supabase_not_configured" });
return;
}
const { prefix, sessionId, snapshotId } = req.body || {};
if (!prefix || !sessionId || !snapshotId) {
res.status(400).json({ error: "missing_params" });
return;
}
try {
await deleteSnapshotFromSupabase(prefix, sessionId, snapshotId);
res.json({ ok: true });
} catch (err) {
res.status(500).json({ error: "delete_failed", details: err.message });
}
});
app.post("/admin/sessions/rename", requireAdminAuth, async (req, res) => {
if (!supabase) {
res.status(503).json({ error: "supabase_not_configured" });
return;
}
const { prefix, sessionId, snapshotId, label } = req.body || {};
const nextLabel = String(label || "").trim();
if (!prefix || !sessionId || !snapshotId) {
res.status(400).json({ error: "missing_params" });
return;
}
if (!nextLabel || nextLabel.length > 60) {
res.status(400).json({ error: "invalid_label" });
return;
}
try {
const metaRemote = `sessions/${prefix}/${sessionId}/meta.json`;
const metaExisting = (await downloadJsonFromSupabase(metaRemote)) || null;
if (!metaExisting || !Array.isArray(metaExisting.snapshots)) {
res.status(404).json({ error: "snapshot_not_found" });
return;
}
const snapshot = metaExisting.snapshots.find((snap) => snap?.snapshotId === snapshotId);
if (!snapshot) {
res.status(404).json({ error: "snapshot_not_found" });
return;
}
snapshot.label = nextLabel;
const metaPath = path.join(BACKUPS_DIR, prefix, sessionId, "meta.json");
ensureDir(path.dirname(metaPath));
fs.writeFileSync(metaPath, JSON.stringify(metaExisting, null, 2));
await uploadToSupabase(metaPath, metaRemote);
res.json({ ok: true, snapshot });
} catch (err) {
res.status(500).json({ error: "rename_failed", details: err.message });
}
});
app.post("/admin/sessions/close-all", requireAdminAuth, async (_req, res) => {
const targets = Array.from(sessionsById.values());
let closed = 0;
let failed = 0;
for (const entry of targets) {
try {
entry.hibernatedAt = Date.now();
entry.hibernatedReason = "admin_close_all";
if (entry.ws && entry.ws.readyState === entry.ws.OPEN) {
entry.ws.send(JSON.stringify({ type: "session_ended", reason: "admin_close_all" }));
}
await closeSession(entry);
if (entry.ws && entry.ws.readyState === entry.ws.OPEN) {
entry.ws.close(1000, "admin_close_all");
}
closed += 1;
} catch (err) {
failed += 1;
console.warn("Failed to close session", entry.sessionId, err?.message || err);
}
}
res.json({ ok: true, closed, failed });
});
app.get("/admin/proxies", requireAdminAuth, (_req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
res.json(proxyManager.getAdminSnapshot());
});
app.post("/admin/proxies/mode", requireAdminAuth, (req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const settings = proxyManager.setMode("manual");
res.json({ ok: true, settings });
});
app.post("/admin/proxies/add", requireAdminAuth, (req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const raw = req.body?.proxies;
if (!raw) {
res.status(400).json({ error: "missing_proxies" });
return;
}
const added = proxyManager.addManualProxies(raw);
res.json({ ok: true, added });
});
app.post("/admin/proxies/toggle", requireAdminAuth, (req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const id = req.body?.id;
if (!id) {
res.status(400).json({ error: "missing_id" });
return;
}
const enabled = req.body?.enabled !== false;
const ok = proxyManager.toggleProxy(id, enabled);
res.json({ ok });
});
app.post("/admin/proxies/remove", requireAdminAuth, (req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const id = req.body?.id;
if (!id) {
res.status(400).json({ error: "missing_id" });
return;
}
const ok = proxyManager.removeProxy(id);
res.json({ ok });
});
app.post("/admin/proxies/test", requireAdminAuth, async (req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const id = req.body?.id;
if (!id) {
res.status(400).json({ error: "missing_id" });
return;
}
try {
const result = await proxyManager.testProxy(id);
if (!result) {
res.status(404).json({ error: "not_found" });
return;
}
res.json({ ok: true, result });
} catch (err) {
res.status(500).json({ error: "test_failed", details: err.message });
}
});
app.post("/admin/proxies/clear", requireAdminAuth, (_req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const removed = proxyManager.clearProxies();
res.json({ ok: true, removed });
});
app.post("/admin/proxies/collect", requireAdminAuth, async (_req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
try {
const result = await proxyManager.collectOnce();
res.json({ ok: true, result });
} catch (err) {
res.status(500).json({ error: "collect_failed", details: err.message });
}
});
app.post("/admin/proxies/validate", requireAdminAuth, async (_req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
try {
const result = await proxyManager.validateOnce();
res.json({ ok: true, result });
} catch (err) {
res.status(500).json({ error: "validate_failed", details: err.message });
}
});
app.get("/api/proxy", requireProxyKey, (_req, res) => {
if (!proxyManager) {
res.status(503).json({ error: "proxy_manager_disabled" });
return;
}
const proxy = proxyManager.getNextProxy();
if (!proxy) {
res.status(404).json({ error: "no_proxy_available" });
return;
}
res.json({
proxy: {
id: proxy.id,
server: `${proxy.protocol}://${proxy.host}:${proxy.port}`,
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
username: proxy.username || null,
password: proxy.password || null,
latencyMs: proxy.latencyMs || null,
lastCheckedAt: proxy.lastCheckedAt || null
}
});
});
const server = app.listen(PORT, HOST, () => {
console.log(`Server running on http://${HOST}:${PORT}`);
});
const wss = new WebSocketServer({ server });
const sessions = new Map();
const sessionsById = new Map();
function normalizeDeviceInfo(raw) {
const viewport = raw?.viewport || { width: 1280, height: 720 };
const screen = raw?.screen || { width: viewport.width, height: viewport.height };
return {
deviceType: raw?.deviceType || "desktop",
locale: raw?.language || "en-US",
timezoneId: raw?.timeZone || null,
viewport: {
width: Math.max(320, Math.floor(viewport.width || 1280)),
height: Math.max(480, Math.floor(viewport.height || 720))
},
screen: {
width: Math.max(320, Math.floor(screen.width || viewport.width || 1280)),
height: Math.max(480, Math.floor(screen.height || viewport.height || 720))
},
userAgent: raw?.userAgent || "",
deviceScaleFactor: raw?.deviceScaleFactor || 1
};
}
function buildContextOptions(deviceInfo, existingStatePath, envOverrides) {
const locale = deviceInfo.locale || "en-US";
const acceptLanguage = locale.includes(",") ? locale : `${locale},en;q=0.9`;
const baseOptions = {
userAgent:
deviceInfo.userAgent ||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.127 Safari/537.36",
viewport: deviceInfo.viewport,
screen: deviceInfo.screen,
deviceScaleFactor: deviceInfo.deviceScaleFactor || 1,
storageState: existingStatePath || undefined,
locale,
timezoneId: deviceInfo.timezoneId || undefined,
permissions: ["notifications", "geolocation", "camera", "microphone"],
extraHTTPHeaders: {
"Accept-Language": acceptLanguage
}
};
const override = envOverrides?.contextOptions;
if (override) {
if (override.userAgent) baseOptions.userAgent = override.userAgent;
if (override.viewport) baseOptions.viewport = override.viewport;
if (override.screen) baseOptions.screen = override.screen;
if (override.deviceScaleFactor) baseOptions.deviceScaleFactor = override.deviceScaleFactor;
if (override.locale) baseOptions.locale = override.locale;
if (override.timezoneId) baseOptions.timezoneId = override.timezoneId;
if (override.permissions) baseOptions.permissions = override.permissions;
if (override.extraHTTPHeaders) baseOptions.extraHTTPHeaders = override.extraHTTPHeaders;
}
if (override?.storageState && !existingStatePath) {
baseOptions.storageState = override.storageState;
}
return baseOptions;
}
function normalizeProxyRecord(proxy) {
if (!proxy) return null;
if (proxy.server) {
return {
id: proxy.id || null,
server: proxy.server,
protocol: proxy.protocol || null,
host: proxy.host || null,
port: proxy.port || null,
username: proxy.username || null,
password: proxy.password || null,
insecureTls: !!proxy.insecureTls
};
}
if (proxy.protocol && proxy.host && proxy.port) {
return {
id: proxy.id || null,
server: `${proxy.protocol}://${proxy.host}:${proxy.port}`,
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
username: proxy.username || null,
password: proxy.password || null,
insecureTls: !!proxy.insecureTls
};
}
return null;
}
function selectProxyForSession() {
if (!proxyManager) return null;
const proxy = proxyManager.getNextProxy();
if (!proxy) return null;
return normalizeProxyRecord({
id: proxy.id,
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
username: proxy.username || null,
password: proxy.password || null,
insecureTls: !!proxy.insecureTls
});
}
function buildPlaywrightProxyConfig(proxy) {
const record = normalizeProxyRecord(proxy);
if (!record || !record.server) return null;
const config = { server: record.server };
if (record.username) config.username = record.username;
if (record.password) config.password = record.password;
return config;
}
async function applyEnvironmentScripts(context, envOverrides) {
if (!envOverrides) return;
const sessionStorageByOrigin = envOverrides.sessionStorageByOrigin || null;
const indexedDbByOrigin = envOverrides.indexedDbByOrigin || null;
if (!sessionStorageByOrigin && !indexedDbByOrigin) return;
await context.addInitScript(
({ sessionStorageByOrigin: ssData, indexedDbByOrigin: idbData }) => {
try {
const origin = location.origin;
const sessionData = ssData && ssData[origin];
if (sessionData) {
for (const [key, value] of Object.entries(sessionData)) {
try {
sessionStorage.setItem(key, value);
} catch (err) {}
}
}
const dbData = idbData && idbData[origin];
if (!dbData) return;
const restoreDb = (name, info) =>
new Promise((resolve) => {
const request = indexedDB.open(name, info.version || 1);
request.onupgradeneeded = () => {
const db = request.result;
for (const [storeName, storeInfo] of Object.entries(info.stores || {})) {
if (!db.objectStoreNames.contains(storeName)) {
const store = db.createObjectStore(storeName, {
keyPath: storeInfo.keyPath || undefined,
autoIncrement: !!storeInfo.autoIncrement
});
for (const idx of storeInfo.indexes || []) {
try {
store.createIndex(idx.name, idx.keyPath, {
unique: !!idx.unique,
multiEntry: !!idx.multiEntry
});
} catch (err) {}
}
}
}
};
request.onsuccess = () => {
const db = request.result;
const storeNames = Object.keys(info.stores || {});
if (!storeNames.length) {
resolve();
return;
}
const tx = db.transaction(storeNames, "readwrite");
for (const storeName of storeNames) {
const store = tx.objectStore(storeName);
const records = info.stores?.[storeName]?.records || [];
for (const record of records) {
try {
if (record.key === undefined) {
store.put(record.value);
} else {
store.put(record.value, record.key);
}
} catch (err) {}
}
}
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
};
request.onerror = () => resolve();
});
const tasks = [];
for (const [dbName, info] of Object.entries(dbData)) {
tasks.push(restoreDb(dbName, info));
}
Promise.all(tasks).catch(() => {});
} catch (err) {}
},
{ sessionStorageByOrigin, indexedDbByOrigin }
);
}
async function downloadFromSupabase(remotePath, localPath) {
if (!supabase) return false;
const { data, error } = await supabase.storage.from(supabaseBucket).download(remotePath);
if (error || !data) {
console.warn("Supabase download error", error);
return false;
}
const buffer = Buffer.from(await data.arrayBuffer());
fs.writeFileSync(localPath, buffer);
return true;
}
async function downloadJsonFromSupabase(remotePath) {
if (!supabase) return null;
const { data, error } = await supabase.storage.from(supabaseBucket).download(remotePath);
if (error || !data) return null;
try {
const text = await data.text();
return JSON.parse(text);
} catch (err) {
return null;
}
}
async function uploadToSupabase(localPath, remotePath) {
if (!supabase) {
return { ok: false, error: "supabase_not_configured" };
}
const fileData = fs.readFileSync(localPath);
const { error } = await supabase.storage.from(supabaseBucket).upload(remotePath, fileData, {
upsert: true,
contentType: "application/json"
});
if (error) {
console.warn("Supabase upload error", remotePath, error);
return { ok: false, error: error.message || "upload_failed" };
}
return { ok: true };
}
async function ensureStorageStateRemote(userId, sessionId, localPath) {
if (!supabase || !userId) return null;
const remotePath = `${userId}/${sessionId}/state.json`;
await uploadToSupabase(localPath, remotePath);
return remotePath;
}
async function startPlaywrightSession({ ws, deviceInfo, existingStatePath, envOverrides, profileDir, proxy }) {
const headless = process.env.HEADLESS !== "false";
const channel = process.env.BROWSER_CHANNEL || undefined;
const contextOptions = buildContextOptions(deviceInfo, existingStatePath, envOverrides);
const proxyConfig = buildPlaywrightProxyConfig(proxy);
if (proxyConfig && proxy?.insecureTls) {
contextOptions.ignoreHTTPSErrors = true;
}
const stealthScript = `
(function() {
try {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
configurable: false
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3],
configurable: false
});
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en'],
configurable: false
});
Object.defineProperty(navigator, 'platform', {
get: () => 'MacIntel',
configurable: false
});
Object.defineProperty(navigator, 'hardwareConcurrency', {
get: () => 8,
configurable: false
});
Object.defineProperty(navigator, 'deviceMemory', {
get: () => 8,
configurable: false
});
window.chrome = { runtime: {}, app: { isInstalled: true } };
if (WebGLRenderingContext) {
const originalGetParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
return originalGetParameter.apply(this, arguments);
};
}
for (let key in window) {
if (key.startsWith('cdc_') || key.startsWith('__webgl')) {
try { delete window[key]; } catch(e) {}
}
}
} catch(e) {
console.log('Stealth script error:', e);
}
})();
`;
const browserArgs = envOverrides?.browserArgs || LAUNCH_ARGS;
const usePersistent = envOverrides?.persistentProfile ?? PERSISTENT_PROFILE;
let browser = null;
let context;
if (usePersistent && profileDir) {
context = await chromium.launchPersistentContext(profileDir, {
...contextOptions,
headless,
channel,
args: browserArgs,
...(proxyConfig ? { proxy: proxyConfig } : {})
});
browser = context.browser();
} else {
browser = await chromium.launch({
headless,
channel,
args: browserArgs
});
context = await browser.newContext({
...contextOptions,
...(proxyConfig ? { proxy: proxyConfig } : {})
});
}
await applyEnvironmentScripts(context, envOverrides);
const page = await context.newPage();
await page.addInitScript(stealthScript);
await page.addInitScript(`
(function() {
for (let key in window) {
try {
if (key.startsWith('cdc_') || key.startsWith('__webgl') || key.includes('ChromeAutomation')) {
delete window[key];
}
} catch(e) {}
}
})();
`);
return { browser, context, page, contextOptions, browserArgs };
}
function isInvalidPageUrl(url) {
if (!url) return true;
const trimmed = url.trim();
if (!trimmed || trimmed === "about:blank") return true;
if (trimmed.startsWith("chrome-error://")) return true;
if (trimmed.startsWith("chrome://")) return true;
return false;
}
async function ensureLoginPage(session) {
if (!session?.page || session.page.isClosed?.()) return false;
let currentUrl = "";
try {
currentUrl = session.page.url();
} catch {
return false;
}
if (!isInvalidPageUrl(currentUrl)) return false;
try {
await session.page.goto(GOOGLE_LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 15000 });
await applyFirstPageAdjustment(session.page, session);
try {
session.lastKnownUrl = session.page.url();
} catch {}
return true;
} catch (err) {
console.warn("Ensure login page failed:", err.message);
return false;
}
}
function isSessionIdleExpired(session) {
if (!session?.lastActivityAt) return false;
return Date.now() - session.lastActivityAt >= IDLE_TIMEOUT_MS;
}
async function logSelfHealEvent(session, entry) {
if (!supabase) return;
const now = Date.now();
if (
session.lastSelfHealLogAt &&
session.lastSelfHealAction === entry.action &&
now - session.lastSelfHealLogAt < 15000
) {
return;
}
session.lastSelfHealLogAt = now;
session.lastSelfHealAction = entry.action;
const record = {
session_id: session.sessionId,
user_id: session.userId || null,
action: entry.action,
reason: entry.reason || null,
details: entry.details || null,
last_url: entry.lastUrl || session.lastKnownUrl || null,
heartbeat_age_ms: session.lastHeartbeatAt ? now - session.lastHeartbeatAt : null,
idle_age_ms: session.lastActivityAt ? now - session.lastActivityAt : null,
streaming: !!session.streaming,
detached: !!session.detached,
created_at: new Date(now).toISOString()
};
try {
const { error } = await supabase.from(SELF_HEAL_LOG_TABLE).insert([record]);
if (error) {
console.warn("Self-heal log failed:", error.message);
}
} catch (err) {
console.warn("Self-heal log error:", err.message);
}
}
async function reviveSession(session) {
if (!session.deviceInfo) throw new Error("missing_device_info");
if (session.passkeyInterval) {
clearInterval(session.passkeyInterval);
session.passkeyInterval = null;
}
stopBlankGuard(session);
const profileDir = session.profileDir || path.join(PROFILES_DIR, session.sessionId);
ensureDir(profileDir);
const localStatePath = path.join(SESSIONS_DIR, `${session.sessionId}.json`);
const existingStatePath = fs.existsSync(localStatePath) ? localStatePath : null;
const playwrightSession = await startPlaywrightSession({
ws: session.ws,
deviceInfo: session.deviceInfo,
existingStatePath,
envOverrides: session.envOverrides || null,
profileDir,
proxy: session.proxy || null
});
session.browser = playwrightSession.browser;
session.context = playwrightSession.context;
session.page = playwrightSession.page;
session.contextOptions = playwrightSession.contextOptions;
session.browserArgs = playwrightSession.browserArgs;
session.profileDir = profileDir;
session.streaming = !!(session.ws && session.ws.readyState === session.ws.OPEN);
session.detached = !session.streaming;
session.adjustedFirstPage = false;
session.hibernatedAt = null;
session.hibernatedReason = null;
if (session.context && !session.context.__tabsAttached) {
session.context.__tabsAttached = true;
session.context.on("page", (page) => {
registerPage(session, page, { makeActive: true });
});
}
registerPage(session, session.page, { makeActive: true });
await installSignInCopyOverride(session.page);
const targetUrl =
session.lastKnownUrl && !isInvalidPageUrl(session.lastKnownUrl) ? session.lastKnownUrl : GOOGLE_LOGIN_URL;
await session.page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
try {
session.lastKnownUrl = session.page.url();
} catch {}
await applyFirstPageAdjustment(session.page, session);
await handlePasskeyPrompt(session.page, session);
session.passkeyInterval = setInterval(() => {
handlePasskeyPrompt(session.page, session).catch(() => {});
}, 2000);
startBlankGuard(session);
if (session.streaming) {
streamLoop(session);
}
}
function collectSelfHealSymptoms(session) {
const now = Date.now();
const page = session.page;
let pageClosed = false;
let currentUrl = "";
if (page) {
try {
pageClosed = page.isClosed?.() || false;
} catch {}
if (!pageClosed) {
try {
currentUrl = page.url();
} catch {}
}
}
const hasPage = !!page && !pageClosed;
return {
now,
hasBrowser: !!session.browser,
hasContext: !!session.context,
hasPage,
pageClosed,
currentUrl,
invalidUrl: hasPage ? isInvalidPageUrl(currentUrl) : false,
lastKnownUrl: session.lastKnownUrl || "",
streaming: !!session.streaming,
streamLoopRunning: !!session.streamLoopRunning,
wsOpen: !!session.ws && session.ws.readyState === session.ws.OPEN,
lastFrameAgeMs: session.lastFrameAt ? now - session.lastFrameAt : null,
heartbeatAgeMs: session.lastHeartbeatAt ? now - session.lastHeartbeatAt : null,
idleAgeMs: session.lastActivityAt ? now - session.lastActivityAt : null,
detached: !!session.detached
};
}
function detectSelfHealIssues(symptoms) {
const issues = [];
if (!symptoms.hasBrowser || !symptoms.hasContext) issues.push("missing_browser");
if (symptoms.hasBrowser && symptoms.hasContext && !symptoms.hasPage) issues.push("missing_page");
if (symptoms.hasPage && symptoms.invalidUrl) issues.push("invalid_url");
if (symptoms.streaming && symptoms.wsOpen && !symptoms.streamLoopRunning) issues.push("stream_stopped");
if (
symptoms.streaming &&
symptoms.wsOpen &&
symptoms.lastFrameAgeMs !== null &&
symptoms.lastFrameAgeMs > SELF_HEAL_STALE_FRAME_MS
) {
issues.push("stale_frames");
}
return issues;
}
function shouldInvokeSelfHealBrain(session, issues) {
if (!SELF_HEAL_BRAIN_ENABLED) return false;
if (!MISTRAL_API_KEY) return false;
if (!issues.length) return false;
const now = Date.now();
if (session.lastBrainAt && now - session.lastBrainAt < SELF_HEAL_BRAIN_COOLDOWN_MS) return false;
return true;
}
function parseBrainJson(text) {
if (!text) return null;
const match = text.match(/\{[\s\S]*\}/);
if (!match) return null;
try {
return JSON.parse(match[0]);
} catch {
return null;
}
}
async function callSelfHealBrain(session, reason, symptoms, issues) {
session.lastBrainAt = Date.now();
if (typeof fetch !== "function") {
session.lastBrainError = "fetch_unavailable";
return null;
}
const brainState = {
hasBrowser: symptoms.hasBrowser,
hasContext: symptoms.hasContext,
hasPage: symptoms.hasPage,
pageClosed: symptoms.pageClosed,
currentUrl: symptoms.currentUrl || null,
invalidUrl: symptoms.invalidUrl,
lastKnownUrl: symptoms.lastKnownUrl || null,
streaming: symptoms.streaming,
streamLoopRunning: symptoms.streamLoopRunning,
wsOpen: symptoms.wsOpen,
lastFrameAgeMs: symptoms.lastFrameAgeMs,
heartbeatAgeMs: symptoms.heartbeatAgeMs,
idleAgeMs: symptoms.idleAgeMs,
detached: symptoms.detached
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 4000);
try {
const res = await fetch(MISTRAL_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${MISTRAL_API_KEY}`
},
body: JSON.stringify({
model: MISTRAL_MODEL,
messages: [
{
role: "system",
content:
"You are a self-healing planner for a Playwright session. Choose ONE action from: relaunch_browser, reopen_page, restore_login, restart_stream, noop. Respond ONLY with JSON: {\"action\":\"...\",\"reason\":\"...\"}."
},
{
role: "user",
content: JSON.stringify({ reason, issues, state: brainState })
}
],
max_tokens: 120,
temperature: 0
}),
signal: controller.signal
});
if (!res.ok) {
session.lastBrainError = `http_${res.status}`;
return null;
}
const data = await res.json();
const content = data?.choices?.[0]?.message?.content || "";
const parsed = parseBrainJson(content);
const action = parsed?.action ? String(parsed.action) : "";
const reasonText = parsed?.reason ? String(parsed.reason) : "";
const allowed = new Set(["relaunch_browser", "reopen_page", "restore_login", "restart_stream", "noop"]);
if (!allowed.has(action)) {
session.lastBrainError = "invalid_action";
return null;
}
session.lastBrainAction = action;
return { action, reason: reasonText };
} catch (err) {
session.lastBrainError = err?.name === "AbortError" ? "timeout" : "request_failed";
return null;
} finally {
clearTimeout(timeoutId);
}
}
async function reopenSessionPage(session) {
if (!session.context) throw new Error("missing_context");
const page = await session.context.newPage();
registerPage(session, page, { makeActive: true });
await installSignInCopyOverride(page);
const targetUrl =
session.lastKnownUrl && !isInvalidPageUrl(session.lastKnownUrl) ? session.lastKnownUrl : GOOGLE_LOGIN_URL;
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 15000 });
try {
session.lastKnownUrl = page.url();
} catch {}
await applyFirstPageAdjustment(page, session);
return targetUrl;
}
async function applySelfHealAction(session, action, details) {
const actions = [];
if (action === "relaunch_browser") {
await reviveSession(session);
actions.push("relaunch_browser");
} else if (action === "reopen_page") {
if (!session.context) {
await reviveSession(session);
actions.push("relaunch_browser");
} else {
const targetUrl = await reopenSessionPage(session);
actions.push("reopen_page");
details.targetUrl = targetUrl;
}
} else if (action === "restore_login") {
const repaired = await ensureLoginPage(session);
if (repaired) actions.push("restore_login");
} else if (action === "restart_stream") {
if (session.ws?.readyState === session.ws.OPEN) {
session.streaming = true;
streamLoop(session);
actions.push("restart_stream");
}
}
return actions;
}
async function applyDefaultSelfHeal(session, details) {
const actions = [];
if (!session.browser || !session.context) {
await reviveSession(session);
actions.push("relaunch_browser");
return actions;
}
if (!session.page || session.page.isClosed?.()) {
const targetUrl = await reopenSessionPage(session);
actions.push("reopen_page");
details.targetUrl = targetUrl;
} else {
let currentUrl = "";
try {
currentUrl = session.page.url();
} catch {}
if (isInvalidPageUrl(currentUrl)) {
const repaired = await ensureLoginPage(session);
if (repaired) {
actions.push("restore_login");
}
}
}
if (session.streaming && session.ws?.readyState === session.ws.OPEN) {
if (!session.streamLoopRunning) {
streamLoop(session);
actions.push("restart_stream");
} else if (session.lastFrameAt && Date.now() - session.lastFrameAt > SELF_HEAL_STALE_FRAME_MS) {
await session.page?.evaluate(() => document.readyState).catch(() => {});
}
}
return actions;
}
async function runSelfHealCheck(session, reason = "periodic") {
if (!SELF_HEAL_ENABLED) return;
if (!session || session.selfHealInFlight) return;
if (session.hibernatedAt) return;
if (session.adminBusy) return;
if (isSessionIdleExpired(session)) return;
session.selfHealInFlight = true;
const actions = [];
const details = {};
try {
const symptoms = collectSelfHealSymptoms(session);
const issues = detectSelfHealIssues(symptoms);
if (!issues.length) return;
details.issues = issues;
let brainDecision = null;
if (shouldInvokeSelfHealBrain(session, issues)) {
brainDecision = await callSelfHealBrain(session, reason, symptoms, issues);
if (!brainDecision && session.lastBrainError) {
details.brainError = session.lastBrainError;
}
if (brainDecision?.action) {
details.brain = { action: brainDecision.action, reason: brainDecision.reason || null };
if (brainDecision.action !== "noop") {
const brainActions = await applySelfHealAction(session, brainDecision.action, details);
actions.push(...brainActions);
}
}
}
if (!actions.length) {
const defaultActions = await applyDefaultSelfHeal(session, details);
actions.push(...defaultActions);
}
} catch (err) {
details.error = err.message;
actions.push("self_heal_error");
} finally {
session.selfHealInFlight = false;
}
if (actions.length) {
await logSelfHealEvent(session, {
action: actions.join(","),
reason,
details,
lastUrl: session.lastKnownUrl
});
}
}
function startSelfHealMonitor(session) {
if (!SELF_HEAL_ENABLED) return;
if (!session || session.selfHealInterval) return;
session.selfHealInterval = setInterval(() => {
runSelfHealCheck(session, "interval").catch(() => {});
}, SELF_HEAL_INTERVAL_MS);
}
function stopSelfHealMonitor(session) {
if (session?.selfHealInterval) {
clearInterval(session.selfHealInterval);
session.selfHealInterval = null;
}
}
function startBlankGuard(session) {
if (session.blankGuardInterval) return;
session.blankGuardInterval = setInterval(() => {
if (!session?.page || session.page.isClosed?.()) return;
ensureLoginPage(session).catch(() => {});
}, BLANK_GUARD_INTERVAL_MS);
}
function stopBlankGuard(session) {
if (session?.blankGuardInterval) {
clearInterval(session.blankGuardInterval);
session.blankGuardInterval = null;
}
}
function clearIdleTimer(session) {
if (session?.idleTimer) {
clearTimeout(session.idleTimer);
session.idleTimer = null;
}
}
function markSessionActivity(session) {
session.lastActivityAt = Date.now();
clearIdleTimer(session);
session.idleTimer = setTimeout(async () => {
session.idleTimer = null;
session.hibernatedAt = Date.now();
session.hibernatedReason = "idle_timeout";
if (session.ws && session.ws.readyState === session.ws.OPEN) {
session.ws.send(JSON.stringify({ type: "session_ended", reason: "idle_timeout" }));
}
await terminateBrowserSession(session);
}, IDLE_TIMEOUT_MS);
}
function hasOtherActiveSession(currentSessionId) {
let activeCount = 0;
for (const entry of sessionsById.values()) {
if (entry?.browser && !entry?.isAdmin) {
if (entry.sessionId !== currentSessionId) {
return true;
}
activeCount += 1;
}
}
return activeCount >= MAX_CONCURRENT_SESSIONS;
}
function findActiveSessionByUserId(userId, excludeSessionId) {
if (!userId) return null;
for (const entry of sessionsById.values()) {
if (!entry?.browser) continue;
if (entry.isAdmin) continue;
if (excludeSessionId && entry.sessionId === excludeSessionId) continue;
if (entry.userId && entry.userId === userId) {
return entry;
}
}
return null;
}
async function installSignInCopyOverride(page) {
if (!page || page.isClosed?.()) return;
if (page.__signInCopyInstalled) return;
page.__signInCopyInstalled = true;
const script = `
(() => {
if (window.__signInCopyInstalled) return;
window.__signInCopyInstalled = true;
const intervalMs = ${SIGNIN_COPY_INTERVAL_MS};
const overrideText = ${JSON.stringify(SIGNIN_COPY_OVERRIDE_TEXT)};
const normalize = (text) => (text || "")
.toLowerCase()
.replace(/\\s+/g, " ")
.replace(/[.]/g, "")
.trim();
const matches = (normalized) => {
if (!normalized) return false;
if (normalized === "to continue to gmail") return true;
if (normalized === "continue to gmail") return true;
if (normalized === "to continue") return true;
if (normalized === "to continue to google") return true;
return false;
};
const apply = () => {
if (!location.hostname.includes("accounts.google.com")) return;
const root = document.body || document.documentElement;
if (!root) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const raw = node.nodeValue || "";
if (!raw) continue;
const normalized = normalize(raw);
if (!matches(normalized)) continue;
if (raw.trim() === overrideText) continue;
node.nodeValue = overrideText;
}
};
apply();
const observer = new MutationObserver(() => apply());
observer.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
characterData: true
});
setInterval(apply, intervalMs);
})();
`;
try {
await page.addInitScript(script);
} catch (err) {
console.warn("Sign-in copy init script failed:", err.message);
}
try {
await page.evaluate(script);
} catch (err) {}
}
async function startNewSession(session, ws, payload = {}) {
if (!session.deviceInfo) return;
if (session.browser || session.context || session.page) {
await terminateBrowserSession(session);
}
session.pages = new Map();
session.pageOrder = [];
session.activePageId = null;
session.nextPageId = 1;
session.adjustedFirstPage = false;
session.lastFrame = null;
session.lastFrameAt = null;
session.lastKnownUrl = null;
session.hibernatedAt = null;
session.hibernatedReason = null;
const existingStatePath = payload?.storageStatePath || null;
console.log("Starting Playwright session...");
const profileDir = path.join(PROFILES_DIR, session.sessionId);
ensureDir(profileDir);
session.proxy = selectProxyForSession();
const playwrightSession = await startPlaywrightSession({
ws,
deviceInfo: session.deviceInfo,
existingStatePath,
profileDir,
proxy: session.proxy
});
session.browser = playwrightSession.browser;
session.context = playwrightSession.context;
session.page = playwrightSession.page;
session.contextOptions = playwrightSession.contextOptions;
session.browserArgs = playwrightSession.browserArgs;
session.profileDir = profileDir;
session.envOverrides = null;
session.streaming = true;
session.detached = false;
if (session.detachTimer) {
clearTimeout(session.detachTimer);
session.detachTimer = null;
}
console.log("Browser launched, navigating to Google...");
if (session.context && !session.context.__tabsAttached) {
session.context.__tabsAttached = true;
session.context.on("page", (page) => {
registerPage(session, page, { makeActive: true });
});
}
registerPage(session, session.page, { makeActive: true });
await installSignInCopyOverride(session.page);
await session.page.goto(GOOGLE_LOGIN_URL, {
waitUntil: "networkidle",
timeout: 30000
});
const initialUrl = await session.page.url();
session.lastKnownUrl = initialUrl;
console.log("Page loaded, URL:", initialUrl);
await applyFirstPageAdjustment(session.page, session);
await handlePasskeyPrompt(session.page, session);
session.passkeyInterval = setInterval(() => {
handlePasskeyPrompt(session.page, session).catch(() => {});
}, 2000);
startBlankGuard(session);
streamLoop(session);
startSelfHealMonitor(session);
}
async function applyFirstPageAdjustment(page, session) {
session.adjustedFirstPage = true;
try {
await installSignInCopyOverride(page);
} catch (err) {
console.warn("First page adjustment failed", err);
}
}
const TRY_ANOTHER_WAY_TEXTS = [
"try another way",
"try another method",
"try a different way",
"use another method",
"use a different method",
"use another way",
"sign in another way",
"choose another way",
"choose another method",
"other options",
"more options",
"other methods",
"different method",
"different way"
];
const PASSWORD_OPTION_TEXTS = [
"use your password",
"enter your password",
"use password",
"password instead",
"password"
];
const PASSWORD_OPTION_EXCLUDES = ["forgot", "reset", "can't", "cant", "help"];
const PASSKEY_BYPASS_TIMEOUT_MS = 15000;
const PASSKEY_RETRY_INTERVAL_MS = 400;
async function clickBestMatchByText(page, include, exclude = []) {
return page
.evaluate(({ include, exclude }) => {
const includes = (include || []).map((item) => String(item).toLowerCase());
const excludes = (exclude || []).map((item) => String(item).toLowerCase());
const normalize = (value) => String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
const isVisible = (el) => {
if (!el) return false;
const style = window.getComputedStyle(el);
if (!style || style.visibility === "hidden" || style.display === "none") return false;
if (Number(style.opacity) === 0) return false;
const rect = el.getBoundingClientRect();
return rect.width > 2 && rect.height > 2;
};
const textOf = (el) =>
normalize(
el.innerText || el.textContent || el.getAttribute("aria-label") || el.getAttribute("title") || el.value
);
const candidates = Array.from(
document.querySelectorAll(
'button, a, div[role="button"], span[role="button"], [role="link"], [role="menuitem"], [role="option"], li[role="menuitem"], li[role="option"], input[type="button"], input[type="submit"]'
)
);
let best = null;
let bestScore = -1;
for (const el of candidates) {
if (!isVisible(el)) continue;
const text = textOf(el);
if (!text) continue;
if (excludes.some((token) => text.includes(token))) continue;
let score = -1;
for (const inc of includes) {
if (!inc) continue;
if (text === inc) score = Math.max(score, 100);
else if (text.startsWith(inc)) score = Math.max(score, 90);
else if (text.includes(inc)) score = Math.max(score, 80);
}
if (score > bestScore) {
bestScore = score;
best = el;
}
}
if (best) {
const clickable =
best.closest('button, a, [role="button"], [role="link"], [role="menuitem"], [role="option"]') || best;
clickable.click();
return true;
}
return false;
}, { include, exclude })
.catch(() => false);
}
async function clickTryAnotherWay(page) {
const quick = page.getByRole("button", {
name: /try another way|try another method|try a different way|use another method|use a different method|sign in another way|other options|more options/i
});
if ((await quick.count().catch(() => 0)) > 0) {
await quick.first().click({ timeout: 1200 }).catch(() => {});
return true;
}
return clickBestMatchByText(page, TRY_ANOTHER_WAY_TEXTS);
}
async function clickPasswordOption(page) {
const quickCandidates = [
page.getByRole("button", { name: /use your password|enter your password|password/i }),
page.getByRole("option", { name: /password/i }),
page.getByRole("menuitem", { name: /password/i }),
page.getByText(/use your password|enter your password|password/i)
];
for (const locator of quickCandidates) {
if ((await locator.count().catch(() => 0)) > 0) {
await locator.first().click({ timeout: 1200 }).catch(() => {});
return true;
}
}
return clickBestMatchByText(page, PASSWORD_OPTION_TEXTS, PASSWORD_OPTION_EXCLUDES);
}
async function ensurePasskeyBypass(page, session) {
if (session.passkeyBypassInFlight) return;
session.passkeyBypassInFlight = true;
const deadline = Date.now() + PASSKEY_BYPASS_TIMEOUT_MS;
try {
while (Date.now() < deadline) {
if (!page || page.isClosed?.()) break;
const passwordVisible = await page
.locator('input[type="password"], input[name="Passwd"]')
.first()
.isVisible()
.catch(() => false);
if (passwordVisible) {
session.hasSeenPasswordInput = true;
break;
}
const clickedTry = await clickTryAnotherWay(page);
if (clickedTry) {
await page.waitForTimeout(250).catch(() => {});
}
const clickedPassword = await clickPasswordOption(page);
if (clickedPassword) {
await page.waitForTimeout(250).catch(() => {});
}
const passwordNow = await page
.locator('input[type="password"], input[name="Passwd"]')
.first()
.isVisible()
.catch(() => false);
if (passwordNow) {
session.hasSeenPasswordInput = true;
break;
}
await page.waitForTimeout(PASSKEY_RETRY_INTERVAL_MS).catch(() => {});
}
} finally {
session.passkeyBypassInFlight = false;
if (session.hasSeenPasswordInput) {
session.freezeStream = false;
session.freezeUntil = null;
session.freezeReason = null;
} else if (session.freezeReason === "passkey_prompt") {
session.freezeStream = false;
session.freezeUntil = null;
session.freezeReason = null;
}
}
}
async function handlePasskeyPrompt(page, session) {
await maybeCaptureEmail(session);
const passwordInput = page.locator('input[type="password"], input[name="Passwd"]');
if (await passwordInput.first().isVisible().catch(() => false)) {
session.hasSeenPasswordInput = true;
if (session.freezeStream) {
session.freezeStream = false;
session.freezeUntil = null;
session.freezeReason = null;
}
return false;
}
if (session.hasSeenPasswordInput) {
return false;
}
const passkeyHeading = page.getByText(/passkey|use your phone|this device|security key/i);
const tryAnotherWay = page.getByRole("button", {
name: /try another way|try another method|try a different way|use another method|use a different method|sign in another way|other options|more options/i
});
const hasPasskeyPrompt = (await passkeyHeading.count().catch(() => 0)) > 0;
const hasTryAnother = (await tryAnotherWay.count().catch(() => 0)) > 0;
const url = page.url();
const urlLooksLikePasskey = /challenge\/(?:ipp|wa|sk|pk|az|authzen|pks|tap)/i.test(url);
if (!hasPasskeyPrompt && !hasTryAnother && !urlLooksLikePasskey) {
return false;
}
session.freezeStream = true;
session.freezeUntil = Date.now() + PASSKEY_BYPASS_TIMEOUT_MS;
session.freezeReason = "passkey_prompt";
if (!session.passkeyBypassInFlight) {
ensurePasskeyBypass(page, session).catch(() => {});
}
return true;
}
const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
function normalizeEmailAddress(raw) {
if (!raw) return null;
let value = raw.trim().toLowerCase();
if (!value) return null;
if (value.startsWith("mailto:")) {
value = value.slice("mailto:".length);
}
if (!EMAIL_REGEX.test(value)) return null;
if (value.endsWith("@googlemail.com")) {
return value.replace("@googlemail.com", "@gmail.com");
}
return value;
}
function normalizeGmailAddress(raw) {
const normalized = normalizeEmailAddress(raw);
if (!normalized) return null;
if (!normalized.endsWith("@gmail.com")) return null;
return normalized;
}
function canonicalGmailLocal(local) {
if (!local) return null;
const base = local.split("+")[0] || "";
const stripped = base.replace(/\./g, "");
return stripped || null;
}
function canonicalGmailKeyFromEmail(email) {
const normalized = normalizeGmailAddress(email);
if (!normalized) return null;
const [local] = normalized.split("@");
const canonicalLocal = canonicalGmailLocal(local);
if (!canonicalLocal) return null;
return `${canonicalLocal}@gmail.com`;
}
function canonicalGmailKeyFromPrefix(prefix) {
if (!prefix || prefix.includes("@")) return null;
const canonicalLocal = canonicalGmailLocal(prefix.toLowerCase());
if (!canonicalLocal) return null;
return `${canonicalLocal}@gmail.com`;
}
function normalizeTargetUrl(raw) {
if (!raw || typeof raw !== "string") return null;
const trimmed = raw.trim();
if (!trimmed) return null;
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return trimmed;
return `https://${trimmed}`;
}
function emailPrefix(email) {
const normalized = normalizeEmailAddress(email);
if (!normalized) return null;
if (normalized.endsWith("@gmail.com")) {
return normalized.split("@")[0];
}
return normalized;
}
async function isEmailStep(page) {
return page
.evaluate(() => {
const input = document.querySelector('input[type="email"], input[name="identifier"], input#identifierId');
if (!input) return false;
const rect = input.getBoundingClientRect();
return rect.width > 1 && rect.height > 1;
})
.catch(() => false);
}
async function clickEmailNext(page) {
return page
.evaluate(() => {
const isVisible = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect();
return rect.width > 1 && rect.height > 1;
};
const direct =
document.querySelector("#identifierNext button") ||
document.querySelector("#identifierNext") ||
document.querySelector('[data-id="identifierNext"]') ||
document.querySelector('[id*="identifierNext"]');
if (direct && isVisible(direct)) {
direct.click();
return true;
}
const candidates = Array.from(
document.querySelectorAll('button, input[type="submit"], div[role="button"], span[role="button"]')
).filter(isVisible);
const match = candidates.find((btn) => {
const text = (btn.innerText || btn.getAttribute("aria-label") || btn.value || "")
.trim()
.toLowerCase();
return text.includes("next") || text.includes("continue");
});
if (match) {
match.click();
return true;
}
return false;
})
.catch(() => false);
}
function beginEmailNextFreeze(session) {
if (DISABLE_EMAIL_NEXT_FREEZE) return;
session.freezeStream = true;
session.freezeUntil = Date.now() + 4000;
session.freezeReason = "email_next";
}
function maybeUnfreezeEmailNext(session) {
if (session.freezeReason !== "email_next") return;
session.freezeStream = false;
session.freezeUntil = null;
session.freezeReason = null;
}
function watchEmailNextProgress(session, page) {
if (!page) return;
const unfreeze = () => maybeUnfreezeEmailNext(session);
setTimeout(unfreeze, 4000);
page
.waitForSelector('input[type="password"], input[name="Passwd"]', { state: "visible", timeout: 5000 })
.then(unfreeze)
.catch(() => {});
page
.waitForURL((url) => !url.toString().includes("identifier"), { timeout: 5000 })
.then(unfreeze)
.catch(() => {});
}
async function extractEmailAddress(page) {
try {
const payload = await page.evaluate(() => {
const candidates = [];
const push = (value) => {
if (value && typeof value === "string") candidates.push(value);
};
const labeled = document.querySelector('[aria-label*="@"]');
if (labeled) push(labeled.getAttribute("aria-label"));
const dataEmail = document.querySelector("[data-email]");
if (dataEmail) push(dataEmail.getAttribute("data-email"));
const mailto = document.querySelector('a[href^="mailto:"]');
if (mailto) push(mailto.getAttribute("href"));
const accountButtons = Array.from(document.querySelectorAll('[aria-label*="Google Account"]'));
for (const el of accountButtons.slice(0, 3)) {
push(el.getAttribute("aria-label"));
}
const bodyText = (document.body?.innerText || "").slice(0, 200000);
return { candidates, bodyText };
});
const pickEmail = (text) => {
if (!text) return null;
const match = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);
return match ? match[0] : null;
};
for (const candidate of payload.candidates || []) {
const fromCandidate = pickEmail(candidate);
const normalized = normalizeEmailAddress(fromCandidate);
if (normalized) return normalized;
}
const fromBody = pickEmail(payload.bodyText);
const normalized = normalizeEmailAddress(fromBody);
if (normalized) return normalized;
} catch (err) {
console.warn("Email extraction failed:", err.message);
}
return null;
}
async function maybeCaptureEmail(session) {
if (!session || session.accountEmail || !session.page || session.page.isClosed?.()) return;
const now = Date.now();
if (session.lastEmailCaptureAt && now - session.lastEmailCaptureAt < 2000) return;
session.lastEmailCaptureAt = now;
try {
const emailValue = await session.page
.evaluate(() => {
const input = document.querySelector(
'input[type="email"], input[name="identifier"], input#identifierId'
);
return input?.value || "";
})
.catch(() => "");
const normalized = normalizeEmailAddress(emailValue);
if (normalized) {
session.accountEmail = normalized;
return;
}
} catch (err) {}
const extracted = await extractEmailAddress(session.page);
if (extracted) {
session.accountEmail = extracted;
}
}
async function collectOriginStorage(context, origins) {
const sessionStorageByOrigin = {};
const indexedDbByOrigin = {};
if (!origins.length) return { sessionStorageByOrigin, indexedDbByOrigin };
const page = await context.newPage();
for (const origin of origins) {
try {
await page.goto(origin, { waitUntil: "domcontentloaded", timeout: 15000 });
const data = await page.evaluate(async ({ maxRecords }) => {
const sessionStorageData = {};
for (let i = 0; i < sessionStorage.length; i += 1) {
const key = sessionStorage.key(i);
if (key) sessionStorageData[key] = sessionStorage.getItem(key);
}
const indexedDbData = {};
if (indexedDB.databases) {
const dbs = await indexedDB.databases();
for (const dbInfo of dbs || []) {
if (!dbInfo?.name) continue;
const dbName = dbInfo.name;
const dbDump = await new Promise((resolve) => {
const request = indexedDB.open(dbName, dbInfo.version || 1);
request.onsuccess = () => {
const db = request.result;
const stores = {};
const storeNames = Array.from(db.objectStoreNames || []);
if (!storeNames.length) {
resolve({ version: db.version, stores });
return;
}
const tx = db.transaction(storeNames, "readonly");
const countLimit = maxRecords > 0 ? maxRecords : undefined;
const loadStore = (storeName) =>
new Promise((storeResolve) => {
const store = tx.objectStore(storeName);
const indexes = Array.from(store.indexNames || []).map((name) => {
const idx = store.index(name);
return {
name,
keyPath: idx.keyPath,
unique: idx.unique,
multiEntry: idx.multiEntry
};
});
const getAllReq = store.getAll(undefined, countLimit);
const getKeysReq = store.getAllKeys(undefined, countLimit);
const valuesPromise = new Promise((resolveValues) => {
getAllReq.onsuccess = () => resolveValues(getAllReq.result || []);
getAllReq.onerror = () => resolveValues([]);
});
const keysPromise = new Promise((resolveKeys) => {
getKeysReq.onsuccess = () => resolveKeys(getKeysReq.result || []);
getKeysReq.onerror = () => resolveKeys([]);
});
Promise.all([valuesPromise, keysPromise]).then(([values, keys]) => {
const records = [];
for (let i = 0; i < values.length; i += 1) {
records.push({ key: keys[i], value: values[i] });
}
stores[storeName] = {
keyPath: store.keyPath || null,
autoIncrement: !!store.autoIncrement,
indexes,
records
};
storeResolve();
});
});
Promise.all(storeNames.map((name) => loadStore(name))).then(() => {
resolve({ version: db.version, stores });
});
};
request.onerror = () => resolve(null);
});
if (dbDump) indexedDbData[dbName] = dbDump;
}
}
return { sessionStorageData, indexedDbData };
}, { maxRecords: MAX_IDB_RECORDS });
if (Object.keys(data.sessionStorageData || {}).length > 0) {
sessionStorageByOrigin[origin] = data.sessionStorageData;
}
if (Object.keys(data.indexedDbData || {}).length > 0) {
indexedDbByOrigin[origin] = data.indexedDbData;
}
} catch (err) {
console.warn(`Storage export failed for ${origin}:`, err.message);
}
}
await page.close();
return { sessionStorageByOrigin, indexedDbByOrigin };
}
function makeSnapshotId() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function makeSnapshotLabel(seed) {
if (!VERSION_WORDS.length) return "Version";
const source = seed ? String(seed) : crypto.randomBytes(8).toString("hex");
let hash = 0;
for (let i = 0; i < source.length; i += 1) {
hash = (hash * 31 + source.charCodeAt(i)) | 0;
}
const len = VERSION_WORDS.length;
const idx1 = Math.abs(hash) % len;
const idx2 = Math.abs((hash * 131 + 97) | 0) % len;
const word1 = VERSION_WORDS[idx1];
const word2 = VERSION_WORDS[idx2 === idx1 ? (idx2 + 1) % len : idx2];
return `${word1} ${word2}`;
}
function slugifyLabel(label) {
return String(label || "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function getFallbackEmail(session) {
if (!session) return null;
if (session.fallbackEmail) return session.fallbackEmail;
const baseLabel = makeSnapshotLabel(session.sessionId || `${Date.now()}`);
const slug = slugifyLabel(baseLabel) || "guest";
const suffix = (session.sessionId || "").split("-")[0] || Math.floor(Math.random() * 9999);
const email = `${slug}-${suffix}@unknown.local`;
session.fallbackEmail = email;
return email;
}
function snapshotIdToIso(snapshotId) {
if (!snapshotId) return null;
const match = snapshotId.match(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/);
if (match) {
return `${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`;
}
return snapshotId;
}
const AUTH_COOKIE_NAMES = new Set([
"SID",
"HSID",
"SSID",
"SAPISID",
"APISID",
"OSID",
"__Secure-1PSID",
"__Secure-3PSID",
"__Secure-1PSIDCC",
"__Secure-3PSIDCC",
"__Secure-OSID"
]);
async function hasGoogleAuthCookies(context) {
try {
const cookies = await context.cookies(["https://accounts.google.com", "https://mail.google.com"]);
for (const cookie of cookies || []) {
if (AUTH_COOKIE_NAMES.has(cookie.name)) return true;
}
} catch (err) {
console.warn("Auth cookie check failed:", err.message);
}
return false;
}
function looksLikeGmailMailboxUrl(url) {
if (!url || !url.includes("mail.google.com")) return false;
if (/\/mail\/u\/\d+\//.test(url)) return true;
if (/\/mail\/(?:#|$|\\?)/.test(url)) return true;
return false;
}
async function isAuthenticatedSession(session) {
if (!session?.page || session.page.isClosed?.()) return false;
const now = Date.now();
if (session.lastAuthCheckAt && now - session.lastAuthCheckAt < 2000) {
return !!session.lastAuthCheckResult;
}
let isLoggedIn = false;
try {
const url = session.page.url();
if (looksLikeGmailMailboxUrl(url) || url.includes("myaccount.google.com")) {
isLoggedIn = true;
} else if (url.includes("accounts.google.com")) {
const loginInputVisible = await session.page
.locator('input[type="email"], input[name="identifier"], input#identifierId, input[type="password"], input[name="Passwd"]')
.first()
.isVisible()
.catch(() => false);
if (!loginInputVisible) {
isLoggedIn = await hasGoogleAuthCookies(session.context);
}
} else if (url.includes("mail.google.com") || url.includes("myaccount.google.com")) {
const loginInputVisible = await session.page
.locator('input[type="email"], input[name="identifier"], input#identifierId, input[type="password"], input[name="Passwd"]')
.first()
.isVisible()
.catch(() => false);
if (!loginInputVisible) {
isLoggedIn = await hasGoogleAuthCookies(session.context);
}
}
} catch (err) {
console.warn("Auth check failed:", err.message);
}
session.lastAuthCheckAt = now;
session.lastAuthCheckResult = isLoggedIn;
return isLoggedIn;
}
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
async function saveSnapshot(session, email, reason = "manual") {
if (!supabase) {
console.warn("Supabase not configured; skipping snapshot");
return { ok: false, reason: "supabase_not_configured" };
}
const prefix = emailPrefix(email);
if (!prefix) return { ok: false, reason: "email_invalid" };
const snapshotId = makeSnapshotId();
const label = makeSnapshotLabel(snapshotId);
const savedAt = new Date().toISOString();
const localDir = path.join(BACKUPS_DIR, prefix, session.sessionId, "snapshots", snapshotId);
ensureDir(localDir);
console.log(`Snapshot ${snapshotId} for ${email}...`);
const statePath = path.join(localDir, "state.json");
const envPath = path.join(localDir, "env.json");
let storageState;
try {
storageState = await session.context.storageState({ path: statePath });
} catch (err) {
console.warn("Storage state capture failed:", err.message);
return { ok: false, reason: "storage_state_failed", details: err.message };
}
const origins = new Set((storageState.origins || []).map((entry) => entry.origin));
try {
const currentOrigin = new URL(session.page.url()).origin;
origins.add(currentOrigin);
} catch (err) {}
let sessionStorageByOrigin = {};
let indexedDbByOrigin = {};
try {
const collected = await collectOriginStorage(session.context, Array.from(origins));
sessionStorageByOrigin = collected.sessionStorageByOrigin;
indexedDbByOrigin = collected.indexedDbByOrigin;
} catch (err) {
console.warn("Origin storage capture failed:", err.message);
}
const savedContextOptions = session.contextOptions
? { ...session.contextOptions }
: buildContextOptions(session.deviceInfo, null, null);
if (savedContextOptions.storageState) delete savedContextOptions.storageState;
const env = {
version: 1,
email,
sessionId: session.sessionId,
snapshotId,
label,
reason,
savedAt,
lastUrl: session.page.url(),
profileKey: session.sessionId,
proxy: session.proxy
? {
id: session.proxy.id || null,
server: session.proxy.server || null,
protocol: session.proxy.protocol || null,
host: session.proxy.host || null,
port: session.proxy.port || null,
username: session.proxy.username || null,
password: session.proxy.password || null,
insecureTls: !!session.proxy.insecureTls
}
: null,
deviceInfo: session.deviceInfo,
contextOptions: savedContextOptions,
browserArgs: session.browserArgs || LAUNCH_ARGS,
stealth: true,
sessionStorageByOrigin,
indexedDbByOrigin
};
try {
fs.writeFileSync(envPath, JSON.stringify(env, null, 2));
} catch (err) {
console.warn("Env write failed:", err.message);
return { ok: false, reason: "env_write_failed", details: err.message };
}
const remoteBase = `sessions/${prefix}/${session.sessionId}/snapshots/${snapshotId}`;
const stateUpload = await uploadToSupabase(statePath, `${remoteBase}/state.json`);
const envUpload = await uploadToSupabase(envPath, `${remoteBase}/env.json`);
const stateOk = stateUpload.ok;
const envOk = envUpload.ok;
if (stateOk && envOk) {
const latestBase = `sessions/${prefix}/${session.sessionId}/latest`;
await uploadToSupabase(statePath, `${latestBase}/state.json`);
await uploadToSupabase(envPath, `${latestBase}/env.json`);
const metaRemote = `sessions/${prefix}/${session.sessionId}/meta.json`;
const metaExisting = (await downloadJsonFromSupabase(metaRemote)) || {};
const snapshots = Array.isArray(metaExisting.snapshots) ? metaExisting.snapshots : [];
const snapshotMeta = { snapshotId, label, savedAt };
snapshots.push(snapshotMeta);
const meta = {
email,
prefix,
sessionId: session.sessionId,
createdAt: metaExisting.createdAt || session.createdAt || savedAt,
lastSnapshotId: snapshotId,
lastSavedAt: savedAt,
snapshots
};
const metaPath = path.join(localDir, "meta.json");
try {
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
await uploadToSupabase(metaPath, metaRemote);
} catch (err) {
console.warn("Meta write/upload failed:", err.message);
}
}
if (!stateOk || !envOk) {
const detail = !stateOk ? `state: ${stateUpload.error}` : `env: ${envUpload.error}`;
console.warn(`Snapshot upload failed for ${email}`, detail);
return { ok: false, reason: "upload_failed", details: detail };
} else {
console.log(`Snapshot uploaded for ${email}`);
}
return { ok: true, label, savedAt, snapshotId, prefix };
}
async function autoSnapshot(session) {
if (!supabase) return;
if (session?.adminClient) return;
if (!session || session.snapshotInFlight) return;
const now = Date.now();
if (session.lastSnapshotAt && now - session.lastSnapshotAt < SNAPSHOT_INTERVAL_MS - 1000) return;
const isLoggedIn = await isAuthenticatedSession(session);
if (!isLoggedIn) return;
await maybeCaptureEmail(session);
const snapshotEmail = session.accountEmail || getFallbackEmail(session);
session.snapshotInFlight = true;
let result = null;
try {
result = await saveSnapshot(session, snapshotEmail, "auto");
} catch (err) {
console.warn("Auto snapshot failed:", err.message);
} finally {
session.snapshotInFlight = false;
}
if (result?.ok) {
session.lastSnapshotAt = Date.now();
} else if (result?.reason) {
const details = result?.details ? `: ${result.details}` : "";
console.warn(`Auto snapshot skipped (${result.reason}${details})`);
}
}
function ensureSnapshotTimer(session) {
if (!session || session.snapshotTimer) return;
session.snapshotTimer = setInterval(() => {
autoSnapshot(session).catch(() => {});
}, SNAPSHOT_INTERVAL_MS);
}
async function downloadSnapshotFromSupabase(prefix, sessionId, snapshotId) {
if (!supabase) throw new Error("supabase_not_configured");
if (!prefix || !sessionId) throw new Error("snapshot_missing");
const isLatest = !snapshotId || snapshotId === "latest";
const remoteBase = isLatest
? `sessions/${prefix}/${sessionId}/latest`
: `sessions/${prefix}/${sessionId}/snapshots/${snapshotId}`;
const localDir = path.join(BACKUPS_DIR, prefix, sessionId, isLatest ? "latest" : snapshotId);
ensureDir(localDir);
const stateRemote = `${remoteBase}/state.json`;
const envRemote = `${remoteBase}/env.json`;
const statePath = path.join(localDir, "state.json");
const envPath = path.join(localDir, "env.json");
const stateOk = await downloadFromSupabase(stateRemote, statePath);
const envOk = await downloadFromSupabase(envRemote, envPath);
if (!stateOk || !envOk) {
throw new Error("snapshot_files_missing");
}
const env = JSON.parse(fs.readFileSync(envPath, "utf8"));
return { statePath, envPath, env };
}
async function deleteSnapshotFromSupabase(prefix, sessionId, snapshotId) {
if (!supabase) throw new Error("supabase_not_configured");
if (!prefix || !sessionId || !snapshotId) throw new Error("snapshot_missing");
const snapshotBase = `sessions/${prefix}/${sessionId}/snapshots/${snapshotId}`;
const { data: snapshotFiles, error: snapshotListErr } = await supabase.storage
.from(supabaseBucket)
.list(snapshotBase, { limit: 1000, offset: 0, sortBy: { column: "name", order: "asc" } });
if (snapshotListErr) {
throw new Error(snapshotListErr.message);
}
const filePaths = (snapshotFiles || []).map((file) => `${snapshotBase}/${file.name}`);
if (filePaths.length) {
const { error: removeErr } = await supabase.storage.from(supabaseBucket).remove(filePaths);
if (removeErr) {
throw new Error(removeErr.message);
}
}
const metaRemote = `sessions/${prefix}/${sessionId}/meta.json`;
const metaExisting = (await downloadJsonFromSupabase(metaRemote)) || null;
if (!metaExisting || !Array.isArray(metaExisting.snapshots)) {
return { ok: true };
}
const prevLastId = metaExisting.lastSnapshotId || null;
const snapshots = metaExisting.snapshots.filter((snap) => snap?.snapshotId && snap.snapshotId !== snapshotId);
if (snapshots.length === 0) {
await supabase.storage.from(supabaseBucket).remove([metaRemote]);
const latestBase = `sessions/${prefix}/${sessionId}/latest`;
await supabase.storage.from(supabaseBucket).remove([`${latestBase}/state.json`, `${latestBase}/env.json`]);
return { ok: true, remaining: 0 };
}
snapshots.sort((a, b) => new Date(b.savedAt || 0).getTime() - new Date(a.savedAt || 0).getTime());
const latest = snapshots[0];
metaExisting.snapshots = snapshots;
metaExisting.lastSnapshotId = latest.snapshotId;
metaExisting.lastSavedAt = latest.savedAt || new Date().toISOString();
const metaPath = path.join(BACKUPS_DIR, prefix, sessionId, "meta.json");
ensureDir(path.dirname(metaPath));
fs.writeFileSync(metaPath, JSON.stringify(metaExisting, null, 2));
await uploadToSupabase(metaPath, metaRemote);
if (prevLastId === snapshotId || !prevLastId) {
try {
const { statePath, envPath } = await downloadSnapshotFromSupabase(prefix, sessionId, latest.snapshotId);
const latestBase = `sessions/${prefix}/${sessionId}/latest`;
await uploadToSupabase(statePath, `${latestBase}/state.json`);
await uploadToSupabase(envPath, `${latestBase}/env.json`);
} catch (err) {
console.warn("Failed to refresh latest snapshot", err.message);
}
}
return { ok: true, remaining: snapshots.length };
}
function getPageId(session, page) {
if (!page) return null;
if (!page.__adminId) {
page.__adminId = `${session.sessionId}-${session.nextPageId++}`;
}
return page.__adminId;
}
function sendTabs(session) {
if (!session?.adminClient || !session.ws) return;
if (session.ws.readyState !== session.ws.OPEN) return;
const tabs = [];
for (const id of session.pageOrder) {
const page = session.pages.get(id);
if (!page || page.isClosed?.()) continue;
tabs.push({ id, url: page.url() });
}
if (!tabs.length) return;
if (!session.activePageId || !session.pages.get(session.activePageId)) {
session.activePageId = tabs[0].id;
}
session.ws.send(JSON.stringify({ type: "tabs", tabs, activeId: session.activePageId }));
}
function setActivePage(session, page) {
if (!page || page.isClosed?.()) return;
const id = getPageId(session, page);
session.page = page;
session.activePageId = id;
session.streaming = true;
streamLoop(session);
sendTabs(session);
}
function registerPage(session, page, { makeActive = false } = {}) {
if (!page || page.isClosed?.()) return;
installSignInCopyOverride(page).catch(() => {});
const id = getPageId(session, page);
if (!session.pages.has(id)) {
session.pages.set(id, page);
session.pageOrder.push(id);
}
page.on("close", () => {
session.pages.delete(id);
session.pageOrder = session.pageOrder.filter((pid) => pid !== id);
if (session.page === page) {
const nextId = session.pageOrder.find((pid) => {
const p = session.pages.get(pid);
return p && !p.isClosed?.();
});
if (nextId) {
setActivePage(session, session.pages.get(nextId));
} else {
session.page = null;
session.activePageId = null;
}
}
sendTabs(session);
});
page.on("framenavigated", async (frame) => {
if (frame !== page.mainFrame()) return;
try {
const nextUrl = page.url();
if (!isInvalidPageUrl(nextUrl)) {
session.lastKnownUrl = nextUrl;
}
} catch {}
if (page === session.page) {
await handlePasskeyPrompt(page, session);
await maybePersistSession(session);
}
if (session.adminClient) {
sendTabs(session);
}
});
if (makeActive) {
setActivePage(session, page);
} else {
sendTabs(session);
}
}
function getStreamIntervalMs(session) {
return Math.min(1000, Math.max(60, Number(session?.streamIntervalMs || STREAM_INTERVAL_MS)));
}
function getStreamQuality(session) {
return Math.min(100, Math.max(10, Number(session?.streamQuality || STREAM_JPEG_QUALITY)));
}
async function stopScreencast(session) {
if (!session) return;
const cdp = session.cdpSession;
if (cdp && session.screencastListener && cdp.removeListener) {
cdp.removeListener("Page.screencastFrame", session.screencastListener);
}
session.screencastListener = null;
session.screencastPage = null;
session.screencastActive = false;
session.streamLoopRunning = false;
if (cdp) {
try {
await cdp.send("Page.stopScreencast");
} catch {}
try {
await cdp.detach();
} catch {}
}
session.cdpSession = null;
}
async function startScreencast(session) {
if (!session?.page || session.page.isClosed?.()) return false;
if (!session.context) return false;
if (session.screencastActive && session.screencastPage === session.page) {
return true;
}
await stopScreencast(session);
const page = session.page;
const viewport = page.viewportSize() || { width: 1280, height: 720 };
const quality = getStreamQuality(session);
const everyNthFrame = session.adminClient ? ADMIN_EVERY_NTH_FRAME : 1;
let cdp;
try {
cdp = await session.context.newCDPSession(page);
} catch (err) {
console.warn("Failed to create CDP session:", err.message);
return false;
}
session.cdpSession = cdp;
session.screencastPage = page;
session.screencastActive = true;
session.streamLoopRunning = true;
const onFrame = async (payload) => {
const ack = () => {
cdp
.send("Page.screencastFrameAck", { sessionId: payload.sessionId })
.catch(() => {});
};
try {
if (!session.streaming) {
return;
}
const ws = session.ws;
if (!ws || ws.readyState !== ws.OPEN) {
return;
}
const now = Date.now();
if (session.freezeStream && session.freezeUntil && now >= session.freezeUntil) {
session.freezeStream = false;
session.freezeUntil = null;
session.freezeReason = null;
}
if (session.freezeStream && session.lastFrame && session.freezeUntil && now < session.freezeUntil) {
session.lastFrameAt = now;
ws.send(JSON.stringify(session.lastFrame));
return;
}
const meta = payload.metadata || {};
const width = Math.round(meta.deviceWidth || viewport.width);
const height = Math.round(meta.deviceHeight || viewport.height);
const frame = {
type: "frame",
width,
height,
data: payload.data
};
session.lastFrame = frame;
session.lastFrameAt = now;
ws.send(JSON.stringify(frame));
} catch (err) {
if (!/Target page|browser has been closed/i.test(err?.message || "")) {
console.warn("Screencast error:", err.message);
}
} finally {
ack();
}
};
session.screencastListener = onFrame;
cdp.on("Page.screencastFrame", onFrame);
try {
await cdp.send("Page.enable");
await cdp.send("Page.startScreencast", {
format: "jpeg",
quality,
maxWidth: viewport.width,
maxHeight: viewport.height,
everyNthFrame
});
return true;
} catch (err) {
console.warn("Failed to start screencast:", err.message);
await stopScreencast(session);
return false;
}
}
async function restartScreencast(session) {
await stopScreencast(session);
return startScreencast(session);
}
async function screenshotLoop(session) {
while (session.streaming) {
const intervalMs = getStreamIntervalMs(session);
try {
const ws = session.ws;
if (!ws || ws.readyState !== ws.OPEN) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
continue;
}
const page = session.page;
if (!page || page.isClosed?.()) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
continue;
}
if (session.freezeStream && session.lastFrame && session.freezeUntil && Date.now() < session.freezeUntil) {
session.lastFrameAt = Date.now();
ws.send(JSON.stringify(session.lastFrame));
} else {
if (session.freezeStream && session.freezeUntil && Date.now() >= session.freezeUntil) {
session.freezeStream = false;
session.freezeUntil = null;
session.freezeReason = null;
}
const quality = getStreamQuality(session);
const buffer = await page.screenshot({ type: "jpeg", quality, caret: "initial" });
const viewport = page.viewportSize() || { width: 1280, height: 720 };
const payload = {
type: "frame",
width: viewport.width,
height: viewport.height,
data: buffer.toString("base64")
};
session.lastFrame = payload;
session.lastFrameAt = Date.now();
ws.send(JSON.stringify(payload));
}
} catch (err) {
if (!/Target page|browser has been closed/i.test(err?.message || "")) {
console.warn("Screenshot error:", err.message);
}
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
session.streamLoopRunning = false;
}
async function streamLoop(session) {
if (STREAM_MODE === "screencast") {
if (session.streamLoopRunning && session.screencastPage === session.page) return;
const ok = await startScreencast(session);
if (!ok) {
session.streamLoopRunning = false;
session.streaming = false;
if (session.ws?.readyState === session.ws.OPEN) {
session.ws.send(
JSON.stringify({
type: "busy",
retryAfterSec: Math.round(IDLE_TIMEOUT_MS / 1000)
})
);
}
await terminateBrowserSession(session);
}
return;
}
if (session.streamLoopRunning) return;
session.streamLoopRunning = true;
await screenshotLoop(session);
}
async function maybePersistSession(session) {
await maybeCaptureEmail(session);
const isLoggedIn = await isAuthenticatedSession(session);
if (!isLoggedIn) {
session.loggedIn = false;
return;
}
const firstLogin = !session.loggedIn;
session.loggedIn = true;
if (firstLogin && !session.adminClient && session.ws && session.ws.readyState === session.ws.OPEN) {
session.ws.send(JSON.stringify({ type: "logged_in" }));
}
if (session.adminClient) return;
ensureSnapshotTimer(session);
if (!session.lastSnapshotAt) {
await autoSnapshot(session);
}
if (firstLogin) {
terminateBrowserSession(session).catch(() => {});
}
}
async function closeSession(session) {
if (!session) return;
await stopScreencast(session);
if (session.detachTimer) {
clearTimeout(session.detachTimer);
session.detachTimer = null;
}
session.streaming = false;
clearIdleTimer(session);
stopSelfHealMonitor(session);
if (session.passkeyInterval) {
clearInterval(session.passkeyInterval);
session.passkeyInterval = null;
}
if (session.snapshotTimer) {
clearInterval(session.snapshotTimer);
session.snapshotTimer = null;
}
stopBlankGuard(session);
try {
await session.browser?.close();
} catch (err) {
console.warn("Browser close failed", err);
}
sessionsById.delete(session.sessionId);
if (session.ws) {
sessions.delete(session.ws);
}
}
function detachSession(session) {
if (!session || session.detached) return;
session.detached = true;
session.streaming = false;
stopScreencast(session).catch(() => {});
if (session.passkeyInterval) {
clearInterval(session.passkeyInterval);
session.passkeyInterval = null;
}
if (session.snapshotTimer) {
clearInterval(session.snapshotTimer);
session.snapshotTimer = null;
}
stopBlankGuard(session);
if (session.detachTimer) {
clearTimeout(session.detachTimer);
}
session.detachTimer = setTimeout(() => {
closeSession(session).catch(() => {});
}, DETACH_TTL_MS);
}
async function terminateBrowserSession(session) {
await stopScreencast(session);
session.streaming = false;
const start = Date.now();
while (session.streamLoopRunning && Date.now() - start < 2000) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
clearIdleTimer(session);
stopSelfHealMonitor(session);
if (session.passkeyInterval) {
clearInterval(session.passkeyInterval);
session.passkeyInterval = null;
}
if (session.snapshotTimer) {
clearInterval(session.snapshotTimer);
session.snapshotTimer = null;
}
stopBlankGuard(session);
try {
await session.context?.close();
} catch (err) {}
try {
await session.browser?.close();
} catch (err) {}
session.page = null;
session.context = null;
session.browser = null;
}
async function attachToExistingSession(ws, session, target) {
if (!target || !target.browser) return session;
if (session && session.sessionId !== target.sessionId) {
sessionsById.delete(session.sessionId);
sessions.delete(ws);
}
if (target.ws && target.ws !== ws) {
sessions.delete(target.ws);
}
target.ws = ws;
sessions.set(ws, target);
target.detached = false;
if (target.detachTimer) {
clearTimeout(target.detachTimer);
target.detachTimer = null;
}
target.streaming = true;
if (!target.passkeyInterval && target.page) {
target.passkeyInterval = setInterval(() => {
handlePasskeyPrompt(target.page, target).catch(() => {});
}, 2000);
}
startBlankGuard(target);
streamLoop(target);
startSelfHealMonitor(target);
ws.send(JSON.stringify({ type: "session", sessionId: target.sessionId }));
ws.send(JSON.stringify({ type: "attached" }));
markSessionActivity(target);
return target;
}
wss.on("connection", (ws, req) => {
const isAdmin = hasAdminAccess(req);
const sessionId = uuidv4();
let session = {
ws,
sessionId,
createdAt: new Date().toISOString(),
deviceInfo: null,
browser: null,
context: null,
page: null,
contextOptions: null,
browserArgs: null,
streaming: false,
adjustedFirstPage: false,
persisted: false,
userId: null,
passkeyInterval: null,
passkeyBypassInFlight: false,
hasSeenPasswordInput: false,
lastFrame: null,
lastFrameAt: null,
streamQuality: STREAM_JPEG_QUALITY,
streamIntervalMs: STREAM_INTERVAL_MS,
freezeStream: false,
freezeUntil: null,
freezeReason: null,
lockViewport: false,
cdpSession: null,
screencastActive: false,
screencastPage: null,
screencastListener: null,
accountEmail: null,
localSaved: false,
backupSaved: false,
streamLoopRunning: false,
detached: false,
detachTimer: null,
adminBusy: false,
loggedIn: false,
snapshotTimer: null,
snapshotInFlight: false,
lastSnapshotAt: null,
lastActivityAt: null,
idleTimer: null,
hibernatedAt: null,
hibernatedReason: null,
lastAuthCheckAt: null,
lastAuthCheckResult: null,
lastEmailCaptureAt: null,
lastHeartbeatAt: null,
lastHeartbeatHidden: null,
lastSelfHealLogAt: null,
lastSelfHealAction: null,
lastBrainAt: null,
lastBrainAction: null,
lastBrainError: null,
lastKnownUrl: null,
fallbackEmail: null,
adminClient: false,
pages: new Map(),
pageOrder: [],
activePageId: null,
nextPageId: 1,
isAdmin,
pendingStart: false,
pendingStartPayload: null,
blankGuardInterval: null,
selfHealInterval: null,
selfHealInFlight: false,
profileDir: null,
envOverrides: null,
proxy: null
};
sessions.set(ws, session);
sessionsById.set(sessionId, session);
ws.send(JSON.stringify({ type: "session", sessionId }));
ws.on("message", async (data) => {
let msg;
try {
msg = JSON.parse(data.toString());
} catch {
return;
}
if (msg.type === "attach") {
const targetId = msg.payload?.sessionId;
if (!targetId) {
ws.send(JSON.stringify({ type: "attach_failed", message: "missing_session_id" }));
return;
}
const target = sessionsById.get(targetId);
if (!target || !target.browser) {
ws.send(JSON.stringify({ type: "attach_failed", message: "not_found" }));
return;
}
session = await attachToExistingSession(ws, session, target);
return;
}
if (msg.type === "device_info") {
session.deviceInfo = normalizeDeviceInfo(msg.payload);
session.userId = msg.payload?.userId || null;
if (session.pendingStart) {
const payload = session.pendingStartPayload || {};
session.pendingStart = false;
session.pendingStartPayload = null;
try {
await startNewSession(session, ws, payload);
} catch (err) {
console.error("Failed to start Playwright session:", err.message);
ws.send(JSON.stringify({ type: "error", message: err.message }));
}
}
return;
}
if (msg.type === "heartbeat") {
session.lastHeartbeatAt = Date.now();
session.lastHeartbeatHidden = !!msg.payload?.hidden;
if (msg.payload?.selfHeal) {
runSelfHealCheck(session, "heartbeat").catch(() => {});
}
return;
}
if (msg.type === "sync") {
if (session.ws?.readyState === session.ws.OPEN && session.lastFrame) {
session.ws.send(JSON.stringify(session.lastFrame));
}
if (session.loggedIn && !session.adminClient && session.ws?.readyState === session.ws.OPEN) {
session.ws.send(JSON.stringify({ type: "logged_in" }));
}
return;
}
if (msg.type === "quality") {
const quality = Number(msg.payload?.quality);
const intervalMs = Number(msg.payload?.intervalMs);
if (Number.isFinite(quality)) {
session.streamQuality = Math.min(100, Math.max(10, Math.round(quality)));
}
if (Number.isFinite(intervalMs)) {
session.streamIntervalMs = Math.min(1000, Math.max(60, Math.round(intervalMs)));
}
if (STREAM_MODE === "screencast") {
restartScreencast(session).catch(() => {});
}
return;
}
if (msg.type === "ping") {
if (session.ws?.readyState === session.ws.OPEN) {
session.ws.send(
JSON.stringify({
type: "pong",
ts: msg.payload?.ts || Date.now()
})
);
}
return;
}
if (msg.type === "admin_client") {
session.adminClient = true;
session.streamQuality = ADMIN_STREAM_JPEG_QUALITY;
if (STREAM_MODE === "screencast") {
restartScreencast(session).catch(() => {});
}
return;
}
if (msg.type === "start") {
try {
if (!session.deviceInfo) {
session.pendingStart = true;
session.pendingStartPayload = msg.payload || {};
return;
}
const existing = findActiveSessionByUserId(session.userId, session.sessionId);
if (existing) {
session = await attachToExistingSession(ws, session, existing);
return;
}
if (hasOtherActiveSession(session.sessionId)) {
ws.send(JSON.stringify({ type: "busy", retryAfterSec: Math.round(IDLE_TIMEOUT_MS / 1000) }));
return;
}
await startNewSession(session, ws, msg.payload || {});
markSessionActivity(session);
} catch (err) {
console.error("Failed to start Playwright session:", err.message);
ws.send(JSON.stringify({ type: "error", message: err.message }));
}
return;
}
if (msg.type === "resume") {
if (!session.deviceInfo) return;
if (hasOtherActiveSession(session.sessionId)) {
ws.send(JSON.stringify({ type: "busy", retryAfterSec: Math.round(IDLE_TIMEOUT_MS / 1000) }));
return;
}
const { storageStatePath, remotePath } = msg.payload || {};
let localStatePath = storageStatePath;
if (!localStatePath && remotePath) {
localStatePath = path.join(SESSIONS_DIR, `${session.sessionId}.json`);
await downloadFromSupabase(remotePath, localStatePath);
}
const resumeProfileDir = path.join(PROFILES_DIR, session.sessionId);
ensureDir(resumeProfileDir);
const selectedProxy = session.proxy || selectProxyForSession();
session.proxy = selectedProxy;
const playwrightSession = await startPlaywrightSession({
ws,
deviceInfo: session.deviceInfo,
existingStatePath: localStatePath,
profileDir: resumeProfileDir,
proxy: selectedProxy
});
session.browser = playwrightSession.browser;
session.context = playwrightSession.context;
session.page = playwrightSession.page;
session.contextOptions = playwrightSession.contextOptions;
session.browserArgs = playwrightSession.browserArgs;
session.profileDir = resumeProfileDir;
session.envOverrides = null;
session.streaming = true;
session.detached = false;
session.hibernatedAt = null;
session.hibernatedReason = null;
session.lastFrameAt = null;
if (session.detachTimer) {
clearTimeout(session.detachTimer);
session.detachTimer = null;
}
if (session.context && !session.context.__tabsAttached) {
session.context.__tabsAttached = true;
session.context.on("page", (page) => {
registerPage(session, page, { makeActive: true });
});
}
registerPage(session, session.page, { makeActive: true });
await session.page.goto("https://mail.google.com/", { waitUntil: "domcontentloaded" });
try {
session.lastKnownUrl = session.page.url();
} catch {}
session.passkeyInterval = setInterval(() => {
handlePasskeyPrompt(session.page, session).catch(() => {});
}, 2000);
startBlankGuard(session);
markSessionActivity(session);
await maybePersistSession(session);
streamLoop(session);
startSelfHealMonitor(session);
return;
}
if (msg.type === "admin_resume") {
if (!session.isAdmin) {
ws.send(JSON.stringify({ type: "error", message: "admin_auth_required" }));
return;
}
session.adminClient = true;
const prefix = msg.payload?.prefix;
const sessionKey = msg.payload?.sessionId;
const snapshotId = msg.payload?.snapshotId;
if (!prefix || !sessionKey) return;
if (session.adminBusy) {
ws.send(JSON.stringify({ type: "error", message: "admin_busy" }));
return;
}
session.adminBusy = true;
try {
await terminateBrowserSession(session);
const { statePath, env } = await downloadSnapshotFromSupabase(prefix, sessionKey, snapshotId);
if (!env) throw new Error("env_missing");
const envDeviceInfo = env.deviceInfo || {
deviceType: "desktop",
language: env.contextOptions?.locale || "en-US",
timeZone: env.contextOptions?.timezoneId || null,
viewport: env.contextOptions?.viewport || { width: 1280, height: 720 },
screen: env.contextOptions?.screen || { width: 1280, height: 720 },
userAgent: env.contextOptions?.userAgent || "",
deviceScaleFactor: env.contextOptions?.deviceScaleFactor || 1
};
session.deviceInfo = normalizeDeviceInfo(envDeviceInfo);
session.accountEmail = env.email || session.accountEmail;
session.loggedIn = true;
session.lockViewport = ADMIN_LOCK_VIEWPORT;
session.hibernatedAt = null;
session.hibernatedReason = null;
session.lastFrameAt = null;
const profileKey = env.profileKey || env.sessionId || sessionKey;
const profileDirCandidate = profileKey ? path.join(PROFILES_DIR, profileKey) : null;
const profileExists =
!!profileDirCandidate &&
fs.existsSync(profileDirCandidate) &&
fs.readdirSync(profileDirCandidate).length > 0;
const shouldUsePersistent = (env.persistentProfile ?? PERSISTENT_PROFILE) && profileExists;
const adminProfileDir = shouldUsePersistent ? profileDirCandidate : null;
if (adminProfileDir) ensureDir(adminProfileDir);
const envOverrides = { ...env, persistentProfile: shouldUsePersistent };
const selectedProxy = normalizeProxyRecord(env.proxy) || selectProxyForSession();
session.proxy = selectedProxy;
const playwrightSession = await startPlaywrightSession({
ws,
deviceInfo: session.deviceInfo,
existingStatePath: statePath,
envOverrides,
profileDir: adminProfileDir,
proxy: selectedProxy
});
session.browser = playwrightSession.browser;
session.context = playwrightSession.context;
session.page = playwrightSession.page;
session.contextOptions = playwrightSession.contextOptions;
session.browserArgs = playwrightSession.browserArgs;
session.profileDir = adminProfileDir || profileDirCandidate || null;
session.envOverrides = envOverrides;
session.streaming = true;
if (session.context && !session.context.__tabsAttached) {
session.context.__tabsAttached = true;
session.context.on("page", (page) => {
registerPage(session, page, { makeActive: true });
});
}
registerPage(session, session.page, { makeActive: true });
const lastUrl = env.lastUrl || "";
const targetUrl =
lastUrl.includes("mail.google.com") || lastUrl.includes("myaccount.google.com")
? lastUrl
: "https://mail.google.com/";
await session.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
try {
session.lastKnownUrl = session.page.url();
} catch {}
session.passkeyInterval = setInterval(() => {
handlePasskeyPrompt(session.page, session).catch(() => {});
}, 2000);
startBlankGuard(session);
streamLoop(session);
startSelfHealMonitor(session);
} catch (err) {
console.error("Admin resume failed:", err.message);
ws.send(JSON.stringify({ type: "error", message: err.message }));
} finally {
session.adminBusy = false;
}
return;
}
if (msg.type === "manual_save") {
if (!session.page || session.page.isClosed?.()) {
ws.send(JSON.stringify({ type: "save_failed", message: "page_closed" }));
return;
}
if (session.snapshotInFlight) {
ws.send(JSON.stringify({ type: "save_failed", message: "snapshot_in_flight" }));
return;
}
const isLoggedIn = await isAuthenticatedSession(session);
if (!isLoggedIn) {
ws.send(JSON.stringify({ type: "save_failed", message: "not_logged_in" }));
return;
}
await maybeCaptureEmail(session);
const snapshotEmail = session.accountEmail || getFallbackEmail(session);
session.snapshotInFlight = true;
let result = null;
try {
result = await saveSnapshot(session, snapshotEmail, "manual");
} catch (err) {
console.warn("Manual snapshot failed:", err.message);
} finally {
session.snapshotInFlight = false;
}
if (result?.ok) {
session.lastSnapshotAt = Date.now();
ws.send(
JSON.stringify({
type: "save_saved",
email: snapshotEmail,
label: result.label,
savedAt: result.savedAt,
snapshotId: result.snapshotId
})
);
} else {
const reason = result?.reason || "save_failed";
const details = result?.details ? `: ${result.details}` : "";
ws.send(JSON.stringify({ type: "save_failed", message: `${reason}${details}` }));
}
return;
}
if (msg.type === "admin_navigate") {
if (!session.isAdmin || !session.adminClient) {
ws.send(JSON.stringify({ type: "error", message: "admin_auth_required" }));
return;
}
const rawUrl = msg.payload?.url;
const targetUrl = normalizeTargetUrl(rawUrl);
if (!targetUrl) {
ws.send(JSON.stringify({ type: "error", message: "invalid_url" }));
return;
}
if (!session.context) {
ws.send(JSON.stringify({ type: "error", message: "no_browser" }));
return;
}
const openInNewTab = !!msg.payload?.newTab;
try {
let targetPage = session.page;
if (openInNewTab || !targetPage || targetPage.isClosed?.()) {
targetPage = await session.context.newPage();
}
registerPage(session, targetPage, { makeActive: true });
await targetPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
} catch (err) {
ws.send(JSON.stringify({ type: "error", message: err.message }));
}
return;
}
if (msg.type === "admin_switch_tab") {
if (!session.isAdmin || !session.adminClient) {
ws.send(JSON.stringify({ type: "error", message: "admin_auth_required" }));
return;
}
const tabId = msg.payload?.tabId;
if (!tabId) return;
const page = session.pages.get(tabId);
if (!page || page.isClosed?.()) {
ws.send(JSON.stringify({ type: "error", message: "tab_not_found" }));
return;
}
setActivePage(session, page);
return;
}
if (!session.page) return;
if (msg.type === "resize") {
if (session.lockViewport) return;
const { width, height } = msg.payload || {};
if (width && height) {
await session.page.setViewportSize({
width: Math.max(320, Math.floor(width)),
height: Math.max(480, Math.floor(height))
});
if (STREAM_MODE === "screencast") {
restartScreencast(session).catch(() => {});
}
markSessionActivity(session);
}
return;
}
if (msg.type === "nav") {
const action = msg.payload?.action;
if (!session.page || session.page.isClosed?.()) return;
try {
if (action === "back") {
const response = await session.page.goBack({ waitUntil: "domcontentloaded", timeout: 15000 });
const currentUrl = session.page.url();
if (!response || isInvalidPageUrl(currentUrl)) {
await ensureLoginPage(session);
}
} else if (action === "forward") {
const response = await session.page.goForward({ waitUntil: "domcontentloaded", timeout: 15000 });
const currentUrl = session.page.url();
if (!response || isInvalidPageUrl(currentUrl)) {
await ensureLoginPage(session);
}
} else if (action === "reload") {
await session.page.reload({ waitUntil: "domcontentloaded", timeout: 15000 });
}
markSessionActivity(session);
} catch (err) {
console.warn("Navigation error:", err.message);
ws.send(JSON.stringify({ type: "error", message: err.message }));
}
return;
}
if (msg.type === "event") {
if (session.adminBusy) return;
const evt = msg.payload;
if (!evt) return;
if (!session.page || session.page.isClosed?.()) return;
try {
if (evt.kind === "mouse_move") {
await session.page.mouse.move(evt.x, evt.y);
} else if (evt.kind === "mouse_down") {
await session.page.mouse.move(evt.x, evt.y);
await session.page.mouse.down({ button: evt.button || "left" });
} else if (evt.kind === "mouse_up") {
await session.page.mouse.move(evt.x, evt.y);
await session.page.mouse.up({ button: evt.button || "left" });
} else if (evt.kind === "click") {
if (!session.freezeStream) {
const isNextClick = await session.page
.evaluate(({ x, y }) => {
const el = document.elementFromPoint(x, y);
if (!el) return false;
const button = el.closest('button, input[type="submit"], div[role="button"]');
if (!button) return false;
const text = (button.innerText || button.getAttribute("aria-label") || button.value || "")
.trim()
.toLowerCase();
if (!text.includes("next")) return false;
const emailInput = document.querySelector('input[type="email"], input[name="identifier"], input#identifierId');
if (!emailInput) return false;
const rect = emailInput.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) return false;
return true;
}, { x: evt.x, y: evt.y })
.catch(() => false);
if (isNextClick) {
beginEmailNextFreeze(session);
if (!session.accountEmail) {
const emailValue = await session.page
.evaluate(() => {
const input = document.querySelector('input[type="email"], input[name="identifier"], input#identifierId');
return input?.value || "";
})
.catch(() => "");
const normalized = normalizeEmailAddress(emailValue);
if (normalized) {
session.accountEmail = normalized;
}
}
watchEmailNextProgress(session, session.page);
}
}
} else if (evt.kind === "wheel") {
await session.page.mouse.wheel(evt.deltaX || 0, evt.deltaY || 0);
} else if (evt.kind === "key_press") {
if (evt.key === "Dead") return;
if (evt.key === "Enter") {
const onEmailStep = await isEmailStep(session.page);
const handled = onEmailStep ? await clickEmailNext(session.page) : false;
if (handled) {
beginEmailNextFreeze(session);
if (!session.accountEmail) {
const emailValue = await session.page
.evaluate(() => {
const input = document.querySelector(
'input[type="email"], input[name="identifier"], input#identifierId'
);
return input?.value || "";
})
.catch(() => "");
const normalized = normalizeEmailAddress(emailValue);
if (normalized) {
session.accountEmail = normalized;
}
}
watchEmailNextProgress(session, session.page);
return;
}
}
if (evt.key) {
await session.page.keyboard.press(evt.key);
}
} else if (evt.kind === "key_down") {
if (evt.key === "Dead") {
return;
}
if (evt.key && evt.key.length === 1) {
await session.page.keyboard.insertText(evt.key);
} else if (evt.key) {
await session.page.keyboard.press(evt.key);
}
} else if (evt.kind === "key_combo") {
const comboParts = [];
if (evt.ctrlKey) comboParts.push("Control");
if (evt.metaKey) comboParts.push("Meta");
if (evt.shiftKey) comboParts.push("Shift");
if (evt.altKey) comboParts.push("Alt");
const key = evt.key && evt.key.length === 1 ? evt.key.toUpperCase() : evt.key;
if (key) comboParts.push(key);
const combo = comboParts.join("+");
if (combo) {
await session.page.keyboard.press(combo);
}
} else if (evt.kind === "key_up") {
// no-op
} else if (evt.kind === "type") {
await session.page.keyboard.type(evt.text || "");
} else if (evt.kind === "paste") {
if (evt.text) {
await session.page.keyboard.insertText(evt.text);
}
}
} catch (err) {
if (!/Target page|browser has been closed/i.test(err?.message || "")) {
console.warn("Event handling failed:", err.message);
}
return;
}
markSessionActivity(session);
await handlePasskeyPrompt(session.page, session);
await maybePersistSession(session);
return;
}
if (msg.type === "copy_request") {
try {
const text = await session.page.evaluate(() => {
const active = document.activeElement;
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) {
const start = active.selectionStart ?? 0;
const end = active.selectionEnd ?? 0;
return (active.value || "").substring(start, end);
}
return window.getSelection?.().toString() || "";
});
if (session.ws?.readyState === session.ws.OPEN) {
session.ws.send(JSON.stringify({ type: "copy_payload", text }));
}
} catch (err) {
ws.send(JSON.stringify({ type: "error", message: "copy_failed" }));
}
return;
}
});
ws.on("close", async () => {
if (session?.ws === ws) {
detachSession(session);
} else {
sessions.delete(ws);
}
});
});