@detiq/crawler 1.1.13 → 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 (112) 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 +1071 -142
  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/http-engine.ts +29 -15
  73. package/src/index.ts +14 -1
  74. package/src/job-queue.ts +3 -1
  75. package/src/locator-extractor.ts +45 -0
  76. package/src/multi-context-handler.ts +296 -0
  77. package/src/nested-window-crawler.ts +310 -0
  78. package/src/network-action-correlator.ts +320 -0
  79. package/src/performance-metrics-collector.ts +243 -0
  80. package/src/performance-metrics.ts +1 -1
  81. package/src/screen-reader.ts +4 -1
  82. package/src/service-worker-interceptor.ts +185 -0
  83. package/src/session-capture.ts +3 -0
  84. package/src/shadow-dom-crawler.ts +198 -0
  85. package/src/sitemap.ts +17 -10
  86. package/src/state-graph.ts +280 -0
  87. package/src/stealth/human-behavior.ts +38 -4
  88. package/src/url-pattern-completer.ts +61 -15
  89. package/tsconfig.json +1 -1
  90. package/types/accessibility-tree-crawler.d.ts +44 -0
  91. package/types/advanced-ui-probes.d.ts +47 -0
  92. package/types/crawl-session-manager.d.ts +44 -0
  93. package/types/crawler.d.ts +3 -1
  94. package/types/crud-interaction.d.ts +31 -0
  95. package/types/dropdown-enumerator.d.ts +20 -0
  96. package/types/filter-pagination-crawler.d.ts +52 -0
  97. package/types/gap-pass.d.ts +1 -0
  98. package/types/index.d.ts +14 -1
  99. package/types/job-queue.d.ts +18 -9
  100. package/types/locator-extractor.d.ts +7 -0
  101. package/types/multi-context-handler.d.ts +55 -0
  102. package/types/nested-window-crawler.d.ts +41 -0
  103. package/types/network-action-correlator.d.ts +47 -0
  104. package/types/performance-metrics-collector.d.ts +50 -0
  105. package/types/performance-metrics.d.ts +1 -2
  106. package/types/screen-reader.d.ts +1 -2
  107. package/types/service-worker-interceptor.d.ts +36 -0
  108. package/types/session-capture.d.ts +1 -1
  109. package/types/shadow-dom-crawler.d.ts +35 -0
  110. package/types/sitemap.d.ts +2 -2
  111. package/types/state-graph.d.ts +59 -0
  112. package/types/stealth/human-behavior.d.ts +1 -1
