iframer-toolkit 0.9.0 → 0.10.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/dist/cli.js +4651 -307
- package/dist/local-server.cjs +1244 -128
- package/dist/mcp-server.cjs +579 -37
- package/extension/README.md +134 -0
- package/extension/background.js +639 -0
- package/extension/manifest.json +32 -0
- package/extension/native-host.cjs +91 -0
- package/extension/popup.html +72 -0
- package/extension/popup.js +108 -0
- package/package.json +12 -4
package/dist/local-server.cjs
CHANGED
|
@@ -41,6 +41,7 @@ var __export = (target, all) => {
|
|
|
41
41
|
set: __exportSetter.bind(all, name)
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
44
45
|
|
|
45
46
|
// src/lib/session/persistence.ts
|
|
46
47
|
var exports_persistence = {};
|
|
@@ -99,17 +100,31 @@ async function injectStorage(page, sessionData) {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// src/lib/paths.ts
|
|
104
|
+
var exports_paths = {};
|
|
105
|
+
__export(exports_paths, {
|
|
106
|
+
getDataDir: () => getDataDir
|
|
107
|
+
});
|
|
108
|
+
function getDataDir() {
|
|
109
|
+
return process.env.IFRAMER_DATA_DIR || import_path.default.join(import_os.default.homedir(), ".iframer");
|
|
110
|
+
}
|
|
111
|
+
var import_path, import_os;
|
|
112
|
+
var init_paths = __esm(() => {
|
|
113
|
+
import_path = __toESM(require("path"));
|
|
114
|
+
import_os = __toESM(require("os"));
|
|
115
|
+
});
|
|
116
|
+
|
|
102
117
|
// index.ts
|
|
103
118
|
var import_express = __toESM(require("express"));
|
|
104
|
-
var
|
|
105
|
-
var
|
|
119
|
+
var import_path12 = __toESM(require("path"));
|
|
120
|
+
var import_fs13 = __toESM(require("fs"));
|
|
106
121
|
var import_url2 = require("url");
|
|
107
122
|
|
|
108
123
|
// src/api/routes.ts
|
|
109
124
|
var import_patchright3 = require("patchright");
|
|
110
125
|
|
|
111
126
|
// src/lib/iframer.ts
|
|
112
|
-
var
|
|
127
|
+
var import_path11 = __toESM(require("path"));
|
|
113
128
|
var import_url = require("url");
|
|
114
129
|
|
|
115
130
|
// src/lib/browser/session-manager.ts
|
|
@@ -661,7 +676,10 @@ var TIMEOUTS = {
|
|
|
661
676
|
TOTP_INPUT: 5000,
|
|
662
677
|
API_REQUEST: 180000,
|
|
663
678
|
HEALTH_CHECK: 3000,
|
|
664
|
-
CHALLENGE_FRAME_WAIT: 5000
|
|
679
|
+
CHALLENGE_FRAME_WAIT: 5000,
|
|
680
|
+
TAB_FOLLOW_SETTLE: 400,
|
|
681
|
+
TAB_LOAD: 15000,
|
|
682
|
+
TAB_BLANK_RESOLVE: 3000
|
|
665
683
|
};
|
|
666
684
|
var CHROME_MIN_VERSION = 130;
|
|
667
685
|
|
|
@@ -821,19 +839,11 @@ async function cleanupAllSessions() {
|
|
|
821
839
|
}
|
|
822
840
|
|
|
823
841
|
// src/lib/auth/crypto.ts
|
|
842
|
+
init_paths();
|
|
824
843
|
var import_crypto = __toESM(require("crypto"));
|
|
825
844
|
var import_fs3 = __toESM(require("fs"));
|
|
826
845
|
var import_os2 = __toESM(require("os"));
|
|
827
846
|
var import_path2 = __toESM(require("path"));
|
|
828
|
-
|
|
829
|
-
// src/lib/paths.ts
|
|
830
|
-
var import_path = __toESM(require("path"));
|
|
831
|
-
var import_os = __toESM(require("os"));
|
|
832
|
-
function getDataDir() {
|
|
833
|
-
return process.env.IFRAMER_DATA_DIR || import_path.default.join(import_os.default.homedir(), ".iframer");
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// src/lib/auth/crypto.ts
|
|
837
847
|
var SALT = "iframer-session";
|
|
838
848
|
var INFO = "encryption";
|
|
839
849
|
var KEY_LENGTH = 32;
|
|
@@ -1077,6 +1087,7 @@ class SqliteStore {
|
|
|
1077
1087
|
}
|
|
1078
1088
|
|
|
1079
1089
|
// src/lib/storage.ts
|
|
1090
|
+
init_paths();
|
|
1080
1091
|
function createStore(options = {}) {
|
|
1081
1092
|
const dataDir = options.dataDir || getDataDir();
|
|
1082
1093
|
return new SqliteStore(dataDir);
|
|
@@ -1225,7 +1236,12 @@ async function ensureChrome() {
|
|
|
1225
1236
|
// src/lib/browser/cloak-browser.ts
|
|
1226
1237
|
var log5 = createLogger("cloak");
|
|
1227
1238
|
var _available = null;
|
|
1239
|
+
function cloakEnabled() {
|
|
1240
|
+
return process.env.IFRAMER_USE_CLOAKBROWSER === "1" || process.env.IFRAMER_USE_CLOAKBROWSER === "true";
|
|
1241
|
+
}
|
|
1228
1242
|
async function tryImport() {
|
|
1243
|
+
if (!cloakEnabled())
|
|
1244
|
+
return null;
|
|
1229
1245
|
try {
|
|
1230
1246
|
return await import("cloakbrowser");
|
|
1231
1247
|
} catch {
|
|
@@ -1272,6 +1288,7 @@ async function launchCloakBrowser(options) {
|
|
|
1272
1288
|
}
|
|
1273
1289
|
|
|
1274
1290
|
// src/lib/browser/registry.ts
|
|
1291
|
+
init_paths();
|
|
1275
1292
|
var import_fs7 = __toESM(require("fs"));
|
|
1276
1293
|
var import_path6 = __toESM(require("path"));
|
|
1277
1294
|
var import_child_process3 = require("child_process");
|
|
@@ -1616,6 +1633,7 @@ class BrowserDaemon {
|
|
|
1616
1633
|
// src/lib/domain-modes.ts
|
|
1617
1634
|
var import_fs8 = __toESM(require("fs"));
|
|
1618
1635
|
var import_path7 = __toESM(require("path"));
|
|
1636
|
+
init_paths();
|
|
1619
1637
|
var log8 = createLogger("domain-modes");
|
|
1620
1638
|
function defaultFile() {
|
|
1621
1639
|
return import_path7.default.join(getDataDir(), "domain-modes.json");
|
|
@@ -1771,11 +1789,56 @@ class RefStore {
|
|
|
1771
1789
|
}
|
|
1772
1790
|
}
|
|
1773
1791
|
|
|
1792
|
+
// src/lib/execution/pipeline-executor.ts
|
|
1793
|
+
var import_playwright_core = require("playwright-core");
|
|
1794
|
+
|
|
1774
1795
|
// src/lib/actions/types.ts
|
|
1775
1796
|
function failedStepResult(step, error, durationMs, stepIndex = -1) {
|
|
1776
1797
|
return { stepIndex, step, ok: false, error, durationMs };
|
|
1777
1798
|
}
|
|
1778
1799
|
|
|
1800
|
+
// src/lib/clipboard.ts
|
|
1801
|
+
var import_child_process4 = require("child_process");
|
|
1802
|
+
function platformTools(mode) {
|
|
1803
|
+
if (process.platform === "darwin")
|
|
1804
|
+
return [[mode === "read" ? "pbpaste" : "pbcopy"]];
|
|
1805
|
+
if (mode === "read")
|
|
1806
|
+
return [["wl-paste", "-n"], ["xclip", "-selection", "clipboard", "-o"], ["xsel", "-b", "-o"]];
|
|
1807
|
+
return [["wl-copy"], ["xclip", "-selection", "clipboard", "-i"], ["xsel", "-b", "-i"]];
|
|
1808
|
+
}
|
|
1809
|
+
function run(cmd, input) {
|
|
1810
|
+
return new Promise((resolve) => {
|
|
1811
|
+
let child;
|
|
1812
|
+
try {
|
|
1813
|
+
child = import_child_process4.spawn(cmd[0], cmd.slice(1));
|
|
1814
|
+
} catch {
|
|
1815
|
+
resolve({ ok: false, out: "", err: `spawn ${cmd[0]} failed` });
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
let out = "";
|
|
1819
|
+
let errOut = "";
|
|
1820
|
+
child.stdout?.on("data", (d) => out += d);
|
|
1821
|
+
child.stderr?.on("data", (d) => errOut += d);
|
|
1822
|
+
child.on("error", (e) => resolve({ ok: false, out: "", err: e.message }));
|
|
1823
|
+
child.on("close", (code) => resolve({ ok: code === 0, out, err: errOut }));
|
|
1824
|
+
if (input !== undefined) {
|
|
1825
|
+
child.stdin?.write(input);
|
|
1826
|
+
child.stdin?.end();
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
async function clipboardRead() {
|
|
1831
|
+
const tools = platformTools("read");
|
|
1832
|
+
let lastErr = "";
|
|
1833
|
+
for (const cmd of tools) {
|
|
1834
|
+
const r = await run(cmd);
|
|
1835
|
+
if (r.ok)
|
|
1836
|
+
return r.out;
|
|
1837
|
+
lastErr = r.err;
|
|
1838
|
+
}
|
|
1839
|
+
throw new Error(`No working clipboard tool found (${tools.map((t) => t[0]).join(", ")}). ${lastErr}`);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1779
1842
|
// src/lib/browser/humanize.ts
|
|
1780
1843
|
function rand(min, max) {
|
|
1781
1844
|
return Math.random() * (max - min) + min;
|
|
@@ -1980,6 +2043,15 @@ function sleep3(ms) {
|
|
|
1980
2043
|
}
|
|
1981
2044
|
// src/lib/actions/resolve-selector.ts
|
|
1982
2045
|
function resolveSelector(selector, ctx) {
|
|
2046
|
+
if (selector.startsWith("@a:")) {
|
|
2047
|
+
const name = selector.slice(3);
|
|
2048
|
+
const anchor = ctx.anchors?.get(name);
|
|
2049
|
+
if (!anchor) {
|
|
2050
|
+
const available = ctx.anchors ? Array.from(ctx.anchors.keys()).join(", ") : "";
|
|
2051
|
+
throw new Error(`Unknown anchor: @a:${name}${ctx.anchorDomain ? ` for ${ctx.anchorDomain}` : ""}. ` + `${available ? `Known anchors: ${available}. ` : "This domain has no saved anchors yet. "}` + `Run a snapshot/find to locate the element, act on it, then save it with the ` + `\`remember\` tool so future runs skip the search.`);
|
|
2052
|
+
}
|
|
2053
|
+
return anchor.selector;
|
|
2054
|
+
}
|
|
1983
2055
|
if (selector.startsWith("@e")) {
|
|
1984
2056
|
const ref = ctx.refMap.get(selector);
|
|
1985
2057
|
if (!ref) {
|
|
@@ -1990,6 +2062,9 @@ function resolveSelector(selector, ctx) {
|
|
|
1990
2062
|
}
|
|
1991
2063
|
return selector;
|
|
1992
2064
|
}
|
|
2065
|
+
function anchorNameOf(selector) {
|
|
2066
|
+
return typeof selector === "string" && selector.startsWith("@a:") ? selector.slice(3) : null;
|
|
2067
|
+
}
|
|
1993
2068
|
|
|
1994
2069
|
// src/lib/actions/handlers/navigation.ts
|
|
1995
2070
|
var log9 = createLogger("actions");
|
|
@@ -2016,7 +2091,42 @@ async function click(page, step, ctx) {
|
|
|
2016
2091
|
await page.click(resolveSelector(step.selector, ctx));
|
|
2017
2092
|
}
|
|
2018
2093
|
async function fill(page, step, ctx) {
|
|
2019
|
-
|
|
2094
|
+
const selector = resolveSelector(step.selector, ctx);
|
|
2095
|
+
const value = step.value;
|
|
2096
|
+
await page.fill(selector, value);
|
|
2097
|
+
const stuck = await page.evaluate(({ sel, val }) => {
|
|
2098
|
+
const el = document.querySelector(sel);
|
|
2099
|
+
if (!el)
|
|
2100
|
+
return false;
|
|
2101
|
+
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
2102
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
2103
|
+
try {
|
|
2104
|
+
el.focus();
|
|
2105
|
+
} catch {}
|
|
2106
|
+
try {
|
|
2107
|
+
setter ? setter.call(el, val) : el.value = val;
|
|
2108
|
+
} catch {
|
|
2109
|
+
el.value = val;
|
|
2110
|
+
}
|
|
2111
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2112
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2113
|
+
try {
|
|
2114
|
+
el.blur();
|
|
2115
|
+
} catch {}
|
|
2116
|
+
return el.value === val;
|
|
2117
|
+
}, { sel: selector, val: value });
|
|
2118
|
+
if (!stuck) {
|
|
2119
|
+
const loc = page.locator(selector);
|
|
2120
|
+
try {
|
|
2121
|
+
await loc.click();
|
|
2122
|
+
await loc.fill("");
|
|
2123
|
+
await loc.pressSequentially(value, { delay: 15 });
|
|
2124
|
+
await page.evaluate((sel) => {
|
|
2125
|
+
const el = document.querySelector(sel);
|
|
2126
|
+
el?.blur?.();
|
|
2127
|
+
}, selector);
|
|
2128
|
+
} catch {}
|
|
2129
|
+
}
|
|
2020
2130
|
}
|
|
2021
2131
|
async function humanClickStep(page, step, ctx) {
|
|
2022
2132
|
if (step.selector) {
|
|
@@ -2051,11 +2161,82 @@ async function wait(page, step) {
|
|
|
2051
2161
|
async function waitFor(page, step, ctx) {
|
|
2052
2162
|
await page.waitForSelector(resolveSelector(step.selector, ctx), { timeout: step.timeout || TIMEOUTS.SELECTOR_WAIT });
|
|
2053
2163
|
}
|
|
2054
|
-
async function scroll(page, step) {
|
|
2055
|
-
|
|
2164
|
+
async function scroll(page, step, ctx) {
|
|
2165
|
+
const selector = step.selector ? resolveSelector(step.selector, ctx) : null;
|
|
2166
|
+
await page.evaluate(({ dy, sel }) => {
|
|
2167
|
+
if (sel) {
|
|
2168
|
+
const el = document.querySelector(sel);
|
|
2169
|
+
if (!el)
|
|
2170
|
+
throw new Error(`scroll: no element for selector ${sel}`);
|
|
2171
|
+
el.scrollBy(0, dy || el.scrollHeight);
|
|
2172
|
+
} else {
|
|
2173
|
+
window.scrollBy(0, dy || document.body.scrollHeight);
|
|
2174
|
+
}
|
|
2175
|
+
}, { dy: step.deltaY ?? 0, sel: selector });
|
|
2056
2176
|
}
|
|
2057
2177
|
async function keyboard(page, step) {
|
|
2058
|
-
|
|
2178
|
+
const mods = [
|
|
2179
|
+
step.meta ? "Meta" : null,
|
|
2180
|
+
step.ctrl ? "Control" : null,
|
|
2181
|
+
step.shift ? "Shift" : null,
|
|
2182
|
+
step.alt ? "Alt" : null
|
|
2183
|
+
].filter(Boolean);
|
|
2184
|
+
await page.keyboard.press(mods.length ? `${mods.join("+")}+${step.key}` : step.key);
|
|
2185
|
+
}
|
|
2186
|
+
async function read(page, step, ctx) {
|
|
2187
|
+
const selector = step.selector ? resolveSelector(step.selector, ctx) : "body";
|
|
2188
|
+
const raw = await page.evaluate((sel) => {
|
|
2189
|
+
const el = sel === "body" ? document.body : document.querySelector(sel);
|
|
2190
|
+
if (!el)
|
|
2191
|
+
return null;
|
|
2192
|
+
return el.innerText || el.textContent || "";
|
|
2193
|
+
}, selector);
|
|
2194
|
+
if (raw == null)
|
|
2195
|
+
throw new Error(`read: no element for selector ${selector}`);
|
|
2196
|
+
const text = raw.replace(/\n{3,}/g, `
|
|
2197
|
+
|
|
2198
|
+
`).trim();
|
|
2199
|
+
const max = step.maxChars || 6000;
|
|
2200
|
+
return { text: text.slice(0, max), truncated: text.length > max };
|
|
2201
|
+
}
|
|
2202
|
+
async function upload(page, step, ctx) {
|
|
2203
|
+
if (!step.files || step.files.length === 0)
|
|
2204
|
+
throw new Error("upload: `files` must be a non-empty array of local paths");
|
|
2205
|
+
await page.setInputFiles(resolveSelector(step.selector, ctx), step.files);
|
|
2206
|
+
return { uploaded: step.files.length, files: step.files };
|
|
2207
|
+
}
|
|
2208
|
+
async function paste(page, step, ctx) {
|
|
2209
|
+
const text = await clipboardRead();
|
|
2210
|
+
if (step.selector) {
|
|
2211
|
+
await page.click(resolveSelector(step.selector, ctx));
|
|
2212
|
+
}
|
|
2213
|
+
await page.keyboard.insertText(text);
|
|
2214
|
+
return { pasted: text.length };
|
|
2215
|
+
}
|
|
2216
|
+
async function download(page, step) {
|
|
2217
|
+
if (!step.url)
|
|
2218
|
+
throw new Error("download: `url` is required");
|
|
2219
|
+
const resp = await page.request.get(step.url);
|
|
2220
|
+
const status = resp.status();
|
|
2221
|
+
if (!resp.ok())
|
|
2222
|
+
throw new Error(`download: HTTP ${status} for ${step.url}`);
|
|
2223
|
+
const buf = await resp.body();
|
|
2224
|
+
const fs9 = await import("fs");
|
|
2225
|
+
const pathMod = await import("path");
|
|
2226
|
+
const { getDataDir: getDataDir2 } = await Promise.resolve().then(() => (init_paths(), exports_paths));
|
|
2227
|
+
let outPath;
|
|
2228
|
+
if (step.path && pathMod.isAbsolute(step.path)) {
|
|
2229
|
+
outPath = step.path;
|
|
2230
|
+
} else {
|
|
2231
|
+
let name = "download";
|
|
2232
|
+
try {
|
|
2233
|
+
name = decodeURIComponent(new URL(step.url).pathname.split("/").pop() || "") || "download";
|
|
2234
|
+
} catch {}
|
|
2235
|
+
outPath = pathMod.join(getDataDir2(), "downloads", step.path || name);
|
|
2236
|
+
}
|
|
2237
|
+
fs9.mkdirSync(pathMod.dirname(outPath), { recursive: true });
|
|
2238
|
+
fs9.writeFileSync(outPath, buf);
|
|
2239
|
+
return { path: outPath, size: buf.length, status };
|
|
2059
2240
|
}
|
|
2060
2241
|
async function typeCode(page, step, ctx) {
|
|
2061
2242
|
const code = String(step.value || "");
|
|
@@ -3083,6 +3264,7 @@ async function findSubmitButton(page, opts) {
|
|
|
3083
3264
|
// src/lib/knowledge.ts
|
|
3084
3265
|
var import_fs9 = __toESM(require("fs"));
|
|
3085
3266
|
var import_path8 = __toESM(require("path"));
|
|
3267
|
+
init_paths();
|
|
3086
3268
|
var log13 = createLogger("knowledge");
|
|
3087
3269
|
function getKnowledgeDir() {
|
|
3088
3270
|
return import_path8.default.join(getDataDir(), "knowledge");
|
|
@@ -3637,6 +3819,10 @@ var registry = {
|
|
|
3637
3819
|
"wait-for": waitFor,
|
|
3638
3820
|
scroll,
|
|
3639
3821
|
keyboard,
|
|
3822
|
+
read,
|
|
3823
|
+
upload,
|
|
3824
|
+
paste,
|
|
3825
|
+
download,
|
|
3640
3826
|
"type-code": typeCode,
|
|
3641
3827
|
find,
|
|
3642
3828
|
screenshot,
|
|
@@ -4236,8 +4422,13 @@ class ApiCapture {
|
|
|
4236
4422
|
currentStep = 0;
|
|
4237
4423
|
requestHandler;
|
|
4238
4424
|
responseHandler;
|
|
4425
|
+
context;
|
|
4426
|
+
hookedPages = new Set;
|
|
4427
|
+
pageHandler;
|
|
4239
4428
|
constructor(page) {
|
|
4240
4429
|
this.page = page;
|
|
4430
|
+
this.context = page.context();
|
|
4431
|
+
this.pageHandler = (p) => this.hookPage(p);
|
|
4241
4432
|
this.requestHandler = (req) => {
|
|
4242
4433
|
const resourceType = req.resourceType();
|
|
4243
4434
|
if (SKIP_RESOURCE_TYPES.has(resourceType))
|
|
@@ -4304,16 +4495,29 @@ class ApiCapture {
|
|
|
4304
4495
|
} catch {}
|
|
4305
4496
|
};
|
|
4306
4497
|
}
|
|
4498
|
+
hookPage(p) {
|
|
4499
|
+
if (this.hookedPages.has(p))
|
|
4500
|
+
return;
|
|
4501
|
+
this.hookedPages.add(p);
|
|
4502
|
+
p.on("request", this.requestHandler);
|
|
4503
|
+
p.on("response", this.responseHandler);
|
|
4504
|
+
}
|
|
4307
4505
|
start() {
|
|
4308
|
-
this.
|
|
4309
|
-
this.
|
|
4506
|
+
this.hookPage(this.page);
|
|
4507
|
+
this.context.on("page", this.pageHandler);
|
|
4310
4508
|
}
|
|
4311
4509
|
setStep(index) {
|
|
4312
4510
|
this.currentStep = index;
|
|
4313
4511
|
}
|
|
4314
4512
|
stop() {
|
|
4315
|
-
this.
|
|
4316
|
-
|
|
4513
|
+
this.context.off("page", this.pageHandler);
|
|
4514
|
+
for (const p of this.hookedPages) {
|
|
4515
|
+
try {
|
|
4516
|
+
p.off("request", this.requestHandler);
|
|
4517
|
+
p.off("response", this.responseHandler);
|
|
4518
|
+
} catch {}
|
|
4519
|
+
}
|
|
4520
|
+
this.hookedPages.clear();
|
|
4317
4521
|
}
|
|
4318
4522
|
async drain(ms = 3000, pendingTimeoutMs = 5000) {
|
|
4319
4523
|
await new Promise((r) => setTimeout(r, ms));
|
|
@@ -4322,99 +4526,241 @@ class ApiCapture {
|
|
|
4322
4526
|
await new Promise((r) => setTimeout(r, 100));
|
|
4323
4527
|
}
|
|
4324
4528
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4529
|
+
getResults() {
|
|
4530
|
+
return buildCapturedApi(this.requests);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
function extractAuth(requests) {
|
|
4534
|
+
const auth = { cookies: {}, tokens: {} };
|
|
4535
|
+
for (const req of requests) {
|
|
4536
|
+
for (const [key, value] of Object.entries(req.requestHeaders)) {
|
|
4537
|
+
const lower = key.toLowerCase();
|
|
4538
|
+
if (lower === "authorization" && !auth.authorization) {
|
|
4539
|
+
auth.authorization = value;
|
|
4540
|
+
} else if (lower === "cookie") {
|
|
4541
|
+
const cookies = parseCookies(value);
|
|
4542
|
+
Object.assign(auth.cookies, cookies);
|
|
4543
|
+
} else if (isAuthHeader(key) && lower !== "authorization" && lower !== "cookie") {
|
|
4544
|
+
auth.tokens[key] = value;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
return auth;
|
|
4549
|
+
}
|
|
4550
|
+
function splitHeaders(headers) {
|
|
4551
|
+
const endpointHeaders = {};
|
|
4552
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4553
|
+
const lower = key.toLowerCase();
|
|
4554
|
+
if (BROWSER_NOISE_HEADERS.has(lower))
|
|
4555
|
+
continue;
|
|
4556
|
+
if (isAuthHeader(key))
|
|
4557
|
+
continue;
|
|
4558
|
+
if (lower === "user-agent")
|
|
4559
|
+
continue;
|
|
4560
|
+
endpointHeaders[key] = value;
|
|
4561
|
+
}
|
|
4562
|
+
return endpointHeaders;
|
|
4563
|
+
}
|
|
4564
|
+
function buildCapturedApi(requests) {
|
|
4565
|
+
const byDomain = new Map;
|
|
4566
|
+
for (const req of requests) {
|
|
4567
|
+
try {
|
|
4568
|
+
const host = new URL(req.url).origin;
|
|
4569
|
+
if (!byDomain.has(host))
|
|
4570
|
+
byDomain.set(host, []);
|
|
4571
|
+
byDomain.get(host)?.push(req);
|
|
4572
|
+
} catch {}
|
|
4573
|
+
}
|
|
4574
|
+
const apis = [];
|
|
4575
|
+
for (const [baseUrl, domainRequests] of byDomain) {
|
|
4576
|
+
const auth = extractAuth(domainRequests);
|
|
4577
|
+
const endpointMap = new Map;
|
|
4578
|
+
for (const req of domainRequests) {
|
|
4579
|
+
const paramPath = parameterizePath(req.path);
|
|
4580
|
+
const { protocol, action } = classifyRequest(req);
|
|
4581
|
+
const key = `${protocol}:${action}`;
|
|
4582
|
+
const endpointHeaders = splitHeaders(req.requestHeaders);
|
|
4583
|
+
if (!endpointMap.has(key)) {
|
|
4584
|
+
const verb = inferVerb(protocol, action, req.method, req.responseBody);
|
|
4585
|
+
const functionName = buildFunctionName(protocol, action, req.method, verb);
|
|
4586
|
+
endpointMap.set(key, {
|
|
4587
|
+
method: req.method,
|
|
4588
|
+
path: paramPath,
|
|
4589
|
+
rawPaths: [req.path],
|
|
4590
|
+
queryParams: req.queryParams,
|
|
4591
|
+
headers: endpointHeaders,
|
|
4592
|
+
requestBody: req.requestBody,
|
|
4593
|
+
responseStatus: req.responseStatus,
|
|
4594
|
+
responseBody: req.responseBody,
|
|
4595
|
+
triggeredAtStep: req.triggeredAtStep,
|
|
4596
|
+
curl: buildCurl(req.method, req.url, endpointHeaders, auth, req.requestBody),
|
|
4597
|
+
protocol,
|
|
4598
|
+
action,
|
|
4599
|
+
verb,
|
|
4600
|
+
functionName
|
|
4601
|
+
});
|
|
4602
|
+
} else {
|
|
4603
|
+
const existing = endpointMap.get(key);
|
|
4604
|
+
if (!existing)
|
|
4605
|
+
continue;
|
|
4606
|
+
if (!existing.rawPaths.includes(req.path)) {
|
|
4607
|
+
existing.rawPaths.push(req.path);
|
|
4337
4608
|
}
|
|
4338
4609
|
}
|
|
4339
4610
|
}
|
|
4340
|
-
|
|
4611
|
+
const domain = new URL(baseUrl).hostname.replace(/\./g, "_");
|
|
4612
|
+
apis.push({
|
|
4613
|
+
domain,
|
|
4614
|
+
baseUrl,
|
|
4615
|
+
auth,
|
|
4616
|
+
endpoints: Array.from(endpointMap.values()).sort((a, b) => a.triggeredAtStep - b.triggeredAtStep),
|
|
4617
|
+
capturedAt: new Date().toISOString()
|
|
4618
|
+
});
|
|
4341
4619
|
}
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4620
|
+
return apis.sort((a, b) => b.endpoints.length - a.endpoints.length);
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
// src/lib/browser/tab-tracker.ts
|
|
4624
|
+
var log15 = createLogger("tabs");
|
|
4625
|
+
|
|
4626
|
+
class TabTracker {
|
|
4627
|
+
context;
|
|
4628
|
+
pages;
|
|
4629
|
+
activePage;
|
|
4630
|
+
newlyOpened = [];
|
|
4631
|
+
disposed = false;
|
|
4632
|
+
constructor(context, initial) {
|
|
4633
|
+
this.context = context;
|
|
4634
|
+
this.activePage = initial;
|
|
4635
|
+
this.pages = [...context.pages()];
|
|
4636
|
+
if (!this.pages.includes(initial))
|
|
4637
|
+
this.pages.push(initial);
|
|
4638
|
+
context.on("page", this.onNewPage);
|
|
4639
|
+
}
|
|
4640
|
+
onNewPage = (p) => {
|
|
4641
|
+
if (this.disposed)
|
|
4642
|
+
return;
|
|
4643
|
+
this.pages.push(p);
|
|
4644
|
+
this.newlyOpened.push(p);
|
|
4645
|
+
p.on("close", () => this.onClose(p));
|
|
4646
|
+
log15.debug(`new tab opened: ${safeUrl(p)}`);
|
|
4647
|
+
};
|
|
4648
|
+
onClose = (p) => {
|
|
4649
|
+
this.pages = this.pages.filter((x) => x !== p);
|
|
4650
|
+
this.newlyOpened = this.newlyOpened.filter((x) => x !== p);
|
|
4651
|
+
if (this.activePage === p) {
|
|
4652
|
+
this.activePage = this.pages[this.pages.length - 1] ?? p;
|
|
4653
|
+
log15.debug(`active tab closed, fell back to: ${safeUrl(this.activePage)}`);
|
|
4353
4654
|
}
|
|
4354
|
-
|
|
4655
|
+
};
|
|
4656
|
+
active() {
|
|
4657
|
+
return this.activePage;
|
|
4355
4658
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
byDomain.set(host, []);
|
|
4363
|
-
byDomain.get(host)?.push(req);
|
|
4364
|
-
} catch {}
|
|
4659
|
+
count() {
|
|
4660
|
+
return this.pages.length;
|
|
4661
|
+
}
|
|
4662
|
+
async settle(opts) {
|
|
4663
|
+
if (this.newlyOpened.length === 0 && opts.waitForPendingMs > 0) {
|
|
4664
|
+
await this.context.waitForEvent("page", { timeout: opts.waitForPendingMs }).catch(() => null);
|
|
4365
4665
|
}
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
headers: endpointHeaders,
|
|
4384
|
-
requestBody: req.requestBody,
|
|
4385
|
-
responseStatus: req.responseStatus,
|
|
4386
|
-
responseBody: req.responseBody,
|
|
4387
|
-
triggeredAtStep: req.triggeredAtStep,
|
|
4388
|
-
curl: buildCurl(req.method, req.url, endpointHeaders, auth, req.requestBody),
|
|
4389
|
-
protocol,
|
|
4390
|
-
action,
|
|
4391
|
-
verb,
|
|
4392
|
-
functionName
|
|
4393
|
-
});
|
|
4394
|
-
} else {
|
|
4395
|
-
const existing = endpointMap.get(key);
|
|
4396
|
-
if (!existing)
|
|
4397
|
-
continue;
|
|
4398
|
-
if (!existing.rawPaths.includes(req.path)) {
|
|
4399
|
-
existing.rawPaths.push(req.path);
|
|
4400
|
-
}
|
|
4401
|
-
}
|
|
4402
|
-
}
|
|
4403
|
-
const domain = new URL(baseUrl).hostname.replace(/\./g, "_");
|
|
4404
|
-
apis.push({
|
|
4405
|
-
domain,
|
|
4406
|
-
baseUrl,
|
|
4407
|
-
auth,
|
|
4408
|
-
endpoints: Array.from(endpointMap.values()).sort((a, b) => a.triggeredAtStep - b.triggeredAtStep),
|
|
4409
|
-
capturedAt: new Date().toISOString()
|
|
4410
|
-
});
|
|
4666
|
+
if (this.newlyOpened.length === 0)
|
|
4667
|
+
return null;
|
|
4668
|
+
const target = this.newlyOpened[this.newlyOpened.length - 1];
|
|
4669
|
+
this.newlyOpened = [];
|
|
4670
|
+
if (target.isClosed())
|
|
4671
|
+
return null;
|
|
4672
|
+
await target.waitForLoadState("domcontentloaded", { timeout: opts.loadTimeoutMs }).catch(() => {});
|
|
4673
|
+
if (safeUrl(target) === "about:blank" || safeUrl(target) === "") {
|
|
4674
|
+
await target.waitForURL((u) => {
|
|
4675
|
+
const s = u.toString();
|
|
4676
|
+
return !!s && s !== "about:blank";
|
|
4677
|
+
}, { timeout: opts.blankResolveMs }).catch(() => {});
|
|
4678
|
+
await target.waitForLoadState("domcontentloaded", { timeout: opts.loadTimeoutMs }).catch(() => {});
|
|
4679
|
+
}
|
|
4680
|
+
const url = safeUrl(target);
|
|
4681
|
+
if (!url || url === "about:blank") {
|
|
4682
|
+
return null;
|
|
4411
4683
|
}
|
|
4412
|
-
|
|
4684
|
+
await target.bringToFront().catch(() => {});
|
|
4685
|
+
this.activePage = target;
|
|
4686
|
+
const sw = { url, title: await target.title().catch(() => "") };
|
|
4687
|
+
log15.info(`followed new tab → ${sw.url}`);
|
|
4688
|
+
return sw;
|
|
4689
|
+
}
|
|
4690
|
+
discardPending() {
|
|
4691
|
+
this.newlyOpened = [];
|
|
4692
|
+
}
|
|
4693
|
+
dispose() {
|
|
4694
|
+
this.disposed = true;
|
|
4695
|
+
try {
|
|
4696
|
+
this.context.off("page", this.onNewPage);
|
|
4697
|
+
} catch {}
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
function safeUrl(p) {
|
|
4701
|
+
try {
|
|
4702
|
+
return p.url();
|
|
4703
|
+
} catch {
|
|
4704
|
+
return "unknown";
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
// src/lib/knowledge/component-map.ts
|
|
4709
|
+
var import_fs10 = __toESM(require("fs"));
|
|
4710
|
+
var import_path9 = __toESM(require("path"));
|
|
4711
|
+
function anchorsPath(domain) {
|
|
4712
|
+
return import_path9.default.join(getKnowledgeDir(), `${sanitizeDomain(domain)}.anchors.json`);
|
|
4713
|
+
}
|
|
4714
|
+
function loadComponentMap(domain) {
|
|
4715
|
+
const norm = normalizeDomain(domain);
|
|
4716
|
+
try {
|
|
4717
|
+
const raw = import_fs10.default.readFileSync(anchorsPath(norm), "utf8");
|
|
4718
|
+
const parsed = JSON.parse(raw);
|
|
4719
|
+
return {
|
|
4720
|
+
domain: parsed.domain || norm,
|
|
4721
|
+
anchors: parsed.anchors || {},
|
|
4722
|
+
quirks: Array.isArray(parsed.quirks) ? parsed.quirks : []
|
|
4723
|
+
};
|
|
4724
|
+
} catch {
|
|
4725
|
+
return { domain: norm, anchors: {}, quirks: [] };
|
|
4413
4726
|
}
|
|
4414
4727
|
}
|
|
4728
|
+
function loadAnchors(domain) {
|
|
4729
|
+
const map = new Map;
|
|
4730
|
+
const cm = loadComponentMap(domain);
|
|
4731
|
+
for (const [name, a] of Object.entries(cm.anchors))
|
|
4732
|
+
map.set(name, a);
|
|
4733
|
+
return map;
|
|
4734
|
+
}
|
|
4735
|
+
function write(cm) {
|
|
4736
|
+
import_fs10.default.mkdirSync(getKnowledgeDir(), { recursive: true });
|
|
4737
|
+
import_fs10.default.writeFileSync(anchorsPath(cm.domain), JSON.stringify(cm, null, 2), "utf8");
|
|
4738
|
+
}
|
|
4739
|
+
function recordAnchorResult(domain, name, ok, now) {
|
|
4740
|
+
try {
|
|
4741
|
+
const cm = loadComponentMap(domain);
|
|
4742
|
+
const a = cm.anchors[name];
|
|
4743
|
+
if (!a)
|
|
4744
|
+
return;
|
|
4745
|
+
if (ok) {
|
|
4746
|
+
a.uses += 1;
|
|
4747
|
+
a.lastVerified = now;
|
|
4748
|
+
} else {
|
|
4749
|
+
a.fails += 1;
|
|
4750
|
+
}
|
|
4751
|
+
write(cm);
|
|
4752
|
+
} catch {}
|
|
4753
|
+
}
|
|
4415
4754
|
|
|
4416
4755
|
// src/lib/pipeline.ts
|
|
4417
4756
|
var DEFAULT_STALE_TIMEOUT_MS2 = 20000;
|
|
4757
|
+
function safePageUrl(page) {
|
|
4758
|
+
try {
|
|
4759
|
+
return page.url();
|
|
4760
|
+
} catch {
|
|
4761
|
+
return "";
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4418
4764
|
function classifyError(err, step) {
|
|
4419
4765
|
if (err instanceof StaleStateError)
|
|
4420
4766
|
return "stale-state";
|
|
@@ -4457,7 +4803,15 @@ class PipelineRunner {
|
|
|
4457
4803
|
constructor(ctx) {
|
|
4458
4804
|
this.ctx = ctx;
|
|
4459
4805
|
}
|
|
4460
|
-
async run(
|
|
4806
|
+
async run(initialPage, pipeline) {
|
|
4807
|
+
const tracker = new TabTracker(initialPage.context(), initialPage);
|
|
4808
|
+
try {
|
|
4809
|
+
return await this.runSteps(initialPage, tracker, pipeline);
|
|
4810
|
+
} finally {
|
|
4811
|
+
tracker.dispose();
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
async runSteps(initialPage, tracker, pipeline) {
|
|
4461
4815
|
const startTime = Date.now();
|
|
4462
4816
|
const opts = pipeline.options || {};
|
|
4463
4817
|
const staleTimeoutMs = opts.staleTimeoutMs ?? this.ctx.staleTimeoutMs ?? DEFAULT_STALE_TIMEOUT_MS2;
|
|
@@ -4466,7 +4820,20 @@ class PipelineRunner {
|
|
|
4466
4820
|
const continueOnError = opts.continueOnError ?? false;
|
|
4467
4821
|
const results = [];
|
|
4468
4822
|
const obstacles = [];
|
|
4469
|
-
const
|
|
4823
|
+
const navStep = pipeline.steps.find((s) => s.type === "navigate");
|
|
4824
|
+
try {
|
|
4825
|
+
const host = new URL(navStep?.url || safePageUrl(initialPage) || "http://x").hostname;
|
|
4826
|
+
if (host && host !== "x") {
|
|
4827
|
+
this.ctx.anchors = loadAnchors(host);
|
|
4828
|
+
this.ctx.anchorDomain = host;
|
|
4829
|
+
}
|
|
4830
|
+
} catch {}
|
|
4831
|
+
const recordAnchor = (step, ok) => {
|
|
4832
|
+
const name = anchorNameOf(step.selector);
|
|
4833
|
+
if (name && this.ctx.anchorDomain)
|
|
4834
|
+
recordAnchorResult(this.ctx.anchorDomain, name, ok, new Date().toISOString());
|
|
4835
|
+
};
|
|
4836
|
+
const capture = opts.captureApi ? new ApiCapture(initialPage) : null;
|
|
4470
4837
|
if (capture)
|
|
4471
4838
|
capture.start();
|
|
4472
4839
|
const finishCapture = async () => {
|
|
@@ -4479,6 +4846,8 @@ class PipelineRunner {
|
|
|
4479
4846
|
if (capture)
|
|
4480
4847
|
capture.setStep(i);
|
|
4481
4848
|
const step = pipeline.steps[i];
|
|
4849
|
+
const page = tracker.active();
|
|
4850
|
+
const urlBefore = safePageUrl(page);
|
|
4482
4851
|
const monitor = new StaleStateMonitor(page, staleTimeoutMs);
|
|
4483
4852
|
let stepResult;
|
|
4484
4853
|
try {
|
|
@@ -4490,9 +4859,10 @@ class PipelineRunner {
|
|
|
4490
4859
|
} catch (err) {
|
|
4491
4860
|
const asError = err instanceof Error ? err : new Error(String(err));
|
|
4492
4861
|
const errorType = classifyError(asError, step);
|
|
4493
|
-
const pageState = await capturePageState(
|
|
4862
|
+
const pageState = await capturePageState(tracker.active(), this.ctx, { screenshot: true });
|
|
4494
4863
|
stepResult = failedStepResult(step, asError.message, Date.now() - startTime, i);
|
|
4495
4864
|
results.push(stepResult);
|
|
4865
|
+
recordAnchor(step, false);
|
|
4496
4866
|
return {
|
|
4497
4867
|
ok: false,
|
|
4498
4868
|
completedSteps: i,
|
|
@@ -4513,15 +4883,29 @@ class PipelineRunner {
|
|
|
4513
4883
|
capturedApi: await finishCapture()
|
|
4514
4884
|
};
|
|
4515
4885
|
}
|
|
4886
|
+
recordAnchor(step, true);
|
|
4887
|
+
const canOpenTab = step.type === "click" || step.type === "human-click";
|
|
4888
|
+
const currentPageNavigated = safePageUrl(page) !== urlBefore;
|
|
4889
|
+
if (canOpenTab && !currentPageNavigated) {
|
|
4890
|
+
const switched = await tracker.settle({
|
|
4891
|
+
waitForPendingMs: TIMEOUTS.TAB_FOLLOW_SETTLE,
|
|
4892
|
+
loadTimeoutMs: TIMEOUTS.TAB_LOAD,
|
|
4893
|
+
blankResolveMs: TIMEOUTS.TAB_BLANK_RESOLVE
|
|
4894
|
+
});
|
|
4895
|
+
if (switched)
|
|
4896
|
+
stepResult.tabSwitchedTo = switched.url;
|
|
4897
|
+
} else {
|
|
4898
|
+
tracker.discardPending();
|
|
4899
|
+
}
|
|
4516
4900
|
if (screenshotAfterEach && stepResult.ok) {
|
|
4517
4901
|
try {
|
|
4518
|
-
const buf = await
|
|
4902
|
+
const buf = await tracker.active().screenshot({ type: "jpeg", quality: 50, fullPage: false });
|
|
4519
4903
|
stepResult.screenshotUrl = saveScreenshot(buf, `step-${i}-${Date.now()}.jpg`, this.ctx.screenshotDir, this.ctx.publicUrl);
|
|
4520
4904
|
} catch {}
|
|
4521
4905
|
}
|
|
4522
4906
|
results.push(stepResult);
|
|
4523
4907
|
if (!stepResult.ok && !continueOnError) {
|
|
4524
|
-
const pageState = await capturePageState(
|
|
4908
|
+
const pageState = await capturePageState(tracker.active(), this.ctx, { screenshot: true });
|
|
4525
4909
|
const errorType = classifyError(new Error(stepResult.error || ""), step);
|
|
4526
4910
|
return {
|
|
4527
4911
|
ok: false,
|
|
@@ -4545,9 +4929,10 @@ class PipelineRunner {
|
|
|
4545
4929
|
}
|
|
4546
4930
|
if (step.type === "navigate" && continueOnObstacle) {
|
|
4547
4931
|
const obstacleStart = Date.now();
|
|
4548
|
-
const
|
|
4932
|
+
const obstaclePage = tracker.active();
|
|
4933
|
+
const obstacle = await detectObstacles(obstaclePage);
|
|
4549
4934
|
if (obstacle) {
|
|
4550
|
-
const resolution = await resolveObstacle(
|
|
4935
|
+
const resolution = await resolveObstacle(obstaclePage, obstacle, this.ctx, monitor);
|
|
4551
4936
|
obstacles.push({
|
|
4552
4937
|
type: obstacle.type,
|
|
4553
4938
|
detectedAtStep: i,
|
|
@@ -4556,7 +4941,7 @@ class PipelineRunner {
|
|
|
4556
4941
|
durationMs: Date.now() - obstacleStart
|
|
4557
4942
|
});
|
|
4558
4943
|
if (!resolution.resolved && obstacle.type === "captcha") {
|
|
4559
|
-
const pageState = await capturePageState(
|
|
4944
|
+
const pageState = await capturePageState(tracker.active(), this.ctx, { screenshot: true });
|
|
4560
4945
|
return {
|
|
4561
4946
|
ok: false,
|
|
4562
4947
|
completedSteps: i,
|
|
@@ -4581,7 +4966,7 @@ class PipelineRunner {
|
|
|
4581
4966
|
}
|
|
4582
4967
|
}
|
|
4583
4968
|
const capturedApi = await finishCapture();
|
|
4584
|
-
const finalState = await capturePageState(
|
|
4969
|
+
const finalState = await capturePageState(tracker.active(), this.ctx, { screenshot: true });
|
|
4585
4970
|
return {
|
|
4586
4971
|
ok: true,
|
|
4587
4972
|
completedSteps: pipeline.steps.length,
|
|
@@ -4595,7 +4980,7 @@ class PipelineRunner {
|
|
|
4595
4980
|
}
|
|
4596
4981
|
}
|
|
4597
4982
|
// src/lib/block-detection.ts
|
|
4598
|
-
var
|
|
4983
|
+
var log16 = createLogger("block-detection");
|
|
4599
4984
|
async function detectBlock(page) {
|
|
4600
4985
|
try {
|
|
4601
4986
|
const [title, url, bodyText] = await Promise.all([
|
|
@@ -4615,7 +5000,7 @@ async function detectBlock(page) {
|
|
|
4615
5000
|
const hasCfChallenge = await page.evaluate(() => {
|
|
4616
5001
|
return !!document.querySelector('iframe[src*="challenges.cloudflare.com"]');
|
|
4617
5002
|
}).catch((err) => {
|
|
4618
|
-
|
|
5003
|
+
log16.warn(`CF challenge check failed, assuming blocked: ${err}`);
|
|
4619
5004
|
return true;
|
|
4620
5005
|
});
|
|
4621
5006
|
if (hasCfChallenge) {
|
|
@@ -4634,7 +5019,7 @@ async function detectBlock(page) {
|
|
|
4634
5019
|
const hasCaptchaIframe = await page.evaluate(() => {
|
|
4635
5020
|
return !!(document.querySelector('iframe[src*="recaptcha"]') || document.querySelector('iframe[src*="hcaptcha"]'));
|
|
4636
5021
|
}).catch((err) => {
|
|
4637
|
-
|
|
5022
|
+
log16.warn(`captcha iframe check failed, assuming blocked: ${err}`);
|
|
4638
5023
|
return true;
|
|
4639
5024
|
});
|
|
4640
5025
|
if (hasCaptchaIframe) {
|
|
@@ -4647,7 +5032,7 @@ async function detectBlock(page) {
|
|
|
4647
5032
|
if (!url.includes("about:blank") && bodyText.trim().length < 20 && title.length < 5) {}
|
|
4648
5033
|
return { blocked: false };
|
|
4649
5034
|
} catch (err) {
|
|
4650
|
-
|
|
5035
|
+
log16.warn(`page evaluation failed, assuming blocked: ${err}`);
|
|
4651
5036
|
return { blocked: true, reason: "evaluation-failed" };
|
|
4652
5037
|
}
|
|
4653
5038
|
}
|
|
@@ -4744,12 +5129,583 @@ function extractKnowledgeFromRun(pipeline, result, sessionData, mode) {
|
|
|
4744
5129
|
});
|
|
4745
5130
|
}
|
|
4746
5131
|
|
|
5132
|
+
// src/lib/extension/cdp-relay.ts
|
|
5133
|
+
var import_http = __toESM(require("http"));
|
|
5134
|
+
var import_ws2 = require("ws");
|
|
5135
|
+
var import_crypto7 = require("crypto");
|
|
5136
|
+
|
|
5137
|
+
// src/lib/extension/bridge.ts
|
|
5138
|
+
var import_ws = require("ws");
|
|
5139
|
+
var import_crypto5 = __toESM(require("crypto"));
|
|
5140
|
+
|
|
5141
|
+
// src/lib/version.ts
|
|
5142
|
+
var import_fs11 = __toESM(require("fs"));
|
|
5143
|
+
var import_path10 = __toESM(require("path"));
|
|
5144
|
+
var __dirname = "/Users/eduardoverona/tools/iframer-toolkit/src/lib";
|
|
5145
|
+
var cached = null;
|
|
5146
|
+
function getVersion() {
|
|
5147
|
+
if (cached)
|
|
5148
|
+
return cached;
|
|
5149
|
+
if (process.env.IFRAMER_VERSION)
|
|
5150
|
+
return cached = process.env.IFRAMER_VERSION;
|
|
5151
|
+
const candidates = [
|
|
5152
|
+
import_path10.default.join(__dirname, "..", "..", "package.json"),
|
|
5153
|
+
import_path10.default.join(__dirname, "..", "package.json"),
|
|
5154
|
+
import_path10.default.join(process.cwd(), "package.json")
|
|
5155
|
+
];
|
|
5156
|
+
for (const p of candidates) {
|
|
5157
|
+
try {
|
|
5158
|
+
const v = JSON.parse(import_fs11.default.readFileSync(p, "utf8")).version;
|
|
5159
|
+
if (v)
|
|
5160
|
+
return cached = v;
|
|
5161
|
+
} catch {}
|
|
5162
|
+
}
|
|
5163
|
+
return cached = "0.0.0";
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
// src/lib/extension/bridge.ts
|
|
5167
|
+
var REQUEST_TIMEOUT_MS = 180000;
|
|
5168
|
+
var HEARTBEAT_MS = 15000;
|
|
5169
|
+
|
|
5170
|
+
class ExtensionBridge {
|
|
5171
|
+
wss = null;
|
|
5172
|
+
clients = new Map;
|
|
5173
|
+
pending = new Map;
|
|
5174
|
+
nextReqId = 1;
|
|
5175
|
+
tabOwner = new Map;
|
|
5176
|
+
collidingTabs = new Set;
|
|
5177
|
+
cdpListeners = new Map;
|
|
5178
|
+
attach(server) {
|
|
5179
|
+
if (this.wss)
|
|
5180
|
+
return;
|
|
5181
|
+
this.wss = new import_ws.WebSocketServer({ server, path: "/extension/ws" });
|
|
5182
|
+
this.wss.on("connection", (ws, req) => {
|
|
5183
|
+
let expected = "";
|
|
5184
|
+
try {
|
|
5185
|
+
expected = getLocalToken();
|
|
5186
|
+
} catch {
|
|
5187
|
+
expected = "";
|
|
5188
|
+
}
|
|
5189
|
+
if (!expected) {
|
|
5190
|
+
ws.close(4001, "unauthorized");
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
5193
|
+
const queryToken = new URL(req.url || "", "http://127.0.0.1").searchParams.get("token");
|
|
5194
|
+
if (queryToken !== null) {
|
|
5195
|
+
if (queryToken !== expected) {
|
|
5196
|
+
ws.close(4001, "unauthorized");
|
|
5197
|
+
return;
|
|
5198
|
+
}
|
|
5199
|
+
this.acceptClient(ws);
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
5202
|
+
const timer = setTimeout(() => ws.close(4001, "auth timeout"), 3000);
|
|
5203
|
+
ws.once("message", (data) => {
|
|
5204
|
+
clearTimeout(timer);
|
|
5205
|
+
try {
|
|
5206
|
+
const m = JSON.parse(data.toString());
|
|
5207
|
+
if (m?.type === "auth" && m.token === expected) {
|
|
5208
|
+
this.acceptClient(ws);
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
} catch {}
|
|
5212
|
+
ws.close(4001, "unauthorized");
|
|
5213
|
+
});
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
acceptClient(ws) {
|
|
5217
|
+
const client = {
|
|
5218
|
+
clientId: import_crypto5.default.randomUUID(),
|
|
5219
|
+
socket: ws,
|
|
5220
|
+
connectedAt: Date.now(),
|
|
5221
|
+
tabs: [],
|
|
5222
|
+
heartbeat: null
|
|
5223
|
+
};
|
|
5224
|
+
this.clients.set(client.clientId, client);
|
|
5225
|
+
this.startHeartbeat(client);
|
|
5226
|
+
try {
|
|
5227
|
+
ws.send(JSON.stringify({ type: "server_info", version: getVersion() }));
|
|
5228
|
+
} catch {}
|
|
5229
|
+
ws.on("message", (data) => this.onMessage(client, data));
|
|
5230
|
+
ws.on("close", () => this.dropClient(client, "socket closed"));
|
|
5231
|
+
ws.on("error", () => {});
|
|
5232
|
+
}
|
|
5233
|
+
startHeartbeat(client) {
|
|
5234
|
+
client.heartbeat = setInterval(() => {
|
|
5235
|
+
this.send(client, "ping", {}).catch(() => {});
|
|
5236
|
+
}, HEARTBEAT_MS);
|
|
5237
|
+
client.heartbeat.unref?.();
|
|
5238
|
+
}
|
|
5239
|
+
dropClient(client, _reason) {
|
|
5240
|
+
if (client.heartbeat)
|
|
5241
|
+
clearInterval(client.heartbeat);
|
|
5242
|
+
if (this.clients.get(client.clientId) === client) {
|
|
5243
|
+
this.clients.delete(client.clientId);
|
|
5244
|
+
}
|
|
5245
|
+
for (const [tabId, owner] of this.tabOwner) {
|
|
5246
|
+
if (owner === client.clientId)
|
|
5247
|
+
this.tabOwner.delete(tabId);
|
|
5248
|
+
}
|
|
5249
|
+
for (const [id, p] of this.pending) {
|
|
5250
|
+
if (p.clientId === client.clientId) {
|
|
5251
|
+
clearTimeout(p.timer);
|
|
5252
|
+
p.reject(new Error("Extension disconnected before responding."));
|
|
5253
|
+
this.pending.delete(id);
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
onMessage(client, data) {
|
|
5258
|
+
let msg;
|
|
5259
|
+
try {
|
|
5260
|
+
msg = JSON.parse(data.toString());
|
|
5261
|
+
} catch {
|
|
5262
|
+
return;
|
|
5263
|
+
}
|
|
5264
|
+
if (msg.type === "hello") {
|
|
5265
|
+
client.profileId = msg.profileId;
|
|
5266
|
+
client.profileName = msg.profileName;
|
|
5267
|
+
client.extVersion = msg.extVersion;
|
|
5268
|
+
if (msg.profileId) {
|
|
5269
|
+
for (const other of this.clients.values()) {
|
|
5270
|
+
if (other !== client && other.profileId === msg.profileId) {
|
|
5271
|
+
try {
|
|
5272
|
+
other.socket.close(4002, "replaced by same profile reconnect");
|
|
5273
|
+
} catch {}
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5279
|
+
if (msg.type === "cdp_event") {
|
|
5280
|
+
const ev = msg;
|
|
5281
|
+
if (typeof ev.tabId === "number") {
|
|
5282
|
+
const fn = this.cdpListeners.get(cdpKey(client.clientId, ev.tabId));
|
|
5283
|
+
if (fn)
|
|
5284
|
+
fn(ev);
|
|
5285
|
+
}
|
|
5286
|
+
return;
|
|
5287
|
+
}
|
|
5288
|
+
if (typeof msg.id !== "number")
|
|
5289
|
+
return;
|
|
5290
|
+
const p = this.pending.get(msg.id);
|
|
5291
|
+
if (!p || p.clientId !== client.clientId)
|
|
5292
|
+
return;
|
|
5293
|
+
this.pending.delete(msg.id);
|
|
5294
|
+
clearTimeout(p.timer);
|
|
5295
|
+
if (msg.ok)
|
|
5296
|
+
p.resolve(msg.result);
|
|
5297
|
+
else
|
|
5298
|
+
p.reject(new Error(msg.error || "Extension reported an error."));
|
|
5299
|
+
}
|
|
5300
|
+
send(client, type, payload) {
|
|
5301
|
+
const ws = client.socket;
|
|
5302
|
+
if (!ws || ws.readyState !== import_ws.WebSocket.OPEN) {
|
|
5303
|
+
return Promise.reject(new Error("Extension client is not connected."));
|
|
5304
|
+
}
|
|
5305
|
+
const id = this.nextReqId++;
|
|
5306
|
+
return new Promise((resolve, reject) => {
|
|
5307
|
+
const timer = setTimeout(() => {
|
|
5308
|
+
this.pending.delete(id);
|
|
5309
|
+
reject(new Error(`Extension did not respond within ${REQUEST_TIMEOUT_MS}ms (${type}).`));
|
|
5310
|
+
}, REQUEST_TIMEOUT_MS);
|
|
5311
|
+
this.pending.set(id, { clientId: client.clientId, resolve, reject, timer });
|
|
5312
|
+
try {
|
|
5313
|
+
ws.send(JSON.stringify({ id, type, ...payload }));
|
|
5314
|
+
} catch (e) {
|
|
5315
|
+
clearTimeout(timer);
|
|
5316
|
+
this.pending.delete(id);
|
|
5317
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
5318
|
+
}
|
|
5319
|
+
});
|
|
5320
|
+
}
|
|
5321
|
+
hasClients() {
|
|
5322
|
+
return this.clients.size > 0;
|
|
5323
|
+
}
|
|
5324
|
+
status() {
|
|
5325
|
+
return {
|
|
5326
|
+
connected: this.clients.size > 0,
|
|
5327
|
+
clients: [...this.clients.values()].map((c) => ({
|
|
5328
|
+
clientId: c.clientId,
|
|
5329
|
+
profileId: c.profileId,
|
|
5330
|
+
profileName: c.profileName,
|
|
5331
|
+
extVersion: c.extVersion,
|
|
5332
|
+
connectedAt: new Date(c.connectedAt).toISOString(),
|
|
5333
|
+
tabCount: c.tabs.length
|
|
5334
|
+
}))
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
async listTabs() {
|
|
5338
|
+
const all = [];
|
|
5339
|
+
this.tabOwner.clear();
|
|
5340
|
+
this.collidingTabs.clear();
|
|
5341
|
+
await Promise.all([...this.clients.values()].map(async (client) => {
|
|
5342
|
+
try {
|
|
5343
|
+
const res = await this.send(client, "list_tabs", {}) || { tabs: [] };
|
|
5344
|
+
const tagged = (res.tabs || []).map((t) => ({
|
|
5345
|
+
...t,
|
|
5346
|
+
clientId: client.clientId,
|
|
5347
|
+
profileId: client.profileId,
|
|
5348
|
+
profileName: client.profileName
|
|
5349
|
+
}));
|
|
5350
|
+
client.tabs = tagged;
|
|
5351
|
+
for (const t of tagged) {
|
|
5352
|
+
const prev = this.tabOwner.get(t.id);
|
|
5353
|
+
if (prev !== undefined && prev !== client.clientId)
|
|
5354
|
+
this.collidingTabs.add(t.id);
|
|
5355
|
+
this.tabOwner.set(t.id, client.clientId);
|
|
5356
|
+
}
|
|
5357
|
+
all.push(...tagged);
|
|
5358
|
+
} catch {
|
|
5359
|
+
client.tabs = [];
|
|
5360
|
+
}
|
|
5361
|
+
}));
|
|
5362
|
+
return { tabs: all, clients: this.status().clients };
|
|
5363
|
+
}
|
|
5364
|
+
async resolveClient(tabId, clientId) {
|
|
5365
|
+
if (clientId) {
|
|
5366
|
+
const c = this.clients.get(clientId);
|
|
5367
|
+
if (!c)
|
|
5368
|
+
throw new Error(`No connected extension with clientId ${clientId}.`);
|
|
5369
|
+
return c;
|
|
5370
|
+
}
|
|
5371
|
+
if (this.clients.size === 0) {
|
|
5372
|
+
throw new Error("No iframer extension is connected. Open Chrome, install/enable the iframer " + "extension, and pair it (paste the token, dot goes green).");
|
|
5373
|
+
}
|
|
5374
|
+
if (this.clients.size === 1) {
|
|
5375
|
+
return [...this.clients.values()][0];
|
|
5376
|
+
}
|
|
5377
|
+
let owner = this.tabOwner.get(tabId);
|
|
5378
|
+
if (!owner || this.collidingTabs.has(tabId)) {
|
|
5379
|
+
await this.listTabs();
|
|
5380
|
+
owner = this.tabOwner.get(tabId);
|
|
5381
|
+
}
|
|
5382
|
+
if (this.collidingTabs.has(tabId)) {
|
|
5383
|
+
throw new Error(`Tab id ${tabId} exists in more than one connected browser (separate browsers ` + `have independent tab-id spaces). Call \`tabs\` and pass the tab's clientId ` + `alongside tabId to pick the right one.`);
|
|
5384
|
+
}
|
|
5385
|
+
if (owner) {
|
|
5386
|
+
const c = this.clients.get(owner);
|
|
5387
|
+
if (c)
|
|
5388
|
+
return c;
|
|
5389
|
+
}
|
|
5390
|
+
throw new Error(`Could not determine which browser profile owns tab ${tabId}. Call \`tabs\` to ` + `refresh the list, then pass the tab's clientId alongside tabId.`);
|
|
5391
|
+
}
|
|
5392
|
+
resolveClientNoTab(clientId) {
|
|
5393
|
+
if (clientId) {
|
|
5394
|
+
const c = this.clients.get(clientId);
|
|
5395
|
+
if (!c)
|
|
5396
|
+
throw new Error(`No connected extension with clientId ${clientId}.`);
|
|
5397
|
+
return c;
|
|
5398
|
+
}
|
|
5399
|
+
if (this.clients.size === 0) {
|
|
5400
|
+
throw new Error("No iframer extension is connected. Open Chrome, install/enable the iframer " + "extension, and pair it (paste the token, dot goes green).");
|
|
5401
|
+
}
|
|
5402
|
+
if (this.clients.size === 1)
|
|
5403
|
+
return [...this.clients.values()][0];
|
|
5404
|
+
throw new Error("Multiple browser profiles are connected — pass clientId to say which one to act in. " + "Call `tabs` to see the profiles and their clientIds.");
|
|
5405
|
+
}
|
|
5406
|
+
async reloadAll() {
|
|
5407
|
+
const clients = [...this.clients.values()];
|
|
5408
|
+
await Promise.all(clients.map((c) => this.send(c, "reload", {}).catch(() => {})));
|
|
5409
|
+
return { reloaded: clients.length };
|
|
5410
|
+
}
|
|
5411
|
+
async groupTabs(tabIds, opts = {}, clientId) {
|
|
5412
|
+
const client = clientId ? this.resolveClientNoTab(clientId) : await this.resolveClient(tabIds[0]);
|
|
5413
|
+
return this.send(client, "group_tabs", {
|
|
5414
|
+
tabIds,
|
|
5415
|
+
title: opts.title,
|
|
5416
|
+
color: opts.color,
|
|
5417
|
+
collapsed: opts.collapsed,
|
|
5418
|
+
groupId: opts.groupId
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
async ungroupTabs(tabIds, clientId) {
|
|
5422
|
+
const client = clientId ? this.resolveClientNoTab(clientId) : await this.resolveClient(tabIds[0]);
|
|
5423
|
+
return this.send(client, "ungroup_tabs", { tabIds });
|
|
5424
|
+
}
|
|
5425
|
+
async updateGroup(groupId, opts, clientId) {
|
|
5426
|
+
const client = this.resolveClientNoTab(clientId);
|
|
5427
|
+
return this.send(client, "update_group", { groupId, ...opts });
|
|
5428
|
+
}
|
|
5429
|
+
async listGroups(clientId) {
|
|
5430
|
+
const client = this.resolveClientNoTab(clientId);
|
|
5431
|
+
return this.send(client, "list_groups", {});
|
|
5432
|
+
}
|
|
5433
|
+
async createTab(url, opts = {}, clientId) {
|
|
5434
|
+
const client = this.resolveClientNoTab(clientId);
|
|
5435
|
+
const res = await this.send(client, "create_tab", {
|
|
5436
|
+
url,
|
|
5437
|
+
active: opts.active,
|
|
5438
|
+
windowId: opts.windowId
|
|
5439
|
+
});
|
|
5440
|
+
return { tab: { ...res.tab, clientId: client.clientId, profileId: client.profileId, profileName: client.profileName }, clientId: client.clientId };
|
|
5441
|
+
}
|
|
5442
|
+
addCdpListener(clientId, tabId, fn) {
|
|
5443
|
+
const key = cdpKey(clientId, tabId);
|
|
5444
|
+
if (this.cdpListeners.has(key)) {
|
|
5445
|
+
throw new Error(`Tab ${tabId} is already being driven by another pipeline. Retry when it finishes.`);
|
|
5446
|
+
}
|
|
5447
|
+
this.cdpListeners.set(key, fn);
|
|
5448
|
+
}
|
|
5449
|
+
removeCdpListener(clientId, tabId) {
|
|
5450
|
+
this.cdpListeners.delete(cdpKey(clientId, tabId));
|
|
5451
|
+
}
|
|
5452
|
+
async cdpAttach(tabId, clientId, focus) {
|
|
5453
|
+
const client = await this.resolveClient(tabId, clientId);
|
|
5454
|
+
const res = await this.send(client, "cdp_attach", { tabId, focus: !!focus }) || {
|
|
5455
|
+
targetInfo: null
|
|
5456
|
+
};
|
|
5457
|
+
return { targetInfo: res.targetInfo, clientId: client.clientId };
|
|
5458
|
+
}
|
|
5459
|
+
async cdpCommand(clientId, tabId, sessionId, method, params) {
|
|
5460
|
+
const client = this.clients.get(clientId);
|
|
5461
|
+
if (!client)
|
|
5462
|
+
throw new Error(`CDP: client ${clientId} is gone.`);
|
|
5463
|
+
return this.send(client, "cdp_command", { tabId, sessionId, method, params });
|
|
5464
|
+
}
|
|
5465
|
+
async cdpDetach(clientId, tabId) {
|
|
5466
|
+
const client = this.clients.get(clientId);
|
|
5467
|
+
if (!client)
|
|
5468
|
+
return;
|
|
5469
|
+
try {
|
|
5470
|
+
await this.send(client, "cdp_detach", { tabId });
|
|
5471
|
+
} catch {}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
function cdpKey(clientId, tabId) {
|
|
5475
|
+
return `${clientId}:${tabId}`;
|
|
5476
|
+
}
|
|
5477
|
+
var extensionBridge = new ExtensionBridge;
|
|
5478
|
+
|
|
5479
|
+
// src/lib/extension/cdp-relay.ts
|
|
5480
|
+
var log17 = createLogger("cdp-relay");
|
|
5481
|
+
|
|
5482
|
+
class CdpRelay {
|
|
5483
|
+
tabId;
|
|
5484
|
+
clientId;
|
|
5485
|
+
focus;
|
|
5486
|
+
httpServer = null;
|
|
5487
|
+
wss = null;
|
|
5488
|
+
pw = null;
|
|
5489
|
+
port = 0;
|
|
5490
|
+
path = `/cdp/${import_crypto7.randomUUID()}`;
|
|
5491
|
+
tabSessionId = "pw-tab-1";
|
|
5492
|
+
targetInfo = null;
|
|
5493
|
+
ownerClientId = "";
|
|
5494
|
+
listenerRegistered = false;
|
|
5495
|
+
constructor(tabId, clientId, focus) {
|
|
5496
|
+
this.tabId = tabId;
|
|
5497
|
+
this.clientId = clientId;
|
|
5498
|
+
this.focus = focus;
|
|
5499
|
+
}
|
|
5500
|
+
async start() {
|
|
5501
|
+
const { targetInfo, clientId } = await extensionBridge.cdpAttach(this.tabId, this.clientId, this.focus);
|
|
5502
|
+
this.ownerClientId = clientId;
|
|
5503
|
+
this.targetInfo = targetInfo || {
|
|
5504
|
+
targetId: `iframer-${this.tabId}`,
|
|
5505
|
+
type: "page",
|
|
5506
|
+
title: "",
|
|
5507
|
+
url: ""
|
|
5508
|
+
};
|
|
5509
|
+
extensionBridge.addCdpListener(this.ownerClientId, this.tabId, (ev) => {
|
|
5510
|
+
this.sendToPw({
|
|
5511
|
+
method: ev.method,
|
|
5512
|
+
params: ev.params,
|
|
5513
|
+
sessionId: ev.sessionId || this.tabSessionId
|
|
5514
|
+
});
|
|
5515
|
+
});
|
|
5516
|
+
this.listenerRegistered = true;
|
|
5517
|
+
await new Promise((resolve, reject) => {
|
|
5518
|
+
this.httpServer = import_http.default.createServer((req, res) => {
|
|
5519
|
+
if (req.url === "/json/version" || req.url === "/json/version/") {
|
|
5520
|
+
res.setHeader("content-type", "application/json");
|
|
5521
|
+
res.end(JSON.stringify({
|
|
5522
|
+
Browser: "Chrome/iframer-extension",
|
|
5523
|
+
"Protocol-Version": "1.3",
|
|
5524
|
+
"User-Agent": "iframer-cdp-relay/1.0",
|
|
5525
|
+
"V8-Version": "",
|
|
5526
|
+
"WebKit-Version": "",
|
|
5527
|
+
webSocketDebuggerUrl: `ws://127.0.0.1:${this.port}${this.path}`
|
|
5528
|
+
}));
|
|
5529
|
+
return;
|
|
5530
|
+
}
|
|
5531
|
+
res.writeHead(404);
|
|
5532
|
+
res.end();
|
|
5533
|
+
});
|
|
5534
|
+
this.httpServer.on("upgrade", (req) => {
|
|
5535
|
+
if (process.env.IFRAMER_RELAY_DEBUG)
|
|
5536
|
+
log17.info(`[relay] upgrade request url=${req.url}`);
|
|
5537
|
+
});
|
|
5538
|
+
this.wss = new import_ws2.WebSocketServer({ server: this.httpServer, path: this.path });
|
|
5539
|
+
this.wss.on("connection", (ws) => {
|
|
5540
|
+
if (process.env.IFRAMER_RELAY_DEBUG)
|
|
5541
|
+
log17.info(`[relay] playwright connected`);
|
|
5542
|
+
if (this.pw) {
|
|
5543
|
+
ws.close(4000, "relay already has a client");
|
|
5544
|
+
return;
|
|
5545
|
+
}
|
|
5546
|
+
this.pw = ws;
|
|
5547
|
+
ws.on("message", (data) => this.onPwMessage(data));
|
|
5548
|
+
ws.on("close", () => {
|
|
5549
|
+
if (this.pw === ws)
|
|
5550
|
+
this.pw = null;
|
|
5551
|
+
});
|
|
5552
|
+
ws.on("error", () => {});
|
|
5553
|
+
});
|
|
5554
|
+
this.httpServer.on("error", reject);
|
|
5555
|
+
this.httpServer.listen(0, "127.0.0.1", () => {
|
|
5556
|
+
const addr = this.httpServer.address();
|
|
5557
|
+
this.port = typeof addr === "object" && addr ? addr.port : 0;
|
|
5558
|
+
resolve();
|
|
5559
|
+
});
|
|
5560
|
+
});
|
|
5561
|
+
}
|
|
5562
|
+
cdpEndpoint() {
|
|
5563
|
+
return `ws://127.0.0.1:${this.port}${this.path}`;
|
|
5564
|
+
}
|
|
5565
|
+
httpEndpoint() {
|
|
5566
|
+
return `http://127.0.0.1:${this.port}`;
|
|
5567
|
+
}
|
|
5568
|
+
sendToPw(msg) {
|
|
5569
|
+
if (this.pw && this.pw.readyState === import_ws2.WebSocket.OPEN) {
|
|
5570
|
+
try {
|
|
5571
|
+
this.pw.send(JSON.stringify(msg));
|
|
5572
|
+
} catch {}
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
async onPwMessage(data) {
|
|
5576
|
+
if (process.env.IFRAMER_RELAY_DEBUG)
|
|
5577
|
+
log17.info(`[relay] raw pw msg (${data?.length ?? 0} bytes): ${data?.toString().slice(0, 120)}`);
|
|
5578
|
+
let msg;
|
|
5579
|
+
try {
|
|
5580
|
+
msg = JSON.parse(data.toString());
|
|
5581
|
+
} catch {
|
|
5582
|
+
return;
|
|
5583
|
+
}
|
|
5584
|
+
const { id, sessionId, method, params } = msg;
|
|
5585
|
+
if (process.env.IFRAMER_RELAY_DEBUG)
|
|
5586
|
+
log17.info(`[relay] pw→ ${method} (id=${id}, sess=${sessionId || "-"})`);
|
|
5587
|
+
if (!method)
|
|
5588
|
+
return;
|
|
5589
|
+
try {
|
|
5590
|
+
const result = await this.handleCdpCommand(method, params, sessionId);
|
|
5591
|
+
if (typeof id === "number")
|
|
5592
|
+
this.sendToPw({ id, sessionId, result });
|
|
5593
|
+
} catch (e) {
|
|
5594
|
+
if (typeof id === "number") {
|
|
5595
|
+
this.sendToPw({ id, sessionId, error: { message: e instanceof Error ? e.message : String(e) } });
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
async handleCdpCommand(method, params, sessionId) {
|
|
5600
|
+
switch (method) {
|
|
5601
|
+
case "Browser.getVersion":
|
|
5602
|
+
return { protocolVersion: "1.3", product: "Chrome/iframer-extension", userAgent: "iframer-cdp-relay/1.0" };
|
|
5603
|
+
case "Browser.setDownloadBehavior":
|
|
5604
|
+
return {};
|
|
5605
|
+
case "Browser.close":
|
|
5606
|
+
return {};
|
|
5607
|
+
case "Target.setDiscoverTargets":
|
|
5608
|
+
return {};
|
|
5609
|
+
case "Target.getTargets":
|
|
5610
|
+
return { targetInfos: this.targetInfo ? [{ ...this.targetInfo, attached: true }] : [] };
|
|
5611
|
+
case "Target.setAutoAttach":
|
|
5612
|
+
if (!sessionId) {
|
|
5613
|
+
this.sendToPw({
|
|
5614
|
+
method: "Target.attachedToTarget",
|
|
5615
|
+
params: {
|
|
5616
|
+
sessionId: this.tabSessionId,
|
|
5617
|
+
targetInfo: { ...this.targetInfo, attached: true },
|
|
5618
|
+
waitingForDebugger: false
|
|
5619
|
+
}
|
|
5620
|
+
});
|
|
5621
|
+
return {};
|
|
5622
|
+
}
|
|
5623
|
+
break;
|
|
5624
|
+
case "Target.getTargetInfo":
|
|
5625
|
+
if (!sessionId)
|
|
5626
|
+
return { targetInfo: this.targetInfo };
|
|
5627
|
+
break;
|
|
5628
|
+
}
|
|
5629
|
+
const realSessionId = sessionId === this.tabSessionId ? undefined : sessionId;
|
|
5630
|
+
if (method === "Page.captureScreenshot") {
|
|
5631
|
+
return this.captureScreenshotWithFallback(params, realSessionId);
|
|
5632
|
+
}
|
|
5633
|
+
return extensionBridge.cdpCommand(this.ownerClientId, this.tabId, realSessionId, method, params);
|
|
5634
|
+
}
|
|
5635
|
+
async captureScreenshotWithFallback(params, sessionId) {
|
|
5636
|
+
const base = params && typeof params === "object" ? { ...params } : {};
|
|
5637
|
+
const attempt = (p) => extensionBridge.cdpCommand(this.ownerClientId, this.tabId, sessionId, "Page.captureScreenshot", p);
|
|
5638
|
+
const first = attempt(base);
|
|
5639
|
+
first.catch(() => {
|
|
5640
|
+
return;
|
|
5641
|
+
});
|
|
5642
|
+
try {
|
|
5643
|
+
return await Promise.race([
|
|
5644
|
+
first,
|
|
5645
|
+
new Promise((_, reject) => {
|
|
5646
|
+
const t = setTimeout(() => reject(new Error("screenshot timed out (no compositor frame)")), 1e4);
|
|
5647
|
+
t.unref?.();
|
|
5648
|
+
})
|
|
5649
|
+
]);
|
|
5650
|
+
} catch {
|
|
5651
|
+
return attempt({ ...base, fromSurface: false });
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
async stop() {
|
|
5655
|
+
const ownedTab = this.listenerRegistered;
|
|
5656
|
+
if (ownedTab) {
|
|
5657
|
+
extensionBridge.removeCdpListener(this.ownerClientId, this.tabId);
|
|
5658
|
+
this.listenerRegistered = false;
|
|
5659
|
+
}
|
|
5660
|
+
try {
|
|
5661
|
+
this.wss?.clients.forEach((c) => {
|
|
5662
|
+
try {
|
|
5663
|
+
c.terminate();
|
|
5664
|
+
} catch {}
|
|
5665
|
+
});
|
|
5666
|
+
} catch {}
|
|
5667
|
+
try {
|
|
5668
|
+
this.pw?.terminate();
|
|
5669
|
+
} catch {}
|
|
5670
|
+
this.pw = null;
|
|
5671
|
+
const withTimeout = (fn) => new Promise((resolve) => {
|
|
5672
|
+
let done = false;
|
|
5673
|
+
const finish = () => {
|
|
5674
|
+
if (!done) {
|
|
5675
|
+
done = true;
|
|
5676
|
+
resolve();
|
|
5677
|
+
}
|
|
5678
|
+
};
|
|
5679
|
+
try {
|
|
5680
|
+
fn(finish);
|
|
5681
|
+
} catch {
|
|
5682
|
+
finish();
|
|
5683
|
+
}
|
|
5684
|
+
setTimeout(finish, 1000).unref?.();
|
|
5685
|
+
});
|
|
5686
|
+
if (this.wss)
|
|
5687
|
+
await withTimeout((cb) => this.wss.close(cb));
|
|
5688
|
+
if (this.httpServer)
|
|
5689
|
+
await withTimeout((cb) => this.httpServer.close(cb));
|
|
5690
|
+
this.wss = null;
|
|
5691
|
+
this.httpServer = null;
|
|
5692
|
+
if (ownedTab) {
|
|
5693
|
+
try {
|
|
5694
|
+
await extensionBridge.cdpDetach(this.ownerClientId, this.tabId);
|
|
5695
|
+
} catch (e) {
|
|
5696
|
+
log17.warn(`cdp detach failed: ${e}`);
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
|
|
4747
5702
|
// src/lib/execution/pipeline-executor.ts
|
|
4748
|
-
var
|
|
5703
|
+
var log18 = createLogger("iframer");
|
|
4749
5704
|
|
|
4750
5705
|
class PipelineExecutor {
|
|
4751
5706
|
deps;
|
|
4752
5707
|
pendingElicitOtp;
|
|
5708
|
+
extensionTabLocks = new Map;
|
|
4753
5709
|
constructor(deps) {
|
|
4754
5710
|
this.deps = deps;
|
|
4755
5711
|
}
|
|
@@ -4763,6 +5719,22 @@ class PipelineExecutor {
|
|
|
4763
5719
|
}
|
|
4764
5720
|
async executeInner(userId, token, pipeline) {
|
|
4765
5721
|
const opts = pipeline.options || {};
|
|
5722
|
+
if (typeof opts.extensionTabId === "number") {
|
|
5723
|
+
const tabId = opts.extensionTabId;
|
|
5724
|
+
const lockKey = `${opts.clientId || "auto"}:${tabId}`;
|
|
5725
|
+
const prev = this.extensionTabLocks.get(lockKey);
|
|
5726
|
+
const run2 = (prev ? prev.catch(() => {
|
|
5727
|
+
return;
|
|
5728
|
+
}) : Promise.resolve()).then(() => this.executeExtension(userId, token, pipeline, tabId, opts.clientId));
|
|
5729
|
+
this.extensionTabLocks.set(lockKey, run2);
|
|
5730
|
+
run2.catch(() => {
|
|
5731
|
+
return;
|
|
5732
|
+
}).finally(() => {
|
|
5733
|
+
if (this.extensionTabLocks.get(lockKey) === run2)
|
|
5734
|
+
this.extensionTabLocks.delete(lockKey);
|
|
5735
|
+
});
|
|
5736
|
+
return run2;
|
|
5737
|
+
}
|
|
4766
5738
|
const forcedMode = opts.mode;
|
|
4767
5739
|
const autoEscalate = opts.autoEscalate !== false;
|
|
4768
5740
|
const instanceId = opts.instanceId || DEFAULT_INSTANCE;
|
|
@@ -4784,7 +5756,7 @@ class PipelineExecutor {
|
|
|
4784
5756
|
this.deps.domainModes.recordFailure(domain, failedMode, result.error?.message || "blocked");
|
|
4785
5757
|
const nextMode = this.deps.domainModes.getNextMode(failedMode, availableModes);
|
|
4786
5758
|
if (nextMode) {
|
|
4787
|
-
|
|
5759
|
+
log18.info(`Auto-escalating from ${failedMode} to ${nextMode} for ${domain}`);
|
|
4788
5760
|
if (failedMode !== "docker-headful") {
|
|
4789
5761
|
await this.deps.daemon.stopMode(failedMode, instanceId);
|
|
4790
5762
|
}
|
|
@@ -4797,7 +5769,7 @@ class PipelineExecutor {
|
|
|
4797
5769
|
this.deps.domainModes.recordFailure(domain, nextMode, result.error?.message || "blocked");
|
|
4798
5770
|
const thirdMode = this.deps.domainModes.getNextMode(nextMode, availableModes);
|
|
4799
5771
|
if (thirdMode) {
|
|
4800
|
-
|
|
5772
|
+
log18.info(`Auto-escalating from ${nextMode} to ${thirdMode} for ${domain}`);
|
|
4801
5773
|
if (nextMode !== "docker-headful") {
|
|
4802
5774
|
await this.deps.daemon.stopMode(nextMode, instanceId);
|
|
4803
5775
|
}
|
|
@@ -4821,6 +5793,89 @@ class PipelineExecutor {
|
|
|
4821
5793
|
}
|
|
4822
5794
|
return this.executeLocal(userId, token, pipeline, mode, instanceId);
|
|
4823
5795
|
}
|
|
5796
|
+
async executeExtension(userId, token, pipeline, tabId, clientId) {
|
|
5797
|
+
const startTime = Date.now();
|
|
5798
|
+
const relay = new CdpRelay(tabId, clientId, pipeline.options?.focus);
|
|
5799
|
+
let browser;
|
|
5800
|
+
try {
|
|
5801
|
+
await relay.start();
|
|
5802
|
+
browser = await import_playwright_core.chromium.connectOverCDP(relay.httpEndpoint(), { timeout: 30000 });
|
|
5803
|
+
const context = browser.contexts()[0];
|
|
5804
|
+
if (!context)
|
|
5805
|
+
throw new Error("no CDP browser context for the tab");
|
|
5806
|
+
let page = context.pages()[0];
|
|
5807
|
+
if (!page) {
|
|
5808
|
+
page = await context.waitForEvent("page", { timeout: 5000 }).catch(() => {
|
|
5809
|
+
return;
|
|
5810
|
+
});
|
|
5811
|
+
}
|
|
5812
|
+
if (!page)
|
|
5813
|
+
throw new Error("no page available for the tab (is it still open?)");
|
|
5814
|
+
const ctx = this.deps.refStore.makeContext(userId, token);
|
|
5815
|
+
if (this.pendingElicitOtp)
|
|
5816
|
+
ctx.elicitOtp = this.pendingElicitOtp;
|
|
5817
|
+
const runner = new PipelineRunner(ctx);
|
|
5818
|
+
const capMs = Math.min(60000 + pipeline.steps.length * 15000, 150000);
|
|
5819
|
+
let watchdog;
|
|
5820
|
+
let result;
|
|
5821
|
+
try {
|
|
5822
|
+
const runPromise = runner.run(page, pipeline);
|
|
5823
|
+
runPromise.catch(() => {
|
|
5824
|
+
return;
|
|
5825
|
+
});
|
|
5826
|
+
result = await Promise.race([
|
|
5827
|
+
runPromise,
|
|
5828
|
+
new Promise((_, reject) => {
|
|
5829
|
+
watchdog = setTimeout(() => reject(new Error(`pipeline exceeded ${Math.round(capMs / 1000)}s — the tab may have stopped ` + `rendering (minimized window?). Un-minimize the Chrome window or retry ` + `with options.focus=true.`)), capMs);
|
|
5830
|
+
watchdog.unref?.();
|
|
5831
|
+
})
|
|
5832
|
+
]);
|
|
5833
|
+
} finally {
|
|
5834
|
+
if (watchdog)
|
|
5835
|
+
clearTimeout(watchdog);
|
|
5836
|
+
}
|
|
5837
|
+
this.deps.refStore.sync(userId, ctx);
|
|
5838
|
+
result.modeUsed = "extension";
|
|
5839
|
+
if (result.ok) {
|
|
5840
|
+
try {
|
|
5841
|
+
extractKnowledgeFromRun(pipeline, result, null, "extension");
|
|
5842
|
+
} catch (e) {
|
|
5843
|
+
log18.warn(`knowledge update failed: ${getErrorMessage(e)}`);
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
return result;
|
|
5847
|
+
} catch (err) {
|
|
5848
|
+
const msg = getErrorMessage(err);
|
|
5849
|
+
const stalled = msg.includes("pipeline exceeded");
|
|
5850
|
+
return {
|
|
5851
|
+
ok: false,
|
|
5852
|
+
completedSteps: 0,
|
|
5853
|
+
totalSteps: pipeline.steps.length,
|
|
5854
|
+
results: [],
|
|
5855
|
+
finalState: { url: "", title: "" },
|
|
5856
|
+
obstacles: [],
|
|
5857
|
+
durationMs: Date.now() - startTime,
|
|
5858
|
+
modeUsed: "extension",
|
|
5859
|
+
error: {
|
|
5860
|
+
failedAtStep: 0,
|
|
5861
|
+
failedStep: pipeline.steps[0],
|
|
5862
|
+
errorType: "action-failed",
|
|
5863
|
+
message: `Extension mode failed: ${msg}`,
|
|
5864
|
+
pageState: { url: "", title: "" },
|
|
5865
|
+
suggestion: stalled ? "STOP retrying and tell the user what happened: the Chrome tab being driven stopped " + "responding — its window is likely minimized or the page is wedged. Ask them to " + "un-minimize the Chrome window (leaving it behind other windows is fine), or ask " + "permission to rerun with options.focus=true to bring it to the front." : "Ensure the iframer extension is connected (green dot) and the tab is still open. See chrome://extensions.",
|
|
5866
|
+
retryable: !stalled
|
|
5867
|
+
}
|
|
5868
|
+
};
|
|
5869
|
+
} finally {
|
|
5870
|
+
try {
|
|
5871
|
+
if (browser)
|
|
5872
|
+
await browser.close();
|
|
5873
|
+
} catch {}
|
|
5874
|
+
try {
|
|
5875
|
+
await relay.stop();
|
|
5876
|
+
} catch {}
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
4824
5879
|
async executeDocker(userId, token, pipeline) {
|
|
4825
5880
|
let session = getSession(userId);
|
|
4826
5881
|
if (!session) {
|
|
@@ -4912,7 +5967,7 @@ class PipelineExecutor {
|
|
|
4912
5967
|
try {
|
|
4913
5968
|
extractKnowledgeFromRun(pipeline, result, updatedSession, mode);
|
|
4914
5969
|
} catch (err) {
|
|
4915
|
-
|
|
5970
|
+
log18.warn(`knowledge update failed: ${getErrorMessage(err)}`);
|
|
4916
5971
|
}
|
|
4917
5972
|
}
|
|
4918
5973
|
this.deps.refStore.sync(userId, ctx);
|
|
@@ -5160,8 +6215,8 @@ class CredentialStore {
|
|
|
5160
6215
|
}
|
|
5161
6216
|
|
|
5162
6217
|
// src/lib/iframer.ts
|
|
5163
|
-
var
|
|
5164
|
-
var DEFAULT_SCREENSHOT_DIR =
|
|
6218
|
+
var log19 = createLogger("iframer");
|
|
6219
|
+
var DEFAULT_SCREENSHOT_DIR = import_path11.default.join(import_path11.default.dirname(import_url.fileURLToPath("file:///Users/eduardoverona/tools/iframer-toolkit/src/lib/iframer.ts")), "../../.screenshots");
|
|
5165
6220
|
var DEFAULT_PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3021}`;
|
|
5166
6221
|
var DEFAULT_STALE_TIMEOUT_MS3 = 20000;
|
|
5167
6222
|
|
|
@@ -5272,7 +6327,7 @@ class Iframer {
|
|
|
5272
6327
|
sessionSaved = true;
|
|
5273
6328
|
}
|
|
5274
6329
|
} catch (err) {
|
|
5275
|
-
|
|
6330
|
+
log19.warn(`stopSession: failed to extract daemon state for ${inst.mode}::${inst.instanceId}: ${getErrorMessage(err)}`);
|
|
5276
6331
|
}
|
|
5277
6332
|
}
|
|
5278
6333
|
}
|
|
@@ -5375,7 +6430,7 @@ function errorHandler(err, _req, res, _next) {
|
|
|
5375
6430
|
}
|
|
5376
6431
|
|
|
5377
6432
|
// src/api/routes.ts
|
|
5378
|
-
var
|
|
6433
|
+
var import_fs12 = __toESM(require("fs"));
|
|
5379
6434
|
function auth(req) {
|
|
5380
6435
|
return req;
|
|
5381
6436
|
}
|
|
@@ -5388,7 +6443,7 @@ function registerRoutes(app) {
|
|
|
5388
6443
|
const browsers = [];
|
|
5389
6444
|
try {
|
|
5390
6445
|
const execPath = import_patchright3.chromium.executablePath();
|
|
5391
|
-
browsers.push({ name: "chromium", installed:
|
|
6446
|
+
browsers.push({ name: "chromium", installed: import_fs12.default.existsSync(execPath), executablePath: execPath });
|
|
5392
6447
|
} catch {
|
|
5393
6448
|
browsers.push({ name: "chromium", installed: false, executablePath: null });
|
|
5394
6449
|
}
|
|
@@ -5557,6 +6612,66 @@ function registerRoutes(app) {
|
|
|
5557
6612
|
const { ok: _ok, ...resultRest } = result;
|
|
5558
6613
|
res.json({ ok: true, message: "Login attempted", ...resultRest });
|
|
5559
6614
|
}));
|
|
6615
|
+
app.get("/extension/status", (_req, res) => {
|
|
6616
|
+
res.json({ ok: true, version: getVersion(), ...extensionBridge.status() });
|
|
6617
|
+
});
|
|
6618
|
+
app.post("/extension/tabs", asyncHandler(async (_req, res) => {
|
|
6619
|
+
const result = await extensionBridge.listTabs();
|
|
6620
|
+
res.json({ ok: true, ...result });
|
|
6621
|
+
}));
|
|
6622
|
+
app.post("/extension/reload", asyncHandler(async (_req, res) => {
|
|
6623
|
+
const result = await extensionBridge.reloadAll();
|
|
6624
|
+
res.json({ ok: true, ...result });
|
|
6625
|
+
}));
|
|
6626
|
+
app.post("/extension/tab/create", asyncHandler(async (req, res) => {
|
|
6627
|
+
const { url, active, windowId, clientId } = req.body || {};
|
|
6628
|
+
if (url !== undefined && typeof url !== "string")
|
|
6629
|
+
throw new AppError(400, "url must be a string");
|
|
6630
|
+
const result = await extensionBridge.createTab(url || "", { active, windowId }, clientId);
|
|
6631
|
+
res.json({ ok: true, ...result });
|
|
6632
|
+
}));
|
|
6633
|
+
app.post("/extension/tab/group", asyncHandler(async (req, res) => {
|
|
6634
|
+
const { tabIds, title, color, collapsed, groupId, clientId } = req.body || {};
|
|
6635
|
+
if (!Array.isArray(tabIds) || tabIds.length === 0 || !tabIds.every((t) => typeof t === "number")) {
|
|
6636
|
+
throw new AppError(400, "tabIds must be a non-empty array of numbers");
|
|
6637
|
+
}
|
|
6638
|
+
const result = await extensionBridge.groupTabs(tabIds, { title, color, collapsed, groupId }, clientId);
|
|
6639
|
+
res.json({ ok: true, group: result });
|
|
6640
|
+
}));
|
|
6641
|
+
app.post("/extension/tab/ungroup", asyncHandler(async (req, res) => {
|
|
6642
|
+
const { tabIds, clientId } = req.body || {};
|
|
6643
|
+
if (!Array.isArray(tabIds) || tabIds.length === 0 || !tabIds.every((t) => typeof t === "number")) {
|
|
6644
|
+
throw new AppError(400, "tabIds must be a non-empty array of numbers");
|
|
6645
|
+
}
|
|
6646
|
+
const result = await extensionBridge.ungroupTabs(tabIds, clientId);
|
|
6647
|
+
res.json({ ok: true, ...result });
|
|
6648
|
+
}));
|
|
6649
|
+
app.post("/extension/group/update", asyncHandler(async (req, res) => {
|
|
6650
|
+
const { groupId, title, color, collapsed, clientId } = req.body || {};
|
|
6651
|
+
if (typeof groupId !== "number")
|
|
6652
|
+
throw new AppError(400, "groupId (number) is required");
|
|
6653
|
+
const result = await extensionBridge.updateGroup(groupId, { title, color, collapsed }, clientId);
|
|
6654
|
+
res.json({ ok: true, group: result });
|
|
6655
|
+
}));
|
|
6656
|
+
app.post("/extension/groups", asyncHandler(async (req, res) => {
|
|
6657
|
+
const { clientId } = req.body || {};
|
|
6658
|
+
const result = await extensionBridge.listGroups(clientId);
|
|
6659
|
+
res.json({ ok: true, ...result });
|
|
6660
|
+
}));
|
|
6661
|
+
app.post("/extension/execute", asyncHandler(async (req, res) => {
|
|
6662
|
+
const { tabId, steps, options, clientId } = req.body || {};
|
|
6663
|
+
if (typeof tabId !== "number")
|
|
6664
|
+
throw new AppError(400, "tabId (number) is required");
|
|
6665
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
6666
|
+
throw new AppError(400, "steps must be a non-empty array");
|
|
6667
|
+
}
|
|
6668
|
+
const r = auth(req);
|
|
6669
|
+
const result = await iframer.execute(r.userId, r.token, {
|
|
6670
|
+
steps,
|
|
6671
|
+
options: { ...options || {}, extensionTabId: tabId, clientId }
|
|
6672
|
+
});
|
|
6673
|
+
res.json(result);
|
|
6674
|
+
}));
|
|
5560
6675
|
app.post("/fetch", asyncHandler(async (req, res) => {
|
|
5561
6676
|
const { url } = req.body || {};
|
|
5562
6677
|
if (!url)
|
|
@@ -5589,8 +6704,8 @@ var PORT = parseInt(process.env.PORT || "3021", 10);
|
|
|
5589
6704
|
var REAP_INTERVAL_MS = 60000;
|
|
5590
6705
|
var IDLE_EXIT_MS = parseInt(process.env.IFRAMER_SERVER_IDLE_EXIT_MS || String(30 * 60 * 1000), 10);
|
|
5591
6706
|
var SHUTDOWN_DEADLINE_MS = 1e4;
|
|
5592
|
-
var SCREENSHOT_DIR =
|
|
5593
|
-
|
|
6707
|
+
var SCREENSHOT_DIR = import_path12.default.join(import_path12.default.dirname(import_url2.fileURLToPath("file:///Users/eduardoverona/tools/iframer-toolkit/index.ts")), ".screenshots");
|
|
6708
|
+
import_fs13.default.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
5594
6709
|
app.use("/screenshots", import_express.default.static(SCREENSHOT_DIR));
|
|
5595
6710
|
app.use(import_express.default.json());
|
|
5596
6711
|
var lastActivity = Date.now();
|
|
@@ -5607,10 +6722,11 @@ process.on("uncaughtException", (err) => {
|
|
|
5607
6722
|
process.on("unhandledRejection", (reason) => {
|
|
5608
6723
|
console.error(`[local-server] unhandledRejection (survived): ${reason}`);
|
|
5609
6724
|
});
|
|
5610
|
-
var server = app.listen(PORT, () => {
|
|
5611
|
-
console.log(`iframer listening on
|
|
6725
|
+
var server = app.listen(PORT, "127.0.0.1", () => {
|
|
6726
|
+
console.log(`iframer listening on 127.0.0.1:${PORT}`);
|
|
5612
6727
|
writeServerInfo({ pid: process.pid, port: PORT, startedAt: new Date().toISOString() });
|
|
5613
6728
|
});
|
|
6729
|
+
extensionBridge.attach(server);
|
|
5614
6730
|
var shutdownStarted = false;
|
|
5615
6731
|
async function gracefulShutdown(reason) {
|
|
5616
6732
|
if (shutdownStarted)
|
|
@@ -5653,7 +6769,7 @@ var reapTimer = setInterval(async () => {
|
|
|
5653
6769
|
await reapOrphanBrowsers();
|
|
5654
6770
|
} catch {}
|
|
5655
6771
|
const idleMs = Date.now() - lastActivity;
|
|
5656
|
-
if (idleMs > IDLE_EXIT_MS && !iframer.browserHealth().alive) {
|
|
6772
|
+
if (idleMs > IDLE_EXIT_MS && !iframer.browserHealth().alive && !extensionBridge.hasClients()) {
|
|
5657
6773
|
gracefulShutdown(`idle for ${Math.round(idleMs / 60000)}min with no browsers`);
|
|
5658
6774
|
}
|
|
5659
6775
|
}, REAP_INTERVAL_MS);
|