@veraxhq/verax 0.1.0 → 0.2.1

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 (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Page Frontier — Multi-page traversal manager
3
+ *
4
+ * Manages a queue of pages to visit and enforces:
5
+ * - Same-origin only
6
+ * - No re-visiting (canonical URLs)
7
+ * - Budget limits (maxPages, maxScanDurationMs, maxUniqueUrls)
8
+ * - Skipping dangerous actions (logout, delete)
9
+ */
10
+
11
+ import { canonicalizeUrl } from '../shared/url-normalizer.js';
12
+
13
+ // Normalize a textual label for deterministic matching
14
+ function normalizeLabel(label) {
15
+ return (label || '').toLowerCase().replace(/\s+/g, ' ').trim();
16
+ }
17
+
18
+ // Deterministic destructive-action classifier
19
+ // Returns { skip: boolean, reasonCode: string|null, matched: string|null }
20
+ function isDestructiveLabel(label) {
21
+ const normalized = normalizeLabel(label);
22
+ if (!normalized) {
23
+ return { skip: false, reasonCode: null, matched: null };
24
+ }
25
+
26
+ // Strong destructive keywords (word-boundary guarded)
27
+ const strongKeywords = ['delete', 'remove', 'erase', 'wipe', 'destroy', 'drop', 'reset', 'terminate', 'unsubscribe', 'deactivate'];
28
+ const strongRegex = new RegExp(`\\b(${strongKeywords.join('|')})\\b`, 'i');
29
+
30
+ // Financial/destructive-ish actions we still treat conservatively
31
+ const financialRegex = /\b(pay|purchase|checkout)\b/i;
32
+
33
+ // Safe allowlist for clear-related actions (explicitly non-destructive)
34
+ const safeClearRegex = /\bclear\b[^\w]*(filters?|search|selection|input|form|field|query|results?)\b/i;
35
+
36
+ // Destructive uses of "clear" require sensitive nouns
37
+ const destructiveClearRegex = /\bclear\b[^\w]*(data|account|all|history|cache|storage|database|everything|session|profile|settings|config)\b/i;
38
+
39
+ // If it is an explicit safe clear phrase, allow
40
+ if (safeClearRegex.test(normalized)) {
41
+ return { skip: false, reasonCode: null, matched: 'safe_clear' };
42
+ }
43
+
44
+ // If strong destructive or financial keyword appears, skip
45
+ const strongMatch = normalized.match(strongRegex);
46
+ if (strongMatch) {
47
+ return { skip: true, reasonCode: 'destructive_keyword', matched: strongMatch[1] };
48
+ }
49
+
50
+ const financialMatch = normalized.match(financialRegex);
51
+ if (financialMatch) {
52
+ return { skip: true, reasonCode: 'financial_action', matched: financialMatch[1] };
53
+ }
54
+
55
+ // Handle ambiguous "clear" only when paired with sensitive nouns
56
+ const clearMatch = normalized.match(destructiveClearRegex);
57
+ if (clearMatch) {
58
+ return { skip: true, reasonCode: 'clear_sensitive', matched: clearMatch[0] };
59
+ }
60
+
61
+ return { skip: false, reasonCode: null, matched: null };
62
+ }
63
+
64
+ export class PageFrontier {
65
+ constructor(startUrl, baseOrigin, scanBudget, startTime) {
66
+ this.baseOrigin = baseOrigin;
67
+ this.scanBudget = scanBudget;
68
+ this.startTime = startTime;
69
+
70
+ this.queue = [startUrl]; // URLs to visit
71
+ this.visited = new Set(); // Visited URLs (canonical form)
72
+ this.pagesVisited = 0;
73
+ this.pagesDiscovered = 1; // include start page
74
+ this.frontierCapped = false; // Track if maxUniqueUrls was exceeded
75
+ }
76
+
77
+ /**
78
+ * Normalize a URL to canonical form using shared URL normalizer.
79
+ * - Remove hash fragments
80
+ * - Sort query params
81
+ * - Drop tracking params (utm_*, gclid, fbclid, etc.)
82
+ */
83
+ normalizeUrl(urlString) {
84
+ return canonicalizeUrl(urlString);
85
+ }
86
+
87
+ /**
88
+ * Check if URL is same-origin.
89
+ */
90
+ isSameOrigin(urlString) {
91
+ try {
92
+ const url = new URL(urlString);
93
+ if (url.protocol === 'file:' && this.baseOrigin.startsWith('file:')) {
94
+ return true;
95
+ }
96
+ return url.origin === this.baseOrigin;
97
+ } catch (error) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check if we've exceeded page limit.
104
+ */
105
+ isPageLimitExceeded() {
106
+ if (this.scanBudget.maxPages && this.pagesVisited >= this.scanBudget.maxPages) {
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Check if we've exceeded time limit.
114
+ */
115
+ isTimeLimitExceeded() {
116
+ const elapsed = Date.now() - this.startTime;
117
+ return elapsed > this.scanBudget.maxScanDurationMs;
118
+ }
119
+
120
+ /**
121
+ * Get next URL to visit (if any and within budget).
122
+ * Returns null if queue empty or limits exceeded.
123
+ */
124
+ getNextUrl() {
125
+ // Check limits
126
+ if (this.isPageLimitExceeded() || this.isTimeLimitExceeded()) {
127
+ return null;
128
+ }
129
+
130
+ // Find next unvisited same-origin URL
131
+ while (this.queue.length > 0) {
132
+ const nextUrl = this.queue.shift();
133
+ const normalized = this.normalizeUrl(nextUrl);
134
+
135
+ // Skip if already visited
136
+ if (this.visited.has(normalized)) {
137
+ continue;
138
+ }
139
+
140
+ // Skip if not same-origin
141
+ if (!this.isSameOrigin(nextUrl)) {
142
+ continue;
143
+ }
144
+
145
+ this.visited.add(normalized);
146
+ return nextUrl;
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Add a new URL to the frontier (discovered during interaction).
154
+ * Only adds if same-origin, not already visited, and within maxUniqueUrls limit.
155
+ * Returns true if added, false if skipped (with reason).
156
+ */
157
+ addUrl(urlString) {
158
+ if (!this.isSameOrigin(urlString)) {
159
+ return false;
160
+ }
161
+
162
+ const normalized = this.normalizeUrl(urlString);
163
+ if (this.visited.has(normalized)) {
164
+ return false;
165
+ }
166
+
167
+ // Check maxUniqueUrls cap
168
+ const maxUniqueUrls = this.scanBudget.maxUniqueUrls || Infinity;
169
+ if (this.pagesDiscovered >= maxUniqueUrls) {
170
+ this.frontierCapped = true;
171
+ return false; // Frontier capped, don't add
172
+ }
173
+
174
+ // Add the normalized URL to visited set and original to queue
175
+ this.visited.add(normalized);
176
+ this.queue.push(urlString);
177
+ this.pagesDiscovered++;
178
+ return true;
179
+ }
180
+
181
+ /**
182
+ * Mark that we've visited a page.
183
+ */
184
+ markVisited() {
185
+ this.pagesVisited++;
186
+ }
187
+
188
+ /**
189
+ * Check if an interaction should be skipped (destructive/safe-action policy).
190
+ * Returns { skip: boolean, reason: string } with strict rules.
191
+ * Only uses properties available from interaction discovery (text, label, selector).
192
+ * Note: label may contain aria-label if that was the source (from extractLabel).
193
+ */
194
+ shouldSkipInteraction(interaction) {
195
+ // Allow explicit auth flows when labeled by type (keep auth testing intact)
196
+ if (interaction.type === 'login' || interaction.type === 'logout') {
197
+ return { skip: false, reason: null };
198
+ }
199
+
200
+ // Aggregate all human-visible labels
201
+ const text = (interaction.text || '').trim();
202
+ const label = (interaction.label || '').trim();
203
+ const ariaLabel = (interaction.ariaLabel || '').trim();
204
+ const combinedText = `${text} ${label} ${ariaLabel}`.trim();
205
+
206
+ const destructiveCheck = isDestructiveLabel(combinedText);
207
+ if (destructiveCheck.skip) {
208
+ return { skip: true, reason: 'safety_policy', matched: destructiveCheck.matched };
209
+ }
210
+
211
+ // Check selector for explicit danger markers
212
+ const selector = (interaction.selector || '').toLowerCase();
213
+ if (selector.includes('data-danger') || selector.includes('data-destructive')) {
214
+ return { skip: true, reason: 'safety_policy', matched: 'data-danger' };
215
+ }
216
+
217
+ return { skip: false, reason: null };
218
+ }
219
+
220
+ /**
221
+ * Get frontier stats.
222
+ */
223
+ getStats() {
224
+ return {
225
+ pagesVisited: this.pagesVisited,
226
+ pagesDiscovered: this.pagesDiscovered,
227
+ queueLength: this.queue.length,
228
+ isPageLimitExceeded: this.isPageLimitExceeded(),
229
+ isTimeLimitExceeded: this.isTimeLimitExceeded()
230
+ };
231
+ }
232
+ }
233
+
234
+ export { normalizeLabel, isDestructiveLabel };
@@ -1,25 +1,30 @@
1
1
  /**
2
2
  * WAVE 2: Deterministic DOM settle logic
3
3
  * Waits for page to stabilize after navigation or interaction
4
+ * @typedef {import('playwright').Page} Page
4
5
  */
5
6
 
7
+ import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
8
+
6
9
  /**
7
10
  * Wait for page to settle after navigation or interaction.
8
11
  * Combines multiple signals: load event, network idle, DOM mutation stabilization.
12
+ * With adaptive stabilization, extends windows if DOM/network still changing.
9
13
  *
10
14
  * @param {Page} page - Playwright page object
11
- * @param {Object} options
12
- * @param {number} options.timeoutMs - Overall timeout (default 30000)
13
- * @param {number} options.idleMs - Network idle threshold (default 1500)
14
- * @param {number} options.domStableMs - DOM stability window (default 2000)
15
+ * @param {Object} scanBudget - ScanBudget with settle timing parameters
15
16
  * @returns {Promise<void>}
16
17
  */
17
- export async function waitForSettle(page, options = {}) {
18
- const {
19
- timeoutMs = 30000,
20
- idleMs = 1500,
21
- domStableMs = 2000
22
- } = options;
18
+ export async function waitForSettle(page, scanBudget = DEFAULT_SCAN_BUDGET) {
19
+ const baseTimeoutMs = scanBudget.settleTimeoutMs;
20
+ const baseIdleMs = scanBudget.settleIdleMs;
21
+ const baseDomStableMs = scanBudget.settleDomStableMs;
22
+ const adaptiveStabilization = scanBudget.adaptiveStabilization || false;
23
+
24
+ // With adaptive stabilization, allow up to 1.5x the base timeout for difficult pages
25
+ const timeoutMs = adaptiveStabilization ? Math.round(baseTimeoutMs * 1.5) : baseTimeoutMs;
26
+ const idleMs = baseIdleMs;
27
+ const domStableMs = baseDomStableMs;
23
28
 
24
29
  const startTime = Date.now();
25
30
 
@@ -31,10 +36,10 @@ export async function waitForSettle(page, options = {}) {
31
36
  ]).catch(() => {});
32
37
 
33
38
  // Signal 2: Network idle detection using Playwright Request/Response events
34
- await waitForNetworkIdle(page, idleMs, timeoutMs - (Date.now() - startTime));
39
+ await waitForNetworkIdle(page, idleMs, timeoutMs - (Date.now() - startTime), adaptiveStabilization);
35
40
 
36
41
  // Signal 3: DOM mutation stabilization
37
- await waitForDomStability(page, domStableMs, timeoutMs - (Date.now() - startTime));
42
+ await waitForDomStability(page, domStableMs, timeoutMs - (Date.now() - startTime), adaptiveStabilization);
38
43
  } catch (err) {
39
44
  // Timeout is acceptable - page may have settled despite timeout
40
45
  if (!err.message?.includes('Timeout')) {
@@ -46,16 +51,23 @@ export async function waitForSettle(page, options = {}) {
46
51
  /**
47
52
  * Wait for network to become idle (no inflight requests for idleMs).
48
53
  * Uses Playwright's Request/Response event listening.
54
+ * With adaptive stabilization, may extend the idle window if network restarts.
49
55
  */
50
- async function waitForNetworkIdle(page, idleMs, timeoutMs) {
56
+ async function waitForNetworkIdle(page, idleMs, timeoutMs, adaptiveStabilization = false) {
51
57
  if (timeoutMs <= 0) return;
52
58
 
53
59
  return new Promise((resolve) => {
54
60
  let lastNetworkActivityTime = Date.now();
55
61
  let hasFinished = false;
62
+ let extensionCount = 0;
63
+ const maxExtensions = adaptiveStabilization ? 2 : 0;
56
64
 
57
65
  const onRequest = () => {
58
66
  lastNetworkActivityTime = Date.now();
67
+ // With adaptive stabilization, if network restarts, we extend the idle window
68
+ if (adaptiveStabilization && extensionCount < maxExtensions) {
69
+ extensionCount++;
70
+ }
59
71
  };
60
72
 
61
73
  const onResponse = () => {
@@ -97,18 +109,26 @@ async function waitForNetworkIdle(page, idleMs, timeoutMs) {
97
109
  /**
98
110
  * Wait for DOM mutations to stabilize (no mutations for domStableMs).
99
111
  * Uses MutationObserver to track DOM changes.
112
+ * With adaptive stabilization, may extend if DOM changes restart.
100
113
  */
101
- async function waitForDomStability(page, domStableMs, timeoutMs) {
114
+ async function waitForDomStability(page, domStableMs, timeoutMs, adaptiveStabilization = false) {
102
115
  if (timeoutMs <= 0) return;
103
116
 
104
117
  await page.evaluate(
105
- async (domStableMs, timeoutMs) => {
118
+ async (domStableMs, timeoutMs, shouldAdapt) => {
106
119
  return new Promise((resolve) => {
107
120
  let lastMutationTime = Date.now();
108
121
  let hasFinished = false;
122
+ let extensionCount = 0;
123
+ const maxExtensions = shouldAdapt ? 2 : 0;
109
124
 
110
125
  const observer = new MutationObserver(() => {
111
- lastMutationTime = Date.now();
126
+ const now = Date.now();
127
+ // With adaptive stabilization, if mutations restart, extend window
128
+ if (shouldAdapt && now - lastMutationTime >= domStableMs && extensionCount < maxExtensions) {
129
+ extensionCount++;
130
+ }
131
+ lastMutationTime = now;
112
132
  });
113
133
 
114
134
  observer.observe(document.documentElement, {
@@ -148,7 +168,8 @@ async function waitForDomStability(page, domStableMs, timeoutMs) {
148
168
  });
149
169
  },
150
170
  domStableMs,
151
- timeoutMs
171
+ timeoutMs,
172
+ adaptiveStabilization
152
173
  ).catch(() => {
153
174
  // Page may have navigated, ignore
154
175
  });