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.
- package/README.md +68 -65
- package/dist/cli.js +2620 -1769
- package/dist/local-server.cjs +767 -1200
- package/dist/mcp-server.cjs +26 -110
- package/extension/README.md +10 -8
- package/extension/background.js +135 -84
- package/extension/manifest.json +1 -1
- package/extension/native-host.cjs +105 -29
- package/extension/popup.js +13 -4
- package/package.json +3 -6
- package/skills/iframer.md +44 -25
package/dist/local-server.cjs
CHANGED
|
@@ -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
|
|
119
|
-
var
|
|
120
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
732
|
+
import_fs2.default.mkdirSync(import_path3.default.dirname(file), { recursive: true });
|
|
1105
733
|
const secret = import_crypto.default.randomBytes(32).toString("hex");
|
|
1106
|
-
|
|
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/
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
|
1232
|
-
var
|
|
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
|
-
|
|
1272
|
-
const dbPath =
|
|
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
|
|
1353
|
-
var
|
|
981
|
+
var import_fs4 = __toESM(require("fs"));
|
|
982
|
+
var import_path5 = __toESM(require("path"));
|
|
1354
983
|
var import_os3 = __toESM(require("os"));
|
|
1355
|
-
var
|
|
1356
|
-
var
|
|
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 =
|
|
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 =
|
|
999
|
+
const entries = import_fs4.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
|
|
1371
1000
|
const dir = entries[0] || "chrome-mac-arm64";
|
|
1372
|
-
return
|
|
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 =
|
|
1004
|
+
const entries = import_fs4.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
|
|
1376
1005
|
const dir = entries[0] || "chrome-linux64";
|
|
1377
|
-
return
|
|
1006
|
+
return import_path5.default.join(installDir, dir, "chrome");
|
|
1378
1007
|
}
|
|
1379
1008
|
if (platform === "win32") {
|
|
1380
|
-
const entries =
|
|
1009
|
+
const entries = import_fs4.default.readdirSync(installDir).filter((e) => e.startsWith("chrome-"));
|
|
1381
1010
|
const dir = entries[0] || "chrome-win64";
|
|
1382
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const zipPath =
|
|
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
|
-
|
|
1406
|
-
|
|
1407
|
-
(0,
|
|
1408
|
-
|
|
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 (!
|
|
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
|
-
|
|
1043
|
+
import_fs4.default.chmodSync(execPath, 493);
|
|
1415
1044
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1117
|
+
log4.info("Downloading CloakBrowser binary...");
|
|
1498
1118
|
await cloak.ensureBinary();
|
|
1499
|
-
|
|
1119
|
+
log4.info("CloakBrowser ready");
|
|
1500
1120
|
}
|
|
1501
1121
|
_available = true;
|
|
1502
1122
|
return true;
|
|
1503
1123
|
} catch (err) {
|
|
1504
|
-
|
|
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
|
-
|
|
1139
|
+
log4.info(`CloakBrowser launched (headless=${options.headless})`);
|
|
1520
1140
|
return browser;
|
|
1521
1141
|
} catch (err) {
|
|
1522
|
-
|
|
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
|
|
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
|
-
|
|
1176
|
+
log5.info(`Page for ${mode} is dead, creating fresh context`);
|
|
1560
1177
|
try {
|
|
1561
1178
|
await context2.close();
|
|
1562
1179
|
} catch (err) {
|
|
1563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1200
|
+
log5.info(`CloakBrowser ${mode} ready`);
|
|
1584
1201
|
browser = cloakBrowser;
|
|
1585
1202
|
} else {
|
|
1586
1203
|
const executablePath = await ensureChrome();
|
|
1587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1300
|
+
log5.info(`blank-orphan sweep: ${key} never left about:blank, stopping`);
|
|
1684
1301
|
stopped.push(key);
|
|
1685
|
-
this.stopKey(key).catch((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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1399
|
+
log5.warn(`Chrome pid=${instance.chromePid} survived SIGKILL?! leaving registry record for reaper`);
|
|
1783
1400
|
}
|
|
1784
1401
|
} else if (!closedInTime) {
|
|
1785
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1817
|
-
this.stopKey(key).catch((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
|
|
1831
|
-
var
|
|
1447
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1448
|
+
var import_path6 = __toESM(require("path"));
|
|
1832
1449
|
init_paths();
|
|
1833
|
-
var
|
|
1450
|
+
var log6 = createLogger("domain-modes");
|
|
1834
1451
|
function defaultFile() {
|
|
1835
|
-
return
|
|
1452
|
+
return import_path6.default.join(getDataDir(), "domain-modes.json");
|
|
1836
1453
|
}
|
|
1837
1454
|
var TTL_DAYS = 14;
|
|
1838
|
-
var ESCALATION_LADDER = ["headless", "
|
|
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 (
|
|
1918
|
-
this.data = JSON.parse(
|
|
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
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
2121
|
-
for (const point of
|
|
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
|
|
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
|
-
|
|
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(
|
|
2260
|
+
await page.evaluate(STEALTH_SCRIPT);
|
|
2542
2261
|
} catch (err) {
|
|
2543
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2765
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2547
|
+
path12.unshift(seg);
|
|
2829
2548
|
current = parent;
|
|
2830
2549
|
}
|
|
2831
|
-
return { tag, text, selector:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2749
|
+
path12.unshift(seg);
|
|
2964
2750
|
current = parent;
|
|
2965
2751
|
}
|
|
2966
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2901
|
+
path12.unshift(seg);
|
|
3116
2902
|
current = parent;
|
|
3117
2903
|
}
|
|
3118
|
-
return
|
|
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/
|
|
3008
|
+
// src/lib/captcha/vision.ts
|
|
3223
3009
|
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
3224
|
-
var
|
|
3225
|
-
var
|
|
3226
|
-
|
|
3227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
{
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
{
|
|
3310
|
-
|
|
3311
|
-
|
|
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
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3199
|
+
const client = getCaptchaClient();
|
|
3367
3200
|
let rounds = 0;
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
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
|
-
|
|
3440
|
-
|
|
3441
|
-
if (
|
|
3442
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
3510
|
-
const
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
return
|
|
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
|
-
|
|
3578
|
-
|
|
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
|
|
3603
|
-
const
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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
|
|
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
|
-
|
|
3513
|
+
log11.warn(`captcha detection failed: ${err}`);
|
|
3728
3514
|
return false;
|
|
3729
3515
|
});
|
|
3730
|
-
|
|
3731
|
-
|
|
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
|
|
3579
|
+
var import_fs7 = __toESM(require("fs"));
|
|
3790
3580
|
var import_path8 = __toESM(require("path"));
|
|
3791
3581
|
init_paths();
|
|
3792
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3860
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4085
|
+
log13.info(`login: generated TOTP from stored secret for ${step.domain}`);
|
|
4296
4086
|
} else if (ctx.elicitOtp) {
|
|
4297
|
-
|
|
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
|
-
|
|
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(
|
|
4802
|
-
const parts =
|
|
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
|
|
4892
|
-
const lowerPath =
|
|
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:
|
|
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 ||
|
|
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(
|
|
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
|
|
4951
|
-
const segs =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
5345
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5578
|
+
var import_crypto8 = require("crypto");
|
|
5791
5579
|
|
|
5792
5580
|
// src/lib/extension/bridge.ts
|
|
5793
5581
|
var import_ws = require("ws");
|
|
5794
|
-
var
|
|
5582
|
+
var import_crypto6 = __toESM(require("crypto"));
|
|
5795
5583
|
|
|
5796
5584
|
// src/lib/version.ts
|
|
5797
|
-
var
|
|
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(
|
|
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
|
-
|
|
5845
|
-
|
|
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
|
|
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 ===
|
|
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:
|
|
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
|
|
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,
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6488
|
-
|
|
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
|
-
|
|
6501
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
7012
|
-
var
|
|
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 ||
|
|
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
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 || "
|
|
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 =
|
|
7074
|
+
var SERVER_DIR = import_path11.default.dirname((0, import_url.fileURLToPath)(importMetaUrl));
|
|
7508
7075
|
var OWN_VERSION = (() => {
|
|
7509
|
-
for (const p of [
|
|
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(
|
|
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 =
|
|
7519
|
-
|
|
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();
|