@oculum/scanner 1.0.0

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 (281) hide show
  1. package/dist/formatters/cli-terminal.d.ts +27 -0
  2. package/dist/formatters/cli-terminal.d.ts.map +1 -0
  3. package/dist/formatters/cli-terminal.js +412 -0
  4. package/dist/formatters/cli-terminal.js.map +1 -0
  5. package/dist/formatters/github-comment.d.ts +41 -0
  6. package/dist/formatters/github-comment.d.ts.map +1 -0
  7. package/dist/formatters/github-comment.js +306 -0
  8. package/dist/formatters/github-comment.js.map +1 -0
  9. package/dist/formatters/grouping.d.ts +52 -0
  10. package/dist/formatters/grouping.d.ts.map +1 -0
  11. package/dist/formatters/grouping.js +152 -0
  12. package/dist/formatters/grouping.js.map +1 -0
  13. package/dist/formatters/index.d.ts +9 -0
  14. package/dist/formatters/index.d.ts.map +1 -0
  15. package/dist/formatters/index.js +35 -0
  16. package/dist/formatters/index.js.map +1 -0
  17. package/dist/formatters/vscode-diagnostic.d.ts +103 -0
  18. package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
  19. package/dist/formatters/vscode-diagnostic.js +151 -0
  20. package/dist/formatters/vscode-diagnostic.js.map +1 -0
  21. package/dist/index.d.ts +52 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +648 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/layer1/comments.d.ts +8 -0
  26. package/dist/layer1/comments.d.ts.map +1 -0
  27. package/dist/layer1/comments.js +203 -0
  28. package/dist/layer1/comments.js.map +1 -0
  29. package/dist/layer1/config-audit.d.ts +8 -0
  30. package/dist/layer1/config-audit.d.ts.map +1 -0
  31. package/dist/layer1/config-audit.js +252 -0
  32. package/dist/layer1/config-audit.js.map +1 -0
  33. package/dist/layer1/entropy.d.ts +8 -0
  34. package/dist/layer1/entropy.d.ts.map +1 -0
  35. package/dist/layer1/entropy.js +500 -0
  36. package/dist/layer1/entropy.js.map +1 -0
  37. package/dist/layer1/file-flags.d.ts +7 -0
  38. package/dist/layer1/file-flags.d.ts.map +1 -0
  39. package/dist/layer1/file-flags.js +112 -0
  40. package/dist/layer1/file-flags.js.map +1 -0
  41. package/dist/layer1/index.d.ts +36 -0
  42. package/dist/layer1/index.d.ts.map +1 -0
  43. package/dist/layer1/index.js +132 -0
  44. package/dist/layer1/index.js.map +1 -0
  45. package/dist/layer1/patterns.d.ts +8 -0
  46. package/dist/layer1/patterns.d.ts.map +1 -0
  47. package/dist/layer1/patterns.js +482 -0
  48. package/dist/layer1/patterns.js.map +1 -0
  49. package/dist/layer1/urls.d.ts +8 -0
  50. package/dist/layer1/urls.d.ts.map +1 -0
  51. package/dist/layer1/urls.js +296 -0
  52. package/dist/layer1/urls.js.map +1 -0
  53. package/dist/layer1/weak-crypto.d.ts +7 -0
  54. package/dist/layer1/weak-crypto.d.ts.map +1 -0
  55. package/dist/layer1/weak-crypto.js +291 -0
  56. package/dist/layer1/weak-crypto.js.map +1 -0
  57. package/dist/layer2/ai-agent-tools.d.ts +19 -0
  58. package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
  59. package/dist/layer2/ai-agent-tools.js +528 -0
  60. package/dist/layer2/ai-agent-tools.js.map +1 -0
  61. package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
  62. package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
  63. package/dist/layer2/ai-endpoint-protection.js +332 -0
  64. package/dist/layer2/ai-endpoint-protection.js.map +1 -0
  65. package/dist/layer2/ai-execution-sinks.d.ts +18 -0
  66. package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
  67. package/dist/layer2/ai-execution-sinks.js +496 -0
  68. package/dist/layer2/ai-execution-sinks.js.map +1 -0
  69. package/dist/layer2/ai-fingerprinting.d.ts +7 -0
  70. package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
  71. package/dist/layer2/ai-fingerprinting.js +654 -0
  72. package/dist/layer2/ai-fingerprinting.js.map +1 -0
  73. package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
  74. package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
  75. package/dist/layer2/ai-prompt-hygiene.js +356 -0
  76. package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
  77. package/dist/layer2/ai-rag-safety.d.ts +21 -0
  78. package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
  79. package/dist/layer2/ai-rag-safety.js +459 -0
  80. package/dist/layer2/ai-rag-safety.js.map +1 -0
  81. package/dist/layer2/ai-schema-validation.d.ts +25 -0
  82. package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
  83. package/dist/layer2/ai-schema-validation.js +375 -0
  84. package/dist/layer2/ai-schema-validation.js.map +1 -0
  85. package/dist/layer2/auth-antipatterns.d.ts +20 -0
  86. package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
  87. package/dist/layer2/auth-antipatterns.js +333 -0
  88. package/dist/layer2/auth-antipatterns.js.map +1 -0
  89. package/dist/layer2/byok-patterns.d.ts +12 -0
  90. package/dist/layer2/byok-patterns.d.ts.map +1 -0
  91. package/dist/layer2/byok-patterns.js +299 -0
  92. package/dist/layer2/byok-patterns.js.map +1 -0
  93. package/dist/layer2/dangerous-functions.d.ts +7 -0
  94. package/dist/layer2/dangerous-functions.d.ts.map +1 -0
  95. package/dist/layer2/dangerous-functions.js +1375 -0
  96. package/dist/layer2/dangerous-functions.js.map +1 -0
  97. package/dist/layer2/data-exposure.d.ts +16 -0
  98. package/dist/layer2/data-exposure.d.ts.map +1 -0
  99. package/dist/layer2/data-exposure.js +279 -0
  100. package/dist/layer2/data-exposure.js.map +1 -0
  101. package/dist/layer2/framework-checks.d.ts +7 -0
  102. package/dist/layer2/framework-checks.d.ts.map +1 -0
  103. package/dist/layer2/framework-checks.js +388 -0
  104. package/dist/layer2/framework-checks.js.map +1 -0
  105. package/dist/layer2/index.d.ts +58 -0
  106. package/dist/layer2/index.d.ts.map +1 -0
  107. package/dist/layer2/index.js +380 -0
  108. package/dist/layer2/index.js.map +1 -0
  109. package/dist/layer2/logic-gates.d.ts +7 -0
  110. package/dist/layer2/logic-gates.d.ts.map +1 -0
  111. package/dist/layer2/logic-gates.js +182 -0
  112. package/dist/layer2/logic-gates.js.map +1 -0
  113. package/dist/layer2/risky-imports.d.ts +7 -0
  114. package/dist/layer2/risky-imports.d.ts.map +1 -0
  115. package/dist/layer2/risky-imports.js +161 -0
  116. package/dist/layer2/risky-imports.js.map +1 -0
  117. package/dist/layer2/variables.d.ts +8 -0
  118. package/dist/layer2/variables.d.ts.map +1 -0
  119. package/dist/layer2/variables.js +152 -0
  120. package/dist/layer2/variables.js.map +1 -0
  121. package/dist/layer3/anthropic.d.ts +83 -0
  122. package/dist/layer3/anthropic.d.ts.map +1 -0
  123. package/dist/layer3/anthropic.js +1745 -0
  124. package/dist/layer3/anthropic.js.map +1 -0
  125. package/dist/layer3/index.d.ts +24 -0
  126. package/dist/layer3/index.d.ts.map +1 -0
  127. package/dist/layer3/index.js +119 -0
  128. package/dist/layer3/index.js.map +1 -0
  129. package/dist/layer3/openai.d.ts +25 -0
  130. package/dist/layer3/openai.d.ts.map +1 -0
  131. package/dist/layer3/openai.js +238 -0
  132. package/dist/layer3/openai.js.map +1 -0
  133. package/dist/layer3/package-check.d.ts +63 -0
  134. package/dist/layer3/package-check.d.ts.map +1 -0
  135. package/dist/layer3/package-check.js +508 -0
  136. package/dist/layer3/package-check.js.map +1 -0
  137. package/dist/modes/incremental.d.ts +66 -0
  138. package/dist/modes/incremental.d.ts.map +1 -0
  139. package/dist/modes/incremental.js +200 -0
  140. package/dist/modes/incremental.js.map +1 -0
  141. package/dist/tiers.d.ts +125 -0
  142. package/dist/tiers.d.ts.map +1 -0
  143. package/dist/tiers.js +234 -0
  144. package/dist/tiers.js.map +1 -0
  145. package/dist/types.d.ts +175 -0
  146. package/dist/types.d.ts.map +1 -0
  147. package/dist/types.js +50 -0
  148. package/dist/types.js.map +1 -0
  149. package/dist/utils/auth-helper-detector.d.ts +56 -0
  150. package/dist/utils/auth-helper-detector.d.ts.map +1 -0
  151. package/dist/utils/auth-helper-detector.js +360 -0
  152. package/dist/utils/auth-helper-detector.js.map +1 -0
  153. package/dist/utils/context-helpers.d.ts +96 -0
  154. package/dist/utils/context-helpers.d.ts.map +1 -0
  155. package/dist/utils/context-helpers.js +493 -0
  156. package/dist/utils/context-helpers.js.map +1 -0
  157. package/dist/utils/diff-detector.d.ts +53 -0
  158. package/dist/utils/diff-detector.d.ts.map +1 -0
  159. package/dist/utils/diff-detector.js +104 -0
  160. package/dist/utils/diff-detector.js.map +1 -0
  161. package/dist/utils/diff-parser.d.ts +80 -0
  162. package/dist/utils/diff-parser.d.ts.map +1 -0
  163. package/dist/utils/diff-parser.js +202 -0
  164. package/dist/utils/diff-parser.js.map +1 -0
  165. package/dist/utils/imported-auth-detector.d.ts +37 -0
  166. package/dist/utils/imported-auth-detector.d.ts.map +1 -0
  167. package/dist/utils/imported-auth-detector.js +251 -0
  168. package/dist/utils/imported-auth-detector.js.map +1 -0
  169. package/dist/utils/middleware-detector.d.ts +55 -0
  170. package/dist/utils/middleware-detector.d.ts.map +1 -0
  171. package/dist/utils/middleware-detector.js +260 -0
  172. package/dist/utils/middleware-detector.js.map +1 -0
  173. package/dist/utils/oauth-flow-detector.d.ts +41 -0
  174. package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
  175. package/dist/utils/oauth-flow-detector.js +202 -0
  176. package/dist/utils/oauth-flow-detector.js.map +1 -0
  177. package/dist/utils/path-exclusions.d.ts +55 -0
  178. package/dist/utils/path-exclusions.d.ts.map +1 -0
  179. package/dist/utils/path-exclusions.js +222 -0
  180. package/dist/utils/path-exclusions.js.map +1 -0
  181. package/dist/utils/project-context-builder.d.ts +119 -0
  182. package/dist/utils/project-context-builder.d.ts.map +1 -0
  183. package/dist/utils/project-context-builder.js +534 -0
  184. package/dist/utils/project-context-builder.js.map +1 -0
  185. package/dist/utils/registry-clients.d.ts +93 -0
  186. package/dist/utils/registry-clients.d.ts.map +1 -0
  187. package/dist/utils/registry-clients.js +273 -0
  188. package/dist/utils/registry-clients.js.map +1 -0
  189. package/dist/utils/trpc-analyzer.d.ts +78 -0
  190. package/dist/utils/trpc-analyzer.d.ts.map +1 -0
  191. package/dist/utils/trpc-analyzer.js +297 -0
  192. package/dist/utils/trpc-analyzer.js.map +1 -0
  193. package/package.json +45 -0
  194. package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
  195. package/src/__tests__/benchmark/fixtures/index.ts +68 -0
  196. package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
  197. package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
  198. package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
  199. package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
  200. package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
  201. package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
  202. package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
  203. package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
  204. package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
  205. package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
  206. package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
  207. package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
  208. package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
  209. package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
  210. package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
  211. package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
  212. package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
  213. package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
  214. package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
  215. package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
  216. package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
  217. package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
  218. package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
  219. package/src/__tests__/benchmark/index.ts +29 -0
  220. package/src/__tests__/benchmark/run-benchmark.ts +144 -0
  221. package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
  222. package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
  223. package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
  224. package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
  225. package/src/__tests__/benchmark/types.ts +144 -0
  226. package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
  227. package/src/__tests__/regression/known-false-positives.test.ts +467 -0
  228. package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
  229. package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
  230. package/src/__tests__/validation/analyze-results.ts +542 -0
  231. package/src/__tests__/validation/extract-for-triage.ts +146 -0
  232. package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
  233. package/src/__tests__/validation/run-validation.ts +364 -0
  234. package/src/__tests__/validation/triage-template.md +132 -0
  235. package/src/formatters/cli-terminal.ts +446 -0
  236. package/src/formatters/github-comment.ts +382 -0
  237. package/src/formatters/grouping.ts +190 -0
  238. package/src/formatters/index.ts +47 -0
  239. package/src/formatters/vscode-diagnostic.ts +243 -0
  240. package/src/index.ts +823 -0
  241. package/src/layer1/comments.ts +218 -0
  242. package/src/layer1/config-audit.ts +289 -0
  243. package/src/layer1/entropy.ts +583 -0
  244. package/src/layer1/file-flags.ts +127 -0
  245. package/src/layer1/index.ts +181 -0
  246. package/src/layer1/patterns.ts +516 -0
  247. package/src/layer1/urls.ts +334 -0
  248. package/src/layer1/weak-crypto.ts +328 -0
  249. package/src/layer2/ai-agent-tools.ts +601 -0
  250. package/src/layer2/ai-endpoint-protection.ts +387 -0
  251. package/src/layer2/ai-execution-sinks.ts +580 -0
  252. package/src/layer2/ai-fingerprinting.ts +758 -0
  253. package/src/layer2/ai-prompt-hygiene.ts +411 -0
  254. package/src/layer2/ai-rag-safety.ts +511 -0
  255. package/src/layer2/ai-schema-validation.ts +421 -0
  256. package/src/layer2/auth-antipatterns.ts +394 -0
  257. package/src/layer2/byok-patterns.ts +336 -0
  258. package/src/layer2/dangerous-functions.ts +1563 -0
  259. package/src/layer2/data-exposure.ts +315 -0
  260. package/src/layer2/framework-checks.ts +433 -0
  261. package/src/layer2/index.ts +473 -0
  262. package/src/layer2/logic-gates.ts +206 -0
  263. package/src/layer2/risky-imports.ts +186 -0
  264. package/src/layer2/variables.ts +166 -0
  265. package/src/layer3/anthropic.ts +2030 -0
  266. package/src/layer3/index.ts +130 -0
  267. package/src/layer3/package-check.ts +604 -0
  268. package/src/modes/incremental.ts +293 -0
  269. package/src/tiers.ts +318 -0
  270. package/src/types.ts +284 -0
  271. package/src/utils/auth-helper-detector.ts +443 -0
  272. package/src/utils/context-helpers.ts +535 -0
  273. package/src/utils/diff-detector.ts +135 -0
  274. package/src/utils/diff-parser.ts +272 -0
  275. package/src/utils/imported-auth-detector.ts +320 -0
  276. package/src/utils/middleware-detector.ts +333 -0
  277. package/src/utils/oauth-flow-detector.ts +246 -0
  278. package/src/utils/path-exclusions.ts +266 -0
  279. package/src/utils/project-context-builder.ts +707 -0
  280. package/src/utils/registry-clients.ts +351 -0
  281. package/src/utils/trpc-analyzer.ts +382 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Auth Helper Detector
