@detiq/crawler 1.1.14 → 1.1.15

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 (109) hide show
  1. package/Dockerfile +1 -1
  2. package/dist/accessibility-tree-crawler.js +156 -0
  3. package/dist/accessibility-tree-crawler.js.map +7 -0
  4. package/dist/advanced-ui-probes.js +310 -0
  5. package/dist/advanced-ui-probes.js.map +7 -0
  6. package/dist/content-chunker.js +2 -2
  7. package/dist/content-chunker.js.map +2 -2
  8. package/dist/crawl-session-manager.js +145 -0
  9. package/dist/crawl-session-manager.js.map +7 -0
  10. package/dist/crawler.js +1142 -263
  11. package/dist/crawler.js.map +2 -2
  12. package/dist/crud-interaction.js +264 -0
  13. package/dist/crud-interaction.js.map +7 -0
  14. package/dist/dropdown-enumerator.js +230 -0
  15. package/dist/dropdown-enumerator.js.map +7 -0
  16. package/dist/filter-pagination-crawler.js +299 -0
  17. package/dist/filter-pagination-crawler.js.map +7 -0
  18. package/dist/frontier.js +5 -3
  19. package/dist/frontier.js.map +2 -2
  20. package/dist/gap-pass.js +47 -14
  21. package/dist/gap-pass.js.map +2 -2
  22. package/dist/http-engine.js +28 -15
  23. package/dist/http-engine.js.map +2 -2
  24. package/dist/index.js +15 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/job-queue.js +12 -5
  27. package/dist/job-queue.js.map +2 -2
  28. package/dist/locator-extractor.js +23 -0
  29. package/dist/locator-extractor.js.map +2 -2
  30. package/dist/multi-context-handler.js +182 -0
  31. package/dist/multi-context-handler.js.map +7 -0
  32. package/dist/nested-window-crawler.js +172 -0
  33. package/dist/nested-window-crawler.js.map +7 -0
  34. package/dist/network-action-correlator.js +209 -0
  35. package/dist/network-action-correlator.js.map +7 -0
  36. package/dist/performance-metrics-collector.js +155 -0
  37. package/dist/performance-metrics-collector.js.map +7 -0
  38. package/dist/performance-metrics.js.map +2 -2
  39. package/dist/policies/crawl-safety.js +5 -1
  40. package/dist/policies/crawl-safety.js.map +2 -2
  41. package/dist/profiles/domain-profile.js +21 -1
  42. package/dist/profiles/domain-profile.js.map +2 -2
  43. package/dist/rate-limiter.js +2 -2
  44. package/dist/rate-limiter.js.map +2 -2
  45. package/dist/screen-reader.js +1 -0
  46. package/dist/screen-reader.js.map +2 -2
  47. package/dist/service-worker-interceptor.js +136 -0
  48. package/dist/service-worker-interceptor.js.map +7 -0
  49. package/dist/session-capture.js +4 -2
  50. package/dist/session-capture.js.map +2 -2
  51. package/dist/shadow-dom-crawler.js +132 -0
  52. package/dist/shadow-dom-crawler.js.map +7 -0
  53. package/dist/sitemap.js +16 -10
  54. package/dist/sitemap.js.map +2 -2
  55. package/dist/state-graph.js +193 -0
  56. package/dist/state-graph.js.map +7 -0
  57. package/dist/stealth/human-behavior.js +36 -3
  58. package/dist/stealth/human-behavior.js.map +2 -2
  59. package/dist/url-pattern-completer.js +48 -14
  60. package/dist/url-pattern-completer.js.map +2 -2
  61. package/dist/visual-diff-verdict.js +3 -1
  62. package/dist/visual-diff-verdict.js.map +2 -2
  63. package/package.json +2 -1
  64. package/src/accessibility-tree-crawler.ts +246 -0
  65. package/src/advanced-ui-probes.ts +470 -0
  66. package/src/crawl-session-manager.ts +231 -0
  67. package/src/crawler.ts +1050 -134
  68. package/src/crud-interaction.ts +386 -0
  69. package/src/dropdown-enumerator.ts +312 -0
  70. package/src/filter-pagination-crawler.ts +500 -0
  71. package/src/gap-pass.ts +58 -16
  72. package/src/index.ts +14 -1
  73. package/src/locator-extractor.ts +45 -0
  74. package/src/multi-context-handler.ts +296 -0
  75. package/src/nested-window-crawler.ts +310 -0
  76. package/src/network-action-correlator.ts +320 -0
  77. package/src/performance-metrics-collector.ts +243 -0
  78. package/src/performance-metrics.ts +1 -1
  79. package/src/screen-reader.ts +4 -1
  80. package/src/service-worker-interceptor.ts +185 -0
  81. package/src/session-capture.ts +3 -0
  82. package/src/shadow-dom-crawler.ts +198 -0
  83. package/src/state-graph.ts +280 -0
  84. package/src/stealth/human-behavior.ts +38 -4
  85. package/src/url-pattern-completer.ts +61 -15
  86. package/tsconfig.json +1 -1
  87. package/types/accessibility-tree-crawler.d.ts +44 -0
  88. package/types/advanced-ui-probes.d.ts +47 -0
  89. package/types/crawl-session-manager.d.ts +44 -0
  90. package/types/crawler.d.ts +3 -1
  91. package/types/crud-interaction.d.ts +31 -0
  92. package/types/dropdown-enumerator.d.ts +20 -0
  93. package/types/filter-pagination-crawler.d.ts +52 -0
  94. package/types/gap-pass.d.ts +1 -0
  95. package/types/index.d.ts +14 -1
  96. package/types/job-queue.d.ts +18 -9
  97. package/types/locator-extractor.d.ts +7 -0
  98. package/types/multi-context-handler.d.ts +55 -0
  99. package/types/nested-window-crawler.d.ts +41 -0
  100. package/types/network-action-correlator.d.ts +47 -0
  101. package/types/performance-metrics-collector.d.ts +50 -0
  102. package/types/performance-metrics.d.ts +1 -2
  103. package/types/screen-reader.d.ts +1 -2
  104. package/types/service-worker-interceptor.d.ts +36 -0
  105. package/types/session-capture.d.ts +1 -1
  106. package/types/shadow-dom-crawler.d.ts +35 -0
  107. package/types/sitemap.d.ts +2 -2
  108. package/types/state-graph.d.ts +59 -0
  109. 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
