opmsec 0.1.3 → 0.1.5

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 (166) hide show
  1. package/.env.example +1 -0
  2. package/.husky/pre-commit +1 -0
  3. package/README.md +71 -275
  4. package/bun.lock +5 -5
  5. package/docs/architecture/agents.mdx +11 -59
  6. package/docs/architecture/benchmarks.mdx +20 -46
  7. package/docs/architecture/overview.mdx +31 -38
  8. package/docs/architecture/scanner.mdx +11 -37
  9. package/docs/cli/audit.mdx +9 -12
  10. package/docs/cli/check.mdx +12 -26
  11. package/docs/cli/fix.mdx +10 -30
  12. package/docs/cli/info.mdx +12 -19
  13. package/docs/cli/install.mdx +27 -39
  14. package/docs/cli/push.mdx +40 -57
  15. package/docs/cli/register-agent.mdx +21 -53
  16. package/docs/cli/view.mdx +12 -29
  17. package/docs/concepts/ens-records.mdx +44 -0
  18. package/docs/concepts/multi-agent-consensus.mdx +18 -36
  19. package/docs/concepts/on-chain-registry.mdx +22 -49
  20. package/docs/concepts/security-model.mdx +20 -52
  21. package/docs/concepts/zk-agent-verification.mdx +26 -64
  22. package/docs/contract/events.mdx +13 -74
  23. package/docs/contract/functions.mdx +40 -126
  24. package/docs/contract/overview.mdx +17 -36
  25. package/docs/introduction.mdx +22 -25
  26. package/docs/mint.json +3 -2
  27. package/docs/quickstart.mdx +34 -70
  28. package/docs/system-design.png +0 -0
  29. package/package.json +7 -6
  30. package/packages/cli/src/commands/author-view.tsx +87 -2
  31. package/packages/cli/src/commands/check.tsx +18 -5
  32. package/packages/cli/src/commands/fix.tsx +25 -12
  33. package/packages/cli/src/commands/info.tsx +92 -4
  34. package/packages/cli/src/commands/install.tsx +327 -23
  35. package/packages/cli/src/commands/push.tsx +112 -0
  36. package/packages/cli/src/commands/register-agent.tsx +72 -31
  37. package/packages/cli/src/index.tsx +7 -5
  38. package/packages/cli/src/services/ens-records.ts +525 -0
  39. package/packages/cli/src/services/version.ts +156 -5
  40. package/packages/core/src/benchmarks.ts +116 -0
  41. package/packages/core/src/constants.ts +18 -6
  42. package/packages/core/src/model-rankings.ts +40 -15
  43. package/packages/core/src/types.ts +10 -0
  44. package/packages/core/src/utils.ts +136 -1
  45. package/packages/scanner/src/index.ts +2 -1
  46. package/packages/scanner/src/queue/memory-queue.ts +7 -2
  47. package/packages/scanner/src/services/benchmark-runner.ts +86 -1
  48. package/packages/scanner/src/services/fileverse.ts +61 -12
  49. package/packages/scanner/src/services/openrouter.ts +18 -7
  50. package/packages/web/.next/BUILD_ID +1 -0
  51. package/packages/web/.next/app-path-routes-manifest.json +4 -0
  52. package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
  53. package/packages/web/.next/diagnostics/framework.json +1 -0
  54. package/packages/web/.next/export-marker.json +6 -0
  55. package/packages/web/.next/images-manifest.json +58 -0
  56. package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
  57. package/packages/web/.next/next-server.js.nft.json +1 -0
  58. package/packages/web/.next/prerender-manifest.json +54 -4
  59. package/packages/web/.next/required-server-files.json +320 -0
  60. package/packages/web/.next/routes-manifest.json +53 -1
  61. package/packages/web/.next/server/app/_not-found/page.js +2 -0
  62. package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  63. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  64. package/packages/web/.next/server/app/_not-found.html +1 -0
  65. package/packages/web/.next/server/app/_not-found.meta +8 -0
  66. package/packages/web/.next/server/app/_not-found.rsc +18 -0
  67. package/packages/web/.next/server/app/index.html +6 -0
  68. package/packages/web/.next/server/app/index.meta +7 -0
  69. package/packages/web/.next/server/app/index.rsc +22 -0
  70. package/packages/web/.next/server/app/page.js +24 -24
  71. package/packages/web/.next/server/app/page.js.nft.json +1 -0
  72. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/packages/web/.next/server/chunks/611.js +6 -0
  74. package/packages/web/.next/server/chunks/778.js +30 -0
  75. package/packages/web/.next/server/functions-config-manifest.json +4 -0
  76. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -1
  77. package/packages/web/.next/server/next-font-manifest.js +1 -1
  78. package/packages/web/.next/server/next-font-manifest.json +1 -1
  79. package/packages/web/.next/server/pages/404.html +1 -0
  80. package/packages/web/.next/server/pages/500.html +1 -0
  81. package/packages/web/.next/server/pages/_app.js +1 -0
  82. package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
  83. package/packages/web/.next/server/pages/_document.js +1 -0
  84. package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
  85. package/packages/web/.next/server/pages/_error.js +19 -0
  86. package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
  87. package/packages/web/.next/server/webpack-runtime.js +2 -2
  88. package/packages/web/.next/static/0esGzFBCzREfVwijEGDfL/_buildManifest.js +1 -0
  89. package/packages/web/.next/static/0esGzFBCzREfVwijEGDfL/_ssgManifest.js +1 -0
  90. package/packages/web/.next/static/chunks/174-5b5efcb3b8efcc01.js +1 -0
  91. package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
  92. package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
  93. package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
  94. package/packages/web/.next/static/chunks/app/layout-de8e841104500505.js +1 -0
  95. package/packages/web/.next/static/chunks/app/layout.js +37 -7
  96. package/packages/web/.next/static/chunks/app/page-7e086379698b9fb0.js +1 -0
  97. package/packages/web/.next/static/chunks/app/page.js +297 -1
  98. package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
  99. package/packages/web/.next/static/chunks/main-4e8d71b5ef7ee7e3.js +1 -0
  100. package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
  101. package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  102. package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  103. package/packages/web/.next/static/chunks/webpack-0dcd67569eb46132.js +1 -0
  104. package/packages/web/.next/static/chunks/webpack.js +2 -2
  105. package/packages/web/.next/static/css/102562cf2d0ae9b0.css +3 -0
  106. package/packages/web/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  107. package/packages/web/.next/static/media/747892c23ea88013-s.woff2 +0 -0
  108. package/packages/web/.next/static/media/8d697b304b401681-s.woff2 +0 -0
  109. package/packages/web/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  110. package/packages/web/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
  111. package/packages/web/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  112. package/packages/web/.next/static/webpack/16f18baa938a434c.webpack.hot-update.json +1 -0
  113. package/packages/web/.next/static/webpack/5fe9fe8578f9c3d2.webpack.hot-update.json +1 -0
  114. package/packages/web/.next/static/webpack/73c7d02260cc80e4.webpack.hot-update.json +1 -0
  115. package/packages/web/.next/static/webpack/a2d85d19aa028de1.webpack.hot-update.json +1 -0
  116. package/packages/web/.next/static/webpack/app/{layout.73e341375c8d429e.hot-update.js → layout.16f18baa938a434c.hot-update.js} +1 -1
  117. package/packages/web/.next/static/webpack/app/{layout.6fee6306e0f98869.hot-update.js → layout.5fe9fe8578f9c3d2.hot-update.js} +1 -1
  118. package/packages/web/.next/static/webpack/app/layout.653e365406c0d9ac.hot-update.js +22 -0
  119. package/packages/web/.next/static/webpack/app/layout.6800169a899e3a8b.hot-update.js +22 -0
  120. package/packages/web/.next/static/webpack/app/layout.73c7d02260cc80e4.hot-update.js +22 -0
  121. package/packages/web/.next/static/webpack/app/layout.a2d85d19aa028de1.hot-update.js +22 -0
  122. package/packages/web/.next/static/webpack/app/page.653e365406c0d9ac.hot-update.js +22 -0
  123. package/packages/web/.next/static/webpack/app/page.6800169a899e3a8b.hot-update.js +22 -0
  124. package/packages/web/.next/static/webpack/app/page.73c7d02260cc80e4.hot-update.js +22 -0
  125. package/packages/web/.next/static/webpack/app/page.a2d85d19aa028de1.hot-update.js +22 -0
  126. package/packages/web/.next/static/webpack/{webpack.6fee6306e0f98869.hot-update.js → webpack.16f18baa938a434c.hot-update.js} +2 -2
  127. package/packages/web/.next/static/webpack/{webpack.73e341375c8d429e.hot-update.js → webpack.5fe9fe8578f9c3d2.hot-update.js} +2 -2
  128. package/packages/web/.next/static/webpack/webpack.653e365406c0d9ac.hot-update.js +12 -0
  129. package/packages/web/.next/static/webpack/webpack.6800169a899e3a8b.hot-update.js +12 -0
  130. package/packages/web/.next/static/webpack/webpack.73c7d02260cc80e4.hot-update.js +12 -0
  131. package/packages/web/.next/static/webpack/webpack.a2d85d19aa028de1.hot-update.js +12 -0
  132. package/packages/web/.next/trace +2 -5
  133. package/packages/web/app/globals.css +197 -51
  134. package/packages/web/app/layout.tsx +6 -3
  135. package/packages/web/app/page.tsx +791 -309
  136. package/packages/web/bun.lock +66 -105
  137. package/packages/web/next.config.ts +8 -1
  138. package/packages/web/package.json +5 -2
  139. package/packages/web/postcss.config.mjs +2 -2
  140. package/packages/web/public/apple-icon.png +1 -0
  141. package/packages/web/public/dependency-bottleneck.png +0 -0
  142. package/packages/web/public/icon-dark-32x32.png +1 -0
  143. package/packages/web/public/icon-light-32x32.png +1 -0
  144. package/packages/web/public/icon.svg +1 -0
  145. package/packages/web/public/nextjs-cve-announcement.png +0 -0
  146. package/packages/web/public/phantomraven-npm-attack.png +0 -0
  147. package/packages/web/public/placeholder-logo.png +1 -0
  148. package/packages/web/public/placeholder-logo.svg +1 -0
  149. package/packages/web/public/placeholder-user.jpg +1 -0
  150. package/packages/web/public/placeholder.jpg +1 -0
  151. package/packages/web/public/placeholder.svg +1 -0
  152. package/packages/web/public/react-cve-meme.png +0 -0
  153. package/packages/web/public/wallet-drain-exploit.png +0 -0
  154. package/packages/web/styles/globals.css +125 -0
  155. package/packages/web/.next/server/vendor-chunks/@swc.js +0 -55
  156. package/packages/web/.next/server/vendor-chunks/next.js +0 -3010
  157. package/packages/web/.next/static/chunks/app-pages-internals.js +0 -182
  158. package/packages/web/.next/static/chunks/main-app.js +0 -1882
  159. package/packages/web/.next/static/css/app/layout.css +0 -1237
  160. package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
  161. package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +0 -22
  162. package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +0 -22
  163. package/packages/web/tailwind.config.ts +0 -48
  164. /package/packages/web/.next/static/chunks/{polyfills.js → polyfills-42372ed130431b0a.js} +0 -0
  165. /package/packages/web/.next/static/webpack/{6fee6306e0f98869.webpack.hot-update.json → 653e365406c0d9ac.webpack.hot-update.json} +0 -0
  166. /package/packages/web/.next/static/webpack/{73e341375c8d429e.webpack.hot-update.json → 6800169a899e3a8b.webpack.hot-update.json} +0 -0
