leapfrog-mcp 0.6.2 → 0.6.3
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/adaptive-wait.d.ts +72 -0
- package/dist/adaptive-wait.js +695 -0
- package/dist/api-intelligence.d.ts +82 -0
- package/dist/api-intelligence.js +575 -0
- package/dist/captcha-solver.d.ts +26 -0
- package/dist/captcha-solver.js +547 -0
- package/dist/cdp-connector.d.ts +33 -0
- package/dist/cdp-connector.js +176 -0
- package/dist/consent-dismiss.d.ts +33 -0
- package/dist/consent-dismiss.js +358 -0
- package/dist/crash-recovery.d.ts +74 -0
- package/dist/crash-recovery.js +242 -0
- package/dist/domain-knowledge.d.ts +149 -0
- package/dist/domain-knowledge.js +449 -0
- package/dist/harness-intelligence.d.ts +65 -0
- package/dist/harness-intelligence.js +432 -0
- package/dist/humanize-fingerprint.d.ts +42 -0
- package/dist/humanize-fingerprint.js +161 -0
- package/dist/humanize-mouse.d.ts +95 -0
- package/dist/humanize-mouse.js +275 -0
- package/dist/humanize-pause.d.ts +48 -0
- package/dist/humanize-pause.js +111 -0
- package/dist/humanize-scroll.d.ts +67 -0
- package/dist/humanize-scroll.js +185 -0
- package/dist/humanize-typing.d.ts +60 -0
- package/dist/humanize-typing.js +258 -0
- package/dist/humanize-utils.d.ts +62 -0
- package/dist/humanize-utils.js +100 -0
- package/dist/intervention.d.ts +65 -0
- package/dist/intervention.js +591 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +47 -0
- package/dist/network-intelligence.d.ts +70 -0
- package/dist/network-intelligence.js +424 -0
- package/dist/page-classifier.d.ts +33 -0
- package/dist/page-classifier.js +1000 -0
- package/dist/paginate.d.ts +42 -0
- package/dist/paginate.js +693 -0
- package/dist/recording.d.ts +72 -0
- package/dist/recording.js +934 -0
- package/dist/script-executor.d.ts +31 -0
- package/dist/script-executor.js +249 -0
- package/dist/session-hud.d.ts +20 -0
- package/dist/session-hud.js +134 -0
- package/dist/snapshot-differ.d.ts +26 -0
- package/dist/snapshot-differ.js +225 -0
- package/dist/ssrf.d.ts +28 -0
- package/dist/ssrf.js +290 -0
- package/dist/stealth-audit.d.ts +27 -0
- package/dist/stealth-audit.js +719 -0
- package/dist/stealth.d.ts +195 -0
- package/dist/stealth.js +1157 -0
- package/dist/tab-manager.d.ts +14 -0
- package/dist/tab-manager.js +306 -0
- package/dist/tiles-coordinator.d.ts +106 -0
- package/dist/tiles-coordinator.js +358 -0
- package/package.json +1 -1
package/dist/logger.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ─── Structured JSON Logger ───────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Zero-dependency structured logger that writes JSON to stderr.
|
|
4
|
+
// MCP servers use stderr for logging (stdout is reserved for the protocol).
|
|
5
|
+
//
|
|
6
|
+
// Control verbosity via LEAP_LOG_LEVEL env var: debug | info | warn | error
|
|
7
|
+
// Default: info
|
|
8
|
+
const LOG_LEVELS = {
|
|
9
|
+
debug: 0,
|
|
10
|
+
info: 1,
|
|
11
|
+
warn: 2,
|
|
12
|
+
error: 3,
|
|
13
|
+
};
|
|
14
|
+
class Logger {
|
|
15
|
+
minLevel;
|
|
16
|
+
constructor() {
|
|
17
|
+
const envLevel = process.env.LEAP_LOG_LEVEL;
|
|
18
|
+
this.minLevel =
|
|
19
|
+
envLevel && envLevel in LOG_LEVELS ? envLevel : 'info';
|
|
20
|
+
}
|
|
21
|
+
log(level, event, data) {
|
|
22
|
+
if (LOG_LEVELS[level] < LOG_LEVELS[this.minLevel])
|
|
23
|
+
return;
|
|
24
|
+
const entry = {
|
|
25
|
+
ts: new Date().toISOString(),
|
|
26
|
+
level,
|
|
27
|
+
event,
|
|
28
|
+
pid: process.pid,
|
|
29
|
+
...data,
|
|
30
|
+
};
|
|
31
|
+
process.stderr.write(JSON.stringify(entry) + '\n');
|
|
32
|
+
}
|
|
33
|
+
debug(event, data) {
|
|
34
|
+
this.log('debug', event, data);
|
|
35
|
+
}
|
|
36
|
+
info(event, data) {
|
|
37
|
+
this.log('info', event, data);
|
|
38
|
+
}
|
|
39
|
+
warn(event, data) {
|
|
40
|
+
this.log('warn', event, data);
|
|
41
|
+
}
|
|
42
|
+
error(event, data) {
|
|
43
|
+
this.log('error', event, data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const logger = new Logger();
|
|
47
|
+
export default logger;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Page } from "playwright-core";
|
|
2
|
+
import type { Session, NetworkInterceptRule } from "./types.js";
|
|
3
|
+
export type ResponseCallback = (session: Session, url: string, method: string, status: number, headers: Record<string, string>, body: Buffer | null, duration: number, resourceType: string, requestHeaders?: Record<string, string>, requestBody?: string) => void;
|
|
4
|
+
export interface NetworkFilter {
|
|
5
|
+
urlPattern?: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
statusMin?: number;
|
|
8
|
+
statusMax?: number;
|
|
9
|
+
contentType?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ConsoleFilter {
|
|
12
|
+
level?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class NetworkIntelligence {
|
|
15
|
+
/**
|
|
16
|
+
* Tracks active route handlers per session so we can unroute them.
|
|
17
|
+
* Key = session.id, Value = Map of ruleId -> { urlPattern, handler }
|
|
18
|
+
*/
|
|
19
|
+
private routeHandlers;
|
|
20
|
+
/** Registered response callbacks — invoked for every captured response */
|
|
21
|
+
private responseCallbacks;
|
|
22
|
+
/**
|
|
23
|
+
* Register a callback that fires for every network response.
|
|
24
|
+
* Used by ApiIntelligence to capture API traffic.
|
|
25
|
+
*/
|
|
26
|
+
onResponse(callback: ResponseCallback): void;
|
|
27
|
+
/**
|
|
28
|
+
* Wire up network and console capture on a page.
|
|
29
|
+
* Call once when a session is created. Initializes the ring buffers
|
|
30
|
+
* on the session and installs Playwright event listeners.
|
|
31
|
+
*/
|
|
32
|
+
attachToPage(page: Page, session: Session): void;
|
|
33
|
+
/**
|
|
34
|
+
* Returns network log entries as compact formatted text.
|
|
35
|
+
* Supports filtering by URL pattern (regex or substring), HTTP method,
|
|
36
|
+
* status code range, and content-type.
|
|
37
|
+
*/
|
|
38
|
+
getNetworkLog(session: Session, filter?: NetworkFilter): string;
|
|
39
|
+
/**
|
|
40
|
+
* Returns console log entries as compact formatted text.
|
|
41
|
+
* Supports filtering by log level.
|
|
42
|
+
*/
|
|
43
|
+
getConsoleLog(session: Session, filter?: ConsoleFilter): string;
|
|
44
|
+
/**
|
|
45
|
+
* Add a network intercept rule to a page.
|
|
46
|
+
* - `block`: Aborts matching requests
|
|
47
|
+
* - `log`: Lets requests through (captured by the response listener)
|
|
48
|
+
* - `mock`: Returns a custom response with specified status, body, and content-type
|
|
49
|
+
*
|
|
50
|
+
* If a rule with the same ID already exists, it is replaced.
|
|
51
|
+
*/
|
|
52
|
+
addIntercept(page: Page, session: Session, rule: NetworkInterceptRule): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Remove a previously added intercept rule by ID.
|
|
55
|
+
* Calls page.unroute() to deregister the Playwright route handler.
|
|
56
|
+
*/
|
|
57
|
+
removeIntercept(page: Page, session: Session, ruleId: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Clear captured logs.
|
|
60
|
+
* @param type - Which logs to clear: 'network', 'console', or 'all' (default).
|
|
61
|
+
*/
|
|
62
|
+
clearLogs(session: Session, type?: "network" | "console" | "all"): void;
|
|
63
|
+
/**
|
|
64
|
+
* Remove tracked route handlers for a destroyed session.
|
|
65
|
+
* Call this from your session destroy logic.
|
|
66
|
+
*/
|
|
67
|
+
cleanupSession(sessionId: string): void;
|
|
68
|
+
}
|
|
69
|
+
export declare const networkIntelligence: NetworkIntelligence;
|
|
70
|
+
export default NetworkIntelligence;
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
2
|
+
const MAX_NETWORK_ENTRIES = 200;
|
|
3
|
+
const MAX_CONSOLE_ENTRIES = 100;
|
|
4
|
+
const MAX_BODY_CAPTURE_BYTES = 10 * 1024; // 10KB
|
|
5
|
+
/** Content-types eligible for body capture */
|
|
6
|
+
const CAPTURABLE_CONTENT_TYPES = [
|
|
7
|
+
"application/json",
|
|
8
|
+
"text/plain",
|
|
9
|
+
"text/html",
|
|
10
|
+
"text/xml",
|
|
11
|
+
"application/xml",
|
|
12
|
+
"text/csv",
|
|
13
|
+
"application/javascript",
|
|
14
|
+
"text/javascript",
|
|
15
|
+
];
|
|
16
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
17
|
+
function isCapturableContentType(contentType) {
|
|
18
|
+
const lower = contentType.toLowerCase();
|
|
19
|
+
return CAPTURABLE_CONTENT_TYPES.some((ct) => lower.startsWith(ct));
|
|
20
|
+
}
|
|
21
|
+
function formatBytes(bytes) {
|
|
22
|
+
if (bytes < 1024)
|
|
23
|
+
return `${bytes}B`;
|
|
24
|
+
const kb = bytes / 1024;
|
|
25
|
+
if (kb < 1024)
|
|
26
|
+
return `${kb.toFixed(1)}KB`;
|
|
27
|
+
const mb = kb / 1024;
|
|
28
|
+
return `${mb.toFixed(1)}MB`;
|
|
29
|
+
}
|
|
30
|
+
function formatTimestamp(ts) {
|
|
31
|
+
const d = new Date(ts);
|
|
32
|
+
return (String(d.getHours()).padStart(2, "0") +
|
|
33
|
+
":" +
|
|
34
|
+
String(d.getMinutes()).padStart(2, "0") +
|
|
35
|
+
":" +
|
|
36
|
+
String(d.getSeconds()).padStart(2, "0"));
|
|
37
|
+
}
|
|
38
|
+
function pushToRingBuffer(buffer, entry, maxSize) {
|
|
39
|
+
if (buffer.length >= maxSize) {
|
|
40
|
+
buffer.shift();
|
|
41
|
+
}
|
|
42
|
+
buffer.push(entry);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Ensure session has initialized arrays for network intelligence.
|
|
46
|
+
* Called defensively before any read/write to these fields since
|
|
47
|
+
* session-manager.ts does not initialize them.
|
|
48
|
+
*/
|
|
49
|
+
function ensureSessionArrays(session) {
|
|
50
|
+
if (!session.networkLog)
|
|
51
|
+
session.networkLog = [];
|
|
52
|
+
if (!session.consoleLog)
|
|
53
|
+
session.consoleLog = [];
|
|
54
|
+
if (!session.interceptRules)
|
|
55
|
+
session.interceptRules = [];
|
|
56
|
+
}
|
|
57
|
+
const CONSOLE_LEVEL_MAP = {
|
|
58
|
+
log: "log",
|
|
59
|
+
warning: "warn",
|
|
60
|
+
error: "error",
|
|
61
|
+
info: "info",
|
|
62
|
+
debug: "debug",
|
|
63
|
+
};
|
|
64
|
+
// ─── NetworkIntelligence ─────────────────────────────────────────────────
|
|
65
|
+
export class NetworkIntelligence {
|
|
66
|
+
/**
|
|
67
|
+
* Tracks active route handlers per session so we can unroute them.
|
|
68
|
+
* Key = session.id, Value = Map of ruleId -> { urlPattern, handler }
|
|
69
|
+
*/
|
|
70
|
+
routeHandlers = new Map();
|
|
71
|
+
/** Registered response callbacks — invoked for every captured response */
|
|
72
|
+
responseCallbacks = [];
|
|
73
|
+
/**
|
|
74
|
+
* Register a callback that fires for every network response.
|
|
75
|
+
* Used by ApiIntelligence to capture API traffic.
|
|
76
|
+
*/
|
|
77
|
+
onResponse(callback) {
|
|
78
|
+
this.responseCallbacks.push(callback);
|
|
79
|
+
}
|
|
80
|
+
// ── Attach listeners to a page ───────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Wire up network and console capture on a page.
|
|
83
|
+
* Call once when a session is created. Initializes the ring buffers
|
|
84
|
+
* on the session and installs Playwright event listeners.
|
|
85
|
+
*/
|
|
86
|
+
attachToPage(page, session) {
|
|
87
|
+
// Initialize arrays on the session
|
|
88
|
+
ensureSessionArrays(session);
|
|
89
|
+
// BUG-006: Track request start times via wall clock as a fallback
|
|
90
|
+
// for when Playwright timing() returns zeros.
|
|
91
|
+
const requestStartTimes = new Map();
|
|
92
|
+
page.on("request", (request) => {
|
|
93
|
+
requestStartTimes.set(request.url() + request.method(), Date.now());
|
|
94
|
+
});
|
|
95
|
+
// --- Network response listener ---
|
|
96
|
+
page.on("response", async (response) => {
|
|
97
|
+
try {
|
|
98
|
+
const request = response.request();
|
|
99
|
+
const timing = request.timing();
|
|
100
|
+
// BUG-006: Duration calculation with multiple fallbacks.
|
|
101
|
+
// 1. Prefer Playwright's built-in timing (responseEnd - requestStart)
|
|
102
|
+
// 2. Fall back to (responseEnd - startTime) if requestStart is missing
|
|
103
|
+
// 3. Final fallback: wall-clock delta from our request listener
|
|
104
|
+
let duration = 0;
|
|
105
|
+
if (timing.responseEnd > 0 && timing.requestStart > 0) {
|
|
106
|
+
duration = Math.round(timing.responseEnd - timing.requestStart);
|
|
107
|
+
}
|
|
108
|
+
else if (timing.responseEnd > 0 && timing.startTime > 0) {
|
|
109
|
+
duration = Math.round(timing.responseEnd - timing.startTime);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Wall-clock fallback
|
|
113
|
+
const key = request.url() + request.method();
|
|
114
|
+
const wallStart = requestStartTimes.get(key);
|
|
115
|
+
if (wallStart) {
|
|
116
|
+
duration = Date.now() - wallStart;
|
|
117
|
+
requestStartTimes.delete(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const url = response.url();
|
|
121
|
+
const status = response.status();
|
|
122
|
+
const headers = response.headers();
|
|
123
|
+
const contentType = headers["content-type"] ?? "";
|
|
124
|
+
const contentLength = headers["content-length"];
|
|
125
|
+
// Determine response size — prefer content-length header, fallback to body length
|
|
126
|
+
let responseSize = contentLength ? parseInt(contentLength, 10) : 0;
|
|
127
|
+
if (isNaN(responseSize))
|
|
128
|
+
responseSize = 0;
|
|
129
|
+
// Optionally capture body for small JSON/text responses
|
|
130
|
+
let responseBody;
|
|
131
|
+
if (isCapturableContentType(contentType)) {
|
|
132
|
+
// Check size hint before attempting body read
|
|
133
|
+
const sizeHint = responseSize || MAX_BODY_CAPTURE_BYTES + 1;
|
|
134
|
+
if (sizeHint <= MAX_BODY_CAPTURE_BYTES) {
|
|
135
|
+
try {
|
|
136
|
+
const body = await response.text();
|
|
137
|
+
if (body.length <= MAX_BODY_CAPTURE_BYTES) {
|
|
138
|
+
responseBody = body;
|
|
139
|
+
}
|
|
140
|
+
// Update size from actual body if content-length was missing
|
|
141
|
+
if (!responseSize) {
|
|
142
|
+
responseSize = body.length;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Body may be unavailable (redirects, aborted) — skip silently
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// If we still don't have a size, try reading the body just for size
|
|
151
|
+
if (!responseSize && !responseBody) {
|
|
152
|
+
try {
|
|
153
|
+
const bodyBuf = await response.body();
|
|
154
|
+
responseSize = bodyBuf.length;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Non-critical — leave as 0
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const entry = {
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
method: request.method(),
|
|
163
|
+
url,
|
|
164
|
+
status,
|
|
165
|
+
contentType: contentType.split(";")[0].trim(),
|
|
166
|
+
responseSize,
|
|
167
|
+
duration,
|
|
168
|
+
...(responseBody !== undefined ? { responseBody } : {}),
|
|
169
|
+
};
|
|
170
|
+
// Defensive: ensure array exists (session may have been created before attach)
|
|
171
|
+
if (!session.networkLog)
|
|
172
|
+
session.networkLog = [];
|
|
173
|
+
pushToRingBuffer(session.networkLog, entry, MAX_NETWORK_ENTRIES);
|
|
174
|
+
// Invoke response callbacks (e.g. ApiIntelligence)
|
|
175
|
+
if (this.responseCallbacks.length > 0) {
|
|
176
|
+
const request = response.request();
|
|
177
|
+
let bodyBuffer = null;
|
|
178
|
+
try {
|
|
179
|
+
bodyBuffer = await response.body();
|
|
180
|
+
}
|
|
181
|
+
catch { /* body may be unavailable */ }
|
|
182
|
+
let reqHeaders;
|
|
183
|
+
try {
|
|
184
|
+
reqHeaders = await request.allHeaders();
|
|
185
|
+
}
|
|
186
|
+
catch { /* */ }
|
|
187
|
+
let reqBody;
|
|
188
|
+
try {
|
|
189
|
+
reqBody = request.postData() ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
catch { /* */ }
|
|
192
|
+
for (const cb of this.responseCallbacks) {
|
|
193
|
+
try {
|
|
194
|
+
cb(session, url, request.method(), status, headers, bodyBuffer, duration, request.resourceType(), reqHeaders, reqBody);
|
|
195
|
+
}
|
|
196
|
+
catch { /* callbacks must not crash the server */ }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Network capture must never crash the server — swallow all errors
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// --- Console message listener ---
|
|
205
|
+
page.on("console", (msg) => {
|
|
206
|
+
try {
|
|
207
|
+
const rawType = msg.type();
|
|
208
|
+
const level = CONSOLE_LEVEL_MAP[rawType] ?? "log";
|
|
209
|
+
const entry = {
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
level,
|
|
212
|
+
text: msg.text(),
|
|
213
|
+
};
|
|
214
|
+
// Defensive: ensure array exists
|
|
215
|
+
if (!session.consoleLog)
|
|
216
|
+
session.consoleLog = [];
|
|
217
|
+
pushToRingBuffer(session.consoleLog, entry, MAX_CONSOLE_ENTRIES);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Console capture must never crash the server
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// ── Get network log (filtered, formatted) ────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* Returns network log entries as compact formatted text.
|
|
227
|
+
* Supports filtering by URL pattern (regex or substring), HTTP method,
|
|
228
|
+
* status code range, and content-type.
|
|
229
|
+
*/
|
|
230
|
+
getNetworkLog(session, filter) {
|
|
231
|
+
const entries = session.networkLog ?? [];
|
|
232
|
+
let filtered = entries;
|
|
233
|
+
if (filter) {
|
|
234
|
+
filtered = entries.filter((e) => {
|
|
235
|
+
if (filter.urlPattern) {
|
|
236
|
+
try {
|
|
237
|
+
const regex = new RegExp(filter.urlPattern, "i");
|
|
238
|
+
if (!regex.test(e.url))
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Invalid regex — fall back to substring match
|
|
243
|
+
if (!e.url.toLowerCase().includes(filter.urlPattern.toLowerCase()))
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (filter.method && e.method.toUpperCase() !== filter.method.toUpperCase()) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (filter.statusMin !== undefined && e.status < filter.statusMin) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (filter.statusMax !== undefined && e.status > filter.statusMax) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (filter.contentType) {
|
|
257
|
+
if (!e.contentType
|
|
258
|
+
.toLowerCase()
|
|
259
|
+
.includes(filter.contentType.toLowerCase())) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (filtered.length === 0) {
|
|
267
|
+
return "(no matching network entries)";
|
|
268
|
+
}
|
|
269
|
+
return filtered
|
|
270
|
+
.map((e) => {
|
|
271
|
+
const parts = [
|
|
272
|
+
e.method,
|
|
273
|
+
String(e.status),
|
|
274
|
+
e.url,
|
|
275
|
+
`(${formatBytes(e.responseSize)})`,
|
|
276
|
+
`${e.duration}ms`,
|
|
277
|
+
];
|
|
278
|
+
if (e.responseBody !== undefined) {
|
|
279
|
+
parts.push("[body captured]");
|
|
280
|
+
}
|
|
281
|
+
return parts.join(" ");
|
|
282
|
+
})
|
|
283
|
+
.join("\n");
|
|
284
|
+
}
|
|
285
|
+
// ── Get console log (filtered, formatted) ────────────────────────
|
|
286
|
+
/**
|
|
287
|
+
* Returns console log entries as compact formatted text.
|
|
288
|
+
* Supports filtering by log level.
|
|
289
|
+
*/
|
|
290
|
+
getConsoleLog(session, filter) {
|
|
291
|
+
const entries = session.consoleLog ?? [];
|
|
292
|
+
let filtered = entries;
|
|
293
|
+
if (filter?.level) {
|
|
294
|
+
const targetLevel = filter.level.toLowerCase();
|
|
295
|
+
filtered = entries.filter((e) => e.level === targetLevel);
|
|
296
|
+
}
|
|
297
|
+
if (filtered.length === 0) {
|
|
298
|
+
return "(no matching console entries)";
|
|
299
|
+
}
|
|
300
|
+
// Pad level labels to align output
|
|
301
|
+
const maxLevelLen = 5; // "error" is longest
|
|
302
|
+
return filtered
|
|
303
|
+
.map((e) => {
|
|
304
|
+
const levelStr = `[${e.level}]`.padEnd(maxLevelLen + 2);
|
|
305
|
+
const timeStr = formatTimestamp(e.timestamp);
|
|
306
|
+
return `${levelStr} ${timeStr} ${e.text}`;
|
|
307
|
+
})
|
|
308
|
+
.join("\n");
|
|
309
|
+
}
|
|
310
|
+
// ── Add intercept rule ───────────────────────────────────────────
|
|
311
|
+
/**
|
|
312
|
+
* Add a network intercept rule to a page.
|
|
313
|
+
* - `block`: Aborts matching requests
|
|
314
|
+
* - `log`: Lets requests through (captured by the response listener)
|
|
315
|
+
* - `mock`: Returns a custom response with specified status, body, and content-type
|
|
316
|
+
*
|
|
317
|
+
* If a rule with the same ID already exists, it is replaced.
|
|
318
|
+
*/
|
|
319
|
+
async addIntercept(page, session, rule) {
|
|
320
|
+
ensureSessionArrays(session);
|
|
321
|
+
// Remove existing rule with same ID if present
|
|
322
|
+
const rules = session.interceptRules;
|
|
323
|
+
if (rules.some((r) => r.id === rule.id)) {
|
|
324
|
+
await this.removeIntercept(page, session, rule.id);
|
|
325
|
+
}
|
|
326
|
+
const handler = async (route) => {
|
|
327
|
+
try {
|
|
328
|
+
switch (rule.action) {
|
|
329
|
+
case "block":
|
|
330
|
+
await route.abort("blockedbyclient");
|
|
331
|
+
break;
|
|
332
|
+
case "log":
|
|
333
|
+
// Let the request through — the response listener will capture it
|
|
334
|
+
await route.continue();
|
|
335
|
+
break;
|
|
336
|
+
case "mock": {
|
|
337
|
+
if (!rule.mockResponse) {
|
|
338
|
+
// No mock config — fall through
|
|
339
|
+
await route.continue();
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
await route.fulfill({
|
|
343
|
+
status: rule.mockResponse.status,
|
|
344
|
+
contentType: rule.mockResponse.contentType,
|
|
345
|
+
body: rule.mockResponse.body,
|
|
346
|
+
});
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Route may already be handled or page navigated away — ignore
|
|
353
|
+
try {
|
|
354
|
+
await route.continue();
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Truly dead route — nothing to do
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
// Register the route with Playwright
|
|
362
|
+
await page.route(rule.urlPattern, handler);
|
|
363
|
+
// Track the handler so we can unroute later
|
|
364
|
+
if (!this.routeHandlers.has(session.id)) {
|
|
365
|
+
this.routeHandlers.set(session.id, new Map());
|
|
366
|
+
}
|
|
367
|
+
this.routeHandlers
|
|
368
|
+
.get(session.id)
|
|
369
|
+
.set(rule.id, { urlPattern: rule.urlPattern, handler });
|
|
370
|
+
// Store the rule on the session
|
|
371
|
+
session.interceptRules.push(rule);
|
|
372
|
+
}
|
|
373
|
+
// ── Remove intercept rule ────────────────────────────────────────
|
|
374
|
+
/**
|
|
375
|
+
* Remove a previously added intercept rule by ID.
|
|
376
|
+
* Calls page.unroute() to deregister the Playwright route handler.
|
|
377
|
+
*/
|
|
378
|
+
async removeIntercept(page, session, ruleId) {
|
|
379
|
+
const sessionHandlers = this.routeHandlers.get(session.id);
|
|
380
|
+
const entry = sessionHandlers?.get(ruleId);
|
|
381
|
+
if (entry) {
|
|
382
|
+
try {
|
|
383
|
+
await page.unroute(entry.urlPattern, entry.handler);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// Route may have already been removed — ignore
|
|
387
|
+
}
|
|
388
|
+
sessionHandlers.delete(ruleId);
|
|
389
|
+
if (sessionHandlers.size === 0) {
|
|
390
|
+
this.routeHandlers.delete(session.id);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (session.interceptRules) {
|
|
394
|
+
session.interceptRules = session.interceptRules.filter((r) => r.id !== ruleId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ── Clear logs ───────────────────────────────────────────────────
|
|
398
|
+
/**
|
|
399
|
+
* Clear captured logs.
|
|
400
|
+
* @param type - Which logs to clear: 'network', 'console', or 'all' (default).
|
|
401
|
+
*/
|
|
402
|
+
clearLogs(session, type) {
|
|
403
|
+
const target = type ?? "all";
|
|
404
|
+
if (target === "network" || target === "all") {
|
|
405
|
+
if (session.networkLog)
|
|
406
|
+
session.networkLog.length = 0;
|
|
407
|
+
}
|
|
408
|
+
if (target === "console" || target === "all") {
|
|
409
|
+
if (session.consoleLog)
|
|
410
|
+
session.consoleLog.length = 0;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ── Cleanup when a session is destroyed ──────────────────────────
|
|
414
|
+
/**
|
|
415
|
+
* Remove tracked route handlers for a destroyed session.
|
|
416
|
+
* Call this from your session destroy logic.
|
|
417
|
+
*/
|
|
418
|
+
cleanupSession(sessionId) {
|
|
419
|
+
this.routeHandlers.delete(sessionId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ─── Export singleton + class ─────────────────────────────────────────────
|
|
423
|
+
export const networkIntelligence = new NetworkIntelligence();
|
|
424
|
+
export default NetworkIntelligence;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PageType = "login" | "search-results" | "product" | "product-list" | "checkout" | "article" | "dashboard" | "form" | "error" | "challenge" | "landing" | "documentation" | "profile" | "media" | "feed" | "qa" | "ecommerce" | "unknown";
|
|
2
|
+
export interface ClassificationResult {
|
|
3
|
+
type: PageType;
|
|
4
|
+
confidence: number;
|
|
5
|
+
signals: string[];
|
|
6
|
+
allScores: Record<PageType, number>;
|
|
7
|
+
metadata?: {
|
|
8
|
+
formFields?: number;
|
|
9
|
+
interactiveElements?: number;
|
|
10
|
+
hasPassword?: boolean;
|
|
11
|
+
hasPrice?: boolean;
|
|
12
|
+
jsonLdType?: string;
|
|
13
|
+
ogType?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface ClassificationInput {
|
|
17
|
+
url: string;
|
|
18
|
+
status?: number;
|
|
19
|
+
snapshotText: string;
|
|
20
|
+
meta?: {
|
|
21
|
+
ogType?: string;
|
|
22
|
+
jsonLdType?: string;
|
|
23
|
+
robots?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare class PageClassifier {
|
|
28
|
+
/** Classify a page based on URL, snapshot, and optional meta */
|
|
29
|
+
static classify(input: ClassificationInput): ClassificationResult;
|
|
30
|
+
/** Quick classify from URL only (Layer 1 only, lower confidence) */
|
|
31
|
+
static classifyUrl(url: string): ClassificationResult;
|
|
32
|
+
}
|
|
33
|
+
export default PageClassifier;
|