iframer-toolkit 0.10.8 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,11 +38,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
38
  // src/lib/paths.ts
39
39
  var paths_exports = {};
40
40
  __export(paths_exports, {
41
- getDataDir: () => getDataDir
41
+ getDataDir: () => getDataDir,
42
+ getScreenshotDir: () => getScreenshotDir
42
43
  });
43
44
  function getDataDir() {
44
45
  return process.env.IFRAMER_DATA_DIR || import_path.default.join(import_os.default.homedir(), ".iframer");
45
46
  }
47
+ function getScreenshotDir() {
48
+ return import_path.default.join(getDataDir(), "screenshots");
49
+ }
46
50
  var import_path, import_os;
47
51
  var init_paths = __esm({
48
52
  "src/lib/paths.ts"() {
@@ -52,90 +56,23 @@ var init_paths = __esm({
52
56
  }
53
57
  });
54
58
 
55
- // src/lib/session/persistence.ts
56
- var persistence_exports = {};
57
- __export(persistence_exports, {
58
- extractSession: () => extractSession,
59
- injectCookies: () => injectCookies,
60
- injectStorage: () => injectStorage
61
- });
62
- async function extractSession(context, page) {
63
- const cookies = await context.cookies();
64
- const { localStorage, sessionStorage } = await page.evaluate(() => {
65
- const ls = {};
66
- const ss = {};
67
- for (let i = 0; i < window.localStorage.length; i++) {
68
- const key = window.localStorage.key(i);
69
- ls[key] = window.localStorage.getItem(key);
70
- }
71
- for (let i = 0; i < window.sessionStorage.length; i++) {
72
- const key = window.sessionStorage.key(i);
73
- ss[key] = window.sessionStorage.getItem(key);
74
- }
75
- return { localStorage: ls, sessionStorage: ss };
76
- });
77
- const origin = new URL(page.url()).origin;
78
- return {
79
- cookies,
80
- localStorage: { [origin]: localStorage },
81
- sessionStorage: { [origin]: sessionStorage },
82
- extractedAt: (/* @__PURE__ */ new Date()).toISOString()
83
- };
84
- }
85
- async function injectCookies(context, sessionData) {
86
- if (sessionData?.cookies?.length > 0) {
87
- await context.addCookies(sessionData.cookies);
88
- }
89
- }
90
- async function injectStorage(page, sessionData) {
91
- if (!sessionData) return;
92
- const origin = new URL(page.url()).origin;
93
- const ls = sessionData.localStorage?.[origin];
94
- const ss = sessionData.sessionStorage?.[origin];
95
- if (ls && Object.keys(ls).length > 0) {
96
- await page.evaluate((data) => {
97
- for (const [key, value] of Object.entries(data)) {
98
- window.localStorage.setItem(key, value);
99
- }
100
- }, ls);
101
- }
102
- if (ss && Object.keys(ss).length > 0) {
103
- await page.evaluate((data) => {
104
- for (const [key, value] of Object.entries(data)) {
105
- window.sessionStorage.setItem(key, value);
106
- }
107
- }, ss);
108
- }
109
- }
110
- var init_persistence = __esm({
111
- "src/lib/session/persistence.ts"() {
112
- "use strict";
113
- }
114
- });
115
-
116
59
  // index.ts
117
60
  var import_express = __toESM(require("express"));
118
- var import_path12 = __toESM(require("path"));
119
- var import_fs13 = __toESM(require("fs"));
120
- var import_url2 = require("url");
61
+ var import_path11 = __toESM(require("path"));
62
+ var import_fs11 = __toESM(require("fs"));
63
+ var import_url = require("url");
64
+ init_paths();
121
65
 
122
66
  // src/api/routes.ts
123
67
  var import_patchright3 = require("patchright");
124
68
 
125
69
  // src/lib/iframer.ts
126
- var import_path11 = __toESM(require("path"));
127
- var import_url = require("url");
128
-
129
- // src/lib/browser/session-manager.ts
130
- var import_child_process2 = require("child_process");
131
- var import_fs3 = __toESM(require("fs"));
70
+ init_paths();
132
71
 
133
72
  // src/lib/browser/launcher.ts
134
- var import_fs2 = __toESM(require("fs"));
135
73
  var import_patchright = require("patchright");
136
74
 
137
75
  // src/lib/browser/stealth.ts
138
- var contextStealthScripts = /* @__PURE__ */ new Map();
139
76
  var CHROME_VERSION = "136.0.7103.93";
140
77
  var USER_AGENT = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROME_VERSION} Safari/537.36`;
141
78
  var NATIVE_TOSTRING_HELPER = `
@@ -561,8 +498,8 @@ function isPidAlive(pid) {
561
498
  try {
562
499
  process.kill(pid, 0);
563
500
  return true;
564
- } catch {
565
- return false;
501
+ } catch (e) {
502
+ return e.code === "EPERM";
566
503
  }
567
504
  }
568
505
  function pidMatchesMarker(pid, marker) {
@@ -683,12 +620,6 @@ function clearServerInfo(pid) {
683
620
 
684
621
  // src/lib/browser/launcher.ts
685
622
  var log2 = createLogger("launcher");
686
- var UBLOCK_PATH = "/extensions/uBlock0.chromium";
687
- function findChromeExecutable() {
688
- if (process.env.CHROME_EXECUTABLE) return process.env.CHROME_EXECUTABLE;
689
- if (import_fs2.default.existsSync("/usr/bin/google-chrome-stable")) return "/usr/bin/google-chrome-stable";
690
- return void 0;
691
- }
692
623
  var cachedBrowser = null;
693
624
  var headedBrowser = null;
694
625
  var headedPid = null;
@@ -771,313 +702,10 @@ async function closeBrowser() {
771
702
  async function getBrowserWithFallback(_preferred) {
772
703
  return { browser: await getBrowser(), name: "chromium" };
773
704
  }
774
- async function launchHeadful(displayNum) {
775
- const executablePath = findChromeExecutable();
776
- const hasExtensions = import_fs2.default.existsSync(UBLOCK_PATH);
777
- const args = [
778
- "--no-sandbox",
779
- "--disable-setuid-sandbox",
780
- "--disable-dev-shm-usage",
781
- "--disable-blink-features=AutomationControlled",
782
- "--disable-features=IsolateOrigins,site-per-process",
783
- "--disable-infobars",
784
- "--window-size=1920,1080",
785
- "--force-device-scale-factor=1.25",
786
- "--use-gl=angle",
787
- "--use-angle=swiftshader"
788
- ];
789
- if (hasExtensions) args.push(`--load-extension=${UBLOCK_PATH}`);
790
- const launchOpts = {
791
- headless: false,
792
- args,
793
- env: { ...process.env, DISPLAY: `:${displayNum}` }
794
- };
795
- if (executablePath) launchOpts.executablePath = executablePath;
796
- log2.debug(`headful: ${executablePath || "patchright chromium"}, extensions: ${hasExtensions}`);
797
- return import_patchright.chromium.launch(launchOpts);
798
- }
799
-
800
- // src/lib/browser/fingerprint.ts
801
- var import_fingerprint_generator = require("fingerprint-generator");
802
-
803
- // src/lib/constants.ts
804
- var TIMING = {
805
- /** Mouse move settle delay */
806
- MOUSE_MOVE: [50, 200],
807
- /** Click down→up delay */
808
- CLICK_HOLD: [30, 90],
809
- /** Post-click settle */
810
- POST_CLICK: [100, 300],
811
- /** Per-character typing delay */
812
- CHAR_DELAY: [30, 150],
813
- /** Occasional word-boundary pause */
814
- WORD_PAUSE: [200, 500],
815
- /** Pre-action move range */
816
- IDLE_MOUSE_X: [100, 400],
817
- IDLE_MOUSE_Y: [100, 300],
818
- /** Pre-checkbox random move */
819
- PRE_CHECKBOX_X: [200, 600],
820
- PRE_CHECKBOX_Y: [150, 400],
821
- /** Pause after moving to checkbox area */
822
- PRE_CHECKBOX_WAIT: [300, 800],
823
- /** Wait after clicking reCAPTCHA checkbox */
824
- POST_CHECKBOX_WAIT: 2500,
825
- /** Wait after clicking verify button */
826
- POST_VERIFY_WAIT: 2e3,
827
- /** Wait between clicking captcha tiles */
828
- TILE_CLICK_DELAY: [200, 500],
829
- /** Time to wait for reCAPTCHA tile images to settle */
830
- TILE_SETTLE: 800,
831
- /** Delay before captcha auto-detection */
832
- CAPTCHA_DETECT_WAIT: 1500,
833
- /** Post-login navigation settle */
834
- POST_LOGIN_WAIT: 1500,
835
- /** Post-submit form delay */
836
- POST_SUBMIT_WAIT: 500,
837
- /** Post-submit form extended wait */
838
- POST_SUBMIT_EXTENDED: 2e3,
839
- /** Delay before navigating (human hesitation) */
840
- PRE_NAVIGATE: [300, 700],
841
- /** Per-digit input delay base */
842
- DIGIT_DELAY_BASE: 80,
843
- DIGIT_DELAY_RANGE: 120,
844
- /** Post-click in forms */
845
- POST_FORM_CLICK: 200,
846
- /** Post-TOTP entry wait */
847
- POST_TOTP_WAIT: 300,
848
- /** Post-cookies inject wait */
849
- POST_COOKIES_WAIT: 300,
850
- /** Scroll step delay */
851
- SCROLL_DELAY: 150,
852
- /** Stale state check interval */
853
- STALE_CHECK_INTERVAL: 2e3,
854
- /** captureApi: grace period after the last step for a submit's XHR to fire */
855
- CAPTURE_SETTLE: 300,
856
- /** captureApi: max wait for in-flight requests to answer before stopping */
857
- CAPTURE_PENDING_MAX: 3e3,
858
- /** Post-failure probe ("does the selector exist right now?") budget */
859
- FAILURE_PROBE: 3e3
860
- };
861
- var CAPTCHA_GRID = {
862
- /** Pixels from bframe top to grid start (reCAPTCHA) */
863
- RECAPTCHA_HEADER_HEIGHT: 112,
864
- /** Pixels from bframe top to grid start (hCaptcha) — slightly different */
865
- HCAPTCHA_HEADER_HEIGHT: 110,
866
- /** Default tile size when bframeBox is unavailable */
867
- DEFAULT_TILE_SIZE: 125,
868
- /** Grid padding (left+right total) */
869
- GRID_PADDING: 24,
870
- /** Grid left margin */
871
- GRID_MARGIN: 12,
872
- /** Verify button offset from bframe bottom */
873
- VERIFY_BTN_BOTTOM_OFFSET: 35,
874
- /** Verify button offset from bframe right */
875
- VERIFY_BTN_RIGHT_OFFSET: 60
876
- };
877
- var SCREEN_DEFAULTS = {
878
- WIDTH: 1920,
879
- HEIGHT: 1080,
880
- AVAIL_HEIGHT: 1040,
881
- DPR: 1.25
882
- };
883
- var THRESHOLDS = {
884
- /** Minimum character change to consider page "changed" */
885
- STALE_CHAR_CHANGE: 100,
886
- /** Minimum percentage change to consider page "changed" */
887
- STALE_PERCENT_CHANGE: 0.05,
888
- /** Minimum body text length to not be considered "empty" */
889
- MIN_BODY_TEXT: 200,
890
- /** Maximum response text length to capture */
891
- MAX_RESPONSE_TEXT: 1e5
892
- };
893
- var TIMEOUTS = {
894
- DEFAULT_STALE: 2e4,
895
- NAVIGATION: 6e4,
896
- SELECTOR_WAIT: 1e4,
897
- // Actionability wait for click. Under the 20s stale timer on purpose, so
898
- // Playwright's own reason (not visible, covered by another element…) reaches
899
- // the agent instead of a generic "no state change".
900
- CLICK: 8e3,
901
- TOTP_INPUT: 5e3,
902
- API_REQUEST: 18e4,
903
- HEALTH_CHECK: 3e3,
904
- CHALLENGE_FRAME_WAIT: 5e3,
905
- // New-tab following: after a click, how long to wait for a just-opened tab's
906
- // 'page' event to arrive (only applies when no tab has registered yet), and
907
- // how long to let the followed tab reach domcontentloaded before continuing.
908
- TAB_FOLLOW_SETTLE: 400,
909
- TAB_LOAD: 15e3,
910
- // A new tab usually opens at about:blank then navigates to its real URL —
911
- // wait this long for that first real navigation before judging it blank.
912
- TAB_BLANK_RESOLVE: 3e3
913
- };
914
- var CHROME_MIN_VERSION = 130;
915
-
916
- // src/lib/browser/fingerprint.ts
917
- var generator = new import_fingerprint_generator.FingerprintGenerator({
918
- browsers: [{ name: "chrome", minVersion: CHROME_MIN_VERSION }],
919
- operatingSystems: ["windows"],
920
- devices: ["desktop"],
921
- locales: ["en-US"]
922
- });
923
- function generateWindowsFingerprint() {
924
- const fp = generator.getFingerprint();
925
- const { navigator: nav, screen } = fp.fingerprint;
926
- const dprOptions = [1.25, 1.5, 1.25, 1.5, 1];
927
- const dpr = dprOptions[Math.floor(Math.random() * dprOptions.length)];
928
- const w = screen.width || SCREEN_DEFAULTS.WIDTH;
929
- const h = screen.height || SCREEN_DEFAULTS.HEIGHT;
930
- return {
931
- userAgent: nav.userAgent,
932
- platform: "Win32",
933
- screenWidth: w,
934
- screenHeight: h,
935
- screenAvailHeight: h - 40,
936
- // Windows taskbar ~40px
937
- colorDepth: 24,
938
- deviceScaleFactor: dpr,
939
- hardwareConcurrency: nav.hardwareConcurrency || 8,
940
- deviceMemory: nav.deviceMemory || 8,
941
- languages: nav.languages || ["en-US", "en"],
942
- uaData: nav.userAgentData
943
- };
944
- }
945
-
946
- // src/lib/browser/session-manager.ts
947
- var log3 = createLogger("session");
948
- var BASE_DISPLAY = parseInt(process.env.VNC_BASE_DISPLAY || "99", 10);
949
- var MAX_SESSIONS = parseInt(process.env.VNC_MAX_SESSIONS || "20", 10);
950
- var SESSION_TIMEOUT = parseInt(process.env.VNC_SESSION_TIMEOUT_MS || "300000", 10);
951
- var sessions = /* @__PURE__ */ new Map();
952
- var usedDisplays = /* @__PURE__ */ new Set();
953
- function allocateDisplay() {
954
- for (let i = 0; i < MAX_SESSIONS; i++) {
955
- const num = BASE_DISPLAY + i;
956
- if (!usedDisplays.has(num)) {
957
- usedDisplays.add(num);
958
- return num;
959
- }
960
- }
961
- throw new Error("No available displays. Max concurrent sessions reached.");
962
- }
963
- function freeDisplay(num) {
964
- usedDisplays.delete(num);
965
- }
966
- function waitForSocket(displayNum, timeoutMs = 5e3) {
967
- const socketPath = `/tmp/.X11-unix/X${displayNum}`;
968
- return new Promise((resolve, reject) => {
969
- const start = Date.now();
970
- const check = () => {
971
- if (import_fs3.default.existsSync(socketPath)) return resolve();
972
- if (Date.now() - start > timeoutMs) return reject(new Error(`Xvfb socket not ready after ${timeoutMs}ms`));
973
- setTimeout(check, 100);
974
- };
975
- check();
976
- });
977
- }
978
- function killProcess(proc) {
979
- if (proc && !proc.killed) {
980
- try {
981
- proc.kill("SIGTERM");
982
- } catch {
983
- }
984
- }
985
- }
986
- async function startSession(userId) {
987
- if (sessions.has(userId)) {
988
- return sessions.get(userId);
989
- }
990
- const displayNum = allocateDisplay();
991
- const vncPort = 5900 + displayNum;
992
- const wsPort = 6080 + (displayNum - BASE_DISPLAY);
993
- const xvfb = (0, import_child_process2.spawn)("Xvfb", [`:${displayNum}`, "-screen", "0", "1920x1080x24", "-ac"], {
994
- stdio: "ignore"
995
- });
996
- await waitForSocket(displayNum);
997
- const x11vnc = (0, import_child_process2.spawn)(
998
- "x11vnc",
999
- ["-display", `:${displayNum}`, "-nopw", "-listen", "localhost", "-rfbport", String(vncPort), "-shared", "-forever"],
1000
- { stdio: "ignore" }
1001
- );
1002
- const noVncPath = import_fs3.default.existsSync("/usr/share/novnc") ? "/usr/share/novnc" : "/usr/share/noVNC";
1003
- const websockify = (0, import_child_process2.spawn)("websockify", ["--web", noVncPath, String(wsPort), `localhost:${vncPort}`], {
1004
- stdio: "ignore"
1005
- });
1006
- await new Promise((r) => setTimeout(r, 500));
1007
- const browser = await launchHeadful(displayNum);
1008
- const fingerprint = generateWindowsFingerprint();
1009
- const ctxOpts = stealthContextOptions({}, userId, fingerprint);
1010
- const context = await browser.newContext(ctxOpts);
1011
- const stealthScript = buildStealthScript(fingerprint);
1012
- contextStealthScripts.set(context, stealthScript);
1013
- const page = await context.newPage();
1014
- log3.debug(`fingerprint: ${fingerprint.userAgent.slice(0, 60)}... DPR=${fingerprint.deviceScaleFactor} screen=${fingerprint.screenWidth}x${fingerprint.screenHeight}`);
1015
- const session = {
1016
- displayNum,
1017
- vncPort,
1018
- wsPort,
1019
- xvfb,
1020
- x11vnc,
1021
- websockify,
1022
- browser,
1023
- context,
1024
- page,
1025
- createdAt: /* @__PURE__ */ new Date(),
1026
- timeoutTimer: null
1027
- };
1028
- session.timeoutTimer = setTimeout(() => stopSession(userId), SESSION_TIMEOUT);
1029
- sessions.set(userId, session);
1030
- return session;
1031
- }
1032
- function resetTimeout(userId) {
1033
- const session = sessions.get(userId);
1034
- if (session) {
1035
- clearTimeout(session.timeoutTimer);
1036
- session.timeoutTimer = setTimeout(() => stopSession(userId), SESSION_TIMEOUT);
1037
- }
1038
- }
1039
- function getSession(userId) {
1040
- return sessions.get(userId) || null;
1041
- }
1042
- async function stopSession(userId) {
1043
- const session = sessions.get(userId);
1044
- if (!session) return null;
1045
- clearTimeout(session.timeoutTimer);
1046
- let sessionData = null;
1047
- try {
1048
- const { extractSession: extractSession2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
1049
- sessionData = await extractSession2(session.context, session.page);
1050
- } catch {
1051
- }
1052
- contextStealthScripts.delete(session.context);
1053
- try {
1054
- await session.context.close();
1055
- } catch {
1056
- }
1057
- try {
1058
- await session.browser.close();
1059
- } catch {
1060
- }
1061
- killProcess(session.websockify);
1062
- killProcess(session.x11vnc);
1063
- killProcess(session.xvfb);
1064
- await new Promise((r) => setTimeout(r, 1e3));
1065
- try {
1066
- import_fs3.default.unlinkSync(`/tmp/.X11-unix/X${session.displayNum}`);
1067
- } catch {
1068
- }
1069
- freeDisplay(session.displayNum);
1070
- sessions.delete(userId);
1071
- return sessionData;
1072
- }
1073
- async function cleanupAllSessions() {
1074
- const userIds = [...sessions.keys()];
1075
- await Promise.all(userIds.map((id) => stopSession(id)));
1076
- }
1077
705
 
1078
706
  // src/lib/auth/crypto.ts
1079
707
  var import_crypto = __toESM(require("crypto"));
1080
- var import_fs4 = __toESM(require("fs"));
708
+ var import_fs2 = __toESM(require("fs"));
1081
709
  var import_os2 = __toESM(require("os"));
1082
710
  var import_path3 = __toESM(require("path"));
1083
711
  init_paths();
@@ -1094,16 +722,16 @@ function getLocalToken() {
1094
722
  ];
1095
723
  for (const file of candidates) {
1096
724
  try {
1097
- const existing = import_fs4.default.readFileSync(file, "utf8").trim();
725
+ const existing = import_fs2.default.readFileSync(file, "utf8").trim();
1098
726
  if (existing) return existing;
1099
727
  } catch {
1100
728
  }
1101
729
  }
1102
730
  for (const file of candidates) {
1103
731
  try {
1104
- import_fs4.default.mkdirSync(import_path3.default.dirname(file), { recursive: true });
732
+ import_fs2.default.mkdirSync(import_path3.default.dirname(file), { recursive: true });
1105
733
  const secret = import_crypto.default.randomBytes(32).toString("hex");
1106
- import_fs4.default.writeFileSync(file, secret, { mode: 384 });
734
+ import_fs2.default.writeFileSync(file, secret, { mode: 384 });
1107
735
  return secret;
1108
736
  } catch {
1109
737
  }
@@ -1112,6 +740,25 @@ function getLocalToken() {
1112
740
  `iframer: could not read or create a persistent encryption secret in any writable location (${candidates.join(", ")}). Set IFRAMER_SECRET to a stable value shared between the MCP server and CLI (openssl rand -hex 32).`
1113
741
  );
1114
742
  }
743
+ function getPairingTokens() {
744
+ const tokens = /* @__PURE__ */ new Set();
745
+ try {
746
+ tokens.add(getLocalToken());
747
+ } catch {
748
+ }
749
+ const candidates = [
750
+ import_path3.default.join(getDataDir(), "secret"),
751
+ import_path3.default.join(process.env.XDG_RUNTIME_DIR || import_os2.default.tmpdir(), "iframer-secret")
752
+ ];
753
+ for (const file of candidates) {
754
+ try {
755
+ tokens.add(import_fs2.default.readFileSync(file, "utf8").trim());
756
+ } catch {
757
+ }
758
+ }
759
+ tokens.delete("");
760
+ return [...tokens];
761
+ }
1115
762
  function deriveKey(token, purpose = INFO) {
1116
763
  return new Promise((resolve, reject) => {
1117
764
  import_crypto.default.hkdf("sha256", token, SALT, purpose, KEY_LENGTH, (err, key) => {
@@ -1159,77 +806,59 @@ function generateTOTP(secret, period = 30, digits = 6) {
1159
806
  return code.toString().padStart(digits, "0");
1160
807
  }
1161
808
 
1162
- // src/lib/iframer.ts
1163
- init_persistence();
1164
-
1165
- // src/lib/screenshot.ts
1166
- var import_fs5 = __toESM(require("fs"));
1167
- var import_path4 = __toESM(require("path"));
1168
- var log4 = createLogger("screenshot");
1169
- var MAX_AGE_MS = parseInt(process.env.IFRAMER_SCREENSHOT_MAX_AGE_MS || String(24 * 60 * 60 * 1e3), 10);
1170
- var MAX_FILES = parseInt(process.env.IFRAMER_SCREENSHOT_MAX_FILES || "500", 10);
1171
- var PRUNE_THROTTLE_MS = 5 * 60 * 1e3;
1172
- var lastPruneAt = 0;
1173
- function saveScreenshot(buffer, filename, screenshotDir, publicUrl) {
1174
- import_fs5.default.mkdirSync(screenshotDir, { recursive: true });
1175
- const filePath = import_path4.default.join(screenshotDir, filename);
1176
- import_fs5.default.writeFileSync(filePath, buffer);
1177
- maybePrune(screenshotDir);
1178
- return `${publicUrl}/screenshots/${filename}`;
809
+ // src/lib/session/persistence.ts
810
+ async function extractSession(context, page) {
811
+ const cookies = await context.cookies();
812
+ const { localStorage, sessionStorage } = await page.evaluate(() => {
813
+ const ls = {};
814
+ const ss = {};
815
+ for (let i = 0; i < window.localStorage.length; i++) {
816
+ const key = window.localStorage.key(i);
817
+ ls[key] = window.localStorage.getItem(key);
818
+ }
819
+ for (let i = 0; i < window.sessionStorage.length; i++) {
820
+ const key = window.sessionStorage.key(i);
821
+ ss[key] = window.sessionStorage.getItem(key);
822
+ }
823
+ return { localStorage: ls, sessionStorage: ss };
824
+ });
825
+ const origin = new URL(page.url()).origin;
826
+ return {
827
+ cookies,
828
+ localStorage: { [origin]: localStorage },
829
+ sessionStorage: { [origin]: sessionStorage },
830
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString()
831
+ };
1179
832
  }
1180
- function maybePrune(dir) {
1181
- const now = Date.now();
1182
- if (now - lastPruneAt < PRUNE_THROTTLE_MS) return;
1183
- lastPruneAt = now;
1184
- pruneScreenshots(dir);
833
+ async function injectCookies(context, sessionData) {
834
+ if (sessionData?.cookies?.length > 0) {
835
+ await context.addCookies(sessionData.cookies);
836
+ }
1185
837
  }
1186
- function pruneScreenshots(dir, opts = {}) {
1187
- const maxAgeMs = opts.maxAgeMs ?? MAX_AGE_MS;
1188
- const maxFiles = opts.maxFiles ?? MAX_FILES;
1189
- const now = opts.now ?? Date.now();
1190
- try {
1191
- const entries = import_fs5.default.readdirSync(dir).filter((f) => f.endsWith(".jpg") || f.endsWith(".jpeg") || f.endsWith(".png")).map((f) => {
1192
- const full = import_path4.default.join(dir, f);
1193
- try {
1194
- return { full, mtimeMs: import_fs5.default.statSync(full).mtimeMs };
1195
- } catch {
1196
- return null;
1197
- }
1198
- }).filter((e) => e !== null);
1199
- let removed = 0;
1200
- const survivors = [];
1201
- for (const e of entries) {
1202
- if (now - e.mtimeMs > maxAgeMs) {
1203
- try {
1204
- import_fs5.default.unlinkSync(e.full);
1205
- removed++;
1206
- } catch {
1207
- }
1208
- } else {
1209
- survivors.push(e);
838
+ async function injectStorage(page, sessionData) {
839
+ if (!sessionData) return;
840
+ const origin = new URL(page.url()).origin;
841
+ const ls = sessionData.localStorage?.[origin];
842
+ const ss = sessionData.sessionStorage?.[origin];
843
+ if (ls && Object.keys(ls).length > 0) {
844
+ await page.evaluate((data) => {
845
+ for (const [key, value] of Object.entries(data)) {
846
+ window.localStorage.setItem(key, value);
1210
847
  }
1211
- }
1212
- if (survivors.length > maxFiles) {
1213
- survivors.sort((a, b) => a.mtimeMs - b.mtimeMs);
1214
- for (const e of survivors.slice(0, survivors.length - maxFiles)) {
1215
- try {
1216
- import_fs5.default.unlinkSync(e.full);
1217
- removed++;
1218
- } catch {
1219
- }
848
+ }, ls);
849
+ }
850
+ if (ss && Object.keys(ss).length > 0) {
851
+ await page.evaluate((data) => {
852
+ for (const [key, value] of Object.entries(data)) {
853
+ window.sessionStorage.setItem(key, value);
1220
854
  }
1221
- }
1222
- if (removed > 0) log4.debug(`pruned ${removed} old screenshot(s) from ${dir}`);
1223
- return removed;
1224
- } catch (err) {
1225
- log4.warn(`screenshot prune failed: ${err instanceof Error ? err.message : String(err)}`);
1226
- return 0;
855
+ }, ss);
1227
856
  }
1228
857
  }
1229
858
 
1230
859
  // src/lib/session/sqlite-store.ts
1231
- var import_path5 = __toESM(require("path"));
1232
- var import_fs6 = __toESM(require("fs"));
860
+ var import_path4 = __toESM(require("path"));
861
+ var import_fs3 = __toESM(require("fs"));
1233
862
  var IS_BUN = typeof globalThis.Bun !== "undefined";
1234
863
  function createBunDb(dbPath) {
1235
864
  const { Database } = require("bun:sqlite");
@@ -1268,8 +897,8 @@ function createNodeDb(dbPath) {
1268
897
  var SqliteStore = class {
1269
898
  db;
1270
899
  constructor(dataDir) {
1271
- import_fs6.default.mkdirSync(dataDir, { recursive: true });
1272
- const dbPath = import_path5.default.join(dataDir, "iframer.db");
900
+ import_fs3.default.mkdirSync(dataDir, { recursive: true });
901
+ const dbPath = import_path4.default.join(dataDir, "iframer.db");
1273
902
  this.db = IS_BUN ? createBunDb(dbPath) : createNodeDb(dbPath);
1274
903
  this.db.run(`
1275
904
  CREATE TABLE IF NOT EXISTS sessions (
@@ -1349,13 +978,13 @@ var import_patchright2 = require("patchright");
1349
978
  var import_crypto2 = require("crypto");
1350
979
 
1351
980
  // src/lib/browser/chrome-downloader.ts
1352
- var import_fs7 = __toESM(require("fs"));
1353
- var import_path6 = __toESM(require("path"));
981
+ var import_fs4 = __toESM(require("fs"));
982
+ var import_path5 = __toESM(require("path"));
1354
983
  var import_os3 = __toESM(require("os"));
1355
- var import_child_process3 = require("child_process");
1356
- var log5 = createLogger("chrome");
984
+ var import_child_process2 = require("child_process");
985
+ var log3 = createLogger("chrome");
1357
986
  var CHROME_VERSIONS_URL = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json";
1358
- var DEFAULT_INSTALL_DIR = import_path6.default.join(import_os3.default.homedir(), ".iframer", "chrome");
987
+ var DEFAULT_INSTALL_DIR = import_path5.default.join(import_os3.default.homedir(), ".iframer", "chrome");
1359
988
  function getPlatform() {
1360
989
  const arch = process.arch;
1361
990
  const platform = process.platform;
@@ -1367,24 +996,24 @@ function getPlatform() {
1367
996
  function getChromeExecutablePath(installDir) {
1368
997
  const platform = process.platform;
1369
998
  if (platform === "darwin") {
1370
- const entries = import_fs7.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
999
+ const entries = import_fs4.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
1371
1000
  const dir = entries[0] || "chrome-mac-arm64";
1372
- return import_path6.default.join(installDir, dir, "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
1001
+ return import_path5.default.join(installDir, dir, "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
1373
1002
  }
1374
1003
  if (platform === "linux") {
1375
- const entries = import_fs7.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
1004
+ const entries = import_fs4.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
1376
1005
  const dir = entries[0] || "chrome-linux64";
1377
- return import_path6.default.join(installDir, dir, "chrome");
1006
+ return import_path5.default.join(installDir, dir, "chrome");
1378
1007
  }
1379
1008
  if (platform === "win32") {
1380
- const entries = import_fs7.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
1009
+ const entries = import_fs4.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
1381
1010
  const dir = entries[0] || "chrome-win64";
1382
- return import_path6.default.join(installDir, dir, "chrome.exe");
1011
+ return import_path5.default.join(installDir, dir, "chrome.exe");
1383
1012
  }
1384
1013
  throw new Error(`Unsupported platform: ${platform}`);
1385
1014
  }
1386
1015
  async function downloadChrome(installDir = DEFAULT_INSTALL_DIR) {
1387
- log5.info("Downloading Chrome for Testing (first time only)...");
1016
+ log3.info("Downloading Chrome for Testing (first time only)...");
1388
1017
  const res = await fetch(CHROME_VERSIONS_URL);
1389
1018
  if (!res.ok) throw new Error(`Failed to fetch Chrome versions: ${res.status}`);
1390
1019
  const data = await res.json();
@@ -1395,35 +1024,35 @@ async function downloadChrome(installDir = DEFAULT_INSTALL_DIR) {
1395
1024
  if (!download2) throw new Error(`No Chrome for Testing download for platform: ${platform}`);
1396
1025
  const url = download2.url;
1397
1026
  const version = channel.version;
1398
- log5.debug(`Version ${version} for ${platform}`);
1399
- log5.debug(`URL: ${url}`);
1400
- import_fs7.default.mkdirSync(installDir, { recursive: true });
1401
- const zipPath = import_path6.default.join(installDir, "chrome.zip");
1027
+ log3.debug(`Version ${version} for ${platform}`);
1028
+ log3.debug(`URL: ${url}`);
1029
+ import_fs4.default.mkdirSync(installDir, { recursive: true });
1030
+ const zipPath = import_path5.default.join(installDir, "chrome.zip");
1402
1031
  const dlRes = await fetch(url);
1403
1032
  if (!dlRes.ok) throw new Error(`Download failed: ${dlRes.status}`);
1404
1033
  const buf = Buffer.from(await dlRes.arrayBuffer());
1405
- import_fs7.default.writeFileSync(zipPath, buf);
1406
- log5.info(`Downloaded ${(buf.length / 1024 / 1024).toFixed(1)}MB`);
1407
- (0, import_child_process3.execSync)(`unzip -o -q "${zipPath}" -d "${installDir}"`, { stdio: "inherit" });
1408
- import_fs7.default.unlinkSync(zipPath);
1034
+ import_fs4.default.writeFileSync(zipPath, buf);
1035
+ log3.info(`Downloaded ${(buf.length / 1024 / 1024).toFixed(1)}MB`);
1036
+ (0, import_child_process2.execSync)(`unzip -o -q "${zipPath}" -d "${installDir}"`, { stdio: "inherit" });
1037
+ import_fs4.default.unlinkSync(zipPath);
1409
1038
  const execPath = getChromeExecutablePath(installDir);
1410
- if (!import_fs7.default.existsSync(execPath)) {
1039
+ if (!import_fs4.default.existsSync(execPath)) {
1411
1040
  throw new Error(`Chrome executable not found after extraction: ${execPath}`);
1412
1041
  }
1413
1042
  if (process.platform !== "win32") {
1414
- import_fs7.default.chmodSync(execPath, 493);
1043
+ import_fs4.default.chmodSync(execPath, 493);
1415
1044
  }
1416
- import_fs7.default.writeFileSync(import_path6.default.join(installDir, "version.json"), JSON.stringify({ version, platform, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() }));
1417
- log5.info(`Installed at: ${execPath}`);
1045
+ import_fs4.default.writeFileSync(import_path5.default.join(installDir, "version.json"), JSON.stringify({ version, platform, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() }));
1046
+ log3.info(`Installed at: ${execPath}`);
1418
1047
  return execPath;
1419
1048
  }
1420
1049
  function findChromeForTesting() {
1421
1050
  if (process.env.CHROME_EXECUTABLE) {
1422
- if (import_fs7.default.existsSync(process.env.CHROME_EXECUTABLE)) return process.env.CHROME_EXECUTABLE;
1051
+ if (import_fs4.default.existsSync(process.env.CHROME_EXECUTABLE)) return process.env.CHROME_EXECUTABLE;
1423
1052
  }
1424
1053
  try {
1425
1054
  const execPath = getChromeExecutablePath(DEFAULT_INSTALL_DIR);
1426
- if (import_fs7.default.existsSync(execPath)) return execPath;
1055
+ if (import_fs4.default.existsSync(execPath)) return execPath;
1427
1056
  } catch {
1428
1057
  }
1429
1058
  return null;
@@ -1439,22 +1068,13 @@ function findChrome() {
1439
1068
  "/usr/bin/google-chrome-stable",
1440
1069
  "/usr/bin/google-chrome",
1441
1070
  "/usr/bin/chromium-browser",
1442
- "/usr/bin/chromium",
1443
- // Playwright/Patchright installed browsers (Docker containers)
1444
- ...(() => {
1445
- try {
1446
- const dirs = import_fs7.default.readdirSync("/ms-playwright").filter((d) => d.startsWith("chromium-")).sort().reverse();
1447
- return dirs.map((d) => import_path6.default.join("/ms-playwright", d, "chrome-linux", "chrome"));
1448
- } catch {
1449
- return [];
1450
- }
1451
- })()
1071
+ "/usr/bin/chromium"
1452
1072
  ] : [
1453
1073
  "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
1454
1074
  "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
1455
1075
  ];
1456
1076
  for (const p of systemPaths) {
1457
- if (import_fs7.default.existsSync(p)) return p;
1077
+ if (import_fs4.default.existsSync(p)) return p;
1458
1078
  }
1459
1079
  return null;
1460
1080
  }
@@ -1464,10 +1084,10 @@ async function ensureChrome() {
1464
1084
  try {
1465
1085
  return await downloadChrome();
1466
1086
  } catch (err) {
1467
- log5.error(`Failed to download Chrome for Testing: ${err instanceof Error ? err.message : String(err)}`);
1087
+ log3.error(`Failed to download Chrome for Testing: ${err instanceof Error ? err.message : String(err)}`);
1468
1088
  const system = findChrome();
1469
1089
  if (system) {
1470
- log5.warn(`Falling back to system Chrome: ${system}`);
1090
+ log3.warn(`Falling back to system Chrome: ${system}`);
1471
1091
  return system;
1472
1092
  }
1473
1093
  throw new Error("No Chrome found. Download failed and no system Chrome available.");
@@ -1475,7 +1095,7 @@ async function ensureChrome() {
1475
1095
  }
1476
1096
 
1477
1097
  // src/lib/browser/cloak-browser.ts
1478
- var log6 = createLogger("cloak");
1098
+ var log4 = createLogger("cloak");
1479
1099
  var _available = null;
1480
1100
  function cloakEnabled() {
1481
1101
  return process.env.IFRAMER_USE_CLOAKBROWSER === "1" || process.env.IFRAMER_USE_CLOAKBROWSER === "true";
@@ -1494,14 +1114,14 @@ async function ensureBinary() {
1494
1114
  try {
1495
1115
  const info = cloak.binaryInfo();
1496
1116
  if (!info.installed) {
1497
- log6.info("Downloading CloakBrowser binary...");
1117
+ log4.info("Downloading CloakBrowser binary...");
1498
1118
  await cloak.ensureBinary();
1499
- log6.info("CloakBrowser ready");
1119
+ log4.info("CloakBrowser ready");
1500
1120
  }
1501
1121
  _available = true;
1502
1122
  return true;
1503
1123
  } catch (err) {
1504
- log6.warn(`CloakBrowser setup failed: ${err instanceof Error ? err.message : String(err)}`);
1124
+ log4.warn(`CloakBrowser setup failed: ${err instanceof Error ? err.message : String(err)}`);
1505
1125
  _available = false;
1506
1126
  return false;
1507
1127
  }
@@ -1516,16 +1136,16 @@ async function launchCloakBrowser(options) {
1516
1136
  headless: options.headless,
1517
1137
  args: options.args
1518
1138
  });
1519
- log6.info(`CloakBrowser launched (headless=${options.headless})`);
1139
+ log4.info(`CloakBrowser launched (headless=${options.headless})`);
1520
1140
  return browser;
1521
1141
  } catch (err) {
1522
- log6.warn(`CloakBrowser launch failed: ${err instanceof Error ? err.message : String(err)}`);
1142
+ log4.warn(`CloakBrowser launch failed: ${err instanceof Error ? err.message : String(err)}`);
1523
1143
  return null;
1524
1144
  }
1525
1145
  }
1526
1146
 
1527
1147
  // src/lib/browser/daemon.ts
1528
- var log7 = createLogger("daemon");
1148
+ var log5 = createLogger("daemon");
1529
1149
  var DEFAULT_IDLE_TIMEOUT = 5 * 60 * 1e3;
1530
1150
  var CLOSE_GRACE_MS = 5e3;
1531
1151
  var BUSY_MAX_MS = 30 * 60 * 1e3;
@@ -1543,9 +1163,6 @@ var BrowserDaemon = class {
1543
1163
  this.idleTimeout = idleTimeout;
1544
1164
  }
1545
1165
  async ensure(mode, instanceId = DEFAULT_INSTANCE, sessionProfile = instanceId) {
1546
- if (mode === "docker-headful") {
1547
- throw new Error("Docker mode doesn't use the daemon. Use the Docker API.");
1548
- }
1549
1166
  const key = keyOf(mode, instanceId);
1550
1167
  let instance = this.instances.get(key);
1551
1168
  if (instance) {
@@ -1556,11 +1173,11 @@ var BrowserDaemon = class {
1556
1173
  try {
1557
1174
  await page2.evaluate("1");
1558
1175
  } catch {
1559
- log7.info(`Page for ${mode} is dead, creating fresh context`);
1176
+ log5.info(`Page for ${mode} is dead, creating fresh context`);
1560
1177
  try {
1561
1178
  await context2.close();
1562
1179
  } catch (err) {
1563
- log7.warn(`dead-page context close failed: ${err}`);
1180
+ log5.warn(`dead-page context close failed: ${err}`);
1564
1181
  }
1565
1182
  context2 = await instance.browser.newContext();
1566
1183
  page2 = await context2.newPage();
@@ -1573,18 +1190,18 @@ var BrowserDaemon = class {
1573
1190
  }
1574
1191
  } catch {
1575
1192
  }
1576
- log7.info(`Browser for ${key} disconnected (window closed?), relaunching...`);
1193
+ log5.info(`Browser for ${key} disconnected (window closed?), relaunching...`);
1577
1194
  await this.stopMode(mode, instanceId);
1578
1195
  }
1579
1196
  const marker = `--iframer-key=${key}-${(0, import_crypto2.randomUUID)()}`;
1580
1197
  let browser;
1581
1198
  const cloakBrowser = await launchCloakBrowser({ headless: mode === "headless", args: [marker] });
1582
1199
  if (cloakBrowser) {
1583
- log7.info(`CloakBrowser ${mode} ready`);
1200
+ log5.info(`CloakBrowser ${mode} ready`);
1584
1201
  browser = cloakBrowser;
1585
1202
  } else {
1586
1203
  const executablePath = await ensureChrome();
1587
- log7.info(`Falling back to Chrome for Testing in ${mode} mode: ${executablePath}`);
1204
+ log5.info(`Falling back to Chrome for Testing in ${mode} mode: ${executablePath}`);
1588
1205
  browser = await import_patchright2.chromium.launch({
1589
1206
  executablePath,
1590
1207
  headless: mode === "headless",
@@ -1607,7 +1224,7 @@ var BrowserDaemon = class {
1607
1224
  launchedAt: (/* @__PURE__ */ new Date()).toISOString()
1608
1225
  });
1609
1226
  } else {
1610
- log7.warn(`could not resolve Chrome PID for ${key} \u2014 force-kill unavailable for this instance`);
1227
+ log5.warn(`could not resolve Chrome PID for ${key} \u2014 force-kill unavailable for this instance`);
1611
1228
  }
1612
1229
  const context = await browser.newContext();
1613
1230
  const page = await context.newPage();
@@ -1626,7 +1243,7 @@ var BrowserDaemon = class {
1626
1243
  };
1627
1244
  this.instances.set(key, instance);
1628
1245
  this.resetIdleTimer(key);
1629
- log7.info(`Chrome ${key} ready (pid=${chromePid ?? "unknown"})`);
1246
+ log5.info(`Chrome ${key} ready (pid=${chromePid ?? "unknown"})`);
1630
1247
  return { browser, context, page };
1631
1248
  }
1632
1249
  /** Mark an instance busy for the duration of a pipeline run. While busy,
@@ -1680,9 +1297,9 @@ var BrowserDaemon = class {
1680
1297
  continue;
1681
1298
  }
1682
1299
  if (!blank) continue;
1683
- log7.info(`blank-orphan sweep: ${key} never left about:blank, stopping`);
1300
+ log5.info(`blank-orphan sweep: ${key} never left about:blank, stopping`);
1684
1301
  stopped.push(key);
1685
- this.stopKey(key).catch((err) => log7.warn(`blank sweep stop failed for ${key}: ${err}`));
1302
+ this.stopKey(key).catch((err) => log5.warn(`blank sweep stop failed for ${key}: ${err}`));
1686
1303
  }
1687
1304
  return stopped;
1688
1305
  }
@@ -1754,17 +1371,17 @@ var BrowserDaemon = class {
1754
1371
  const timer = this.idleTimers.get(key);
1755
1372
  if (timer) clearTimeout(timer);
1756
1373
  this.idleTimers.delete(key);
1757
- log7.info(`Stopping Chrome ${key} (pid=${instance.chromePid ?? "unknown"})...`);
1374
+ log5.info(`Stopping Chrome ${key} (pid=${instance.chromePid ?? "unknown"})...`);
1758
1375
  const politeClose = (async () => {
1759
1376
  try {
1760
1377
  await instance.context.close();
1761
1378
  } catch (err) {
1762
- log7.warn(`context.close failed for ${key}: ${err}`);
1379
+ log5.warn(`context.close failed for ${key}: ${err}`);
1763
1380
  }
1764
1381
  try {
1765
1382
  await instance.browser.close();
1766
1383
  } catch (err) {
1767
- log7.warn(`browser.close failed for ${key}: ${err}`);
1384
+ log5.warn(`browser.close failed for ${key}: ${err}`);
1768
1385
  }
1769
1386
  })();
1770
1387
  const closedInTime = await Promise.race([
@@ -1772,20 +1389,20 @@ var BrowserDaemon = class {
1772
1389
  sleep2(CLOSE_GRACE_MS).then(() => false)
1773
1390
  ]);
1774
1391
  if (!closedInTime) {
1775
- log7.warn(`polite close timed out after ${CLOSE_GRACE_MS}ms for ${key}, force-killing`);
1392
+ log5.warn(`polite close timed out after ${CLOSE_GRACE_MS}ms for ${key}, force-killing`);
1776
1393
  }
1777
1394
  if (instance.chromePid !== null) {
1778
1395
  const dead = await forceKillBrowser({ chromePid: instance.chromePid, marker: instance.marker });
1779
1396
  if (dead) {
1780
1397
  unregisterBrowser(instance.chromePid);
1781
1398
  } else {
1782
- log7.warn(`Chrome pid=${instance.chromePid} survived SIGKILL?! leaving registry record for reaper`);
1399
+ log5.warn(`Chrome pid=${instance.chromePid} survived SIGKILL?! leaving registry record for reaper`);
1783
1400
  }
1784
1401
  } else if (!closedInTime) {
1785
- log7.warn(`no PID recorded for ${key} and polite close hung \u2014 this Chrome may leak until the next reap`);
1402
+ log5.warn(`no PID recorded for ${key} and polite close hung \u2014 this Chrome may leak until the next reap`);
1786
1403
  }
1787
1404
  this.instances.delete(key);
1788
- log7.info(`Stopped Chrome ${key}`);
1405
+ log5.info(`Stopped Chrome ${key}`);
1789
1406
  }
1790
1407
  /**
1791
1408
  * Stop browsers. By default skips instances that are mid-pipeline (active>0)
@@ -1809,12 +1426,12 @@ var BrowserDaemon = class {
1809
1426
  this.resetIdleTimer(key);
1810
1427
  return;
1811
1428
  }
1812
- log7.warn(`${key} reported busy for ${Math.round(busyMs / 6e4)}m \u2014 treating active=${instance.active} as leaked, stopping`);
1429
+ log5.warn(`${key} reported busy for ${Math.round(busyMs / 6e4)}m \u2014 treating active=${instance.active} as leaked, stopping`);
1813
1430
  instance.active = 0;
1814
1431
  instance.busySince = null;
1815
1432
  }
1816
- log7.info(`Idle timeout for ${key}, stopping...`);
1817
- this.stopKey(key).catch((err) => log7.warn(`idle stop failed for ${key}: ${err}`));
1433
+ log5.info(`Idle timeout for ${key}, stopping...`);
1434
+ this.stopKey(key).catch((err) => log5.warn(`idle stop failed for ${key}: ${err}`));
1818
1435
  }, this.idleTimeout)
1819
1436
  );
1820
1437
  }
@@ -1827,15 +1444,15 @@ var BrowserDaemon = class {
1827
1444
  };
1828
1445
 
1829
1446
  // src/lib/domain-modes.ts
1830
- var import_fs8 = __toESM(require("fs"));
1831
- var import_path7 = __toESM(require("path"));
1447
+ var import_fs5 = __toESM(require("fs"));
1448
+ var import_path6 = __toESM(require("path"));
1832
1449
  init_paths();
1833
- var log8 = createLogger("domain-modes");
1450
+ var log6 = createLogger("domain-modes");
1834
1451
  function defaultFile() {
1835
- return import_path7.default.join(getDataDir(), "domain-modes.json");
1452
+ return import_path6.default.join(getDataDir(), "domain-modes.json");
1836
1453
  }
1837
1454
  var TTL_DAYS = 14;
1838
- var ESCALATION_LADDER = ["headless", "docker-headful", "binary-headful"];
1455
+ var ESCALATION_LADDER = ["headless", "binary-headful"];
1839
1456
  var DomainModeStore = class {
1840
1457
  data = {};
1841
1458
  filePath;
@@ -1914,8 +1531,8 @@ var DomainModeStore = class {
1914
1531
  }
1915
1532
  load() {
1916
1533
  try {
1917
- if (import_fs8.default.existsSync(this.filePath)) {
1918
- this.data = JSON.parse(import_fs8.default.readFileSync(this.filePath, "utf-8"));
1534
+ if (import_fs5.default.existsSync(this.filePath)) {
1535
+ this.data = JSON.parse(import_fs5.default.readFileSync(this.filePath, "utf-8"));
1919
1536
  }
1920
1537
  } catch {
1921
1538
  this.data = {};
@@ -1923,10 +1540,10 @@ var DomainModeStore = class {
1923
1540
  }
1924
1541
  save() {
1925
1542
  try {
1926
- import_fs8.default.mkdirSync(import_path7.default.dirname(this.filePath), { recursive: true });
1927
- import_fs8.default.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
1543
+ import_fs5.default.mkdirSync(import_path6.default.dirname(this.filePath), { recursive: true });
1544
+ import_fs5.default.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
1928
1545
  } catch (err) {
1929
- log8.error("Failed to save:", err);
1546
+ log6.error("Failed to save:", err);
1930
1547
  }
1931
1548
  }
1932
1549
  };
@@ -2042,7 +1659,7 @@ function failedStepResult(step, error, durationMs, stepIndex = -1) {
2042
1659
  }
2043
1660
 
2044
1661
  // src/lib/clipboard.ts
2045
- var import_child_process4 = require("child_process");
1662
+ var import_child_process3 = require("child_process");
2046
1663
  function platformTools(mode) {
2047
1664
  if (process.platform === "darwin") return [[mode === "read" ? "pbpaste" : "pbcopy"]];
2048
1665
  if (mode === "read") return [["wl-paste", "-n"], ["xclip", "-selection", "clipboard", "-o"], ["xsel", "-b", "-o"]];
@@ -2052,7 +1669,7 @@ function run(cmd, input) {
2052
1669
  return new Promise((resolve) => {
2053
1670
  let child;
2054
1671
  try {
2055
- child = (0, import_child_process4.spawn)(cmd[0], cmd.slice(1));
1672
+ child = (0, import_child_process3.spawn)(cmd[0], cmd.slice(1));
2056
1673
  } catch {
2057
1674
  resolve({ ok: false, out: "", err: `spawn ${cmd[0]} failed` });
2058
1675
  return;
@@ -2080,6 +1697,112 @@ async function clipboardRead() {
2080
1697
  throw new Error(`No working clipboard tool found (${tools.map((t) => t[0]).join(", ")}). ${lastErr}`);
2081
1698
  }
2082
1699
 
1700
+ // src/lib/constants.ts
1701
+ var TIMING = {
1702
+ /** Mouse move settle delay */
1703
+ MOUSE_MOVE: [50, 200],
1704
+ /** Click down→up delay */
1705
+ CLICK_HOLD: [30, 90],
1706
+ /** Post-click settle */
1707
+ POST_CLICK: [100, 300],
1708
+ /** Per-character typing delay */
1709
+ CHAR_DELAY: [30, 150],
1710
+ /** Occasional word-boundary pause */
1711
+ WORD_PAUSE: [200, 500],
1712
+ /** Pre-action move range */
1713
+ IDLE_MOUSE_X: [100, 400],
1714
+ IDLE_MOUSE_Y: [100, 300],
1715
+ /** Pre-checkbox random move */
1716
+ PRE_CHECKBOX_X: [200, 600],
1717
+ PRE_CHECKBOX_Y: [150, 400],
1718
+ /** Pause after moving to checkbox area */
1719
+ PRE_CHECKBOX_WAIT: [300, 800],
1720
+ /** Wait after clicking reCAPTCHA checkbox */
1721
+ POST_CHECKBOX_WAIT: 2500,
1722
+ /** Wait after clicking verify button */
1723
+ POST_VERIFY_WAIT: 2e3,
1724
+ /** Wait between clicking captcha tiles */
1725
+ TILE_CLICK_DELAY: [200, 500],
1726
+ /** Time to wait for reCAPTCHA tile images to settle */
1727
+ TILE_SETTLE: 800,
1728
+ /** Delay before captcha auto-detection */
1729
+ CAPTCHA_DETECT_WAIT: 1500,
1730
+ /** Post-login navigation settle */
1731
+ POST_LOGIN_WAIT: 1500,
1732
+ /** Post-submit form delay */
1733
+ POST_SUBMIT_WAIT: 500,
1734
+ /** Post-submit form extended wait */
1735
+ POST_SUBMIT_EXTENDED: 2e3,
1736
+ /** Delay before navigating (human hesitation) */
1737
+ PRE_NAVIGATE: [300, 700],
1738
+ /** Per-digit input delay base */
1739
+ DIGIT_DELAY_BASE: 80,
1740
+ DIGIT_DELAY_RANGE: 120,
1741
+ /** Post-click in forms */
1742
+ POST_FORM_CLICK: 200,
1743
+ /** Post-TOTP entry wait */
1744
+ POST_TOTP_WAIT: 300,
1745
+ /** Post-cookies inject wait */
1746
+ POST_COOKIES_WAIT: 300,
1747
+ /** Scroll step delay */
1748
+ SCROLL_DELAY: 150,
1749
+ /** Stale state check interval */
1750
+ STALE_CHECK_INTERVAL: 2e3,
1751
+ /** captureApi: grace period after the last step for a submit's XHR to fire */
1752
+ CAPTURE_SETTLE: 300,
1753
+ /** captureApi: max wait for in-flight requests to answer before stopping */
1754
+ CAPTURE_PENDING_MAX: 3e3,
1755
+ /** Post-failure probe ("does the selector exist right now?") budget */
1756
+ FAILURE_PROBE: 3e3
1757
+ };
1758
+ var CAPTCHA_GRID = {
1759
+ /** Pixels from bframe top to grid start (reCAPTCHA) */
1760
+ RECAPTCHA_HEADER_HEIGHT: 112,
1761
+ /** Pixels from bframe top to grid start (hCaptcha) — slightly different */
1762
+ HCAPTCHA_HEADER_HEIGHT: 110,
1763
+ /** Default tile size when bframeBox is unavailable */
1764
+ DEFAULT_TILE_SIZE: 125,
1765
+ /** Grid padding (left+right total) */
1766
+ GRID_PADDING: 24,
1767
+ /** Grid left margin */
1768
+ GRID_MARGIN: 12,
1769
+ /** Verify button offset from bframe bottom */
1770
+ VERIFY_BTN_BOTTOM_OFFSET: 35,
1771
+ /** Verify button offset from bframe right */
1772
+ VERIFY_BTN_RIGHT_OFFSET: 60
1773
+ };
1774
+ var THRESHOLDS = {
1775
+ /** Minimum character change to consider page "changed" */
1776
+ STALE_CHAR_CHANGE: 100,
1777
+ /** Minimum percentage change to consider page "changed" */
1778
+ STALE_PERCENT_CHANGE: 0.05,
1779
+ /** Minimum body text length to not be considered "empty" */
1780
+ MIN_BODY_TEXT: 200,
1781
+ /** Maximum response text length to capture */
1782
+ MAX_RESPONSE_TEXT: 1e5
1783
+ };
1784
+ var TIMEOUTS = {
1785
+ DEFAULT_STALE: 2e4,
1786
+ NAVIGATION: 6e4,
1787
+ SELECTOR_WAIT: 1e4,
1788
+ // Actionability wait for click. Under the 20s stale timer on purpose, so
1789
+ // Playwright's own reason (not visible, covered by another element…) reaches
1790
+ // the agent instead of a generic "no state change".
1791
+ CLICK: 8e3,
1792
+ TOTP_INPUT: 5e3,
1793
+ API_REQUEST: 18e4,
1794
+ HEALTH_CHECK: 3e3,
1795
+ CHALLENGE_FRAME_WAIT: 5e3,
1796
+ // New-tab following: after a click, how long to wait for a just-opened tab's
1797
+ // 'page' event to arrive (only applies when no tab has registered yet), and
1798
+ // how long to let the followed tab reach domcontentloaded before continuing.
1799
+ TAB_FOLLOW_SETTLE: 400,
1800
+ TAB_LOAD: 15e3,
1801
+ // A new tab usually opens at about:blank then navigates to its real URL —
1802
+ // wait this long for that first real navigation before judging it blank.
1803
+ TAB_BLANK_RESOLVE: 3e3
1804
+ };
1805
+
2083
1806
  // src/lib/browser/humanize.ts
2084
1807
  function rand(min, max) {
2085
1808
  return Math.random() * (max - min) + min;
@@ -2117,8 +1840,8 @@ async function humanMove(page, toX, toY) {
2117
1840
  const lastPos = mousePositions.get(page);
2118
1841
  const fromX = lastPos?.x ?? randRange(TIMING.IDLE_MOUSE_X);
2119
1842
  const fromY = lastPos?.y ?? randRange(TIMING.IDLE_MOUSE_Y);
2120
- const path13 = generatePath(fromX, fromY, toX, toY);
2121
- for (const point of path13) {
1843
+ const path12 = generatePath(fromX, fromY, toX, toY);
1844
+ for (const point of path12) {
2122
1845
  await mouse.move(point.x, point.y);
2123
1846
  await sleep3(rand(2, 12));
2124
1847
  }
@@ -2340,9 +2063,6 @@ function sleep3(ms) {
2340
2063
  return new Promise((r) => setTimeout(r, ms));
2341
2064
  }
2342
2065
 
2343
- // src/lib/actions/handlers/navigation.ts
2344
- init_persistence();
2345
-
2346
2066
  // src/lib/budget.ts
2347
2067
  var BudgetExceededError = class extends Error {
2348
2068
  constructor(label, ms) {
@@ -2496,7 +2216,7 @@ async function probeField(page, selector) {
2496
2216
  }
2497
2217
 
2498
2218
  // src/lib/actions/handlers/navigation.ts
2499
- var log9 = createLogger("actions");
2219
+ var log7 = createLogger("actions");
2500
2220
  var NAV_COMMIT_SUSPECT_MS = 12e3;
2501
2221
  async function navigate(page, step, ctx, monitor) {
2502
2222
  const waitUntil = step.waitUntil || "domcontentloaded";
@@ -2519,7 +2239,7 @@ async function navigate(page, step, ctx, monitor) {
2519
2239
  if (page.url() !== urlBefore) {
2520
2240
  await nav;
2521
2241
  } else {
2522
- log9.warn(`navigate: no commit after ${NAV_COMMIT_SUSPECT_MS}ms, falling back to location.assign`);
2242
+ log7.warn(`navigate: no commit after ${NAV_COMMIT_SUSPECT_MS}ms, falling back to location.assign`);
2523
2243
  monitor?.reportActivity();
2524
2244
  await withBudget(page.evaluate((u) => {
2525
2245
  location.assign(u);
@@ -2536,17 +2256,16 @@ async function navigate(page, step, ctx, monitor) {
2536
2256
  } finally {
2537
2257
  page.off("dialog", onDialog);
2538
2258
  }
2539
- const stealthScript = contextStealthScripts.get(page.context()) ?? STEALTH_SCRIPT;
2540
2259
  try {
2541
- await page.evaluate(stealthScript);
2260
+ await page.evaluate(STEALTH_SCRIPT);
2542
2261
  } catch (err) {
2543
- log9.warn(`stealth injection failed: ${err}`);
2262
+ log7.warn(`stealth injection failed: ${err}`);
2544
2263
  }
2545
2264
  if (ctx.sessionData) {
2546
2265
  try {
2547
2266
  await injectStorage(page, ctx.sessionData);
2548
2267
  } catch (err) {
2549
- log9.warn(`storage injection after navigate failed: ${err}`);
2268
+ log7.warn(`storage injection after navigate failed: ${err}`);
2550
2269
  }
2551
2270
  }
2552
2271
  }
@@ -2747,7 +2466,7 @@ async function download(page, step) {
2747
2466
  const status = resp.status();
2748
2467
  if (!resp.ok()) throw new Error(`download: HTTP ${status} for ${step.url}`);
2749
2468
  const buf = await resp.body();
2750
- const fs14 = await import("fs");
2469
+ const fs12 = await import("fs");
2751
2470
  const pathMod = await import("path");
2752
2471
  const { getDataDir: getDataDir2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
2753
2472
  let outPath;
@@ -2761,8 +2480,8 @@ async function download(page, step) {
2761
2480
  }
2762
2481
  outPath = pathMod.join(getDataDir2(), "downloads", step.path || name);
2763
2482
  }
2764
- fs14.mkdirSync(pathMod.dirname(outPath), { recursive: true });
2765
- fs14.writeFileSync(outPath, buf);
2483
+ fs12.mkdirSync(pathMod.dirname(outPath), { recursive: true });
2484
+ fs12.writeFileSync(outPath, buf);
2766
2485
  return { path: outPath, size: buf.length, status };
2767
2486
  }
2768
2487
  async function typeCode(page, step, ctx) {
@@ -2808,12 +2527,12 @@ async function find(page, step, ctx) {
2808
2527
  const elInfo = await element.evaluate((el) => {
2809
2528
  const tag = el.tagName.toLowerCase();
2810
2529
  const text = (el.textContent?.trim() || "").slice(0, 60);
2811
- const path13 = [];
2530
+ const path12 = [];
2812
2531
  let current = el;
2813
2532
  while (current && current !== document.body && current !== document.documentElement) {
2814
2533
  let seg = current.tagName.toLowerCase();
2815
2534
  if (current.id && /^[a-zA-Z][\w-]*$/.test(current.id)) {
2816
- path13.unshift(`#${current.id}`);
2535
+ path12.unshift(`#${current.id}`);
2817
2536
  break;
2818
2537
  }
2819
2538
  const parent = current.parentElement;
@@ -2825,10 +2544,10 @@ async function find(page, step, ctx) {
2825
2544
  seg += `:nth-of-type(${idx})`;
2826
2545
  }
2827
2546
  }
2828
- path13.unshift(seg);
2547
+ path12.unshift(seg);
2829
2548
  current = parent;
2830
2549
  }
2831
- return { tag, text, selector: path13.join(" > ") };
2550
+ return { tag, text, selector: path12.join(" > ") };
2832
2551
  });
2833
2552
  const ref = `@e${ctx.nextRefId++}`;
2834
2553
  const displayRole = step.role || elInfo.tag;
@@ -2848,6 +2567,73 @@ async function find(page, step, ctx) {
2848
2567
  };
2849
2568
  }
2850
2569
 
2570
+ // src/lib/screenshot.ts
2571
+ var import_crypto3 = require("crypto");
2572
+ var import_fs6 = __toESM(require("fs"));
2573
+ var import_path7 = __toESM(require("path"));
2574
+ var log8 = createLogger("screenshot");
2575
+ var MAX_AGE_MS = parseInt(process.env.IFRAMER_SCREENSHOT_MAX_AGE_MS || String(24 * 60 * 60 * 1e3), 10);
2576
+ var MAX_FILES = parseInt(process.env.IFRAMER_SCREENSHOT_MAX_FILES || "500", 10);
2577
+ var PRUNE_THROTTLE_MS = 5 * 60 * 1e3;
2578
+ var lastPruneAt = 0;
2579
+ function saveScreenshot(buffer, filename, screenshotDir, publicUrl) {
2580
+ import_fs6.default.mkdirSync(screenshotDir, { recursive: true, mode: 448 });
2581
+ const ext = import_path7.default.extname(filename);
2582
+ const unique = `${import_path7.default.basename(filename, ext)}-${(0, import_crypto3.randomBytes)(8).toString("hex")}${ext}`;
2583
+ import_fs6.default.writeFileSync(import_path7.default.join(screenshotDir, unique), buffer, { mode: 384 });
2584
+ maybePrune(screenshotDir);
2585
+ return `${publicUrl}/screenshots/${unique}`;
2586
+ }
2587
+ function maybePrune(dir) {
2588
+ const now = Date.now();
2589
+ if (now - lastPruneAt < PRUNE_THROTTLE_MS) return;
2590
+ lastPruneAt = now;
2591
+ pruneScreenshots(dir);
2592
+ }
2593
+ function pruneScreenshots(dir, opts = {}) {
2594
+ const maxAgeMs = opts.maxAgeMs ?? MAX_AGE_MS;
2595
+ const maxFiles = opts.maxFiles ?? MAX_FILES;
2596
+ const now = opts.now ?? Date.now();
2597
+ try {
2598
+ const entries = import_fs6.default.readdirSync(dir).filter((f) => f.endsWith(".jpg") || f.endsWith(".jpeg") || f.endsWith(".png")).map((f) => {
2599
+ const full = import_path7.default.join(dir, f);
2600
+ try {
2601
+ return { full, mtimeMs: import_fs6.default.statSync(full).mtimeMs };
2602
+ } catch {
2603
+ return null;
2604
+ }
2605
+ }).filter((e) => e !== null);
2606
+ let removed = 0;
2607
+ const survivors = [];
2608
+ for (const e of entries) {
2609
+ if (now - e.mtimeMs > maxAgeMs) {
2610
+ try {
2611
+ import_fs6.default.unlinkSync(e.full);
2612
+ removed++;
2613
+ } catch {
2614
+ }
2615
+ } else {
2616
+ survivors.push(e);
2617
+ }
2618
+ }
2619
+ if (survivors.length > maxFiles) {
2620
+ survivors.sort((a, b) => a.mtimeMs - b.mtimeMs);
2621
+ for (const e of survivors.slice(0, survivors.length - maxFiles)) {
2622
+ try {
2623
+ import_fs6.default.unlinkSync(e.full);
2624
+ removed++;
2625
+ } catch {
2626
+ }
2627
+ }
2628
+ }
2629
+ if (removed > 0) log8.debug(`pruned ${removed} old screenshot(s) from ${dir}`);
2630
+ return removed;
2631
+ } catch (err) {
2632
+ log8.warn(`screenshot prune failed: ${err instanceof Error ? err.message : String(err)}`);
2633
+ return 0;
2634
+ }
2635
+ }
2636
+
2851
2637
  // src/lib/snapshot.ts
2852
2638
  async function takeSnapshot(page, ctx, options) {
2853
2639
  const interactiveOnly = options?.interactiveOnly ?? true;
@@ -2943,12 +2729,12 @@ async function takeSnapshot(page, ctx, options) {
2943
2729
  return textual && el.getAttribute("tabindex") === "-1";
2944
2730
  }
2945
2731
  function buildSelector(el) {
2946
- const path13 = [];
2732
+ const path12 = [];
2947
2733
  let current = el;
2948
2734
  while (current && current !== document.body && current !== document.documentElement) {
2949
2735
  let seg = current.tagName.toLowerCase();
2950
2736
  if (current.id && /^[a-zA-Z][\w-]*$/.test(current.id)) {
2951
- path13.unshift(`#${current.id}`);
2737
+ path12.unshift(`#${current.id}`);
2952
2738
  break;
2953
2739
  }
2954
2740
  const parent = current.parentElement;
@@ -2960,10 +2746,10 @@ async function takeSnapshot(page, ctx, options) {
2960
2746
  seg += `:nth-of-type(${idx})`;
2961
2747
  }
2962
2748
  }
2963
- path13.unshift(seg);
2749
+ path12.unshift(seg);
2964
2750
  current = parent;
2965
2751
  }
2966
- return path13.join(" > ");
2752
+ return path12.join(" > ");
2967
2753
  }
2968
2754
  function getName(el) {
2969
2755
  const ariaLabel = el.getAttribute("aria-label");
@@ -3095,12 +2881,12 @@ async function annotatedScreenshot(page, ctx) {
3095
2881
  return true;
3096
2882
  }
3097
2883
  function buildSelector(el) {
3098
- const path13 = [];
2884
+ const path12 = [];
3099
2885
  let current = el;
3100
2886
  while (current && current !== document.body && current !== document.documentElement) {
3101
2887
  let seg = current.tagName.toLowerCase();
3102
2888
  if (current.id && /^[a-zA-Z][\w-]*$/.test(current.id)) {
3103
- path13.unshift(`#${current.id}`);
2889
+ path12.unshift(`#${current.id}`);
3104
2890
  break;
3105
2891
  }
3106
2892
  const parent = current.parentElement;
@@ -3112,10 +2898,10 @@ async function annotatedScreenshot(page, ctx) {
3112
2898
  seg += `:nth-of-type(${idx})`;
3113
2899
  }
3114
2900
  }
3115
- path13.unshift(seg);
2901
+ path12.unshift(seg);
3116
2902
  current = parent;
3117
2903
  }
3118
- return path13.join(" > ");
2904
+ return path12.join(" > ");
3119
2905
  }
3120
2906
  function getName(el) {
3121
2907
  const ariaLabel = el.getAttribute("aria-label");
@@ -3219,18 +3005,72 @@ async function snapshot(page, step, ctx) {
3219
3005
  return { elementCount: snap.nodes.length, snapshot: snap.text };
3220
3006
  }
3221
3007
 
3222
- // src/lib/captcha/recaptcha.ts
3008
+ // src/lib/captcha/vision.ts
3223
3009
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
3224
- var log10 = createLogger("captcha-solver");
3225
- var MAX_ROUNDS = 8;
3226
- var MAX_DURATION_MS = 45e3;
3227
- var TILE_SETTLE_MS = TIMING.TILE_SETTLE;
3228
- var MODEL = "claude-haiku-4-5-20251001";
3229
- function getClient() {
3010
+ var CAPTCHA_MODEL = process.env.IFRAMER_CAPTCHA_MODEL || "claude-haiku-4-5-20251001";
3011
+ var CaptchaApiError = class extends Error {
3012
+ };
3013
+ function getCaptchaClient() {
3230
3014
  const apiKey = process.env.ANTHROPIC_API_KEY;
3231
3015
  if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set \u2014 required for captcha auto-solve");
3232
3016
  return new import_sdk.default({ apiKey });
3233
3017
  }
3018
+ function toCaptchaApiError(err) {
3019
+ if (err instanceof import_sdk.default.APIConnectionError) {
3020
+ const cause = err.cause instanceof Error ? `: ${err.cause.message}` : "";
3021
+ return new CaptchaApiError(`Claude API call failed (no response): ${err.message}${cause}`);
3022
+ }
3023
+ if (err instanceof import_sdk.default.APIError) {
3024
+ const body = err.error;
3025
+ return new CaptchaApiError(`Claude API call failed (${err.status ?? "no response"}): ${body?.error?.message ?? err.message}`);
3026
+ }
3027
+ return new CaptchaApiError(`Claude API call failed: ${err instanceof Error ? err.message : String(err)}`);
3028
+ }
3029
+ async function askVision(client, params) {
3030
+ try {
3031
+ const response = await client.messages.create({ ...params, model: CAPTCHA_MODEL });
3032
+ return response.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("\n").trim();
3033
+ } catch (err) {
3034
+ throw toCaptchaApiError(err);
3035
+ }
3036
+ }
3037
+ var MAX_CLICKS = 12;
3038
+ function parseChallengeAnswer(text, width, height) {
3039
+ const candidates = text.match(/\{[^{}]*\}/g);
3040
+ if (!candidates) return null;
3041
+ const toPoint = (p) => {
3042
+ if (!Array.isArray(p) || p.length !== 2) return null;
3043
+ const [x, y] = p;
3044
+ if (typeof x !== "number" || typeof y !== "number") return null;
3045
+ return x >= 0 && y >= 0 && x <= width && y <= height ? { x, y } : null;
3046
+ };
3047
+ for (const raw of candidates.reverse()) {
3048
+ let parsed;
3049
+ try {
3050
+ parsed = JSON.parse(raw);
3051
+ } catch {
3052
+ continue;
3053
+ }
3054
+ if (Array.isArray(parsed.drag)) {
3055
+ if (parsed.drag.length !== 2) return null;
3056
+ const from = toPoint(parsed.drag[0]);
3057
+ const to = toPoint(parsed.drag[1]);
3058
+ return from && to ? { drag: { from, to } } : null;
3059
+ }
3060
+ if (Array.isArray(parsed.clicks)) {
3061
+ if (parsed.clicks.length > MAX_CLICKS) return null;
3062
+ const clicks = parsed.clicks.map(toPoint);
3063
+ return clicks.every((c) => c !== null) ? { clicks } : null;
3064
+ }
3065
+ }
3066
+ return null;
3067
+ }
3068
+
3069
+ // src/lib/captcha/recaptcha.ts
3070
+ var log9 = createLogger("captcha-solver");
3071
+ var MAX_ROUNDS = 8;
3072
+ var MAX_DURATION_MS = 45e3;
3073
+ var TILE_SETTLE_MS = TIMING.TILE_SETTLE;
3234
3074
  function extractTarget(prompt) {
3235
3075
  const lines = prompt.split("\n").map((l) => l.trim()).filter(Boolean);
3236
3076
  for (let i = 0; i < lines.length; i++) {
@@ -3259,7 +3099,7 @@ async function screenshotFullGrid(page, challengeInfo) {
3259
3099
  const buf = await page.screenshot({ type: "jpeg", quality: 85, clip: gridClip });
3260
3100
  return buf.toString("base64");
3261
3101
  } catch (err) {
3262
- log10.error(`full grid screenshot failed: ${err instanceof Error ? err.message : String(err)}`);
3102
+ log9.error(`full grid screenshot failed: ${err instanceof Error ? err.message : String(err)}`);
3263
3103
  return null;
3264
3104
  }
3265
3105
  }
@@ -3294,40 +3134,33 @@ async function classifyTiles(client, fullGridBase64, tiles, target, rows, cols)
3294
3134
  tiles.map(async (tile) => {
3295
3135
  const tileRow = Math.floor(tile.index / cols) + 1;
3296
3136
  const tileCol = tile.index % cols + 1;
3297
- try {
3298
- const response = await client.messages.create({
3299
- model: MODEL,
3300
- max_tokens: 10,
3301
- messages: [
3302
- {
3303
- role: "user",
3304
- content: [
3305
- {
3306
- type: "image",
3307
- source: { type: "base64", media_type: "image/jpeg", data: fullGridBase64 }
3308
- },
3309
- {
3310
- type: "image",
3311
- source: { type: "base64", media_type: "image/jpeg", data: tile.imageBase64 }
3312
- },
3313
- {
3314
- type: "text",
3315
- text: `Image 1 is a full picture divided into a ${rows}x${cols} grid. Image 2 is the tile at row ${tileRow}, column ${tileCol} of that grid.
3137
+ const answer = await askVision(client, {
3138
+ max_tokens: 256,
3139
+ messages: [
3140
+ {
3141
+ role: "user",
3142
+ content: [
3143
+ {
3144
+ type: "image",
3145
+ source: { type: "base64", media_type: "image/jpeg", data: fullGridBase64 }
3146
+ },
3147
+ {
3148
+ type: "image",
3149
+ source: { type: "base64", media_type: "image/jpeg", data: tile.imageBase64 }
3150
+ },
3151
+ {
3152
+ type: "text",
3153
+ text: `Image 1 is a full picture divided into a ${rows}x${cols} grid. Image 2 is the tile at row ${tileRow}, column ${tileCol} of that grid.
3316
3154
 
3317
3155
  Does this tile contain a ${target}? Reply ONLY "yes" or "no".`
3318
- }
3319
- ]
3320
- }
3321
- ]
3322
- });
3323
- const answer = (response.content[0].text ?? "").toLowerCase().trim();
3324
- const match = answer.startsWith("yes");
3325
- if (match) log10.debug(`tile ${tile.index} (r${tileRow}c${tileCol}): YES`);
3326
- return { index: tile.index, match };
3327
- } catch (err) {
3328
- log10.error(`tile ${tile.index} classification failed: ${err instanceof Error ? err.message : String(err)}`);
3329
- return { index: tile.index, match: false };
3330
- }
3156
+ }
3157
+ ]
3158
+ }
3159
+ ]
3160
+ });
3161
+ const match = answer.toLowerCase().startsWith("yes");
3162
+ if (match) log9.debug(`tile ${tile.index} (r${tileRow}c${tileCol}): YES`);
3163
+ return { index: tile.index, match };
3331
3164
  })
3332
3165
  );
3333
3166
  return results.filter((r) => r.match).map((r) => r.index);
@@ -3348,7 +3181,7 @@ async function submitForm(page) {
3348
3181
  if (el) {
3349
3182
  const visible = await el.isVisible();
3350
3183
  if (visible) {
3351
- log10.info(`Submitting form via: ${selector}`);
3184
+ log9.info(`Submitting form via: ${selector}`);
3352
3185
  await new Promise((r) => setTimeout(r, 500));
3353
3186
  await humanClick(page, selector);
3354
3187
  await new Promise((r) => setTimeout(r, 2e3));
@@ -3358,117 +3191,130 @@ async function submitForm(page) {
3358
3191
  } catch {
3359
3192
  }
3360
3193
  }
3361
- log10.info("No submit button found \u2014 skipping form submission");
3194
+ log9.info("No submit button found \u2014 skipping form submission");
3362
3195
  return false;
3363
3196
  }
3364
3197
  async function solveRecaptcha(page, monitor) {
3365
3198
  const startTime = Date.now();
3366
- const client = getClient();
3199
+ const client = getCaptchaClient();
3367
3200
  let rounds = 0;
3368
- const checkboxResult = await clickRecaptchaCheckbox(page);
3369
- if (checkboxResult.solved) {
3370
- const submitted = await submitForm(page);
3371
- return { solved: true, rounds: 0, durationMs: Date.now() - startTime, submitted };
3372
- }
3373
- if (!checkboxResult.challengeInfo) {
3374
- return { solved: false, rounds: 0, durationMs: Date.now() - startTime, reason: "No challenge appeared after clicking checkbox" };
3375
- }
3376
- let challengeInfo = checkboxResult.challengeInfo;
3377
- while (rounds < MAX_ROUNDS) {
3378
- if (Date.now() - startTime > MAX_DURATION_MS) {
3379
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Timeout exceeded" };
3380
- }
3381
- rounds++;
3382
- monitor?.reportActivity();
3383
- if (!challengeInfo) {
3384
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Challenge info lost" };
3385
- }
3386
- const target = extractTarget(challengeInfo.prompt);
3387
- log10.info(`Round ${rounds}: looking for "${target}" in ${challengeInfo.rows}x${challengeInfo.cols} grid`);
3388
- const [fullGridImage, tileImages] = await Promise.all([
3389
- screenshotFullGrid(page, challengeInfo),
3390
- screenshotTiles(page, challengeInfo)
3391
- ]);
3392
- if (!fullGridImage || tileImages.length === 0) {
3393
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Failed to screenshot challenge" };
3394
- }
3395
- monitor?.reportActivity();
3396
- const matchingIndices = await classifyTiles(
3397
- client,
3398
- fullGridImage,
3399
- tileImages,
3400
- target,
3401
- challengeInfo.rows,
3402
- challengeInfo.cols
3403
- );
3404
- log10.info(`Round ${rounds}: matched tiles [${matchingIndices.join(", ")}]`);
3405
- monitor?.reportActivity();
3406
- if (matchingIndices.length > 0) {
3407
- await clickChallengeTiles(page, matchingIndices);
3408
- const isDynamic = challengeInfo.prompt.toLowerCase().includes("none left");
3409
- if (isDynamic) {
3410
- await new Promise((r) => setTimeout(r, TILE_SETTLE_MS));
3411
- const newInfo = await getChallengeInfo(page);
3412
- if (newInfo) {
3413
- const [newFullGrid, newTiles] = await Promise.all([
3414
- screenshotFullGrid(page, newInfo),
3415
- screenshotTiles(page, newInfo)
3416
- ]);
3417
- if (newFullGrid && newTiles.length > 0) {
3418
- const replacedTiles = newTiles.filter((t) => matchingIndices.includes(t.index));
3419
- if (replacedTiles.length > 0) {
3420
- monitor?.reportActivity();
3421
- const newMatches = await classifyTiles(
3422
- client,
3423
- newFullGrid,
3424
- replacedTiles,
3425
- target,
3426
- newInfo.rows,
3427
- newInfo.cols
3428
- );
3429
- if (newMatches.length > 0) {
3430
- log10.info(`Round ${rounds}: dynamic tiles matched [${newMatches.join(", ")}]`);
3431
- await clickChallengeTiles(page, newMatches);
3432
- await new Promise((r) => setTimeout(r, TILE_SETTLE_MS));
3201
+ try {
3202
+ const checkboxResult = await clickRecaptchaCheckbox(page);
3203
+ if (checkboxResult.solved) {
3204
+ const submitted = await submitForm(page);
3205
+ return { solved: true, rounds: 0, durationMs: Date.now() - startTime, submitted };
3206
+ }
3207
+ if (!checkboxResult.challengeInfo) {
3208
+ return { solved: false, rounds: 0, durationMs: Date.now() - startTime, reason: "No challenge appeared after clicking checkbox" };
3209
+ }
3210
+ let challengeInfo = checkboxResult.challengeInfo;
3211
+ while (rounds < MAX_ROUNDS) {
3212
+ if (Date.now() - startTime > MAX_DURATION_MS) {
3213
+ return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Timeout exceeded" };
3214
+ }
3215
+ rounds++;
3216
+ monitor?.reportActivity();
3217
+ if (!challengeInfo) {
3218
+ return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Challenge info lost" };
3219
+ }
3220
+ const target = extractTarget(challengeInfo.prompt);
3221
+ log9.info(`Round ${rounds}: looking for "${target}" in ${challengeInfo.rows}x${challengeInfo.cols} grid`);
3222
+ const [fullGridImage, tileImages] = await Promise.all([
3223
+ screenshotFullGrid(page, challengeInfo),
3224
+ screenshotTiles(page, challengeInfo)
3225
+ ]);
3226
+ if (!fullGridImage || tileImages.length === 0) {
3227
+ return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Failed to screenshot challenge" };
3228
+ }
3229
+ monitor?.reportActivity();
3230
+ const matchingIndices = await classifyTiles(
3231
+ client,
3232
+ fullGridImage,
3233
+ tileImages,
3234
+ target,
3235
+ challengeInfo.rows,
3236
+ challengeInfo.cols
3237
+ );
3238
+ log9.info(`Round ${rounds}: matched tiles [${matchingIndices.join(", ")}]`);
3239
+ monitor?.reportActivity();
3240
+ if (matchingIndices.length > 0) {
3241
+ await clickChallengeTiles(page, matchingIndices);
3242
+ const isDynamic = challengeInfo.prompt.toLowerCase().includes("none left");
3243
+ if (isDynamic) {
3244
+ await new Promise((r) => setTimeout(r, TILE_SETTLE_MS));
3245
+ const newInfo = await getChallengeInfo(page);
3246
+ if (newInfo) {
3247
+ const [newFullGrid, newTiles] = await Promise.all([
3248
+ screenshotFullGrid(page, newInfo),
3249
+ screenshotTiles(page, newInfo)
3250
+ ]);
3251
+ if (newFullGrid && newTiles.length > 0) {
3252
+ const replacedTiles = newTiles.filter((t) => matchingIndices.includes(t.index));
3253
+ if (replacedTiles.length > 0) {
3254
+ monitor?.reportActivity();
3255
+ const newMatches = await classifyTiles(
3256
+ client,
3257
+ newFullGrid,
3258
+ replacedTiles,
3259
+ target,
3260
+ newInfo.rows,
3261
+ newInfo.cols
3262
+ );
3263
+ if (newMatches.length > 0) {
3264
+ log9.info(`Round ${rounds}: dynamic tiles matched [${newMatches.join(", ")}]`);
3265
+ await clickChallengeTiles(page, newMatches);
3266
+ await new Promise((r) => setTimeout(r, TILE_SETTLE_MS));
3267
+ }
3433
3268
  }
3434
3269
  }
3435
3270
  }
3436
3271
  }
3437
3272
  }
3273
+ const verifyResult = await clickChallengeVerify(page);
3274
+ monitor?.reportActivity();
3275
+ if (verifyResult.solved) {
3276
+ log9.info(`Solved in ${rounds} rounds, ${Date.now() - startTime}ms`);
3277
+ const submitted = await submitForm(page);
3278
+ return { solved: true, rounds, durationMs: Date.now() - startTime, submitted };
3279
+ }
3280
+ challengeInfo = verifyResult.challengeInfo || null;
3281
+ if (!challengeInfo) {
3282
+ return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Challenge disappeared after verify" };
3283
+ }
3284
+ log9.info(`Round ${rounds}: not solved, new challenge appeared`);
3438
3285
  }
3439
- const verifyResult = await clickChallengeVerify(page);
3440
- monitor?.reportActivity();
3441
- if (verifyResult.solved) {
3442
- log10.info(`Solved in ${rounds} rounds, ${Date.now() - startTime}ms`);
3443
- const submitted = await submitForm(page);
3444
- return { solved: true, rounds, durationMs: Date.now() - startTime, submitted };
3445
- }
3446
- challengeInfo = verifyResult.challengeInfo || null;
3447
- if (!challengeInfo) {
3448
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Challenge disappeared after verify" };
3286
+ return { solved: false, rounds, durationMs: Date.now() - startTime, reason: `Max rounds (${MAX_ROUNDS}) exceeded` };
3287
+ } catch (err) {
3288
+ if (err instanceof CaptchaApiError) {
3289
+ return { solved: false, rounds, durationMs: Date.now() - startTime, reason: err.message };
3449
3290
  }
3450
- log10.info(`Round ${rounds}: not solved, new challenge appeared`);
3291
+ throw err;
3451
3292
  }
3452
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: `Max rounds (${MAX_ROUNDS}) exceeded` };
3453
3293
  }
3454
3294
 
3455
3295
  // src/lib/captcha/hcaptcha.ts
3456
- var import_sdk2 = __toESM(require("@anthropic-ai/sdk"));
3457
- var log11 = createLogger("hcaptcha-solver");
3296
+ var log10 = createLogger("hcaptcha-solver");
3458
3297
  var MAX_ROUNDS2 = 8;
3459
3298
  var MAX_DURATION_MS2 = 6e4;
3460
- var MODEL2 = "claude-haiku-4-5-20251001";
3461
- function getClient2() {
3462
- const apiKey = process.env.ANTHROPIC_API_KEY;
3463
- if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set \u2014 required for captcha auto-solve");
3464
- return new import_sdk2.default({ apiKey });
3465
- }
3299
+ var CHALLENGE_FRAME = 'iframe[title="hCaptcha challenge"], iframe[title*="hcaptcha challenge" i]';
3466
3300
  function rand2(min, max) {
3467
3301
  return Math.random() * (max - min) + min;
3468
3302
  }
3469
3303
  function sleep4(ms) {
3470
3304
  return new Promise((r) => setTimeout(r, ms));
3471
3305
  }
3306
+ async function hasToken(page, waitMs = 0) {
3307
+ const deadline = Date.now() + waitMs;
3308
+ for (; ; ) {
3309
+ const found = await page.evaluate(
3310
+ () => Array.from(document.querySelectorAll('[name="h-captcha-response"]')).some(
3311
+ (el) => el.value.length > 0
3312
+ )
3313
+ ).catch(() => false);
3314
+ if (found || Date.now() >= deadline) return found;
3315
+ await sleep4(250);
3316
+ }
3317
+ }
3472
3318
  async function clickCheckbox(page) {
3473
3319
  const checkboxFrame = await page.waitForSelector(
3474
3320
  'iframe[src*="hcaptcha.com"], iframe[data-hcaptcha-widget-id], iframe[title*="hCaptcha"]',
@@ -3483,187 +3329,119 @@ async function clickCheckbox(page) {
3483
3329
  const cy = box.y + box.height * rand2(0.3, 0.7);
3484
3330
  await humanClickXY(page, cx, cy);
3485
3331
  await sleep4(2500);
3486
- const frame = await checkboxFrame.contentFrame();
3487
- if (frame) {
3488
- try {
3489
- const checked = await frame.evaluate(() => {
3490
- const cb = document.querySelector("#checkbox");
3491
- return cb?.getAttribute("aria-checked") === "true";
3492
- });
3493
- if (checked) return true;
3494
- } catch {
3495
- }
3496
- }
3497
- return false;
3332
+ return hasToken(page);
3498
3333
  }
3499
- async function getChallengeInfo2(page) {
3500
- const challengeFrame = await page.waitForSelector(
3501
- 'iframe[title="hCaptcha challenge"], iframe[title*="hcaptcha challenge" i]',
3502
- { timeout: 8e3 }
3503
- ).catch(() => null);
3334
+ async function getChallenge(page) {
3335
+ const challengeFrame = await page.waitForSelector(CHALLENGE_FRAME, { timeout: 8e3 }).catch(() => null);
3504
3336
  if (!challengeFrame) return null;
3505
- const frameBox = await challengeFrame.boundingBox();
3506
- if (!frameBox) return null;
3507
3337
  const frame = await challengeFrame.contentFrame();
3508
3338
  if (!frame) return null;
3509
- const info = await frame.evaluate(() => {
3510
- const promptEl = document.querySelector(
3511
- ".prompt-text, .task-instructions, [class*='prompt'], [class*='task-description']"
3512
- );
3513
- const prompt = promptEl ? promptEl.innerText.trim() : "";
3514
- const tileEls = document.querySelectorAll(
3515
- ".task-image, [class*='task-grid'] > *, [class*='challenge-container'] .image-wrapper, .image-wrapper"
3516
- );
3517
- const count = tileEls.length;
3518
- let rows = 3, cols = 3;
3519
- if (count === 16) {
3520
- rows = 4;
3521
- cols = 4;
3522
- } else if (count === 9) {
3523
- rows = 3;
3524
- cols = 3;
3525
- } else if (count === 6) {
3526
- rows = 2;
3527
- cols = 3;
3528
- }
3529
- return { prompt, count, rows, cols };
3530
- }).catch(() => ({ prompt: "", count: 0, rows: 3, cols: 3 }));
3531
- if (!info.prompt && info.count === 0) return null;
3532
- const gridPadTop = 150;
3533
- const gridPadLeft = 20;
3534
- const gridPadRight = 20;
3535
- const gridPadBottom = 80;
3536
- const gridWidth = frameBox.width - gridPadLeft - gridPadRight;
3537
- const gridHeight = frameBox.height - gridPadTop - gridPadBottom;
3538
- const tileW = gridWidth / info.cols;
3539
- const tileH = gridHeight / info.rows;
3540
- const tiles = [];
3541
- for (let r = 0; r < info.rows; r++) {
3542
- for (let c = 0; c < info.cols; c++) {
3543
- tiles.push({
3544
- index: r * info.cols + c,
3545
- centerX: Math.round(frameBox.x + gridPadLeft + c * tileW + tileW / 2),
3546
- centerY: Math.round(frameBox.y + gridPadTop + r * tileH + tileH / 2)
3547
- });
3548
- }
3549
- }
3550
- const verifyBtnBox = await frame.evaluate(() => {
3551
- const btn = document.querySelector('.button-submit.button, [aria-label="Verify"], [aria-label="Skip Challenge"]');
3552
- if (!btn) return null;
3553
- const r = btn.getBoundingClientRect();
3554
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
3555
- }).catch(() => null);
3556
- const verifyButton = verifyBtnBox ? { x: Math.round(frameBox.x + verifyBtnBox.x), y: Math.round(frameBox.y + verifyBtnBox.y) } : { x: Math.round(frameBox.x + frameBox.width - 55), y: Math.round(frameBox.y + frameBox.height - 30) };
3557
- log11.info(`Round challenge: "${info.prompt}" (${info.rows}x${info.cols})`);
3558
- return { prompt: info.prompt, rows: info.rows, cols: info.cols, tiles, verifyButton, frameBox };
3559
- }
3560
- async function screenshotChallenge(page, challenge) {
3561
- const { frameBox } = challenge;
3562
- try {
3563
- await sleep4(1e3);
3564
- const challengeEl = await page.$('iframe[title="hCaptcha challenge"], iframe[title*="hcaptcha challenge" i]').catch(() => null);
3565
- let buf;
3566
- if (challengeEl) {
3567
- buf = Buffer.from(await challengeEl.screenshot({ type: "jpeg", quality: 85 }));
3568
- } else {
3569
- buf = await page.screenshot({ type: "jpeg", quality: 85, clip: frameBox });
3570
- }
3571
- return buf.toString("base64");
3572
- } catch (err) {
3573
- log11.error(`screenshot failed: ${err instanceof Error ? err.message : String(err)}`);
3574
- return null;
3339
+ const prompt = await frame.evaluate(() => {
3340
+ const el = document.querySelector(".prompt-text, .task-instructions, [class*='prompt'], [class*='task-description']");
3341
+ return el ? el.innerText.trim() : "";
3342
+ }).catch(() => "");
3343
+ if (!prompt) return null;
3344
+ const target = await frame.$("canvas").catch(() => null) ?? challengeFrame;
3345
+ const box = await target.boundingBox();
3346
+ if (!box) return null;
3347
+ log10.info(`Round challenge: "${prompt}" (${Math.round(box.width)}x${Math.round(box.height)})`);
3348
+ return { prompt, target, box, frame };
3349
+ }
3350
+ async function askForAnswer(client, challenge) {
3351
+ const buf = await challenge.target.screenshot({ type: "jpeg", quality: 85, scale: "css" });
3352
+ const width = Math.round(challenge.box.width);
3353
+ const height = Math.round(challenge.box.height);
3354
+ const text = await askVision(client, {
3355
+ max_tokens: 1024,
3356
+ messages: [{
3357
+ role: "user",
3358
+ content: [
3359
+ { type: "image", source: { type: "base64", media_type: "image/jpeg", data: Buffer.from(buf).toString("base64") } },
3360
+ {
3361
+ type: "text",
3362
+ text: `This image is ${width}x${height} pixels. The task shown with it: "${challenge.prompt}".
3363
+ Work out the answer, then end your reply with one line of JSON, using pixel coordinates in this image.
3364
+ To click: {"clicks":[[x,y],[x,y]]}
3365
+ To drag: {"drag":[[fromX,fromY],[toX,toY]]}
3366
+ If you can't tell: {"clicks":[]}`
3367
+ }
3368
+ ]
3369
+ }]
3370
+ });
3371
+ const answer = parseChallengeAnswer(text, width, height);
3372
+ if (!answer) log10.warn(`no usable answer in model reply: "${text.slice(0, 120)}"`);
3373
+ return answer;
3374
+ }
3375
+ async function perform(page, challenge, answer) {
3376
+ const { x, y } = challenge.box;
3377
+ if ("drag" in answer) {
3378
+ await humanMove(page, x + answer.drag.from.x, y + answer.drag.from.y);
3379
+ await sleep4(rand2(100, 250));
3380
+ await page.mouse.down();
3381
+ await humanMove(page, x + answer.drag.to.x, y + answer.drag.to.y);
3382
+ await sleep4(rand2(100, 250));
3383
+ await page.mouse.up();
3384
+ return;
3575
3385
  }
3576
- }
3577
- async function classifyTiles2(client, screenshotBase64, challenge) {
3578
- const { prompt, rows, cols } = challenge;
3579
- const total = rows * cols;
3580
- try {
3581
- const response = await client.messages.create({
3582
- model: MODEL2,
3583
- max_tokens: 30,
3584
- system: `Output ONLY a comma-separated list of tile numbers or "none". Valid examples: "0,3,5" or "2" or "none". Output nothing else.`,
3585
- messages: [{
3586
- role: "user",
3587
- content: [
3588
- { type: "image", source: { type: "base64", media_type: "image/jpeg", data: screenshotBase64 } },
3589
- { type: "text", text: `Task: "${prompt}". Tiles 0-${total - 1} left-to-right top-to-bottom in a ${rows}\xD7${cols} grid. Which tiles match? Reply ONLY with numbers or "none".` }
3590
- ]
3591
- }]
3592
- });
3593
- const text = (response.content[0].text ?? "").trim();
3594
- log11.debug(`classify response: "${text}"`);
3595
- if (text.toLowerCase().startsWith("none")) return [];
3596
- return text.split(/[,\s]+/).map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n >= 0 && n < total);
3597
- } catch (err) {
3598
- log11.error(`classification failed: ${err instanceof Error ? err.message : String(err)}`);
3599
- return [];
3386
+ for (const click2 of answer.clicks) {
3387
+ await humanClickXY(page, x + click2.x, y + click2.y);
3388
+ await sleep4(rand2(150, 400));
3600
3389
  }
3601
3390
  }
3602
- async function isSolved(page) {
3603
- const el = await page.$('iframe[title="hCaptcha challenge"], iframe[title*="hcaptcha challenge" i]').catch(() => null);
3604
- if (!el) return true;
3605
- const visible = await el.isVisible().catch(() => false);
3606
- return !visible;
3607
- }
3608
- async function clickVerify(page, challenge) {
3609
- await humanClickXY(page, challenge.verifyButton.x, challenge.verifyButton.y);
3391
+ async function clickSubmit(page, challenge) {
3392
+ const button = await challenge.frame.$(".button-submit").catch(() => null);
3393
+ const box = button ? await button.boundingBox() : null;
3394
+ if (!box) throw new Error("hCaptcha submit button not found");
3395
+ await humanClickXY(page, box.x + box.width / 2, box.y + box.height / 2);
3610
3396
  await sleep4(2500);
3611
- return isSolved(page);
3612
3397
  }
3613
3398
  async function solveHCaptcha(page, monitor) {
3614
3399
  const startTime = Date.now();
3615
- const client = getClient2();
3400
+ const client = getCaptchaClient();
3401
+ const result = (solved, reason) => ({
3402
+ solved,
3403
+ rounds,
3404
+ durationMs: Date.now() - startTime,
3405
+ ...reason ? { reason } : {}
3406
+ });
3616
3407
  let rounds = 0;
3617
- let solvedOnCheckbox = false;
3618
3408
  try {
3619
- solvedOnCheckbox = await clickCheckbox(page);
3409
+ if (await clickCheckbox(page)) {
3410
+ log10.info("Solved on checkbox click (no challenge)");
3411
+ return result(true);
3412
+ }
3413
+ while (rounds < MAX_ROUNDS2) {
3414
+ if (Date.now() - startTime > MAX_DURATION_MS2) return result(false, "Timeout exceeded");
3415
+ rounds++;
3416
+ monitor?.reportActivity();
3417
+ const challenge = await getChallenge(page);
3418
+ if (!challenge) {
3419
+ if (await hasToken(page, 3e3)) return result(true);
3420
+ return result(false, "The challenge closed without a token");
3421
+ }
3422
+ await sleep4(1e3);
3423
+ const answer = await askForAnswer(client, challenge);
3424
+ monitor?.reportActivity();
3425
+ log10.info(`Round ${rounds}: ${answer ? JSON.stringify(answer) : "no answer"}`);
3426
+ if (answer) await perform(page, challenge, answer);
3427
+ await sleep4(rand2(500, 1e3));
3428
+ await clickSubmit(page, challenge);
3429
+ monitor?.reportActivity();
3430
+ if (await hasToken(page)) {
3431
+ log10.info(`Solved in ${rounds} rounds, ${Date.now() - startTime}ms`);
3432
+ return result(true);
3433
+ }
3434
+ log10.info(`Round ${rounds}: not solved, retrying`);
3435
+ }
3436
+ return result(false, `Max rounds (${MAX_ROUNDS2}) exceeded`);
3620
3437
  } catch (err) {
3621
- return { solved: false, rounds: 0, durationMs: Date.now() - startTime, reason: err instanceof Error ? err.message : String(err) };
3622
- }
3623
- if (solvedOnCheckbox) {
3624
- log11.info("Solved on checkbox click (no challenge)");
3625
- return { solved: true, rounds: 0, durationMs: Date.now() - startTime };
3626
- }
3627
- while (rounds < MAX_ROUNDS2) {
3628
- if (Date.now() - startTime > MAX_DURATION_MS2) {
3629
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Timeout exceeded" };
3630
- }
3631
- rounds++;
3632
- monitor?.reportActivity();
3633
- const challenge = await getChallengeInfo2(page);
3634
- if (!challenge) {
3635
- log11.info("Challenge frame gone \u2014 assuming solved");
3636
- return { solved: true, rounds, durationMs: Date.now() - startTime };
3637
- }
3638
- const screenshotBase64 = await screenshotChallenge(page, challenge);
3639
- if (!screenshotBase64) {
3640
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: "Failed to screenshot challenge" };
3641
- }
3642
- monitor?.reportActivity();
3643
- const matchingIndices = await classifyTiles2(client, screenshotBase64, challenge);
3644
- log11.info(`Round ${rounds}: clicking tiles [${matchingIndices.join(", ")}]`);
3645
- monitor?.reportActivity();
3646
- for (const idx of matchingIndices) {
3647
- const tile = challenge.tiles[idx];
3648
- if (!tile) continue;
3649
- await humanClickXY(page, tile.centerX + rand2(-5, 5), tile.centerY + rand2(-5, 5));
3650
- await sleep4(rand2(150, 400));
3651
- }
3652
- await sleep4(rand2(500, 1e3));
3653
- const solved = await clickVerify(page, challenge);
3654
- monitor?.reportActivity();
3655
- if (solved) {
3656
- log11.info(`Solved in ${rounds} rounds, ${Date.now() - startTime}ms`);
3657
- return { solved: true, rounds, durationMs: Date.now() - startTime };
3658
- }
3659
- log11.info(`Round ${rounds}: not solved, retrying`);
3660
- await sleep4(rand2(500, 1e3));
3661
- }
3662
- return { solved: false, rounds, durationMs: Date.now() - startTime, reason: `Max rounds (${MAX_ROUNDS2}) exceeded` };
3438
+ if (err instanceof CaptchaApiError) return result(false, err.message);
3439
+ return result(false, err instanceof Error ? err.message : String(err));
3440
+ }
3663
3441
  }
3664
3442
 
3665
3443
  // src/lib/actions/handlers/captcha.ts
3666
- var log12 = createLogger("actions");
3444
+ var log11 = createLogger("actions");
3667
3445
  async function screenshotTiles2(page, ci) {
3668
3446
  const tileSize = ci.bframeBox ? Math.round((ci.bframeBox.width - CAPTCHA_GRID.GRID_PADDING) / ci.cols) : CAPTCHA_GRID.DEFAULT_TILE_SIZE;
3669
3447
  const tiles = [];
@@ -3715,7 +3493,15 @@ async function recaptchaAnswer(page, step) {
3715
3493
  return { solved: false, tiles: [] };
3716
3494
  }
3717
3495
  async function solveCaptcha(page, _step, _ctx, monitor) {
3496
+ const startTime = Date.now();
3718
3497
  await page.waitForTimeout(TIMING.CAPTCHA_DETECT_WAIT);
3498
+ const widget = await page.waitForSelector(
3499
+ 'iframe[src*="recaptcha/api2/anchor"], iframe[src*="recaptcha/enterprise/anchor"], iframe[title*="reCAPTCHA"], iframe[src*="hcaptcha.com"], iframe[title*="hCaptcha"]',
3500
+ { timeout: 5e3 }
3501
+ ).catch(() => null);
3502
+ if (!widget) {
3503
+ return { solved: false, noCaptcha: true, rounds: 0, durationMs: Date.now() - startTime, reason: "No captcha on this page" };
3504
+ }
3719
3505
  const isHCaptcha = await page.evaluate(() => {
3720
3506
  const iframes = Array.from(document.querySelectorAll("iframe"));
3721
3507
  return iframes.some((f) => {
@@ -3724,11 +3510,15 @@ async function solveCaptcha(page, _step, _ctx, monitor) {
3724
3510
  return src.includes("hcaptcha.com") || title.includes("hcaptcha") || !!document.querySelector("[data-hcaptcha-widget-id]");
3725
3511
  });
3726
3512
  }).catch((err) => {
3727
- log12.warn(`captcha detection failed: ${err}`);
3513
+ log11.warn(`captcha detection failed: ${err}`);
3728
3514
  return false;
3729
3515
  });
3730
- log12.info(`detected: ${isHCaptcha ? "hCaptcha" : "reCAPTCHA"}`);
3731
- return isHCaptcha ? await solveHCaptcha(page, monitor) : await solveRecaptcha(page, monitor);
3516
+ log11.info(`detected: ${isHCaptcha ? "hCaptcha" : "reCAPTCHA"}`);
3517
+ const result = isHCaptcha ? await solveHCaptcha(page, monitor) : await solveRecaptcha(page, monitor);
3518
+ if (!result.solved) {
3519
+ throw new Error(`Captcha not solved after ${result.rounds} round(s): ${result.reason ?? "unknown reason"}`);
3520
+ }
3521
+ return result;
3732
3522
  }
3733
3523
 
3734
3524
  // src/lib/browser/form-fill.ts
@@ -3786,10 +3576,10 @@ async function findSubmitButton(page, opts) {
3786
3576
  }
3787
3577
 
3788
3578
  // src/lib/knowledge.ts
3789
- var import_fs9 = __toESM(require("fs"));
3579
+ var import_fs7 = __toESM(require("fs"));
3790
3580
  var import_path8 = __toESM(require("path"));
3791
3581
  init_paths();
3792
- var log13 = createLogger("knowledge");
3582
+ var log12 = createLogger("knowledge");
3793
3583
  function getKnowledgeDir() {
3794
3584
  return import_path8.default.join(getDataDir(), "knowledge");
3795
3585
  }
@@ -3828,12 +3618,12 @@ function sanitizeDomain(input) {
3828
3618
  return normalized.replace(/[^a-z0-9.-]/g, "_");
3829
3619
  }
3830
3620
  function ensureDir() {
3831
- import_fs9.default.mkdirSync(getKnowledgeDir(), { recursive: true });
3621
+ import_fs7.default.mkdirSync(getKnowledgeDir(), { recursive: true });
3832
3622
  }
3833
3623
  function readKnowledge(domain) {
3834
3624
  const p = getKnowledgePath(domain);
3835
3625
  try {
3836
- return import_fs9.default.readFileSync(p, "utf8");
3626
+ return import_fs7.default.readFileSync(p, "utf8");
3837
3627
  } catch {
3838
3628
  return null;
3839
3629
  }
@@ -3856,8 +3646,8 @@ function mergeKnowledge(domain, updates) {
3856
3646
  notes: dedupeNotes([...existing?.notes ?? [], ...updates.notes ?? []])
3857
3647
  };
3858
3648
  const md = renderMarkdown(merged);
3859
- import_fs9.default.writeFileSync(getKnowledgePath(domain), md, "utf8");
3860
- log13.info(`knowledge updated: ${merged.domain} (${merged.endpoints.length} endpoints)`);
3649
+ import_fs7.default.writeFileSync(getKnowledgePath(domain), md, "utf8");
3650
+ log12.info(`knowledge updated: ${merged.domain} (${merged.endpoints.length} endpoints)`);
3861
3651
  }
3862
3652
  function dedupeEndpoints(endpoints) {
3863
3653
  const seen = /* @__PURE__ */ new Map();
@@ -4075,7 +3865,7 @@ var EMAIL_FORM_ANCHOR = 'input[type="email"], input[name*="email" i], input[type
4075
3865
  var PASSWORD_FORM_ANCHOR = PASSWORD_SELECTOR;
4076
3866
 
4077
3867
  // src/lib/actions/handlers/login/index.ts
4078
- var log14 = createLogger("actions");
3868
+ var log13 = createLogger("actions");
4079
3869
  async function login(page, step, ctx) {
4080
3870
  const resolved = await resolveCredential(ctx.store, ctx.userId, ctx.token, step.domain);
4081
3871
  if (!resolved) {
@@ -4087,7 +3877,7 @@ async function login(page, step, ctx) {
4087
3877
  }
4088
3878
  const { credential, matchedDomain } = resolved;
4089
3879
  if (matchedDomain !== normalizeDomain(step.domain)) {
4090
- log14.info(`login: credentials for ${step.domain} resolved via parent domain ${matchedDomain}`);
3880
+ log13.info(`login: credentials for ${step.domain} resolved via parent domain ${matchedDomain}`);
4091
3881
  }
4092
3882
  const beforeUrl = page.url();
4093
3883
  const hasExplicitSelectors = !!(step.usernameSelector || step.passwordSelector || step.submitSelector);
@@ -4120,7 +3910,7 @@ async function runExplicitFlow(page, step, ctx, credential) {
4120
3910
  }
4121
3911
  }
4122
3912
  async function runAutoDetect(page, step, ctx, credential, beforeUrl) {
4123
- log14.info(`login: auto-detecting form on ${beforeUrl}`);
3913
+ log13.info(`login: auto-detecting form on ${beforeUrl}`);
4124
3914
  await page.waitForLoadState("domcontentloaded").catch(() => {
4125
3915
  });
4126
3916
  await page.waitForTimeout(500);
@@ -4130,7 +3920,7 @@ async function runAutoDetect(page, step, ctx, credential, beforeUrl) {
4130
3920
  return { url: location.href, title: document.title, pwdVisible };
4131
3921
  }, PASSWORD_SELECTOR).catch(() => ({ url: page.url(), title: "", pwdVisible: false }));
4132
3922
  if (!initialCheck.pwdVisible && !LOGIN_URL_RE.test(initialCheck.url)) {
4133
- log14.info(`login: already logged in (no password field, URL=${initialCheck.url})`);
3923
+ log13.info(`login: already logged in (no password field, URL=${initialCheck.url})`);
4134
3924
  return {
4135
3925
  loggedIn: true,
4136
3926
  alreadyLoggedIn: true,
@@ -4150,7 +3940,7 @@ async function runAutoDetect(page, step, ctx, credential, beforeUrl) {
4150
3940
  async function runNoPasswordBranch(page, step, ctx, credential, beforeUrl) {
4151
3941
  const currentUrl = page.url();
4152
3942
  if (!LOGIN_URL_RE.test(currentUrl)) {
4153
- log14.info(`login: no password field and URL left login area (${currentUrl}) \u2014 treating as success`);
3943
+ log13.info(`login: no password field and URL left login area (${currentUrl}) \u2014 treating as success`);
4154
3944
  return {
4155
3945
  loggedIn: true,
4156
3946
  alreadyLoggedIn: true,
@@ -4177,7 +3967,7 @@ async function runNoPasswordBranch(page, step, ctx, credential, beforeUrl) {
4177
3967
  const hasCloudflare = !!document.querySelector('[class*="cf-" i], iframe[src*="challenges.cloudflare"]');
4178
3968
  return { title: document.title, visibleText, inputCount, hiddenPassword, hasCaptcha, hasCloudflare };
4179
3969
  }).catch(() => ({ title: "", visibleText: "", inputCount: 0, hiddenPassword: false, hasCaptcha: false, hasCloudflare: false }));
4180
- log14.warn(`login: no visible password or email field on ${currentUrl} \u2014 title="${pageDiag.title}", inputs=${pageDiag.inputCount}`);
3970
+ log13.warn(`login: no visible password or email field on ${currentUrl} \u2014 title="${pageDiag.title}", inputs=${pageDiag.inputCount}`);
4181
3971
  const indicators = [];
4182
3972
  if (pageDiag.hasCaptcha) indicators.push("CAPTCHA detected");
4183
3973
  if (pageDiag.hasCloudflare) indicators.push("Cloudflare challenge");
@@ -4185,12 +3975,12 @@ async function runNoPasswordBranch(page, step, ctx, credential, beforeUrl) {
4185
3975
  if (pageDiag.hiddenPassword) indicators.push("password field exists but is hidden/disabled");
4186
3976
  const indicatorStr = indicators.length > 0 ? ` (${indicators.join(", ")})` : "";
4187
3977
  throw new Error(
4188
- `login: no visible password or email field on ${currentUrl} after 5000ms${indicatorStr}. Page title: "${pageDiag.title}". The site may be showing a bot-detection wall, captcha, or an unsupported login flow. Retry with a stronger browser mode (binary-headful or docker-headful).`
3978
+ `login: no visible password or email field on ${currentUrl} after 5000ms${indicatorStr}. Page title: "${pageDiag.title}". The site may be showing a bot-detection wall, captcha, or an unsupported login flow. Retry with a stronger browser mode (binary-headful).`
4189
3979
  );
4190
3980
  }
4191
3981
  async function runEmailFirstFlow(page, credential, beforeUrl, emailEl) {
4192
3982
  const currentUrl = page.url();
4193
- log14.info(`login: no password field but found email input \u2014 running email-first flow on ${currentUrl}`);
3983
+ log13.info(`login: no password field but found email input \u2014 running email-first flow on ${currentUrl}`);
4194
3984
  await fillHandleNative(page, emailEl, credential.username);
4195
3985
  const submitEl = await findSubmitButton(page, {
4196
3986
  formAnchor: EMAIL_FORM_ANCHOR,
@@ -4204,7 +3994,7 @@ async function runEmailFirstFlow(page, credential, beforeUrl, emailEl) {
4204
3994
  await submitEl.evaluate((el) => el.click());
4205
3995
  });
4206
3996
  } else {
4207
- log14.warn("login: email-first flow, no submit button found \u2014 pressing Enter");
3997
+ log13.warn("login: email-first flow, no submit button found \u2014 pressing Enter");
4208
3998
  await emailEl.press("Enter").catch(() => {
4209
3999
  });
4210
4000
  }
@@ -4218,7 +4008,7 @@ async function runEmailFirstFlow(page, credential, beforeUrl, emailEl) {
4218
4008
  ]);
4219
4009
  const laterPasswordHandle = await page.$(PASSWORD_SELECTOR);
4220
4010
  if (laterPasswordHandle && credential.password) {
4221
- log14.info("login: password field appeared after email submit \u2014 filling it");
4011
+ log13.info("login: password field appeared after email submit \u2014 filling it");
4222
4012
  await fillHandleNative(page, laterPasswordHandle, credential.password);
4223
4013
  const laterSubmit = await page.$('button[type="submit"]:not([disabled])');
4224
4014
  if (laterSubmit) {
@@ -4257,7 +4047,7 @@ async function runPasswordFlow(page, credential, beforeUrl, passwordHandle) {
4257
4047
  if (usernameEl && credential.username) {
4258
4048
  await fillHandleNative(page, usernameEl, credential.username);
4259
4049
  } else if (!usernameEl) {
4260
- log14.warn("login: no username field detected, proceeding with password only");
4050
+ log13.warn("login: no username field detected, proceeding with password only");
4261
4051
  }
4262
4052
  if (credential.password) {
4263
4053
  await fillHandleNative(page, passwordHandle, credential.password);
@@ -4274,7 +4064,7 @@ async function runPasswordFlow(page, credential, beforeUrl, passwordHandle) {
4274
4064
  await submitEl.evaluate((el) => el.click());
4275
4065
  });
4276
4066
  } else {
4277
- log14.warn("login: no submit button detected, pressing Enter in password field");
4067
+ log13.warn("login: no submit button detected, pressing Enter in password field");
4278
4068
  await passwordHandle.press("Enter").catch(() => {
4279
4069
  });
4280
4070
  }
@@ -4292,13 +4082,13 @@ async function handleOtp(page, step, ctx, credential, beforeUrl) {
4292
4082
  let code = null;
4293
4083
  if (credential.totp_secret) {
4294
4084
  code = generateTOTP(credential.totp_secret);
4295
- log14.info(`login: generated TOTP from stored secret for ${step.domain}`);
4085
+ log13.info(`login: generated TOTP from stored secret for ${step.domain}`);
4296
4086
  } else if (ctx.elicitOtp) {
4297
- log14.info(`login: prompting user for OTP for ${step.domain}`);
4087
+ log13.info(`login: prompting user for OTP for ${step.domain}`);
4298
4088
  try {
4299
4089
  code = await ctx.elicitOtp(step.domain);
4300
4090
  } catch (err) {
4301
- log14.warn(`login: OTP elicitation failed: ${err instanceof Error ? err.message : String(err)}`);
4091
+ log13.warn(`login: OTP elicitation failed: ${err instanceof Error ? err.message : String(err)}`);
4302
4092
  }
4303
4093
  if (!code) {
4304
4094
  throw new Error(`login: OTP required but user did not provide one for ${step.domain}`);
@@ -4524,7 +4314,7 @@ var RecaptchaDetector = class {
4524
4314
  () => [
4525
4315
  'iframe[src*="recaptcha/api2/anchor"], iframe[title*="reCAPTCHA"]',
4526
4316
  'iframe[src*="recaptcha/api2/bframe"]',
4527
- '.g-recaptcha:not([data-size="invisible"]), [data-sitekey]:not([data-size="invisible"])'
4317
+ '.g-recaptcha:not([data-size="invisible"]), [data-sitekey]:not([data-size="invisible"]):not(.h-captcha):not(.cf-turnstile)'
4528
4318
  ].some((sel) => {
4529
4319
  const el = document.querySelector(sel);
4530
4320
  if (!el) return false;
@@ -4547,7 +4337,8 @@ var HCaptchaDetector = class {
4547
4337
  () => [
4548
4338
  'iframe[src*="hcaptcha.com"]',
4549
4339
  'iframe[title*="hCaptcha"]',
4550
- "[data-hcaptcha-widget-id]"
4340
+ "[data-hcaptcha-widget-id]",
4341
+ '.h-captcha:not([data-size="invisible"])'
4551
4342
  ].some((sel) => {
4552
4343
  const el = document.querySelector(sel);
4553
4344
  if (!el) return false;
@@ -4611,8 +4402,8 @@ var LoginWallDetector = class {
4611
4402
  }
4612
4403
  };
4613
4404
  var defaultDetectors = [
4614
- new RecaptchaDetector(),
4615
4405
  new HCaptchaDetector(),
4406
+ new RecaptchaDetector(),
4616
4407
  new CookieConsentDetector(),
4617
4408
  new LoginWallDetector()
4618
4409
  ];
@@ -4798,8 +4589,8 @@ function isLikelyId(segment) {
4798
4589
  function isRecord(x) {
4799
4590
  return typeof x === "object" && x !== null && !Array.isArray(x);
4800
4591
  }
4801
- function parameterizePath(path13) {
4802
- const parts = path13.split("/");
4592
+ function parameterizePath(path12) {
4593
+ const parts = path12.split("/");
4803
4594
  let idCount = 0;
4804
4595
  const parameterized = parts.map((part) => {
4805
4596
  if (part && isLikelyId(part)) {
@@ -4888,16 +4679,16 @@ function gqlActionFromBody(body) {
4888
4679
  return void 0;
4889
4680
  }
4890
4681
  function classifyRequest(req) {
4891
- const path13 = req.path;
4892
- const lowerPath = path13.toLowerCase();
4682
+ const path12 = req.path;
4683
+ const lowerPath = path12.toLowerCase();
4893
4684
  const ct = (req.requestHeaders["content-type"] || req.requestHeaders["Content-Type"] || "").toLowerCase();
4894
4685
  const body = req.requestBody;
4895
4686
  if (ct.includes("application/grpc")) {
4896
- return { protocol: "grpc-web", action: path13.replace(/^\//, "") };
4687
+ return { protocol: "grpc-web", action: path12.replace(/^\//, "") };
4897
4688
  }
4898
4689
  const soapAction = req.requestHeaders["soapaction"] || req.requestHeaders["SOAPAction"];
4899
4690
  if (soapAction || ct.includes("text/xml") || ct.includes("application/soap+xml")) {
4900
- return { protocol: "soap", action: (soapAction || path13).replace(/^["/]|["/]$/g, "") };
4691
+ return { protocol: "soap", action: (soapAction || path12).replace(/^["/]|["/]$/g, "") };
4901
4692
  }
4902
4693
  if (/\/graphql\b/.test(lowerPath) || hasGraphQLShape(body)) {
4903
4694
  return { protocol: "graphql", action: gqlActionFromBody(body) ?? "anonymous" };
@@ -4912,7 +4703,7 @@ function classifyRequest(req) {
4912
4703
  const friendly = gqlActionFromBody(body);
4913
4704
  if (friendly) return { protocol: "form-rpc", action: friendly };
4914
4705
  }
4915
- return { protocol: "rest", action: `${req.method} ${parameterizePath(path13)}` };
4706
+ return { protocol: "rest", action: `${req.method} ${parameterizePath(path12)}` };
4916
4707
  }
4917
4708
  function inferVerb(protocol, action, method, responseBody) {
4918
4709
  const lower = action.toLowerCase();
@@ -4947,8 +4738,8 @@ function buildFunctionName(protocol, action, method, verb) {
4947
4738
  if (protocol === "rest") {
4948
4739
  const parts = action.split(" ");
4949
4740
  const httpMethod = parts[0];
4950
- const path13 = parts.slice(1).join(" ");
4951
- const segs = path13.split("/").filter((s) => s && !s.startsWith("{"));
4741
+ const path12 = parts.slice(1).join(" ");
4742
+ const segs = path12.split("/").filter((s) => s && !s.startsWith("{"));
4952
4743
  const verbPrefix = httpMethod === "GET" ? verb === "list" ? "list" : "get" : httpMethod === "POST" ? "create" : httpMethod === "PUT" ? "update" : httpMethod === "PATCH" ? "patch" : httpMethod === "DELETE" ? "delete" : httpMethod.toLowerCase();
4953
4744
  return camelCase(`${verbPrefix} ${segs.join(" ")}`) || camelCase(action);
4954
4745
  }
@@ -5220,7 +5011,7 @@ function buildCapturedApi(requests) {
5220
5011
  }
5221
5012
 
5222
5013
  // src/lib/browser/tab-tracker.ts
5223
- var log15 = createLogger("tabs");
5014
+ var log14 = createLogger("tabs");
5224
5015
  var TabTracker = class {
5225
5016
  constructor(context, initial) {
5226
5017
  this.context = context;
@@ -5239,14 +5030,14 @@ var TabTracker = class {
5239
5030
  this.pages.push(p);
5240
5031
  this.newlyOpened.push(p);
5241
5032
  p.on("close", () => this.onClose(p));
5242
- log15.debug(`new tab opened: ${safeUrl(p)}`);
5033
+ log14.debug(`new tab opened: ${safeUrl(p)}`);
5243
5034
  };
5244
5035
  onClose = (p) => {
5245
5036
  this.pages = this.pages.filter((x) => x !== p);
5246
5037
  this.newlyOpened = this.newlyOpened.filter((x) => x !== p);
5247
5038
  if (this.activePage === p) {
5248
5039
  this.activePage = this.pages[this.pages.length - 1] ?? p;
5249
- log15.debug(`active tab closed, fell back to: ${safeUrl(this.activePage)}`);
5040
+ log14.debug(`active tab closed, fell back to: ${safeUrl(this.activePage)}`);
5250
5041
  }
5251
5042
  };
5252
5043
  /** The page the pipeline should currently drive. */
@@ -5289,7 +5080,7 @@ var TabTracker = class {
5289
5080
  });
5290
5081
  this.activePage = target;
5291
5082
  const sw = { url, title: await target.title().catch(() => "") };
5292
- log15.info(`followed new tab \u2192 ${sw.url}`);
5083
+ log14.info(`followed new tab \u2192 ${sw.url}`);
5293
5084
  return sw;
5294
5085
  }
5295
5086
  /** Drop pending new-tab opens without following them. Used when a step opened
@@ -5315,7 +5106,7 @@ function safeUrl(p) {
5315
5106
  }
5316
5107
 
5317
5108
  // src/lib/knowledge/component-map.ts
5318
- var import_fs10 = __toESM(require("fs"));
5109
+ var import_fs8 = __toESM(require("fs"));
5319
5110
  var import_path9 = __toESM(require("path"));
5320
5111
  function anchorsPath(domain) {
5321
5112
  return import_path9.default.join(getKnowledgeDir(), `${sanitizeDomain(domain)}.anchors.json`);
@@ -5323,7 +5114,7 @@ function anchorsPath(domain) {
5323
5114
  function loadComponentMap(domain) {
5324
5115
  const norm = normalizeDomain(domain);
5325
5116
  try {
5326
- const raw = import_fs10.default.readFileSync(anchorsPath(norm), "utf8");
5117
+ const raw = import_fs8.default.readFileSync(anchorsPath(norm), "utf8");
5327
5118
  const parsed = JSON.parse(raw);
5328
5119
  return {
5329
5120
  domain: parsed.domain || norm,
@@ -5341,8 +5132,8 @@ function loadAnchors(domain) {
5341
5132
  return map;
5342
5133
  }
5343
5134
  function write(cm) {
5344
- import_fs10.default.mkdirSync(getKnowledgeDir(), { recursive: true });
5345
- import_fs10.default.writeFileSync(anchorsPath(cm.domain), JSON.stringify(cm, null, 2), "utf8");
5135
+ import_fs8.default.mkdirSync(getKnowledgeDir(), { recursive: true });
5136
+ import_fs8.default.writeFileSync(anchorsPath(cm.domain), JSON.stringify(cm, null, 2), "utf8");
5346
5137
  }
5347
5138
  function recordAnchorResult(domain, name, ok, now) {
5348
5139
  try {
@@ -5371,6 +5162,7 @@ function safePageUrl(page) {
5371
5162
  }
5372
5163
  function classifyError(err, step) {
5373
5164
  if (err instanceof StaleStateError) return "stale-state";
5165
+ if (step.type === "solve-captcha") return "captcha-unsolvable";
5374
5166
  const msg = err.message.toLowerCase();
5375
5167
  if (step.type === "login" && (msg.includes("no visible password field") || msg.includes("password field was not visible"))) {
5376
5168
  return "bot-blocked";
@@ -5378,7 +5170,6 @@ function classifyError(err, step) {
5378
5170
  if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
5379
5171
  if (msg.includes("not found") || msg.includes("no element") || msg.includes("waiting for selector")) return "element-not-found";
5380
5172
  if (msg.includes("navigation") || msg.includes("net::err")) return "navigation-failed";
5381
- if (step.type === "solve-captcha") return "captcha-unsolvable";
5382
5173
  return "action-failed";
5383
5174
  }
5384
5175
  async function refineError(page, step, ctx, errorType, message) {
@@ -5423,7 +5214,7 @@ function getSuggestion(errorType, step, message = "") {
5423
5214
  case "navigation-failed":
5424
5215
  return "Navigation failed. The URL may be unreachable, blocked, or require authentication.";
5425
5216
  case "captcha-unsolvable":
5426
- return "Automatic reCAPTCHA solving failed. The challenge may require human intervention.";
5217
+ return "Automatic captcha solving failed. Ask a person to solve it in a binary-headful window, then continue from the same page.";
5427
5218
  case "timeout":
5428
5219
  return "Operation timed out. The page may be slow or the element may not appear.";
5429
5220
  default:
@@ -5642,11 +5433,8 @@ var PipelineRunner = class {
5642
5433
  }
5643
5434
  };
5644
5435
 
5645
- // src/lib/execution/pipeline-executor.ts
5646
- init_persistence();
5647
-
5648
5436
  // src/lib/block-detection.ts
5649
- var log16 = createLogger("block-detection");
5437
+ var log15 = createLogger("block-detection");
5650
5438
  async function detectBlock(page) {
5651
5439
  try {
5652
5440
  const [title, url, bodyText] = await Promise.all([
@@ -5666,7 +5454,7 @@ async function detectBlock(page) {
5666
5454
  const hasCfChallenge = await page.evaluate(() => {
5667
5455
  return !!document.querySelector('iframe[src*="challenges.cloudflare.com"]');
5668
5456
  }).catch((err) => {
5669
- log16.warn(`CF challenge check failed, assuming blocked: ${err}`);
5457
+ log15.warn(`CF challenge check failed, assuming blocked: ${err}`);
5670
5458
  return true;
5671
5459
  });
5672
5460
  if (hasCfChallenge) {
@@ -5685,7 +5473,7 @@ async function detectBlock(page) {
5685
5473
  const hasCaptchaIframe = await page.evaluate(() => {
5686
5474
  return !!(document.querySelector('iframe[src*="recaptcha"]') || document.querySelector('iframe[src*="hcaptcha"]'));
5687
5475
  }).catch((err) => {
5688
- log16.warn(`captcha iframe check failed, assuming blocked: ${err}`);
5476
+ log15.warn(`captcha iframe check failed, assuming blocked: ${err}`);
5689
5477
  return true;
5690
5478
  });
5691
5479
  if (hasCaptchaIframe) {
@@ -5699,7 +5487,7 @@ async function detectBlock(page) {
5699
5487
  }
5700
5488
  return { blocked: false };
5701
5489
  } catch (err) {
5702
- log16.warn(`page evaluation failed, assuming blocked: ${err}`);
5490
+ log15.warn(`page evaluation failed, assuming blocked: ${err}`);
5703
5491
  return { blocked: true, reason: "evaluation-failed" };
5704
5492
  }
5705
5493
  }
@@ -5787,14 +5575,14 @@ function extractKnowledgeFromRun(pipeline, result, sessionData, mode) {
5787
5575
  // src/lib/extension/cdp-relay.ts
5788
5576
  var import_http = __toESM(require("http"));
5789
5577
  var import_ws2 = require("ws");
5790
- var import_crypto7 = require("crypto");
5578
+ var import_crypto8 = require("crypto");
5791
5579
 
5792
5580
  // src/lib/extension/bridge.ts
5793
5581
  var import_ws = require("ws");
5794
- var import_crypto5 = __toESM(require("crypto"));
5582
+ var import_crypto6 = __toESM(require("crypto"));
5795
5583
 
5796
5584
  // src/lib/version.ts
5797
- var import_fs11 = __toESM(require("fs"));
5585
+ var import_fs9 = __toESM(require("fs"));
5798
5586
  var import_path10 = __toESM(require("path"));
5799
5587
  var cached = null;
5800
5588
  function getVersion() {
@@ -5809,7 +5597,7 @@ function getVersion() {
5809
5597
  ];
5810
5598
  for (const p of candidates) {
5811
5599
  try {
5812
- const v = JSON.parse(import_fs11.default.readFileSync(p, "utf8")).version;
5600
+ const v = JSON.parse(import_fs9.default.readFileSync(p, "utf8")).version;
5813
5601
  if (v) return cached = v;
5814
5602
  } catch {
5815
5603
  }
@@ -5820,8 +5608,12 @@ function getVersion() {
5820
5608
  // src/lib/extension/bridge.ts
5821
5609
  var REQUEST_TIMEOUT_MS = 18e4;
5822
5610
  var HEARTBEAT_MS = 15e3;
5611
+ var CONNECT_GRACE_MS = 35e3;
5612
+ var NOT_CONNECTED_MESSAGE = "No iframer extension is connected. Open Chrome and make sure the iframer extension is enabled (setup: `iframer install extension chrome`). It pairs by itself; the popup says why if it can't.";
5823
5613
  var ExtensionBridge = class {
5824
5614
  wss = null;
5615
+ attachedAt = 0;
5616
+ clientWaiters = [];
5825
5617
  clients = /* @__PURE__ */ new Map();
5826
5618
  pending = /* @__PURE__ */ new Map();
5827
5619
  nextReqId = 1;
@@ -5839,21 +5631,17 @@ var ExtensionBridge = class {
5839
5631
  /** Attach the WS server to the already-listening HTTP server. Idempotent. */
5840
5632
  attach(server2) {
5841
5633
  if (this.wss) return;
5634
+ this.attachedAt = Date.now();
5842
5635
  this.wss = new import_ws.WebSocketServer({ server: server2, path: "/extension/ws" });
5843
5636
  this.wss.on("connection", (ws, req) => {
5844
- let expected = "";
5845
- try {
5846
- expected = getLocalToken();
5847
- } catch {
5848
- expected = "";
5849
- }
5850
- if (!expected) {
5637
+ const accepted = getPairingTokens();
5638
+ if (accepted.length === 0) {
5851
5639
  ws.close(4001, "unauthorized");
5852
5640
  return;
5853
5641
  }
5854
5642
  const queryToken = new URL(req.url || "", "http://127.0.0.1").searchParams.get("token");
5855
5643
  if (queryToken !== null) {
5856
- if (queryToken !== expected) {
5644
+ if (!accepted.includes(queryToken)) {
5857
5645
  ws.close(4001, "unauthorized");
5858
5646
  return;
5859
5647
  }
@@ -5865,7 +5653,7 @@ var ExtensionBridge = class {
5865
5653
  clearTimeout(timer);
5866
5654
  try {
5867
5655
  const m = JSON.parse(data.toString());
5868
- if (m?.type === "auth" && m.token === expected) {
5656
+ if (m?.type === "auth" && typeof m.token === "string" && accepted.includes(m.token)) {
5869
5657
  this.acceptClient(ws);
5870
5658
  return;
5871
5659
  }
@@ -5877,7 +5665,7 @@ var ExtensionBridge = class {
5877
5665
  }
5878
5666
  acceptClient(ws) {
5879
5667
  const client = {
5880
- clientId: import_crypto5.default.randomUUID(),
5668
+ clientId: import_crypto6.default.randomUUID(),
5881
5669
  socket: ws,
5882
5670
  connectedAt: Date.now(),
5883
5671
  tabs: [],
@@ -5885,6 +5673,7 @@ var ExtensionBridge = class {
5885
5673
  };
5886
5674
  this.clients.set(client.clientId, client);
5887
5675
  this.startHeartbeat(client);
5676
+ for (const wake of this.clientWaiters.splice(0)) wake();
5888
5677
  try {
5889
5678
  ws.send(JSON.stringify({ type: "server_info", version: getVersion() }));
5890
5679
  } catch {
@@ -5980,6 +5769,22 @@ var ExtensionBridge = class {
5980
5769
  hasClients() {
5981
5770
  return this.clients.size > 0;
5982
5771
  }
5772
+ /** Resolves once a client is connected, or when the startup grace window ends.
5773
+ * After that window it returns at once, so a machine with no extension still
5774
+ * gets its "not connected" answer immediately. */
5775
+ async waitForClient() {
5776
+ if (this.clients.size > 0) return;
5777
+ const left = this.attachedAt + CONNECT_GRACE_MS - Date.now();
5778
+ if (left <= 0) return;
5779
+ await new Promise((resolve) => {
5780
+ const timer = setTimeout(resolve, left);
5781
+ timer.unref?.();
5782
+ this.clientWaiters.push(() => {
5783
+ clearTimeout(timer);
5784
+ resolve();
5785
+ });
5786
+ });
5787
+ }
5983
5788
  status() {
5984
5789
  return {
5985
5790
  connected: this.clients.size > 0,
@@ -5996,6 +5801,7 @@ var ExtensionBridge = class {
5996
5801
  /** List tabs across ALL connected clients, tagged with profile/client, and
5997
5802
  * refresh the tabId → client ownership map used for routing. */
5998
5803
  async listTabs() {
5804
+ await this.waitForClient();
5999
5805
  const all = [];
6000
5806
  this.tabOwner.clear();
6001
5807
  this.collidingTabs.clear();
@@ -6031,10 +5837,9 @@ var ExtensionBridge = class {
6031
5837
  if (!c) throw new Error(`No connected extension with clientId ${clientId}.`);
6032
5838
  return c;
6033
5839
  }
5840
+ await this.waitForClient();
6034
5841
  if (this.clients.size === 0) {
6035
- throw new Error(
6036
- "No iframer extension is connected. Open Chrome, install/enable the iframer extension, and pair it (paste the token, dot goes green)."
6037
- );
5842
+ throw new Error(NOT_CONNECTED_MESSAGE);
6038
5843
  }
6039
5844
  if (this.clients.size === 1) {
6040
5845
  return [...this.clients.values()][0];
@@ -6059,16 +5864,15 @@ var ExtensionBridge = class {
6059
5864
  }
6060
5865
  /** Pick a client for a browser-level op that has no tabId (create tab, etc.).
6061
5866
  * Explicit clientId wins; otherwise require exactly one connected profile. */
6062
- resolveClientNoTab(clientId) {
5867
+ async resolveClientNoTab(clientId) {
6063
5868
  if (clientId) {
6064
5869
  const c = this.clients.get(clientId);
6065
5870
  if (!c) throw new Error(`No connected extension with clientId ${clientId}.`);
6066
5871
  return c;
6067
5872
  }
5873
+ await this.waitForClient();
6068
5874
  if (this.clients.size === 0) {
6069
- throw new Error(
6070
- "No iframer extension is connected. Open Chrome, install/enable the iframer extension, and pair it (paste the token, dot goes green)."
6071
- );
5875
+ throw new Error(NOT_CONNECTED_MESSAGE);
6072
5876
  }
6073
5877
  if (this.clients.size === 1) return [...this.clients.values()][0];
6074
5878
  throw new Error(
@@ -6086,7 +5890,7 @@ var ExtensionBridge = class {
6086
5890
  }
6087
5891
  /** Group tabs into a native Chrome tab group. */
6088
5892
  async groupTabs(tabIds, opts = {}, clientId) {
6089
- const client = clientId ? this.resolveClientNoTab(clientId) : await this.resolveClient(tabIds[0]);
5893
+ const client = clientId ? await this.resolveClientNoTab(clientId) : await this.resolveClient(tabIds[0]);
6090
5894
  return this.send(client, "group_tabs", {
6091
5895
  tabIds,
6092
5896
  title: opts.title,
@@ -6097,22 +5901,22 @@ var ExtensionBridge = class {
6097
5901
  }
6098
5902
  /** Remove tabs from their group (chrome.tabs.ungroup). */
6099
5903
  async ungroupTabs(tabIds, clientId) {
6100
- const client = clientId ? this.resolveClientNoTab(clientId) : await this.resolveClient(tabIds[0]);
5904
+ const client = clientId ? await this.resolveClientNoTab(clientId) : await this.resolveClient(tabIds[0]);
6101
5905
  return this.send(client, "ungroup_tabs", { tabIds });
6102
5906
  }
6103
5907
  /** Rename / recolor / collapse an existing group by id. */
6104
5908
  async updateGroup(groupId, opts, clientId) {
6105
- const client = this.resolveClientNoTab(clientId);
5909
+ const client = await this.resolveClientNoTab(clientId);
6106
5910
  return this.send(client, "update_group", { groupId, ...opts });
6107
5911
  }
6108
5912
  /** List all tab groups across the browser. */
6109
5913
  async listGroups(clientId) {
6110
- const client = this.resolveClientNoTab(clientId);
5914
+ const client = await this.resolveClientNoTab(clientId);
6111
5915
  return this.send(client, "list_groups", {});
6112
5916
  }
6113
5917
  /** Open a new tab in the user's real Chrome via chrome.tabs.create. */
6114
5918
  async createTab(url, opts = {}, clientId) {
6115
- const client = this.resolveClientNoTab(clientId);
5919
+ const client = await this.resolveClientNoTab(clientId);
6116
5920
  const res = await this.send(client, "create_tab", {
6117
5921
  url,
6118
5922
  active: opts.active,
@@ -6160,7 +5964,7 @@ function cdpKey(clientId, tabId) {
6160
5964
  var extensionBridge = new ExtensionBridge();
6161
5965
 
6162
5966
  // src/lib/extension/cdp-relay.ts
6163
- var log17 = createLogger("cdp-relay");
5967
+ var log16 = createLogger("cdp-relay");
6164
5968
  var CdpRelay = class {
6165
5969
  constructor(tabId, clientId, focus) {
6166
5970
  this.tabId = tabId;
@@ -6174,7 +5978,7 @@ var CdpRelay = class {
6174
5978
  wss = null;
6175
5979
  pw = null;
6176
5980
  port = 0;
6177
- path = `/cdp/${(0, import_crypto7.randomUUID)()}`;
5981
+ path = `/cdp/${(0, import_crypto8.randomUUID)()}`;
6178
5982
  tabSessionId = "pw-tab-1";
6179
5983
  targetInfo = null;
6180
5984
  ownerClientId = "";
@@ -6220,11 +6024,11 @@ var CdpRelay = class {
6220
6024
  res.end();
6221
6025
  });
6222
6026
  this.httpServer.on("upgrade", (req) => {
6223
- if (process.env.IFRAMER_RELAY_DEBUG) log17.info(`[relay] upgrade request url=${req.url}`);
6027
+ if (process.env.IFRAMER_RELAY_DEBUG) log16.info(`[relay] upgrade request url=${req.url}`);
6224
6028
  });
6225
6029
  this.wss = new import_ws2.WebSocketServer({ server: this.httpServer, path: this.path });
6226
6030
  this.wss.on("connection", (ws) => {
6227
- if (process.env.IFRAMER_RELAY_DEBUG) log17.info(`[relay] playwright connected`);
6031
+ if (process.env.IFRAMER_RELAY_DEBUG) log16.info(`[relay] playwright connected`);
6228
6032
  if (this.pw) {
6229
6033
  ws.close(4e3, "relay already has a client");
6230
6034
  return;
@@ -6263,7 +6067,7 @@ var CdpRelay = class {
6263
6067
  }
6264
6068
  }
6265
6069
  async onPwMessage(data) {
6266
- if (process.env.IFRAMER_RELAY_DEBUG) log17.info(`[relay] raw pw msg (${data?.length ?? 0} bytes): ${data?.toString().slice(0, 120)}`);
6070
+ if (process.env.IFRAMER_RELAY_DEBUG) log16.info(`[relay] raw pw msg (${data?.length ?? 0} bytes): ${data?.toString().slice(0, 120)}`);
6267
6071
  let msg;
6268
6072
  try {
6269
6073
  msg = JSON.parse(data.toString());
@@ -6271,7 +6075,7 @@ var CdpRelay = class {
6271
6075
  return;
6272
6076
  }
6273
6077
  const { id, sessionId, method, params } = msg;
6274
- if (process.env.IFRAMER_RELAY_DEBUG) log17.info(`[relay] pw\u2192 ${method} (id=${id}, sess=${sessionId || "-"})`);
6078
+ if (process.env.IFRAMER_RELAY_DEBUG) log16.info(`[relay] pw\u2192 ${method} (id=${id}, sess=${sessionId || "-"})`);
6275
6079
  if (!method) return;
6276
6080
  try {
6277
6081
  const result = await this.handleCdpCommand(method, params, sessionId);
@@ -6384,14 +6188,14 @@ var CdpRelay = class {
6384
6188
  try {
6385
6189
  await extensionBridge.cdpDetach(this.ownerClientId, this.tabId);
6386
6190
  } catch (e) {
6387
- log17.warn(`cdp detach failed: ${e}`);
6191
+ log16.warn(`cdp detach failed: ${e}`);
6388
6192
  }
6389
6193
  }
6390
6194
  }
6391
6195
  };
6392
6196
 
6393
6197
  // src/lib/execution/pipeline-executor.ts
6394
- var log18 = createLogger("iframer");
6198
+ var log17 = createLogger("iframer");
6395
6199
  function blankPipelineResult(pipeline, modeUsed, error) {
6396
6200
  return {
6397
6201
  ok: false,
@@ -6484,10 +6288,8 @@ var PipelineExecutor = class {
6484
6288
  if (domain) this.deps.domainModes.recordFailure(domain, failedMode, result.error?.message || "blocked");
6485
6289
  const nextMode = this.deps.domainModes.getNextMode(failedMode, availableModes);
6486
6290
  if (nextMode) {
6487
- log18.info(`Auto-escalating from ${failedMode} to ${nextMode} for ${domain}`);
6488
- if (failedMode !== "docker-headful") {
6489
- await this.deps.daemon.stopMode(failedMode, instanceId);
6490
- }
6291
+ log17.info(`Auto-escalating from ${failedMode} to ${nextMode} for ${domain}`);
6292
+ await this.deps.daemon.stopMode(failedMode, instanceId);
6491
6293
  result = await this.executeWithMode(userId, token, pipeline, nextMode, instanceId);
6492
6294
  result.modeEscalated = true;
6493
6295
  result.modeUsed = nextMode;
@@ -6497,10 +6299,8 @@ var PipelineExecutor = class {
6497
6299
  this.deps.domainModes.recordFailure(domain, nextMode, result.error?.message || "blocked");
6498
6300
  const thirdMode = this.deps.domainModes.getNextMode(nextMode, availableModes);
6499
6301
  if (thirdMode) {
6500
- log18.info(`Auto-escalating from ${nextMode} to ${thirdMode} for ${domain}`);
6501
- if (nextMode !== "docker-headful") {
6502
- await this.deps.daemon.stopMode(nextMode, instanceId);
6503
- }
6302
+ log17.info(`Auto-escalating from ${nextMode} to ${thirdMode} for ${domain}`);
6303
+ await this.deps.daemon.stopMode(nextMode, instanceId);
6504
6304
  result = await this.executeWithMode(userId, token, pipeline, thirdMode, instanceId);
6505
6305
  result.modeEscalated = true;
6506
6306
  result.modeUsed = thirdMode;
@@ -6516,9 +6316,6 @@ var PipelineExecutor = class {
6516
6316
  return result;
6517
6317
  }
6518
6318
  async executeWithMode(userId, token, pipeline, mode, instanceId = DEFAULT_INSTANCE, respectful = false) {
6519
- if (mode === "docker-headful") {
6520
- return this.executeDocker(userId, token, pipeline, respectful);
6521
- }
6522
6319
  return this.executeLocal(userId, token, pipeline, mode, instanceId, respectful);
6523
6320
  }
6524
6321
  /** Execute against the user's real Chrome tab via the extension CDP relay,
@@ -6576,7 +6373,7 @@ var PipelineExecutor = class {
6576
6373
  try {
6577
6374
  extractKnowledgeFromRun(pipeline, result, null, "extension");
6578
6375
  } catch (e) {
6579
- log18.warn(`knowledge update failed: ${getErrorMessage(e)}`);
6376
+ log17.warn(`knowledge update failed: ${getErrorMessage(e)}`);
6580
6377
  }
6581
6378
  }
6582
6379
  return result;
@@ -6616,42 +6413,6 @@ var PipelineExecutor = class {
6616
6413
  }
6617
6414
  }
6618
6415
  }
6619
- /** Execute via Docker session-manager. */
6620
- async executeDocker(userId, token, pipeline, respectful = false) {
6621
- let session = getSession(userId);
6622
- if (!session) {
6623
- const firstNav = pipeline.steps.find((s) => s.type === "navigate");
6624
- await this.deps.startSession(userId, token, firstNav ? { url: firstNav.url } : {});
6625
- session = getSession(userId);
6626
- }
6627
- resetTimeout(userId);
6628
- const ctx = this.deps.refStore.makeContext(userId, token);
6629
- const runner = new PipelineRunner(ctx, protectionHooks(false, respectful));
6630
- const result = await runner.run(session.page, pipeline);
6631
- if (result.ok) {
6632
- const blockResult = await detectBlock(session.page);
6633
- if (blockResult.blocked) {
6634
- const pageState = await capturePageState(session.page, ctx, { screenshot: true, namePrefix: "block" });
6635
- return {
6636
- ...result,
6637
- ok: false,
6638
- modeUsed: "docker-headful",
6639
- error: {
6640
- failedAtStep: result.completedSteps - 1,
6641
- failedStep: pipeline.steps[result.completedSteps - 1],
6642
- errorType: "bot-blocked",
6643
- message: `Page blocked by bot detection: ${blockResult.reason}`,
6644
- pageState,
6645
- suggestion: "The page is blocked by bot detection. Try a different browser mode.",
6646
- retryable: true
6647
- }
6648
- };
6649
- }
6650
- }
6651
- this.deps.refStore.sync(userId, ctx);
6652
- result.modeUsed = "docker-headful";
6653
- return result;
6654
- }
6655
6416
  /** Execute via local Chrome daemon (headless + binary-headful). */
6656
6417
  async executeLocal(userId, token, pipeline, mode, instanceId = DEFAULT_INSTANCE, respectful = false) {
6657
6418
  const startTime = Date.now();
@@ -6685,7 +6446,7 @@ var PipelineExecutor = class {
6685
6446
  acquired = false;
6686
6447
  await this.deps.daemon.stopMode(mode, instanceId);
6687
6448
  const available = this.deps.availableModes();
6688
- const headed = available.includes("binary-headful") ? "binary-headful" : "docker-headful";
6449
+ const headed = "binary-headful";
6689
6450
  if (!available.includes(headed)) throw new Error("A headed browser is required but unavailable.");
6690
6451
  await new Promise((resolve) => setTimeout(resolve, PROTECTED_INTERVAL_MS));
6691
6452
  const index = err.stepIndex;
@@ -6721,7 +6482,7 @@ var PipelineExecutor = class {
6721
6482
  errorType: "bot-blocked",
6722
6483
  message: `Page blocked by bot detection: ${blockResult.reason}`,
6723
6484
  pageState,
6724
- suggestion: `The page was blocked in ${mode} mode. ${mode === "headless" ? "Try docker-headful mode." : mode === "docker-headful" ? "Try binary-headful mode." : "All modes exhausted."}`,
6485
+ suggestion: `The page was blocked in ${mode} mode. ${mode === "headless" ? "Try binary-headful mode." : "All modes exhausted."}`,
6725
6486
  retryable: true
6726
6487
  }
6727
6488
  };
@@ -6740,7 +6501,7 @@ var PipelineExecutor = class {
6740
6501
  try {
6741
6502
  extractKnowledgeFromRun(pipeline, result, updatedSession, mode);
6742
6503
  } catch (err) {
6743
- log18.warn(`knowledge update failed: ${getErrorMessage(err)}`);
6504
+ log17.warn(`knowledge update failed: ${getErrorMessage(err)}`);
6744
6505
  }
6745
6506
  }
6746
6507
  this.deps.refStore.sync(userId, ctx);
@@ -6773,7 +6534,6 @@ var PipelineExecutor = class {
6773
6534
  };
6774
6535
 
6775
6536
  // src/lib/execution/fetch-service.ts
6776
- init_persistence();
6777
6537
  var FetchService = class {
6778
6538
  constructor(store) {
6779
6539
  this.store = store;
@@ -6942,12 +6702,10 @@ var CaptureManager = class {
6942
6702
 
6943
6703
  // src/lib/auth/credential-store.ts
6944
6704
  var CredentialStore = class {
6945
- constructor(store, config) {
6705
+ constructor(store) {
6946
6706
  this.store = store;
6947
- this.config = config;
6948
6707
  }
6949
6708
  store;
6950
- config;
6951
6709
  async storeCredential(userId, token, credential) {
6952
6710
  const credKey = await deriveKey(token, "credentials");
6953
6711
  const normalizedDomain = normalizeDomain(credential.domain);
@@ -6970,53 +6728,16 @@ var CredentialStore = class {
6970
6728
  async deleteCredential(userId, domain) {
6971
6729
  await this.store.deleteCredential(userId, normalizeDomain(domain));
6972
6730
  }
6973
- async loginWithCredentials(userId, token, domain, selectors) {
6974
- const session = getSession(userId);
6975
- if (!session) return { ok: false, url: "", title: "", error: "No active interactive session. Start one first." };
6976
- const resolved = await resolveCredential(this.store, userId, token, domain);
6977
- if (!resolved) {
6978
- const stored = await this.store.listCredentialDomains(userId);
6979
- return { ok: false, url: "", title: "", error: `No credentials stored for ${normalizeDomain(domain)}. Stored: ${stored.join(", ") || "(none)"}` };
6980
- }
6981
- const { credential } = resolved;
6982
- const page = session.page;
6983
- resetTimeout(userId);
6984
- if (selectors.username && credential.username) {
6985
- await humanType(page, selectors.username, credential.username);
6986
- await page.waitForTimeout(TIMING.PRE_NAVIGATE[0] + Math.random() * (TIMING.PRE_NAVIGATE[1] - TIMING.PRE_NAVIGATE[0]));
6987
- }
6988
- if (selectors.password && credential.password) {
6989
- await humanType(page, selectors.password, credential.password);
6990
- await page.waitForTimeout(TIMING.PRE_NAVIGATE[0] + Math.random() * (TIMING.PRE_NAVIGATE[1] - TIMING.PRE_NAVIGATE[0]));
6991
- }
6992
- if (selectors.submit) {
6993
- await humanClick(page, selectors.submit);
6994
- await page.waitForLoadState("domcontentloaded").catch(() => {
6995
- });
6996
- await page.waitForTimeout(TIMING.POST_LOGIN_WAIT);
6997
- }
6998
- if (selectors.totp && credential.totp_secret) {
6999
- const totp = generateTOTP(credential.totp_secret);
7000
- await page.click(selectors.totp);
7001
- await page.keyboard.type(totp, { delay: 50 });
7002
- await page.waitForTimeout(TIMING.POST_TOTP_WAIT);
7003
- }
7004
- const buf = await page.screenshot({ type: "jpeg", quality: 50, fullPage: false });
7005
- const screenshotUrl = saveScreenshot(buf, `login-${Date.now()}.jpg`, this.config.screenshotDir, this.config.publicUrl);
7006
- return { ok: true, url: page.url(), title: await page.title(), screenshotUrl };
7007
- }
7008
6731
  };
7009
6732
 
7010
6733
  // src/lib/iframer.ts
7011
- var log19 = createLogger("iframer");
7012
- var DEFAULT_SCREENSHOT_DIR = import_path11.default.join(import_path11.default.dirname((0, import_url.fileURLToPath)(importMetaUrl)), "../../.screenshots");
7013
- var DEFAULT_PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3021}`;
6734
+ var log18 = createLogger("iframer");
6735
+ var DEFAULT_PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3022}`;
7014
6736
  var DEFAULT_STALE_TIMEOUT_MS3 = 2e4;
7015
6737
  var Iframer = class {
7016
6738
  store;
7017
6739
  daemon;
7018
6740
  domainModes;
7019
- operatingMode;
7020
6741
  config;
7021
6742
  refStore;
7022
6743
  executor;
@@ -7025,25 +6746,23 @@ var Iframer = class {
7025
6746
  credentials;
7026
6747
  constructor(config = {}) {
7027
6748
  this.config = {
7028
- screenshotDir: config.screenshotDir || DEFAULT_SCREENSHOT_DIR,
6749
+ screenshotDir: config.screenshotDir || getScreenshotDir(),
7029
6750
  publicUrl: config.publicUrl || DEFAULT_PUBLIC_URL,
7030
6751
  staleTimeoutMs: config.staleTimeoutMs ?? DEFAULT_STALE_TIMEOUT_MS3
7031
6752
  };
7032
6753
  this.store = createStore({ dataDir: config.dataDir });
7033
6754
  this.daemon = new BrowserDaemon(config.sessionTimeoutMs);
7034
6755
  this.domainModes = new DomainModeStore();
7035
- this.operatingMode = config.mode || "local";
7036
6756
  this.refStore = new RefStore(this.store, this.config);
7037
6757
  this.fetchService = new FetchService(this.store);
7038
6758
  this.captureManager = new CaptureManager(this.daemon);
7039
- this.credentials = new CredentialStore(this.store, this.config);
6759
+ this.credentials = new CredentialStore(this.store);
7040
6760
  this.executor = new PipelineExecutor({
7041
6761
  daemon: this.daemon,
7042
6762
  store: this.store,
7043
6763
  domainModes: this.domainModes,
7044
6764
  refStore: this.refStore,
7045
- availableModes: () => this.getAvailableModes(),
7046
- startSession: (userId, token, options) => this.startSession(userId, token, options)
6765
+ availableModes: () => this.getAvailableModes()
7047
6766
  });
7048
6767
  }
7049
6768
  // ─── Mode Detection ─────────────────────────────────────────────────
@@ -7051,7 +6770,6 @@ var Iframer = class {
7051
6770
  const modes = ["headless"];
7052
6771
  const { binaryHeadful } = checkModeAvailability();
7053
6772
  if (binaryHeadful) modes.push("binary-headful");
7054
- if (this.operatingMode === "docker") modes.push("docker-headful");
7055
6773
  return modes;
7056
6774
  }
7057
6775
  async getModeAvailability() {
@@ -7061,10 +6779,6 @@ var Iframer = class {
7061
6779
  "binary-headful": {
7062
6780
  available: binaryHeadful,
7063
6781
  reason: binaryHeadful ? void 0 : "No display available"
7064
- },
7065
- "docker-headful": {
7066
- available: this.operatingMode === "docker",
7067
- reason: this.operatingMode === "docker" ? void 0 : "Docker not configured"
7068
6782
  }
7069
6783
  };
7070
6784
  }
@@ -7076,36 +6790,7 @@ var Iframer = class {
7076
6790
  execute(userId, token, pipeline, runtime) {
7077
6791
  return this.executor.execute(userId, token, pipeline, runtime);
7078
6792
  }
7079
- // ─── Interactive Sessions (Docker mode) ──────────────────────────
7080
- async startSession(userId, token, options = {}) {
7081
- const existing = getSession(userId);
7082
- if (existing) {
7083
- resetTimeout(userId);
7084
- return {
7085
- noVncUrl: `http://localhost:${existing.wsPort}/vnc.html?autoconnect=true`,
7086
- wsPort: existing.wsPort
7087
- };
7088
- }
7089
- const session = await startSession(userId);
7090
- const encryptionKey = await deriveKey(token);
7091
- const blob = await this.store.getSession(userId);
7092
- let sessionData = null;
7093
- if (blob && blob.length > 0) {
7094
- sessionData = JSON.parse(decrypt(blob, encryptionKey));
7095
- if (sessionData) await injectCookies(session.context, sessionData);
7096
- }
7097
- if (options.url) {
7098
- await session.page.goto(options.url, { waitUntil: "domcontentloaded", timeout: TIMEOUTS.NAVIGATION });
7099
- if (sessionData) await injectStorage(session.page, sessionData);
7100
- }
7101
- return {
7102
- noVncUrl: `http://localhost:${session.wsPort}/vnc.html?autoconnect=true`,
7103
- wsPort: session.wsPort
7104
- };
7105
- }
7106
- getSession(userId) {
7107
- return getSession(userId);
7108
- }
6793
+ // ─── Sessions ────────────────────────────────────────────────────
7109
6794
  async stopSession(userId, token) {
7110
6795
  let sessionSaved = false;
7111
6796
  if (token) {
@@ -7119,17 +6804,10 @@ var Iframer = class {
7119
6804
  sessionSaved = true;
7120
6805
  }
7121
6806
  } catch (err) {
7122
- log19.warn(`stopSession: failed to extract daemon state for ${inst.mode}::${inst.instanceId}: ${getErrorMessage(err)}`);
6807
+ log18.warn(`stopSession: failed to extract daemon state for ${inst.mode}::${inst.instanceId}: ${getErrorMessage(err)}`);
7123
6808
  }
7124
6809
  }
7125
6810
  }
7126
- const dockerSessionData = await stopSession(userId);
7127
- if (dockerSessionData && token) {
7128
- const encryptionKey = await deriveKey(token);
7129
- const encrypted = encrypt(JSON.stringify(dockerSessionData), encryptionKey);
7130
- await this.store.setSession(userId, encrypted);
7131
- sessionSaved = true;
7132
- }
7133
6811
  await this.daemon.stopAll();
7134
6812
  return { ok: true, sessionSaved };
7135
6813
  }
@@ -7167,25 +6845,11 @@ var Iframer = class {
7167
6845
  async restartBrowser() {
7168
6846
  const health = this.browserHealth();
7169
6847
  await this.daemon.stopAll(true);
7170
- await cleanupAllSessions();
7171
6848
  return {
7172
6849
  killed: health.modes,
7173
6850
  message: health.modes.length > 0 ? `Killed browser(s): ${health.modes.join(", ")}. Next execute call will launch a fresh instance.` : "No browsers were running. Next execute call will launch fresh."
7174
6851
  };
7175
6852
  }
7176
- // ─── Screenshots ─────────────────────────────────────────────────
7177
- async screenshot(userId) {
7178
- const session = getSession(userId);
7179
- if (!session) return null;
7180
- resetTimeout(userId);
7181
- const buf = await session.page.screenshot({ type: "jpeg", quality: 50, fullPage: false });
7182
- const screenshotUrl = saveScreenshot(buf, `screenshot-${Date.now()}.jpg`, this.config.screenshotDir, this.config.publicUrl);
7183
- return {
7184
- screenshotUrl,
7185
- url: session.page.url(),
7186
- title: await session.page.title()
7187
- };
7188
- }
7189
6853
  // ─── Credentials ─────────────────────────────────────────────────
7190
6854
  storeCredential(userId, token, credential) {
7191
6855
  return this.credentials.storeCredential(userId, token, credential);
@@ -7199,9 +6863,6 @@ var Iframer = class {
7199
6863
  deleteCredential(userId, domain) {
7200
6864
  return this.credentials.deleteCredential(userId, domain);
7201
6865
  }
7202
- loginWithCredentials(userId, token, domain, selectors) {
7203
- return this.credentials.loginWithCredentials(userId, token, domain, selectors);
7204
- }
7205
6866
  // ─── Session Data ─────────────────────────────────────────────────
7206
6867
  async clearSession(userId) {
7207
6868
  await this.store.deleteSession(userId);
@@ -7210,7 +6871,6 @@ var Iframer = class {
7210
6871
  async shutdown() {
7211
6872
  await this.daemon.stopAll(true);
7212
6873
  await closeBrowser();
7213
- await cleanupAllSessions();
7214
6874
  if ("close" in this.store && typeof this.store.close === "function") {
7215
6875
  this.store.close();
7216
6876
  }
@@ -7240,20 +6900,18 @@ function errorHandler(err, _req, res, _next) {
7240
6900
  }
7241
6901
 
7242
6902
  // src/api/routes.ts
7243
- var import_fs12 = __toESM(require("fs"));
6903
+ var import_fs10 = __toESM(require("fs"));
7244
6904
  function auth(req) {
7245
6905
  return req;
7246
6906
  }
7247
- var iframer = new Iframer({
7248
- mode: process.env.IFRAMER_MODE || "local"
7249
- });
6907
+ var iframer = new Iframer();
7250
6908
  function registerRoutes(app2) {
7251
6909
  app2.get("/health", (_req, res) => res.json({ ok: true }));
7252
6910
  app2.get("/browsers", async (_req, res) => {
7253
6911
  const browsers = [];
7254
6912
  try {
7255
6913
  const execPath = import_patchright3.chromium.executablePath();
7256
- browsers.push({ name: "chromium", installed: import_fs12.default.existsSync(execPath), executablePath: execPath });
6914
+ browsers.push({ name: "chromium", installed: import_fs10.default.existsSync(execPath), executablePath: execPath });
7257
6915
  } catch {
7258
6916
  browsers.push({ name: "chromium", installed: false, executablePath: null });
7259
6917
  }
@@ -7298,88 +6956,11 @@ function registerRoutes(app2) {
7298
6956
  const result = await iframer.execute(r.userId, r.token, { steps, options });
7299
6957
  res.json(result);
7300
6958
  }));
7301
- app2.post("/interactive/start", asyncHandler(async (req, res) => {
7302
- const { url } = req.body || {};
7303
- const r = auth(req);
7304
- const result = await iframer.startSession(r.userId, r.token, { url });
7305
- const existing = iframer.getSession(r.userId);
7306
- res.json({
7307
- ok: true,
7308
- noVncUrl: result.noVncUrl,
7309
- wsPort: result.wsPort,
7310
- message: existing ? "Session already active" : void 0
7311
- });
7312
- }));
7313
- app2.get("/interactive/status", (req, res) => {
7314
- const session = iframer.getSession(auth(req).userId);
7315
- if (!session) return res.json({ ok: true, active: false });
7316
- res.json({
7317
- ok: true,
7318
- active: true,
7319
- noVncUrl: `http://localhost:${session.wsPort}/vnc.html?autoconnect=true`,
7320
- wsPort: session.wsPort,
7321
- createdAt: session.createdAt.toISOString()
7322
- });
7323
- });
7324
6959
  app2.post("/interactive/stop", asyncHandler(async (req, res) => {
7325
6960
  const r = auth(req);
7326
6961
  const result = await iframer.stopSession(r.userId, r.token);
7327
6962
  res.json(result);
7328
6963
  }));
7329
- app2.get("/interactive/screenshot", asyncHandler(async (req, res) => {
7330
- const r = auth(req);
7331
- const session = iframer.getSession(r.userId);
7332
- if (!session) throw new AppError(404, "No active interactive session");
7333
- if (req.query.format === "raw") {
7334
- const buf = await session.page.screenshot({ type: "jpeg", quality: 50, fullPage: false });
7335
- res.set("Content-Type", "image/jpeg");
7336
- res.send(buf);
7337
- return;
7338
- }
7339
- const result = await iframer.screenshot(r.userId);
7340
- if (!result) throw new AppError(404, "No active interactive session");
7341
- res.json({ ok: true, ...result });
7342
- }));
7343
- app2.post("/interactive/act", asyncHandler(async (req, res) => {
7344
- const r = auth(req);
7345
- const session = iframer.getSession(r.userId);
7346
- if (!session) throw new AppError(404, "No active interactive session");
7347
- const { action, screenshot: wantScreenshot = true } = req.body || {};
7348
- if (!action || !action.type) throw new AppError(400, "Missing action.type");
7349
- const result = await iframer.execute(r.userId, r.token, {
7350
- steps: [action],
7351
- options: { screenshotAfterEach: wantScreenshot, continueOnObstacle: false }
7352
- });
7353
- const stepResult = result.results[0];
7354
- res.json({
7355
- ok: result.ok,
7356
- result: stepResult?.result,
7357
- screenshotUrl: stepResult?.screenshotUrl || result.finalState?.screenshotUrl,
7358
- url: result.finalState?.url,
7359
- title: result.finalState?.title,
7360
- error: result.error?.message
7361
- });
7362
- }));
7363
- app2.post("/interactive/batch", asyncHandler(async (req, res) => {
7364
- const r = auth(req);
7365
- const session = iframer.getSession(r.userId);
7366
- if (!session) throw new AppError(404, "No active interactive session");
7367
- const { actions, screenshot: wantScreenshot = true, continueOnError = false } = req.body || {};
7368
- if (!Array.isArray(actions) || actions.length === 0) {
7369
- throw new AppError(400, "actions must be a non-empty array");
7370
- }
7371
- const result = await iframer.execute(r.userId, r.token, {
7372
- steps: actions,
7373
- options: { continueOnError, screenshotAfterEach: false }
7374
- });
7375
- res.json({
7376
- ok: result.ok,
7377
- results: result.results.map((r2) => ({ index: r2.stepIndex, ok: r2.ok, result: r2.result, error: r2.error })),
7378
- screenshotUrl: wantScreenshot ? result.finalState?.screenshotUrl : void 0,
7379
- url: result.finalState?.url,
7380
- title: result.finalState?.title
7381
- });
7382
- }));
7383
6964
  app2.delete("/session", asyncHandler(async (req, res) => {
7384
6965
  await iframer.clearSession(auth(req).userId);
7385
6966
  res.json({ ok: true });
@@ -7402,20 +6983,6 @@ function registerRoutes(app2) {
7402
6983
  await iframer.deleteCredential(auth(req).userId, req.params.domain);
7403
6984
  res.json({ ok: true, message: `Credentials for ${req.params.domain} deleted` });
7404
6985
  }));
7405
- app2.post("/credentials/login", asyncHandler(async (req, res) => {
7406
- const { domain, usernameSelector, passwordSelector, submitSelector, totpSelector } = req.body || {};
7407
- if (!domain) throw new AppError(400, "Missing domain");
7408
- const r = auth(req);
7409
- const result = await iframer.loginWithCredentials(r.userId, r.token, domain, {
7410
- username: usernameSelector,
7411
- password: passwordSelector,
7412
- submit: submitSelector,
7413
- totp: totpSelector
7414
- });
7415
- if (!result.ok) throw new AppError(400, result.error || "Login failed");
7416
- const { ok: _ok, ...resultRest } = result;
7417
- res.json({ ok: true, message: "Login attempted", ...resultRest });
7418
- }));
7419
6986
  app2.get("/extension/status", (_req, res) => {
7420
6987
  res.json({ ok: true, version: getVersion(), ...extensionBridge.status() });
7421
6988
  });
@@ -7500,23 +7067,23 @@ function tokenAuth(req, res, next) {
7500
7067
 
7501
7068
  // index.ts
7502
7069
  var app = (0, import_express.default)();
7503
- var PORT = parseInt(process.env.PORT || "3021", 10);
7070
+ var PORT = parseInt(process.env.PORT || "3022", 10);
7504
7071
  var REAP_INTERVAL_MS = 6e4;
7505
7072
  var IDLE_EXIT_MS = parseInt(process.env.IFRAMER_SERVER_IDLE_EXIT_MS || String(30 * 60 * 1e3), 10);
7506
7073
  var SHUTDOWN_DEADLINE_MS = 1e4;
7507
- var SERVER_DIR = import_path12.default.dirname((0, import_url2.fileURLToPath)(importMetaUrl));
7074
+ var SERVER_DIR = import_path11.default.dirname((0, import_url.fileURLToPath)(importMetaUrl));
7508
7075
  var OWN_VERSION = (() => {
7509
- for (const p of [import_path12.default.join(SERVER_DIR, "package.json"), import_path12.default.join(SERVER_DIR, "..", "package.json")]) {
7076
+ for (const p of [import_path11.default.join(SERVER_DIR, "package.json"), import_path11.default.join(SERVER_DIR, "..", "package.json")]) {
7510
7077
  try {
7511
- const pkg = JSON.parse(import_fs13.default.readFileSync(p, "utf8"));
7078
+ const pkg = JSON.parse(import_fs11.default.readFileSync(p, "utf8"));
7512
7079
  if (pkg.name === "iframer-toolkit") return pkg.version;
7513
7080
  } catch {
7514
7081
  }
7515
7082
  }
7516
7083
  return void 0;
7517
7084
  })();
7518
- var SCREENSHOT_DIR = import_path12.default.join(import_path12.default.dirname((0, import_url2.fileURLToPath)(importMetaUrl)), ".screenshots");
7519
- import_fs13.default.mkdirSync(SCREENSHOT_DIR, { recursive: true });
7085
+ var SCREENSHOT_DIR = getScreenshotDir();
7086
+ import_fs11.default.mkdirSync(SCREENSHOT_DIR, { recursive: true, mode: 448 });
7520
7087
  app.use("/screenshots", import_express.default.static(SCREENSHOT_DIR));
7521
7088
  app.use(import_express.default.json());
7522
7089
  var lastActivity = Date.now();