opmsec 0.1.5 → 0.1.52

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 (71) hide show
  1. package/.env.example +1 -2
  2. package/package.json +3 -3
  3. package/packages/cli/src/commands/audit.tsx +72 -5
  4. package/packages/cli/src/commands/check.tsx +80 -12
  5. package/packages/cli/src/commands/push.tsx +40 -3
  6. package/packages/cli/src/index.tsx +16 -0
  7. package/packages/cli/src/services/dep-graph.ts +298 -0
  8. package/packages/cli/src/services/ens-records.ts +5 -0
  9. package/packages/cli/src/services/lockfile.ts +224 -0
  10. package/packages/cli/src/services/merkle.ts +52 -0
  11. package/packages/cli/src/services/osv.ts +50 -0
  12. package/packages/core/src/constants.ts +2 -1
  13. package/packages/core/src/model-rankings.ts +12 -4
  14. package/packages/core/src/types.ts +38 -0
  15. package/packages/core/src/utils.ts +18 -0
  16. package/packages/scanner/src/agents/base-agent.ts +22 -10
  17. package/packages/scanner/src/queue/memory-queue.ts +23 -9
  18. package/packages/scanner/src/services/openrouter.ts +8 -2
  19. package/packages/web/.next/BUILD_ID +1 -1
  20. package/packages/web/.next/app-build-manifest.json +19 -7
  21. package/packages/web/.next/build-manifest.json +19 -6
  22. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/packages/web/.next/server/app/_not-found.html +1 -1
  24. package/packages/web/.next/server/app/_not-found.rsc +1 -1
  25. package/packages/web/.next/server/app/index.html +2 -2
  26. package/packages/web/.next/server/app/index.rsc +2 -2
  27. package/packages/web/.next/server/app/page.js +8 -272
  28. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/packages/web/.next/server/app-paths-manifest.json +1 -0
  30. package/packages/web/.next/server/middleware-build-manifest.js +1 -22
  31. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  32. package/packages/web/.next/server/next-font-manifest.js +1 -1
  33. package/packages/web/.next/server/pages/404.html +1 -1
  34. package/packages/web/.next/server/pages/500.html +1 -1
  35. package/packages/web/.next/server/pages-manifest.json +6 -1
  36. package/packages/web/.next/server/server-reference-manifest.js +1 -1
  37. package/packages/web/.next/server/server-reference-manifest.json +1 -5
  38. package/packages/web/.next/server/webpack-runtime.js +1 -209
  39. package/packages/web/.next/static/chunks/app/page-7d2a23ad744d529a.js +1 -0
  40. package/packages/web/.next/trace +2 -2
  41. package/packages/web/app/page.tsx +0 -2
  42. package/packages/web/.next/static/chunks/app/layout.js +0 -69
  43. package/packages/web/.next/static/chunks/app/page-7e086379698b9fb0.js +0 -1
  44. package/packages/web/.next/static/chunks/app/page.js +0 -357
  45. package/packages/web/.next/static/chunks/webpack.js +0 -1393
  46. package/packages/web/.next/static/development/_buildManifest.js +0 -1
  47. package/packages/web/.next/static/development/_ssgManifest.js +0 -1
  48. package/packages/web/.next/static/webpack/16f18baa938a434c.webpack.hot-update.json +0 -1
  49. package/packages/web/.next/static/webpack/5fe9fe8578f9c3d2.webpack.hot-update.json +0 -1
  50. package/packages/web/.next/static/webpack/653e365406c0d9ac.webpack.hot-update.json +0 -1
  51. package/packages/web/.next/static/webpack/6800169a899e3a8b.webpack.hot-update.json +0 -1
  52. package/packages/web/.next/static/webpack/73c7d02260cc80e4.webpack.hot-update.json +0 -1
  53. package/packages/web/.next/static/webpack/a2d85d19aa028de1.webpack.hot-update.json +0 -1
  54. package/packages/web/.next/static/webpack/app/layout.16f18baa938a434c.hot-update.js +0 -22
  55. package/packages/web/.next/static/webpack/app/layout.5fe9fe8578f9c3d2.hot-update.js +0 -22
  56. package/packages/web/.next/static/webpack/app/layout.653e365406c0d9ac.hot-update.js +0 -22
  57. package/packages/web/.next/static/webpack/app/layout.6800169a899e3a8b.hot-update.js +0 -22
  58. package/packages/web/.next/static/webpack/app/layout.73c7d02260cc80e4.hot-update.js +0 -22
  59. package/packages/web/.next/static/webpack/app/layout.a2d85d19aa028de1.hot-update.js +0 -22
  60. package/packages/web/.next/static/webpack/app/page.653e365406c0d9ac.hot-update.js +0 -22
  61. package/packages/web/.next/static/webpack/app/page.6800169a899e3a8b.hot-update.js +0 -22
  62. package/packages/web/.next/static/webpack/app/page.73c7d02260cc80e4.hot-update.js +0 -22
  63. package/packages/web/.next/static/webpack/app/page.a2d85d19aa028de1.hot-update.js +0 -22
  64. package/packages/web/.next/static/webpack/webpack.16f18baa938a434c.hot-update.js +0 -12
  65. package/packages/web/.next/static/webpack/webpack.5fe9fe8578f9c3d2.hot-update.js +0 -12
  66. package/packages/web/.next/static/webpack/webpack.653e365406c0d9ac.hot-update.js +0 -12
  67. package/packages/web/.next/static/webpack/webpack.6800169a899e3a8b.hot-update.js +0 -12
  68. package/packages/web/.next/static/webpack/webpack.73c7d02260cc80e4.hot-update.js +0 -12
  69. package/packages/web/.next/static/webpack/webpack.a2d85d19aa028de1.hot-update.js +0 -12
  70. /package/packages/web/.next/static/{0esGzFBCzREfVwijEGDfL → dQsL29tmXanBGrmJ9Agh2}/_buildManifest.js +0 -0
  71. /package/packages/web/.next/static/{0esGzFBCzREfVwijEGDfL → dQsL29tmXanBGrmJ9Agh2}/_ssgManifest.js +0 -0
