opmsec 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/bun.lock +4 -4
  3. package/docs/mint.json +2 -2
  4. package/package.json +7 -6
  5. package/packages/cli/src/commands/install.tsx +282 -24
  6. package/packages/cli/src/index.tsx +3 -1
  7. package/packages/cli/src/services/version.ts +156 -5
  8. package/packages/core/src/utils.ts +135 -0
  9. package/packages/scanner/src/services/openrouter.ts +18 -7
  10. package/packages/web/.next/BUILD_ID +1 -0
  11. package/packages/web/.next/app-build-manifest.json +18 -7
  12. package/packages/web/.next/app-path-routes-manifest.json +4 -0
  13. package/packages/web/.next/build-manifest.json +19 -6
  14. package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
  15. package/packages/web/.next/diagnostics/framework.json +1 -0
  16. package/packages/web/.next/export-marker.json +6 -0
  17. package/packages/web/.next/images-manifest.json +58 -0
  18. package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
  19. package/packages/web/.next/next-server.js.nft.json +1 -0
  20. package/packages/web/.next/prerender-manifest.json +54 -4
  21. package/packages/web/.next/required-server-files.json +320 -0
  22. package/packages/web/.next/routes-manifest.json +53 -1
  23. package/packages/web/.next/server/app/_not-found/page.js +2 -0
  24. package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  25. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  26. package/packages/web/.next/server/app/_not-found.html +1 -0
  27. package/packages/web/.next/server/app/_not-found.meta +8 -0
  28. package/packages/web/.next/server/app/_not-found.rsc +16 -0
  29. package/packages/web/.next/server/app/index.html +1 -0
  30. package/packages/web/.next/server/app/index.meta +7 -0
  31. package/packages/web/.next/server/app/index.rsc +20 -0
  32. package/packages/web/.next/server/app/page.js +2 -272
  33. package/packages/web/.next/server/app/page.js.nft.json +1 -0
  34. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/packages/web/.next/server/app-paths-manifest.json +1 -0
  36. package/packages/web/.next/server/chunks/611.js +6 -0
  37. package/packages/web/.next/server/chunks/778.js +30 -0
  38. package/packages/web/.next/server/functions-config-manifest.json +4 -0
  39. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -1
  40. package/packages/web/.next/server/middleware-build-manifest.js +1 -22
  41. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  42. package/packages/web/.next/server/next-font-manifest.js +1 -1
  43. package/packages/web/.next/server/pages/404.html +1 -0
  44. package/packages/web/.next/server/pages/500.html +1 -0
  45. package/packages/web/.next/server/pages/_app.js +1 -0
  46. package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
  47. package/packages/web/.next/server/pages/_document.js +1 -0
  48. package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
  49. package/packages/web/.next/server/pages/_error.js +19 -0
  50. package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
  51. package/packages/web/.next/server/pages-manifest.json +6 -1
  52. package/packages/web/.next/server/server-reference-manifest.js +1 -1
  53. package/packages/web/.next/server/server-reference-manifest.json +1 -5
  54. package/packages/web/.next/server/webpack-runtime.js +1 -209
  55. package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_buildManifest.js +1 -0
  56. package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_ssgManifest.js +1 -0
  57. package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
  58. package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
  59. package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
  60. package/packages/web/.next/static/chunks/app/layout-28a489fb4398663f.js +1 -0
  61. package/packages/web/.next/static/chunks/app/page-e58ccdb78625bce6.js +1 -0
  62. package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
  63. package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
  64. package/packages/web/.next/static/chunks/main-ee293fa6aa18bdd1.js +1 -0
  65. package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  66. package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  67. package/packages/web/.next/static/chunks/webpack-e1ae44446e7f7355.js +1 -0
  68. package/packages/web/.next/static/css/21d69157e271f2ab.css +3 -0
  69. package/packages/web/.next/trace +2 -5
  70. package/packages/web/app/page.tsx +5 -2
  71. package/packages/web/.next/server/vendor-chunks/@swc.js +0 -55
  72. package/packages/web/.next/server/vendor-chunks/next.js +0 -3010
  73. package/packages/web/.next/static/chunks/app/layout.js +0 -39
  74. package/packages/web/.next/static/chunks/app/page.js +0 -61
  75. package/packages/web/.next/static/chunks/app-pages-internals.js +0 -182
  76. package/packages/web/.next/static/chunks/main-app.js +0 -1882
  77. package/packages/web/.next/static/chunks/webpack.js +0 -1393
  78. package/packages/web/.next/static/css/app/layout.css +0 -1237
  79. package/packages/web/.next/static/development/_buildManifest.js +0 -1
  80. package/packages/web/.next/static/development/_ssgManifest.js +0 -1
  81. package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
  82. package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +0 -1
  83. package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +0 -1
  84. package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +0 -22
  85. package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +0 -22
  86. package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +0 -22
  87. package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +0 -22
  88. package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +0 -12
  89. package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +0 -12
  90. /package/packages/web/.next/static/chunks/{polyfills.js → polyfills-42372ed130431b0a.js} +0 -0
