@vibecheckai/cli 3.0.3 → 3.0.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.
Files changed (69) hide show
  1. package/bin/cli-hygiene.js +241 -0
  2. package/bin/guardrail.js +834 -0
  3. package/bin/runners/cli-utils.js +1070 -0
  4. package/bin/runners/context/ai-task-decomposer.js +337 -0
  5. package/bin/runners/context/analyzer.js +462 -0
  6. package/bin/runners/context/api-contracts.js +427 -0
  7. package/bin/runners/context/context-diff.js +342 -0
  8. package/bin/runners/context/context-pruner.js +291 -0
  9. package/bin/runners/context/dependency-graph.js +414 -0
  10. package/bin/runners/context/generators/claude.js +107 -0
  11. package/bin/runners/context/generators/codex.js +108 -0
  12. package/bin/runners/context/generators/copilot.js +119 -0
  13. package/bin/runners/context/generators/cursor.js +514 -0
  14. package/bin/runners/context/generators/mcp.js +151 -0
  15. package/bin/runners/context/generators/windsurf.js +180 -0
  16. package/bin/runners/context/git-context.js +302 -0
  17. package/bin/runners/context/index.js +1042 -0
  18. package/bin/runners/context/insights.js +173 -0
  19. package/bin/runners/context/mcp-server/generate-rules.js +337 -0
  20. package/bin/runners/context/mcp-server/index.js +1176 -0
  21. package/bin/runners/context/mcp-server/package.json +24 -0
  22. package/bin/runners/context/memory.js +200 -0
  23. package/bin/runners/context/monorepo.js +215 -0
  24. package/bin/runners/context/multi-repo-federation.js +404 -0
  25. package/bin/runners/context/patterns.js +253 -0
  26. package/bin/runners/context/proof-context.js +972 -0
  27. package/bin/runners/context/security-scanner.js +303 -0
  28. package/bin/runners/context/semantic-search.js +350 -0
  29. package/bin/runners/context/shared.js +264 -0
  30. package/bin/runners/context/team-conventions.js +310 -0
  31. package/bin/runners/lib/ai-bridge.js +416 -0
  32. package/bin/runners/lib/analysis-core.js +271 -0
  33. package/bin/runners/lib/analyzers.js +541 -0
  34. package/bin/runners/lib/audit-bridge.js +391 -0
  35. package/bin/runners/lib/auth-truth.js +193 -0
  36. package/bin/runners/lib/auth.js +215 -0
  37. package/bin/runners/lib/backup.js +62 -0
  38. package/bin/runners/lib/billing.js +107 -0
  39. package/bin/runners/lib/claims.js +118 -0
  40. package/bin/runners/lib/cli-ui.js +540 -0
  41. package/bin/runners/lib/compliance-bridge-new.js +0 -0
  42. package/bin/runners/lib/compliance-bridge.js +165 -0
  43. package/bin/runners/lib/contracts/auth-contract.js +194 -0
  44. package/bin/runners/lib/contracts/env-contract.js +178 -0
  45. package/bin/runners/lib/contracts/external-contract.js +198 -0
  46. package/bin/runners/lib/contracts/guard.js +168 -0
  47. package/bin/runners/lib/contracts/index.js +89 -0
  48. package/bin/runners/lib/contracts/plan-validator.js +311 -0
  49. package/bin/runners/lib/contracts/route-contract.js +192 -0
  50. package/bin/runners/lib/detect.js +89 -0
  51. package/bin/runners/lib/doctor/autofix.js +254 -0
  52. package/bin/runners/lib/doctor/index.js +37 -0
  53. package/bin/runners/lib/doctor/modules/dependencies.js +325 -0
  54. package/bin/runners/lib/doctor/modules/index.js +46 -0
  55. package/bin/runners/lib/doctor/modules/network.js +250 -0
  56. package/bin/runners/lib/doctor/modules/project.js +312 -0
  57. package/bin/runners/lib/doctor/modules/runtime.js +224 -0
  58. package/bin/runners/lib/doctor/modules/security.js +348 -0
  59. package/bin/runners/lib/doctor/modules/system.js +213 -0
  60. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -0
  61. package/bin/runners/lib/doctor/reporter.js +262 -0
  62. package/bin/runners/lib/doctor/service.js +262 -0
  63. package/bin/runners/lib/doctor/types.js +113 -0
  64. package/bin/runners/lib/doctor/ui.js +263 -0
  65. package/bin/runners/lib/doctor-enhanced.js +233 -0
  66. package/bin/runners/lib/doctor-v2.js +608 -0
  67. package/bin/runners/lib/enforcement.js +72 -0
  68. package/bin/vibecheck.js +0 -0
  69. package/package.json +8 -9
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Audit Bridge - CLI Integration
3
+ *
4
+ * Provides a CommonJS wrapper for the audit trail functionality.
5
+ * Used by CLI runners to emit audit events.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const path = require("path");
11
+ const fs = require("fs");
12
+ const crypto = require("crypto");
13
+
14
+ // Configuration
15
+ const AUDIT_DIR = ".vibecheck/audit";
16
+ const AUDIT_FILE = "audit.log.jsonl";
17
+ const GENESIS_HASH = "0".repeat(64);
18
+
19
+ // Tier from environment or default
20
+ function getCurrentTier() {
21
+ return process.env.VIBECHECK_TIER || "free";
22
+ }
23
+
24
+ // Get current actor from environment
25
+ function getCurrentActor() {
26
+ const userId = process.env.VIBECHECK_USER_ID || process.env.USER || process.env.USERNAME || "anonymous";
27
+ const userName = process.env.VIBECHECK_USER_NAME || process.env.USERNAME;
28
+ const userEmail = process.env.VIBECHECK_USER_EMAIL;
29
+
30
+ // Detect CI environment
31
+ if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI) {
32
+ return {
33
+ id: process.env.GITHUB_ACTOR || process.env.GITLAB_USER_LOGIN || "ci-system",
34
+ type: "ci",
35
+ name: process.env.GITHUB_ACTOR || process.env.GITLAB_USER_NAME,
36
+ };
37
+ }
38
+
39
+ return {
40
+ id: userId,
41
+ type: "user",
42
+ name: userName,
43
+ email: userEmail,
44
+ };
45
+ }
46
+
47
+ // Redaction patterns for sensitive data
48
+ const REDACTION_PATTERNS = [
49
+ /(?:api[_-]?key|apikey|token|secret|password|pwd|auth)[=:]\s*['"]?([a-zA-Z0-9_\-]{16,})['"]?/gi,
50
+ /eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g,
51
+ /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g,
52
+ /(?:sk_live_|sk_test_|pk_live_|pk_test_)[a-zA-Z0-9]+/g,
53
+ ];
54
+
55
+ function redactSensitive(input) {
56
+ if (typeof input !== "string") return input;
57
+ let result = input;
58
+ for (const pattern of REDACTION_PATTERNS) {
59
+ result = result.replace(pattern, "[REDACTED]");
60
+ }
61
+ return result;
62
+ }
63
+
64
+ function redactMetadata(metadata, tier) {
65
+ if (!metadata) return undefined;
66
+
67
+ // Compliance+ gets full metadata (still redact secrets)
68
+ if (["compliance", "enterprise", "unlimited"].includes(tier)) {
69
+ return redactObject(metadata);
70
+ }
71
+
72
+ // Pro gets limited metadata
73
+ if (tier === "pro") {
74
+ return {
75
+ command: metadata.command,
76
+ score: metadata.score,
77
+ grade: metadata.grade,
78
+ issueCount: metadata.issueCount,
79
+ fixCount: metadata.fixCount,
80
+ durationMs: metadata.durationMs,
81
+ errorCode: metadata.errorCode,
82
+ };
83
+ }
84
+
85
+ // Free/Starter get minimal
86
+ return {
87
+ score: metadata.score,
88
+ grade: metadata.grade,
89
+ };
90
+ }
91
+
92
+ function redactObject(obj) {
93
+ if (!obj || typeof obj !== "object") return obj;
94
+ const result = {};
95
+ for (const [key, value] of Object.entries(obj)) {
96
+ if (typeof value === "string") {
97
+ result[key] = redactSensitive(value);
98
+ } else if (Array.isArray(value)) {
99
+ result[key] = value.map((v) => (typeof v === "string" ? redactSensitive(v) : v));
100
+ } else if (typeof value === "object" && value !== null) {
101
+ result[key] = redactObject(value);
102
+ } else {
103
+ result[key] = value;
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+
109
+ // Compute SHA-256 hash
110
+ function computeHash(event) {
111
+ const payload = JSON.stringify({
112
+ id: event.id,
113
+ timestamp: event.timestamp,
114
+ actor: event.actor,
115
+ surface: event.surface,
116
+ action: event.action,
117
+ category: event.category,
118
+ target: event.target,
119
+ tier: event.tier,
120
+ result: event.result,
121
+ metadata: event.metadata,
122
+ prevHash: event.prevHash,
123
+ version: event.version,
124
+ });
125
+ return crypto.createHash("sha256").update(payload).digest("hex");
126
+ }
127
+
128
+ // Get audit file path
129
+ function getAuditFilePath(basePath = process.cwd()) {
130
+ return path.join(basePath, AUDIT_DIR, AUDIT_FILE);
131
+ }
132
+
133
+ // Ensure audit directory exists
134
+ function ensureAuditDir(basePath = process.cwd()) {
135
+ const dir = path.join(basePath, AUDIT_DIR);
136
+ if (!fs.existsSync(dir)) {
137
+ fs.mkdirSync(dir, { recursive: true });
138
+ }
139
+ }
140
+
141
+ // Get last hash from audit log
142
+ function getLastHash(basePath = process.cwd()) {
143
+ const filePath = getAuditFilePath(basePath);
144
+ if (!fs.existsSync(filePath)) {
145
+ return GENESIS_HASH;
146
+ }
147
+
148
+ const content = fs.readFileSync(filePath, "utf8");
149
+ const lines = content.split("\n").filter((line) => line.trim());
150
+ if (lines.length === 0) {
151
+ return GENESIS_HASH;
152
+ }
153
+
154
+ try {
155
+ const lastEvent = JSON.parse(lines[lines.length - 1]);
156
+ return lastEvent.hash || GENESIS_HASH;
157
+ } catch {
158
+ return GENESIS_HASH;
159
+ }
160
+ }
161
+
162
+ // Create audit event
163
+ function createEvent(input, prevHash) {
164
+ const tier = getCurrentTier();
165
+ const id = crypto.randomUUID();
166
+ const timestamp = new Date().toISOString();
167
+
168
+ const eventWithoutHash = {
169
+ id,
170
+ timestamp,
171
+ actor: input.actor || getCurrentActor(),
172
+ surface: input.surface,
173
+ action: input.action,
174
+ category: input.category,
175
+ target: input.target,
176
+ tier,
177
+ result: input.result,
178
+ metadata: redactMetadata(input.metadata, tier),
179
+ prevHash,
180
+ version: 1,
181
+ };
182
+
183
+ const hash = computeHash(eventWithoutHash);
184
+
185
+ return {
186
+ ...eventWithoutHash,
187
+ hash,
188
+ };
189
+ }
190
+
191
+ // Emit audit event
192
+ function emit(input, basePath = process.cwd()) {
193
+ try {
194
+ ensureAuditDir(basePath);
195
+ const prevHash = getLastHash(basePath);
196
+ const event = createEvent(input, prevHash);
197
+
198
+ const filePath = getAuditFilePath(basePath);
199
+ fs.appendFileSync(filePath, JSON.stringify(event) + "\n", "utf8");
200
+
201
+ return event;
202
+ } catch (err) {
203
+ // Silently fail - audit should not break main functionality
204
+ if (process.env.VIBECHECK_DEBUG) {
205
+ console.error("[audit] Failed to emit event:", err.message);
206
+ }
207
+ return null;
208
+ }
209
+ }
210
+
211
+ // Pre-defined actions
212
+ const AuditActions = {
213
+ SCAN_START: "scan.start",
214
+ SCAN_COMPLETE: "scan.complete",
215
+ SCAN_ERROR: "scan.error",
216
+ SHIP_CHECK: "ship.check",
217
+ SHIP_APPROVE: "ship.approve",
218
+ SHIP_REJECT: "ship.reject",
219
+ REALITY_START: "reality.start",
220
+ REALITY_COMPLETE: "reality.complete",
221
+ REALITY_ERROR: "reality.error",
222
+ AUTOPILOT_ENABLE: "autopilot.enable",
223
+ AUTOPILOT_DISABLE: "autopilot.disable",
224
+ AUTOPILOT_RUN: "autopilot.run",
225
+ AUTOPILOT_REPORT: "autopilot.report",
226
+ FIX_PLAN: "fix.plan",
227
+ FIX_APPLY: "fix.apply",
228
+ FIX_REVERT: "fix.revert",
229
+ GATE_CHECK: "gate.check",
230
+ GATE_PASS: "gate.pass",
231
+ GATE_FAIL: "gate.fail",
232
+ AUTH_LOGIN: "auth.login",
233
+ AUTH_LOGOUT: "auth.logout",
234
+ TOOL_INVOKE: "tool.invoke",
235
+ TOOL_COMPLETE: "tool.complete",
236
+ TOOL_ERROR: "tool.error",
237
+ };
238
+
239
+ // Convenience emitters
240
+ function emitScanStart(projectPath, args) {
241
+ return emit({
242
+ surface: "cli",
243
+ action: AuditActions.SCAN_START,
244
+ category: "scan",
245
+ target: { type: "project", path: projectPath },
246
+ result: "success",
247
+ metadata: { command: "scan", args, projectPath },
248
+ });
249
+ }
250
+
251
+ function emitScanComplete(projectPath, result, metadata) {
252
+ return emit({
253
+ surface: "cli",
254
+ action: AuditActions.SCAN_COMPLETE,
255
+ category: "scan",
256
+ target: { type: "project", path: projectPath },
257
+ result,
258
+ metadata: { command: "scan", projectPath, ...metadata },
259
+ });
260
+ }
261
+
262
+ function emitShipCheck(projectPath, result, metadata) {
263
+ return emit({
264
+ surface: "cli",
265
+ action: AuditActions.SHIP_CHECK,
266
+ category: "ship",
267
+ target: { type: "project", path: projectPath },
268
+ result,
269
+ metadata: { command: "ship", projectPath, ...metadata },
270
+ });
271
+ }
272
+
273
+ function emitRealityStart(url, flows) {
274
+ return emit({
275
+ surface: "cli",
276
+ action: AuditActions.REALITY_START,
277
+ category: "reality",
278
+ target: { type: "url", path: url },
279
+ result: "success",
280
+ metadata: { command: "reality", url, flows },
281
+ });
282
+ }
283
+
284
+ function emitRealityComplete(url, result, metadata) {
285
+ return emit({
286
+ surface: "cli",
287
+ action: AuditActions.REALITY_COMPLETE,
288
+ category: "reality",
289
+ target: { type: "url", path: url },
290
+ result,
291
+ metadata: { command: "reality", ...metadata },
292
+ });
293
+ }
294
+
295
+ function emitAutopilotAction(action, projectPath, result, metadata) {
296
+ const actionMap = {
297
+ enable: AuditActions.AUTOPILOT_ENABLE,
298
+ disable: AuditActions.AUTOPILOT_DISABLE,
299
+ run: AuditActions.AUTOPILOT_RUN,
300
+ report: AuditActions.AUTOPILOT_REPORT,
301
+ };
302
+
303
+ return emit({
304
+ surface: "cli",
305
+ action: actionMap[action] || action,
306
+ category: "autopilot",
307
+ target: { type: "project", path: projectPath },
308
+ result,
309
+ metadata: { command: "autopilot", projectPath, ...metadata },
310
+ });
311
+ }
312
+
313
+ function emitFixPlan(projectPath, result, metadata) {
314
+ return emit({
315
+ surface: "cli",
316
+ action: AuditActions.FIX_PLAN,
317
+ category: "fix",
318
+ target: { type: "project", path: projectPath },
319
+ result,
320
+ metadata: { command: "fix", projectPath, ...metadata },
321
+ });
322
+ }
323
+
324
+ function emitFixApply(projectPath, result, metadata) {
325
+ return emit({
326
+ surface: "cli",
327
+ action: AuditActions.FIX_APPLY,
328
+ category: "fix",
329
+ target: { type: "project", path: projectPath },
330
+ result,
331
+ metadata: { command: "fix", projectPath, ...metadata },
332
+ });
333
+ }
334
+
335
+ function emitGateCheck(projectPath, passed, metadata) {
336
+ return emit({
337
+ surface: "cli",
338
+ action: passed ? AuditActions.GATE_PASS : AuditActions.GATE_FAIL,
339
+ category: "gate",
340
+ target: { type: "project", path: projectPath },
341
+ result: passed ? "success" : "failure",
342
+ metadata: { command: "gate", projectPath, ...metadata },
343
+ });
344
+ }
345
+
346
+ function emitToolInvoke(surface, toolName, args, result, metadata) {
347
+ return emit({
348
+ surface,
349
+ action: AuditActions.TOOL_INVOKE,
350
+ category: "tool",
351
+ target: { type: "tool", name: toolName },
352
+ result,
353
+ metadata: { command: toolName, args, ...metadata },
354
+ });
355
+ }
356
+
357
+ function emitAuth(action, result, metadata) {
358
+ const actionMap = {
359
+ login: AuditActions.AUTH_LOGIN,
360
+ logout: AuditActions.AUTH_LOGOUT,
361
+ };
362
+
363
+ return emit({
364
+ surface: "cli",
365
+ action: actionMap[action] || action,
366
+ category: "auth",
367
+ target: { type: "auth" },
368
+ result,
369
+ metadata,
370
+ });
371
+ }
372
+
373
+ // Export the audit bridge
374
+ module.exports = {
375
+ emit,
376
+ AuditActions,
377
+ emitScanStart,
378
+ emitScanComplete,
379
+ emitShipCheck,
380
+ emitRealityStart,
381
+ emitRealityComplete,
382
+ emitAutopilotAction,
383
+ emitFixPlan,
384
+ emitFixApply,
385
+ emitGateCheck,
386
+ emitToolInvoke,
387
+ emitAuth,
388
+ getCurrentTier,
389
+ getCurrentActor,
390
+ getAuditFilePath,
391
+ };
@@ -0,0 +1,193 @@
1
+ // bin/runners/lib/auth-truth.js
2
+ // Auth Truth v1 - Detects auth/protection patterns in Next + Fastify codebases
3
+ const fg = require("fast-glob");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const crypto = require("crypto");
7
+
8
+ function sha256(text) {
9
+ return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
10
+ }
11
+
12
+ function safeRead(fileAbs) {
13
+ return fs.readFileSync(fileAbs, "utf8");
14
+ }
15
+
16
+ function evidenceFromLine({ fileAbs, repoRoot, lineNo, reason }) {
17
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
18
+ const lines = safeRead(fileAbs).split(/\r?\n/);
19
+ const idx = Math.max(0, Math.min(lines.length - 1, lineNo - 1));
20
+ const snippet = lines[idx] || "";
21
+ return {
22
+ id: `ev_${crypto.randomBytes(4).toString("hex")}`,
23
+ file: fileRel,
24
+ lines: `${lineNo}-${lineNo}`,
25
+ snippetHash: sha256(snippet),
26
+ reason
27
+ };
28
+ }
29
+
30
+ function findLineMatches(code, regex) {
31
+ const out = [];
32
+ const lines = code.split(/\r?\n/);
33
+ for (let i = 0; i < lines.length; i++) {
34
+ if (regex.test(lines[i])) out.push(i + 1);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function guessAuthSignalsFromCode(code) {
40
+ const signals = [];
41
+
42
+ const patterns = [
43
+ { key: "next_middleware", rx: /\bNextResponse\.(redirect|rewrite)\b/ },
44
+ { key: "next_auth", rx: /\bgetServerSession\b|\bNextAuth\b|\bauth\(\)\b/ },
45
+ { key: "clerk", rx: /\bclerkMiddleware\b|\bauthMiddleware\b|@clerk\/nextjs/ },
46
+ { key: "supabase", rx: /\bcreateRouteHandlerClient\b|\bcreateServerClient\b|@supabase/ },
47
+ { key: "jwt_verify", rx: /\b(jwtVerify|verifyJWT|verifyToken|authorization|bearer)\b/i },
48
+ { key: "session", rx: /\b(session|cookie|setCookie|getCookie)\b/i },
49
+ { key: "rbac", rx: /\b(role|roles|permissions|rbac|isAdmin|adminOnly)\b/i },
50
+ { key: "fastify_hook", rx: /\.addHook\(\s*['"](onRequest|preHandler|preValidation)['"]/ },
51
+ { key: "fastify_jwt", rx: /@fastify\/jwt|fastify-jwt|fastify\.jwt/i },
52
+ ];
53
+
54
+ for (const p of patterns) {
55
+ if (p.rx.test(code)) signals.push(p.key);
56
+ }
57
+ return Array.from(new Set(signals));
58
+ }
59
+
60
+ async function resolveNextMiddleware(repoRoot) {
61
+ const candidates = await fg(
62
+ ["middleware.@(ts|js)", "src/middleware.@(ts|js)"],
63
+ { cwd: repoRoot, absolute: true, onlyFiles: true }
64
+ );
65
+
66
+ const middlewares = [];
67
+
68
+ for (const fileAbs of candidates) {
69
+ const code = safeRead(fileAbs);
70
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
71
+
72
+ const matcherLines = findLineMatches(code, /\bmatcher\b/);
73
+ const redirectLines = findLineMatches(code, /\bNextResponse\.(redirect|rewrite)\b/);
74
+
75
+ const evidence = [];
76
+ for (const ln of matcherLines.slice(0, 5)) {
77
+ evidence.push(evidenceFromLine({ fileAbs, repoRoot, lineNo: ln, reason: "Next middleware matcher config" }));
78
+ }
79
+ for (const ln of redirectLines.slice(0, 5)) {
80
+ evidence.push(evidenceFromLine({ fileAbs, repoRoot, lineNo: ln, reason: "Next middleware redirect/rewrite" }));
81
+ }
82
+
83
+ const matcher = [];
84
+ const matcherBlock = code.match(/matcher\s*:\s*(\[[\s\S]*?\])/);
85
+ if (matcherBlock && matcherBlock[1]) {
86
+ const raw = matcherBlock[1];
87
+ const strings = Array.from(raw.matchAll(/['"`]([^'"`]+)['"`]/g)).map(m => m[1]);
88
+ matcher.push(...strings);
89
+ }
90
+
91
+ middlewares.push({
92
+ file: fileRel,
93
+ matcher,
94
+ signals: guessAuthSignalsFromCode(code),
95
+ evidence
96
+ });
97
+ }
98
+
99
+ return middlewares;
100
+ }
101
+
102
+ async function resolveFastifyAuthSignals(repoRoot, truthpackRoutes) {
103
+ const handlerFiles = new Set((truthpackRoutes || []).map(r => r.handler).filter(Boolean));
104
+ const signals = [];
105
+ const evidence = [];
106
+
107
+ for (const fileRel of handlerFiles) {
108
+ const fileAbs = path.join(repoRoot, fileRel);
109
+ if (!fs.existsSync(fileAbs)) continue;
110
+
111
+ const code = safeRead(fileAbs);
112
+ const sigs = guessAuthSignalsFromCode(code);
113
+ if (!sigs.length) continue;
114
+
115
+ for (const s of sigs) signals.push({ type: s, file: fileRel });
116
+
117
+ const authLinePatterns = [
118
+ { rx: /\.addHook\(\s*['"](onRequest|preHandler|preValidation)['"]/, reason: "Fastify hook likely used for auth" },
119
+ { rx: /\b(jwtVerify|authorization|bearer)\b/i, reason: "JWT/Authorization verification signal" },
120
+ { rx: /@fastify\/jwt|fastify\.jwt/i, reason: "Fastify JWT plugin signal" },
121
+ { rx: /\b(isAdmin|adminOnly|permissions|rbac)\b/i, reason: "RBAC/permissions signal" },
122
+ ];
123
+
124
+ const lines = code.split(/\r?\n/);
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const line = lines[i];
127
+ for (const p of authLinePatterns) {
128
+ if (p.rx.test(line)) {
129
+ evidence.push({
130
+ id: `ev_${crypto.randomBytes(4).toString("hex")}`,
131
+ file: fileRel,
132
+ lines: `${i + 1}-${i + 1}`,
133
+ snippetHash: sha256(line),
134
+ reason: p.reason
135
+ });
136
+ }
137
+ }
138
+ if (evidence.length > 30) break;
139
+ }
140
+ }
141
+
142
+ const uniqueTypes = Array.from(new Set(signals.map(s => s.type)));
143
+
144
+ return {
145
+ signalTypes: uniqueTypes,
146
+ signals,
147
+ evidence
148
+ };
149
+ }
150
+
151
+ function matcherCoversPath(matcherList, p) {
152
+ if (!Array.isArray(matcherList) || !matcherList.length) return false;
153
+ const pathStr = p.startsWith("/") ? p : `/${p}`;
154
+
155
+ return matcherList.some(m => {
156
+ if (!m) return false;
157
+
158
+ if (m.includes(":path*")) {
159
+ const prefix = m.split(":path*")[0].replace(/\/$/, "");
160
+ return pathStr.startsWith(prefix || "/");
161
+ }
162
+ if (m.includes("(.*)")) {
163
+ const prefix = m.split("(.*)")[0].replace(/\/$/, "");
164
+ return pathStr.startsWith(prefix || "/");
165
+ }
166
+
167
+ if (m === pathStr) return true;
168
+ if (pathStr.startsWith(m.endsWith("/") ? m : m + "/")) return true;
169
+
170
+ return false;
171
+ });
172
+ }
173
+
174
+ async function buildAuthTruth(repoRoot, routesServer) {
175
+ const middlewares = await resolveNextMiddleware(repoRoot);
176
+ const matchers = middlewares.flatMap(mw => mw.matcher || []);
177
+ const fastify = await resolveFastifyAuthSignals(repoRoot, routesServer);
178
+
179
+ return {
180
+ nextMiddleware: middlewares,
181
+ nextMatcherPatterns: matchers,
182
+ fastify,
183
+ helpers: {
184
+ matcherCoversPath: "runtime-only"
185
+ }
186
+ };
187
+ }
188
+
189
+ module.exports = {
190
+ buildAuthTruth,
191
+ matcherCoversPath,
192
+ guessAuthSignalsFromCode
193
+ };