package/.env.example CHANGED
@@ -21,5 +21,4 @@ FILEVERSE_API_KEY= # from ddocs.new → Settings → Developer
21
21
  # ETH_SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
22
22
  # FILEVERSE_API_URL=http://localhost:8001
23
23
  # CHAINPATROL_API_KEY= # optional, for blocklist checks
24
- # ARTIFICIAL_ANALYSIS_API_KEY= # optional, for model-weighted scoring
25
- PINATA_JWT=your_pinata_jwt_here
24
+ # ARTIFICIAL_ANALYSIS_API_KEY= # optional, for model-weighted scoring
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opmsec",
3
- "version": "0.1.5",
3
+ "version": "0.1.52",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "opm": "packages/cli/src/index.tsx"
@@ -32,9 +32,9 @@
32
32
  "bun-types": "latest"
33
33
  },
34
34
  "opm": {
35
- "signature": "0xe975b8c26bb6dc450a51c01fa1e5cb2f04b35d7535cde3432def8b4ee209c31158a4743c7477d45b1da6445c3ad11608e515fff40850aba8848d22d01aaf64621b",
35
+ "signature": "0xa1c1b29e1b878ba3cee4cd14d75e79e5a7e683bbd57ef7cc2917c2693f2310400aaae5dccfb655498cb26e2e6ced586fd3d987ca484f82b8721e480836e88fa61b",
36
36
  "author": "0x2a3942EbDd8c5ea3E66D3fC4301F56d0F15d4bE2",
37
37
  "ensName": "djpaiethg.eth",
38
- "checksum": "0x2633d7353118b1451f120ebb71a31c2c18901388f37ac9943090bb57a3c5e454"
38
+ "checksum": "0x0ce8eaad637d74e4172c3b9de6af85ec600bb0726dd53bc3d1520615542daa64"
39
39
  }
