@vibecheckai/cli 3.8.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -98
  2. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -318
  3. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -484
  4. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -418
  5. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -333
  6. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -622
  7. package/bin/runners/lib/agent-firewall/intent/index.js +102 -102
  8. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -352
  9. package/bin/runners/lib/agent-firewall/intent/store.js +283 -283
  10. package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
  11. package/bin/runners/lib/engine/ast-cache.js +210 -210
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -211
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -112
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
  15. package/bin/runners/lib/engine/env-extractor.js +207 -207
  16. package/bin/runners/lib/engine/express-extractor.js +208 -208
  17. package/bin/runners/lib/engine/extractors.js +849 -849
  18. package/bin/runners/lib/engine/index.js +207 -207
  19. package/bin/runners/lib/engine/repo-index.js +514 -514
  20. package/bin/runners/lib/engine/types.js +124 -124
  21. package/bin/runners/lib/unified-cli-output.js +16 -0
  22. package/bin/runners/runCI.js +353 -0
  23. package/bin/runners/runCheckpoint.js +2 -2
  24. package/bin/runners/runIntent.js +906 -906
  25. package/bin/runners/runPacks.js +2089 -2089
  26. package/bin/runners/runReality.js +178 -1
  27. package/bin/runners/runShield.js +1282 -1282
  28. package/mcp-server/handlers/index.ts +2 -2
  29. package/mcp-server/handlers/tool-handler.ts +47 -8
  30. package/mcp-server/lib/executor.ts +5 -5
  31. package/mcp-server/lib/index.ts +14 -4
  32. package/mcp-server/lib/sandbox.test.ts +4 -4
  33. package/mcp-server/lib/sandbox.ts +2 -2
  34. package/mcp-server/package.json +1 -1
  35. package/mcp-server/registry.test.ts +18 -12
  36. package/mcp-server/tsconfig.json +1 -0
  37. package/package.json +2 -1
