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.
Files changed (57) hide show
  1. package/dist/adaptive-wait.d.ts +72 -0
  2. package/dist/adaptive-wait.js +695 -0
  3. package/dist/api-intelligence.d.ts +82 -0
  4. package/dist/api-intelligence.js +575 -0
  5. package/dist/captcha-solver.d.ts +26 -0
  6. package/dist/captcha-solver.js +547 -0
  7. package/dist/cdp-connector.d.ts +33 -0
  8. package/dist/cdp-connector.js +176 -0
  9. package/dist/consent-dismiss.d.ts +33 -0
  10. package/dist/consent-dismiss.js +358 -0
  11. package/dist/crash-recovery.d.ts +74 -0
  12. package/dist/crash-recovery.js +242 -0
  13. package/dist/domain-knowledge.d.ts +149 -0
  14. package/dist/domain-knowledge.js +449 -0
  15. package/dist/harness-intelligence.d.ts +65 -0
  16. package/dist/harness-intelligence.js +432 -0
  17. package/dist/humanize-fingerprint.d.ts +42 -0
  18. package/dist/humanize-fingerprint.js +161 -0
  19. package/dist/humanize-mouse.d.ts +95 -0
  20. package/dist/humanize-mouse.js +275 -0
  21. package/dist/humanize-pause.d.ts +48 -0
  22. package/dist/humanize-pause.js +111 -0
  23. package/dist/humanize-scroll.d.ts +67 -0
  24. package/dist/humanize-scroll.js +185 -0
  25. package/dist/humanize-typing.d.ts +60 -0
  26. package/dist/humanize-typing.js +258 -0
  27. package/dist/humanize-utils.d.ts +62 -0
  28. package/dist/humanize-utils.js +100 -0
  29. package/dist/intervention.d.ts +65 -0
  30. package/dist/intervention.js +591 -0
  31. package/dist/logger.d.ts +13 -0
  32. package/dist/logger.js +47 -0
  33. package/dist/network-intelligence.d.ts +70 -0
  34. package/dist/network-intelligence.js +424 -0
  35. package/dist/page-classifier.d.ts +33 -0
  36. package/dist/page-classifier.js +1000 -0
  37. package/dist/paginate.d.ts +42 -0
  38. package/dist/paginate.js +693 -0
  39. package/dist/recording.d.ts +72 -0
  40. package/dist/recording.js +934 -0
  41. package/dist/script-executor.d.ts +31 -0
  42. package/dist/script-executor.js +249 -0
  43. package/dist/session-hud.d.ts +20 -0
  44. package/dist/session-hud.js +134 -0
  45. package/dist/snapshot-differ.d.ts +26 -0
  46. package/dist/snapshot-differ.js +225 -0
  47. package/dist/ssrf.d.ts +28 -0
  48. package/dist/ssrf.js +290 -0
  49. package/dist/stealth-audit.d.ts +27 -0
  50. package/dist/stealth-audit.js +719 -0
  51. package/dist/stealth.d.ts +195 -0
  52. package/dist/stealth.js +1157 -0
  53. package/dist/tab-manager.d.ts +14 -0
  54. package/dist/tab-manager.js +306 -0
  55. package/dist/tiles-coordinator.d.ts +106 -0
  56. package/dist/tiles-coordinator.js +358 -0
  57. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ import type { Session } from "./types.js";
