@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Project Discovery Module
|
|
6
|
+
* Detects framework, router, source root, and dev server configuration
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} ProjectProfile
|
|
9
|
+
* @property {string} framework
|
|
10
|
+
* @property {string|null} router
|
|
11
|
+
* @property {string} sourceRoot
|
|
12
|
+
* @property {string} packageManager
|
|
13
|
+
* @property {{dev: string|null, build: string|null, start: string|null}} scripts
|
|
14
|
+
* @property {string} detectedAt
|
|
15
|
+
* @property {string|null} packageJsonPath
|
|
16
|
+
* @property {number} [fileCount] - Optional file count for budget calculation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} srcPath
|
|
21
|
+
* @returns {Promise<ProjectProfile>}
|
|
22
|
+
*/
|
|
23
|
+
export async function discoverProject(srcPath) {
|
|
24
|
+
const projectRoot = resolve(srcPath);
|
|
25
|
+
|
|
26
|
+
// Find the nearest package.json
|
|
27
|
+
const packageJsonPath = findPackageJson(projectRoot);
|
|
28
|
+
|
|
29
|
+
// If there's a package.json, use its directory
|
|
30
|
+
// Otherwise, use the srcPath (even if it's a static HTML project)
|
|
31
|
+
const projectDir = packageJsonPath ? dirname(packageJsonPath) : projectRoot;
|
|
32
|
+
|
|
33
|
+
let packageJson = null;
|
|
34
|
+
if (packageJsonPath && existsSync(packageJsonPath)) {
|
|
35
|
+
try {
|
|
36
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
packageJson = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Detect framework
|
|
43
|
+
const framework = detectFramework(projectDir, packageJson);
|
|
44
|
+
const router = detectRouter(framework, projectDir);
|
|
45
|
+
|
|
46
|
+
// Determine package manager
|
|
47
|
+
const packageManager = detectPackageManager(projectDir);
|
|
48
|
+
|
|
49
|
+
// Extract scripts
|
|
50
|
+
const scripts = {
|
|
51
|
+
dev: packageJson?.scripts?.dev || null,
|
|
52
|
+
build: packageJson?.scripts?.build || null,
|
|
53
|
+
start: packageJson?.scripts?.start || null,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
framework,
|
|
58
|
+
router,
|
|
59
|
+
sourceRoot: projectDir,
|
|
60
|
+
packageManager,
|
|
61
|
+
scripts,
|
|
62
|
+
detectedAt: new Date().toISOString(),
|
|
63
|
+
packageJsonPath,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find the nearest package.json by walking up directories
|
|
69
|
+
*/
|
|
70
|
+
function findPackageJson(startPath) {
|
|
71
|
+
let currentPath = resolve(startPath);
|
|
72
|
+
|
|
73
|
+
// First check if package.json exists in startPath itself
|
|
74
|
+
const immediatePackage = resolve(currentPath, 'package.json');
|
|
75
|
+
if (existsSync(immediatePackage)) {
|
|
76
|
+
return immediatePackage;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For static HTML projects, don't walk up - use the startPath as project root
|
|
80
|
+
// This prevents finding parent package.json files that aren't relevant
|
|
81
|
+
if (hasStaticHtml(currentPath)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Then walk up (limit to 5 levels for monorepos, not 10)
|
|
86
|
+
for (let i = 0; i < 5; i++) {
|
|
87
|
+
const parentPath = dirname(currentPath);
|
|
88
|
+
if (parentPath === currentPath) {
|
|
89
|
+
// Reached filesystem root
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
currentPath = parentPath;
|
|
94
|
+
const packageJsonPath = resolve(currentPath, 'package.json');
|
|
95
|
+
if (existsSync(packageJsonPath)) {
|
|
96
|
+
return packageJsonPath;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detect the framework type
|
|
105
|
+
*/
|
|
106
|
+
function detectFramework(projectDir, packageJson) {
|
|
107
|
+
// Check for Next.js
|
|
108
|
+
if (hasNextJs(projectDir, packageJson)) {
|
|
109
|
+
return 'nextjs';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for Vite + React
|
|
113
|
+
if (hasViteReact(projectDir, packageJson)) {
|
|
114
|
+
return 'react-vite';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for Create React App
|
|
118
|
+
if (hasCreateReactApp(packageJson)) {
|
|
119
|
+
return 'react-cra';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for static HTML
|
|
123
|
+
if (hasStaticHtml(projectDir)) {
|
|
124
|
+
return 'static-html';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Unknown framework
|
|
128
|
+
return 'unknown';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detect Next.js
|
|
133
|
+
*/
|
|
134
|
+
function hasNextJs(projectDir, packageJson) {
|
|
135
|
+
// Check for next.config.js or next.config.mjs
|
|
136
|
+
const hasNextConfig = existsSync(resolve(projectDir, 'next.config.js')) ||
|
|
137
|
+
existsSync(resolve(projectDir, 'next.config.mjs')) ||
|
|
138
|
+
existsSync(resolve(projectDir, 'next.config.ts'));
|
|
139
|
+
|
|
140
|
+
if (hasNextConfig) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for 'next' dependency
|
|
145
|
+
if (packageJson?.dependencies?.next || packageJson?.devDependencies?.next) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Detect router type for Next.js
|
|
154
|
+
*/
|
|
155
|
+
function detectRouter(framework, projectDir) {
|
|
156
|
+
if (framework !== 'nextjs') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for /app directory (app router) - must contain actual files
|
|
161
|
+
const appPath = resolve(projectDir, 'app');
|
|
162
|
+
if (existsSync(appPath) && hasRouteFiles(appPath)) {
|
|
163
|
+
return 'app';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check for /pages directory (pages router)
|
|
167
|
+
if (existsSync(resolve(projectDir, 'pages'))) {
|
|
168
|
+
return 'pages';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a directory contains route files (not just an empty scaffold)
|
|
176
|
+
*/
|
|
177
|
+
function hasRouteFiles(dirPath) {
|
|
178
|
+
try {
|
|
179
|
+
const entries = readdirSync(dirPath);
|
|
180
|
+
return entries.some(entry => {
|
|
181
|
+
// Look for .js, .ts, .jsx, .tsx files (not just directories)
|
|
182
|
+
return /\.(js|ts|jsx|tsx)$/.test(entry);
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Detect Vite + React
|
|
191
|
+
*/
|
|
192
|
+
function hasViteReact(projectDir, packageJson) {
|
|
193
|
+
const hasViteConfig = existsSync(resolve(projectDir, 'vite.config.js')) ||
|
|
194
|
+
existsSync(resolve(projectDir, 'vite.config.ts')) ||
|
|
195
|
+
existsSync(resolve(projectDir, 'vite.config.mjs'));
|
|
196
|
+
|
|
197
|
+
if (!hasViteConfig) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for react dependency
|
|
202
|
+
if (packageJson?.dependencies?.react || packageJson?.devDependencies?.react) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Detect Create React App
|
|
211
|
+
*/
|
|
212
|
+
function hasCreateReactApp(packageJson) {
|
|
213
|
+
return !!(packageJson?.dependencies?.['react-scripts'] ||
|
|
214
|
+
packageJson?.devDependencies?.['react-scripts']);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Detect static HTML (no framework)
|
|
219
|
+
*/
|
|
220
|
+
function hasStaticHtml(projectDir) {
|
|
221
|
+
return existsSync(resolve(projectDir, 'index.html'));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Detect package manager
|
|
226
|
+
*/
|
|
227
|
+
function detectPackageManager(projectDir) {
|
|
228
|
+
// Check for pnpm-lock.yaml
|
|
229
|
+
if (existsSync(resolve(projectDir, 'pnpm-lock.yaml'))) {
|
|
230
|
+
return 'pnpm';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check for yarn.lock
|
|
234
|
+
if (existsSync(resolve(projectDir, 'yarn.lock'))) {
|
|
235
|
+
return 'yarn';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for package-lock.json (npm)
|
|
239
|
+
if (existsSync(resolve(projectDir, 'package-lock.json'))) {
|
|
240
|
+
return 'npm';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Default to npm if none found but package.json exists
|
|
244
|
+
if (existsSync(resolve(projectDir, 'package.json'))) {
|
|
245
|
+
return 'npm';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return 'unknown';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get human-readable framework name
|
|
253
|
+
*/
|
|
254
|
+
export function getFrameworkDisplayName(framework, router) {
|
|
255
|
+
if (framework === 'nextjs') {
|
|
256
|
+
const routerType = router === 'app' ? 'app router' : router === 'pages' ? 'pages router' : 'unknown router';
|
|
257
|
+
return `Next.js (${routerType})`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (framework === 'react-vite') {
|
|
261
|
+
return 'Vite + React';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (framework === 'react-cra') {
|
|
265
|
+
return 'Create React App';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (framework === 'static-html') {
|
|
269
|
+
return 'Static HTML';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return 'Unknown';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extract probable port from dev script
|
|
277
|
+
*/
|
|
278
|
+
export function extractPortFromScript(script) {
|
|
279
|
+
if (!script) return null;
|
|
280
|
+
|
|
281
|
+
// Common patterns:
|
|
282
|
+
// - --port 3000
|
|
283
|
+
// - -p 3000
|
|
284
|
+
// - PORT=3000
|
|
285
|
+
|
|
286
|
+
const portMatch = script.match(/(?:--port|-p)\s+(\d+)/);
|
|
287
|
+
if (portMatch) {
|
|
288
|
+
return parseInt(portMatch[1], 10);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const portEnvMatch = script.match(/PORT=(\d+)/);
|
|
292
|
+
if (portEnvMatch) {
|
|
293
|
+
return parseInt(portEnvMatch[1], 10);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { atomicWriteJson } from './atomic-write.js';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Write project profile artifact
|
|
6
|
+
*/
|
|
7
|
+
export function writeProjectJson(runPaths, projectProfile) {
|
|
8
|
+
const projectJsonPath = resolve(runPaths.baseDir, 'project.json');
|
|
9
|
+
|
|
10
|
+
const projectJson = {
|
|
11
|
+
framework: projectProfile.framework,
|
|
12
|
+
router: projectProfile.router,
|
|
13
|
+
sourceRoot: projectProfile.sourceRoot,
|
|
14
|
+
packageManager: projectProfile.packageManager,
|
|
15
|
+
scripts: {
|
|
16
|
+
dev: projectProfile.scripts.dev,
|
|
17
|
+
build: projectProfile.scripts.build,
|
|
18
|
+
start: projectProfile.scripts.start,
|
|
19
|
+
},
|
|
20
|
+
detectedAt: projectProfile.detectedAt,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
atomicWriteJson(projectJsonPath, projectJson);
|
|
24
|
+
|
|
25
|
+
return projectJsonPath;
|
|
26
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction utilities (Phase 8.2)
|
|
3
|
+
* Deterministic redaction for headers, URLs, bodies, console messages.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const REDACTED = '***REDACTED***';
|
|
7
|
+
const SENSITIVE_HEADERS = [
|
|
8
|
+
'authorization',
|
|
9
|
+
'cookie',
|
|
10
|
+
'set-cookie',
|
|
11
|
+
'x-api-key',
|
|
12
|
+
'proxy-authorization',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function ensureCounters(counters) {
|
|
16
|
+
if (!counters) return { headersRedacted: 0, tokensRedacted: 0 };
|
|
17
|
+
if (typeof counters.headersRedacted !== 'number') counters.headersRedacted = 0;
|
|
18
|
+
if (typeof counters.tokensRedacted !== 'number') counters.tokensRedacted = 0;
|
|
19
|
+
return counters;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function redactHeaders(headers = {}, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
|
|
23
|
+
const c = ensureCounters(counters);
|
|
24
|
+
const sanitized = {};
|
|
25
|
+
Object.entries(headers || {}).forEach(([key, value]) => {
|
|
26
|
+
const lower = key.toLowerCase();
|
|
27
|
+
if (SENSITIVE_HEADERS.includes(lower)) {
|
|
28
|
+
sanitized[key] = REDACTED;
|
|
29
|
+
c.headersRedacted += 1;
|
|
30
|
+
} else {
|
|
31
|
+
sanitized[key] = value;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return sanitized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function redactUrl(url, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
|
|
38
|
+
if (url == null) return '';
|
|
39
|
+
return redactTokensInText(url, counters);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function redactBody(body, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
|
|
43
|
+
if (body == null) return body;
|
|
44
|
+
|
|
45
|
+
const c = ensureCounters(counters);
|
|
46
|
+
|
|
47
|
+
if (typeof body === 'string') {
|
|
48
|
+
return redactTokensInText(body, counters);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof body === 'object' && !Array.isArray(body)) {
|
|
52
|
+
// Handle plain objects by redacting sensitive property names
|
|
53
|
+
const redacted = {};
|
|
54
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
55
|
+
const keyLower = key.toLowerCase();
|
|
56
|
+
if (keyLower === 'token' || keyLower === 'api_key' || keyLower === 'access_token' ||
|
|
57
|
+
keyLower === 'password' || keyLower === 'secret') {
|
|
58
|
+
// Redact sensitive property values
|
|
59
|
+
c.tokensRedacted += 1;
|
|
60
|
+
redacted[key] = REDACTED;
|
|
61
|
+
} else if (typeof value === 'string') {
|
|
62
|
+
// Redact token patterns within string values
|
|
63
|
+
redacted[key] = redactTokensInText(value, counters);
|
|
64
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
65
|
+
// Recursively redact nested objects/arrays
|
|
66
|
+
redacted[key] = redactBody(value, counters);
|
|
67
|
+
} else {
|
|
68
|
+
redacted[key] = value;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return redacted;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(body)) {
|
|
75
|
+
return body.map(item => redactBody(item, counters));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const str = JSON.stringify(body);
|
|
80
|
+
const redacted = redactTokensInText(str, counters);
|
|
81
|
+
return JSON.parse(redacted);
|
|
82
|
+
} catch {
|
|
83
|
+
return REDACTED;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function redactConsole(text = '', counters = { headersRedacted: 0, tokensRedacted: 0 }) {
|
|
88
|
+
return redactTokensInText(text, counters);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function redactTokensInText(text, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
|
|
92
|
+
const c = ensureCounters(counters);
|
|
93
|
+
if (text == null) return '';
|
|
94
|
+
if (typeof text !== 'string') return String(text);
|
|
95
|
+
|
|
96
|
+
let output = text;
|
|
97
|
+
|
|
98
|
+
// Query/string parameters FIRST (most specific - only api_key, access_token)
|
|
99
|
+
// Use word boundary \b to avoid matching within words like "token" inside "example.com"
|
|
100
|
+
output = output.replace(/\b(api_key|access_token)=([^&\s]+)/gi, (match, key, value) => {
|
|
101
|
+
// Skip if value is itself just REDACTED (avoid double-redacting)
|
|
102
|
+
if (value === REDACTED) return match;
|
|
103
|
+
c.tokensRedacted += 1;
|
|
104
|
+
return `${key}=${REDACTED}`;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Bearer tokens
|
|
108
|
+
output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (_match, _token) => {
|
|
109
|
+
c.tokensRedacted += 1;
|
|
110
|
+
return `Bearer ${REDACTED}`;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// JWT-like strings (three base64url-ish segments)
|
|
114
|
+
// More specific: require uppercase or numbers, not just domain patterns like "api.example.com"
|
|
115
|
+
output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (_match) => {
|
|
116
|
+
c.tokensRedacted += 1;
|
|
117
|
+
return REDACTED;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getRedactionCounters(counters) {
|
|
124
|
+
const c = ensureCounters(counters);
|
|
125
|
+
return { headersRedacted: c.headersRedacted, tokensRedacted: c.tokensRedacted };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const REDACTION_PLACEHOLDER = REDACTED;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a run ID in format: <ISO_TIMESTAMP_UTC>_<6-8 char short hash>
|
|
5
|
+
* Example: 2026-01-11T00-59-12Z_4f2a9c
|
|
6
|
+
*/
|
|
7
|
+
export function generateRunId() {
|
|
8
|
+
// Create ISO timestamp with colons replaced by dashes for filesystem compatibility
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const isoString = now.toISOString();
|
|
11
|
+
// Format: 2026-01-11T00:59:12.123Z -> 2026-01-11T00-59-12Z
|
|
12
|
+
const timestamp = isoString.replace(/:/g, '-').replace(/\.\d+Z/, 'Z');
|
|
13
|
+
|
|
14
|
+
// Generate a short hash (6-8 chars)
|
|
15
|
+
const hash = crypto
|
|
16
|
+
.randomBytes(4)
|
|
17
|
+
.toString('hex')
|
|
18
|
+
.substring(0, 6);
|
|
19
|
+
|
|
20
|
+
return `${timestamp}_${hash}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate a run ID format
|
|
25
|
+
*/
|
|
26
|
+
export function isValidRunId(runId) {
|
|
27
|
+
// Pattern: YYYY-MM-DDTHH-MM-SSZ_hexchars
|
|
28
|
+
const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z_[a-f0-9]{6,8}$/;
|
|
29
|
+
return pattern.test(runId);
|
|
30
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Budget Model
|
|
3
|
+
* Computes timeouts based on project size, execution mode, and framework
|
|
4
|
+
* Ensures deterministic, bounded execution times
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} RuntimeBudgetOptions
|
|
9
|
+
* @property {number} [expectationsCount=0] - Number of expectations to process
|
|
10
|
+
* @property {string} [mode='default'] - Execution mode: 'default', 'run', 'ci'
|
|
11
|
+
* @property {string} [framework='unknown'] - Detected framework (optional)
|
|
12
|
+
* @property {number|null} [fileCount=null] - Number of files scanned (optional, fallback to expectationsCount)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute runtime budgets for a VERAX run
|
|
17
|
+
* @param {RuntimeBudgetOptions} [options={}] - Budget computation options
|
|
18
|
+
* @returns {Object} Budget object with phase timeouts
|
|
19
|
+
*/
|
|
20
|
+
export function computeRuntimeBudget(options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
expectationsCount = 0,
|
|
23
|
+
mode = 'default',
|
|
24
|
+
framework = 'unknown',
|
|
25
|
+
fileCount = null,
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
// TEST MODE OVERRIDE: Fixed deterministic budgets for integration tests
|
|
29
|
+
if (process.env.VERAX_TEST_MODE === '1') {
|
|
30
|
+
return {
|
|
31
|
+
totalMaxMs: 30000, // Hard cap per run
|
|
32
|
+
learnMaxMs: 5000, // Keep learn bounded
|
|
33
|
+
observeMaxMs: 20000, // Deterministic observe budget
|
|
34
|
+
detectMaxMs: 5000, // Bounded detect
|
|
35
|
+
perExpectationMaxMs: 5000, // Deterministic per-expectation guard
|
|
36
|
+
mode: 'test',
|
|
37
|
+
framework,
|
|
38
|
+
expectationsCount,
|
|
39
|
+
projectSize: fileCount !== null ? fileCount : expectationsCount,
|
|
40
|
+
frameworkMultiplier: 1.0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Use file count if available, otherwise use expectations count as proxy
|
|
45
|
+
const projectSize = fileCount !== null ? fileCount : expectationsCount;
|
|
46
|
+
|
|
47
|
+
// Base timeouts (milliseconds)
|
|
48
|
+
// Small project: < 10 expectations/files
|
|
49
|
+
// Medium project: 10-50 expectations/files
|
|
50
|
+
// Large project: > 50 expectations/files
|
|
51
|
+
|
|
52
|
+
// Learn phase: file scanning and AST parsing
|
|
53
|
+
const learnBaseMs = mode === 'ci' ? 30000 : 60000; // CI: 30s, default: 60s
|
|
54
|
+
const learnPerFileMs = 50; // 50ms per file
|
|
55
|
+
const learnMaxMs = mode === 'ci' ? 120000 : 300000; // CI: 2min, default: 5min
|
|
56
|
+
|
|
57
|
+
// Observe phase: browser automation
|
|
58
|
+
const observeBaseMs = mode === 'ci' ? 60000 : 120000; // CI: 1min, default: 2min
|
|
59
|
+
const observePerExpectationMs = mode === 'ci' ? 2000 : 5000; // CI: 2s, default: 5s per expectation
|
|
60
|
+
const observeMaxMs = mode === 'ci' ? 600000 : 1800000; // CI: 10min, default: 30min
|
|
61
|
+
|
|
62
|
+
// Detect phase: analysis and comparison
|
|
63
|
+
const detectBaseMs = mode === 'ci' ? 15000 : 30000; // CI: 15s, default: 30s
|
|
64
|
+
const detectPerExpectationMs = 100; // 100ms per expectation
|
|
65
|
+
const detectMaxMs = mode === 'ci' ? 120000 : 300000; // CI: 2min, default: 5min
|
|
66
|
+
|
|
67
|
+
// Per-expectation timeout during observe phase
|
|
68
|
+
const perExpectationBaseMs = mode === 'ci' ? 10000 : 30000; // CI: 10s, default: 30s
|
|
69
|
+
const perExpectationMaxMs = 120000; // 2min max per expectation
|
|
70
|
+
|
|
71
|
+
// Framework weighting (some frameworks may need more time)
|
|
72
|
+
let frameworkMultiplier = 1.0;
|
|
73
|
+
if (framework === 'nextjs' || framework === 'remix') {
|
|
74
|
+
frameworkMultiplier = 1.2; // SSR frameworks may need slightly more time
|
|
75
|
+
} else if (framework === 'react' || framework === 'vue') {
|
|
76
|
+
frameworkMultiplier = 1.1; // SPA frameworks
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Compute phase budgets
|
|
80
|
+
const computedLearnMaxMs = Math.min(
|
|
81
|
+
learnBaseMs + (projectSize * learnPerFileMs * frameworkMultiplier),
|
|
82
|
+
learnMaxMs
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const computedObserveMaxMs = Math.min(
|
|
86
|
+
observeBaseMs + (expectationsCount * observePerExpectationMs * frameworkMultiplier),
|
|
87
|
+
observeMaxMs
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const computedDetectMaxMs = Math.min(
|
|
91
|
+
detectBaseMs + (expectationsCount * detectPerExpectationMs * frameworkMultiplier),
|
|
92
|
+
detectMaxMs
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const computedPerExpectationMaxMs = Math.min(
|
|
96
|
+
perExpectationBaseMs * frameworkMultiplier,
|
|
97
|
+
perExpectationMaxMs
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Global watchdog timeout (must be >= sum of all phases + buffer)
|
|
101
|
+
// Add 30s buffer for finalization
|
|
102
|
+
const totalMaxMs = Math.max(
|
|
103
|
+
computedLearnMaxMs + computedObserveMaxMs + computedDetectMaxMs + 30000,
|
|
104
|
+
mode === 'ci' ? 900000 : 2400000 // CI: 15min minimum, default: 40min minimum
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Cap global timeout
|
|
108
|
+
const totalMaxMsCap = mode === 'ci' ? 1800000 : 3600000; // CI: 30min, default: 60min
|
|
109
|
+
const finalTotalMaxMs = Math.min(totalMaxMs, totalMaxMsCap);
|
|
110
|
+
|
|
111
|
+
// Ensure minimums are met
|
|
112
|
+
const finalLearnMaxMs = Math.max(computedLearnMaxMs, 10000); // At least 10s
|
|
113
|
+
const finalObserveMaxMs = Math.max(computedObserveMaxMs, 30000); // At least 30s
|
|
114
|
+
const finalDetectMaxMs = Math.max(computedDetectMaxMs, 5000); // At least 5s
|
|
115
|
+
const finalPerExpectationMaxMs = Math.max(computedPerExpectationMaxMs, 5000); // At least 5s
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
totalMaxMs: finalTotalMaxMs,
|
|
119
|
+
learnMaxMs: finalLearnMaxMs,
|
|
120
|
+
observeMaxMs: finalObserveMaxMs,
|
|
121
|
+
detectMaxMs: finalDetectMaxMs,
|
|
122
|
+
perExpectationMaxMs: finalPerExpectationMaxMs,
|
|
123
|
+
mode,
|
|
124
|
+
framework,
|
|
125
|
+
expectationsCount,
|
|
126
|
+
projectSize,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a timeout wrapper that rejects after specified milliseconds
|
|
132
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
133
|
+
* @param {Promise} promise - Promise to wrap
|
|
134
|
+
* @param {string} phase - Phase name for error messages
|
|
135
|
+
* @returns {Promise} Promise that rejects on timeout
|
|
136
|
+
*/
|
|
137
|
+
export function withTimeout(timeoutMs, promise, phase = 'unknown') {
|
|
138
|
+
return Promise.race([
|
|
139
|
+
promise,
|
|
140
|
+
new Promise((_, reject) => {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
reject(new Error(`Phase timeout: ${phase} exceeded ${timeoutMs}ms`));
|
|
143
|
+
}, timeoutMs);
|
|
144
|
+
}),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { atomicWriteJson } from './atomic-write.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write summary.json with deterministic digest
|
|
5
|
+
* The digest provides stable counts that should be identical across runs
|
|
6
|
+
* on the same input (assuming site behavior is stable)
|
|
7
|
+
*/
|
|
8
|
+
export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
|
|
9
|
+
const payload = {
|
|
10
|
+
runId: summaryData.runId,
|
|
11
|
+
status: summaryData.status,
|
|
12
|
+
startedAt: summaryData.startedAt,
|
|
13
|
+
completedAt: summaryData.completedAt,
|
|
14
|
+
command: summaryData.command,
|
|
15
|
+
url: summaryData.url,
|
|
16
|
+
notes: summaryData.notes,
|
|
17
|
+
metrics: summaryData.metrics || {
|
|
18
|
+
learnMs: stats.learnMs || 0,
|
|
19
|
+
observeMs: stats.observeMs || 0,
|
|
20
|
+
detectMs: stats.detectMs || 0,
|
|
21
|
+
totalMs: stats.totalMs || 0,
|
|
22
|
+
},
|
|
23
|
+
findingsCounts: summaryData.findingsCounts || {
|
|
24
|
+
HIGH: stats.HIGH || 0,
|
|
25
|
+
MEDIUM: stats.MEDIUM || 0,
|
|
26
|
+
LOW: stats.LOW || 0,
|
|
27
|
+
UNKNOWN: stats.UNKNOWN || 0,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Stable digest that should be identical across repeated runs on same input
|
|
31
|
+
digest: {
|
|
32
|
+
expectationsTotal: stats.expectationsTotal || 0,
|
|
33
|
+
attempted: stats.attempted || 0,
|
|
34
|
+
observed: stats.observed || 0,
|
|
35
|
+
silentFailures: stats.silentFailures || 0,
|
|
36
|
+
coverageGaps: stats.coverageGaps || 0,
|
|
37
|
+
unproven: stats.unproven || 0,
|
|
38
|
+
informational: stats.informational || 0,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
atomicWriteJson(summaryPath, payload);
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global type declarations for VERAX
|
|
3
|
+
* These extend built-in types to support runtime-injected properties
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Extend Window interface for browser-injected properties
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
__veraxNavTracking?: any;
|
|
10
|
+
next?: any;
|
|
11
|
+
__REDUX_STORE__?: any;
|
|
12
|
+
store?: any;
|
|
13
|
+
__REDUX_DEVTOOLS_EXTENSION__?: any;
|
|
14
|
+
__REACT_DEVTOOLS_GLOBAL_HOOK__?: any;
|
|
15
|
+
__VERAX_STATE_SENSOR__?: any;
|
|
16
|
+
__unhandledRejections?: any[];
|
|
17
|
+
__ZUSTAND_STORE__?: any;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Playwright Page type (imported from playwright)
|
|
22
|
+
import type { Page as PlaywrightPage } from 'playwright';
|
|
23
|
+
|
|
24
|
+
// Re-export for use in JS files
|
|
25
|
+
export type Page = PlaywrightPage;
|
|
26
|
+
|
|
27
|
+
export {};
|
|
28
|
+
|