40
40
  }
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { HIGH_RISK_THRESHOLD, MEDIUM_RISK_THRESHOLD, truncateAddress, classifyRisk } from '@opm/core';
4
+ import type { DepGraphResult, FlaggedPath } from '@opm/core';
4
5
  import { Header } from '../components/Header';
5
6
  import { RiskBadge } from '../components/RiskBadge';
6
7
  import { StatusLine } from '../components/StatusLine';
@@ -8,6 +9,7 @@ import { getPackageInfo } from '../services/contract';
8
9
  import { checkPackageWithChainPatrol } from '../services/chainpatrol';
9
10
  import { queryOSV, getOSVSeverity } from '../services/osv';
10
11
  import { resolveVersion } from '../services/version';
12
+ import { resolveAndScanDepGraph } from '../services/dep-graph';
11
13
  import * as fs from 'fs';
12
14
  import * as path from 'path';
13
15
 
@@ -22,8 +24,12 @@ interface DepResult {
22
24
  cveHighCount: number;
23
25
  }
24
26
 
27
+ type Phase = 'graph' | 'direct' | 'done';
28
+
25
29
  export function AuditCommand() {
26
- const [status, setStatus] = useState<'running' | 'done'>('running');
30
+ const [phase, setPhase] = useState<Phase>('graph');
31
+ const [graphStatus, setGraphStatus] = useState('Resolving...');
32
+ const [graph, setGraph] = useState<DepGraphResult | null>(null);
27
33
  const [results, setResults] = useState<DepResult[]>([]);
28
34
  const [error, setError] = useState<string | null>(null);
29
35
 
@@ -32,6 +38,16 @@ export function AuditCommand() {
32
38
  }, []);
33
39
 