@@ -1,211 +1,211 @@
1
- // bin/runners/lib/engine/auth-extractor.js
2
- // Optimized auth/middleware extraction using RepoIndex
3
-
4
- const path = require("path");
5
- const fs = require("fs");
6
- const crypto = require("crypto");
7
-
8
- function sha256(text) {
9
- return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
10
- }
11
-
12
- function evidenceFromContent(content, fileRel, lineNo, reason) {
13
- if (!content) return null;
14
- const lines = content.split(/\r?\n/);
15
- const idx = Math.max(0, Math.min(lines.length - 1, lineNo - 1));
16
- const snippet = lines[idx] || "";
17
- return {
18
- id: `ev_${crypto.randomBytes(4).toString("hex")}`,
19
- file: fileRel,
20
- lines: `${lineNo}-${lineNo}`,
21
- snippetHash: sha256(snippet),
22
- reason
23
- };
24
- }
25
-
26
- function findLineMatches(code, regex) {
27
- const out = [];
28
- const lines = code.split(/\r?\n/);
29
- for (let i = 0; i < lines.length; i++) {
30
- if (regex.test(lines[i])) out.push(i + 1);
31
- }
32
- return out;
33
- }
34
-
35
- function guessAuthSignalsFromCode(code) {
36
- const signals = [];
37
-
38
- const patterns = [
39
- { key: "next_middleware", rx: /\bNextResponse\.(redirect|rewrite)\b/ },
40
- { key: "next_auth", rx: /\bgetServerSession\b|\bNextAuth\b|\bauth\(\)\b/ },
41
- { key: "clerk", rx: /\bclerkMiddleware\b|\bauthMiddleware\b|@clerk\/nextjs/ },
42
- { key: "supabase", rx: /\bcreateRouteHandlerClient\b|\bcreateServerClient\b|@supabase/ },
43
- { key: "jwt_verify", rx: /\b(jwtVerify|verifyJWT|verifyToken|authorization|bearer)\b/i },
44
- { key: "session", rx: /\b(session|cookie|setCookie|getCookie)\b/i },
45
- { key: "rbac", rx: /\b(role|roles|permissions|rbac|isAdmin|adminOnly)\b/i },
46
- { key: "fastify_hook", rx: /\.addHook\(\s*['"](onRequest|preHandler|preValidation)['"]/ },
47
- { key: "fastify_jwt", rx: /@fastify\/jwt|fastify-jwt|fastify\.jwt/i },
48
- ];
49
-
50
- for (const p of patterns) {
51
- if (p.rx.test(code)) signals.push(p.key);
52
- }
53
- return Array.from(new Set(signals));
54
- }
55
-
56
- /**
57
- * Resolve Next.js middleware using RepoIndex
58
- * @param {import('./repo-index').RepoIndex} index
59
- * @returns {Array}
60
- */
61
- function extractNextMiddleware(index) {
62
- // Find middleware.ts/js files
63
- const middlewareFiles = index.files.filter(f =>
64
- /^(src\/)?middleware\.(ts|tsx|js|jsx)$/.test(f.rel)
65
- );
66
-
67
- const middlewares = [];
68
-
69
- for (const file of middlewareFiles) {
70
- const content = index.getContent(file.abs);
71
- if (!content) continue;
72
-
73
- const matcherLines = findLineMatches(content, /\bmatcher\b/);
74
- const redirectLines = findLineMatches(content, /\bNextResponse\.(redirect|rewrite)\b/);
75
-
76
- const evidence = [];
77
- for (const ln of matcherLines.slice(0, 5)) {
78
- evidence.push(evidenceFromContent(content, file.rel, ln, "Next middleware matcher config"));
79
- }
80
- for (const ln of redirectLines.slice(0, 5)) {
81
- evidence.push(evidenceFromContent(content, file.rel, ln, "Next middleware redirect/rewrite"));
82
- }
83
-
84
- const matcher = [];
85
- const matcherBlock = content.match(/matcher\s*:\s*(\[[\s\S]*?\])/);
86
- if (matcherBlock && matcherBlock[1]) {
87
- const raw = matcherBlock[1];
88
- const strings = Array.from(raw.matchAll(/['"`]([^'"`]+)['"`]/g)).map(m => m[1]);
89
- matcher.push(...strings);
90
- }
91
-
92
- middlewares.push({
93
- file: file.rel,
94
- matcher,
95
- signals: guessAuthSignalsFromCode(content),
96
- evidence
97
- });
98
- }
99
-
100
- return middlewares;
101
- }
102
-
103
- /**
104
- * Resolve Fastify auth signals using RepoIndex
105
- * @param {import('./repo-index').RepoIndex} index
106
- * @param {Array} truthpackRoutes - Server routes from truthpack
107
- * @returns {Object}
108
- */
109
- function extractFastifyAuthSignals(index, truthpackRoutes) {
110
- const handlerFiles = new Set((truthpackRoutes || []).map(r => r.handler).filter(Boolean));
111
- const signals = [];
112
- const evidence = [];
113
-
114
- for (const fileRel of handlerFiles) {
115
- // Try to get content from index first (fast path)
116
- const fileAbs = path.join(index.repoRoot, fileRel);
117
- let content = index.getContent(fileAbs);
118
-
119
- // Fall back to direct read if not in index
120
- if (!content) {
121
- try {
122
- content = fs.readFileSync(fileAbs, "utf8");
123
- } catch {
124
- continue;
125
- }
126
- }
127
-
128
- const sigs = guessAuthSignalsFromCode(content);
129
- if (!sigs.length) continue;
130
-
131
- for (const s of sigs) signals.push({ type: s, file: fileRel });
132
-
133
- const authLinePatterns = [
134
- { rx: /\.addHook\(\s*['"](onRequest|preHandler|preValidation)['"]/, reason: "Fastify hook likely used for auth" },
135
- { rx: /\b(jwtVerify|authorization|bearer)\b/i, reason: "JWT/Authorization verification signal" },
136
- { rx: /@fastify\/jwt|fastify\.jwt/i, reason: "Fastify JWT plugin signal" },
137
- { rx: /\b(isAdmin|adminOnly|permissions|rbac)\b/i, reason: "RBAC/permissions signal" },
138
- ];
139
-
140
- const lines = content.split(/\r?\n/);
141
- for (let i = 0; i < lines.length; i++) {
142
- const line = lines[i];
143
- for (const p of authLinePatterns) {
144
- if (p.rx.test(line)) {
145
- evidence.push(evidenceFromContent(content, fileRel, i + 1, p.reason));
146
- }
147
- }
148
- if (evidence.length > 30) break;
149
- }
150
- }
151
-
152
- const uniqueTypes = Array.from(new Set(signals.map(s => s.type)));
153
-
154
- return {
155
- signalTypes: uniqueTypes,
156
- signals,
157
- evidence
158
- };
159
- }
160
-
161
- function matcherCoversPath(matcherList, p) {
162
- if (!Array.isArray(matcherList) || !matcherList.length) return false;
163
- const pathStr = p.startsWith("/") ? p : `/${p}`;
164
-
165
- return matcherList.some(m => {
166
- if (!m) return false;
167
-
168
- if (m.includes(":path*")) {
169
- const prefix = m.split(":path*")[0].replace(/\/$/, "");
170
- return pathStr.startsWith(prefix || "/");
171
- }
172
- if (m.includes("(.*)")) {
173
- const prefix = m.split("(.*)")[0].replace(/\/$/, "");
174
- return pathStr.startsWith(prefix || "/");
175
- }
176
-
177
- if (m === pathStr) return true;
178
- if (pathStr.startsWith(m.endsWith("/") ? m : m + "/")) return true;
179
-
180
- return false;
181
- });
182
- }
183
-
184
- /**
185
- * Build auth truth using RepoIndex (optimized)
186
- * @param {import('./repo-index').RepoIndex} index
187
- * @param {Array} serverRoutes
188
- * @returns {Object}
189
- */
190
- function buildAuthTruthV2(index, serverRoutes) {
191
- const middlewares = extractNextMiddleware(index);
192
- const matchers = middlewares.flatMap(mw => mw.matcher || []);
193
- const fastify = extractFastifyAuthSignals(index, serverRoutes);
194
-
195
- return {
196
- nextMiddleware: middlewares,
197
- nextMatcherPatterns: matchers,
198
- fastify,
199
- helpers: {
200
- matcherCoversPath: "runtime-only"
201
- }
202
- };
203
- }
204
-
205
- module.exports = {
206
- buildAuthTruthV2,
207
- extractNextMiddleware,
208
- extractFastifyAuthSignals,
209
- matcherCoversPath,
210
- guessAuthSignalsFromCode,
211
- };
1
+ // bin/runners/lib/engine/auth-extractor.js
2
+ // Optimized auth/middleware extraction using RepoIndex
3
+
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const crypto = require("crypto");
7
+
8
+ function sha256(text) {
9
+ return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
10
+ }
11
+
12
+ function evidenceFromContent(content, fileRel, lineNo, reason) {
13
+ if (!content) return null;
14
+ const lines = content.split(/\r?\n/);
15
+ const idx = Math.max(0, Math.min(lines.length - 1, lineNo - 1));
16
+ const snippet = lines[idx] || "";
17
+ return {
18
+ id: `ev_${crypto.randomBytes(4).toString("hex")}`,
19
+ file: fileRel,
20
+ lines: `${lineNo}-${lineNo}`,
21
+ snippetHash: sha256(snippet),
22
+ reason
23
+ };
24
+ }
25
+
26
+ function findLineMatches(code, regex) {
27
+ const out = [];
28
+ const lines = code.split(/\r?\n/);
29
+ for (let i = 0; i < lines.length; i++) {
30
+ if (regex.test(lines[i])) out.push(i + 1);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function guessAuthSignalsFromCode(code) {
36
+ const signals = [];
37
+
38
+ const patterns = [
39
+ { key: "next_middleware", rx: /\bNextResponse\.(redirect|rewrite)\b/ },
40
+ { key: "next_auth", rx: /\bgetServerSession\b|\bNextAuth\b|\bauth\(\)\b/ },
41
+ { key: "clerk", rx: /\bclerkMiddleware\b|\bauthMiddleware\b|@clerk\/nextjs/ },
42
+ { key: "supabase", rx: /\bcreateRouteHandlerClient\b|\bcreateServerClient\b|@supabase/ },
43
+ { key: "jwt_verify", rx: /\b(jwtVerify|verifyJWT|verifyToken|authorization|bearer)\b/i },
44
+ { key: "session", rx: /\b(session|cookie|setCookie|getCookie)\b/i },
45
+ { key: "rbac", rx: /\b(role|roles|permissions|rbac|isAdmin|adminOnly)\b/i },
46
+ { key: "fastify_hook", rx: /\.addHook\(\s*['"](onRequest|preHandler|preValidation)['"]/ },
47
+ { key: "fastify_jwt", rx: /@fastify\/jwt|fastify-jwt|fastify\.jwt/i },
48
+ ];
49
+
50
+ for (const p of patterns) {
51
+ if (p.rx.test(code)) signals.push(p.key);
52
+ }
53
+ return Array.from(new Set(signals));
54
+ }
55
+
56
+ /**
57
+ * Resolve Next.js middleware using RepoIndex
58
+ * @param {import('./repo-index').RepoIndex} index
59
+ * @returns {Array}
60
+ */
61
+ function extractNextMiddleware(index) {
62
+ // Find middleware.ts/js files
63
+ const middlewareFiles = index.files.filter(f =>
64
+ /^(src\/)?middleware\.(ts|tsx|js|jsx)$/.test(f.rel)
65
+ );
66
+
67
+ const middlewares = [];
68
+
69
+ for (const file of middlewareFiles) {
70
+ const content = index.getContent(file.abs);
71
+ if (!content) continue;
72
+
73
+ const matcherLines = findLineMatches(content, /\bmatcher\b/);
74
+ const redirectLines = findLineMatches(content, /\bNextResponse\.(redirect|rewrite)\b/);
75
+
76
+ const evidence = [];
77
+ for (const ln of matcherLines.slice(0, 5)) {
78
+ evidence.push(evidenceFromContent(content, file.rel, ln, "Next middleware matcher config"));
79
+ }
80
+ for (const ln of redirectLines.slice(0, 5)) {
81
+ evidence.push(evidenceFromContent(content, file.rel, ln, "Next middleware redirect/rewrite"));
82
+ }
83
+
84
+ const matcher = [];
85
+ const matcherBlock = content.match(/matcher\s*:\s*(\[[\s\S]*?\])/);
86
+ if (matcherBlock && matcherBlock[1]) {
87
+ const raw = matcherBlock[1];
88
+ const strings = Array.from(raw.matchAll(/['"`]([^'"`]+)['"`]/g)).map(m => m[1]);
89
+ matcher.push(...strings);
90
+ }
91
+
92
+ middlewares.push({
93
+ file: file.rel,
94
+ matcher,
95
+ signals: guessAuthSignalsFromCode(content),
96
+ evidence
97
+ });
98
+ }
99
+
100
+ return middlewares;
101
+ }
102
+
103
+ /**
104
+ * Resolve Fastify auth signals using RepoIndex
105
+ * @param {import('./repo-index').RepoIndex} index
106
+ * @param {Array} truthpackRoutes - Server routes from truthpack
107
+ * @returns {Object}
108
+ */
109
+ function extractFastifyAuthSignals(index, truthpackRoutes) {
110
+ const handlerFiles = new Set((truthpackRoutes || []).map(r => r.handler).filter(Boolean));
111
+ const signals = [];
112
+ const evidence = [];
113
+
114
+ for (const fileRel of handlerFiles) {
115
+ // Try to get content from index first (fast path)
116
+ const fileAbs = path.join(index.repoRoot, fileRel);
117
+ let content = index.getContent(fileAbs);
118
+
119
+ // Fall back to direct read if not in index
120
+ if (!content) {
121
+ try {
122
+ content = fs.readFileSync(fileAbs, "utf8");
123
+ } catch {
124
+ continue;
125
+ }
126
+ }
127
+
128
+ const sigs = guessAuthSignalsFromCode(content);
129
+ if (!sigs.length) continue;
130
+
131
+ for (const s of sigs) signals.push({ type: s, file: fileRel });
132
+
133
+ const authLinePatterns = [
134
+ { rx: /\.addHook\(\s*['"](onRequest|preHandler|preValidation)['"]/, reason: "Fastify hook likely used for auth" },
135
+ { rx: /\b(jwtVerify|authorization|bearer)\b/i, reason: "JWT/Authorization verification signal" },
136
+ { rx: /@fastify\/jwt|fastify\.jwt/i, reason: "Fastify JWT plugin signal" },
137
+ { rx: /\b(isAdmin|adminOnly|permissions|rbac)\b/i, reason: "RBAC/permissions signal" },
138
+ ];
139
+
140
+ const lines = content.split(/\r?\n/);
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const line = lines[i];
143
+ for (const p of authLinePatterns) {
144
+ if (p.rx.test(line)) {
145
+ evidence.push(evidenceFromContent(content, fileRel, i + 1, p.reason));
146
+ }
147
+ }
148
+ if (evidence.length > 30) break;
149
+ }
150
+ }
151
+
152
+ const uniqueTypes = Array.from(new Set(signals.map(s => s.type)));
153
+
154
+ return {
155
+ signalTypes: uniqueTypes,
156
+ signals,
157
+ evidence
158
+ };
159
+ }
160
+
161
+ function matcherCoversPath(matcherList, p) {
162
+ if (!Array.isArray(matcherList) || !matcherList.length) return false;
163
+ const pathStr = p.startsWith("/") ? p : `/${p}`;
164
+
165
+ return matcherList.some(m => {
166
+ if (!m) return false;
167
+
168
+ if (m.includes(":path*")) {
169
+ const prefix = m.split(":path*")[0].replace(/\/$/, "");
170
+ return pathStr.startsWith(prefix || "/");
171
+ }
172
+ if (m.includes("(.*)")) {
173
+ const prefix = m.split("(.*)")[0].replace(/\/$/, "");
174
+ return pathStr.startsWith(prefix || "/");
175
+ }
176
+
177
+ if (m === pathStr) return true;
178
+ if (pathStr.startsWith(m.endsWith("/") ? m : m + "/")) return true;
179
+
180
+ return false;
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Build auth truth using RepoIndex (optimized)
186
+ * @param {import('./repo-index').RepoIndex} index
187
+ * @param {Array} serverRoutes
188
+ * @returns {Object}
189
+ */
190
+ function buildAuthTruthV2(index, serverRoutes) {
191
+ const middlewares = extractNextMiddleware(index);
192
+ const matchers = middlewares.flatMap(mw => mw.matcher || []);
193
+ const fastify = extractFastifyAuthSignals(index, serverRoutes);
194
+
195
+ return {
196
+ nextMiddleware: middlewares,
197
+ nextMatcherPatterns: matchers,
198
+ fastify,
199
+ helpers: {
200
+ matcherCoversPath: "runtime-only"
201
+ }
202
+ };
203
+ }
204
+
205
+ module.exports = {
206
+ buildAuthTruthV2,
207
+ extractNextMiddleware,
208
+ extractFastifyAuthSignals,
209
+ matcherCoversPath,
210
+ guessAuthSignalsFromCode,
211
+ };
@@ -1,112 +1,112 @@
1
- // bin/runners/lib/engine/billing-extractor.js
2
- // Optimized billing/Stripe extraction using RepoIndex
3
-
4
- const path = require("path");
5
- const crypto = require("crypto");
6
-
7
- function sha256(text) {
8
- return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
9
- }
10
-
11
- function evidenceFromContent(content, fileRel, lineNo, reason) {
12
- if (!content) return null;
13
- const lines = content.split(/\r?\n/);
14
- const idx = Math.max(0, Math.min(lines.length - 1, lineNo - 1));
15
- const snippet = lines[idx] || "";
16
- return {
17
- id: `ev_${crypto.randomBytes(4).toString("hex")}`,
18
- file: fileRel,
19
- lines: `${lineNo}-${lineNo}`,
20
- snippetHash: sha256(snippet),
21
- reason
22
- };
23
- }
24
-
25
- function findLineMatches(code, regex) {
26
- const out = [];
27
- const lines = code.split(/\r?\n/);
28
- for (let i = 0; i < lines.length; i++) {
29
- if (regex.test(lines[i])) out.push(i + 1);
30
- }
31
- return out;
32
- }
33
-
34
- function classifyStripeSignals(code) {
35
- return {
36
- usesStripeSdk: /\bstripe\b/i.test(code) && /from\s+['"]stripe['"]|require\(['"]stripe['"]\)/.test(code),
37
- webhookConstructEvent: /\bconstructEvent(Async)?\b/.test(code) || /\bstripe\.webhooks\.constructEvent\b/.test(code),
38
- readsStripeSignatureHeader: /stripe-signature/i.test(code) || /\bStripe-Signature\b/.test(code),
39
- rawBodySignal:
40
- /\bbodyParser\s*:\s*false\b/.test(code) ||
41
- /\breq\.(text|arrayBuffer)\(\)/.test(code) ||
42
- /\brawBody\b/.test(code) || /\brequest\.raw\b/.test(code) || /\bcontentTypeParser\b/i.test(code),
43
- idempotencySignal:
44
- /\bevent\.id\b/.test(code) && /\b(prisma|db|redis|cache|processed|dedupe|idempotent)\b/i.test(code) ||
45
- /\bidempotenc(y|e)\b/i.test(code)
46
- };
47
- }
48
-
49
- /**
50
- * Build billing truth using RepoIndex (optimized)
51
- * @param {import('./repo-index').RepoIndex} index
52
- * @param {Object} stats
53
- * @returns {Object}
54
- */
55
- function buildBillingTruthV2(index, stats) {
56
- // Use token prefilter - only scan files that mention stripe
57
- const candidateAbs = index.getByAnyToken(["stripe", "Stripe"]);
58
-
59
- // Filter to JS/TS files
60
- const jsExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
61
- const files = candidateAbs.filter(abs => {
62
- const ext = path.extname(abs).toLowerCase();
63
- return jsExtensions.has(ext);
64
- });
65
-
66
- const webhookCandidates = [];
67
- const stripeFiles = [];
68
-
69
- for (const fileAbs of files) {
70
- const fileRel = index.relPath(fileAbs);
71
- const content = index.getContent(fileAbs);
72
- if (!content) continue;
73
-
74
- const signals = classifyStripeSignals(content);
75
-
76
- if (signals.usesStripeSdk) stripeFiles.push(fileRel);
77
-
78
- if (signals.webhookConstructEvent || signals.readsStripeSignatureHeader) {
79
- const ev = [];
80
- const lines1 = findLineMatches(content, /\bconstructEvent(Async)?\b|stripe\.webhooks\.constructEvent/);
81
- const lines2 = findLineMatches(content, /stripe-signature|Stripe-Signature/i);
82
- const lines3 = findLineMatches(content, /bodyParser\s*:\s*false|req\.(text|arrayBuffer)\(|rawBody|contentTypeParser/i);
83
- const lines4 = findLineMatches(content, /event\.id|idempotenc(y|e)|dedupe|processed/i);
84
-
85
- for (const ln of lines1.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Stripe webhook signature constructEvent signal"));
86
- for (const ln of lines2.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Stripe-Signature header usage signal"));
87
- for (const ln of lines3.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Raw body handling signal (required for Stripe verification)"));
88
- for (const ln of lines4.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Idempotency/dedupe signal (event replay protection)"));
89
-
90
- webhookCandidates.push({
91
- file: fileRel,
92
- signals,
93
- evidence: ev
94
- });
95
- }
96
- }
97
-
98
- const hasStripe = stripeFiles.length > 0;
99
-
100
- return {
101
- hasStripe,
102
- stripeFiles: stripeFiles.slice(0, 200),
103
- webhookCandidates,
104
- summary: {
105
- webhookHandlersFound: webhookCandidates.length,
106
- verifiedWebhookHandlers: webhookCandidates.filter(w => w.signals.webhookConstructEvent && w.signals.rawBodySignal).length,
107
- idempotentWebhookHandlers: webhookCandidates.filter(w => w.signals.idempotencySignal).length
108
- }
109
- };
110
- }
111
-
112
- module.exports = { buildBillingTruthV2 };
1
+ // bin/runners/lib/engine/billing-extractor.js
2
+ // Optimized billing/Stripe extraction using RepoIndex
3
+
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+
7
+ function sha256(text) {
8
+ return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
9
+ }
10
+
11
+ function evidenceFromContent(content, fileRel, lineNo, reason) {
12
+ if (!content) return null;
13
+ const lines = content.split(/\r?\n/);
14
+ const idx = Math.max(0, Math.min(lines.length - 1, lineNo - 1));
15
+ const snippet = lines[idx] || "";
16
+ return {
17
+ id: `ev_${crypto.randomBytes(4).toString("hex")}`,
18
+ file: fileRel,
19
+ lines: `${lineNo}-${lineNo}`,
20
+ snippetHash: sha256(snippet),
21
+ reason
22
+ };
23
+ }
24
+
25
+ function findLineMatches(code, regex) {
26
+ const out = [];
27
+ const lines = code.split(/\r?\n/);
28
+ for (let i = 0; i < lines.length; i++) {
29
+ if (regex.test(lines[i])) out.push(i + 1);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function classifyStripeSignals(code) {
35
+ return {
36
+ usesStripeSdk: /\bstripe\b/i.test(code) && /from\s+['"]stripe['"]|require\(['"]stripe['"]\)/.test(code),
37
+ webhookConstructEvent: /\bconstructEvent(Async)?\b/.test(code) || /\bstripe\.webhooks\.constructEvent\b/.test(code),
38
+ readsStripeSignatureHeader: /stripe-signature/i.test(code) || /\bStripe-Signature\b/.test(code),
39
+ rawBodySignal:
40
+ /\bbodyParser\s*:\s*false\b/.test(code) ||
41
+ /\breq\.(text|arrayBuffer)\(\)/.test(code) ||
42
+ /\brawBody\b/.test(code) || /\brequest\.raw\b/.test(code) || /\bcontentTypeParser\b/i.test(code),
43
+ idempotencySignal:
44
+ /\bevent\.id\b/.test(code) && /\b(prisma|db|redis|cache|processed|dedupe|idempotent)\b/i.test(code) ||
45
+ /\bidempotenc(y|e)\b/i.test(code)
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Build billing truth using RepoIndex (optimized)
51
+ * @param {import('./repo-index').RepoIndex} index
52
+ * @param {Object} stats
53
+ * @returns {Object}
54
+ */
55
+ function buildBillingTruthV2(index, stats) {
56
+ // Use token prefilter - only scan files that mention stripe
57
+ const candidateAbs = index.getByAnyToken(["stripe", "Stripe"]);
58
+
59
+ // Filter to JS/TS files
60
+ const jsExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
61
+ const files = candidateAbs.filter(abs => {
62
+ const ext = path.extname(abs).toLowerCase();
63
+ return jsExtensions.has(ext);
64
+ });
65
+
66
+ const webhookCandidates = [];
67
+ const stripeFiles = [];
68
+
69
+ for (const fileAbs of files) {
70
+ const fileRel = index.relPath(fileAbs);
71
+ const content = index.getContent(fileAbs);
72
+ if (!content) continue;
73
+
74
+ const signals = classifyStripeSignals(content);
75
+
76
+ if (signals.usesStripeSdk) stripeFiles.push(fileRel);
77
+
78
+ if (signals.webhookConstructEvent || signals.readsStripeSignatureHeader) {
79
+ const ev = [];
80
+ const lines1 = findLineMatches(content, /\bconstructEvent(Async)?\b|stripe\.webhooks\.constructEvent/);
81
+ const lines2 = findLineMatches(content, /stripe-signature|Stripe-Signature/i);
82
+ const lines3 = findLineMatches(content, /bodyParser\s*:\s*false|req\.(text|arrayBuffer)\(|rawBody|contentTypeParser/i);
83
+ const lines4 = findLineMatches(content, /event\.id|idempotenc(y|e)|dedupe|processed/i);
84
+
85
+ for (const ln of lines1.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Stripe webhook signature constructEvent signal"));
86
+ for (const ln of lines2.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Stripe-Signature header usage signal"));
87
+ for (const ln of lines3.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Raw body handling signal (required for Stripe verification)"));
88
+ for (const ln of lines4.slice(0, 3)) ev.push(evidenceFromContent(content, fileRel, ln, "Idempotency/dedupe signal (event replay protection)"));
89
+
90
+ webhookCandidates.push({
91
+ file: fileRel,
92
+ signals,
93
+ evidence: ev
94
+ });
95
+ }
96
+ }
97
+
98
+ const hasStripe = stripeFiles.length > 0;
99
+
100
+ return {
101
+ hasStripe,
102
+ stripeFiles: stripeFiles.slice(0, 200),
103
+ webhookCandidates,
104
+ summary: {
105
+ webhookHandlersFound: webhookCandidates.length,
106
+ verifiedWebhookHandlers: webhookCandidates.filter(w => w.signals.webhookConstructEvent && w.signals.rawBodySignal).length,
107
+ idempotentWebhookHandlers: webhookCandidates.filter(w => w.signals.idempotencySignal).length
108
+ }
109
+ };
110
+ }
111
+
112
+ module.exports = { buildBillingTruthV2 };