@vibecheckai/cli 3.2.2 → 3.2.4
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/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,416 +1,416 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request Body Hashing v2
|
|
3
|
-
*
|
|
4
|
-
* Hashes request/response bodies for correlation and duplicate detection.
|
|
5
|
-
* Enables catching: duplicate mutations, no-op responses, optimistic UI mismatches.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
"use strict";
|
|
9
|
-
|
|
10
|
-
const crypto = require("crypto");
|
|
11
|
-
|
|
12
|
-
// =============================================================================
|
|
13
|
-
// CONSTANTS
|
|
14
|
-
// =============================================================================
|
|
15
|
-
|
|
16
|
-
const MAX_BODY_SIZE = 100 * 1024; // 100KB max for hashing
|
|
17
|
-
const REDACT_KEYS = [
|
|
18
|
-
"password",
|
|
19
|
-
"token",
|
|
20
|
-
"secret",
|
|
21
|
-
"apiKey",
|
|
22
|
-
"api_key",
|
|
23
|
-
"authorization",
|
|
24
|
-
"auth",
|
|
25
|
-
"credit_card",
|
|
26
|
-
"creditCard",
|
|
27
|
-
"ssn",
|
|
28
|
-
"social_security",
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
// =============================================================================
|
|
32
|
-
// BODY HASHING
|
|
33
|
-
// =============================================================================
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Hash request/response body with redaction
|
|
37
|
-
*/
|
|
38
|
-
function hashBody(body, options = {}) {
|
|
39
|
-
const { redact = true, maxSize = MAX_BODY_SIZE } = options;
|
|
40
|
-
|
|
41
|
-
if (!body) {
|
|
42
|
-
return { hash: null, isEmpty: true, size: 0 };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let content;
|
|
46
|
-
let contentType = "unknown";
|
|
47
|
-
|
|
48
|
-
// Handle different body types
|
|
49
|
-
if (typeof body === "string") {
|
|
50
|
-
content = body;
|
|
51
|
-
contentType = detectContentType(body);
|
|
52
|
-
} else if (Buffer.isBuffer(body)) {
|
|
53
|
-
content = body.toString("utf8");
|
|
54
|
-
contentType = "binary";
|
|
55
|
-
} else if (typeof body === "object") {
|
|
56
|
-
try {
|
|
57
|
-
content = JSON.stringify(body);
|
|
58
|
-
contentType = "json";
|
|
59
|
-
} catch {
|
|
60
|
-
content = String(body);
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
content = String(body);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Size check
|
|
67
|
-
const size = content.length;
|
|
68
|
-
if (size > maxSize) {
|
|
69
|
-
return {
|
|
70
|
-
hash: null,
|
|
71
|
-
isEmpty: false,
|
|
72
|
-
size,
|
|
73
|
-
truncated: true,
|
|
74
|
-
contentType,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Redact sensitive data before hashing
|
|
79
|
-
let hashContent = content;
|
|
80
|
-
if (redact && contentType === "json") {
|
|
81
|
-
try {
|
|
82
|
-
const parsed = JSON.parse(content);
|
|
83
|
-
hashContent = JSON.stringify(redactSensitiveData(parsed));
|
|
84
|
-
} catch {
|
|
85
|
-
// Keep original if can't parse
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Compute hash
|
|
90
|
-
const hash = crypto.createHash("sha256").update(hashContent).digest("hex").slice(0, 16);
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
hash,
|
|
94
|
-
isEmpty: size === 0 || content === "{}" || content === "null" || content === "[]",
|
|
95
|
-
size,
|
|
96
|
-
contentType,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Detect content type from string
|
|
102
|
-
*/
|
|
103
|
-
function detectContentType(content) {
|
|
104
|
-
if (!content) return "empty";
|
|
105
|
-
|
|
106
|
-
const trimmed = content.trim();
|
|
107
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
108
|
-
if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) return "xml";
|
|
109
|
-
if (trimmed.includes("=") && trimmed.includes("&")) return "form";
|
|
110
|
-
|
|
111
|
-
return "text";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Redact sensitive data from object
|
|
116
|
-
*/
|
|
117
|
-
function redactSensitiveData(obj, depth = 0) {
|
|
118
|
-
if (depth > 10) return obj; // Prevent infinite recursion
|
|
119
|
-
|
|
120
|
-
if (Array.isArray(obj)) {
|
|
121
|
-
return obj.map(item => redactSensitiveData(item, depth + 1));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (typeof obj === "object" && obj !== null) {
|
|
125
|
-
const redacted = {};
|
|
126
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
127
|
-
const keyLower = key.toLowerCase();
|
|
128
|
-
const isSensitive = REDACT_KEYS.some(k => keyLower.includes(k.toLowerCase()));
|
|
129
|
-
|
|
130
|
-
if (isSensitive) {
|
|
131
|
-
redacted[key] = "[REDACTED]";
|
|
132
|
-
} else if (typeof value === "object" && value !== null) {
|
|
133
|
-
redacted[key] = redactSensitiveData(value, depth + 1);
|
|
134
|
-
} else {
|
|
135
|
-
redacted[key] = value;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return redacted;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return obj;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// =============================================================================
|
|
145
|
-
// REQUEST RECORD ENHANCEMENT
|
|
146
|
-
// =============================================================================
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Enhance request record with body hashes
|
|
150
|
-
*/
|
|
151
|
-
function enhanceRequestWithHashes(request, options = {}) {
|
|
152
|
-
const enhanced = { ...request };
|
|
153
|
-
|
|
154
|
-
// Hash request body
|
|
155
|
-
if (request.postData || request.body) {
|
|
156
|
-
const bodyHash = hashBody(request.postData || request.body, options);
|
|
157
|
-
enhanced.requestBodyHash = bodyHash.hash;
|
|
158
|
-
enhanced.requestBodyEmpty = bodyHash.isEmpty;
|
|
159
|
-
enhanced.requestBodySize = bodyHash.size;
|
|
160
|
-
enhanced.requestContentType = bodyHash.contentType;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Hash response body if available
|
|
164
|
-
if (request.responseBody) {
|
|
165
|
-
const respHash = hashBody(request.responseBody, options);
|
|
166
|
-
enhanced.responseBodyHash = respHash.hash;
|
|
167
|
-
enhanced.responseBodyEmpty = respHash.isEmpty;
|
|
168
|
-
enhanced.responseBodySize = respHash.size;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return enhanced;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// =============================================================================
|
|
175
|
-
// DUPLICATE DETECTION
|
|
176
|
-
// =============================================================================
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Find duplicate mutation requests
|
|
180
|
-
*/
|
|
181
|
-
function findDuplicateMutations(requests) {
|
|
182
|
-
const mutations = requests.filter(r =>
|
|
183
|
-
["POST", "PUT", "PATCH", "DELETE"].includes(r.method?.toUpperCase())
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Group by method + path + requestBodyHash
|
|
187
|
-
const groups = new Map();
|
|
188
|
-
|
|
189
|
-
for (const req of mutations) {
|
|
190
|
-
if (!req.requestBodyHash) continue;
|
|
191
|
-
|
|
192
|
-
const key = `${req.method}:${req.canonicalPath || req.url}:${req.requestBodyHash}`;
|
|
193
|
-
if (!groups.has(key)) {
|
|
194
|
-
groups.set(key, []);
|
|
195
|
-
}
|
|
196
|
-
groups.set(key, [...groups.get(key), req]);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Find groups with duplicates
|
|
200
|
-
const duplicates = [];
|
|
201
|
-
for (const [key, reqs] of groups) {
|
|
202
|
-
if (reqs.length > 1) {
|
|
203
|
-
duplicates.push({
|
|
204
|
-
key,
|
|
205
|
-
count: reqs.length,
|
|
206
|
-
requests: reqs,
|
|
207
|
-
firstTimestamp: reqs[0].timestamp,
|
|
208
|
-
lastTimestamp: reqs[reqs.length - 1].timestamp,
|
|
209
|
-
timeDeltaMs: reqs.length > 1
|
|
210
|
-
? new Date(reqs[reqs.length - 1].timestamp) - new Date(reqs[0].timestamp)
|
|
211
|
-
: 0,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return duplicates;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Find no-op mutations (success response with empty body)
|
|
221
|
-
*/
|
|
222
|
-
function findNoOpMutations(requests) {
|
|
223
|
-
const noOps = [];
|
|
224
|
-
|
|
225
|
-
for (const req of requests) {
|
|
226
|
-
if (!["POST", "PUT", "PATCH"].includes(req.method?.toUpperCase())) continue;
|
|
227
|
-
|
|
228
|
-
const isSuccess = req.status >= 200 && req.status < 300;
|
|
229
|
-
const isEmptyResponse = req.responseBodyEmpty;
|
|
230
|
-
|
|
231
|
-
if (isSuccess && isEmptyResponse) {
|
|
232
|
-
noOps.push({
|
|
233
|
-
request: req,
|
|
234
|
-
reason: "Mutation succeeded but returned empty response",
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return noOps;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// =============================================================================
|
|
243
|
-
// UI MISMATCH DETECTION
|
|
244
|
-
// =============================================================================
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Detect optimistic UI that didn't match server response
|
|
248
|
-
* Requires action records with before/after UI state
|
|
249
|
-
*/
|
|
250
|
-
function detectOptimisticMismatch(action, request) {
|
|
251
|
-
const mismatches = [];
|
|
252
|
-
|
|
253
|
-
// Check if action had optimistic update
|
|
254
|
-
if (!action.optimisticUpdate) return mismatches;
|
|
255
|
-
|
|
256
|
-
// Check if request succeeded
|
|
257
|
-
if (!request || request.status < 200 || request.status >= 300) {
|
|
258
|
-
mismatches.push({
|
|
259
|
-
type: "optimistic_request_failed",
|
|
260
|
-
action,
|
|
261
|
-
request,
|
|
262
|
-
reason: "Optimistic update shown but request failed",
|
|
263
|
-
});
|
|
264
|
-
return mismatches;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Check if response body matches what UI expected
|
|
268
|
-
if (action.expectedResponseHash && request.responseBodyHash) {
|
|
269
|
-
if (action.expectedResponseHash !== request.responseBodyHash) {
|
|
270
|
-
mismatches.push({
|
|
271
|
-
type: "optimistic_response_mismatch",
|
|
272
|
-
action,
|
|
273
|
-
request,
|
|
274
|
-
expected: action.expectedResponseHash,
|
|
275
|
-
actual: request.responseBodyHash,
|
|
276
|
-
reason: "Optimistic UI didn't match server response",
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return mismatches;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Analyze all actions and requests for UI mismatches
|
|
286
|
-
*/
|
|
287
|
-
function analyzeUIMismatches(actions, requests) {
|
|
288
|
-
const mismatches = [];
|
|
289
|
-
const duplicates = findDuplicateMutations(requests);
|
|
290
|
-
const noOps = findNoOpMutations(requests);
|
|
291
|
-
|
|
292
|
-
// Duplicate mutations
|
|
293
|
-
for (const dup of duplicates) {
|
|
294
|
-
mismatches.push({
|
|
295
|
-
type: "duplicate_mutation",
|
|
296
|
-
severity: dup.timeDeltaMs < 1000 ? "WARN" : "INFO",
|
|
297
|
-
...dup,
|
|
298
|
-
reason: `Same mutation submitted ${dup.count} times`,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// No-op mutations
|
|
303
|
-
for (const noOp of noOps) {
|
|
304
|
-
mismatches.push({
|
|
305
|
-
type: "noop_mutation",
|
|
306
|
-
severity: "WARN",
|
|
307
|
-
...noOp,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Optimistic mismatches
|
|
312
|
-
for (const action of actions) {
|
|
313
|
-
// Find associated request
|
|
314
|
-
const request = requests.find(r =>
|
|
315
|
-
r.actionId === action.id ||
|
|
316
|
-
(r.timestamp >= action.startTime && r.timestamp <= action.endTime)
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const actionMismatches = detectOptimisticMismatch(action, request);
|
|
320
|
-
for (const m of actionMismatches) {
|
|
321
|
-
mismatches.push({
|
|
322
|
-
...m,
|
|
323
|
-
severity: "BLOCK",
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return mismatches;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// =============================================================================
|
|
332
|
-
// PLAYWRIGHT INTEGRATION
|
|
333
|
-
// =============================================================================
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Playwright script to capture request/response bodies
|
|
337
|
-
* Inject this to capture bodies during crawling
|
|
338
|
-
*/
|
|
339
|
-
const PLAYWRIGHT_BODY_CAPTURE_SCRIPT = `
|
|
340
|
-
// Vibecheck Request Body Capture
|
|
341
|
-
window.__vibecheck_request_bodies = window.__vibecheck_request_bodies || new Map();
|
|
342
|
-
|
|
343
|
-
// Intercept fetch
|
|
344
|
-
const originalFetch = window.fetch;
|
|
345
|
-
window.fetch = async function(...args) {
|
|
346
|
-
const [input, init] = args;
|
|
347
|
-
const url = typeof input === 'string' ? input : input.url;
|
|
348
|
-
const method = init?.method || 'GET';
|
|
349
|
-
|
|
350
|
-
// Capture request body
|
|
351
|
-
if (init?.body) {
|
|
352
|
-
const id = crypto.randomUUID();
|
|
353
|
-
window.__vibecheck_request_bodies.set(id, {
|
|
354
|
-
url,
|
|
355
|
-
method,
|
|
356
|
-
requestBody: typeof init.body === 'string' ? init.body : JSON.stringify(init.body),
|
|
357
|
-
timestamp: Date.now(),
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const response = await originalFetch.apply(this, args);
|
|
362
|
-
return response;
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// Intercept XMLHttpRequest
|
|
366
|
-
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
367
|
-
const originalXhrSend = XMLHttpRequest.prototype.send;
|
|
368
|
-
|
|
369
|
-
XMLHttpRequest.prototype.open = function(method, url) {
|
|
370
|
-
this.__vibecheck_method = method;
|
|
371
|
-
this.__vibecheck_url = url;
|
|
372
|
-
return originalXhrOpen.apply(this, arguments);
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
XMLHttpRequest.prototype.send = function(body) {
|
|
376
|
-
if (body) {
|
|
377
|
-
const id = crypto.randomUUID();
|
|
378
|
-
window.__vibecheck_request_bodies.set(id, {
|
|
379
|
-
url: this.__vibecheck_url,
|
|
380
|
-
method: this.__vibecheck_method,
|
|
381
|
-
requestBody: typeof body === 'string' ? body : JSON.stringify(body),
|
|
382
|
-
timestamp: Date.now(),
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
return originalXhrSend.apply(this, arguments);
|
|
386
|
-
};
|
|
387
|
-
`;
|
|
388
|
-
|
|
389
|
-
// =============================================================================
|
|
390
|
-
// EXPORTS
|
|
391
|
-
// =============================================================================
|
|
392
|
-
|
|
393
|
-
module.exports = {
|
|
394
|
-
// Core hashing
|
|
395
|
-
hashBody,
|
|
396
|
-
detectContentType,
|
|
397
|
-
redactSensitiveData,
|
|
398
|
-
REDACT_KEYS,
|
|
399
|
-
|
|
400
|
-
// Request enhancement
|
|
401
|
-
enhanceRequestWithHashes,
|
|
402
|
-
|
|
403
|
-
// Duplicate detection
|
|
404
|
-
findDuplicateMutations,
|
|
405
|
-
findNoOpMutations,
|
|
406
|
-
|
|
407
|
-
// UI mismatch detection
|
|
408
|
-
detectOptimisticMismatch,
|
|
409
|
-
analyzeUIMismatches,
|
|
410
|
-
|
|
411
|
-
// Playwright integration
|
|
412
|
-
PLAYWRIGHT_BODY_CAPTURE_SCRIPT,
|
|
413
|
-
|
|
414
|
-
// Constants
|
|
415
|
-
MAX_BODY_SIZE,
|
|
416
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Request Body Hashing v2
|
|
3
|
+
*
|
|
4
|
+
* Hashes request/response bodies for correlation and duplicate detection.
|
|
5
|
+
* Enables catching: duplicate mutations, no-op responses, optimistic UI mismatches.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// CONSTANTS
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
const MAX_BODY_SIZE = 100 * 1024; // 100KB max for hashing
|
|
17
|
+
const REDACT_KEYS = [
|
|
18
|
+
"password",
|
|
19
|
+
"token",
|
|
20
|
+
"secret",
|
|
21
|
+
"apiKey",
|
|
22
|
+
"api_key",
|
|
23
|
+
"authorization",
|
|
24
|
+
"auth",
|
|
25
|
+
"credit_card",
|
|
26
|
+
"creditCard",
|
|
27
|
+
"ssn",
|
|
28
|
+
"social_security",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// BODY HASHING
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hash request/response body with redaction
|
|
37
|
+
*/
|
|
38
|
+
function hashBody(body, options = {}) {
|
|
39
|
+
const { redact = true, maxSize = MAX_BODY_SIZE } = options;
|
|
40
|
+
|
|
41
|
+
if (!body) {
|
|
42
|
+
return { hash: null, isEmpty: true, size: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let content;
|
|
46
|
+
let contentType = "unknown";
|
|
47
|
+
|
|
48
|
+
// Handle different body types
|
|
49
|
+
if (typeof body === "string") {
|
|
50
|
+
content = body;
|
|
51
|
+
contentType = detectContentType(body);
|
|
52
|
+
} else if (Buffer.isBuffer(body)) {
|
|
53
|
+
content = body.toString("utf8");
|
|
54
|
+
contentType = "binary";
|
|
55
|
+
} else if (typeof body === "object") {
|
|
56
|
+
try {
|
|
57
|
+
content = JSON.stringify(body);
|
|
58
|
+
contentType = "json";
|
|
59
|
+
} catch {
|
|
60
|
+
content = String(body);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
content = String(body);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Size check
|
|
67
|
+
const size = content.length;
|
|
68
|
+
if (size > maxSize) {
|
|
69
|
+
return {
|
|
70
|
+
hash: null,
|
|
71
|
+
isEmpty: false,
|
|
72
|
+
size,
|
|
73
|
+
truncated: true,
|
|
74
|
+
contentType,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Redact sensitive data before hashing
|
|
79
|
+
let hashContent = content;
|
|
80
|
+
if (redact && contentType === "json") {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(content);
|
|
83
|
+
hashContent = JSON.stringify(redactSensitiveData(parsed));
|
|
84
|
+
} catch {
|
|
85
|
+
// Keep original if can't parse
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Compute hash
|
|
90
|
+
const hash = crypto.createHash("sha256").update(hashContent).digest("hex").slice(0, 16);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
hash,
|
|
94
|
+
isEmpty: size === 0 || content === "{}" || content === "null" || content === "[]",
|
|
95
|
+
size,
|
|
96
|
+
contentType,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detect content type from string
|
|
102
|
+
*/
|
|
103
|
+
function detectContentType(content) {
|
|
104
|
+
if (!content) return "empty";
|
|
105
|
+
|
|
106
|
+
const trimmed = content.trim();
|
|
107
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
108
|
+
if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) return "xml";
|
|
109
|
+
if (trimmed.includes("=") && trimmed.includes("&")) return "form";
|
|
110
|
+
|
|
111
|
+
return "text";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Redact sensitive data from object
|
|
116
|
+
*/
|
|
117
|
+
function redactSensitiveData(obj, depth = 0) {
|
|
118
|
+
if (depth > 10) return obj; // Prevent infinite recursion
|
|
119
|
+
|
|
120
|
+
if (Array.isArray(obj)) {
|
|
121
|
+
return obj.map(item => redactSensitiveData(item, depth + 1));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof obj === "object" && obj !== null) {
|
|
125
|
+
const redacted = {};
|
|
126
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
127
|
+
const keyLower = key.toLowerCase();
|
|
128
|
+
const isSensitive = REDACT_KEYS.some(k => keyLower.includes(k.toLowerCase()));
|
|
129
|
+
|
|
130
|
+
if (isSensitive) {
|
|
131
|
+
redacted[key] = "[REDACTED]";
|
|
132
|
+
} else if (typeof value === "object" && value !== null) {
|
|
133
|
+
redacted[key] = redactSensitiveData(value, depth + 1);
|
|
134
|
+
} else {
|
|
135
|
+
redacted[key] = value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return redacted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return obj;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// REQUEST RECORD ENHANCEMENT
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Enhance request record with body hashes
|
|
150
|
+
*/
|
|
151
|
+
function enhanceRequestWithHashes(request, options = {}) {
|
|
152
|
+
const enhanced = { ...request };
|
|
153
|
+
|
|
154
|
+
// Hash request body
|
|
155
|
+
if (request.postData || request.body) {
|
|
156
|
+
const bodyHash = hashBody(request.postData || request.body, options);
|
|
157
|
+
enhanced.requestBodyHash = bodyHash.hash;
|
|
158
|
+
enhanced.requestBodyEmpty = bodyHash.isEmpty;
|
|
159
|
+
enhanced.requestBodySize = bodyHash.size;
|
|
160
|
+
enhanced.requestContentType = bodyHash.contentType;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Hash response body if available
|
|
164
|
+
if (request.responseBody) {
|
|
165
|
+
const respHash = hashBody(request.responseBody, options);
|
|
166
|
+
enhanced.responseBodyHash = respHash.hash;
|
|
167
|
+
enhanced.responseBodyEmpty = respHash.isEmpty;
|
|
168
|
+
enhanced.responseBodySize = respHash.size;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return enhanced;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// DUPLICATE DETECTION
|
|
176
|
+
// =============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Find duplicate mutation requests
|
|
180
|
+
*/
|
|
181
|
+
function findDuplicateMutations(requests) {
|
|
182
|
+
const mutations = requests.filter(r =>
|
|
183
|
+
["POST", "PUT", "PATCH", "DELETE"].includes(r.method?.toUpperCase())
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Group by method + path + requestBodyHash
|
|
187
|
+
const groups = new Map();
|
|
188
|
+
|
|
189
|
+
for (const req of mutations) {
|
|
190
|
+
if (!req.requestBodyHash) continue;
|
|
191
|
+
|
|
192
|
+
const key = `${req.method}:${req.canonicalPath || req.url}:${req.requestBodyHash}`;
|
|
193
|
+
if (!groups.has(key)) {
|
|
194
|
+
groups.set(key, []);
|
|
195
|
+
}
|
|
196
|
+
groups.set(key, [...groups.get(key), req]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Find groups with duplicates
|
|
200
|
+
const duplicates = [];
|
|
201
|
+
for (const [key, reqs] of groups) {
|
|
202
|
+
if (reqs.length > 1) {
|
|
203
|
+
duplicates.push({
|
|
204
|
+
key,
|
|
205
|
+
count: reqs.length,
|
|
206
|
+
requests: reqs,
|
|
207
|
+
firstTimestamp: reqs[0].timestamp,
|
|
208
|
+
lastTimestamp: reqs[reqs.length - 1].timestamp,
|
|
209
|
+
timeDeltaMs: reqs.length > 1
|
|
210
|
+
? new Date(reqs[reqs.length - 1].timestamp) - new Date(reqs[0].timestamp)
|
|
211
|
+
: 0,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return duplicates;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Find no-op mutations (success response with empty body)
|
|
221
|
+
*/
|
|
222
|
+
function findNoOpMutations(requests) {
|
|
223
|
+
const noOps = [];
|
|
224
|
+
|
|
225
|
+
for (const req of requests) {
|
|
226
|
+
if (!["POST", "PUT", "PATCH"].includes(req.method?.toUpperCase())) continue;
|
|
227
|
+
|
|
228
|
+
const isSuccess = req.status >= 200 && req.status < 300;
|
|
229
|
+
const isEmptyResponse = req.responseBodyEmpty;
|
|
230
|
+
|
|
231
|
+
if (isSuccess && isEmptyResponse) {
|
|
232
|
+
noOps.push({
|
|
233
|
+
request: req,
|
|
234
|
+
reason: "Mutation succeeded but returned empty response",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return noOps;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// UI MISMATCH DETECTION
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Detect optimistic UI that didn't match server response
|
|
248
|
+
* Requires action records with before/after UI state
|
|
249
|
+
*/
|
|
250
|
+
function detectOptimisticMismatch(action, request) {
|
|
251
|
+
const mismatches = [];
|
|
252
|
+
|
|
253
|
+
// Check if action had optimistic update
|
|
254
|
+
if (!action.optimisticUpdate) return mismatches;
|
|
255
|
+
|
|
256
|
+
// Check if request succeeded
|
|
257
|
+
if (!request || request.status < 200 || request.status >= 300) {
|
|
258
|
+
mismatches.push({
|
|
259
|
+
type: "optimistic_request_failed",
|
|
260
|
+
action,
|
|
261
|
+
request,
|
|
262
|
+
reason: "Optimistic update shown but request failed",
|
|
263
|
+
});
|
|
264
|
+
return mismatches;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if response body matches what UI expected
|
|
268
|
+
if (action.expectedResponseHash && request.responseBodyHash) {
|
|
269
|
+
if (action.expectedResponseHash !== request.responseBodyHash) {
|
|
270
|
+
mismatches.push({
|
|
271
|
+
type: "optimistic_response_mismatch",
|
|
272
|
+
action,
|
|
273
|
+
request,
|
|
274
|
+
expected: action.expectedResponseHash,
|
|
275
|
+
actual: request.responseBodyHash,
|
|
276
|
+
reason: "Optimistic UI didn't match server response",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return mismatches;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Analyze all actions and requests for UI mismatches
|
|
286
|
+
*/
|
|
287
|
+
function analyzeUIMismatches(actions, requests) {
|
|
288
|
+
const mismatches = [];
|
|
289
|
+
const duplicates = findDuplicateMutations(requests);
|
|
290
|
+
const noOps = findNoOpMutations(requests);
|
|
291
|
+
|
|
292
|
+
// Duplicate mutations
|
|
293
|
+
for (const dup of duplicates) {
|
|
294
|
+
mismatches.push({
|
|
295
|
+
type: "duplicate_mutation",
|
|
296
|
+
severity: dup.timeDeltaMs < 1000 ? "WARN" : "INFO",
|
|
297
|
+
...dup,
|
|
298
|
+
reason: `Same mutation submitted ${dup.count} times`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// No-op mutations
|
|
303
|
+
for (const noOp of noOps) {
|
|
304
|
+
mismatches.push({
|
|
305
|
+
type: "noop_mutation",
|
|
306
|
+
severity: "WARN",
|
|
307
|
+
...noOp,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Optimistic mismatches
|
|
312
|
+
for (const action of actions) {
|
|
313
|
+
// Find associated request
|
|
314
|
+
const request = requests.find(r =>
|
|
315
|
+
r.actionId === action.id ||
|
|
316
|
+
(r.timestamp >= action.startTime && r.timestamp <= action.endTime)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const actionMismatches = detectOptimisticMismatch(action, request);
|
|
320
|
+
for (const m of actionMismatches) {
|
|
321
|
+
mismatches.push({
|
|
322
|
+
...m,
|
|
323
|
+
severity: "BLOCK",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return mismatches;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// =============================================================================
|
|
332
|
+
// PLAYWRIGHT INTEGRATION
|
|
333
|
+
// =============================================================================
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Playwright script to capture request/response bodies
|
|
337
|
+
* Inject this to capture bodies during crawling
|
|
338
|
+
*/
|
|
339
|
+
const PLAYWRIGHT_BODY_CAPTURE_SCRIPT = `
|
|
340
|
+
// Vibecheck Request Body Capture
|
|
341
|
+
window.__vibecheck_request_bodies = window.__vibecheck_request_bodies || new Map();
|
|
342
|
+
|
|
343
|
+
// Intercept fetch
|
|
344
|
+
const originalFetch = window.fetch;
|
|
345
|
+
window.fetch = async function(...args) {
|
|
346
|
+
const [input, init] = args;
|
|
347
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
348
|
+
const method = init?.method || 'GET';
|
|
349
|
+
|
|
350
|
+
// Capture request body
|
|
351
|
+
if (init?.body) {
|
|
352
|
+
const id = crypto.randomUUID();
|
|
353
|
+
window.__vibecheck_request_bodies.set(id, {
|
|
354
|
+
url,
|
|
355
|
+
method,
|
|
356
|
+
requestBody: typeof init.body === 'string' ? init.body : JSON.stringify(init.body),
|
|
357
|
+
timestamp: Date.now(),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const response = await originalFetch.apply(this, args);
|
|
362
|
+
return response;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Intercept XMLHttpRequest
|
|
366
|
+
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
367
|
+
const originalXhrSend = XMLHttpRequest.prototype.send;
|
|
368
|
+
|
|
369
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
370
|
+
this.__vibecheck_method = method;
|
|
371
|
+
this.__vibecheck_url = url;
|
|
372
|
+
return originalXhrOpen.apply(this, arguments);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
376
|
+
if (body) {
|
|
377
|
+
const id = crypto.randomUUID();
|
|
378
|
+
window.__vibecheck_request_bodies.set(id, {
|
|
379
|
+
url: this.__vibecheck_url,
|
|
380
|
+
method: this.__vibecheck_method,
|
|
381
|
+
requestBody: typeof body === 'string' ? body : JSON.stringify(body),
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return originalXhrSend.apply(this, arguments);
|
|
386
|
+
};
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// EXPORTS
|
|
391
|
+
// =============================================================================
|
|
392
|
+
|
|
393
|
+
module.exports = {
|
|
394
|
+
// Core hashing
|
|
395
|
+
hashBody,
|
|
396
|
+
detectContentType,
|
|
397
|
+
redactSensitiveData,
|
|
398
|
+
REDACT_KEYS,
|
|
399
|
+
|
|
400
|
+
// Request enhancement
|
|
401
|
+
enhanceRequestWithHashes,
|
|
402
|
+
|
|
403
|
+
// Duplicate detection
|
|
404
|
+
findDuplicateMutations,
|
|
405
|
+
findNoOpMutations,
|
|
406
|
+
|
|
407
|
+
// UI mismatch detection
|
|
408
|
+
detectOptimisticMismatch,
|
|
409
|
+
analyzeUIMismatches,
|
|
410
|
+
|
|
411
|
+
// Playwright integration
|
|
412
|
+
PLAYWRIGHT_BODY_CAPTURE_SCRIPT,
|
|
413
|
+
|
|
414
|
+
// Constants
|
|
415
|
+
MAX_BODY_SIZE,
|
|
416
|
+
};
|