@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
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracts PROVEN network action contracts from JSX/React source files.
|
|
5
5
|
* Uses AST analysis to find onClick/onSubmit handlers that call fetch/axios
|
|
6
|
-
* with static URL literals.
|
|
6
|
+
* with static URL literals and template literals.
|
|
7
7
|
*
|
|
8
8
|
* NO HEURISTICS. Only static, deterministic analysis.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { parse } from '@babel/parser';
|
|
11
|
+
import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';import { parse } from '@babel/parser';
|
|
12
12
|
import traverse from '@babel/traverse';
|
|
13
13
|
import { readFileSync } from 'fs';
|
|
14
|
-
import {
|
|
14
|
+
import { relative, sep } from 'path';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Extract action contracts from a source file.
|
|
@@ -21,113 +21,234 @@ import { resolve, relative, sep } from 'path';
|
|
|
21
21
|
* @returns {Array<Object>} - Array of contract objects
|
|
22
22
|
*/
|
|
23
23
|
export function extractActionContracts(filePath, workspaceRoot) {
|
|
24
|
-
const contracts = [];
|
|
25
|
-
|
|
26
24
|
try {
|
|
27
25
|
const code = readFileSync(filePath, 'utf-8');
|
|
28
|
-
|
|
29
|
-
sourceType: 'module',
|
|
30
|
-
plugins: ['jsx', 'typescript'],
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Track function declarations and arrow function assignments
|
|
34
|
-
const functionBodies = new Map(); // name -> AST node
|
|
35
|
-
|
|
36
|
-
traverse.default(ast, {
|
|
37
|
-
// Track function declarations
|
|
38
|
-
FunctionDeclaration(path) {
|
|
39
|
-
if (path.node.id && path.node.id.name) {
|
|
40
|
-
functionBodies.set(path.node.id.name, path.node.body);
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
// Track arrow function variable assignments
|
|
45
|
-
VariableDeclarator(path) {
|
|
46
|
-
if (
|
|
47
|
-
path.node.id.type === 'Identifier' &&
|
|
48
|
-
path.node.init &&
|
|
49
|
-
path.node.init.type === 'ArrowFunctionExpression'
|
|
50
|
-
) {
|
|
51
|
-
functionBodies.set(path.node.id.name, path.node.init.body);
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
// Find JSX elements with onClick or onSubmit
|
|
56
|
-
JSXAttribute(path) {
|
|
57
|
-
const attrName = path.node.name.name;
|
|
58
|
-
if (attrName !== 'onClick' && attrName !== 'onSubmit') {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const value = path.node.value;
|
|
63
|
-
if (!value) return;
|
|
64
|
-
|
|
65
|
-
// Only analyze inline arrow functions for now
|
|
66
|
-
// Case 1: Inline arrow function: onClick={() => fetch(...)}
|
|
67
|
-
if (
|
|
68
|
-
value.type === 'JSXExpressionContainer' &&
|
|
69
|
-
value.expression.type === 'ArrowFunctionExpression'
|
|
70
|
-
) {
|
|
71
|
-
const handlerBody = value.expression.body;
|
|
72
|
-
const networkCalls = findNetworkCallsInNode(handlerBody);
|
|
73
|
-
|
|
74
|
-
for (const call of networkCalls) {
|
|
75
|
-
const loc = path.node.loc;
|
|
76
|
-
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc);
|
|
77
|
-
|
|
78
|
-
contracts.push({
|
|
79
|
-
kind: 'NETWORK_ACTION',
|
|
80
|
-
method: call.method,
|
|
81
|
-
urlPath: call.url,
|
|
82
|
-
source: sourceRef,
|
|
83
|
-
elementType: path.parent.name.name, // button, form, etc.
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Case 2: Function reference - analyze the function declaration
|
|
88
|
-
else if (
|
|
89
|
-
value.type === 'JSXExpressionContainer' &&
|
|
90
|
-
value.expression.type === 'Identifier'
|
|
91
|
-
) {
|
|
92
|
-
const refName = value.expression.name;
|
|
93
|
-
const handlerBody = functionBodies.get(refName);
|
|
94
|
-
|
|
95
|
-
if (handlerBody) {
|
|
96
|
-
const networkCalls = findNetworkCallsInNode(handlerBody);
|
|
97
|
-
|
|
98
|
-
for (const call of networkCalls) {
|
|
99
|
-
const loc = path.node.loc;
|
|
100
|
-
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc);
|
|
101
|
-
|
|
102
|
-
contracts.push({
|
|
103
|
-
kind: 'NETWORK_ACTION',
|
|
104
|
-
method: call.method,
|
|
105
|
-
urlPath: call.url,
|
|
106
|
-
source: sourceRef,
|
|
107
|
-
elementType: path.parent.name.name,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
});
|
|
26
|
+
return extractActionContractsFromCode(filePath, workspaceRoot, code, 0);
|
|
114
27
|
} catch (err) {
|
|
115
|
-
// Parse errors are not fatal; just return empty contracts
|
|
116
28
|
console.warn(`Failed to parse ${filePath}: ${err.message}`);
|
|
29
|
+
return [];
|
|
117
30
|
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
|
|
34
|
+
const contracts = [];
|
|
35
|
+
|
|
36
|
+
// Track function declarations and arrow function assignments with location
|
|
37
|
+
const functionBodies = new Map(); // name -> { body, loc }
|
|
38
|
+
|
|
39
|
+
const ast = parse(code, {
|
|
40
|
+
sourceType: 'unambiguous',
|
|
41
|
+
plugins: ['jsx', 'typescript'],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
traverse.default(ast, {
|
|
45
|
+
FunctionDeclaration(path) {
|
|
46
|
+
if (path.node.id && path.node.id.name) {
|
|
47
|
+
functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
VariableDeclarator(path) {
|
|
52
|
+
if (
|
|
53
|
+
path.node.id.type === 'Identifier' &&
|
|
54
|
+
path.node.init &&
|
|
55
|
+
path.node.init.type === 'ArrowFunctionExpression'
|
|
56
|
+
) {
|
|
57
|
+
functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// JSX handlers (React)
|
|
62
|
+
JSXAttribute(path) {
|
|
63
|
+
const attrName = path.node.name.name;
|
|
64
|
+
if (attrName !== 'onClick' && attrName !== 'onSubmit') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isSubmitHandler = attrName === 'onSubmit';
|
|
69
|
+
|
|
70
|
+
const value = path.node.value;
|
|
71
|
+
if (!value) return;
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
value.type === 'JSXExpressionContainer' &&
|
|
75
|
+
value.expression.type === 'ArrowFunctionExpression'
|
|
76
|
+
) {
|
|
77
|
+
const handlerBody = value.expression.body;
|
|
78
|
+
const networkCalls = findNetworkCallsInNode(handlerBody);
|
|
79
|
+
|
|
80
|
+
for (const call of networkCalls) {
|
|
81
|
+
const loc = path.node.loc;
|
|
82
|
+
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
|
|
83
|
+
|
|
84
|
+
if (call.kind === 'VALIDATION_BLOCK' && isSubmitHandler) {
|
|
85
|
+
contracts.push({
|
|
86
|
+
kind: 'VALIDATION_BLOCK',
|
|
87
|
+
method: call.method,
|
|
88
|
+
urlPath: null,
|
|
89
|
+
source: sourceRef,
|
|
90
|
+
elementType: path.parent.name.name,
|
|
91
|
+
handlerRef: sourceRef,
|
|
92
|
+
selectorHint: null
|
|
93
|
+
});
|
|
94
|
+
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
95
|
+
contracts.push({
|
|
96
|
+
kind: call.kind || 'NETWORK_ACTION',
|
|
97
|
+
method: call.method,
|
|
98
|
+
urlPath: call.url,
|
|
99
|
+
source: sourceRef,
|
|
100
|
+
elementType: path.parent.name.name,
|
|
101
|
+
handlerRef: sourceRef
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (
|
|
106
|
+
value.type === 'JSXExpressionContainer' &&
|
|
107
|
+
value.expression.type === 'Identifier'
|
|
108
|
+
) {
|
|
109
|
+
const refName = value.expression.name;
|
|
110
|
+
const handlerRecord = functionBodies.get(refName);
|
|
111
|
+
|
|
112
|
+
if (handlerRecord) {
|
|
113
|
+
const networkCalls = findNetworkCallsInNode(handlerRecord.body);
|
|
114
|
+
|
|
115
|
+
for (const call of networkCalls) {
|
|
116
|
+
const loc = path.node.loc;
|
|
117
|
+
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
|
|
118
|
+
|
|
119
|
+
if (call.kind === 'VALIDATION_BLOCK' && isSubmitHandler) {
|
|
120
|
+
contracts.push({
|
|
121
|
+
kind: 'VALIDATION_BLOCK',
|
|
122
|
+
method: call.method,
|
|
123
|
+
urlPath: null,
|
|
124
|
+
source: sourceRef,
|
|
125
|
+
elementType: path.parent.name.name,
|
|
126
|
+
handlerRef: sourceRef,
|
|
127
|
+
selectorHint: null
|
|
128
|
+
});
|
|
129
|
+
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
130
|
+
contracts.push({
|
|
131
|
+
kind: call.kind || 'NETWORK_ACTION',
|
|
132
|
+
method: call.method,
|
|
133
|
+
urlPath: call.url,
|
|
134
|
+
source: sourceRef,
|
|
135
|
+
elementType: path.parent.name.name,
|
|
136
|
+
handlerRef: sourceRef
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// DOM addEventListener handlers (static HTML/JS)
|
|
145
|
+
CallExpression(path) {
|
|
146
|
+
const callee = path.node.callee;
|
|
147
|
+
if (
|
|
148
|
+
callee.type === 'MemberExpression' &&
|
|
149
|
+
callee.property.type === 'Identifier' &&
|
|
150
|
+
callee.property.name === 'addEventListener'
|
|
151
|
+
) {
|
|
152
|
+
const args = path.node.arguments || [];
|
|
153
|
+
if (args.length < 2) return;
|
|
154
|
+
|
|
155
|
+
const eventArg = args[0];
|
|
156
|
+
const handlerArg = args[1];
|
|
157
|
+
const eventType = eventArg && eventArg.type === 'StringLiteral' ? eventArg.value : 'event';
|
|
158
|
+
|
|
159
|
+
let handlerBody = null;
|
|
160
|
+
let handlerLoc = handlerArg?.loc || path.node.loc;
|
|
161
|
+
|
|
162
|
+
if (handlerArg && (handlerArg.type === 'ArrowFunctionExpression' || handlerArg.type === 'FunctionExpression')) {
|
|
163
|
+
handlerBody = handlerArg.body;
|
|
164
|
+
handlerLoc = handlerArg.loc || path.node.loc;
|
|
165
|
+
} else if (handlerArg && handlerArg.type === 'Identifier') {
|
|
166
|
+
const record = functionBodies.get(handlerArg.name);
|
|
167
|
+
if (record) {
|
|
168
|
+
handlerBody = record.body;
|
|
169
|
+
handlerLoc = record.loc || path.node.loc;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!handlerBody) return;
|
|
174
|
+
|
|
175
|
+
const networkCalls = findNetworkCallsInNode(handlerBody);
|
|
176
|
+
const isSubmitEvent = eventType === 'submit';
|
|
177
|
+
|
|
178
|
+
for (const call of networkCalls) {
|
|
179
|
+
const handlerRef = formatSourceRef(filePath, workspaceRoot, handlerLoc, lineOffset);
|
|
180
|
+
|
|
181
|
+
if (call.kind === 'VALIDATION_BLOCK' && isSubmitEvent) {
|
|
182
|
+
contracts.push({
|
|
183
|
+
kind: 'VALIDATION_BLOCK',
|
|
184
|
+
method: call.method,
|
|
185
|
+
urlPath: null,
|
|
186
|
+
source: handlerRef,
|
|
187
|
+
handlerRef: `${handlerRef}#${eventType}`,
|
|
188
|
+
elementType: 'dom',
|
|
189
|
+
selectorHint: null
|
|
190
|
+
});
|
|
191
|
+
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
192
|
+
contracts.push({
|
|
193
|
+
kind: call.kind || 'NETWORK_ACTION',
|
|
194
|
+
method: call.method,
|
|
195
|
+
urlPath: call.url,
|
|
196
|
+
source: handlerRef,
|
|
197
|
+
handlerRef: `${handlerRef}#${eventType}`,
|
|
198
|
+
elementType: 'dom'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
118
205
|
|
|
119
206
|
return contracts;
|
|
120
207
|
}
|
|
121
208
|
|
|
122
209
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
210
|
+
* Extract template literal pattern from node.
|
|
211
|
+
* Returns null if template has complex expressions.
|
|
212
|
+
*/
|
|
213
|
+
function extractTemplateLiteralPath(node) {
|
|
214
|
+
if (!node || node.type !== 'TemplateLiteral') {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Build template string
|
|
219
|
+
let templateStr = node.quasis[0]?.value?.cooked || '';
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < node.expressions.length; i++) {
|
|
222
|
+
const expr = node.expressions[i];
|
|
223
|
+
|
|
224
|
+
// Only support simple identifiers
|
|
225
|
+
if (expr.type === 'Identifier') {
|
|
226
|
+
templateStr += '${' + expr.name + '}';
|
|
227
|
+
} else {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (node.quasis[i + 1]) {
|
|
232
|
+
templateStr += node.quasis[i + 1].value.cooked || '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return templateStr;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find action calls (fetch, axios, router.push, navigate) in an AST node by recursively scanning.
|
|
241
|
+
* Only returns calls with static URL/path literals or template patterns.
|
|
125
242
|
*
|
|
126
243
|
* @param {Object} node - AST node to scan
|
|
127
|
-
* @returns {Array<Object>} - Array of {method, url}
|
|
244
|
+
* @returns {Array<Object>} - Array of {kind, method, url} where kind is 'NETWORK_ACTION' or 'NAVIGATION_ACTION'
|
|
128
245
|
*/
|
|
129
246
|
function findNetworkCallsInNode(node) {
|
|
130
247
|
const calls = [];
|
|
248
|
+
|
|
249
|
+
// Track if we found preventDefault or return false for validation block detection
|
|
250
|
+
let hasPreventDefault = false;
|
|
251
|
+
let hasReturnFalse = false;
|
|
131
252
|
|
|
132
253
|
// Recursive function to scan all nodes
|
|
133
254
|
function scan(n) {
|
|
@@ -156,7 +277,7 @@ function findNetworkCallsInNode(node) {
|
|
|
156
277
|
}
|
|
157
278
|
}
|
|
158
279
|
|
|
159
|
-
calls.push({ method, url: urlArg.value });
|
|
280
|
+
calls.push({ kind: 'NETWORK_ACTION', method, url: urlArg.value });
|
|
160
281
|
}
|
|
161
282
|
}
|
|
162
283
|
|
|
@@ -171,7 +292,7 @@ function findNetworkCallsInNode(node) {
|
|
|
171
292
|
const urlArg = n.arguments[0];
|
|
172
293
|
|
|
173
294
|
if (urlArg && urlArg.type === 'StringLiteral') {
|
|
174
|
-
calls.push({ method: methodName, url: urlArg.value });
|
|
295
|
+
calls.push({ kind: 'NETWORK_ACTION', method: methodName, url: urlArg.value });
|
|
175
296
|
}
|
|
176
297
|
}
|
|
177
298
|
|
|
@@ -190,16 +311,90 @@ function findNetworkCallsInNode(node) {
|
|
|
190
311
|
urlArg && urlArg.type === 'StringLiteral'
|
|
191
312
|
) {
|
|
192
313
|
calls.push({
|
|
314
|
+
kind: 'NETWORK_ACTION',
|
|
193
315
|
method: methodArg.value.toUpperCase(),
|
|
194
316
|
url: urlArg.value,
|
|
195
317
|
});
|
|
196
318
|
}
|
|
197
319
|
}
|
|
320
|
+
|
|
321
|
+
// Case 4: router.push(path), router.replace(path), history.push(path)
|
|
322
|
+
if (
|
|
323
|
+
callee.type === 'MemberExpression' &&
|
|
324
|
+
callee.object.type === 'Identifier' &&
|
|
325
|
+
(callee.object.name === 'router' || callee.object.name === 'history') &&
|
|
326
|
+
callee.property.type === 'Identifier' &&
|
|
327
|
+
['push', 'replace', 'navigate'].includes(callee.property.name)
|
|
328
|
+
) {
|
|
329
|
+
const pathArg = n.arguments[0];
|
|
330
|
+
if (pathArg && pathArg.type === 'StringLiteral') {
|
|
331
|
+
calls.push({
|
|
332
|
+
kind: 'NAVIGATION_ACTION',
|
|
333
|
+
method: `${callee.object.name}.${callee.property.name}`,
|
|
334
|
+
url: pathArg.value
|
|
335
|
+
});
|
|
336
|
+
} else if (pathArg && pathArg.type === 'TemplateLiteral') {
|
|
337
|
+
// Template literal: router.push(`/users/${id}`)
|
|
338
|
+
const templatePath = extractTemplateLiteralPath(pathArg);
|
|
339
|
+
if (templatePath && templatePath.startsWith('/')) {
|
|
340
|
+
// Normalize to example path
|
|
341
|
+
const normalized = normalizeTemplateLiteral(templatePath);
|
|
342
|
+
calls.push({
|
|
343
|
+
kind: 'NAVIGATION_ACTION',
|
|
344
|
+
method: `${callee.object.name}.${callee.property.name}`,
|
|
345
|
+
url: normalized ? normalized.examplePath : templatePath
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Case 5: navigate(path) - standalone function
|
|
352
|
+
if (callee.type === 'Identifier' && callee.name === 'navigate') {
|
|
353
|
+
const pathArg = n.arguments[0];
|
|
354
|
+
if (pathArg && pathArg.type === 'StringLiteral') {
|
|
355
|
+
calls.push({
|
|
356
|
+
kind: 'NAVIGATION_ACTION',
|
|
357
|
+
method: 'navigate',
|
|
358
|
+
url: pathArg.value
|
|
359
|
+
});
|
|
360
|
+
} else if (pathArg && pathArg.type === 'TemplateLiteral') {
|
|
361
|
+
// Template literal: navigate(`/users/${id}`)
|
|
362
|
+
const templatePath = extractTemplateLiteralPath(pathArg);
|
|
363
|
+
if (templatePath && templatePath.startsWith('/')) {
|
|
364
|
+
// Normalize to example path
|
|
365
|
+
const normalized = normalizeTemplateLiteral(templatePath);
|
|
366
|
+
calls.push({
|
|
367
|
+
kind: 'NAVIGATION_ACTION',
|
|
368
|
+
method: 'navigate',
|
|
369
|
+
url: normalized ? normalized.examplePath : templatePath
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Case 6: event.preventDefault() - validation block
|
|
376
|
+
if (
|
|
377
|
+
callee.type === 'MemberExpression' &&
|
|
378
|
+
callee.property.type === 'Identifier' &&
|
|
379
|
+
callee.property.name === 'preventDefault'
|
|
380
|
+
) {
|
|
381
|
+
hasPreventDefault = true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Check for return false statement
|
|
386
|
+
if (n.type === 'ReturnStatement' && n.argument) {
|
|
387
|
+
if (
|
|
388
|
+
(n.argument.type === 'BooleanLiteral' && n.argument.value === false) ||
|
|
389
|
+
(n.argument.type === 'Identifier' && n.argument.name === 'false')
|
|
390
|
+
) {
|
|
391
|
+
hasReturnFalse = true;
|
|
392
|
+
}
|
|
198
393
|
}
|
|
199
394
|
|
|
200
395
|
// Recursively scan all properties
|
|
201
396
|
for (const key in n) {
|
|
202
|
-
if (
|
|
397
|
+
if (Object.prototype.hasOwnProperty.call(n, key)) {
|
|
203
398
|
const value = n[key];
|
|
204
399
|
if (Array.isArray(value)) {
|
|
205
400
|
value.forEach(item => scan(item));
|
|
@@ -211,6 +406,16 @@ function findNetworkCallsInNode(node) {
|
|
|
211
406
|
}
|
|
212
407
|
|
|
213
408
|
scan(node);
|
|
409
|
+
|
|
410
|
+
// If validation block detected, add VALIDATION_BLOCK contract
|
|
411
|
+
if (hasPreventDefault || hasReturnFalse) {
|
|
412
|
+
calls.push({
|
|
413
|
+
kind: 'VALIDATION_BLOCK',
|
|
414
|
+
method: hasPreventDefault ? 'preventDefault' : 'return-false',
|
|
415
|
+
url: null
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
214
419
|
return calls;
|
|
215
420
|
}
|
|
216
421
|
|
|
@@ -223,14 +428,14 @@ function findNetworkCallsInNode(node) {
|
|
|
223
428
|
* @param {Object} loc - Location object from AST
|
|
224
429
|
* @returns {string} - Formatted source reference
|
|
225
430
|
*/
|
|
226
|
-
function formatSourceRef(filePath, workspaceRoot, loc) {
|
|
431
|
+
function formatSourceRef(filePath, workspaceRoot, loc, lineOffset = 0) {
|
|
227
432
|
let relPath = relative(workspaceRoot, filePath);
|
|
228
433
|
|
|
229
434
|
// Normalize to forward slashes
|
|
230
435
|
relPath = relPath.split(sep).join('/');
|
|
231
436
|
|
|
232
|
-
const line = loc
|
|
233
|
-
const col = loc
|
|
437
|
+
const line = (loc?.start?.line || 1) + lineOffset;
|
|
438
|
+
const col = loc?.start?.column || 0;
|
|
234
439
|
|
|
235
440
|
return `${relPath}:${line}:${col}`;
|
|
236
441
|
}
|
|
@@ -240,7 +445,7 @@ function formatSourceRef(filePath, workspaceRoot, loc) {
|
|
|
240
445
|
*
|
|
241
446
|
* @param {string} rootPath - Root directory to scan
|
|
242
447
|
* @param {string} workspaceRoot - Workspace root
|
|
243
|
-
* @returns {Array<Object
|
|
448
|
+
* @returns {Promise<Array<Object>>} - All contracts found
|
|
244
449
|
*/
|
|
245
450
|
export async function scanForContracts(rootPath, workspaceRoot) {
|
|
246
451
|
const contracts = [];
|
|
@@ -260,14 +465,43 @@ export async function scanForContracts(rootPath, workspaceRoot) {
|
|
|
260
465
|
|
|
261
466
|
if (stat.isDirectory()) {
|
|
262
467
|
// Skip node_modules, .git, etc.
|
|
263
|
-
|
|
468
|
+
const ignoredDirs = new Set([
|
|
469
|
+
'node_modules',
|
|
470
|
+
'.git',
|
|
471
|
+
'.verax',
|
|
472
|
+
'dist',
|
|
473
|
+
'build',
|
|
474
|
+
'out',
|
|
475
|
+
'artifacts',
|
|
476
|
+
'logs',
|
|
477
|
+
'temp-installs',
|
|
478
|
+
'tmp',
|
|
479
|
+
'temp'
|
|
480
|
+
]);
|
|
481
|
+
if (!entry.startsWith('.') && !ignoredDirs.has(entry)) {
|
|
264
482
|
walk(fullPath);
|
|
265
483
|
}
|
|
266
484
|
} else if (stat.isFile()) {
|
|
267
|
-
// Only process .js, .jsx, .ts, .tsx files
|
|
268
485
|
if (/\.(jsx?|tsx?)$/.test(entry)) {
|
|
269
486
|
const fileContracts = extractActionContracts(fullPath, workspaceRoot);
|
|
270
487
|
contracts.push(...fileContracts);
|
|
488
|
+
} else if (/\.html?$/.test(entry)) {
|
|
489
|
+
try {
|
|
490
|
+
const html = readFileSync(fullPath, 'utf-8');
|
|
491
|
+
const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
492
|
+
let match;
|
|
493
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
494
|
+
const tagOpen = html.slice(match.index, html.indexOf('>', match.index) + 1);
|
|
495
|
+
if (/\ssrc=/i.test(tagOpen)) continue; // skip external scripts
|
|
496
|
+
const before = html.slice(0, match.index);
|
|
497
|
+
const lineOffset = (before.match(/\n/g) || []).length;
|
|
498
|
+
const code = match[1];
|
|
499
|
+
const blockContracts = extractActionContractsFromCode(fullPath, workspaceRoot, code, lineOffset + 1);
|
|
500
|
+
contracts.push(...blockContracts);
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.warn(`Failed to parse HTML scripts in ${fullPath}: ${err.message}`);
|
|
504
|
+
}
|
|
271
505
|
}
|
|
272
506
|
}
|
|
273
507
|
}
|