@@ -0,0 +1 @@
1
+ cd packages/web && bun run build
package/bun.lock CHANGED
@@ -12,10 +12,10 @@
12
12
  "react": "^18.3.1",
13
13
  "react-devtools-core": "^5.3.2",
14
14
  "terminal-image": "^4.2.0",
15
- "viem": "^2.47.2",
15
+ "viem": "^2.47.4",
16
16
  },
17
17
  "devDependencies": {
18
- "bun-types": "latest",
18
+ "bun-types": "^1.3.10",
19
19
  },
20
20
  },
21
21
  "packages/cli": {
@@ -317,7 +317,7 @@
317
317
 
318
318
  "opm": ["opm@workspace:packages/cli"],
319
319
 
320
- "ox": ["ox@0.14.0", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw=="],
320
+ "ox": ["ox@0.14.5", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-HgmHmBveYO40H/R3K6TMrwYtHsx/u6TAB+GpZlgJCoW0Sq5Ttpjih0IZZiwGQw7T6vdW4IAyobYrE2mdAvyF8Q=="],
321
321
 
322
322
  "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
323
323
 
@@ -427,7 +427,7 @@
427
427
 
428
428
  "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
429
429
 
430
- "viem": ["viem@2.47.2", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.0", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-etDIwDgmDiGaPg8rUbJtUFuC3/nAJCbhMYyfh5dOcqNNkzBWTNcS2VluPSM5JVo+9U3b2hle2RkBEq3+xyvlvg=="],
430
+ "viem": ["viem@2.47.4", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.5", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-h0Wp/SYmJO/HB4B/em1OZ3W1LaKrmr7jzaN7talSlZpo0LCn0V6rZ5g923j6sf4VUSrqp/gUuWuHFc7UcoIp8A=="],
431
431
 
432
432
  "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
433
433
 
package/docs/mint.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://mintlify.com/schema.json",
3
3
  "name": "OPM Documentation",
4
4
  "logo": {
5
- "dark": "/logo/dark.svg",
6
- "light": "/logo/light.svg"
5
+ "dark": "/favicon.svg",
6
+ "light": "/favicon.svg"
7
7
  },
8
8
  "favicon": "/favicon.svg",
9
9
  "colors": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opmsec",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "opm": "packages/cli/src/index.tsx"
@@ -14,7 +14,8 @@
14
14
  "build:contracts": "cd packages/contracts && npx hardhat compile",
15
15
  "deploy:contracts": "cd packages/contracts && npx hardhat run scripts/deploy.ts --network baseSepolia",
16
16
  "test:contracts": "cd packages/contracts && npx hardhat test",
17
- "scan": "bun run packages/scanner/src/index.ts"
17
+ "scan": "bun run packages/scanner/src/index.ts",
18
+ "prepare": "husky || true"
18
19
  },
19
20
  "dependencies": {
20
21
  "@ensdomains/ensjs": "^4.2.2",
@@ -25,15 +26,15 @@
25
26
  "react": "^18.3.1",
26
27
  "react-devtools-core": "^5.3.2",
27
28
  "terminal-image": "^4.2.0",
28
- "viem": "^2.47.2"
29
+ "viem": "^2.47.4"
29
30
  },
30
31
  "devDependencies": {
31
- "bun-types": "latest"
32
+ "bun-types": "^1.3.10"
32
33
  },
33
34
  "opm": {
34
- "signature": "0xacf623c584df3b03a13588107cb0024212b50a20078a74d3a19394ea7378f2b23c679f3e351fd27393c5212f09ead8c826af1f103f535b9a802944e27b5ffa081b",
35
+ "signature": "0xf469749982344b8aee24fadd624cad2b5262dd48964ceb0de8d79d2d256343a04aef4e99682b3e48c8468191bb1ff2076615b49006e9c83cd0de2bd696aa92e91b",
35
36
  "author": "0x2a3942EbDd8c5ea3E66D3fC4301F56d0F15d4bE2",
36
37
  "ensName": "djpaiethg.eth",
37
- "checksum": "0x5e73c81a9f22b1381766bbea30dee60a945ece4d320f3c4f65597beb0bc19269"
38
+ "checksum": "0x3c9a3dfc1e62fd2b36611988cf642d34e6bc7de352c903d4dc6968253cfe1b61"
38
39
  }
39
40
  }
@@ -10,7 +10,8 @@ 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';
14
15
  import { execSync } from 'child_process';
15
16
  import * as fs from 'fs';
16
17
  import * as path from 'path';
@@ -19,6 +20,7 @@ type StepStatus = 'pending' | 'running' | 'done' | 'error' | 'skip';
19
20
 
20
21
  interface Steps {
21
22
  resolve: StepStatus;
23
+ ens: StepStatus;
22
24
  cve: StepStatus;
23
25
  onchain: StepStatus;
24
26
  signature: StepStatus;
@@ -31,6 +33,7 @@ interface SecurityResult {
31
33
  name: string;
32
34
  version: string;
33
35
  resolvedVersion: string;
36
+ resolved?: ResolvedVersion;
34
37
  cves: OSVVulnerability[];
35
38
  info?: OnChainPackageInfo;
36
39
  signatureValid?: boolean;
@@ -40,6 +43,9 @@ interface SecurityResult {
40
43
  warning: boolean;
41
44
  blockReason?: string;
42
45
  safestVersion?: string;
46
+ autoBumped?: boolean;
47
+ autoBumpedFrom?: string;
48
+ autoBumpReason?: string;
43
49
  }
44
50
 
45
51
  interface InstallCommandProps {
@@ -76,12 +82,16 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
76
82
  // ─── Single package install with full security pipeline ───────────────────────
77
83
 
78
84
  function SingleInstall({ packageName, version }: { packageName: string; version?: string }) {
85
+ const isEns = version ? isENSVersion(version) : false;
86
+
79
87
  const [steps, setSteps] = useState<Steps>({
80
- resolve: 'pending', cve: 'pending', onchain: 'pending',
88
+ resolve: 'pending', ens: isEns ? 'pending' : 'skip',
89
+ cve: 'pending', onchain: 'pending',
81
90
  signature: 'pending', chainpatrol: 'pending', report: 'pending',
82
91
  install: 'pending',
83
92
  });
84
93
  const [result, setResult] = useState<SecurityResult | null>(null);
94
+ const [ensDetail, setEnsDetail] = useState<string | undefined>(undefined);
85
95
  const [error, setError] = useState<string | null>(null);
86
96
  const [done, setDone] = useState(false);
87
97
 
@@ -99,11 +109,35 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
99
109
  blocked: false, warning: false,
100
110
  };
101
111
 
102
- update('resolve', 'running');
103
- r.resolvedVersion = await resolveVersion(packageName, r.version);
104
- setResult({ ...r });
105
- update('resolve', 'done');
112
+ // ── Resolve version (+ ENS if applicable) ──
113
+ if (isEns) {
114
+ update('resolve', 'done');
115
+ update('ens', 'running');
116
+ try {
117
+ const resolved = await resolveVersion(packageName, r.version, (msg) => setEnsDetail(msg));
118
+ r.resolved = resolved;
119
+ r.resolvedVersion = resolved.version;
120
+ r.ensName = resolved.ensName;
121
+ setResult({ ...r });
122
+ setEnsDetail(`${resolved.ensName} → v${resolved.version} (${resolved.reason})`);
123
+ update('ens', 'done');
124
+ } catch (err: any) {
125
+ setEnsDetail(err?.message || 'ENS resolution failed');
126
+ update('ens', 'error');
127
+ setError(err?.message || 'ENS resolution failed');
128
+ update('install', 'error');
129
+ return;
130
+ }
131
+ } else {
132
+ update('resolve', 'running');
133
+ const resolved = await resolveVersion(packageName, r.version);
134
+ r.resolved = resolved;
135
+ r.resolvedVersion = resolved.version;
136
+ setResult({ ...r });
137
+ update('resolve', 'done');
138
+ }
106
139
 
140
+ // ── CVE check ──
107
141
  update('cve', 'running');
108
142
  r.cves = await queryOSV(packageName, r.resolvedVersion);
109
143
  const cveCounts = categorizeCVEs(r.cves);
@@ -116,6 +150,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
116
150
  setResult({ ...r });
117
151
  update('cve', 'done');
118
152
 
153
+ // ── On-chain registry lookup ──
119
154
  update('onchain', 'running');
120
155
  try {
121
156
  const info = await getPackageInfo(packageName, r.resolvedVersion);
@@ -133,12 +168,35 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
133
168
  setResult({ ...r });
134
169
  update('onchain', 'done');
135
170
 
171
+ // ── Auto-bump: try to find a safe version instead of blocking ──
172
+ if (r.blocked && !isEns) {
173
+ const safe = await findSafeVersion(packageName, r.resolvedVersion, r.cves);
174
+ if (safe) {
175
+ r.autoBumped = true;
176
+ r.autoBumpedFrom = r.resolvedVersion;
177
+ r.autoBumpReason = safe.reason;
178
+ r.resolvedVersion = safe.version;
179
+ r.blocked = false;
180
+ r.blockReason = undefined;
181
+ r.warning = true;
182
+
183
+ r.cves = await queryOSV(packageName, safe.version).catch(() => []);
184
+ try {
185
+ const newInfo = await getPackageInfo(packageName, safe.version);
186
+ if (newInfo.exists) r.info = newInfo;
187
+ } catch { /* keep existing */ }
188
+
189
+ setResult({ ...r });
190
+ }
191
+ }
192
+
193
+ // ── Signature verification ──
136
194
  if (r.info?.exists) {
137
195
  update('signature', 'running');
138
196
  r.signatureValid = r.info.signature !== '0x'
139
197
  ? verifyChecksum(r.info.checksum, r.info.signature, r.info.author)
140
198
  : false;
141
- if (r.info.author) {
199
+ if (r.info.author && !r.ensName) {
142
200
  r.ensName = await resolveENSName(r.info.author).catch(() => null) || undefined;
143
201
  }
144
202
  setResult({ ...r });
@@ -147,6 +205,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
147
205
  update('signature', 'skip');
148
206
  }
149
207
 
208
+ // ── ChainPatrol check ──
150
209
  if (!r.info?.exists) {
151
210
  update('chainpatrol', 'running');
152
211
  const cp = await checkPackageWithChainPatrol(packageName).catch(() => null);
@@ -161,12 +220,14 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
161
220
  update('chainpatrol', 'skip');
162
221
  }
163
222
 
223
+ // ── Fileverse report ──
164
224
  if (r.info?.reportURI && !r.info.reportURI.startsWith('local://')) {
165
225
  update('report', 'done');
166
226
  } else {
167
227
  update('report', 'skip');
168
228
  }
169
229
 
230
+ // ── Block or install ──
170
231
  if (r.blocked) {
171
232
  setError(`Blocked: ${r.blockReason || 'security risk detected'}`);
172
233
  update('install', 'error');
@@ -175,7 +236,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
175
236
 
176
237
  update('install', 'running');
177
238
  try {
178
- const target = `${packageName}${version ? `@${version}` : ''}`;
239
+ const target = `${packageName}@${r.resolvedVersion}`;
179
240
  execSync(`npm install ${target}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
180
241
  } catch { /* non-fatal */ }
181
242
  update('install', 'done');
@@ -191,12 +252,43 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
191
252
  return (
192
253
  <Box flexDirection="column">
193
254
  <Header subtitle="install" />
194
- {result && <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>}
255
+ {result && (
256
+ <Box>
257
+ <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>
258
+ {result.ensName && result.resolved?.source === 'ens' && (
259
+ <Text color="cyan"> via {result.ensName}</Text>
260
+ )}
261
+ {result.autoBumped && (
262
+ <Text color="yellow"> (bumped from {result.autoBumpedFrom})</Text>
263
+ )}
264
+ </Box>
265
+ )}
195
266
  <Text> </Text>
196
267
 
197
268
  <StatusLine label="Resolve version" status={steps.resolve}
198
269
  detail={steps.resolve === 'done' ? result?.resolvedVersion : undefined} />
199
270
 
271
+ {isEns && (
272
+ <StatusLine label="Resolve ENS author" status={steps.ens} detail={ensDetail} />
273
+ )}
274
+ {steps.ens === 'done' && result?.resolved?.source === 'ens' && (
275
+ <Box flexDirection="column" marginLeft={4}>
276
+ <Box>
277
+ <Text color="gray">Author: </Text>
278
+ <Text color="green">{result.ensName}</Text>
279
+ {result.resolved.authorAddress && (
280
+ <Text color="gray"> ({truncateAddress(result.resolved.authorAddress)})</Text>
281
+ )}
282
+ <Text color="green"> ✓ on-chain</Text>
283
+ </Box>
284
+ <Box>
285
+ <Text color="gray">Version: </Text>
286
+ <Text color="cyan">{result.resolvedVersion}</Text>
287
+ <Text color="gray"> (safest on-chain version)</Text>
288
+ </Box>
289
+ </Box>
290
+ )}
291
+
200
292
  <StatusLine label="Query CVE database (OSV)" status={steps.cve}
201
293
  detail={steps.cve === 'done'
202
294
  ? (result?.cves.length
@@ -231,6 +323,22 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
231
323
  </Box>
232
324
  )}
233
325
 
326
+ {result?.autoBumped && (
327
+ <Box flexDirection="column" marginLeft={4} marginTop={0}>
328
+ <Box>
329
+ <Text color="yellow">↑ Auto-bumped: </Text>
330
+ <Text color="red">{result.autoBumpedFrom}</Text>
331
+ <Text color="yellow"> → </Text>
332
+ <Text color="green" bold>{result.resolvedVersion}</Text>
333
+ </Box>
334
+ {result.autoBumpReason && (
335
+ <Box marginLeft={2}>
336
+ <Text color="gray">{result.autoBumpReason}</Text>
337
+ </Box>
338
+ )}
339
+ </Box>
340
+ )}
341
+
234
342
  <StatusLine label="On-chain registry lookup" status={steps.onchain}
235
343
  detail={steps.onchain === 'done' && result?.info?.exists
236
344
  ? `${result.info.aggregateScore}/100 (${classifyRisk(result.info.aggregateScore)})`
@@ -269,14 +377,30 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
269
377
  <Box flexDirection="column" marginTop={1}>
270
378
  <Text color="gray">────────────────────────────────────────</Text>
271
379
  <Text color="white" bold> Security Summary</Text>
380
+ {result.resolved?.source === 'ens' && (
381
+ <Box marginLeft={2}>
382
+ <Text color="gray">Resolved: </Text>
383
+ <Text color="green">{result.ensName}</Text>
384
+ <Text color="gray"> → </Text>
385
+ <Text color="cyan">{result.resolvedVersion}</Text>
386
+ </Box>
387
+ )}
388
+ {result.autoBumped && (
389
+ <Box marginLeft={2}>
390
+ <Text color="gray">Bumped: </Text>
391
+ <Text color="red">{result.autoBumpedFrom}</Text>
392
+ <Text color="gray"> → </Text>
393
+ <Text color="green">{result.resolvedVersion}</Text>
394
+ </Box>
395
+ )}
272
396
  {result.info?.exists && (
273
397
  <Box marginLeft={2}>
274
- <Text color="gray">Risk: </Text>
398
+ <Text color="gray">Risk: </Text>
275
399
  <RiskBadge level={classifyRisk(result.info.aggregateScore)} score={result.info.aggregateScore} />
276
400
  </Box>
277
401
  )}
278
402
  <Box marginLeft={2}>
279
- <Text color="gray">CVEs: </Text>
403
+ <Text color="gray">CVEs: </Text>
280
404
  {result.cves.length > 0 ? (
281
405
  <Text color={severeCount > 0 ? 'red' : 'yellow'}>
282
406
  {result.cves.length} known ({cveCounts.critical > 0 ? `${cveCounts.critical} critical, ` : ''}{cveCounts.high} high, {cveCounts.medium} medium, {cveCounts.low} low)
@@ -287,19 +411,19 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
287
411
  </Box>
288
412
  {result.info?.exists && (
289
413
  <Box marginLeft={2}>
290
- <Text color="gray">Signature: </Text>
414
+ <Text color="gray">Signature:</Text>
291
415
  <Text color={result.signatureValid ? 'green' : 'red'}>
292
- {result.signatureValid ? 'verified' : 'unverified'}
416
+ {' '}{result.signatureValid ? 'verified' : 'unverified'}
293
417
  </Text>
294
418
  </Box>
295
419
  )}
296
420
  {result.ensName && (
297
421
  <Box marginLeft={2}>
298
- <Text color="gray">Author: </Text>
422
+ <Text color="gray">Author: </Text>
299
423
  <Text color="green">{result.ensName}</Text>
300
424
  </Box>
301
425
  )}
302
- {result.warning && !result.blocked && (
426
+ {result.warning && !result.blocked && !result.autoBumped && (
303
427
  <Box marginLeft={2}>
304
428
  <Text color="yellow">⚠ Vulnerabilities detected — review before using in production</Text>
305
429
  </Box>
@@ -311,7 +435,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
311
435
  <Text color="yellow"> to fix known CVEs</Text>
312
436
  </Box>
313
437
  )}
314
- {result.warning && result.safestVersion && (
438
+ {result.warning && result.safestVersion && !result.autoBumped && (
315
439
  <Box marginLeft={2}>
316
440
  <Text color="yellow">⚠ Consider using safest on-chain version: {result.safestVersion}</Text>
317
441
  </Box>
@@ -338,6 +462,11 @@ interface BulkDepResult {
338
462
  blocked: boolean;
339
463
  blockReason?: string;
340
464
  suggestedUpgrade?: string;
465
+ ensResolved?: boolean;
466
+ ensName?: string;
467
+ autoBumped?: boolean;
468
+ originalVersion?: string;
469
+ autoBumpReason?: string;
341
470
  }
342
471
 
343
472
  function BulkInstall() {
@@ -346,6 +475,9 @@ function BulkInstall() {
346
475
  const [error, setError] = useState<string | null>(null);
347
476
  const [installStatus, setInstallStatus] = useState<StepStatus>('pending');
348
477
  const [total, setTotal] = useState(0);
478
+ const [ensCount, setEnsCount] = useState(0);
479
+ const [ensResolvingStatus, setEnsResolvingStatus] = useState<StepStatus>('skip');
480
+ const [ensResolvedCount, setEnsResolvedCount] = useState(0);
349
481
 
350
482
  useEffect(() => {
351
483
  runBulk().catch((err) => setError(String(err)));
@@ -368,15 +500,79 @@ function BulkInstall() {
368
500
  return;
369
501
  }
370
502
 
503
+ // ── Phase 1: Batch-resolve all ENS names in parallel ──
504
+ const ensEntries = entries.filter(([, ver]) => isENSVersion(String(ver)));
505
+ setEnsCount(ensEntries.length);
506
+
507
+ const ensCache = new Map<string, { address: string; version: string }>();
508
+
509
+ if (ensEntries.length > 0) {
510
+ setEnsResolvingStatus('running');
511
+
512
+ const uniqueEnsNames = [...new Set(ensEntries.map(([, v]) => String(v)))];
513
+ const ensResults = await Promise.allSettled(
514
+ uniqueEnsNames.map(async (ensName) => {
515
+ const addr = await resolveAddress(ensName);
516
+ return { ensName, address: addr };
517
+ }),
518
+ );
519
+
520
+ const ensAddresses = new Map<string, string>();
521
+ for (const result of ensResults) {
522
+ if (result.status === 'fulfilled' && result.value.address) {
523
+ ensAddresses.set(result.value.ensName, result.value.address);
524
+ }
525
+ }
526
+
527
+ const ensVersionResults = await Promise.allSettled(
528
+ ensEntries.map(async ([name, ensName]) => {
529
+ const addr = ensAddresses.get(String(ensName));
530
+ if (!addr) return null;
531
+ const resolved = await resolveVersion(name, String(ensName));
532
+ return { name, ensName: String(ensName), resolved };
533
+ }),
534
+ );
535
+
536
+ for (const result of ensVersionResults) {
537
+ if (result.status === 'fulfilled' && result.value) {
538
+ const { name, ensName, resolved } = result.value;
539
+ ensCache.set(name, {
540
+ address: resolved.authorAddress || '',
541
+ version: resolved.version,
542
+ });
543
+ setEnsResolvedCount((c) => c + 1);
544
+ }
545
+ }
546
+
547
+ setEnsResolvingStatus('done');
548
+ }
549
+
550
+ // ── Phase 2: Scan each dependency ──
371
551
  const checked: BulkDepResult[] = [];
372
552
 
373
553
  for (const [name, verRange] of entries) {
374
- const rawVersion = String(verRange).replace(/^[\^~]/, '');
554
+ const rawVerStr = String(verRange);
555
+ const isEns = isENSVersion(rawVerStr);
556
+
557
+ let rawVersion: string;
558
+ let ensName: string | undefined;
559
+ let ensResolved = false;
560
+
561
+ if (isEns && ensCache.has(name)) {
562
+ const cached = ensCache.get(name)!;
563
+ rawVersion = cached.version;
564
+ ensName = rawVerStr;
565
+ ensResolved = true;
566
+ } else {
567
+ rawVersion = rawVerStr.replace(/^[\^~]/, '');
568
+ }
569
+
375
570
  const entry: BulkDepResult = {
376
571
  name, version: rawVersion,
377
572
  cves: [], cvesCritical: 0, cvesHigh: 0,
378
573
  onChain: false, score: null,
379
574
  blocked: false,
575
+ ensResolved, ensName,
380
576
  };
381
577
 
382
578
  const [osvResult, infoResult] = await Promise.allSettled([
@@ -406,6 +602,25 @@ function BulkInstall() {
406
602
  }
407
603
  }
408
604
 
605
+ // ── Auto-bump blocked deps ──
606
+ if (entry.blocked && !ensResolved) {
607
+ const safe = await findSafeVersion(name, rawVersion, entry.cves);
608
+ if (safe) {
609
+ entry.autoBumped = true;
610
+ entry.originalVersion = rawVersion;
611
+ entry.autoBumpReason = safe.reason;
612
+ entry.version = safe.version;
613
+ entry.blocked = false;
614
+ entry.blockReason = undefined;
615
+
616
+ const newCves = await queryOSV(name, safe.version).catch(() => []);
617
+ entry.cves = newCves;
618
+ const newCounts = categorizeCVEs(newCves);
619
+ entry.cvesCritical = newCounts.critical;
620
+ entry.cvesHigh = newCounts.high;
621
+ }
622
+ }
623
+
409
624
  checked.push(entry);
410
625
  setDeps([...checked]);
411
626
  }
@@ -419,16 +634,24 @@ function BulkInstall() {
419
634
  return;
420
635
  }
421
636
 
637
+ // Build npm install command with correct versions
638
+ const bumpedDeps = checked.filter((d) => d.autoBumped || d.ensResolved);
422
639
  setInstallStatus('running');
423
640
  try {
424
- execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
641
+ if (bumpedDeps.length > 0) {
642
+ const args = checked.map((d) => `${d.name}@${d.version}`).join(' ');
643
+ execSync(`npm install ${args}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
644
+ } else {
645
+ execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
646
+ }
425
647
  } catch { /* non-fatal */ }
426
648
  setInstallStatus('done');
427
649
  }
428
650
 
429
651
  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));
652
+ const bumpedDeps = deps.filter((d) => d.autoBumped || d.ensResolved);
653
+ const warnDeps = deps.filter((d) => !d.blocked && !d.autoBumped && !d.ensResolved && (d.cvesHigh > 0 || (d.score !== null && d.score >= MEDIUM_RISK_THRESHOLD)));
654
+ const safeDeps = deps.filter((d) => !d.blocked && !d.autoBumped && !d.ensResolved && d.cvesHigh === 0 && (d.score === null || d.score < MEDIUM_RISK_THRESHOLD));
432
655
  const totalCves = deps.reduce((s, d) => s + d.cves.length, 0);
433
656
 
434
657
  return (
@@ -436,13 +659,48 @@ function BulkInstall() {
436
659
  <Header subtitle="install" />
437
660
  <Text> </Text>
438
661
 
662
+ {ensCount > 0 && (
663
+ <StatusLine label={`Resolve ${ensCount} ENS author(s)`} status={ensResolvingStatus}
664
+ detail={ensResolvingStatus === 'done' ? `${ensResolvedCount} resolved` : ensResolvingStatus === 'running' ? 'resolving...' : undefined} />
665
+ )}
666
+
439
667
  <StatusLine label={`Scanning ${total} dependencies`} status={scanning ? 'running' : 'done'}
440
668
  detail={!scanning ? `${deps.length} checked` : `${deps.length}/${total}`} />
441
669
 
442
670
  {deps.length > 0 && (
443
671
  <Box flexDirection="column" marginTop={1}>
444
- {blockedDeps.length > 0 && (
672
+ {bumpedDeps.length > 0 && (
445
673
  <Box flexDirection="column">
674
+ <Text color="cyan" bold> ENS / AUTO-BUMPED ({bumpedDeps.length})</Text>
675
+ {bumpedDeps.map((d) => (
676
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
677
+ <Box>
678
+ {d.ensResolved ? (
679
+ <Text color="cyan">◈ </Text>
680
+ ) : (
681
+ <Text color="yellow">↑ </Text>
682
+ )}
683
+ <Text color="white" bold>{d.name}</Text>
684
+ <Text color="green">@{d.version}</Text>
685
+ {d.ensResolved && d.ensName && (
686
+ <Text color="cyan"> via {d.ensName}</Text>
687
+ )}
688
+ {d.autoBumped && d.originalVersion && (
689
+ <Text color="yellow"> bumped from {d.originalVersion}</Text>
690
+ )}
691
+ </Box>
692
+ {d.autoBumpReason && (
693
+ <Box marginLeft={4}>
694
+ <Text color="gray">{d.autoBumpReason}</Text>
695
+ </Box>
696
+ )}
697
+ </Box>
698
+ ))}
699
+ </Box>
700
+ )}
701
+
702
+ {blockedDeps.length > 0 && (
703
+ <Box flexDirection="column" marginTop={bumpedDeps.length > 0 ? 1 : 0}>
446
704
  <Text color="red" bold> BLOCKED ({blockedDeps.length})</Text>
447
705
  {blockedDeps.map((d) => (
448
706
  <Box key={d.name} flexDirection="column" marginLeft={2}>
@@ -476,7 +734,7 @@ function BulkInstall() {
476
734
  )}
477
735
 
478
736
  {warnDeps.length > 0 && (
479
- <Box flexDirection="column" marginTop={blockedDeps.length > 0 ? 1 : 0}>
737
+ <Box flexDirection="column" marginTop={(blockedDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
480
738
  <Text color="yellow" bold> WARNING ({warnDeps.length})</Text>
481
739
  {warnDeps.map((d) => (
482
740
  <Box key={d.name} marginLeft={2}>
@@ -491,7 +749,7 @@ function BulkInstall() {
491
749
  )}
492
750
 
493
751
  {safeDeps.length > 0 && (
494
- <Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length) > 0 ? 1 : 0}>
752
+ <Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
495
753
  <Text color="green" bold> SAFE ({safeDeps.length})</Text>
496
754
  {safeDeps.map((d) => (
497
755
  <Box key={d.name} marginLeft={2}>
@@ -518,7 +776,7 @@ function BulkInstall() {
518
776
 
519
777
  <Box marginTop={1}>
520
778
  <Text color={blockedDeps.length > 0 ? 'red' : totalCves > 0 ? 'yellow' : 'green'} bold>
521
- {deps.length} packages scanned: {blockedDeps.length} blocked, {warnDeps.length} warnings, {totalCves} CVEs
779
+ {deps.length} packages scanned: {blockedDeps.length} blocked, {bumpedDeps.length} resolved, {warnDeps.length} warnings, {totalCves} CVEs
522
780
  </Text>
523
781
  </Box>
524
782
 
@@ -106,7 +106,8 @@ function Help() {
106
106
  <Box flexDirection="column" marginLeft={2}>
107
107
  <Text color="cyan" bold>Security commands:</Text>
108
108
  <Text> opm push [--token t] [--otp c] Sign, scan, publish, register</Text>
109
- <Text> opm install [pkg] Install with on-chain security verification</Text>
109
+ <Text> opm install [pkg[@ver]] Install with on-chain security verification</Text>
110
+ <Text> opm install pkg@ens.eth Install safest version by ENS author</Text>
110
111
  <Text> opm check Scan all deps: typosquats, CVEs, AI analysis</Text>
111
112
  <Text> opm fix Auto-fix typosquats and vulnerable versions</Text>
112
113
  <Text> opm audit Scan all deps against on-chain security data</Text>
@@ -135,6 +136,7 @@ function Help() {
135
136
  <Text> </Text>
136
137
  <Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
137
138
  <Text color="gray"> view name.eth → author profile, view pkg → info</Text>
139
+ <Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
138
140
  <Text> </Text>
139
141
  <Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
140
142
  <Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>