@detiq/crawler 1.1.14 → 1.1.16
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/Dockerfile +1 -1
- package/dist/accessibility-tree-crawler.js +156 -0
- package/dist/accessibility-tree-crawler.js.map +7 -0
- package/dist/advanced-ui-probes.js +310 -0
- package/dist/advanced-ui-probes.js.map +7 -0
- package/dist/content-chunker.js +2 -2
- package/dist/content-chunker.js.map +2 -2
- package/dist/crawl-session-manager.js +145 -0
- package/dist/crawl-session-manager.js.map +7 -0
- package/dist/crawler.js +1377 -344
- package/dist/crawler.js.map +2 -2
- package/dist/crud-interaction.js +264 -0
- package/dist/crud-interaction.js.map +7 -0
- package/dist/dropdown-enumerator.js +230 -0
- package/dist/dropdown-enumerator.js.map +7 -0
- package/dist/filter-pagination-crawler.js +299 -0
- package/dist/filter-pagination-crawler.js.map +7 -0
- package/dist/frontier.js +5 -3
- package/dist/frontier.js.map +2 -2
- package/dist/gap-pass.js +47 -14
- package/dist/gap-pass.js.map +2 -2
- package/dist/http-engine.js +28 -15
- package/dist/http-engine.js.map +2 -2
- package/dist/index.js +15 -1
- package/dist/index.js.map +2 -2
- package/dist/job-queue.js +23 -6
- package/dist/job-queue.js.map +2 -2
- package/dist/locator-extractor.js +23 -0
- package/dist/locator-extractor.js.map +2 -2
- package/dist/multi-context-handler.js +182 -0
- package/dist/multi-context-handler.js.map +7 -0
- package/dist/nested-window-crawler.js +172 -0
- package/dist/nested-window-crawler.js.map +7 -0
- package/dist/network-action-correlator.js +209 -0
- package/dist/network-action-correlator.js.map +7 -0
- package/dist/performance-metrics-collector.js +155 -0
- package/dist/performance-metrics-collector.js.map +7 -0
- package/dist/performance-metrics.js.map +2 -2
- package/dist/policies/crawl-safety.js +5 -1
- package/dist/policies/crawl-safety.js.map +2 -2
- package/dist/profiles/domain-profile.js +21 -1
- package/dist/profiles/domain-profile.js.map +2 -2
- package/dist/rate-limiter.js +2 -2
- package/dist/rate-limiter.js.map +2 -2
- package/dist/screen-reader.js +1 -0
- package/dist/screen-reader.js.map +2 -2
- package/dist/service-worker-interceptor.js +136 -0
- package/dist/service-worker-interceptor.js.map +7 -0
- package/dist/session-capture.js +4 -2
- package/dist/session-capture.js.map +2 -2
- package/dist/shadow-dom-crawler.js +132 -0
- package/dist/shadow-dom-crawler.js.map +7 -0
- package/dist/sitemap.js +16 -10
- package/dist/sitemap.js.map +2 -2
- package/dist/state-graph.js +193 -0
- package/dist/state-graph.js.map +7 -0
- package/dist/stealth/human-behavior.js +36 -3
- package/dist/stealth/human-behavior.js.map +2 -2
- package/dist/url-pattern-completer.js +48 -14
- package/dist/url-pattern-completer.js.map +2 -2
- package/dist/visual-diff-verdict.js +3 -1
- package/dist/visual-diff-verdict.js.map +2 -2
- package/package.json +2 -1
- package/src/accessibility-tree-crawler.ts +246 -0
- package/src/advanced-ui-probes.ts +470 -0
- package/src/crawl-session-manager.ts +231 -0
- package/src/crawler.ts +1196 -150
- package/src/crud-interaction.ts +386 -0
- package/src/dropdown-enumerator.ts +312 -0
- package/src/filter-pagination-crawler.ts +500 -0
- package/src/gap-pass.ts +58 -16
- package/src/index.ts +14 -1
- package/src/job-queue.ts +9 -0
- package/src/locator-extractor.ts +45 -0
- package/src/multi-context-handler.ts +296 -0
- package/src/nested-window-crawler.ts +310 -0
- package/src/network-action-correlator.ts +320 -0
- package/src/performance-metrics-collector.ts +243 -0
- package/src/performance-metrics.ts +1 -1
- package/src/screen-reader.ts +4 -1
- package/src/service-worker-interceptor.ts +185 -0
- package/src/session-capture.ts +3 -0
- package/src/shadow-dom-crawler.ts +198 -0
- package/src/state-graph.ts +280 -0
- package/src/stealth/human-behavior.ts +38 -4
- package/src/url-pattern-completer.ts +61 -15
- package/tsconfig.json +1 -1
- package/types/accessibility-tree-crawler.d.ts +44 -0
- package/types/advanced-ui-probes.d.ts +47 -0
- package/types/crawl-session-manager.d.ts +44 -0
- package/types/crawler.d.ts +3 -1
- package/types/crud-interaction.d.ts +31 -0
- package/types/dropdown-enumerator.d.ts +20 -0
- package/types/filter-pagination-crawler.d.ts +52 -0
- package/types/gap-pass.d.ts +1 -0
- package/types/index.d.ts +14 -1
- package/types/job-queue.d.ts +18 -9
- package/types/locator-extractor.d.ts +7 -0
- package/types/multi-context-handler.d.ts +55 -0
- package/types/nested-window-crawler.d.ts +41 -0
- package/types/network-action-correlator.d.ts +47 -0
- package/types/performance-metrics-collector.d.ts +50 -0
- package/types/performance-metrics.d.ts +1 -2
- package/types/screen-reader.d.ts +1 -2
- package/types/service-worker-interceptor.d.ts +36 -0
- package/types/session-capture.d.ts +1 -1
- package/types/shadow-dom-crawler.d.ts +35 -0
- package/types/sitemap.d.ts +2 -2
- package/types/state-graph.d.ts +59 -0
- package/types/stealth/human-behavior.d.ts +1 -1
|
@@ -104,29 +104,62 @@ export async function injectFingerprintOverrides(page: Page): Promise<void> {
|
|
|
104
104
|
|
|
105
105
|
export async function infiniteScrollToBottom(
|
|
106
106
|
page: Page,
|
|
107
|
-
opts: { maxScrolls?: number; scrollDelay?: number; stabilizeMs?: number } = {}
|
|
107
|
+
opts: { maxScrolls?: number; scrollDelay?: number; stabilizeMs?: number } = {},
|
|
108
|
+
rawPage?: any,
|
|
108
109
|
): Promise<{ scrollCount: number; heightReached: number }> {
|
|
109
110
|
const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;
|
|
110
111
|
let scrollCount = 0;
|
|
111
112
|
let stableScrollCount = 0;
|
|
112
113
|
let previousHeight = 0;
|
|
113
114
|
|
|
115
|
+
// One CDP session for wheel events (triggers IntersectionObserver-based virtual scrollers).
|
|
116
|
+
// React-window / react-virtualized / TanStack Virtual ignore window.scrollBy but respond to
|
|
117
|
+
// wheel events because they attach their own scroll listeners to the list container.
|
|
118
|
+
let cdpForWheel: any = null;
|
|
119
|
+
let viewW = 1280;
|
|
120
|
+
let viewH = 800;
|
|
121
|
+
if (rawPage && typeof rawPage.context === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
const ctx = rawPage.context();
|
|
124
|
+
if (typeof ctx?.newCDPSession === 'function') {
|
|
125
|
+
cdpForWheel = await ctx.newCDPSession(rawPage);
|
|
126
|
+
const { result } = await cdpForWheel.send('Runtime.evaluate', {
|
|
127
|
+
expression: `({"w":window.innerWidth,"h":window.innerHeight})`,
|
|
128
|
+
returnByValue: true,
|
|
129
|
+
}).catch(() => ({ result: { value: null } }));
|
|
130
|
+
if (result?.value) { viewW = result.value.w ?? 1280; viewH = result.value.h ?? 800; }
|
|
131
|
+
}
|
|
132
|
+
} catch { cdpForWheel = null; }
|
|
133
|
+
}
|
|
134
|
+
|
|
114
135
|
try {
|
|
115
136
|
while (scrollCount < maxScrolls) {
|
|
116
137
|
previousHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
|
|
117
138
|
|
|
118
139
|
const scrollBy = 500 + Math.floor(Math.random() * 300);
|
|
140
|
+
// window.scrollBy handles standard scroll containers
|
|
119
141
|
await page.evaluate((by: number) => window.scrollBy(0, by), scrollBy).catch(() => {});
|
|
142
|
+
// CDP wheel event triggers IntersectionObserver + virtual list scroll listeners
|
|
143
|
+
if (cdpForWheel) {
|
|
144
|
+
await cdpForWheel.send('Input.dispatchMouseEvent', {
|
|
145
|
+
type: 'mouseWheel',
|
|
146
|
+
x: Math.floor(viewW / 2),
|
|
147
|
+
y: Math.floor(viewH / 2),
|
|
148
|
+
deltaX: 0,
|
|
149
|
+
deltaY: scrollBy,
|
|
150
|
+
modifiers: 0,
|
|
151
|
+
}).catch(() => {});
|
|
152
|
+
}
|
|
120
153
|
await humanDelay(scrollDelay - 100, scrollDelay + 200);
|
|
121
154
|
|
|
122
|
-
//
|
|
123
|
-
await
|
|
155
|
+
// Use Node.js setTimeout — page.waitForTimeout may not be available on Stagehand proxy
|
|
156
|
+
await new Promise<void>(r => setTimeout(r, stabilizeMs));
|
|
124
157
|
scrollCount++;
|
|
125
158
|
|
|
126
159
|
const newHeight: number = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
|
|
127
160
|
if (newHeight - previousHeight < 10) {
|
|
128
161
|
stableScrollCount++;
|
|
129
|
-
if (stableScrollCount >= 3) break; // no
|
|
162
|
+
if (stableScrollCount >= 3) break; // no new content after 3 consecutive non-growing scrolls
|
|
130
163
|
} else {
|
|
131
164
|
stableScrollCount = 0;
|
|
132
165
|
}
|
|
@@ -134,6 +167,7 @@ export async function infiniteScrollToBottom(
|
|
|
134
167
|
}
|
|
135
168
|
} catch { /* non-fatal */ }
|
|
136
169
|
|
|
170
|
+
if (cdpForWheel) cdpForWheel.detach().catch(() => {});
|
|
137
171
|
return { scrollCount, heightReached: previousHeight };
|
|
138
172
|
}
|
|
139
173
|
|
|
@@ -27,6 +27,33 @@ interface LlmCfg {
|
|
|
27
27
|
baseUrl?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Only these two providers are supported by this module (they use standard SDK calls).
|
|
31
|
+
// NVIDIA, groq, ollama, openrouter, etc. are skipped — fall through to env fallback.
|
|
32
|
+
const SUPPORTED = new Set(['openai', 'anthropic']);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the effective LLM to use. Priority: openai → anthropic → skip others.
|
|
36
|
+
* Falls back to env vars when the platform-configured provider is unsupported.
|
|
37
|
+
*/
|
|
38
|
+
function resolveTextLLM(llmCfg: LlmCfg | null | undefined): { provider: 'openai' | 'anthropic'; apiKey: string; model: string; baseUrl?: string } | null {
|
|
39
|
+
// Use platform config if it's a supported provider with a key
|
|
40
|
+
if (llmCfg?.provider === 'openai' && llmCfg.apiKey) {
|
|
41
|
+
return { provider: 'openai', apiKey: llmCfg.apiKey, model: llmCfg.model, baseUrl: llmCfg.baseUrl };
|
|
42
|
+
}
|
|
43
|
+
if (llmCfg?.provider === 'anthropic' && llmCfg.apiKey) {
|
|
44
|
+
return { provider: 'anthropic', apiKey: llmCfg.apiKey, model: llmCfg.model };
|
|
45
|
+
}
|
|
46
|
+
// Platform provider is unsupported (nvidia, groq, etc.) or no key — fall back to env vars.
|
|
47
|
+
// OpenAI takes priority over Anthropic.
|
|
48
|
+
if (process.env.OPENAI_API_KEY) {
|
|
49
|
+
return { provider: 'openai', apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini' };
|
|
50
|
+
}
|
|
51
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
52
|
+
return { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-haiku-4-5-20251001' };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
/**
|
|
31
58
|
* Returns validated URL candidates (HEAD=200) predicted by LLM from
|
|
32
59
|
* the pattern of already-discovered URLs.
|
|
@@ -36,7 +63,8 @@ export async function runUrlPatternCompletion(
|
|
|
36
63
|
appUrl: string,
|
|
37
64
|
llmCfg: LlmCfg | null | undefined,
|
|
38
65
|
): Promise<UrlCandidate[]> {
|
|
39
|
-
|
|
66
|
+
const effective = resolveTextLLM(llmCfg);
|
|
67
|
+
if (!effective || discoveredUrls.length < 2) return [];
|
|
40
68
|
|
|
41
69
|
const origin = (() => { try { return new URL(appUrl).origin; } catch { return ''; } })();
|
|
42
70
|
if (!origin) return [];
|
|
@@ -49,7 +77,8 @@ export async function runUrlPatternCompletion(
|
|
|
49
77
|
|
|
50
78
|
if (internalUrls.length < 2) return [];
|
|
51
79
|
|
|
52
|
-
const
|
|
80
|
+
const systemPrompt = 'You are a URL pattern analyst. Always respond with valid JSON only.';
|
|
81
|
+
const userPrompt = `You are analyzing a web application's URL structure to find screens that exist but weren't discovered during automated crawling.
|
|
53
82
|
|
|
54
83
|
Discovered URLs:
|
|
55
84
|
${internalUrls.map(u => { try { return new URL(u).pathname + new URL(u).search; } catch { return u; } }).join('\n')}
|
|
@@ -67,18 +96,35 @@ Respond with ONLY valid JSON, no explanation:
|
|
|
67
96
|
{"candidates":[{"path":"/settings/billing","confidence":0.85,"reason":"settings subpage pattern from /settings/profile"},{"path":"/admin/users","confidence":0.6,"reason":"admin prefix implied by /admin/dashboard"}]}`;
|
|
68
97
|
|
|
69
98
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
let text = '';
|
|
100
|
+
|
|
101
|
+
if (effective.provider === 'openai') {
|
|
102
|
+
const { OpenAI } = await import('openai');
|
|
103
|
+
const client = new OpenAI({ apiKey: effective.apiKey, ...(effective.baseUrl ? { baseURL: effective.baseUrl } : {}) });
|
|
104
|
+
const response = await client.chat.completions.create({
|
|
105
|
+
model: effective.model ?? 'gpt-4o-mini',
|
|
106
|
+
max_tokens: 1024,
|
|
107
|
+
temperature: 0,
|
|
108
|
+
messages: [
|
|
109
|
+
{ role: 'system', content: systemPrompt },
|
|
110
|
+
{ role: 'user', content: userPrompt },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
text = response.choices[0]?.message?.content ?? '';
|
|
114
|
+
} else {
|
|
115
|
+
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
116
|
+
const client = new Anthropic({ apiKey: effective.apiKey });
|
|
117
|
+
const response = await client.messages.create({
|
|
118
|
+
model: effective.model ?? 'claude-haiku-4-5-20251001',
|
|
119
|
+
max_tokens: 1024,
|
|
120
|
+
temperature: 0,
|
|
121
|
+
// @ts-ignore: cache_control supported in beta
|
|
122
|
+
system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
|
|
123
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
124
|
+
});
|
|
125
|
+
text = (response.content[0] as any)?.text ?? '';
|
|
126
|
+
}
|
|
127
|
+
|
|
82
128
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
83
129
|
if (!jsonMatch) return [];
|
|
84
130
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
@@ -106,7 +152,7 @@ Respond with ONLY valid JSON, no explanation:
|
|
|
106
152
|
}
|
|
107
153
|
|
|
108
154
|
const hits = results.filter(r => r.status === 'hit');
|
|
109
|
-
logger.info({ total: results.length, hits: hits.length }, '[url-completer] pattern completion done');
|
|
155
|
+
logger.info({ total: results.length, hits: hits.length, provider: effective.provider }, '[url-completer] pattern completion done');
|
|
110
156
|
return results;
|
|
111
157
|
} catch (err) {
|
|
112
158
|
logger.warn({ err: String(err) }, '[url-completer] LLM call failed — skipping pattern completion');
|
package/tsconfig.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility tree crawler — extract landmark, heading, and ARIA metadata from crawled pages.
|
|
3
|
+
*
|
|
4
|
+
* Captures: landmarks with interactive counts, heading hierarchy with violation detection,
|
|
5
|
+
* interactive elements missing accessible names, live regions, and skip-link presence.
|
|
6
|
+
*
|
|
7
|
+
* This data feeds ZeTa's accessibility test generator to surface WCAG 2.1 Level A/AA
|
|
8
|
+
* failures (missing names, heading skips, absent skip links, unlabeled live regions).
|
|
9
|
+
*/
|
|
10
|
+
export interface LandmarkInfo {
|
|
11
|
+
role: string;
|
|
12
|
+
label: string;
|
|
13
|
+
elementId: string;
|
|
14
|
+
interactiveCount: number;
|
|
15
|
+
}
|
|
16
|
+
export interface HeadingInfo {
|
|
17
|
+
level: number;
|
|
18
|
+
text: string;
|
|
19
|
+
elementId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface MissingNameEl {
|
|
22
|
+
tagName: string;
|
|
23
|
+
role: string;
|
|
24
|
+
elementId: string;
|
|
25
|
+
htmlSnippet: string;
|
|
26
|
+
}
|
|
27
|
+
export interface LiveRegionInfo {
|
|
28
|
+
role: string;
|
|
29
|
+
ariaLive: string;
|
|
30
|
+
ariaAtomic: string;
|
|
31
|
+
currentText: string;
|
|
32
|
+
}
|
|
33
|
+
export interface AccessibilityTreeResult {
|
|
34
|
+
landmarkCount: number;
|
|
35
|
+
landmarks: LandmarkInfo[];
|
|
36
|
+
headings: HeadingInfo[];
|
|
37
|
+
headingHierarchyViolations: string[];
|
|
38
|
+
missingNameElements: MissingNameEl[];
|
|
39
|
+
missingNameCount: number;
|
|
40
|
+
liveRegions: LiveRegionInfo[];
|
|
41
|
+
totalInteractiveCount: number;
|
|
42
|
+
hasSkipLink: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function crawlAccessibilityTree(page: any): Promise<AccessibilityTreeResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced UI probes — detect and exercise drag-drop, date pickers, sliders,
|
|
3
|
+
* and typeahead/autocomplete widgets on a crawled page.
|
|
4
|
+
*
|
|
5
|
+
* Results feed ZeTa's test generator so it can emit interaction-level test
|
|
6
|
+
* cases for these widget families instead of falling back to generic clicks.
|
|
7
|
+
*
|
|
8
|
+
* Each probe is independently try/caught so a broken widget does not abort
|
|
9
|
+
* the others. All browser-side code runs inside page.evaluate / page.$$eval
|
|
10
|
+
* to avoid serialisation issues with DOM handles.
|
|
11
|
+
*/
|
|
12
|
+
import type { Page } from 'playwright';
|
|
13
|
+
export interface DragDropCapture {
|
|
14
|
+
library: 'html5' | 'dnd-kit' | 'react-beautiful-dnd' | 'sortablejs' | 'pointer-event' | 'none';
|
|
15
|
+
draggableCount: number;
|
|
16
|
+
dropZoneCount: number;
|
|
17
|
+
testAttempted: boolean;
|
|
18
|
+
testCompleted: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface DatePickerCapture {
|
|
21
|
+
fieldLabel: string;
|
|
22
|
+
pickerType: 'calendar-popup' | 'native-date' | 'native-datetime' | 'none';
|
|
23
|
+
minDate?: string;
|
|
24
|
+
maxDate?: string;
|
|
25
|
+
disabledDatesFound: boolean;
|
|
26
|
+
sampleSelectedValue?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SliderCapture {
|
|
29
|
+
label: string;
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
step: number;
|
|
33
|
+
currentValue: number;
|
|
34
|
+
}
|
|
35
|
+
export interface TypeaheadCapture {
|
|
36
|
+
label: string;
|
|
37
|
+
minCharsToTrigger: number;
|
|
38
|
+
sampleSuggestions: string[];
|
|
39
|
+
noResultsText: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface AdvancedProbeResult {
|
|
42
|
+
dragDrop: DragDropCapture;
|
|
43
|
+
datePickers: DatePickerCapture[];
|
|
44
|
+
sliders: SliderCapture[];
|
|
45
|
+
typeaheads: TypeaheadCapture[];
|
|
46
|
+
}
|
|
47
|
+
export declare function probeAdvancedUI(page: Page): Promise<AdvancedProbeResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management for authenticated crawl sessions.
|
|
3
|
+
* Detects auth expiry via HTTP status codes and login-page redirects,
|
|
4
|
+
* then attempts recovery via a token refresh endpoint or re-login flow.
|
|
5
|
+
* Designed to keep long-running crawls alive without manual intervention.
|
|
6
|
+
*/
|
|
7
|
+
import type { Page } from 'playwright';
|
|
8
|
+
export interface SessionConfig {
|
|
9
|
+
loginUrl?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
password?: string;
|
|
12
|
+
healthCheckUrl?: string;
|
|
13
|
+
refreshEndpoint?: string;
|
|
14
|
+
tokenLocalStorageKey?: string;
|
|
15
|
+
maxRefreshAttempts?: number;
|
|
16
|
+
}
|
|
17
|
+
export type SessionEventType = 'HEALTHY' | 'EXPIRY_DETECTED' | 'REFRESH_ATTEMPT' | 'REFRESH_SUCCESS' | 'REFRESH_FAILED' | 'AUTH_FAILURE';
|
|
18
|
+
export interface SessionEvent {
|
|
19
|
+
type: SessionEventType;
|
|
20
|
+
isoTimestamp: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
strategy?: string;
|
|
23
|
+
attemptCount?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionManager {
|
|
26
|
+
onResponse(url: string, status: number): void;
|
|
27
|
+
onNavigation(url: string): void;
|
|
28
|
+
isHealthy(): boolean;
|
|
29
|
+
needsRefresh(): boolean;
|
|
30
|
+
markHealthy(isoTimestamp: string): void;
|
|
31
|
+
refresh(page: Page, isoTimestamp: string): Promise<boolean>;
|
|
32
|
+
getEvents(): SessionEvent[];
|
|
33
|
+
getStats(): {
|
|
34
|
+
healthy: boolean;
|
|
35
|
+
refreshAttempts: number;
|
|
36
|
+
authFailure: boolean;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export declare function createSessionManager(config: SessionConfig): SessionManager;
|
|
40
|
+
/**
|
|
41
|
+
* Attaches a Playwright response listener to feed HTTP status codes into the
|
|
42
|
+
* session manager. Returns a cleanup function to remove the listener.
|
|
43
|
+
*/
|
|
44
|
+
export declare function attachSessionWatcher(page: Page, manager: SessionManager): () => void;
|
package/types/crawler.d.ts
CHANGED
|
@@ -164,6 +164,7 @@ export declare function crawlProject(opts: {
|
|
|
164
164
|
segmentIndex: number;
|
|
165
165
|
hasMoreSegments: boolean;
|
|
166
166
|
domHashes: string[];
|
|
167
|
+
crawlWarnings?: undefined;
|
|
167
168
|
} | {
|
|
168
169
|
screensFound: number;
|
|
169
170
|
screensErrored: number;
|
|
@@ -188,9 +189,10 @@ export declare function crawlProject(opts: {
|
|
|
188
189
|
segmentIndex: number;
|
|
189
190
|
hasMoreSegments: boolean;
|
|
190
191
|
domHashes: string[];
|
|
192
|
+
crawlWarnings: string[] | undefined;
|
|
191
193
|
discoveredUrls?: undefined;
|
|
192
194
|
}>;
|
|
193
|
-
export declare function domFallbackElements(page: any): Promise<Array<{
|
|
195
|
+
export declare function domFallbackElements(page: any, rawPage?: any): Promise<Array<{
|
|
194
196
|
meaning: string;
|
|
195
197
|
role: string;
|
|
196
198
|
expectedData?: string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export type EditFlowType = 'inline' | 'modal' | 'navigation' | 'unknown';
|
|
3
|
+
export interface CrudFlowCapture {
|
|
4
|
+
entityName: string;
|
|
5
|
+
createTrigger?: string;
|
|
6
|
+
editFlowType: EditFlowType;
|
|
7
|
+
editFields: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
prefilledValue?: string;
|
|
11
|
+
}>;
|
|
12
|
+
deleteConfirmation?: {
|
|
13
|
+
message: string;
|
|
14
|
+
confirmLabel: string;
|
|
15
|
+
cancelLabel: string;
|
|
16
|
+
};
|
|
17
|
+
createSuccessToast?: string;
|
|
18
|
+
editSuccessToast?: string;
|
|
19
|
+
rowCount: number;
|
|
20
|
+
hasDeleteButton: boolean;
|
|
21
|
+
hasEditButton: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface CrudProbeResult {
|
|
24
|
+
flows: CrudFlowCapture[];
|
|
25
|
+
toastsObserved: Array<{
|
|
26
|
+
action: string;
|
|
27
|
+
text: string;
|
|
28
|
+
role: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export declare function probeCrudFlows(page: Page, entityName?: string): Promise<CrudProbeResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export interface DropdownOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface DropdownDef {
|
|
8
|
+
kind: 'native-select' | 'combobox' | 'listbox' | 'multiselect';
|
|
9
|
+
label: string;
|
|
10
|
+
locatorHint: string;
|
|
11
|
+
options: DropdownOption[];
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
cascadesFrom?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DropdownEnumerationResult {
|
|
16
|
+
dropdowns: DropdownDef[];
|
|
17
|
+
conditionalRevealMap: Record<string, string[]>;
|
|
18
|
+
totalOptionsFound: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function enumerateDropdowns(page: Page): Promise<DropdownEnumerationResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter state crawling and pagination capture.
|
|
3
|
+
*
|
|
4
|
+
* Discovers filter controls (select, checkbox-group, radio-group, search),
|
|
5
|
+
* applies each meaningful option and records result counts and first-row
|
|
6
|
+
* values, then detects and walks pagination (numbered, load-more, infinite
|
|
7
|
+
* scroll). Also captures sort column behaviour.
|
|
8
|
+
*
|
|
9
|
+
* All per-filter / per-page errors are caught and skipped so one bad control
|
|
10
|
+
* never aborts the full crawl.
|
|
11
|
+
*/
|
|
12
|
+
import type { Page } from 'playwright';
|
|
13
|
+
export interface FilterOption {
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
export interface FilterDef {
|
|
18
|
+
kind: 'select' | 'checkbox-group' | 'radio-group' | 'search';
|
|
19
|
+
label: string;
|
|
20
|
+
locatorHint: string;
|
|
21
|
+
options: FilterOption[];
|
|
22
|
+
}
|
|
23
|
+
export interface FilterState {
|
|
24
|
+
filterLabel: string;
|
|
25
|
+
filterValue: string;
|
|
26
|
+
resultCount: number | null;
|
|
27
|
+
firstRowValues: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface PaginationCapture {
|
|
30
|
+
kind: 'numbered' | 'load-more' | 'infinite-scroll' | 'none';
|
|
31
|
+
totalPages?: number;
|
|
32
|
+
totalItems?: number;
|
|
33
|
+
pageSizes?: number[];
|
|
34
|
+
pages: Array<{
|
|
35
|
+
pageNum: number;
|
|
36
|
+
rowValues: string[];
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export interface FilterPaginationResult {
|
|
40
|
+
filters: FilterDef[];
|
|
41
|
+
filterStates: FilterState[];
|
|
42
|
+
pagination: PaginationCapture;
|
|
43
|
+
sortColumns: Array<{
|
|
44
|
+
header: string;
|
|
45
|
+
ariaSortAfterClick: string | null;
|
|
46
|
+
}>;
|
|
47
|
+
searchCapture?: {
|
|
48
|
+
noResultsText: string | null;
|
|
49
|
+
sampleSuggestions: string[];
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function crawlFiltersAndPagination(page: Page): Promise<FilterPaginationResult>;
|
package/types/gap-pass.d.ts
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -35,5 +35,18 @@ export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemet
|
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
36
|
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
37
|
export type { FailureContext } from './failure-analysis-agent.js';
|
|
38
|
-
export { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt } from './locator-extractor.js';
|
|
38
|
+
export { elementToPlaywrightLocator, elementsToPlaywrightLocators, formatLocatorsForPrompt, detectAuthWallLocators } from './locator-extractor.js';
|
|
39
39
|
export type { PlaywrightLocator } from './locator-extractor.js';
|
|
40
|
+
export * from './dropdown-enumerator.js';
|
|
41
|
+
export * from './crud-interaction.js';
|
|
42
|
+
export * from './filter-pagination-crawler.js';
|
|
43
|
+
export * from './multi-context-handler.js';
|
|
44
|
+
export * from './advanced-ui-probes.js';
|
|
45
|
+
export * from './state-graph.js';
|
|
46
|
+
export * from './nested-window-crawler.js';
|
|
47
|
+
export * from './shadow-dom-crawler.js';
|
|
48
|
+
export * from './network-action-correlator.js';
|
|
49
|
+
export * from './crawl-session-manager.js';
|
|
50
|
+
export * from './accessibility-tree-crawler.js';
|
|
51
|
+
export * from './performance-metrics.js';
|
|
52
|
+
export * from './service-worker-interceptor.js';
|
package/types/job-queue.d.ts
CHANGED
|
@@ -111,22 +111,24 @@ export declare function createTrackedJob(type: string, config: {
|
|
|
111
111
|
projectId: string;
|
|
112
112
|
[k: string]: any;
|
|
113
113
|
}): Promise<string>;
|
|
114
|
-
/** Mark a tracked job DONE or FAILED. Safe to call in finally — swallows errors.
|
|
114
|
+
/** Mark a tracked job DONE or FAILED. Safe to call in finally — swallows errors.
|
|
115
|
+
* Gap 30: When marking FAILED, sets nextRetryAt (1 min) so the retry worker picks it up promptly. */
|
|
115
116
|
export declare function finishTrackedJob(tenantId: string, jobId: string, error?: string): Promise<void>;
|
|
116
117
|
/** Enqueue a generic platform job — returns the job ID immediately. */
|
|
117
118
|
export declare function enqueueJob(type: string, config: any): Promise<string>;
|
|
118
119
|
/** Get the current state of a platform job. */
|
|
119
120
|
export declare function getJob(tenantId: string, jobId: string): Promise<{
|
|
120
121
|
progress: number;
|
|
121
|
-
|
|
122
|
+
id: string;
|
|
123
|
+
name: string | null;
|
|
122
124
|
type: string;
|
|
125
|
+
error: string | null;
|
|
126
|
+
result: import("@prisma/client/runtime/library").JsonValue | null;
|
|
123
127
|
status: string;
|
|
124
128
|
tenantId: string;
|
|
125
129
|
projectId: string;
|
|
126
|
-
name: string | null;
|
|
127
|
-
result: import("@prisma/client/runtime/library").JsonValue | null;
|
|
128
|
-
id: string;
|
|
129
130
|
config: import("@prisma/client/runtime/library").JsonValue;
|
|
131
|
+
retryCount: number;
|
|
130
132
|
queuedAt: Date;
|
|
131
133
|
startedAt: Date | null;
|
|
132
134
|
finishedAt: Date | null;
|
|
@@ -137,19 +139,23 @@ export declare function getJob(tenantId: string, jobId: string): Promise<{
|
|
|
137
139
|
totalCacheReadTokens: number | null;
|
|
138
140
|
totalEstimatedCostUsd: number | null;
|
|
139
141
|
llmCallCount: number;
|
|
142
|
+
nextRetryAt: Date | null;
|
|
143
|
+
dlq: boolean;
|
|
144
|
+
dlqReason: string | null;
|
|
140
145
|
} | null>;
|
|
141
146
|
/** List recent jobs for a project. */
|
|
142
147
|
export declare function listJobs(tenantId: string, projectId: string): Promise<{
|
|
143
148
|
progress: number;
|
|
144
|
-
|
|
149
|
+
id: string;
|
|
150
|
+
name: string | null;
|
|
145
151
|
type: string;
|
|
152
|
+
error: string | null;
|
|
153
|
+
result: import("@prisma/client/runtime/library").JsonValue | null;
|
|
146
154
|
status: string;
|
|
147
155
|
tenantId: string;
|
|
148
156
|
projectId: string;
|
|
149
|
-
name: string | null;
|
|
150
|
-
result: import("@prisma/client/runtime/library").JsonValue | null;
|
|
151
|
-
id: string;
|
|
152
157
|
config: import("@prisma/client/runtime/library").JsonValue;
|
|
158
|
+
retryCount: number;
|
|
153
159
|
queuedAt: Date;
|
|
154
160
|
startedAt: Date | null;
|
|
155
161
|
finishedAt: Date | null;
|
|
@@ -160,6 +166,9 @@ export declare function listJobs(tenantId: string, projectId: string): Promise<{
|
|
|
160
166
|
totalCacheReadTokens: number | null;
|
|
161
167
|
totalEstimatedCostUsd: number | null;
|
|
162
168
|
llmCallCount: number;
|
|
169
|
+
nextRetryAt: Date | null;
|
|
170
|
+
dlq: boolean;
|
|
171
|
+
dlqReason: string | null;
|
|
163
172
|
}[]>;
|
|
164
173
|
/** Cancel a RUNNING or QUEUED job. Aborts the in-process crawl and marks DB CANCELLED. */
|
|
165
174
|
export declare function cancelJob(tenantId: string, jobId: string): Promise<void>;
|
|
@@ -22,6 +22,7 @@ export declare function elementToPlaywrightLocator(element: {
|
|
|
22
22
|
/**
|
|
23
23
|
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
24
24
|
* Deduplicates by key — higher confidence wins.
|
|
25
|
+
* Filters out crawl-analysis meta-annotations and produces an auth-wall warning when applicable.
|
|
25
26
|
*/
|
|
26
27
|
export declare function elementsToPlaywrightLocators(elements: Array<{
|
|
27
28
|
role?: string | null;
|
|
@@ -32,6 +33,12 @@ export declare function elementsToPlaywrightLocators(elements: Array<{
|
|
|
32
33
|
isInteractive?: boolean | null;
|
|
33
34
|
ariaState?: Record<string, unknown> | null;
|
|
34
35
|
}>): PlaywrightLocator[];
|
|
36
|
+
/**
|
|
37
|
+
* Detect whether a set of locators was extracted from an auth-wall page.
|
|
38
|
+
* Returns a warning message if auth-wall detected, null otherwise.
|
|
39
|
+
* Heuristic: >50% of locators are low-confidence text matches AND at least one matches auth-wall pattern.
|
|
40
|
+
*/
|
|
41
|
+
export declare function detectAuthWallLocators(locators: PlaywrightLocator[]): string | null;
|
|
35
42
|
/**
|
|
36
43
|
* Format locators as a block for injection into LLM script generation prompts.
|
|
37
44
|
* Returns empty string if no locators.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-context browser handling — detects and records new tabs, iframes,
|
|
3
|
+
* popups, and file downloads triggered during a page crawl.
|
|
4
|
+
* This lets ZeTa discover cross-origin flows, OAuth redirects, embedded
|
|
5
|
+
* third-party content, and downloadable resources that single-page HTTP
|
|
6
|
+
* crawlers would miss entirely.
|
|
7
|
+
*/
|
|
8
|
+
import type { Page, BrowserContext } from 'playwright';
|
|
9
|
+
export interface NewTabRecord {
|
|
10
|
+
url: string;
|
|
11
|
+
linkText: string;
|
|
12
|
+
isInternal: boolean;
|
|
13
|
+
httpStatus?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface IframeRecord {
|
|
16
|
+
src: string;
|
|
17
|
+
crossOrigin: boolean;
|
|
18
|
+
sandboxed: boolean;
|
|
19
|
+
sandboxValue: string | null;
|
|
20
|
+
accessible: boolean;
|
|
21
|
+
contentSample: string;
|
|
22
|
+
hasForm: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface PopupRecord {
|
|
25
|
+
url: string;
|
|
26
|
+
triggerLabel: string;
|
|
27
|
+
isOAuthLike: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface DownloadRecord {
|
|
30
|
+
filename: string;
|
|
31
|
+
downloadUrl: string;
|
|
32
|
+
triggerLabel: string;
|
|
33
|
+
}
|
|
34
|
+
export interface MultiContextResult {
|
|
35
|
+
newTabs: NewTabRecord[];
|
|
36
|
+
iframes: IframeRecord[];
|
|
37
|
+
popups: PopupRecord[];
|
|
38
|
+
downloads: DownloadRecord[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Attaches a listener to the BrowserContext that records every new tab
|
|
42
|
+
* opened while the listener is active. Returns an unsubscribe function.
|
|
43
|
+
*
|
|
44
|
+
* Call this before navigating / clicking so that tabs opened during the
|
|
45
|
+
* crawl are captured, then call the returned function to stop listening.
|
|
46
|
+
*/
|
|
47
|
+
export declare function attachNewTabListener(context: BrowserContext, seedUrl: string, result: MultiContextResult): () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Probes the current page for multi-context browser events: iframes,
|
|
50
|
+
* popup windows, and file downloads.
|
|
51
|
+
*
|
|
52
|
+
* New-tab tracking is intentionally excluded here — it must be set up at
|
|
53
|
+
* the crawler level (before navigation) using `attachNewTabListener`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function probeMultiContext(page: Page, context: BrowserContext, seedUrl: string): Promise<MultiContextResult>;
|