@@ -0,0 +1,209 @@
1
+ function extractUrlPattern(url) {
2
+ try {
3
+ const u = new URL(url);
4
+ const path = u.pathname.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ":uuid").replace(/\/[0-9a-f]{24}(\/|$)/g, "/:mongoId$1").replace(/\/\d{6,}(\/|$)/g, "/:id$1").replace(/\/\d{1,5}(\/|$)/g, "/:id$1");
5
+ return path;
6
+ } catch {
7
+ return url.slice(0, 80);
8
+ }
9
+ }
10
+ function extractBodySchema(bodyText) {
11
+ if (!bodyText || bodyText.length > 5e4) return null;
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(bodyText);
15
+ } catch {
16
+ return null;
17
+ }
18
+ const schema = {};
19
+ let keyCount = 0;
20
+ function traverse(value, prefix, depth) {
21
+ if (keyCount >= 30 || depth > 3) return;
22
+ if (value === null || value === void 0) return;
23
+ if (Array.isArray(value)) {
24
+ schema[prefix] = "array";
25
+ keyCount++;
26
+ return;
27
+ }
28
+ if (typeof value === "object") {
29
+ for (const [k, v] of Object.entries(value)) {
30
+ if (keyCount >= 30) break;
31
+ const key = prefix ? `${prefix}.${k}` : k;
32
+ if (v === null) {
33
+ schema[key] = "null";
34
+ keyCount++;
35
+ } else if (Array.isArray(v)) {
36
+ schema[key] = "array";
37
+ keyCount++;
38
+ } else if (typeof v === "object") {
39
+ traverse(v, key, depth + 1);
40
+ } else {
41
+ schema[key] = typeof v;
42
+ keyCount++;
43
+ }
44
+ }
45
+ return;
46
+ }
47
+ if (prefix) {
48
+ schema[prefix] = typeof value;
49
+ keyCount++;
50
+ }
51
+ }
52
+ traverse(parsed, "", 0);
53
+ return keyCount > 0 ? schema : null;
54
+ }
55
+ const ANALYTICS_DOMAINS = [
56
+ "google-analytics",
57
+ "doubleclick",
58
+ "hotjar",
59
+ "mixpanel",
60
+ "segment",
61
+ "sentry",
62
+ "datadog",
63
+ "newrelic",
64
+ "amplitude",
65
+ "fullstory"
66
+ ];
67
+ const API_PATH_PATTERNS = [
68
+ "/api/",
69
+ "/graphql",
70
+ "/v1/",
71
+ "/v2/",
72
+ "/rest/",
73
+ "/query",
74
+ "/rpc",
75
+ "/trpc"
76
+ ];
77
+ const EXCLUDED_RESOURCE_TYPES = /* @__PURE__ */ new Set(["image", "font", "stylesheet", "media", "websocket"]);
78
+ function isApiRequest(url, resourceType) {
79
+ if (EXCLUDED_RESOURCE_TYPES.has(resourceType)) return false;
80
+ try {
81
+ const hostname = new URL(url).hostname;
82
+ if (ANALYTICS_DOMAINS.some((d) => hostname.includes(d))) return false;
83
+ } catch {
84
+ return false;
85
+ }
86
+ if (resourceType === "xhr" || resourceType === "fetch") return true;
87
+ const lower = url.toLowerCase();
88
+ return API_PATH_PATTERNS.some((p) => lower.includes(p));
89
+ }
90
+ function createCorrelator() {
91
+ let currentLabel = null;
92
+ let currentRequests = [];
93
+ const correlations = [];
94
+ return {
95
+ startWindow(actionLabel) {
96
+ currentLabel = actionLabel;
97
+ currentRequests = [];
98
+ },
99
+ recordRequest(rec) {
100
+ if (currentLabel === null) return;
101
+ currentRequests.push({ ...rec, correlatedActionLabel: currentLabel });
102
+ },
103
+ endWindow() {
104
+ const label = currentLabel ?? "(unknown)";
105
+ const correlation = {
106
+ actionLabel: label,
107
+ requests: currentRequests
108
+ };
109
+ correlations.push(correlation);
110
+ currentLabel = null;
111
+ currentRequests = [];
112
+ return correlation;
113
+ },
114
+ getResult() {
115
+ const uniqueEndpoints = [
116
+ ...new Set(
117
+ correlations.flatMap((c) => c.requests.map((r) => r.urlPattern))
118
+ )
119
+ ];
120
+ const graphqlOperations = [
121
+ ...new Set(
122
+ correlations.flatMap((c) => c.requests).filter((r) => r.isGraphQL && r.graphQLOperation).map((r) => r.graphQLOperation)
123
+ )
124
+ ];
125
+ const totalRequestsCaptured = correlations.reduce(
126
+ (sum, c) => sum + c.requests.length,
127
+ 0
128
+ );
129
+ const coverage = correlations.length === 0 ? 0 : correlations.filter((c) => c.requests.length >= 1).length / correlations.length;
130
+ return {
131
+ correlations,
132
+ uniqueEndpoints,
133
+ graphqlOperations,
134
+ totalRequestsCaptured,
135
+ coverage
136
+ };
137
+ }
138
+ };
139
+ }
140
+ function attachNetworkInterceptor(page, correlator) {
141
+ const pendingRequests = /* @__PURE__ */ new Map();
142
+ const onRequest = (req) => {
143
+ const url = req.url();
144
+ const resourceType = req.resourceType();
145
+ if (!isApiRequest(url, resourceType)) return;
146
+ let bodyText = "";
147
+ try {
148
+ bodyText = req.postData() ?? "";
149
+ } catch {
150
+ }
151
+ pendingRequests.set(url, { bodyText, resourceType });
152
+ };
153
+ const onResponse = async (res) => {
154
+ const url = res.url();
155
+ const pending = pendingRequests.get(url);
156
+ if (!pending) return;
157
+ pendingRequests.delete(url);
158
+ const method = res.request().method();
159
+ let bodySchema = null;
160
+ try {
161
+ bodySchema = extractBodySchema(pending.bodyText);
162
+ } catch {
163
+ }
164
+ let responseSchema = null;
165
+ let isGraphQL = false;
166
+ let graphQLOperation;
167
+ const contentType = res.headers()["content-type"] ?? "";
168
+ if (contentType.includes("application/json")) {
169
+ try {
170
+ const respText = await res.text();
171
+ if (respText.length < 2e4) {
172
+ responseSchema = extractBodySchema(respText);
173
+ }
174
+ } catch {
175
+ }
176
+ }
177
+ try {
178
+ const bodyParsed = JSON.parse(pending.bodyText);
179
+ if (typeof bodyParsed?.operationName === "string") {
180
+ isGraphQL = true;
181
+ graphQLOperation = bodyParsed.operationName;
182
+ }
183
+ } catch {
184
+ }
185
+ const rec = {
186
+ url,
187
+ urlPattern: extractUrlPattern(url),
188
+ method,
189
+ bodySchema,
190
+ status: res.status(),
191
+ responseSchema,
192
+ isGraphQL,
193
+ graphQLOperation,
194
+ resourceType: pending.resourceType
195
+ };
196
+ correlator.recordRequest(rec);
197
+ };
198
+ page.on("request", onRequest);
199
+ page.on("response", onResponse);
200
+ return () => {
201
+ page.off("request", onRequest);
202
+ page.off("response", onResponse);
203
+ };
204
+ }
205
+ export {
206
+ attachNetworkInterceptor,
207
+ createCorrelator
208
+ };
209
+ //# sourceMappingURL=network-action-correlator.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/network-action-correlator.ts"],
4
+ "sourcesContent": ["/**\n * Network-action correlator \u2014 captures XHR/fetch requests that occur during\n * a named UI action (click, form submit, navigation) and maps them to API\n * endpoint contracts.\n *\n * Usage:\n * const correlator = createCorrelator();\n * const detach = attachNetworkInterceptor(page, correlator);\n * correlator.startWindow('Submit login form');\n * await page.click('#login-btn');\n * const correlation = correlator.endWindow();\n * detach();\n * const result = correlator.getResult();\n */\n\nimport type { Page, Request, Response } from 'playwright';\n\nexport interface NetworkRequestRecord {\n url: string;\n urlPattern: string;\n method: string;\n bodySchema: Record<string, string> | null;\n status: number | null;\n responseSchema: Record<string, string> | null;\n isGraphQL: boolean;\n graphQLOperation?: string;\n resourceType: string;\n correlatedActionLabel?: string;\n}\n\nexport interface ActionCorrelation {\n actionLabel: string;\n requests: NetworkRequestRecord[];\n}\n\nexport interface NetworkCorrelationResult {\n correlations: ActionCorrelation[];\n uniqueEndpoints: string[];\n graphqlOperations: string[];\n totalRequestsCaptured: number;\n coverage: number;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction extractUrlPattern(url: string): string {\n try {\n const u = new URL(url);\n const path = u.pathname\n .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':uuid')\n .replace(/\\/[0-9a-f]{24}(\\/|$)/g, '/:mongoId$1')\n .replace(/\\/\\d{6,}(\\/|$)/g, '/:id$1')\n .replace(/\\/\\d{1,5}(\\/|$)/g, '/:id$1');\n return path;\n } catch {\n return url.slice(0, 80);\n }\n}\n\nfunction extractBodySchema(bodyText: string): Record<string, string> | null {\n if (!bodyText || bodyText.length > 50_000) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(bodyText);\n } catch {\n return null;\n }\n\n const schema: Record<string, string> = {};\n let keyCount = 0;\n\n function traverse(value: unknown, prefix: string, depth: number): void {\n if (keyCount >= 30 || depth > 3) return;\n if (value === null || value === undefined) return;\n\n if (Array.isArray(value)) {\n schema[prefix] = 'array';\n keyCount++;\n return;\n }\n\n if (typeof value === 'object') {\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (keyCount >= 30) break;\n const key = prefix ? `${prefix}.${k}` : k;\n if (v === null) {\n schema[key] = 'null';\n keyCount++;\n } else if (Array.isArray(v)) {\n schema[key] = 'array';\n keyCount++;\n } else if (typeof v === 'object') {\n traverse(v, key, depth + 1);\n } else {\n schema[key] = typeof v;\n keyCount++;\n }\n }\n return;\n }\n\n // Primitive at top level\n if (prefix) {\n schema[prefix] = typeof value;\n keyCount++;\n }\n }\n\n traverse(parsed, '', 0);\n return keyCount > 0 ? schema : null;\n}\n\nconst ANALYTICS_DOMAINS = [\n 'google-analytics',\n 'doubleclick',\n 'hotjar',\n 'mixpanel',\n 'segment',\n 'sentry',\n 'datadog',\n 'newrelic',\n 'amplitude',\n 'fullstory',\n];\n\nconst API_PATH_PATTERNS = [\n '/api/',\n '/graphql',\n '/v1/',\n '/v2/',\n '/rest/',\n '/query',\n '/rpc',\n '/trpc',\n];\n\nconst EXCLUDED_RESOURCE_TYPES = new Set(['image', 'font', 'stylesheet', 'media', 'websocket']);\n\nfunction isApiRequest(url: string, resourceType: string): boolean {\n if (EXCLUDED_RESOURCE_TYPES.has(resourceType)) return false;\n\n try {\n const hostname = new URL(url).hostname;\n if (ANALYTICS_DOMAINS.some((d) => hostname.includes(d))) return false;\n } catch {\n return false;\n }\n\n if (resourceType === 'xhr' || resourceType === 'fetch') return true;\n\n const lower = url.toLowerCase();\n return API_PATH_PATTERNS.some((p) => lower.includes(p));\n}\n\n// ---------------------------------------------------------------------------\n// Correlator\n// ---------------------------------------------------------------------------\n\ninterface NetworkCorrelator {\n startWindow(actionLabel: string): void;\n recordRequest(rec: NetworkRequestRecord): void;\n endWindow(): ActionCorrelation;\n getResult(): NetworkCorrelationResult;\n}\n\nexport function createCorrelator(): NetworkCorrelator {\n let currentLabel: string | null = null;\n let currentRequests: NetworkRequestRecord[] = [];\n const correlations: ActionCorrelation[] = [];\n\n return {\n startWindow(actionLabel: string): void {\n currentLabel = actionLabel;\n currentRequests = [];\n },\n\n recordRequest(rec: NetworkRequestRecord): void {\n if (currentLabel === null) return;\n currentRequests.push({ ...rec, correlatedActionLabel: currentLabel });\n },\n\n endWindow(): ActionCorrelation {\n const label = currentLabel ?? '(unknown)';\n const correlation: ActionCorrelation = {\n actionLabel: label,\n requests: currentRequests,\n };\n correlations.push(correlation);\n currentLabel = null;\n currentRequests = [];\n return correlation;\n },\n\n getResult(): NetworkCorrelationResult {\n const uniqueEndpoints = [\n ...new Set(\n correlations.flatMap((c) => c.requests.map((r) => r.urlPattern)),\n ),\n ];\n\n const graphqlOperations = [\n ...new Set(\n correlations\n .flatMap((c) => c.requests)\n .filter((r) => r.isGraphQL && r.graphQLOperation)\n .map((r) => r.graphQLOperation as string),\n ),\n ];\n\n const totalRequestsCaptured = correlations.reduce(\n (sum, c) => sum + c.requests.length,\n 0,\n );\n\n const coverage =\n correlations.length === 0\n ? 0\n : correlations.filter((c) => c.requests.length >= 1).length /\n correlations.length;\n\n return {\n correlations,\n uniqueEndpoints,\n graphqlOperations,\n totalRequestsCaptured,\n coverage,\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Page interceptor\n// ---------------------------------------------------------------------------\n\nexport function attachNetworkInterceptor(\n page: Page,\n correlator: ReturnType<typeof createCorrelator>,\n): () => void {\n const pendingRequests = new Map<string, { bodyText: string; resourceType: string }>();\n\n const onRequest = (req: Request): void => {\n const url = req.url();\n const resourceType = req.resourceType();\n if (!isApiRequest(url, resourceType)) return;\n let bodyText = '';\n try {\n bodyText = req.postData() ?? '';\n } catch {\n // postData() may throw on non-POST requests in some Playwright versions\n }\n pendingRequests.set(url, { bodyText, resourceType });\n };\n\n const onResponse = async (res: Response): Promise<void> => {\n const url = res.url();\n const pending = pendingRequests.get(url);\n if (!pending) return;\n pendingRequests.delete(url);\n\n const method = res.request().method();\n\n let bodySchema: Record<string, string> | null = null;\n try {\n bodySchema = extractBodySchema(pending.bodyText);\n } catch {\n // ignore schema extraction failures\n }\n\n let responseSchema: Record<string, string> | null = null;\n let isGraphQL = false;\n let graphQLOperation: string | undefined;\n\n const contentType = res.headers()['content-type'] ?? '';\n if (contentType.includes('application/json')) {\n try {\n const respText = await res.text();\n if (respText.length < 20_000) {\n responseSchema = extractBodySchema(respText);\n }\n } catch {\n // response body may no longer be available\n }\n }\n\n // Detect GraphQL from request body\n try {\n const bodyParsed = JSON.parse(pending.bodyText) as Record<string, unknown>;\n if (typeof bodyParsed?.operationName === 'string') {\n isGraphQL = true;\n graphQLOperation = bodyParsed.operationName;\n }\n } catch {\n // not JSON or no operationName\n }\n\n const rec: NetworkRequestRecord = {\n url,\n urlPattern: extractUrlPattern(url),\n method,\n bodySchema,\n status: res.status(),\n responseSchema,\n isGraphQL,\n graphQLOperation,\n resourceType: pending.resourceType,\n };\n correlator.recordRequest(rec);\n };\n\n page.on('request', onRequest);\n page.on('response', onResponse);\n\n return () => {\n page.off('request', onRequest);\n page.off('response', onResponse);\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,155 @@
1
+ async function injectPerformanceObservers(page) {
2
+ await page.addInitScript(() => {
3
+ window.__zetaPerf = { lcp: null, cls: 0, clsCount: 0, inp: [], longTaskCount: 0 };
4
+ try {
5
+ new PerformanceObserver((list) => {
6
+ const entries = list.getEntries();
7
+ const last = entries[entries.length - 1];
8
+ if (last) {
9
+ window.__zetaPerf.lcp = {
10
+ value: last.startTime,
11
+ elementTag: last.element?.tagName || null,
12
+ url: last.url || null
13
+ };
14
+ }
15
+ }).observe({ entryTypes: ["largest-contentful-paint"] });
16
+ } catch {
17
+ }
18
+ try {
19
+ new PerformanceObserver((list) => {
20
+ for (const entry of list.getEntries()) {
21
+ if (!entry.hadRecentInput) {
22
+ window.__zetaPerf.cls += entry.value;
23
+ window.__zetaPerf.clsCount++;
24
+ }
25
+ }
26
+ }).observe({ entryTypes: ["layout-shift"] });
27
+ } catch {
28
+ }
29
+ try {
30
+ new PerformanceObserver((list) => {
31
+ for (const entry of list.getEntries()) {
32
+ window.__zetaPerf.inp.push(entry.duration);
33
+ }
34
+ }).observe({ entryTypes: ["event"], durationThreshold: 40 });
35
+ } catch {
36
+ }
37
+ try {
38
+ new PerformanceObserver((list) => {
39
+ window.__zetaPerf.longTaskCount += list.getEntries().length;
40
+ }).observe({ entryTypes: ["longtask"] });
41
+ } catch {
42
+ }
43
+ });
44
+ }
45
+ function scoreLcp(value) {
46
+ if (value <= 2500) return "good";
47
+ if (value <= 4e3) return "needs-improvement";
48
+ return "poor";
49
+ }
50
+ function scoreCls(value) {
51
+ if (value <= 0.1) return "good";
52
+ if (value <= 0.25) return "needs-improvement";
53
+ return "poor";
54
+ }
55
+ function scoreInp(p75) {
56
+ if (p75 <= 200) return "good";
57
+ if (p75 <= 500) return "needs-improvement";
58
+ return "poor";
59
+ }
60
+ function scoreTtfb(value) {
61
+ if (value <= 800) return "good";
62
+ if (value <= 1800) return "needs-improvement";
63
+ return "poor";
64
+ }
65
+ async function collectMetrics(page) {
66
+ const rawMetrics = await page.evaluate(() => {
67
+ const p = window.__zetaPerf || {};
68
+ const nav = performance.getEntriesByType("navigation")[0];
69
+ const resources = performance.getEntriesByType("resource").filter((r) => r.duration > 500).sort((a, b) => b.duration - a.duration).slice(0, 10).map((r) => ({
70
+ path: (() => {
71
+ try {
72
+ return new URL(r.name).pathname;
73
+ } catch {
74
+ return r.name.slice(0, 80);
75
+ }
76
+ })(),
77
+ durationMs: Math.round(r.duration),
78
+ transferSizeBytes: r.transferSize || 0,
79
+ initiatorType: r.initiatorType || ""
80
+ }));
81
+ const inpValues = p.inp || [];
82
+ const p75 = inpValues.length > 0 ? inpValues.sort((a, b) => a - b)[Math.floor(inpValues.length * 0.75)] : 0;
83
+ return {
84
+ lcp: p.lcp || null,
85
+ cls: p.cls ?? null,
86
+ clsCount: p.clsCount ?? 0,
87
+ inpP75: p75,
88
+ inpCount: inpValues.length,
89
+ longTaskCount: p.longTaskCount || 0,
90
+ nav: nav ? {
91
+ ttfb: Math.round(nav.responseStart - nav.requestStart),
92
+ domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
93
+ domComplete: Math.round(nav.domComplete - nav.startTime),
94
+ loadComplete: Math.round(nav.loadEventEnd - nav.startTime),
95
+ transferSizeBytes: nav.transferSize || 0,
96
+ protocol: nav.nextHopProtocol || ""
97
+ } : null,
98
+ resources
99
+ };
100
+ });
101
+ const lcp = rawMetrics.lcp ? {
102
+ value: rawMetrics.lcp.value,
103
+ score: scoreLcp(rawMetrics.lcp.value),
104
+ elementTag: rawMetrics.lcp.elementTag,
105
+ url: rawMetrics.lcp.url
106
+ } : null;
107
+ const cls = rawMetrics.cls !== null ? {
108
+ value: rawMetrics.cls,
109
+ score: scoreCls(rawMetrics.cls),
110
+ shiftCount: rawMetrics.clsCount
111
+ } : null;
112
+ const inp = rawMetrics.inpCount > 0 ? {
113
+ p75: rawMetrics.inpP75,
114
+ score: scoreInp(rawMetrics.inpP75),
115
+ sampleCount: rawMetrics.inpCount
116
+ } : null;
117
+ const navTiming = rawMetrics.nav ? {
118
+ ttfb: rawMetrics.nav.ttfb,
119
+ ttfbScore: scoreTtfb(rawMetrics.nav.ttfb),
120
+ domContentLoaded: rawMetrics.nav.domContentLoaded,
121
+ domComplete: rawMetrics.nav.domComplete,
122
+ loadComplete: rawMetrics.nav.loadComplete,
123
+ transferSizeBytes: rawMetrics.nav.transferSizeBytes,
124
+ protocol: rawMetrics.nav.protocol
125
+ } : null;
126
+ return {
127
+ lcp,
128
+ cls,
129
+ inp,
130
+ navTiming,
131
+ slowResources: rawMetrics.resources,
132
+ longTaskCount: rawMetrics.longTaskCount,
133
+ captureSuccess: true
134
+ };
135
+ }
136
+ const emptyResult = {
137
+ lcp: null,
138
+ cls: null,
139
+ inp: null,
140
+ navTiming: null,
141
+ slowResources: [],
142
+ longTaskCount: 0
143
+ };
144
+ async function collectPagePerformance(page) {
145
+ try {
146
+ return await collectMetrics(page);
147
+ } catch {
148
+ return { ...emptyResult, captureSuccess: false };
149
+ }
150
+ }
151
+ export {
152
+ collectPagePerformance,
153
+ injectPerformanceObservers
154
+ };
155
+ //# sourceMappingURL=performance-metrics-collector.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/performance-metrics-collector.ts"],
4
+ "sourcesContent": ["/**\n * Performance metrics collector \u2014 captures Core Web Vitals and navigation timing\n * for pages visited during crawl runs.\n * Collects LCP, CLS, INP, TTFB, long tasks, and slow resources via PerformanceObserver\n * and the Navigation/Resource Timing APIs.\n */\n\nimport type { Page } from 'playwright';\n\nexport type CwvScore = 'good' | 'needs-improvement' | 'poor';\n\nexport interface LcpMetric {\n value: number;\n score: CwvScore;\n elementTag: string | null;\n url: string | null;\n}\n\nexport interface ClsMetric {\n value: number;\n score: CwvScore;\n shiftCount: number;\n}\n\nexport interface InpMetric {\n p75: number;\n score: CwvScore;\n sampleCount: number;\n}\n\nexport interface NavTimingMetric {\n ttfb: number;\n ttfbScore: CwvScore;\n domContentLoaded: number;\n domComplete: number;\n loadComplete: number;\n transferSizeBytes: number;\n protocol: string;\n}\n\nexport interface SlowResource {\n path: string;\n durationMs: number;\n transferSizeBytes: number;\n initiatorType: string;\n}\n\nexport interface PerformanceMetricsResult {\n lcp: LcpMetric | null;\n cls: ClsMetric | null;\n inp: InpMetric | null;\n navTiming: NavTimingMetric | null;\n slowResources: SlowResource[];\n longTaskCount: number;\n captureSuccess: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Observer injection \u2014 call before page navigation via addInitScript\n// ---------------------------------------------------------------------------\n\nexport async function injectPerformanceObservers(page: Page): Promise<void> {\n await page.addInitScript(() => {\n (window as any).__zetaPerf = { lcp: null, cls: 0, clsCount: 0, inp: [], longTaskCount: 0 };\n\n try {\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n const last = entries[entries.length - 1] as any;\n if (last) {\n (window as any).__zetaPerf.lcp = {\n value: last.startTime,\n elementTag: last.element?.tagName || null,\n url: last.url || null,\n };\n }\n }).observe({ entryTypes: ['largest-contentful-paint'] });\n } catch {}\n\n try {\n new PerformanceObserver(list => {\n for (const entry of list.getEntries() as any[]) {\n if (!entry.hadRecentInput) {\n (window as any).__zetaPerf.cls += entry.value;\n (window as any).__zetaPerf.clsCount++;\n }\n }\n }).observe({ entryTypes: ['layout-shift'] });\n } catch {}\n\n try {\n new PerformanceObserver(list => {\n for (const entry of list.getEntries() as any[]) {\n (window as any).__zetaPerf.inp.push(entry.duration);\n }\n }).observe({ entryTypes: ['event'], durationThreshold: 40 } as any);\n } catch {}\n\n try {\n new PerformanceObserver(list => {\n (window as any).__zetaPerf.longTaskCount += list.getEntries().length;\n }).observe({ entryTypes: ['longtask'] });\n } catch {}\n });\n}\n\n// ---------------------------------------------------------------------------\n// Scoring helpers\n// ---------------------------------------------------------------------------\n\nfunction scoreLcp(value: number): CwvScore {\n if (value <= 2500) return 'good';\n if (value <= 4000) return 'needs-improvement';\n return 'poor';\n}\n\nfunction scoreCls(value: number): CwvScore {\n if (value <= 0.1) return 'good';\n if (value <= 0.25) return 'needs-improvement';\n return 'poor';\n}\n\nfunction scoreInp(p75: number): CwvScore {\n if (p75 <= 200) return 'good';\n if (p75 <= 500) return 'needs-improvement';\n return 'poor';\n}\n\nfunction scoreTtfb(value: number): CwvScore {\n if (value <= 800) return 'good';\n if (value <= 1800) return 'needs-improvement';\n return 'poor';\n}\n\n// ---------------------------------------------------------------------------\n// Internal collector\n// ---------------------------------------------------------------------------\n\nasync function collectMetrics(page: Page): Promise<PerformanceMetricsResult> {\n const rawMetrics = await page.evaluate(() => {\n const p = (window as any).__zetaPerf || {};\n const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;\n const resources = performance.getEntriesByType('resource')\n .filter(r => r.duration > 500)\n .sort((a, b) => b.duration - a.duration)\n .slice(0, 10)\n .map(r => ({\n path: (() => { try { return new URL(r.name).pathname; } catch { return r.name.slice(0, 80); } })(),\n durationMs: Math.round(r.duration),\n transferSizeBytes: (r as any).transferSize || 0,\n initiatorType: (r as any).initiatorType || '',\n }));\n const inpValues = p.inp || [];\n const p75 = inpValues.length > 0\n ? inpValues.sort((a: number, b: number) => a - b)[Math.floor(inpValues.length * 0.75)]\n : 0;\n return {\n lcp: p.lcp || null,\n cls: p.cls ?? null,\n clsCount: p.clsCount ?? 0,\n inpP75: p75,\n inpCount: inpValues.length,\n longTaskCount: p.longTaskCount || 0,\n nav: nav ? {\n ttfb: Math.round(nav.responseStart - nav.requestStart),\n domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),\n domComplete: Math.round(nav.domComplete - nav.startTime),\n loadComplete: Math.round(nav.loadEventEnd - nav.startTime),\n transferSizeBytes: (nav as any).transferSize || 0,\n protocol: (nav as any).nextHopProtocol || '',\n } : null,\n resources,\n };\n });\n\n const lcp: LcpMetric | null = rawMetrics.lcp\n ? {\n value: rawMetrics.lcp.value,\n score: scoreLcp(rawMetrics.lcp.value),\n elementTag: rawMetrics.lcp.elementTag,\n url: rawMetrics.lcp.url,\n }\n : null;\n\n const cls: ClsMetric | null = rawMetrics.cls !== null\n ? {\n value: rawMetrics.cls,\n score: scoreCls(rawMetrics.cls),\n shiftCount: rawMetrics.clsCount,\n }\n : null;\n\n const inp: InpMetric | null = rawMetrics.inpCount > 0\n ? {\n p75: rawMetrics.inpP75,\n score: scoreInp(rawMetrics.inpP75),\n sampleCount: rawMetrics.inpCount,\n }\n : null;\n\n const navTiming: NavTimingMetric | null = rawMetrics.nav\n ? {\n ttfb: rawMetrics.nav.ttfb,\n ttfbScore: scoreTtfb(rawMetrics.nav.ttfb),\n domContentLoaded: rawMetrics.nav.domContentLoaded,\n domComplete: rawMetrics.nav.domComplete,\n loadComplete: rawMetrics.nav.loadComplete,\n transferSizeBytes: rawMetrics.nav.transferSizeBytes,\n protocol: rawMetrics.nav.protocol,\n }\n : null;\n\n return {\n lcp,\n cls,\n inp,\n navTiming,\n slowResources: rawMetrics.resources,\n longTaskCount: rawMetrics.longTaskCount,\n captureSuccess: true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nconst emptyResult: Omit<PerformanceMetricsResult, 'captureSuccess'> = {\n lcp: null,\n cls: null,\n inp: null,\n navTiming: null,\n slowResources: [],\n longTaskCount: 0,\n};\n\nexport async function collectPagePerformance(page: Page): Promise<PerformanceMetricsResult> {\n try {\n return await collectMetrics(page);\n } catch {\n return { ...emptyResult, captureSuccess: false };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/performance-metrics.ts"],
4
- "sourcesContent": ["/**\n * Performance metrics collection via CDP + Playwright page.evaluate().\n *\n * Collects Core Web Vitals and resource timing after page navigation.\n * Stored alongside screen data for test generation context and regression detection.\n *\n * Metrics:\n * - LCP (Largest Contentful Paint) \u2014 via PerformanceObserver in page\n * - CLS (Cumulative Layout Shift) \u2014 via PerformanceObserver in page\n * - TTFB (Time to First Byte) \u2014 from navigation timing\n * - DOMContentLoaded \u2014 from navigation timing\n * - Resource count and size by type\n */\n\nimport type { Page } from 'playwright';\n\nexport interface CoreWebVitals {\n lcp?: number; // ms\n cls?: number; // unitless score\n ttfb?: number; // ms\n fcp?: number; // First Contentful Paint ms\n domContentLoaded?: number; // ms\n loadEventEnd?: number; // ms\n}\n\nexport interface ResourceSummary {\n scripts: { count: number; totalBytes: number };\n styles: { count: number; totalBytes: number };\n images: { count: number; totalBytes: number };\n fonts: { count: number; totalBytes: number };\n other: { count: number; totalBytes: number };\n}\n\nexport interface PagePerfMetrics {\n url: string;\n vitals: CoreWebVitals;\n resources: ResourceSummary;\n totalTransferBytes: number;\n requestCount: number;\n measuredAt: string;\n}\n\nexport async function collectPerfMetrics(page: Page, url: string): Promise<PagePerfMetrics> {\n const measuredAt = new Date().toISOString();\n\n // Inject PerformanceObserver for LCP + CLS, wait up to 3s\n let vitals: CoreWebVitals = {};\n try {\n vitals = await page.evaluate(() => {\n return new Promise<any>((resolve) => {\n const result: any = {};\n\n // Navigation timing\n const nav = performance.getEntriesByType('navigation')[0] as any;\n if (nav) {\n result.ttfb = Math.round(nav.responseStart - nav.requestStart);\n result.domContentLoaded = Math.round(nav.domContentLoadedEventEnd);\n result.loadEventEnd = Math.round(nav.loadEventEnd);\n }\n\n let lcpDone = false;\n let clsDone = false;\n const tryResolve = () => { if (lcpDone && clsDone) resolve(result); };\n\n // LCP\n try {\n new PerformanceObserver((list) => {\n const entries = list.getEntries();\n if (entries.length > 0) result.lcp = Math.round((entries[entries.length - 1] as any).startTime);\n lcpDone = true;\n tryResolve();\n }).observe({ type: 'largest-contentful-paint', buffered: true });\n } catch { lcpDone = true; }\n\n // CLS\n try {\n let cls = 0;\n new PerformanceObserver((list) => {\n for (const e of list.getEntries() as any[]) {\n if (!e.hadRecentInput) cls += e.value;\n }\n result.cls = Math.round(cls * 1000) / 1000;\n clsDone = true;\n tryResolve();\n }).observe({ type: 'layout-shift', buffered: true });\n } catch { clsDone = true; }\n\n // FCP\n try {\n const fcp = performance.getEntriesByName('first-contentful-paint')[0] as any;\n if (fcp) result.fcp = Math.round(fcp.startTime);\n } catch {}\n\n // Fallback resolve after 3s\n setTimeout(() => resolve(result), 3_000);\n });\n }).catch(() => ({}));\n } catch { /* page closed or navigate already */ }\n\n // Resource timing\n let resources: ResourceSummary = {\n scripts: { count: 0, totalBytes: 0 },\n styles: { count: 0, totalBytes: 0 },\n images: { count: 0, totalBytes: 0 },\n fonts: { count: 0, totalBytes: 0 },\n other: { count: 0, totalBytes: 0 },\n };\n let totalTransferBytes = 0;\n let requestCount = 0;\n\n try {\n const entries = await page.evaluate(() =>\n performance.getEntriesByType('resource').map((e: any) => ({\n name: e.name,\n initiatorType: e.initiatorType,\n transferSize: e.transferSize ?? 0,\n }))\n ).catch(() => [] as any[]);\n\n for (const e of entries) {\n requestCount++;\n totalTransferBytes += e.transferSize;\n const cat = e.initiatorType === 'script' ? 'scripts'\n : e.initiatorType === 'css' || e.name?.endsWith('.css') ? 'styles'\n : e.initiatorType === 'img' ? 'images'\n : e.name?.match(/\\.(woff|woff2|ttf|otf|eot)/) ? 'fonts'\n : 'other';\n resources[cat as keyof ResourceSummary].count++;\n resources[cat as keyof ResourceSummary].totalBytes += e.transferSize;\n }\n } catch {}\n\n return { url, vitals, resources, totalTransferBytes, requestCount, measuredAt };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Performance metrics collection via CDP + Playwright page.evaluate().\n *\n * Collects Core Web Vitals and resource timing after page navigation.\n * Stored alongside screen data for test generation context and regression detection.\n *\n * Metrics:\n * - LCP (Largest Contentful Paint) \u2014 via PerformanceObserver in page\n * - CLS (Cumulative Layout Shift) \u2014 via PerformanceObserver in page\n * - TTFB (Time to First Byte) \u2014 from navigation timing\n * - DOMContentLoaded \u2014 from navigation timing\n * - Resource count and size by type\n */\n\nimport type { Page } from 'playwright';\n\nexport interface CoreWebVitals {\n lcp?: number; // ms\n cls?: number; // unitless score\n ttfb?: number; // ms\n fcp?: number; // First Contentful Paint ms\n domContentLoaded?: number; // ms\n loadEventEnd?: number; // ms\n}\n\nexport interface ResourceSummary {\n scripts: { count: number; totalBytes: number };\n styles: { count: number; totalBytes: number };\n images: { count: number; totalBytes: number };\n fonts: { count: number; totalBytes: number };\n other: { count: number; totalBytes: number };\n}\n\nexport interface PagePerfMetrics {\n url: string;\n vitals: CoreWebVitals;\n resources: ResourceSummary;\n totalTransferBytes: number;\n requestCount: number;\n measuredAt: string;\n}\n\nexport async function collectPerfMetrics(page: any, url: string): Promise<PagePerfMetrics> {\n const measuredAt = new Date().toISOString();\n\n // Inject PerformanceObserver for LCP + CLS, wait up to 3s\n let vitals: CoreWebVitals = {};\n try {\n vitals = await page.evaluate(() => {\n return new Promise<any>((resolve) => {\n const result: any = {};\n\n // Navigation timing\n const nav = performance.getEntriesByType('navigation')[0] as any;\n if (nav) {\n result.ttfb = Math.round(nav.responseStart - nav.requestStart);\n result.domContentLoaded = Math.round(nav.domContentLoadedEventEnd);\n result.loadEventEnd = Math.round(nav.loadEventEnd);\n }\n\n let lcpDone = false;\n let clsDone = false;\n const tryResolve = () => { if (lcpDone && clsDone) resolve(result); };\n\n // LCP\n try {\n new PerformanceObserver((list) => {\n const entries = list.getEntries();\n if (entries.length > 0) result.lcp = Math.round((entries[entries.length - 1] as any).startTime);\n lcpDone = true;\n tryResolve();\n }).observe({ type: 'largest-contentful-paint', buffered: true });\n } catch { lcpDone = true; }\n\n // CLS\n try {\n let cls = 0;\n new PerformanceObserver((list) => {\n for (const e of list.getEntries() as any[]) {\n if (!e.hadRecentInput) cls += e.value;\n }\n result.cls = Math.round(cls * 1000) / 1000;\n clsDone = true;\n tryResolve();\n }).observe({ type: 'layout-shift', buffered: true });\n } catch { clsDone = true; }\n\n // FCP\n try {\n const fcp = performance.getEntriesByName('first-contentful-paint')[0] as any;\n if (fcp) result.fcp = Math.round(fcp.startTime);\n } catch {}\n\n // Fallback resolve after 3s\n setTimeout(() => resolve(result), 3_000);\n });\n }).catch(() => ({}));\n } catch { /* page closed or navigate already */ }\n\n // Resource timing\n let resources: ResourceSummary = {\n scripts: { count: 0, totalBytes: 0 },\n styles: { count: 0, totalBytes: 0 },\n images: { count: 0, totalBytes: 0 },\n fonts: { count: 0, totalBytes: 0 },\n other: { count: 0, totalBytes: 0 },\n };\n let totalTransferBytes = 0;\n let requestCount = 0;\n\n try {\n const entries = await page.evaluate(() =>\n performance.getEntriesByType('resource').map((e: any) => ({\n name: e.name,\n initiatorType: e.initiatorType,\n transferSize: e.transferSize ?? 0,\n }))\n ).catch(() => [] as any[]);\n\n for (const e of entries) {\n requestCount++;\n totalTransferBytes += e.transferSize;\n const cat = e.initiatorType === 'script' ? 'scripts'\n : e.initiatorType === 'css' || e.name?.endsWith('.css') ? 'styles'\n : e.initiatorType === 'img' ? 'images'\n : e.name?.match(/\\.(woff|woff2|ttf|otf|eot)/) ? 'fonts'\n : 'other';\n resources[cat as keyof ResourceSummary].count++;\n resources[cat as keyof ResourceSummary].totalBytes += e.transferSize;\n }\n } catch {}\n\n return { url, vitals, resources, totalTransferBytes, requestCount, measuredAt };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,8 @@
1
- const DESTRUCTIVE_ACTION = /\b(delete|remove|destroy|deactivate|disable|cancel|refund|void|transfer|withdraw|pay now|place order|submit order|confirm payment|send money|publish|approve|reject|bulk update|drop)\b/i;
1
+ const _extraDestructive = process.env.CRAWL_EXTRA_DESTRUCTIVE_PATTERNS ? process.env.CRAWL_EXTRA_DESTRUCTIVE_PATTERNS.split(",").map((p) => p.trim()).filter(Boolean) : [];
2
+ const DESTRUCTIVE_ACTION = new RegExp(
3
+ `\\b(delete|remove|destroy|deactivate|disable|cancel|refund|void|transfer|withdraw|pay now|place order|submit order|confirm payment|send money|publish|approve|reject|bulk update|drop${_extraDestructive.length ? `|${_extraDestructive.join("|")}` : ""})\\b`,
4
+ "i"
5
+ );
2
6
  const SECRET_LIKE = /\b(api[_ -]?key|secret|token|password|authorization|cookie|set-cookie)\b/i;
3
7
  function assessActionSafety(label, profile) {
4
8
  const riskyByProfile = profile.riskyActions.some((risk) => label.toLowerCase().includes(risk.toLowerCase()));
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/policies/crawl-safety.ts"],
4
- "sourcesContent": ["import type { DomainProfile } from '../profiles/domain-profile.js';\n\nexport interface CrawlSafetyDecision {\n allowed: boolean;\n reason?: string;\n severity: 'info' | 'warn' | 'block';\n}\n\nconst DESTRUCTIVE_ACTION = /\\b(delete|remove|destroy|deactivate|disable|cancel|refund|void|transfer|withdraw|pay now|place order|submit order|confirm payment|send money|publish|approve|reject|bulk update|drop)\\b/i;\nconst SECRET_LIKE = /\\b(api[_ -]?key|secret|token|password|authorization|cookie|set-cookie)\\b/i;\n\nexport function assessActionSafety(label: string, profile: DomainProfile): CrawlSafetyDecision {\n const riskyByProfile = profile.riskyActions.some((risk) => label.toLowerCase().includes(risk.toLowerCase()));\n if (DESTRUCTIVE_ACTION.test(label) || riskyByProfile) {\n return {\n allowed: false,\n severity: 'block',\n reason: `Blocked risky action \"${label}\" for ${profile.industry} profile.`,\n };\n }\n return { allowed: true, severity: 'info' };\n}\n\nexport function redactSensitiveValue(value: string): string {\n let redacted = value\n .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi, '[email]')\n .replace(/\\b(?:\\+?\\d[\\d -]{8,}\\d)\\b/g, '[phone-or-id]')\n .replace(/\\b(?:\\d[ -]*?){13,19}\\b/g, '[card-like-number]')\n .replace(/\\b[A-Za-z0-9_-]{24,}\\b/g, '[token-like-value]');\n if (SECRET_LIKE.test(redacted)) redacted = redacted.replace(/:\\s*[^,\\s}]+/g, ': [redacted]');\n return redacted;\n}\n\nexport function redactJson(value: unknown): unknown {\n if (typeof value === 'string') return redactSensitiveValue(value);\n if (Array.isArray(value)) return value.map(redactJson);\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n if (SECRET_LIKE.test(key)) out[key] = '[redacted]';\n else out[key] = redactJson(item);\n }\n return out;\n }\n return value;\n}\n"],
5
- "mappings": "AAQA,MAAM,qBAAqB;AAC3B,MAAM,cAAc;AAEb,SAAS,mBAAmB,OAAe,SAA6C;AAC7F,QAAM,iBAAiB,QAAQ,aAAa,KAAK,CAAC,SAAS,MAAM,YAAY,EAAE,SAAS,KAAK,YAAY,CAAC,CAAC;AAC3G,MAAI,mBAAmB,KAAK,KAAK,KAAK,gBAAgB;AACpD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,UAAU;AAAA,MACV,QAAQ,yBAAyB,KAAK,SAAS,QAAQ,QAAQ;AAAA,IACjE;AAAA,EACF;AACA,SAAO,EAAE,SAAS,MAAM,UAAU,OAAO;AAC3C;AAEO,SAAS,qBAAqB,OAAuB;AAC1D,MAAI,WAAW,MACZ,QAAQ,2CAA2C,SAAS,EAC5D,QAAQ,8BAA8B,eAAe,EACrD,QAAQ,4BAA4B,oBAAoB,EACxD,QAAQ,2BAA2B,oBAAoB;AAC1D,MAAI,YAAY,KAAK,QAAQ,EAAG,YAAW,SAAS,QAAQ,iBAAiB,cAAc;AAC3F,SAAO;AACT;AAEO,SAAS,WAAW,OAAyB;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,qBAAqB,KAAK;AAChE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,UAAU;AACrD,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC/C,UAAI,YAAY,KAAK,GAAG,EAAG,KAAI,GAAG,IAAI;AAAA,UACjC,KAAI,GAAG,IAAI,WAAW,IAAI;AAAA,IACjC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { DomainProfile } from '../profiles/domain-profile.js';\n\nexport interface CrawlSafetyDecision {\n allowed: boolean;\n reason?: string;\n severity: 'info' | 'warn' | 'block';\n}\n\nconst _extraDestructive = process.env.CRAWL_EXTRA_DESTRUCTIVE_PATTERNS\n ? process.env.CRAWL_EXTRA_DESTRUCTIVE_PATTERNS.split(',').map((p) => p.trim()).filter(Boolean)\n : [];\nconst DESTRUCTIVE_ACTION = new RegExp(\n `\\\\b(delete|remove|destroy|deactivate|disable|cancel|refund|void|transfer|withdraw|pay now|place order|submit order|confirm payment|send money|publish|approve|reject|bulk update|drop${_extraDestructive.length ? `|${_extraDestructive.join('|')}` : ''})\\\\b`,\n 'i',\n);\nconst SECRET_LIKE = /\\b(api[_ -]?key|secret|token|password|authorization|cookie|set-cookie)\\b/i;\n\nexport function assessActionSafety(label: string, profile: DomainProfile): CrawlSafetyDecision {\n const riskyByProfile = profile.riskyActions.some((risk) => label.toLowerCase().includes(risk.toLowerCase()));\n if (DESTRUCTIVE_ACTION.test(label) || riskyByProfile) {\n return {\n allowed: false,\n severity: 'block',\n reason: `Blocked risky action \"${label}\" for ${profile.industry} profile.`,\n };\n }\n return { allowed: true, severity: 'info' };\n}\n\nexport function redactSensitiveValue(value: string): string {\n let redacted = value\n .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi, '[email]')\n .replace(/\\b(?:\\+?\\d[\\d -]{8,}\\d)\\b/g, '[phone-or-id]')\n .replace(/\\b(?:\\d[ -]*?){13,19}\\b/g, '[card-like-number]')\n .replace(/\\b[A-Za-z0-9_-]{24,}\\b/g, '[token-like-value]');\n if (SECRET_LIKE.test(redacted)) redacted = redacted.replace(/:\\s*[^,\\s}]+/g, ': [redacted]');\n return redacted;\n}\n\nexport function redactJson(value: unknown): unknown {\n if (typeof value === 'string') return redactSensitiveValue(value);\n if (Array.isArray(value)) return value.map(redactJson);\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n if (SECRET_LIKE.test(key)) out[key] = '[redacted]';\n else out[key] = redactJson(item);\n }\n return out;\n }\n return value;\n}\n"],
5
+ "mappings": "AAQA,MAAM,oBAAoB,QAAQ,IAAI,mCAClC,QAAQ,IAAI,iCAAiC,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,IAC3F,CAAC;AACL,MAAM,qBAAqB,IAAI;AAAA,EAC7B,wLAAwL,kBAAkB,SAAS,IAAI,kBAAkB,KAAK,GAAG,CAAC,KAAK,EAAE;AAAA,EACzP;AACF;AACA,MAAM,cAAc;AAEb,SAAS,mBAAmB,OAAe,SAA6C;AAC7F,QAAM,iBAAiB,QAAQ,aAAa,KAAK,CAAC,SAAS,MAAM,YAAY,EAAE,SAAS,KAAK,YAAY,CAAC,CAAC;AAC3G,MAAI,mBAAmB,KAAK,KAAK,KAAK,gBAAgB;AACpD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,UAAU;AAAA,MACV,QAAQ,yBAAyB,KAAK,SAAS,QAAQ,QAAQ;AAAA,IACjE;AAAA,EACF;AACA,SAAO,EAAE,SAAS,MAAM,UAAU,OAAO;AAC3C;AAEO,SAAS,qBAAqB,OAAuB;AAC1D,MAAI,WAAW,MACZ,QAAQ,2CAA2C,SAAS,EAC5D,QAAQ,8BAA8B,eAAe,EACrD,QAAQ,4BAA4B,oBAAoB,EACxD,QAAQ,2BAA2B,oBAAoB;AAC1D,MAAI,YAAY,KAAK,QAAQ,EAAG,YAAW,SAAS,QAAQ,iBAAiB,cAAc;AAC3F,SAAO;AACT;AAEO,SAAS,WAAW,OAAyB;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,qBAAqB,KAAK;AAChE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,UAAU;AACrD,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC/C,UAAI,YAAY,KAAK,GAAG,EAAG,KAAI,GAAG,IAAI;AAAA,UACjC,KAAI,GAAG,IAAI,WAAW,IAAI;AAAA,IACjC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -89,7 +89,7 @@ const PROFILES = {
89
89
  requiredArtifacts: COMMON_REQUIRED_ARTIFACTS
90
90
  }
91
91
  };