3
+ *
4
+ * Detects authentication helper functions that throw on missing auth,
5
+ * return non-null types, and provide security guarantees.
6
+ *
7
+ * When these helpers are called, subsequent code is GUARANTEED to have
8
+ * an authenticated user - no additional `if (!userId)` checks are needed.
9
+ */
10
+
11
+ import type { ScanFile } from '../types'
12
+
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
17
+ export interface AuthHelper {
18
+ /** Name of the helper function */
19
+ name: string
20
+ /** File where it's defined (if detected) */
21
+ definedIn?: string
22
+ /** Whether it throws on missing auth (vs returning null) */
23
+ throwsOnMissing: boolean
24
+ /** Return type if detected (e.g., 'string', 'User', 'Promise<string>') */
25
+ returnType?: string
26
+ /** Whether return type is non-null */
27
+ returnsNonNull: boolean
28
+ /** Pattern that matches calls to this helper */
29
+ callPattern: RegExp
30
+ }
31
+
32
+ export interface AuthHelperContext {
33
+ /** All detected auth helpers */
34
+ helpers: AuthHelper[]
35
+ /** Whether the project has any throwing auth helpers */
36
+ hasThrowingHelpers: boolean
37
+ /** Summary for AI validation */
38
+ summary: string
39
+ }
40
+
41
+ // ============================================================================
42
+ // Well-Known Auth Helper Patterns
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Common patterns for auth helpers that THROW on missing auth
47
+ * These are functions that guarantee authenticated context after call
48
+ */
49
+ const THROWING_AUTH_HELPER_PATTERNS = [
50
+ // Generic patterns
51
+ {
52
+ namePattern: /^(get|fetch|require|ensure)(Current)?(User|UserId|Auth|Session|Principal)(Id)?$/i,
53
+ callPattern: /\b(get|fetch|require|ensure)(Current)?(User|UserId|Auth|Session|Principal)(Id)?\s*\(/gi,
54
+ description: 'Auth helper that retrieves authenticated user',
55
+ },
56
+ // Clerk patterns
57
+ {
58
+ namePattern: /^auth$/,
59
+ callPattern: /\bauth\s*\(\s*\)/gi,
60
+ description: 'Clerk auth() helper',
61
+ },
62
+ {
63
+ namePattern: /^currentUser$/,
64
+ callPattern: /\bcurrentUser\s*\(\s*\)/gi,
65
+ description: 'Clerk currentUser() helper',
66
+ },
67
+ // NextAuth patterns
68
+ {
69
+ namePattern: /^getServerSession$/,
70
+ callPattern: /\bgetServerSession\s*\(/gi,
71
+ description: 'NextAuth getServerSession()',
72
+ },
73
+ {
74
+ namePattern: /^getSession$/,
75
+ callPattern: /\bgetSession\s*\(/gi,
76
+ description: 'Session helper',
77
+ },
78
+ // Supabase patterns
79
+ {
80
+ namePattern: /^getUser$/,
81
+ callPattern: /\bsupabase\.auth\.getUser\s*\(/gi,
82
+ description: 'Supabase getUser()',
83
+ },
84
+ ]
85
+
86
+ /**
87
+ * Patterns that indicate a function THROWS on missing auth
88
+ */
89
+ const THROWING_INDICATORS = [
90
+ /throw\s+new\s+(Error|UnauthorizedError|AuthError|HttpException)/i,
91
+ /throw\s+.*401/i,
92
+ /throw\s+.*unauthorized/i,
93
+ /throw\s+.*unauthenticated/i,
94
+ /return\s+.*401/i,
95
+ /NextResponse\.json\s*\([^)]*401/i,
96
+ /res\.status\s*\(\s*401\s*\)/i,
97
+ /redirect\s*\(\s*['"`].*login/i,
98
+ ]
99
+
100
+ /**
101
+ * Patterns that indicate NON-NULL return type
102
+ */
103
+ const NON_NULL_RETURN_PATTERNS = [
104
+ /:\s*Promise<string>/i, // : Promise<string>
105
+ /:\s*string(?!\s*\|)/i, // : string (not string | null)
106
+ /:\s*Promise<User>/i, // : Promise<User>
107
+ /:\s*User(?!\s*\|)/i, // : User (not User | null)
108
+ /:\s*Promise<\w+>(?!\s*\|)/i, // : Promise<SomeType>
109
+ /\w+!$/i, // Non-null assertion in return
110
+ ]
111
+
112
+ // ============================================================================
113
+ // Detection Functions
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Detect auth helper functions in the codebase
118
+ */
119
+ export function detectAuthHelpers(files: ScanFile[]): AuthHelperContext {
120
+ const helpers: AuthHelper[] = []
121
+ const detectedNames = new Set<string>()
122
+
123
+ // First pass: find auth helper definitions
124
+ for (const file of files) {
125
+ // Skip non-code files
126
+ if (!/\.(ts|tsx|js|jsx)$/i.test(file.path)) continue
127
+
128
+ // Look for function definitions that look like auth helpers
129
+ const functionMatches = findAuthHelperDefinitions(file.content, file.path)
130
+ for (const helper of functionMatches) {
131
+ if (!detectedNames.has(helper.name)) {
132
+ detectedNames.add(helper.name)
133
+ helpers.push(helper)
134
+ }
135
+ }
136
+ }
137
+
138
+ // Add well-known helpers if not already detected
139
+ for (const pattern of THROWING_AUTH_HELPER_PATTERNS) {
140
+ const nameMatch = pattern.namePattern.source.replace(/[\^\$\\b]/g, '')
141
+ if (!detectedNames.has(nameMatch)) {
142
+ // Check if this pattern is used in any file
143
+ const isUsed = files.some(f => pattern.callPattern.test(f.content))
144
+ if (isUsed) {
145
+ helpers.push({
146
+ name: nameMatch,
147
+ throwsOnMissing: true, // Assume throwing for well-known patterns
148
+ returnsNonNull: true,
149
+ callPattern: pattern.callPattern,
150
+ })
151
+ }
152
+ }
153
+ }
154
+
155
+ // Generate summary
156
+ const throwingHelpers = helpers.filter(h => h.throwsOnMissing)
157
+ const summary = generateAuthHelperSummary(helpers)
158
+
159
+ return {
160
+ helpers,
161
+ hasThrowingHelpers: throwingHelpers.length > 0,
162
+ summary,
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Find auth helper function definitions in a file
168
+ */
169
+ function findAuthHelperDefinitions(content: string, filePath: string): AuthHelper[] {
170
+ const helpers: AuthHelper[] = []
171
+ const lines = content.split('\n')
172
+
173
+ // Patterns for function definitions
174
+ const funcDefPatterns = [
175
+ // async function getCurrentUserId(): Promise<string> { ... throw
176
+ /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*([^{]+))?\s*\{/gi,
177
+ // const getCurrentUserId = async (): Promise<string> => { ... throw
178
+ /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*([^=]+))?\s*=>/gi,
179
+ ]
180
+
181
+ for (let i = 0; i < lines.length; i++) {
182
+ const line = lines[i]
183
+
184
+ for (const pattern of funcDefPatterns) {
185
+ pattern.lastIndex = 0
186
+ const match = pattern.exec(line)
187
+ if (!match) continue
188
+
189
+ const funcName = match[1]
190
+ const returnType = match[2]?.trim()
191
+
192
+ // Check if this looks like an auth helper by name
193
+ const isAuthHelperName = THROWING_AUTH_HELPER_PATTERNS.some(p =>
194
+ p.namePattern.test(funcName)
195
+ )
196
+
197
+ if (!isAuthHelperName) continue
198
+
199
+ // Look ahead for throwing patterns and return type
200
+ const functionBody = extractFunctionBody(lines, i)
201
+ const throwsOnMissing = THROWING_INDICATORS.some(p => p.test(functionBody))
202
+ const returnsNonNull = returnType
203
+ ? NON_NULL_RETURN_PATTERNS.some(p => p.test(`: ${returnType}`))
204
+ : false
205
+
206
+ // Create call pattern for this helper
207
+ const callPattern = new RegExp(`\\b${escapeRegex(funcName)}\\s*\\(`, 'gi')
208
+
209
+ helpers.push({
210
+ name: funcName,
211
+ definedIn: filePath,
212
+ throwsOnMissing,
213
+ returnType,
214
+ returnsNonNull: returnsNonNull || throwsOnMissing, // If it throws, the return is non-null
215
+ callPattern,
216
+ })
217
+ }
218
+ }
219
+
220
+ return helpers
221
+ }
222
+
223
+ /**
224
+ * Extract function body for analysis (up to closing brace)
225
+ */
226
+ function extractFunctionBody(lines: string[], startLine: number, maxLines: number = 50): string {
227
+ let braceCount = 0
228
+ let started = false
229
+ const bodyLines: string[] = []
230
+
231
+ for (let i = startLine; i < Math.min(lines.length, startLine + maxLines); i++) {
232
+ const line = lines[i]
233
+ bodyLines.push(line)
234
+
235
+ for (const char of line) {
236
+ if (char === '{') {
237
+ braceCount++
238
+ started = true
239
+ } else if (char === '}') {
240
+ braceCount--
241
+ if (started && braceCount === 0) {
242
+ return bodyLines.join('\n')
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ return bodyLines.join('\n')
249
+ }
250
+
251
+ /**
252
+ * Check if a code line uses a throwing auth helper
253
+ * Returns the helper if found, undefined otherwise
254
+ */
255
+ export function usesThrowingAuthHelper(
256
+ lineContent: string,
257
+ surroundingContent: string,
258
+ helpers: AuthHelper[]
259
+ ): AuthHelper | undefined {
260
+ // Check if any throwing helper is called in the surrounding context
261
+ const throwingHelpers = helpers.filter(h => h.throwsOnMissing)
262
+
263
+ for (const helper of throwingHelpers) {
264
+ helper.callPattern.lastIndex = 0
265
+ if (helper.callPattern.test(surroundingContent)) {
266
+ return helper
267
+ }
268
+ }
269
+
270
+ return undefined
271
+ }
272
+
273
+ /**
274
+ * Well-known auth helper call patterns - used as fallback when no helpers are detected
275
+ * These patterns are common across frameworks and should be recognized even without project analysis
276
+ */
277
+ const WELL_KNOWN_AUTH_CALL_PATTERNS: Array<{ pattern: RegExp; name: string }> = [
278
+ // Generic throwing auth patterns
279
+ { pattern: /\bgetCurrentUserId\s*\(/i, name: 'getCurrentUserId' },
280
+ { pattern: /\bgetCurrentUser\s*\(/i, name: 'getCurrentUser' },
281
+ { pattern: /\brequireAuth\s*\(/i, name: 'requireAuth' },
282
+ { pattern: /\brequireUser\s*\(/i, name: 'requireUser' },
283
+ { pattern: /\bensureAuth\s*\(/i, name: 'ensureAuth' },
284
+ { pattern: /\bensureAuthenticated\s*\(/i, name: 'ensureAuthenticated' },
285
+ { pattern: /\bverifyAuth\s*\(/i, name: 'verifyAuth' },
286
+ { pattern: /\bcheckAuth\s*\(/i, name: 'checkAuth' },
287
+ { pattern: /\bvalidateAuth\s*\(/i, name: 'validateAuth' },
288
+ { pattern: /\bassertAuth\s*\(/i, name: 'assertAuth' },
289
+ { pattern: /\bgetAuth\s*\(/i, name: 'getAuth' },
290
+ { pattern: /\bfetchCurrentUser\s*\(/i, name: 'fetchCurrentUser' },
291
+ // Clerk
292
+ { pattern: /\bauth\s*\(\s*\)\.protect\s*\(/i, name: 'auth().protect' },
293
+ { pattern: /\bcurrentUser\s*\(\s*\)/i, name: 'currentUser' },
294
+ // NextAuth
295
+ { pattern: /\bgetServerSession\s*\(/i, name: 'getServerSession' },
296
+ // Supabase
297
+ { pattern: /\bsupabase\.auth\.getUser\s*\(/i, name: 'supabase.auth.getUser' },
298
+ // Destructuring pattern
299
+ { pattern: /const\s+\{\s*user\s*\}\s*=\s*await\s+auth/i, name: 'destructured auth' },
300
+ ]
301
+
302
+ /**
303
+ * Check if a file has auth helper calls before a given line
304
+ * This indicates the code after the call is in authenticated context
305
+ */
306
+ export function hasAuthHelperCallBefore(
307
+ content: string,
308
+ lineNumber: number,
309
+ helpers: AuthHelper[]
310
+ ): { hasCall: boolean; helper?: AuthHelper; callLine?: number } {
311
+ const lines = content.split('\n')
312
+ const throwingHelpers = helpers.filter(h => h.throwsOnMissing)
313
+
314
+ // Increase search window from 30 to 100 lines for better coverage
315
+ const searchStart = Math.max(0, lineNumber - 100)
316
+
317
+ // Look backwards from the current line
318
+ for (let i = lineNumber - 1; i >= searchStart; i--) {
319
+ const line = lines[i]
320
+
321
+ // Check detected helpers first
322
+ for (const helper of throwingHelpers) {
323
+ helper.callPattern.lastIndex = 0
324
+ if (helper.callPattern.test(line)) {
325
+ return { hasCall: true, helper, callLine: i + 1 }
326
+ }
327
+ }
328
+
329
+ // Stop at function boundaries (for module-level helpers, we still check inside function)
330
+ if (/\b(function|async function|=>|export\s+default)\b/.test(line) && /\{/.test(line)) {
331
+ // If this is the route handler definition line, continue checking inside it
332
+ if (i !== lineNumber - 1) {
333
+ break
334
+ }
335
+ }
336
+ }
337
+
338
+ // FORWARD SEARCH: Always search forward into the function body
339
+ // This catches auth helpers called INSIDE the route handler, not just before it
340
+ // Example: export async function GET() { const userId = await getCurrentUserId(); ... }
341
+ const endLine = Math.min(lines.length, lineNumber + 30)
342
+ for (let i = lineNumber; i < endLine; i++) {
343
+ const line = lines[i]
344
+
345
+ // Check detected helpers
346
+ for (const helper of throwingHelpers) {
347
+ helper.callPattern.lastIndex = 0
348
+ if (helper.callPattern.test(line)) {
349
+ return { hasCall: true, helper, callLine: i + 1 }
350
+ }
351
+ }
352
+
353
+ // Also check well-known patterns (fallback for single-file scans)
354
+ for (const known of WELL_KNOWN_AUTH_CALL_PATTERNS) {
355
+ if (known.pattern.test(line)) {
356
+ return {
357
+ hasCall: true,
358
+ helper: {
359
+ name: known.name,
360
+ throwsOnMissing: true,
361
+ returnsNonNull: true,
362
+ callPattern: known.pattern,
363
+ },
364
+ callLine: i + 1
365
+ }
366
+ }
367
+ }
368
+
369
+ // Stop at another function boundary (but not the current line)
370
+ if (i > lineNumber && /\bexport\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/i.test(line)) {
371
+ break
372
+ }
373
+ }
374
+
375
+ return { hasCall: false }
376
+ }
377
+
378
+ /**
379
+ * Generate summary for AI validation
380
+ */
381
+ function generateAuthHelperSummary(helpers: AuthHelper[]): string {
382
+ if (helpers.length === 0) {
383
+ return 'No auth helper functions detected.'
384
+ }
385
+
386
+ const throwing = helpers.filter(h => h.throwsOnMissing)
387
+ const lines: string[] = []
388
+
389
+ lines.push('### Auth Helper Functions')
390
+ lines.push('')
391
+
392
+ if (throwing.length > 0) {
393
+ lines.push('**Throwing auth helpers** (guarantee authenticated context when called):')
394
+ for (const h of throwing) {
395
+ const location = h.definedIn ? ` (defined in ${h.definedIn})` : ''
396
+ lines.push(`- \`${h.name}()\`${location}`)
397
+ }
398
+ lines.push('')
399
+ lines.push('When these helpers are called at the start of a function, subsequent code is GUARANTEED to have an authenticated user. Do NOT flag "missing auth" or suggest `if (!userId)` checks after these calls.')
400
+ }
401
+
402
+ return lines.join('\n')
403
+ }
404
+
405
+ /**
406
+ * Escape special regex characters
407
+ */
408
+ function escapeRegex(str: string): string {
409
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
410
+ }
411
+
412
+ /**
413
+ * Check if code suggests user ID is already validated
414
+ * Detects patterns like: const userId = await getCurrentUserId()
415
+ */
416
+ export function isUserIdAlreadyValidated(
417
+ content: string,
418
+ lineNumber: number,
419
+ helpers: AuthHelper[]
420
+ ): boolean {
421
+ const lines = content.split('\n')
422
+ const contextStart = Math.max(0, lineNumber - 20)
423
+ const context = lines.slice(contextStart, lineNumber).join('\n')
424
+
425
+ // Check for throwing helper calls
426
+ const throwingHelpers = helpers.filter(h => h.throwsOnMissing)
427
+ for (const helper of throwingHelpers) {
428
+ helper.callPattern.lastIndex = 0
429
+ if (helper.callPattern.test(context)) {
430
+ return true
431
+ }
432
+ }
433
+
434
+ // Check for common validation patterns
435
+ const validationPatterns = [
436
+ /const\s+(?:userId|user_id|currentUserId)\s*=\s*await/i,
437
+ /if\s*\(\s*!(?:userId|user_id|user|session)\s*\)/i, // Already has the check
438
+ /(?:userId|user_id)\s*\|\|\s*throw/i,
439
+ /auth\(\)\.protect\(\)/i, // Clerk protect
440
+ ]
441
+
442
+ return validationPatterns.some(p => p.test(context))
443
+ }