2
+ export type ApiCategory = "data" | "tracking" | "auth" | "cdn" | "ads";
3
+ export interface ApiCapture {
4
+ index: number;
5
+ url: string;
6
+ method: string;
7
+ status: number;
8
+ bodySize: number;
9
+ parsedBody?: unknown;
10
+ rawBody?: string;
11
+ bodyTruncated?: boolean;
12
+ category: ApiCategory;
13
+ confidence: number;
14
+ classifiedBy: "domain" | "url-pattern" | "response-shape" | "heuristic" | "default";
15
+ headers: Record<string, string>;
16
+ timestamp: number;
17
+ duration: number;
18
+ dataShape?: Record<string, string>;
19
+ graphql?: {
20
+ operationName: string | null;
21
+ operationType: "query" | "mutation" | "subscription" | null;
22
+ };
23
+ contentType: string;
24
+ }
25
+ export interface ApiDiscoverResult {
26
+ total: number;
27
+ captured: ApiCapture[];
28
+ summary: {
29
+ data: number;
30
+ tracking: number;
31
+ auth: number;
32
+ cdn: number;
33
+ ads: number;
34
+ };
35
+ }
36
+ export interface OpenApiSpec {
37
+ openapi: string;
38
+ info: {
39
+ title: string;
40
+ version: string;
41
+ };
42
+ paths: Record<string, unknown>;
43
+ components?: {
44
+ securitySchemes?: Record<string, unknown>;
45
+ schemas?: Record<string, unknown>;
46
+ };
47
+ }
48
+ export declare class ApiIntelligence {
49
+ /**
50
+ * Classify a URL + response into a category.
51
+ * Exported for testability.
52
+ */
53
+ static classify(url: string, headers: Record<string, string>, body: unknown, method: string, bodySize: number): {
54
+ category: ApiCategory;
55
+ confidence: number;
56
+ classifiedBy: string;
57
+ };
58
+ /**
59
+ * Process a response event and store if it's a JSON API call.
60
+ * Designed to be called from the existing response listener in network-intelligence.ts.
61
+ */
62
+ static capture(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;
63
+ /**
64
+ * Get captured API calls for a session, optionally filtered.
65
+ */
66
+ static discover(sessionId: string, options?: {
67
+ category?: ApiCategory;
68
+ minConfidence?: number;
69
+ }): ApiDiscoverResult;
70
+ /**
71
+ * Generate OpenAPI v3 spec from captured traffic.
72
+ */
73
+ static exportOpenApi(sessionId: string, options?: {
74
+ title?: string;
75
+ includeTracking?: boolean;
76
+ }): OpenApiSpec;
77
+ /**
78
+ * Clear captures for a session.
79
+ */
80
+ static clearSession(sessionId: string): void;
81
+ }
82
+ export default ApiIntelligence;
@@ -0,0 +1,575 @@
1
+ import { logger } from "./logger.js";
2
+ // ─── Constants ───────────────────────────────────────────────────────────────
3
+ const MAX_API_ENTRIES = 50;
4
+ const MAX_API_BODY_BYTES = 256 * 1024;
5
+ const JSON_CONTENT_TYPES = [
6
+ "application/json",
7
+ "application/graphql+json",
8
+ "application/vnd.api+json",
9
+ "application/hal+json",
10
+ "text/json",
11
+ ];
12
+ const ELIGIBLE_RESOURCE_TYPES = ["xhr", "fetch"];
13
+ // ─── Domain Blocklists ───────────────────────────────────────────────────────
14
+ const TRACKING_DOMAINS = [
15
+ "google-analytics.com",
16
+ "analytics.google.com",
17
+ "www.googletagmanager.com",
18
+ "stats.g.doubleclick.net",
19
+ "api.segment.io",
20
+ "cdn.segment.com",
21
+ "api.mixpanel.com",
22
+ "api-js.mixpanel.com",
23
+ "connect.facebook.net",
24
+ "www.facebook.com",
25
+ "api2.amplitude.com",
26
+ "cdn.amplitude.com",
27
+ "heapanalytics.com",
28
+ "script.hotjar.com",
29
+ "vars.hotjar.com",
30
+ "edge.fullstory.com",
31
+ "browser-intake-datadoghq.com",
32
+ "sentry.io",
33
+ "bam.nr-data.net",
34
+ "js-agent.newrelic.com",
35
+ "events.launchdarkly.com",
36
+ "clientstream.launchdarkly.com",
37
+ "api-iam.intercom.io",
38
+ "widget.intercom.io",
39
+ ];
40
+ const AD_DOMAINS = [
41
+ "doubleclick.net",
42
+ "googlesyndication.com",
43
+ "googleadservices.com",
44
+ "adservice.google.com",
45
+ "amazon-adsystem.com",
46
+ "ads-api.twitter.com",
47
+ "ads.linkedin.com",
48
+ "adsapi.snapchat.com",
49
+ "an.facebook.com",
50
+ "moat.com",
51
+ "adsrvr.org",
52
+ "criteo.com",
53
+ "outbrain.com",
54
+ "taboola.com",
55
+ "pubmatic.com",
56
+ "rubiconproject.com",
57
+ "openx.net",
58
+ "casalemedia.com",
59
+ "sharethrough.com",
60
+ ];
61
+ // ─── URL Pattern Classifiers ─────────────────────────────────────────────────
62
+ const URL_CLASSIFIERS = [
63
+ // Auth patterns
64
+ { pattern: /\/(oauth|auth|login|logout|signin|signup|token|csrf|session|sso)\b/i, category: "auth" },
65
+ { pattern: /\/(\.well-known\/openid|authorize|callback|refresh)\b/i, category: "auth" },
66
+ // Tracking patterns on first-party domains
67
+ { pattern: /\/(collect|beacon|pixel|telemetry|ping|heartbeat|log-event)\b/i, category: "tracking" },
68
+ { pattern: /\/(analytics|tracking|metrics|events\/track)\b/i, category: "tracking" },
69
+ { pattern: /\/_analytics/i, category: "tracking" },
70
+ // CDN / static config patterns
71
+ { pattern: /\/(manifest\.json|sw\.js|service-worker|workbox)/i, category: "cdn" },
72
+ { pattern: /\/(_next\/data|__next|_nuxt)\//i, category: "data" },
73
+ ];
74
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
75
+ function isJsonContentType(contentType) {
76
+ const lower = contentType.toLowerCase();
77
+ return JSON_CONTENT_TYPES.some((ct) => lower.startsWith(ct));
78
+ }
79
+ function extractHostname(url) {
80
+ try {
81
+ return new URL(url).hostname;
82
+ }
83
+ catch {
84
+ return "";
85
+ }
86
+ }
87
+ function matchesDomain(hostname, domains) {
88
+ const lower = hostname.toLowerCase();
89
+ return domains.some((d) => lower === d || lower.endsWith("." + d));
90
+ }
91
+ function isGraphqlUrl(url) {
92
+ try {
93
+ const pathname = new URL(url).pathname;
94
+ return /\/graphql\b/i.test(pathname);
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ function parseGraphqlRequest(requestBody) {
101
+ if (!requestBody)
102
+ return null;
103
+ try {
104
+ const parsed = JSON.parse(requestBody);
105
+ const queryStr = parsed.query;
106
+ let opType = null;
107
+ if (typeof queryStr === "string") {
108
+ const trimmed = queryStr.trimStart();
109
+ if (trimmed.startsWith("mutation"))
110
+ opType = "mutation";
111
+ else if (trimmed.startsWith("subscription"))
112
+ opType = "subscription";
113
+ else
114
+ opType = "query";
115
+ }
116
+ return {
117
+ operationName: parsed.operationName ?? null,
118
+ operationType: opType,
119
+ };
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ function inferDataShape(body) {
126
+ if (!body || typeof body !== "object" || Array.isArray(body))
127
+ return undefined;
128
+ const shape = {};
129
+ const obj = body;
130
+ for (const [key, value] of Object.entries(obj)) {
131
+ if (value === null)
132
+ shape[key] = "null";
133
+ else if (Array.isArray(value))
134
+ shape[key] = `array(${value.length})`;
135
+ else if (typeof value === "object")
136
+ shape[key] = "object";
137
+ else
138
+ shape[key] = typeof value;
139
+ }
140
+ return Object.keys(shape).length > 0 ? shape : undefined;
141
+ }
142
+ function classifyByShape(body) {
143
+ if (!body || typeof body !== "object" || Array.isArray(body))
144
+ return null;
145
+ const obj = body;
146
+ const keys = Object.keys(obj);
147
+ // Auth response shapes
148
+ if (keys.includes("access_token") ||
149
+ keys.includes("refresh_token") ||
150
+ keys.includes("id_token") ||
151
+ keys.includes("csrf_token") ||
152
+ (keys.includes("token") && keys.includes("expires_in"))) {
153
+ return { category: "auth", confidence: 0.8 };
154
+ }
155
+ // Tracking acknowledgment shapes (tiny success-only)
156
+ if (keys.length <= 2 &&
157
+ (keys.includes("success") || keys.includes("ok") || keys.includes("status")) &&
158
+ !keys.some((k) => Array.isArray(obj[k]) || (typeof obj[k] === "object" && obj[k] !== null))) {
159
+ return { category: "tracking", confidence: 0.8 };
160
+ }
161
+ return null;
162
+ }
163
+ function classifyByHeuristic(method, bodySize) {
164
+ // POST with tiny response is likely tracking beacon
165
+ if (method === "POST" && bodySize < 100) {
166
+ return { category: "tracking", confidence: 0.6 };
167
+ }
168
+ // GET with substantial JSON is likely data
169
+ if (method === "GET" && bodySize > 500) {
170
+ return { category: "data", confidence: 0.6 };
171
+ }
172
+ return null;
173
+ }
174
+ /** UUID regex: 8-4-4-4-12 hex chars */
175
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
176
+ /** Pure numeric segment */
177
+ const NUMERIC_REGEX = /^\d+$/;
178
+ /** Long hash-like segment (>20 chars, hex or alphanum) */
179
+ const HASH_REGEX = /^[0-9a-f]{21,}$/i;
180
+ function isParameterSegment(segment) {
181
+ if (NUMERIC_REGEX.test(segment))
182
+ return true;
183
+ if (UUID_REGEX.test(segment))
184
+ return true;
185
+ if (segment.length > 20 && HASH_REGEX.test(segment))
186
+ return true;
187
+ return false;
188
+ }
189
+ function templatizePath(pathname) {
190
+ return pathname
191
+ .split("/")
192
+ .map((seg) => (seg && isParameterSegment(seg) ? "{id}" : seg))
193
+ .join("/");
194
+ }
195
+ // ─── Module-Level Storage ────────────────────────────────────────────────────
196
+ const captureStore = new Map();
197
+ let globalIndex = 0;
198
+ function getCaptures(sessionId) {
199
+ let arr = captureStore.get(sessionId);
200
+ if (!arr) {
201
+ arr = [];
202
+ captureStore.set(sessionId, arr);
203
+ }
204
+ return arr;
205
+ }
206
+ function pushToRingBuffer(buffer, entry) {
207
+ if (buffer.length >= MAX_API_ENTRIES) {
208
+ buffer.shift();
209
+ }
210
+ buffer.push(entry);
211
+ }
212
+ // ─── ApiIntelligence ─────────────────────────────────────────────────────────
213
+ export class ApiIntelligence {
214
+ /**
215
+ * Classify a URL + response into a category.
216
+ * Exported for testability.
217
+ */
218
+ static classify(url, headers, body, method, bodySize) {
219
+ const hostname = extractHostname(url);
220
+ // Layer 1: Known domain blocklist
221
+ if (matchesDomain(hostname, TRACKING_DOMAINS)) {
222
+ return { category: "tracking", confidence: 1.0, classifiedBy: "domain" };
223
+ }
224
+ if (matchesDomain(hostname, AD_DOMAINS)) {
225
+ return { category: "ads", confidence: 1.0, classifiedBy: "domain" };
226
+ }
227
+ // Layer 2: URL path patterns
228
+ let pathname = "";
229
+ try {
230
+ pathname = new URL(url).pathname;
231
+ }
232
+ catch {
233
+ /* ignore */
234
+ }
235
+ for (const { pattern, category } of URL_CLASSIFIERS) {
236
+ if (pattern.test(pathname)) {
237
+ return { category, confidence: 0.9, classifiedBy: "url-pattern" };
238
+ }
239
+ }
240
+ // Layer 3: Response shape analysis
241
+ const shapeResult = classifyByShape(body);
242
+ if (shapeResult) {
243
+ return { ...shapeResult, classifiedBy: "response-shape" };
244
+ }
245
+ // Layer 4: Size/method heuristics
246
+ const heuristicResult = classifyByHeuristic(method, bodySize);
247
+ if (heuristicResult) {
248
+ return { ...heuristicResult, classifiedBy: "heuristic" };
249
+ }
250
+ // Default
251
+ return { category: "data", confidence: 0.5, classifiedBy: "default" };
252
+ }
253
+ /**
254
+ * Process a response event and store if it's a JSON API call.
255
+ * Designed to be called from the existing response listener in network-intelligence.ts.
256
+ */
257
+ static capture(session, url, method, status, headers, body, duration, resourceType, requestHeaders, requestBody) {
258
+ try {
259
+ // Skip non-XHR/fetch
260
+ if (!ELIGIBLE_RESOURCE_TYPES.includes(resourceType))
261
+ return;
262
+ // Skip OPTIONS (CORS preflight)
263
+ if (method === "OPTIONS" || method === "HEAD")
264
+ return;
265
+ // Skip non-success responses
266
+ if (status < 200 || status > 399)
267
+ return;
268
+ const contentType = (headers["content-type"] ?? "").toLowerCase();
269
+ if (!isJsonContentType(contentType))
270
+ return;
271
+ const captures = getCaptures(session.id);
272
+ let parsedBody;
273
+ let rawBody;
274
+ let bodyTruncated = false;
275
+ let bodySize = 0;
276
+ if (body) {
277
+ bodySize = body.length;
278
+ if (bodySize > MAX_API_BODY_BYTES) {
279
+ bodyTruncated = true;
280
+ }
281
+ else {
282
+ const text = body.toString("utf-8");
283
+ try {
284
+ parsedBody = JSON.parse(text);
285
+ }
286
+ catch {
287
+ rawBody = text;
288
+ }
289
+ }
290
+ }
291
+ const classification = ApiIntelligence.classify(url, headers, parsedBody, method, bodySize);
292
+ // GraphQL handling
293
+ let graphql;
294
+ if (isGraphqlUrl(url)) {
295
+ const gqlInfo = parseGraphqlRequest(requestBody);
296
+ if (gqlInfo) {
297
+ graphql = gqlInfo;
298
+ }
299
+ }
300
+ // Data shape inference for data category
301
+ let dataShape;
302
+ if (classification.category === "data" && parsedBody) {
303
+ dataShape = inferDataShape(parsedBody);
304
+ }
305
+ const entry = {
306
+ index: globalIndex++,
307
+ url,
308
+ method,
309
+ status,
310
+ bodySize,
311
+ ...(parsedBody !== undefined ? { parsedBody } : {}),
312
+ ...(rawBody !== undefined ? { rawBody } : {}),
313
+ ...(bodyTruncated ? { bodyTruncated } : {}),
314
+ category: classification.category,
315
+ confidence: classification.confidence,
316
+ classifiedBy: classification.classifiedBy,
317
+ headers: { ...headers },
318
+ timestamp: Date.now(),
319
+ duration,
320
+ ...(dataShape ? { dataShape } : {}),
321
+ ...(graphql ? { graphql } : {}),
322
+ contentType: contentType.split(";")[0].trim(),
323
+ };
324
+ // Also merge request headers for auth detection in export
325
+ if (requestHeaders) {
326
+ entry.headers = { ...entry.headers, _requestHeaders: JSON.stringify(requestHeaders) };
327
+ }
328
+ pushToRingBuffer(captures, entry);
329
+ logger.debug("api-intelligence:capture", {
330
+ url,
331
+ category: classification.category,
332
+ confidence: classification.confidence,
333
+ classifiedBy: classification.classifiedBy,
334
+ bodySize,
335
+ });
336
+ }
337
+ catch {
338
+ // Capture must never crash the server
339
+ }
340
+ }
341
+ /**
342
+ * Get captured API calls for a session, optionally filtered.
343
+ */
344
+ static discover(sessionId, options) {
345
+ const captures = captureStore.get(sessionId) ?? [];
346
+ let filtered = captures;
347
+ if (options?.category) {
348
+ filtered = filtered.filter((c) => c.category === options.category);
349
+ }
350
+ if (options?.minConfidence !== undefined) {
351
+ filtered = filtered.filter((c) => c.confidence >= options.minConfidence);
352
+ }
353
+ const summary = { data: 0, tracking: 0, auth: 0, cdn: 0, ads: 0 };
354
+ for (const c of captures) {
355
+ summary[c.category]++;
356
+ }
357
+ return {
358
+ total: captures.length,
359
+ captured: filtered,
360
+ summary,
361
+ };
362
+ }
363
+ /**
364
+ * Generate OpenAPI v3 spec from captured traffic.
365
+ */
366
+ static exportOpenApi(sessionId, options) {
367
+ const captures = captureStore.get(sessionId) ?? [];
368
+ const title = options?.title ?? "Discovered API";
369
+ const includeTracking = options?.includeTracking ?? false;
370
+ // Filter to relevant captures
371
+ const relevant = includeTracking
372
+ ? captures
373
+ : captures.filter((c) => c.category === "data" || c.category === "auth");
374
+ // ── Endpoint clustering ──────────────────────────────────────────
375
+ // Group by templatized path + method
376
+ const endpointMap = new Map();
377
+ for (const cap of relevant) {
378
+ let parsed;
379
+ try {
380
+ parsed = new URL(cap.url);
381
+ }
382
+ catch {
383
+ continue;
384
+ }
385
+ const pathSegments = parsed.pathname.split("/");
386
+ const templateSegments = pathSegments.map((seg) => seg && isParameterSegment(seg) ? "{id}" : seg);
387
+ const template = templateSegments.join("/") || "/";
388
+ const key = `${cap.method}::${template}`;
389
+ let group = endpointMap.get(key);
390
+ if (!group) {
391
+ // Record which segments are parameterized and their first real values
392
+ const pathParams = new Set();
393
+ const exampleValues = new Map();
394
+ for (let i = 0; i < pathSegments.length; i++) {
395
+ if (pathSegments[i] && isParameterSegment(pathSegments[i])) {
396
+ pathParams.add(i);
397
+ exampleValues.set(i, pathSegments[i]);
398
+ }
399
+ }
400
+ group = {
401
+ template,
402
+ method: cap.method.toLowerCase(),
403
+ captures: [],
404
+ pathParams,
405
+ exampleValues,
406
+ queryParams: new Map(),
407
+ };
408
+ endpointMap.set(key, group);
409
+ }
410
+ group.captures.push(cap);
411
+ // Collect query params from the first observation
412
+ if (group.queryParams.size === 0) {
413
+ for (const [k, v] of parsed.searchParams.entries()) {
414
+ group.queryParams.set(k, v);
415
+ }
416
+ }
417
+ }
418
+ // ── Build paths ──────────────────────────────────────────────────
419
+ const paths = {};
420
+ for (const [, group] of endpointMap) {
421
+ const pathKey = group.template;
422
+ if (!paths[pathKey])
423
+ paths[pathKey] = {};
424
+ const parameters = [];
425
+ // Path parameters
426
+ for (const [idx, example] of group.exampleValues) {
427
+ parameters.push({
428
+ name: "id",
429
+ in: "path",
430
+ required: true,
431
+ schema: { type: "string" },
432
+ example,
433
+ });
434
+ // Only push one param named "id" even if multiple segments
435
+ break;
436
+ }
437
+ // Query parameters
438
+ for (const [name, example] of group.queryParams) {
439
+ parameters.push({
440
+ name,
441
+ in: "query",
442
+ schema: { type: "string" },
443
+ example,
444
+ });
445
+ }
446
+ // Response schema from first capture with a parsed body
447
+ const sampleCapture = group.captures.find((c) => c.parsedBody !== undefined);
448
+ const responseSchema = sampleCapture?.parsedBody
449
+ ? inferJsonSchema(sampleCapture.parsedBody)
450
+ : { type: "object" };
451
+ const responses = {};
452
+ // Use the most common status code
453
+ const statusCounts = new Map();
454
+ for (const c of group.captures) {
455
+ statusCounts.set(c.status, (statusCounts.get(c.status) ?? 0) + 1);
456
+ }
457
+ const primaryStatus = [...statusCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 200;
458
+ responses[String(primaryStatus)] = {
459
+ description: "Observed response",
460
+ content: {
461
+ "application/json": {
462
+ schema: responseSchema,
463
+ },
464
+ },
465
+ };
466
+ const operation = {
467
+ responses,
468
+ ...(parameters.length > 0 ? { parameters } : {}),
469
+ };
470
+ // GraphQL annotation
471
+ const gqlCapture = group.captures.find((c) => c.graphql);
472
+ if (gqlCapture?.graphql) {
473
+ operation["x-graphql"] = gqlCapture.graphql;
474
+ }
475
+ paths[pathKey][group.method] = operation;
476
+ }
477
+ // ── Auth detection → security schemes ────────────────────────────
478
+ const securitySchemes = {};
479
+ for (const cap of relevant) {
480
+ const reqHeaders = extractRequestHeaders(cap);
481
+ if (!reqHeaders)
482
+ continue;
483
+ const authHeader = reqHeaders["authorization"] ?? reqHeaders["Authorization"];
484
+ if (authHeader) {
485
+ if (authHeader.startsWith("Bearer ")) {
486
+ securitySchemes["bearerAuth"] = {
487
+ type: "http",
488
+ scheme: "bearer",
489
+ };
490
+ }
491
+ else if (authHeader.startsWith("Basic ")) {
492
+ securitySchemes["basicAuth"] = {
493
+ type: "http",
494
+ scheme: "basic",
495
+ };
496
+ }
497
+ }
498
+ // API key patterns
499
+ for (const header of Object.keys(reqHeaders)) {
500
+ const lower = header.toLowerCase();
501
+ if (lower === "x-api-key" || lower === "api-key") {
502
+ securitySchemes["apiKeyHeader"] = {
503
+ type: "apiKey",
504
+ in: "header",
505
+ name: header,
506
+ };
507
+ }
508
+ if (lower === "x-csrf-token" || lower === "csrf-token") {
509
+ securitySchemes["csrfToken"] = {
510
+ type: "apiKey",
511
+ in: "header",
512
+ name: header,
513
+ };
514
+ }
515
+ }
516
+ }
517
+ const spec = {
518
+ openapi: "3.0.3",
519
+ info: { title, version: "1.0.0" },
520
+ paths,
521
+ };
522
+ if (Object.keys(securitySchemes).length > 0) {
523
+ spec.components = { securitySchemes };
524
+ }
525
+ return spec;
526
+ }
527
+ /**
528
+ * Clear captures for a session.
529
+ */
530
+ static clearSession(sessionId) {
531
+ captureStore.delete(sessionId);
532
+ }
533
+ }
534
+ // ─── Internal Helpers ────────────────────────────────────────────────────────
535
+ function extractRequestHeaders(cap) {
536
+ const raw = cap.headers["_requestHeaders"];
537
+ if (!raw)
538
+ return null;
539
+ try {
540
+ return JSON.parse(raw);
541
+ }
542
+ catch {
543
+ return null;
544
+ }
545
+ }
546
+ /**
547
+ * Infer a minimal JSON Schema object from a runtime value.
548
+ * Used to generate OpenAPI response schemas from observed traffic.
549
+ */
550
+ function inferJsonSchema(value, depth = 0) {
551
+ if (depth > 3)
552
+ return { type: "object" }; // prevent deep recursion
553
+ if (value === null)
554
+ return { type: "string", nullable: true };
555
+ if (Array.isArray(value)) {
556
+ const items = value.length > 0 ? inferJsonSchema(value[0], depth + 1) : { type: "object" };
557
+ return { type: "array", items };
558
+ }
559
+ if (typeof value === "object") {
560
+ const properties = {};
561
+ const obj = value;
562
+ for (const [k, v] of Object.entries(obj)) {
563
+ properties[k] = inferJsonSchema(v, depth + 1);
564
+ }
565
+ return { type: "object", properties };
566
+ }
567
+ if (typeof value === "number") {
568
+ return Number.isInteger(value) ? { type: "integer" } : { type: "number" };
569
+ }
570
+ if (typeof value === "boolean")
571
+ return { type: "boolean" };
572
+ return { type: "string" };
573
+ }
574
+ // ─── Exports ─────────────────────────────────────────────────────────────────
575
+ export default ApiIntelligence;
@@ -0,0 +1,26 @@
1
+ import type { Page } from "playwright-core";
2
+ export type CaptchaProvider = "capsolver" | "2captcha" | "nopecha";
3
+ export type CaptchaType = "recaptcha-v2" | "recaptcha-v3" | "hcaptcha" | "turnstile";
4
+ export interface CaptchaSolveResult {
5
+ solved: boolean;
6
+ provider: string;
7
+ captchaType: string;
8
+ solveTimeMs: number;
9
+ error?: string;
10
+ }
11
+ /**
12
+ * Returns true if a CAPTCHA solving provider is configured and ready.
13
+ * Safe to call at any time — reads env vars on each invocation so config
14
+ * changes at runtime are picked up immediately.
15
+ */
16
+ export declare function isCaptchaSolverEnabled(): boolean;
17
+ /**
18
+ * Attempts to solve a CAPTCHA on the given page using the configured
19
+ * external solving service.
20
+ *
21
+ * @param page - Playwright page with the CAPTCHA
22
+ * @param captchaType - Free-form type string (e.g. "recaptcha", "hcaptcha", "turnstile")
23
+ * @param elementSelector - Optional CSS selector hint for the CAPTCHA element
24
+ * @returns - Result object with solve status, timing, and any error
25
+ */
26
+ export declare function solveCaptcha(page: Page, captchaType: string, elementSelector?: string): Promise<CaptchaSolveResult>;