@@ -10,7 +10,10 @@ import { verifyChecksum } from '../services/signature';
10
10
  import { resolveENSName } from '../services/ens';
11
11
  import { checkPackageWithChainPatrol } from '../services/chainpatrol';
12
12
  import { queryOSV, getOSVSeverity, getFixedVersion, type OSVVulnerability } from '../services/osv';
13
- import { resolveVersion } from '../services/version';
13
+ import { resolveVersion, findSafeVersion, isENSVersion, type ResolvedVersion } from '../services/version';
14
+ import { resolveAddress } from '../services/ens';
15
+ import { readOPMRecords, readPackageENSRecords } from '../services/ens-records';
16
+ import type { OPMENSRecords } from '@opm/core';
14
17
  import { execSync } from 'child_process';
15
18
  import * as fs from 'fs';
16
19
  import * as path from 'path';
@@ -19,6 +22,7 @@ type StepStatus = 'pending' | 'running' | 'done' | 'error' | 'skip';
19
22
 
20
23
  interface Steps {
21
24
  resolve: StepStatus;
25
+ ens: StepStatus;
22
26
  cve: StepStatus;
23
27
  onchain: StepStatus;
24
28
  signature: StepStatus;
@@ -31,6 +35,7 @@ interface SecurityResult {
31
35
  name: string;
32
36
  version: string;
33
37
  resolvedVersion: string;
38
+ resolved?: ResolvedVersion;
34
39
  cves: OSVVulnerability[];
35
40
  info?: OnChainPackageInfo;
36
41
  signatureValid?: boolean;
@@ -40,6 +45,9 @@ interface SecurityResult {
40
45
  warning: boolean;
41
46
  blockReason?: string;
42
47
  safestVersion?: string;
48
+ autoBumped?: boolean;
49
+ autoBumpedFrom?: string;
50
+ autoBumpReason?: string;
43
51
  }
44
52
 
45
53
  interface InstallCommandProps {
@@ -76,12 +84,17 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
76
84
  // ─── Single package install with full security pipeline ───────────────────────
77
85
 
78
86
  function SingleInstall({ packageName, version }: { packageName: string; version?: string }) {
87
+ const isEns = version ? isENSVersion(version) : false;
88
+
79
89
  const [steps, setSteps] = useState<Steps>({
80
- resolve: 'pending', cve: 'pending', onchain: 'pending',
90
+ resolve: 'pending', ens: isEns ? 'pending' : 'skip',
91
+ cve: 'pending', onchain: 'pending',
81
92
  signature: 'pending', chainpatrol: 'pending', report: 'pending',
82
93
  install: 'pending',
83
94
  });
84
95
  const [result, setResult] = useState<SecurityResult | null>(null);
96
+ const [ensDetail, setEnsDetail] = useState<string | undefined>(undefined);
97
+ const [ensRecords, setEnsRecords] = useState<OPMENSRecords>({});
85
98
  const [error, setError] = useState<string | null>(null);
86
99
  const [done, setDone] = useState(false);
87
100
 
@@ -99,11 +112,51 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
99
112
  blocked: false, warning: false,
100
113
  };
101
114
 
102
- update('resolve', 'running');
103
- r.resolvedVersion = await resolveVersion(packageName, r.version);
104
- setResult({ ...r });
105
- update('resolve', 'done');
115
+ // ── Resolve version (+ ENS if applicable) ──
116
+ if (isEns) {
117
+ update('resolve', 'done');
118
+ update('ens', 'running');
119
+ try {
120
+ const resolved = await resolveVersion(packageName, r.version, (msg) => setEnsDetail(msg));
121
+ r.resolved = resolved;
122
+ r.resolvedVersion = resolved.version;
123
+ r.ensName = resolved.ensName;
124
+ setResult({ ...r });
125
+ setEnsDetail(`${resolved.ensName} → v${resolved.version} (${resolved.reason})`);
126
+
127
+ if (resolved.ensName) {
128
+ const [opmRecs, pkgRecs] = await Promise.allSettled([
129
+ readOPMRecords(resolved.ensName),
130
+ readPackageENSRecords(resolved.ensName, packageName),
131
+ ]);
132
+ const merged: OPMENSRecords = {};
133
+ if (pkgRecs.status === 'fulfilled') Object.assign(merged, pkgRecs.value);
134
+ if (opmRecs.status === 'fulfilled') {
135
+ for (const [k, v] of Object.entries(opmRecs.value)) {
136
+ if (v && !(merged as any)[k]) (merged as any)[k] = v;
137
+ }
138
+ }
139
+ setEnsRecords(merged);
140
+ }
106
141
 
142
+ update('ens', 'done');
143
+ } catch (err: any) {
144
+ setEnsDetail(err?.message || 'ENS resolution failed');
145
+ update('ens', 'error');
146
+ setError(err?.message || 'ENS resolution failed');
147
+ update('install', 'error');
148
+ return;
149
+ }
150
+ } else {
151
+ update('resolve', 'running');
152
+ const resolved = await resolveVersion(packageName, r.version);
153
+ r.resolved = resolved;
154
+ r.resolvedVersion = resolved.version;
155
+ setResult({ ...r });
156
+ update('resolve', 'done');
157
+ }
158
+
159
+ // ── CVE check ──
107
160
  update('cve', 'running');
108
161
  r.cves = await queryOSV(packageName, r.resolvedVersion);
109
162
  const cveCounts = categorizeCVEs(r.cves);
@@ -116,6 +169,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
116
169
  setResult({ ...r });
117
170
  update('cve', 'done');
118
171
 
172
+ // ── On-chain registry lookup ──
119
173
  update('onchain', 'running');
120
174
  try {
121
175
  const info = await getPackageInfo(packageName, r.resolvedVersion);
@@ -133,12 +187,35 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
133
187
  setResult({ ...r });
134
188
  update('onchain', 'done');
135
189
 
190
+ // ── Auto-bump: try to find a safe version instead of blocking ──
191
+ if (r.blocked && !isEns) {
192
+ const safe = await findSafeVersion(packageName, r.resolvedVersion, r.cves);
193
+ if (safe) {
194
+ r.autoBumped = true;
195
+ r.autoBumpedFrom = r.resolvedVersion;
196
+ r.autoBumpReason = safe.reason;
197
+ r.resolvedVersion = safe.version;
198
+ r.blocked = false;
199
+ r.blockReason = undefined;
200
+ r.warning = true;
201
+
202
+ r.cves = await queryOSV(packageName, safe.version).catch(() => []);
203
+ try {
204
+ const newInfo = await getPackageInfo(packageName, safe.version);
205
+ if (newInfo.exists) r.info = newInfo;
206
+ } catch { /* keep existing */ }
207
+
208
+ setResult({ ...r });
209
+ }
210
+ }
211
+
212
+ // ── Signature verification ──
136
213
  if (r.info?.exists) {
137
214
  update('signature', 'running');
138
215
  r.signatureValid = r.info.signature !== '0x'
139
216
  ? verifyChecksum(r.info.checksum, r.info.signature, r.info.author)
140
217
  : false;
141
- if (r.info.author) {
218
+ if (r.info.author && !r.ensName) {
142
219
  r.ensName = await resolveENSName(r.info.author).catch(() => null) || undefined;
143
220
  }
144
221
  setResult({ ...r });
@@ -147,6 +224,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
147
224
  update('signature', 'skip');
148
225
  }
149
226
 
227
+ // ── ChainPatrol check ──
150
228
  if (!r.info?.exists) {
151
229
  update('chainpatrol', 'running');
152
230
  const cp = await checkPackageWithChainPatrol(packageName).catch(() => null);
@@ -161,12 +239,14 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
161
239
  update('chainpatrol', 'skip');
162
240
  }
163
241
 
242
+ // ── Fileverse report ──
164
243
  if (r.info?.reportURI && !r.info.reportURI.startsWith('local://')) {
165
244
  update('report', 'done');
166
245
  } else {
167
246
  update('report', 'skip');
168
247
  }
169
248
 
249
+ // ── Block or install ──
170
250
  if (r.blocked) {
171
251
  setError(`Blocked: ${r.blockReason || 'security risk detected'}`);
172
252
  update('install', 'error');
@@ -175,7 +255,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
175
255
 
176
256
  update('install', 'running');
177
257
  try {
178
- const target = `${packageName}${version ? `@${version}` : ''}`;
258
+ const target = `${packageName}@${r.resolvedVersion}`;
179
259
  execSync(`npm install ${target}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
180
260
  } catch { /* non-fatal */ }
181
261
  update('install', 'done');
@@ -191,12 +271,55 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
191
271
  return (
192
272
  <Box flexDirection="column">
193
273
  <Header subtitle="install" />
194
- {result && <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>}
274
+ {result && (
275
+ <Box>
276
+ <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>
277
+ {result.ensName && result.resolved?.source === 'ens' && (
278
+ <Text color="cyan"> via {result.ensName}</Text>
279
+ )}
280
+ {result.autoBumped && (
281
+ <Text color="yellow"> (bumped from {result.autoBumpedFrom})</Text>
282
+ )}
283
+ </Box>
284
+ )}
195
285
  <Text> </Text>
196
286
 
197
287
  <StatusLine label="Resolve version" status={steps.resolve}
198
288
  detail={steps.resolve === 'done' ? result?.resolvedVersion : undefined} />
199
289
 
290
+ {isEns && (
291
+ <StatusLine label="Resolve ENS author" status={steps.ens} detail={ensDetail} />
292
+ )}
293
+ {steps.ens === 'done' && result?.resolved?.source === 'ens' && (
294
+ <Box flexDirection="column" marginLeft={4}>
295
+ <Box>
296
+ <Text color="gray">Author: </Text>
297
+ <Text color="green">{result.ensName}</Text>
298
+ {result.resolved.authorAddress && (
299
+ <Text color="gray"> ({truncateAddress(result.resolved.authorAddress)})</Text>
300
+ )}
301
+ <Text color="green"> on-chain</Text>
302
+ </Box>
303
+ <Box>
304
+ <Text color="gray">Version: </Text>
305
+ <Text color="cyan">{result.resolvedVersion}</Text>
306
+ <Text color="gray"> (safest on-chain version)</Text>
307
+ </Box>
308
+ {ensRecords.fileverse && (
309
+ <Box>
310
+ <Text color="gray">Report: </Text>
311
+ <Text color="green">{ensRecords.fileverse.length > 50 ? ensRecords.fileverse.slice(0, 50) + '...' : ensRecords.fileverse}</Text>
312
+ </Box>
313
+ )}
314
+ {ensRecords.riskScore && (
315
+ <Box>
316
+ <Text color="gray">Risk: </Text>
317
+ <Text color="cyan">{ensRecords.riskScore}/100 (from ENS)</Text>
318
+ </Box>
319
+ )}
320
+ </Box>
321
+ )}
322
+
200
323
  <StatusLine label="Query CVE database (OSV)" status={steps.cve}
201
324
  detail={steps.cve === 'done'
202
325
  ? (result?.cves.length
@@ -231,6 +354,22 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
231
354
  </Box>
232
355
  )}
233
356
 
357
+ {result?.autoBumped && (
358
+ <Box flexDirection="column" marginLeft={4} marginTop={0}>
359
+ <Box>
360
+ <Text color="yellow">↑ Auto-bumped: </Text>
361
+ <Text color="red">{result.autoBumpedFrom}</Text>
362
+ <Text color="yellow"> → </Text>
363
+ <Text color="green" bold>{result.resolvedVersion}</Text>
364
+ </Box>
365
+ {result.autoBumpReason && (
366
+ <Box marginLeft={2}>
367
+ <Text color="gray">{result.autoBumpReason}</Text>
368
+ </Box>
369
+ )}
370
+ </Box>
371
+ )}
372
+
234
373
  <StatusLine label="On-chain registry lookup" status={steps.onchain}
235
374
  detail={steps.onchain === 'done' && result?.info?.exists
236
375
  ? `${result.info.aggregateScore}/100 (${classifyRisk(result.info.aggregateScore)})`
@@ -269,14 +408,30 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
269
408
  <Box flexDirection="column" marginTop={1}>
270
409
  <Text color="gray">────────────────────────────────────────</Text>
271
410
  <Text color="white" bold> Security Summary</Text>
411
+ {result.resolved?.source === 'ens' && (
412
+ <Box marginLeft={2}>
413
+ <Text color="gray">Resolved: </Text>
414
+ <Text color="green">{result.ensName}</Text>
415
+ <Text color="gray"> → </Text>
416
+ <Text color="cyan">{result.resolvedVersion}</Text>
417
+ </Box>
418
+ )}
419
+ {result.autoBumped && (
420
+ <Box marginLeft={2}>
421
+ <Text color="gray">Bumped: </Text>
422
+ <Text color="red">{result.autoBumpedFrom}</Text>
423
+ <Text color="gray"> → </Text>
424
+ <Text color="green">{result.resolvedVersion}</Text>
425
+ </Box>
426
+ )}
272
427
  {result.info?.exists && (
273
428
  <Box marginLeft={2}>
274
- <Text color="gray">Risk: </Text>
429
+ <Text color="gray">Risk: </Text>
275
430
  <RiskBadge level={classifyRisk(result.info.aggregateScore)} score={result.info.aggregateScore} />
276
431
  </Box>
277
432
  )}
278
433
  <Box marginLeft={2}>
279
- <Text color="gray">CVEs: </Text>
434
+ <Text color="gray">CVEs: </Text>
280
435
  {result.cves.length > 0 ? (
281
436
  <Text color={severeCount > 0 ? 'red' : 'yellow'}>
282
437
  {result.cves.length} known ({cveCounts.critical > 0 ? `${cveCounts.critical} critical, ` : ''}{cveCounts.high} high, {cveCounts.medium} medium, {cveCounts.low} low)
@@ -287,19 +442,25 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
287
442
  </Box>
288
443
  {result.info?.exists && (
289
444
  <Box marginLeft={2}>
290
- <Text color="gray">Signature: </Text>
445
+ <Text color="gray">Signature:</Text>
291
446
  <Text color={result.signatureValid ? 'green' : 'red'}>
292
- {result.signatureValid ? 'verified' : 'unverified'}
447
+ {' '}{result.signatureValid ? 'verified' : 'unverified'}
293
448
  </Text>
294
449
  </Box>
295
450
  )}
296
451
  {result.ensName && (
297
452
  <Box marginLeft={2}>
298
- <Text color="gray">Author: </Text>
453
+ <Text color="gray">Author: </Text>
299
454
  <Text color="green">{result.ensName}</Text>
300
455
  </Box>
301
456
  )}
302
- {result.warning && !result.blocked && (
457
+ {ensRecords.fileverse && (
458
+ <Box marginLeft={2}>
459
+ <Text color="gray">Fileverse:</Text>
460
+ <Text color="green"> {ensRecords.fileverse.length > 45 ? ensRecords.fileverse.slice(0, 45) + '...' : ensRecords.fileverse}</Text>
461
+ </Box>
462
+ )}
463
+ {result.warning && !result.blocked && !result.autoBumped && (
303
464
  <Box marginLeft={2}>
304
465
  <Text color="yellow">⚠ Vulnerabilities detected — review before using in production</Text>
305
466
  </Box>
@@ -311,7 +472,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
311
472
  <Text color="yellow"> to fix known CVEs</Text>
312
473
  </Box>
313
474
  )}
314
- {result.warning && result.safestVersion && (
475
+ {result.warning && result.safestVersion && !result.autoBumped && (
315
476
  <Box marginLeft={2}>
316
477
  <Text color="yellow">⚠ Consider using safest on-chain version: {result.safestVersion}</Text>
317
478
  </Box>
@@ -338,6 +499,11 @@ interface BulkDepResult {
338
499
  blocked: boolean;
339
500
  blockReason?: string;
340
501
  suggestedUpgrade?: string;
502
+ ensResolved?: boolean;
503
+ ensName?: string;
504
+ autoBumped?: boolean;
505
+ originalVersion?: string;
506
+ autoBumpReason?: string;
341
507
  }
342
508
 
343
509
  function BulkInstall() {
@@ -346,6 +512,9 @@ function BulkInstall() {
346
512
  const [error, setError] = useState<string | null>(null);
347
513
  const [installStatus, setInstallStatus] = useState<StepStatus>('pending');
348
514
  const [total, setTotal] = useState(0);
515
+ const [ensCount, setEnsCount] = useState(0);
516
+ const [ensResolvingStatus, setEnsResolvingStatus] = useState<StepStatus>('skip');
517
+ const [ensResolvedCount, setEnsResolvedCount] = useState(0);
349
518
 
350
519
  useEffect(() => {
351
520
  runBulk().catch((err) => setError(String(err)));
@@ -368,15 +537,79 @@ function BulkInstall() {
368
537
  return;
369
538
  }
370
539
 
540
+ // ── Phase 1: Batch-resolve all ENS names in parallel ──
541
+ const ensEntries = entries.filter(([, ver]) => isENSVersion(String(ver)));
542
+ setEnsCount(ensEntries.length);
543
+
544
+ const ensCache = new Map<string, { address: string; version: string }>();
545
+
546
+ if (ensEntries.length > 0) {
547
+ setEnsResolvingStatus('running');
548
+
549
+ const uniqueEnsNames = [...new Set(ensEntries.map(([, v]) => String(v)))];
550
+ const ensResults = await Promise.allSettled(
551
+ uniqueEnsNames.map(async (ensName) => {
552
+ const addr = await resolveAddress(ensName);
553
+ return { ensName, address: addr };
554
+ }),
555
+ );
556
+
557
+ const ensAddresses = new Map<string, string>();
558
+ for (const result of ensResults) {
559
+ if (result.status === 'fulfilled' && result.value.address) {
560
+ ensAddresses.set(result.value.ensName, result.value.address);
561
+ }
562
+ }
563
+
564
+ const ensVersionResults = await Promise.allSettled(
565
+ ensEntries.map(async ([name, ensName]) => {
566
+ const addr = ensAddresses.get(String(ensName));
567
+ if (!addr) return null;
568
+ const resolved = await resolveVersion(name, String(ensName));
569
+ return { name, ensName: String(ensName), resolved };
570
+ }),
571
+ );
572
+
573
+ for (const result of ensVersionResults) {
574
+ if (result.status === 'fulfilled' && result.value) {
575
+ const { name, ensName, resolved } = result.value;
576
+ ensCache.set(name, {
577
+ address: resolved.authorAddress || '',
578
+ version: resolved.version,
579
+ });
580
+ setEnsResolvedCount((c) => c + 1);
581
+ }
582
+ }
583
+
584
+ setEnsResolvingStatus('done');
585
+ }
586
+
587
+ // ── Phase 2: Scan each dependency ──
371
588
  const checked: BulkDepResult[] = [];
372
589
 
373
590
  for (const [name, verRange] of entries) {
374
- const rawVersion = String(verRange).replace(/^[\^~]/, '');
591
+ const rawVerStr = String(verRange);
592
+ const isEns = isENSVersion(rawVerStr);
593
+
594
+ let rawVersion: string;
595
+ let ensName: string | undefined;
596
+ let ensResolved = false;
597
+
598
+ if (isEns && ensCache.has(name)) {
599
+ const cached = ensCache.get(name)!;
600
+ rawVersion = cached.version;
601
+ ensName = rawVerStr;
602
+ ensResolved = true;
603
+ } else {
604
+ rawVersion = rawVerStr.replace(/^[\^~]/, '');
605
+ }
606
+
375
607
  const entry: BulkDepResult = {
376
608
  name, version: rawVersion,
377
609
  cves: [], cvesCritical: 0, cvesHigh: 0,
378
610
  onChain: false, score: null,
379
611
  blocked: false,
612
+ ensResolved, ensName,
380
613
  };
381
614
 
382
615
  const [osvResult, infoResult] = await Promise.allSettled([
@@ -406,6 +639,25 @@ function BulkInstall() {
406
639
  }
407
640
  }
408
641
 
642
+ // ── Auto-bump blocked deps ──
643
+ if (entry.blocked && !ensResolved) {
644
+ const safe = await findSafeVersion(name, rawVersion, entry.cves);
645
+ if (safe) {
646
+ entry.autoBumped = true;
647
+ entry.originalVersion = rawVersion;
648
+ entry.autoBumpReason = safe.reason;
649
+ entry.version = safe.version;
650
+ entry.blocked = false;
651
+ entry.blockReason = undefined;
652
+
653
+ const newCves = await queryOSV(name, safe.version).catch(() => []);
654
+ entry.cves = newCves;
655
+ const newCounts = categorizeCVEs(newCves);
656
+ entry.cvesCritical = newCounts.critical;
657
+ entry.cvesHigh = newCounts.high;
658
+ }
659
+ }
660
+
409
661
  checked.push(entry);
410
662
  setDeps([...checked]);
411
663
  }
@@ -419,6 +671,22 @@ function BulkInstall() {
419
671
  return;
420
672
  }
421
673
 
674
+ // Update package.json with resolved versions before installing
675
+ const bumpedDeps = checked.filter((d) => d.autoBumped || d.ensResolved);
676
+ if (bumpedDeps.length > 0) {
677
+ const freshPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
678
+ for (const dep of bumpedDeps) {
679
+ const resolved = `^${dep.version}`;
680
+ if (freshPkg.dependencies && dep.name in freshPkg.dependencies) {
681
+ freshPkg.dependencies[dep.name] = resolved;
682
+ }
683
+ if (freshPkg.devDependencies && dep.name in freshPkg.devDependencies) {
684
+ freshPkg.devDependencies[dep.name] = resolved;
685
+ }
686
+ }
687
+ fs.writeFileSync(pkgPath, JSON.stringify(freshPkg, null, 2) + '\n');
688
+ }
689
+
422
690
  setInstallStatus('running');
423
691
  try {
424
692
  execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
@@ -427,8 +695,9 @@ function BulkInstall() {
427
695
  }
428
696
 
429
697
  const blockedDeps = deps.filter((d) => d.blocked);
430
- const warnDeps = deps.filter((d) => !d.blocked && (d.cvesHigh > 0 || (d.score !== null && d.score >= MEDIUM_RISK_THRESHOLD)));
431
- const safeDeps = deps.filter((d) => !d.blocked && d.cvesHigh === 0 && (d.score === null || d.score < MEDIUM_RISK_THRESHOLD));
698
+ const bumpedDeps = deps.filter((d) => d.autoBumped || d.ensResolved);
699
+ const warnDeps = deps.filter((d) => !d.blocked && !d.autoBumped && !d.ensResolved && (d.cvesHigh > 0 || (d.score !== null && d.score >= MEDIUM_RISK_THRESHOLD)));
700
+ const safeDeps = deps.filter((d) => !d.blocked && !d.autoBumped && !d.ensResolved && d.cvesHigh === 0 && (d.score === null || d.score < MEDIUM_RISK_THRESHOLD));
432
701
  const totalCves = deps.reduce((s, d) => s + d.cves.length, 0);
433
702
 
434
703
  return (
@@ -436,13 +705,48 @@ function BulkInstall() {
436
705
  <Header subtitle="install" />
437
706
  <Text> </Text>
438
707
 
708
+ {ensCount > 0 && (
709
+ <StatusLine label={`Resolve ${ensCount} ENS author(s)`} status={ensResolvingStatus}
710
+ detail={ensResolvingStatus === 'done' ? `${ensResolvedCount} resolved` : ensResolvingStatus === 'running' ? 'resolving...' : undefined} />
711
+ )}
712
+
439
713
  <StatusLine label={`Scanning ${total} dependencies`} status={scanning ? 'running' : 'done'}
440
714
  detail={!scanning ? `${deps.length} checked` : `${deps.length}/${total}`} />
441
715
 
442
716
  {deps.length > 0 && (
443
717
  <Box flexDirection="column" marginTop={1}>
444
- {blockedDeps.length > 0 && (
718
+ {bumpedDeps.length > 0 && (
445
719
  <Box flexDirection="column">
720
+ <Text color="cyan" bold> ENS / AUTO-BUMPED ({bumpedDeps.length})</Text>
721
+ {bumpedDeps.map((d) => (
722
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
723
+ <Box>
724
+ {d.ensResolved ? (
725
+ <Text color="cyan">◈ </Text>
726
+ ) : (
727
+ <Text color="yellow">↑ </Text>
728
+ )}
729
+ <Text color="white" bold>{d.name}</Text>
730
+ <Text color="green">@{d.version}</Text>
731
+ {d.ensResolved && d.ensName && (
732
+ <Text color="cyan"> via {d.ensName}</Text>
733
+ )}
734
+ {d.autoBumped && d.originalVersion && (
735
+ <Text color="yellow"> bumped from {d.originalVersion}</Text>
736
+ )}
737
+ </Box>
738
+ {d.autoBumpReason && (
739
+ <Box marginLeft={4}>
740
+ <Text color="gray">{d.autoBumpReason}</Text>
741
+ </Box>
742
+ )}
743
+ </Box>
744
+ ))}
745
+ </Box>
746
+ )}
747
+
748
+ {blockedDeps.length > 0 && (
749
+ <Box flexDirection="column" marginTop={bumpedDeps.length > 0 ? 1 : 0}>
446
750
  <Text color="red" bold> BLOCKED ({blockedDeps.length})</Text>
447
751
  {blockedDeps.map((d) => (
448
752
  <Box key={d.name} flexDirection="column" marginLeft={2}>
@@ -476,7 +780,7 @@ function BulkInstall() {
476
780
  )}
477
781
 
478
782
  {warnDeps.length > 0 && (
479
- <Box flexDirection="column" marginTop={blockedDeps.length > 0 ? 1 : 0}>
783
+ <Box flexDirection="column" marginTop={(blockedDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
480
784
  <Text color="yellow" bold> WARNING ({warnDeps.length})</Text>
481
785
  {warnDeps.map((d) => (
482
786
  <Box key={d.name} marginLeft={2}>
@@ -491,7 +795,7 @@ function BulkInstall() {
491
795
  )}
492
796
 
493
797
  {safeDeps.length > 0 && (
494
- <Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length) > 0 ? 1 : 0}>
798
+ <Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
495
799
  <Text color="green" bold> SAFE ({safeDeps.length})</Text>
496
800
  {safeDeps.map((d) => (
497
801
  <Box key={d.name} marginLeft={2}>
@@ -518,7 +822,7 @@ function BulkInstall() {
518
822
 
519
823
  <Box marginTop={1}>
520
824
  <Text color={blockedDeps.length > 0 ? 'red' : totalCves > 0 ? 'yellow' : 'green'} bold>
521
- {deps.length} packages scanned: {blockedDeps.length} blocked, {warnDeps.length} warnings, {totalCves} CVEs
825
+ {deps.length} packages scanned: {blockedDeps.length} blocked, {bumpedDeps.length} resolved, {warnDeps.length} warnings, {totalCves} CVEs
522
826
  </Text>
523
827
  </Box>
524
828