34
40
  async function runAudit() {
41
+ let graphResult: DepGraphResult | null = null;
42
+ try {
43
+ graphResult = await resolveAndScanDepGraph(process.cwd(), (msg) => setGraphStatus(msg));
44
+ setGraph(graphResult);
45
+ } catch {
46
+ setGraphStatus('Lockfile not found — direct deps only');
47
+ }
48
+
49
+ setPhase('direct');
50
+
35
51
  const pkgPath = path.resolve('package.json');
36
52
  if (!fs.existsSync(pkgPath)) throw new Error('No package.json found');
37
53
 
@@ -40,7 +56,7 @@ export function AuditCommand() {
40
56
  const entries = Object.entries(deps) as [string, string][];
41
57
 
42
58
  if (entries.length === 0) {
43
- setStatus('done');
59
+ setPhase('done');
44
60
  return;
45
61
  }
46
62
 
@@ -76,7 +92,7 @@ export function AuditCommand() {
76
92
  setResults([...checked]);
77
93
  }
78
94
 
79
- setStatus('done');
95
+ setPhase('done');
80
96
  }
81
97
 
82
98
  const high = results.filter((r) => r.score !== null && r.score >= HIGH_RISK_THRESHOLD);
@@ -88,8 +104,17 @@ export function AuditCommand() {
88
104
  return (
89
105
  <Box flexDirection="column">
90
106
  <Header subtitle="audit" />
91
- <StatusLine label={`Checking ${results.length} dependencies`} status={status === 'done' ? 'done' : 'running'} />
107
+
108
+ <StatusLine label="Resolving dependency tree"
109
+ status={phase === 'graph' ? 'running' : 'done'}
110
+ detail={phase === 'graph' ? graphStatus : graph
111
+ ? `${graph.totalDeps} deps (${graph.directDeps} direct, ${graph.transitiveDeps} transitive)`
112
+ : 'direct deps only'} />
113
+
114
+ <StatusLine label={`Checking ${results.length} direct dependencies`}
115
+ status={phase === 'done' ? 'done' : 'running'} />
92
116
  <Text> </Text>
117
+
93
118
  {results.map((r) => (
94
119
  <Box key={r.name} marginLeft={2}>
95
120
  <Box width={30}>
@@ -113,7 +138,38 @@ export function AuditCommand() {
113
138
  )}
114
139
  </Box>
115
140
  ))}
116
- {status === 'done' && (
141
+
142
+ {/* Transitive flagged paths from dep graph */}
143
+ {phase === 'done' && graph && graph.flaggedPaths.length > 0 && (
144
+ <Box flexDirection="column" marginTop={1}>
145
+ <Text color="gray">────────────────────────────────────────</Text>
146
+ <Text color="white" bold> Transitive Risk Paths ({graph.flaggedPaths.length})</Text>
147
+ {graph.flaggedPaths.slice(0, 10).map((fp, i) => {
148
+ const sevColor = fp.severity === 'CRITICAL' || fp.severity === 'HIGH' ? 'red' : 'yellow';
149
+ const chain = fp.path.slice(1).join(' → ');
150
+ return (
151
+ <Box key={i} marginLeft={2} flexDirection="column">
152
+ <Box>
153
+ <Text color={sevColor}>{fp.severity === 'CRITICAL' || fp.severity === 'HIGH' ? '✖' : '⚠'} </Text>
154
+ <Text color={sevColor} bold>{fp.severity.padEnd(9)}</Text>
155
+ <Text color="white">{chain}</Text>
156
+ </Box>
157
+ <Box marginLeft={12}>
158
+ <Text color="gray">{fp.reason}</Text>
159
+ {fp.fix && <Text color="green"> → {fp.fix}</Text>}
160
+ </Box>
161
+ </Box>
162
+ );
163
+ })}
164
+ {graph.flaggedPaths.length > 10 && (
165
+ <Box marginLeft={2}>
166
+ <Text color="gray">... and {graph.flaggedPaths.length - 10} more</Text>
167
+ </Box>
168
+ )}
169
+ </Box>
170
+ )}
171
+
172
+ {phase === 'done' && (
117
173
  <Box flexDirection="column" marginTop={1}>
118
174
  <Text color="gray">────────────────────────────────────────</Text>
119
175
  <Box>
@@ -131,6 +187,17 @@ export function AuditCommand() {
131
187
  </>
132
188
  )}
133
189
  </Box>
190
+ {graph && (
191
+ <Box marginTop={0}>
192
+ <Text color="gray">📦 {graph.totalDeps} total ({graph.directDeps} direct, {graph.transitiveDeps} transitive)</Text>
193
+ {graph.flaggedPaths.length > 0 && (
194
+ <Text color="yellow"> · {graph.flaggedPaths.length} transitive risk paths</Text>
195
+ )}
196
+ </Box>
197
+ )}
198
+ {graph && (
199
+ <Text color="gray">🔗 Merkle root: {graph.merkleRoot.slice(0, 18)}...</Text>
200
+ )}
134
201
  {high.length > 0 && (
135
202
  <Text color="red" bold>⚠ {high.length} package(s) above risk threshold!</Text>
136
203
  )}
@@ -1,11 +1,12 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { CHECK_SYSTEM_PROMPT, buildCheckPrompt, classifyRisk, getModelRankingFor } from '@opm/core';
4
- import type { DepEntry, CheckReport, CheckDepResult, CheckAgentResult } from '@opm/core';
4
+ import type { DepEntry, CheckReport, CheckDepResult, CheckAgentResult, DepGraphResult, FlaggedPath } from '@opm/core';
5
5
  import { Header } from '../components/Header';
6
6
  import { StatusLine } from '../components/StatusLine';
7
7
  import { RiskBadge } from '../components/RiskBadge';
8
8
  import { Hyperlink } from '../components/Hyperlink';
9
+ import { resolveAndScanDepGraph } from '../services/dep-graph';
9
10
  import { queryOSV, getOSVSeverity, getFixedVersion } from '../services/osv';
10
11
  import { getPackageInfo } from '../services/contract';
11
12
  import { detectTyposquatBatch } from '../services/typosquat';
@@ -13,10 +14,12 @@ import { callLLMRaw, getAgentConfigs, uploadCheckReportToFileverse } from '@opm/
13
14
  import * as fs from 'fs';
14
15
  import * as path from 'path';
15
16
 
16
- type Phase = 'scanning' | 'agents' | 'upload' | 'done';
17
+ type Phase = 'graph' | 'scanning' | 'agents' | 'upload' | 'done';
17
18
 
18
19
  export function CheckCommand() {
19
- const [phase, setPhase] = useState<Phase>('scanning');
20
+ const [phase, setPhase] = useState<Phase>('graph');
21
+ const [graphStatus, setGraphStatus] = useState('Resolving...');
22
+ const [graph, setGraph] = useState<DepGraphResult | null>(null);
20
23
  const [report, setReport] = useState<CheckReport | null>(null);
21
24
  const [reportLink, setReportLink] = useState<string | null>(null);
22
25
  const [uploadError, setUploadError] = useState<string | null>(null);
@@ -34,6 +37,16 @@ export function CheckCommand() {
34
37
  const projectName = pkgJson.name || path.basename(process.cwd());
35
38
  const deps = Object.entries(pkgJson.dependencies || {}) as [string, string][];
36
39
  const devDeps = Object.entries(pkgJson.devDependencies || {}) as [string, string][];
40
+
41
+ let graphResult: DepGraphResult | null = null;
42
+ try {
43
+ graphResult = await resolveAndScanDepGraph(process.cwd(), (msg) => setGraphStatus(msg));
44
+ setGraph(graphResult);
45
+ } catch {
46
+ setGraphStatus('Lockfile not found — scanning direct deps only');
47
+ }
48
+
49
+ setPhase('scanning');
37
50
  const allEntries = [
38
51
  ...deps.map(([n, v]) => ({ n, v: clean(v) })),
39
52
  ...devDeps.map(([n, v]) => ({ n, v: clean(v) })),
@@ -120,10 +133,8 @@ export function CheckCommand() {
120
133
  };
121
134
  setReport(checkReport);
122
135
 
123
- setPhase('upload');
124
- if (!process.env.FILEVERSE_API_KEY) {
125
- setUploadError('FILEVERSE_API_KEY not set');
126
- } else {
136
+ if (process.env.FILEVERSE_API_KEY) {
137
+ setPhase('upload');
127
138
  try {
128
139
  const uploadResult = await uploadCheckReportToFileverse(checkReport);
129
140
  setReportLink(uploadResult.link);
@@ -154,22 +165,46 @@ export function CheckCommand() {
154
165
  <Header subtitle="check" />
155
166
  <Text> </Text>
156
167
 
157
- <StatusLine label={`Scanning ${report?.totalDeps || '...'} dependencies`}
158
- status={phase === 'scanning' ? 'running' : 'done'}
159
- detail={phase === 'scanning' ? 'typosquats + CVEs + on-chain (parallel)' : `${report?.totalDeps} checked`} />
168
+ <StatusLine label="Resolving dependency tree"
169
+ status={phase === 'graph' ? 'running' : 'done'}
170
+ detail={phase === 'graph' ? graphStatus : graph
171
+ ? `${graph.totalDeps} deps (${graph.directDeps} direct, ${graph.transitiveDeps} transitive)`
172
+ : 'direct deps only'} />
173
+
174
+ {phase !== 'graph' && (
175
+ <StatusLine label={`Scanning ${report?.totalDeps || '...'} direct dependencies`}
176
+ status={phase === 'scanning' ? 'running' : 'done'}
177
+ detail={phase === 'scanning' ? 'typosquats + CVEs + on-chain' : `${report?.totalDeps} checked`} />
178
+ )}
160
179
 
161
- {phase !== 'scanning' && (
180
+ {phase !== 'graph' && phase !== 'scanning' && (
162
181
  <StatusLine label="AI agents analyzing dependency tree"
163
182
  status={phase === 'agents' ? 'running' : 'done'}
164
183
  detail={report?.agents.length ? `${report.agents.length} agents` : undefined} />
165
184
  )}
166
185
 
167
- {(phase === 'upload' || phase === 'done') && (
186
+ {process.env.FILEVERSE_API_KEY && (phase === 'upload' || phase === 'done') && (
168
187
  <StatusLine label="Upload report to Fileverse"
169
188
  status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'}
170
189
  detail={uploadError || undefined} />
171
190
  )}
172
191
 
192
+ {/* Dep graph flagged paths */}
193
+ {phase === 'done' && graph && graph.flaggedPaths.length > 0 && (
194
+ <Box flexDirection="column" marginTop={1}>
195
+ <Text color="gray">────────────────────────────────────────</Text>
196
+ <Text color="white" bold> Flagged Dependency Paths ({graph.flaggedPaths.length})</Text>
197
+ {graph.flaggedPaths.slice(0, 15).map((fp, i) => (
198
+ <FlaggedPathRow key={i} fp={fp} />
199
+ ))}
200
+ {graph.flaggedPaths.length > 15 && (
201
+ <Box marginLeft={2} marginTop={1}>
202
+ <Text color="gray">... and {graph.flaggedPaths.length - 15} more</Text>
203
+ </Box>
204
+ )}
205
+ </Box>
206
+ )}
207
+
173
208
  {phase === 'done' && report && (
174
209
  <Box flexDirection="column" marginTop={1}>
175
210
  {typosquats.length > 0 && (
@@ -289,12 +324,25 @@ export function CheckCommand() {
289
324
  <Text color="gray">────────────────────────────────────────</Text>
290
325
  <Text color="white" bold> Summary</Text>
291
326
  <Box marginLeft={2} flexDirection="column">
327
+ {graph && (
328
+ <Text color="white">
329
+ 📦 {graph.totalDeps} deps ({graph.directDeps} direct, {graph.transitiveDeps} transitive)
330
+ </Text>
331
+ )}
292
332
  <Text>{typosquats.length > 0 ? '🔴' : '🟢'} Typosquats: {typosquats.length}</Text>
293
333
  <Text>{criticalCves.length > 0 ? '🔴' : cveWarnings.length > 0 ? '🟡' : '🟢'} CVEs: {criticalCves.length + cveWarnings.length} packages ({criticalCves.length} critical)</Text>
294
334
  <Text>{highRisk.length > 0 ? '🔴' : '🟢'} On-chain risk: {highRisk.length} high-risk</Text>
295
335
  {report.agents.length > 0 && (
296
336
  <Text>{uniqueAgentFlags.length > 0 ? '🟡' : '🟢'} AI agents: {uniqueAgentFlags.length} flagged</Text>
297
337
  )}
338
+ {graph && graph.flaggedPaths.length > 0 && (
339
+ <Text>
340
+ {graph.stats.critical > 0 ? '🔴' : '🟡'} Transitive risks: {graph.flaggedPaths.length} flagged paths
341
+ </Text>
342
+ )}
343
+ {graph && (
344
+ <Text color="gray">🔗 Merkle root: {graph.merkleRoot.slice(0, 18)}...</Text>
345
+ )}
298
346
  </Box>
299
347
  {reportLink && (
300
348
  <Box marginLeft={2} marginTop={1}>
@@ -318,6 +366,26 @@ export function CheckCommand() {
318
366
  );
319
367
  }
320
368
 
369
+ function FlaggedPathRow({ fp }: { fp: FlaggedPath }) {
370
+ const sevColor = fp.severity === 'CRITICAL' ? 'red' : fp.severity === 'HIGH' ? 'red' : 'yellow';
371
+ const sevIcon = fp.severity === 'CRITICAL' || fp.severity === 'HIGH' ? '✖' : '⚠';
372
+ const chain = fp.path.slice(1).join(' → ');
373
+
374
+ return (
375
+ <Box flexDirection="column" marginLeft={2}>
376
+ <Box>
377
+ <Text color={sevColor}>{sevIcon} </Text>
378
+ <Text color={sevColor} bold>{fp.severity.padEnd(9)}</Text>
379
+ <Text color="white">{chain}</Text>
380
+ </Box>
381
+ <Box marginLeft={12}>
382
+ <Text color="gray">{fp.reason}</Text>
383
+ {fp.fix && <Text color="green"> → fix: {fp.fix}</Text>}
384
+ </Box>
385
+ </Box>
386
+ );
387
+ }
388
+
321
389
  function clean(v: string): string { return String(v).replace(/^[\^~]/, ''); }
322
390
 
323
391
  function compareSemver(a: string, b: string): number {
@@ -10,7 +10,8 @@ import { computeChecksum, signChecksumAsync } from '../services/signature';
10
10
  import { resolveENSName } from '../services/ens';
11
11
  import { registerPackageOnChain } from '../services/contract';
12
12
  import { writeENSRecords, buildOPMRecords, readOPMRecords, createPackageSubname, setENSContenthash, parseFileverseLink, readFileverseContentHash } from '../services/ens-records';
13
- import { enqueueScan } from '@opm/scanner';
13
+ import { enqueueScan, submitScoreOnChain, setReportURIOnChain } from '@opm/scanner';
14
+ import { resolveAndScanDepGraph } from '../services/dep-graph';
14
15
  import * as fs from 'fs';
15
16
  import * as path from 'path';
16
17
  import { execSync } from 'child_process';
@@ -46,6 +47,7 @@ interface PushResult {
46
47
  ensRecordsCount?: number;
47
48
  ensSubname?: string;
48
49
  ipfsContenthash?: string;
50
+ merkleRoot?: string;
49
51
  }
50
52
 
51
53
  interface PushCommandProps {
@@ -105,10 +107,11 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
105
107
  let finalReportURI: string | undefined;
106
108
  let finalRiskScore: number | undefined;
107
109
  let finalIpfsHash: string | undefined;
110
+ let scanAgents: AgentEntry[] = [];
108
111
  try {
109
112
  const scanResult = await enqueueScan(name, version, (msg) =>
110
113
  setScanLogs((prev) => [...prev.slice(-8), msg]),
111
- { tarballPath: tarballFile, pkgJsonPath },
114
+ { local: { tarballPath: tarballFile, pkgJsonPath }, skipOnChainScore: true },
112
115
  );
113
116
 
114
117
  const riskScore = scanResult.report.aggregate_risk_score;
@@ -116,6 +119,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
116
119
  finalReportURI = scanResult.reportURI;
117
120
  finalRiskScore = riskScore;
118
121
  finalIpfsHash = scanResult.ipfsHash;
122
+ scanAgents = scanResult.report.agents || [];
119
123
 
120
124
  setResult((r) => ({
121
125
  ...r,
@@ -216,11 +220,43 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
216
220
  const sigBytes = new Uint8Array(Buffer.from(signature.slice(2), 'hex'));
217
221
  const txHash = await registerPackageOnChain(name, version, checksum, sigBytes, ensName);
218
222
  setResult((r) => ({ ...r, txHash }));
223
+
224
+ // Submit individual agent scores now that the version exists on-chain
225
+ for (const agent of scanAgents) {
226
+ try {
227
+ setScanLogs((prev) => [...prev.slice(-8), `[${agent.agent_id}] Submitting score (${agent.result.risk_score}) to contract...`]);
228
+ await submitScoreOnChain(name, version, agent.result.risk_score, agent.result.reasoning);
229
+ setScanLogs((prev) => [...prev.slice(-8), `[${agent.agent_id}] Score submitted on-chain ✓`]);
230
+ } catch (err: any) {
231
+ setScanLogs((prev) => [...prev.slice(-8), `[${agent.agent_id}] Score submission: ${err?.shortMessage || err?.message || 'failed'}`]);
232
+ }
233
+ }
234
+
235
+ // Set report URI on-chain
236
+ if (finalReportURI) {
237
+ try {
238
+ await setReportURIOnChain(name, version, finalReportURI);
239
+ setScanLogs((prev) => [...prev.slice(-8), 'Report URI stored on-chain ✓']);
240
+ } catch (err: any) {
241
+ setScanLogs((prev) => [...prev.slice(-8), `Report URI: ${err?.shortMessage || err?.message || 'failed'}`]);
242
+ }
243
+ }
219
244
  } catch (err: any) {
220
245
  setScanLogs((prev) => [...prev, `Registration: ${err?.shortMessage || err?.message || 'failed'}`]);
221
246
  }
222
247
  updateStep('register', 'done');
223
248
 
249
+ // ── Compute dependency graph Merkle root ──
250
+ let merkleRoot: string | undefined;
251
+ try {
252
+ const graphResult = await resolveAndScanDepGraph(process.cwd());
253
+ merkleRoot = graphResult.merkleRoot;
254
+ setResult((r) => ({ ...r, merkleRoot }));
255
+ setScanLogs((prev) => [...prev.slice(-8), `Dep tree Merkle root: ${merkleRoot!.slice(0, 18)}...`]);
256
+ } catch {
257
+ setScanLogs((prev) => [...prev.slice(-8), 'Dep tree: no lockfile — skipped Merkle root']);
258
+ }
259
+
224
260
  // ── Write package metadata to ENS text records ──
225
261
  if (ensName) {
226
262
  updateStep('ensRecords', 'running');
@@ -236,6 +272,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
236
272
  reportURI: finalReportURI,
237
273
  riskScore: finalRiskScore,
238
274
  existingPackages: existingRecords.packages,
275
+ merkleRoot,
239
276
  });
240
277
 
241
278
  const writeResult = await writeENSRecords(
@@ -420,7 +457,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
420
457
  </Box>
421
458
  <Box>
422
459
  <Text color="gray">Records: </Text>
423
- <Text color="white">url, opm.version, opm.checksum, opm.fileverse, opm.risk_score{result.ipfsContenthash ? ', contenthash' : ''}</Text>
460
+ <Text color="white">url, opm.version, opm.checksum, opm.fileverse, opm.risk_score{result.merkleRoot ? ', opm.deptree' : ''}{result.ipfsContenthash ? ', contenthash' : ''}</Text>
424
461
  </Box>
425
462
  {result.ensSubname && (
426
463
  <Box>
@@ -16,6 +16,19 @@ const args = process.argv.slice(2);
16
16
  const command = args[0];
17
17
  const rest = args.slice(1);
18
18
 
19
+ if (command === '--version' || command === '-v' || command === '-V' || command === 'version') {
20
+ try {
21
+ const fs = await import('fs');
22
+ const path = await import('path');
23
+ const pkgPath = path.resolve(__dirname, '../../../package.json');
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
25
+ console.log(`opm v${pkg.version}`);
26
+ } catch {
27
+ console.log('opm (version unknown)');
28
+ }
29
+ process.exit(0);
30
+ }
31
+
19
32
  function parsePackageArg(pkg?: string) {
20
33
  if (!pkg) return {};
21
34
  const atIdx = pkg.lastIndexOf('@');
@@ -138,6 +151,9 @@ function Help() {
138
151
  <Text color="gray"> view name.eth → author profile + OPM ENS records</Text>
139
152
  <Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
140
153
  <Text> </Text>
154
+ <Text color="cyan" bold>Other:</Text>
155
+ <Text> opm --version / -v Show OPM version</Text>
156
+ <Text> </Text>
141
157
  <Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
142
158
  <Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>
143
159
  <Text> NPM_TOKEN npm automation token (for push only)</Text>