@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,351 @@
1
+ /**
2
+ * Registry Clients for Package Metadata
3
+ * Fetches package information from npm and PyPI registries
4
+ * Used by the Hallucination Firewall (Story C) to assess dependency risk
5
+ */
6
+
7
+ // Cache for package metadata to avoid repeated requests
8
+ const npmMetadataCache = new Map<string, NPMPackageMetadata | null>()
9
+ const pypiMetadataCache = new Map<string, PyPIPackageMetadata | null>()
10
+
11
+ // Rate limiting configuration
12
+ const RATE_LIMIT_DELAY_MS = 100
13
+
14
+ /**
15
+ * NPM Package Metadata Interface
16
+ */
17
+ export interface NPMPackageMetadata {
18
+ name: string
19
+ version: string
20
+ description?: string
21
+ maintainers: Array<{ name: string; email?: string }>
22
+ time: {
23
+ created: string
24
+ modified: string
25
+ [version: string]: string
26
+ }
27
+ repository?: {
28
+ type: string
29
+ url: string
30
+ }
31
+ homepage?: string
32
+ license?: string
33
+ downloads?: {
34
+ weekly: number
35
+ }
36
+ }
37
+
38
+ /**
39
+ * PyPI Package Metadata Interface
40
+ */
41
+ export interface PyPIPackageMetadata {
42
+ name: string
43
+ version: string
44
+ summary?: string
45
+ author?: string
46
+ authorEmail?: string
47
+ license?: string
48
+ projectUrls?: Record<string, string>
49
+ releaseDate?: string
50
+ requiresPython?: string
51
+ }
52
+
53
+ /**
54
+ * Extracted dependency information
55
+ */
56
+ export interface ExtractedDependency {
57
+ name: string
58
+ version?: string
59
+ source: 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'requirements'
60
+ line: number
61
+ }
62
+
63
+ /**
64
+ * Fetch package metadata from npm registry
65
+ * Returns null if package doesn't exist
66
+ */
67
+ export async function fetchNPMMetadata(packageName: string): Promise<NPMPackageMetadata | null> {
68
+ // Check cache first
69
+ if (npmMetadataCache.has(packageName)) {
70
+ return npmMetadataCache.get(packageName) || null
71
+ }
72
+
73
+ try {
74
+ // Fetch package info from npm registry
75
+ const registryResponse = await fetch(
76
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
77
+ {
78
+ headers: {
79
+ 'Accept': 'application/json',
80
+ },
81
+ }
82
+ )
83
+
84
+ if (!registryResponse.ok) {
85
+ if (registryResponse.status === 404) {
86
+ npmMetadataCache.set(packageName, null)
87
+ return null
88
+ }
89
+ // Non-404 error - don't cache, might be transient
90
+ console.warn(`[Registry] npm registry error for ${packageName}: ${registryResponse.status}`)
91
+ return null
92
+ }
93
+
94
+ const data = await registryResponse.json() as Record<string, unknown>
95
+
96
+ // Fetch download counts separately (different API)
97
+ let weeklyDownloads = 0
98
+ try {
99
+ const downloadsResponse = await fetch(
100
+ `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`
101
+ )
102
+ if (downloadsResponse.ok) {
103
+ const downloadsData = await downloadsResponse.json() as Record<string, unknown>
104
+ weeklyDownloads = (downloadsData.downloads as number) || 0
105
+ }
106
+ } catch {
107
+ // Download count is optional, don't fail
108
+ }
109
+
110
+ const metadata: NPMPackageMetadata = {
111
+ name: data.name as string,
112
+ version: ((data['dist-tags'] as Record<string, string>)?.latest) || '',
113
+ description: data.description as string | undefined,
114
+ maintainers: (data.maintainers as Array<{ name: string; email?: string }>) || [],
115
+ time: (data.time as NPMPackageMetadata['time']) || { created: '', modified: '' },
116
+ repository: data.repository as NPMPackageMetadata['repository'],
117
+ homepage: data.homepage as string | undefined,
118
+ license: data.license as string | undefined,
119
+ downloads: { weekly: weeklyDownloads },
120
+ }
121
+
122
+ npmMetadataCache.set(packageName, metadata)
123
+ return metadata
124
+ } catch (error) {
125
+ console.warn(`[Registry] Failed to fetch npm metadata for ${packageName}:`, error)
126
+ // Don't cache network errors
127
+ return null
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Fetch package metadata from PyPI registry
133
+ * Returns null if package doesn't exist
134
+ */
135
+ export async function fetchPyPIMetadata(packageName: string): Promise<PyPIPackageMetadata | null> {
136
+ // Check cache first
137
+ if (pypiMetadataCache.has(packageName)) {
138
+ return pypiMetadataCache.get(packageName) || null
139
+ }
140
+
141
+ try {
142
+ const response = await fetch(
143
+ `https://pypi.org/pypi/${encodeURIComponent(packageName)}/json`,
144
+ {
145
+ headers: {
146
+ 'Accept': 'application/json',
147
+ },
148
+ }
149
+ )
150
+
151
+ if (!response.ok) {
152
+ if (response.status === 404) {
153
+ pypiMetadataCache.set(packageName, null)
154
+ return null
155
+ }
156
+ console.warn(`[Registry] PyPI registry error for ${packageName}: ${response.status}`)
157
+ return null
158
+ }
159
+
160
+ const data = await response.json() as Record<string, unknown>
161
+ const info = (data.info || {}) as Record<string, unknown>
162
+
163
+ // Get release date from the latest version
164
+ let releaseDate: string | undefined
165
+ const releases = (data.releases as Record<string, Array<{ upload_time?: string }>>)?.[info.version as string]
166
+ if (releases && releases.length > 0) {
167
+ releaseDate = releases[0].upload_time
168
+ }
169
+
170
+ const metadata: PyPIPackageMetadata = {
171
+ name: info.name as string,
172
+ version: info.version as string,
173
+ summary: info.summary as string | undefined,
174
+ author: info.author as string | undefined,
175
+ authorEmail: info.author_email as string | undefined,
176
+ license: info.license as string | undefined,
177
+ projectUrls: info.project_urls as Record<string, string> | undefined,
178
+ releaseDate,
179
+ requiresPython: info.requires_python as string | undefined,
180
+ }
181
+
182
+ pypiMetadataCache.set(packageName, metadata)
183
+ return metadata
184
+ } catch (error) {
185
+ console.warn(`[Registry] Failed to fetch PyPI metadata for ${packageName}:`, error)
186
+ return null
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Extract dependencies from package.json content
192
+ */
193
+ export function extractNpmDependencies(content: string): ExtractedDependency[] {
194
+ try {
195
+ const pkg = JSON.parse(content)
196
+ const deps: ExtractedDependency[] = []
197
+ const lines = content.split('\n')
198
+
199
+ const depSections = [
200
+ { key: 'dependencies', source: 'dependencies' as const },
201
+ { key: 'devDependencies', source: 'devDependencies' as const },
202
+ { key: 'peerDependencies', source: 'peerDependencies' as const },
203
+ { key: 'optionalDependencies', source: 'optionalDependencies' as const },
204
+ ]
205
+
206
+ for (const { key, source } of depSections) {
207
+ const depsObj = pkg[key]
208
+ if (!depsObj || typeof depsObj !== 'object') continue
209
+
210
+ for (const [name, version] of Object.entries(depsObj)) {
211
+ // Find the line number for this dependency
212
+ const lineIndex = lines.findIndex(l => l.includes(`"${name}"`))
213
+ deps.push({
214
+ name,
215
+ version: version as string,
216
+ source,
217
+ line: lineIndex >= 0 ? lineIndex + 1 : 1,
218
+ })
219
+ }
220
+ }
221
+
222
+ return deps
223
+ } catch {
224
+ return []
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Extract dependencies from requirements.txt content
230
+ */
231
+ export function extractPythonRequirements(content: string): ExtractedDependency[] {
232
+ const deps: ExtractedDependency[] = []
233
+ const lines = content.split('\n')
234
+
235
+ lines.forEach((line, index) => {
236
+ // Skip comments and empty lines
237
+ const trimmed = line.trim()
238
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
239
+ return
240
+ }
241
+
242
+ // Parse package name and optional version
243
+ // Formats: package, package==1.0.0, package>=1.0.0, package[extra], etc.
244
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:[=<>~!]+(.+))?/)
245
+ if (match) {
246
+ deps.push({
247
+ name: match[1],
248
+ version: match[2],
249
+ source: 'requirements',
250
+ line: index + 1,
251
+ })
252
+ }
253
+ })
254
+
255
+ return deps
256
+ }
257
+
258
+ /**
259
+ * Extract dependencies from pyproject.toml content
260
+ */
261
+ export function extractPyprojectDependencies(content: string): ExtractedDependency[] {
262
+ const deps: ExtractedDependency[] = []
263
+ const lines = content.split('\n')
264
+
265
+ let inDependencies = false
266
+
267
+ lines.forEach((line, index) => {
268
+ const trimmed = line.trim()
269
+
270
+ // Check for dependencies section
271
+ if (trimmed === '[project.dependencies]' || trimmed === 'dependencies = [') {
272
+ inDependencies = true
273
+ return
274
+ }
275
+
276
+ // Exit dependencies section
277
+ if (inDependencies && (trimmed.startsWith('[') || trimmed === ']')) {
278
+ inDependencies = false
279
+ return
280
+ }
281
+
282
+ if (inDependencies) {
283
+ // Parse dependency line: "package>=1.0.0", or package = ">=1.0.0"
284
+ const match = trimmed.match(/^["']?([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:[=<>~!]+)?/)
285
+ if (match && match[1]) {
286
+ deps.push({
287
+ name: match[1],
288
+ version: undefined,
289
+ source: 'requirements',
290
+ line: index + 1,
291
+ })
292
+ }
293
+ }
294
+ })
295
+
296
+ return deps
297
+ }
298
+
299
+ /**
300
+ * Determine the package file type from path
301
+ */
302
+ export function getPackageFileType(filePath: string): 'npm' | 'python' | null {
303
+ const fileName = filePath.split('/').pop()?.toLowerCase() || ''
304
+
305
+ if (fileName === 'package.json' ||
306
+ fileName === 'package-lock.json' ||
307
+ fileName === 'yarn.lock' ||
308
+ fileName === 'pnpm-lock.yaml') {
309
+ return 'npm'
310
+ }
311
+
312
+ if (fileName === 'requirements.txt' ||
313
+ fileName === 'pyproject.toml' ||
314
+ fileName === 'pipfile' ||
315
+ fileName === 'pipfile.lock') {
316
+ return 'python'
317
+ }
318
+
319
+ return null
320
+ }
321
+
322
+ /**
323
+ * Calculate package age in days from creation date
324
+ */
325
+ export function calculatePackageAgeDays(createdDate: string | undefined): number {
326
+ if (!createdDate) return Infinity // Unknown age, treat as old (safe)
327
+
328
+ try {
329
+ const created = new Date(createdDate)
330
+ const now = new Date()
331
+ const diffMs = now.getTime() - created.getTime()
332
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24))
333
+ } catch {
334
+ return Infinity
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Rate limiter helper - adds delay between registry requests
340
+ */
341
+ export async function rateLimitDelay(): Promise<void> {
342
+ return new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY_MS))
343
+ }
344
+
345
+ /**
346
+ * Clear all caches (useful for testing)
347
+ */
348
+ export function clearRegistryCaches(): void {
349
+ npmMetadataCache.clear()
350
+ pypiMetadataCache.clear()
351
+ }