- // Wait for potential lazy-loaded content
123
- await page.waitForTimeout(stabilizeMs).catch(() => {});
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 meaningful new content after 3 consecutive non-growing scrolls
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
- if (!llmCfg?.apiKey || discoveredUrls.length < 2) return [];
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 prompt = `You are analyzing a web application's URL structure to find screens that exist but weren't discovered during automated crawling.
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
- const Anthropic = (await import('@anthropic-ai/sdk')).default;
71
- const client = new Anthropic({ apiKey: llmCfg.apiKey });
72
- const response = await client.messages.create({
73
- model: llmCfg.model ?? process.env.CRAWLER_DEFAULT_MODEL ?? 'claude-haiku-4-5-20251001',
74
- max_tokens: 1024,
75
- temperature: 0,
76
- // @ts-ignore: cache_control supported in beta
77
- system: [{ type: 'text', text: 'You are a URL pattern analyst. Always respond with valid JSON only.', cache_control: { type: 'ephemeral' } }],
78
- messages: [{ role: 'user', content: prompt }],
79
- });
80
-
81
- const text = (response.content[0] as any)?.text ?? '';
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
@@ -3,7 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "rootDir": "src",
5
5
  "outDir": "dist",
6
- "lib": ["ES2022", "DOM"],
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
7
  "isolatedModules": true,
8
8
  "composite": false,
9
9
  "noEmitOnError": false
@@ -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;
@@ -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>;
@@ -26,6 +26,7 @@ interface LlmCfg {
26
26
  provider: string;
27
27
  model: string;
28
28
  apiKey?: string;
29
+ baseUrl?: string;
29
30
  }
30
31
  /**
31
32
  * Run LLM gap analysis + HEAD validation.
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';
@@ -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
- error: string | null;
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
- error: string | null;
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>;