92
- const INDUSTRY_HINTS = [
92
+ const BASE_INDUSTRY_HINTS = [
93
93
  { industry: "ecommerce", terms: [/cart/i, /checkout/i, /product/i, /catalog/i, /sku/i, /wishlist/i, /order/i] },
94
94
  { industry: "banking", terms: [/account/i, /statement/i, /beneficiar/i, /transfer/i, /iban/i, /routing/i, /balance/i] },
95
95
  { industry: "fintech", terms: [/wallet/i, /kyc/i, /payout/i, /card/i, /payment/i, /risk/i, /dispute/i] },
@@ -99,6 +99,26 @@ const INDUSTRY_HINTS = [
99
99
  { industry: "legacy-enterprise", terms: [/frameset/i, /iframe/i, /popup/i, /legacy/i, /report export/i, /mainframe/i] },
100
100
  { industry: "saas", terms: [/workspace/i, /tenant/i, /integration/i, /billing/i, /api key/i, /admin/i, /dashboard/i] }
101
101
  ];
102
+ const _extraHints = (() => {
103
+ const raw = process.env.EXTRA_INDUSTRY_HINTS_JSON;
104
+ if (!raw) return [];
105
+ try {
106
+ const parsed = JSON.parse(raw);
107
+ return parsed.map((h) => ({ industry: h.industry, terms: h.terms.map((t) => new RegExp(t, "i")) }));
108
+ } catch {
109
+ return [];
110
+ }
111
+ })();
112
+ const INDUSTRY_HINTS = [...BASE_INDUSTRY_HINTS, ..._extraHints];
113
+ const _profileOverrides = (() => {
114
+ const raw = process.env.DOMAIN_PROFILE_OVERRIDES_JSON;
115
+ if (!raw) return {};
116
+ try {
117
+ return JSON.parse(raw);
118
+ } catch {
119
+ return {};
120
+ }
121
+ })();
102
122
  function classifyDomainProfile(input) {
103
123
  const haystack = [input.appUrl, input.projectName ?? "", input.appType ?? "", input.seedText ?? ""].join(" ");
104
124
  const scores = /* @__PURE__ */ new Map();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/profiles/domain-profile.ts"],
4
- "sourcesContent": ["export type Industry =\n | 'ecommerce'\n | 'banking'\n | 'fintech'\n | 'healthcare'\n | 'pharma'\n | 'agriculture'\n | 'legacy-enterprise'\n | 'saas'\n | 'unknown';\n\nexport interface DomainProfile {\n industry: Industry;\n confidence: number;\n importantScreenPatterns: string[];\n workflowGoals: string[];\n piiRules: string[];\n riskyActions: string[];\n safeProbeRules: string[];\n requiredArtifacts: string[];\n evidence: string[];\n}\n\nconst COMMON_REQUIRED_ARTIFACTS = [\n 'screenshot',\n 'dom',\n 'markdown',\n 'accessibility',\n 'network',\n 'performance',\n];\n\nconst PROFILES: Record<Industry, Omit<DomainProfile, 'confidence' | 'evidence'>> = {\n ecommerce: {\n industry: 'ecommerce',\n importantScreenPatterns: ['catalog', 'product', 'search', 'filter', 'cart', 'checkout', 'payment', 'order', 'returns'],\n workflowGoals: ['discover product catalogue', 'map cart and checkout', 'capture order confirmation and failure states', 'map search/filter/sort'],\n piiRules: ['mask email', 'mask phone', 'mask address', 'mask card-like numbers'],\n riskyActions: ['place order', 'submit payment', 'save card', 'apply real coupon'],\n safeProbeRules: ['do not click final payment/order buttons', 'use invalid test card data only', 'stop before irreversible purchase'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n banking: {\n industry: 'banking',\n importantScreenPatterns: ['login', 'mfa', 'dashboard', 'accounts', 'transactions', 'transfer', 'beneficiary', 'statement', 'dispute'],\n workflowGoals: ['map account overview', 'map transactions and statements', 'map transfer setup without submitting', 'map MFA/session timeout states'],\n piiRules: ['mask account numbers', 'mask balances', 'mask names', 'mask addresses', 'mask tax identifiers'],\n riskyActions: ['transfer money', 'add beneficiary', 'submit dispute', 'download sensitive statement'],\n safeProbeRules: ['read-only by default', 'never submit transfer confirmation', 'never alter beneficiary data'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n fintech: {\n industry: 'fintech',\n importantScreenPatterns: ['kyc', 'wallet', 'transactions', 'cards', 'payments', 'payouts', 'risk', 'compliance', 'disputes'],\n workflowGoals: ['map KYC onboarding', 'map wallet/payment states', 'map failed payment and dispute flows', 'capture compliance screens'],\n piiRules: ['mask card numbers', 'mask bank accounts', 'mask identity documents', 'mask balances'],\n riskyActions: ['send payment', 'withdraw funds', 'change card status', 'submit KYC with real data'],\n safeProbeRules: ['sandbox data only', 'stop before money movement', 'never upload real identity documents'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n healthcare: {\n industry: 'healthcare',\n importantScreenPatterns: ['patient', 'appointment', 'claims', 'provider', 'prescription', 'records', 'lab', 'consent'],\n workflowGoals: ['map patient lookup safely', 'map appointment booking before final submit', 'map claims and prescription screens', 'capture consent and records UX'],\n piiRules: ['mask PHI', 'mask patient names', 'mask MRN', 'mask DOB', 'mask diagnosis', 'mask prescriptions'],\n riskyActions: ['schedule real appointment', 'submit claim', 'change medication', 'message provider with PHI'],\n safeProbeRules: ['use synthetic patients only', 'do not submit clinical changes', 'avoid external notifications'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n pharma: {\n industry: 'pharma',\n importantScreenPatterns: ['product', 'dosage', 'safety', 'batch', 'adverse-event', 'regulatory', 'trial', 'label'],\n workflowGoals: ['map product and safety content', 'map adverse event intake before submit', 'capture regulatory and batch workflows'],\n piiRules: ['mask patient reports', 'mask reporter contact', 'mask trial participant identifiers'],\n riskyActions: ['submit adverse event', 'change safety record', 'publish regulatory content'],\n safeProbeRules: ['stop before regulatory or safety submission', 'use synthetic reports only'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n agriculture: {\n industry: 'agriculture',\n importantScreenPatterns: ['crop', 'field', 'weather', 'inventory', 'marketplace', 'logistics', 'sensor', 'subsidy'],\n workflowGoals: ['map crop and field planning', 'map inventory/marketplace flows', 'capture logistics and sensor dashboards'],\n piiRules: ['mask farmer identity', 'mask location precision', 'mask subsidy identifiers'],\n riskyActions: ['place market order', 'submit subsidy application', 'change sensor/device control'],\n safeProbeRules: ['avoid irreversible orders', 'avoid device actuation', 'use test farm data'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n 'legacy-enterprise': {\n industry: 'legacy-enterprise',\n importantScreenPatterns: ['frameset', 'iframe', 'popup', 'table', 'search', 'admin', 'report', 'export'],\n workflowGoals: ['map menu trees', 'capture iframe and popup states', 'map table filters and reports', 'detect session timeout'],\n piiRules: ['mask table cells that look like PII', 'mask exported identifiers'],\n riskyActions: ['bulk update', 'delete record', 'export production data', 'submit admin change'],\n safeProbeRules: ['read-only table probing', 'never click delete/bulk actions', 'capture popups without committing'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n saas: {\n industry: 'saas',\n importantScreenPatterns: ['dashboard', 'settings', 'users', 'billing', 'integrations', 'reports', 'admin', 'workspace'],\n workflowGoals: ['map workspace navigation', 'map settings and integrations', 'map role/user management without changing state', 'capture dashboards and reports'],\n piiRules: ['mask emails', 'mask tokens', 'mask API keys', 'mask billing data'],\n riskyActions: ['delete workspace', 'invite real user', 'rotate key', 'change billing plan'],\n safeProbeRules: ['never submit destructive settings', 'redact secrets immediately', 'use sandbox users'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n unknown: {\n industry: 'unknown',\n importantScreenPatterns: ['login', 'dashboard', 'settings', 'search', 'detail', 'report', 'form'],\n workflowGoals: ['map top-level navigation', 'capture forms and validation states', 'discover hidden menus', 'classify domain after first pages'],\n piiRules: ['mask emails', 'mask phone numbers', 'mask long identifiers', 'mask card-like numbers'],\n riskyActions: ['delete', 'submit payment', 'transfer', 'publish', 'send notification'],\n safeProbeRules: ['read-only by default', 'stop before irreversible actions', 'use synthetic data only'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n};\n\nconst INDUSTRY_HINTS: Array<{ industry: Industry; terms: RegExp[] }> = [\n { industry: 'ecommerce', terms: [/cart/i, /checkout/i, /product/i, /catalog/i, /sku/i, /wishlist/i, /order/i] },\n { industry: 'banking', terms: [/account/i, /statement/i, /beneficiar/i, /transfer/i, /iban/i, /routing/i, /balance/i] },\n { industry: 'fintech', terms: [/wallet/i, /kyc/i, /payout/i, /card/i, /payment/i, /risk/i, /dispute/i] },\n { industry: 'healthcare', terms: [/patient/i, /provider/i, /appointment/i, /claim/i, /prescription/i, /medical/i, /lab result/i] },\n { industry: 'pharma', terms: [/pharma/i, /dosage/i, /adverse/i, /batch/i, /regulatory/i, /clinical trial/i, /label/i] },\n { industry: 'agriculture', terms: [/crop/i, /field/i, /farm/i, /harvest/i, /sensor/i, /weather/i, /subsidy/i] },\n { industry: 'legacy-enterprise', terms: [/frameset/i, /iframe/i, /popup/i, /legacy/i, /report export/i, /mainframe/i] },\n { industry: 'saas', terms: [/workspace/i, /tenant/i, /integration/i, /billing/i, /api key/i, /admin/i, /dashboard/i] },\n];\n\nexport function classifyDomainProfile(input: {\n appUrl: string;\n projectName?: string | null;\n appType?: string | null;\n seedText?: string;\n}): DomainProfile {\n const haystack = [input.appUrl, input.projectName ?? '', input.appType ?? '', input.seedText ?? ''].join(' ');\n const scores = new Map<Industry, { score: number; evidence: string[] }>();\n for (const { industry, terms } of INDUSTRY_HINTS) {\n for (const term of terms) {\n if (term.test(haystack)) {\n const prev = scores.get(industry) ?? { score: 0, evidence: [] };\n prev.score += 1;\n prev.evidence.push(term.source);\n scores.set(industry, prev);\n }\n }\n }\n let selected: Industry = 'unknown';\n let best = 0;\n let evidence: string[] = [];\n for (const [industry, result] of scores) {\n if (result.score > best) {\n selected = industry;\n best = result.score;\n evidence = result.evidence;\n }\n }\n const base = PROFILES[selected];\n return {\n ...base,\n confidence: best === 0 ? 0.25 : Math.min(0.95, 0.35 + best * 0.15),\n evidence,\n };\n}\n\nexport function getProfile(industry: Industry): DomainProfile {\n const base = PROFILES[industry] ?? PROFILES.unknown;\n return { ...base, confidence: 1, evidence: ['explicit'] };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export type Industry =\n | 'ecommerce'\n | 'banking'\n | 'fintech'\n | 'healthcare'\n | 'pharma'\n | 'agriculture'\n | 'legacy-enterprise'\n | 'saas'\n | 'unknown';\n\nexport interface DomainProfile {\n industry: Industry;\n confidence: number;\n importantScreenPatterns: string[];\n workflowGoals: string[];\n piiRules: string[];\n riskyActions: string[];\n safeProbeRules: string[];\n requiredArtifacts: string[];\n evidence: string[];\n}\n\nconst COMMON_REQUIRED_ARTIFACTS = [\n 'screenshot',\n 'dom',\n 'markdown',\n 'accessibility',\n 'network',\n 'performance',\n];\n\nconst PROFILES: Record<Industry, Omit<DomainProfile, 'confidence' | 'evidence'>> = {\n ecommerce: {\n industry: 'ecommerce',\n importantScreenPatterns: ['catalog', 'product', 'search', 'filter', 'cart', 'checkout', 'payment', 'order', 'returns'],\n workflowGoals: ['discover product catalogue', 'map cart and checkout', 'capture order confirmation and failure states', 'map search/filter/sort'],\n piiRules: ['mask email', 'mask phone', 'mask address', 'mask card-like numbers'],\n riskyActions: ['place order', 'submit payment', 'save card', 'apply real coupon'],\n safeProbeRules: ['do not click final payment/order buttons', 'use invalid test card data only', 'stop before irreversible purchase'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n banking: {\n industry: 'banking',\n importantScreenPatterns: ['login', 'mfa', 'dashboard', 'accounts', 'transactions', 'transfer', 'beneficiary', 'statement', 'dispute'],\n workflowGoals: ['map account overview', 'map transactions and statements', 'map transfer setup without submitting', 'map MFA/session timeout states'],\n piiRules: ['mask account numbers', 'mask balances', 'mask names', 'mask addresses', 'mask tax identifiers'],\n riskyActions: ['transfer money', 'add beneficiary', 'submit dispute', 'download sensitive statement'],\n safeProbeRules: ['read-only by default', 'never submit transfer confirmation', 'never alter beneficiary data'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n fintech: {\n industry: 'fintech',\n importantScreenPatterns: ['kyc', 'wallet', 'transactions', 'cards', 'payments', 'payouts', 'risk', 'compliance', 'disputes'],\n workflowGoals: ['map KYC onboarding', 'map wallet/payment states', 'map failed payment and dispute flows', 'capture compliance screens'],\n piiRules: ['mask card numbers', 'mask bank accounts', 'mask identity documents', 'mask balances'],\n riskyActions: ['send payment', 'withdraw funds', 'change card status', 'submit KYC with real data'],\n safeProbeRules: ['sandbox data only', 'stop before money movement', 'never upload real identity documents'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n healthcare: {\n industry: 'healthcare',\n importantScreenPatterns: ['patient', 'appointment', 'claims', 'provider', 'prescription', 'records', 'lab', 'consent'],\n workflowGoals: ['map patient lookup safely', 'map appointment booking before final submit', 'map claims and prescription screens', 'capture consent and records UX'],\n piiRules: ['mask PHI', 'mask patient names', 'mask MRN', 'mask DOB', 'mask diagnosis', 'mask prescriptions'],\n riskyActions: ['schedule real appointment', 'submit claim', 'change medication', 'message provider with PHI'],\n safeProbeRules: ['use synthetic patients only', 'do not submit clinical changes', 'avoid external notifications'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n pharma: {\n industry: 'pharma',\n importantScreenPatterns: ['product', 'dosage', 'safety', 'batch', 'adverse-event', 'regulatory', 'trial', 'label'],\n workflowGoals: ['map product and safety content', 'map adverse event intake before submit', 'capture regulatory and batch workflows'],\n piiRules: ['mask patient reports', 'mask reporter contact', 'mask trial participant identifiers'],\n riskyActions: ['submit adverse event', 'change safety record', 'publish regulatory content'],\n safeProbeRules: ['stop before regulatory or safety submission', 'use synthetic reports only'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n agriculture: {\n industry: 'agriculture',\n importantScreenPatterns: ['crop', 'field', 'weather', 'inventory', 'marketplace', 'logistics', 'sensor', 'subsidy'],\n workflowGoals: ['map crop and field planning', 'map inventory/marketplace flows', 'capture logistics and sensor dashboards'],\n piiRules: ['mask farmer identity', 'mask location precision', 'mask subsidy identifiers'],\n riskyActions: ['place market order', 'submit subsidy application', 'change sensor/device control'],\n safeProbeRules: ['avoid irreversible orders', 'avoid device actuation', 'use test farm data'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n 'legacy-enterprise': {\n industry: 'legacy-enterprise',\n importantScreenPatterns: ['frameset', 'iframe', 'popup', 'table', 'search', 'admin', 'report', 'export'],\n workflowGoals: ['map menu trees', 'capture iframe and popup states', 'map table filters and reports', 'detect session timeout'],\n piiRules: ['mask table cells that look like PII', 'mask exported identifiers'],\n riskyActions: ['bulk update', 'delete record', 'export production data', 'submit admin change'],\n safeProbeRules: ['read-only table probing', 'never click delete/bulk actions', 'capture popups without committing'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n saas: {\n industry: 'saas',\n importantScreenPatterns: ['dashboard', 'settings', 'users', 'billing', 'integrations', 'reports', 'admin', 'workspace'],\n workflowGoals: ['map workspace navigation', 'map settings and integrations', 'map role/user management without changing state', 'capture dashboards and reports'],\n piiRules: ['mask emails', 'mask tokens', 'mask API keys', 'mask billing data'],\n riskyActions: ['delete workspace', 'invite real user', 'rotate key', 'change billing plan'],\n safeProbeRules: ['never submit destructive settings', 'redact secrets immediately', 'use sandbox users'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n unknown: {\n industry: 'unknown',\n importantScreenPatterns: ['login', 'dashboard', 'settings', 'search', 'detail', 'report', 'form'],\n workflowGoals: ['map top-level navigation', 'capture forms and validation states', 'discover hidden menus', 'classify domain after first pages'],\n piiRules: ['mask emails', 'mask phone numbers', 'mask long identifiers', 'mask card-like numbers'],\n riskyActions: ['delete', 'submit payment', 'transfer', 'publish', 'send notification'],\n safeProbeRules: ['read-only by default', 'stop before irreversible actions', 'use synthetic data only'],\n requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,\n },\n};\n\nconst BASE_INDUSTRY_HINTS: Array<{ industry: Industry; terms: RegExp[] }> = [\n { industry: 'ecommerce', terms: [/cart/i, /checkout/i, /product/i, /catalog/i, /sku/i, /wishlist/i, /order/i] },\n { industry: 'banking', terms: [/account/i, /statement/i, /beneficiar/i, /transfer/i, /iban/i, /routing/i, /balance/i] },\n { industry: 'fintech', terms: [/wallet/i, /kyc/i, /payout/i, /card/i, /payment/i, /risk/i, /dispute/i] },\n { industry: 'healthcare', terms: [/patient/i, /provider/i, /appointment/i, /claim/i, /prescription/i, /medical/i, /lab result/i] },\n { industry: 'pharma', terms: [/pharma/i, /dosage/i, /adverse/i, /batch/i, /regulatory/i, /clinical trial/i, /label/i] },\n { industry: 'agriculture', terms: [/crop/i, /field/i, /farm/i, /harvest/i, /sensor/i, /weather/i, /subsidy/i] },\n { industry: 'legacy-enterprise', terms: [/frameset/i, /iframe/i, /popup/i, /legacy/i, /report export/i, /mainframe/i] },\n { industry: 'saas', terms: [/workspace/i, /tenant/i, /integration/i, /billing/i, /api key/i, /admin/i, /dashboard/i] },\n];\n\n// Allow additional industry hint keywords via ENV without replacing the baseline.\n// Format: JSON array of { industry: string; terms: string[] } \u2014 terms are treated as case-insensitive regex.\n// Example: EXTRA_INDUSTRY_HINTS_JSON='[{\"industry\":\"saas\",\"terms\":[\"platform\",\"multitenant\"]}]'\nconst _extraHints: Array<{ industry: Industry; terms: RegExp[] }> = (() => {\n const raw = process.env.EXTRA_INDUSTRY_HINTS_JSON;\n if (!raw) return [];\n try {\n const parsed: Array<{ industry: string; terms: string[] }> = JSON.parse(raw);\n return parsed.map(h => ({ industry: h.industry as Industry, terms: h.terms.map(t => new RegExp(t, 'i')) }));\n } catch { return []; }\n})();\n\nconst INDUSTRY_HINTS = [...BASE_INDUSTRY_HINTS, ..._extraHints];\n\n// Allow patching profile fields (riskyActions, piiRules, etc.) per industry via ENV.\n// Format: JSON object keyed by industry, value is partial DomainProfile fields to merge.\n// Example: DOMAIN_PROFILE_OVERRIDES_JSON='{\"saas\":{\"riskyActions\":[\"delete workspace\",\"export all data\"]}}'\nconst _profileOverrides: Partial<Record<Industry, Partial<Omit<DomainProfile, 'confidence' | 'evidence'>>>> = (() => {\n const raw = process.env.DOMAIN_PROFILE_OVERRIDES_JSON;\n if (!raw) return {};\n try { return JSON.parse(raw); } catch { return {}; }\n})();\n\nexport function classifyDomainProfile(input: {\n appUrl: string;\n projectName?: string | null;\n appType?: string | null;\n seedText?: string;\n}): DomainProfile {\n const haystack = [input.appUrl, input.projectName ?? '', input.appType ?? '', input.seedText ?? ''].join(' ');\n const scores = new Map<Industry, { score: number; evidence: string[] }>();\n for (const { industry, terms } of INDUSTRY_HINTS) {\n for (const term of terms) {\n if (term.test(haystack)) {\n const prev = scores.get(industry) ?? { score: 0, evidence: [] };\n prev.score += 1;\n prev.evidence.push(term.source);\n scores.set(industry, prev);\n }\n }\n }\n let selected: Industry = 'unknown';\n let best = 0;\n let evidence: string[] = [];\n for (const [industry, result] of scores) {\n if (result.score > best) {\n selected = industry;\n best = result.score;\n evidence = result.evidence;\n }\n }\n const base = PROFILES[selected];\n return {\n ...base,\n confidence: best === 0 ? 0.25 : Math.min(0.95, 0.35 + best * 0.15),\n evidence,\n };\n}\n\nexport function getProfile(industry: Industry): DomainProfile {\n const base = PROFILES[industry] ?? PROFILES.unknown;\n return { ...base, confidence: 1, evidence: ['explicit'] };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -11,8 +11,8 @@ class DomainRateLimiter {
11
11
  burstSize;
12
12
  rateMap;
13
13
  constructor(opts = {}) {
14
- this.defaultRps = opts.defaultRps ?? 2;
15
- this.burstSize = opts.burstSize ?? 5;
14
+ this.defaultRps = opts.defaultRps ?? (parseFloat(process.env.CRAWLER_RATE_LIMIT_DEFAULT_RPS ?? "") || 2);
15
+ this.burstSize = opts.burstSize ?? (parseInt(process.env.CRAWLER_RATE_LIMIT_BURST_SIZE ?? "") || 5);
16
16
  this.rateMap = opts.rateMap ?? {};
17
17
  }
18
18
  getBucket(domain) {