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
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": {
@@ -65,6 +65,7 @@
65
65
  "concepts/security-model",
66
66
  "concepts/multi-agent-consensus",
67
67
  "concepts/on-chain-registry",
68
+ "concepts/ens-records",
68
69
  "concepts/zk-agent-verification"
69
70
  ]
70
71
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: 'Quickstart'
3
- description: 'Get OPM up and running in minutes. Install the CLI, configure environment variables, and run your first security-verified install.'
3
+ description: 'Get OPM up and running in minutes.'
4
4
  ---
5
5
 
6
6
  # Quickstart
@@ -19,115 +19,79 @@ bun add -g opmsec
19
19
 
20
20
  </CodeGroup>
21
21
 
22
- The `opm` binary is available globally after installation.
23
-
24
- ## 2. Set Environment Variables
22
+ ## 2. Configure (optional)
25
23
 
26
24
  <Note>
27
- **Read-only commands** (`install`, `audit`, `info`, `view`, `whois`, `check`) require **no configuration**. Defaults for RPC, contract address, and API URLs are built-in.
25
+ **Read-only commands** (`install`, `audit`, `info`, `view`, `check`) work with **zero configuration**.
28
26
  </Note>
29
27
 
30
- For **author-side commands** (`push`, `register-agent`), configure:
31
-
32
- <CodeGroup>
33
-
34
- ```bash .env
35
- # Required for opm push
36
- OPM_SIGNING_KEY=0x... # Your Ethereum private key for package signing
37
- AGENT_PRIVATE_KEY=0x... # Agent wallet key for score submission
38
- NPM_TOKEN=... # npm automation token (optional; use --token flag otherwise)
39
-
40
- # At least one required for AI scanning
41
- OPENROUTER_API_KEY=... # Multi-model access (Claude, Gemini, DeepSeek)
42
- # OR
43
- OPENAI_API_KEY=... # Fallback (GPT-4.1 family)
28
+ For publishing (`opm push`) or agent registration, create a `.env`:
44
29
 
45
- # Optional: report uploads to IPFS
46
- FILEVERSE_API_KEY=...
30
+ ```bash
31
+ OPM_SIGNING_KEY=0x... # Ethereum key for package signing
32
+ AGENT_PRIVATE_KEY=0x... # Agent wallet for on-chain score submission
33
+ OPENROUTER_API_KEY=sk-or-... # Multi-model AI scanning (Claude, Gemini, DeepSeek)
34
+ FILEVERSE_API_KEY=... # Audit report uploads to IPFS
47
35
  ```
48
36
 
49
- </CodeGroup>
50
-
51
- ## 3. Basic Usage
37
+ See [Configuration](/configuration) for the full list.
52
38
 
53
- ### Security-Verified Install
39
+ ## 3. Use It
54
40
 
55
- <CodeGroup>
41
+ ### Install with security verification
56
42
 
57
- ```bash Install specific package
43
+ ```bash
58
44
  opm install lodash
59
45
  ```
60
46
 
61
- ```bash Install with version
62
- opm install lodash@4.17.21
63
- ```
47
+ Checks CVEs, verifies on-chain risk scores, validates signatures — then installs.
64
48
 
65
- ```bash Verify all dependencies
66
- opm install
67
- ```
68
-
69
- </CodeGroup>
70
-
71
- `opm install` resolves versions against the on-chain registry, verifies ECDSA signatures, checks OSV for CVEs, and blocks installation if risk exceeds the threshold (80).
72
-
73
- ### Sign, Scan, and Publish
49
+ ### Scan your dependencies
74
50
 
75
51
  ```bash
76
- opm push
52
+ opm check
77
53
  ```
78
54
 
79
- Computes SHA-256 checksum, signs with your wallet, dispatches 3 AI agents in parallel, submits scores on-chain, uploads report to Fileverse, publishes to npm, and registers on OPMRegistry.
55
+ Finds typosquats, CVEs, and AI-detected threats across all your deps.
80
56
 
81
- ### Scan Dependencies
57
+ ### Auto-fix vulnerabilities
82
58
 
83
59
  ```bash
84
- opm check
60
+ opm fix
85
61
  ```
86
62
 
87
- Scans all `dependencies` and `devDependencies` for typosquats, CVEs, and AI-detected risks. Outputs categorized findings.
63
+ Patches typosquats and upgrades vulnerable versions in `package.json`.
88
64
 
89
- ### On-Chain Audit
65
+ ### Publish securely
90
66
 
91
67
  ```bash
92
- opm audit
68
+ opm push
93
69
  ```
94
70
 
95
- Audits all dependencies against on-chain risk scores and CVE data.
71
+ Signs your package, scans with 3 AI agents, registers on-chain, writes ENS records, publishes to npm.
96
72
 
97
- ### View Package Info
73
+ ### View package security
98
74
 
99
75
  ```bash
100
76
  opm info lodash
101
77
  ```
102
78
 
103
- Displays on-chain security metadata: author, checksum, aggregate score, report URI.
79
+ Shows on-chain risk score, agent assessments, checksums, audit report link.
104
80
 
105
- ### View Author Profile
81
+ ### Look up an author
106
82
 
107
83
  ```bash
108
- opm view vitalik.eth
84
+ opm view djpai.eth
109
85
  ```
110
86
 
111
- Resolves ENS identity, fetches avatar and text records, and displays on-chain author reputation and published packages.
112
-
113
- ## What Happens Under the Hood
87
+ ENS profile, published packages, reputation score.
114
88
 
115
- ### `opm push` Verification Pipeline
89
+ ### Register your own agent
116
90
 
117
- 1. **Checksum**: SHA-256 over packed tarball
118
- 2. **Sign**: ECDSA signature with author's Ethereum private key
119
- 3. **ENS**: Resolve author identity (Sepolia, Mainnet fallback)
120
- 4. **AI agents**: 3 models run in parallel—static analysis, risk scoring (0–100), structured JSON
121
- 5. **On-chain**: Agent wallets call `OPMRegistry.submitScore()`; aggregate computed; publish blocked if score ≥ 80
122
- 6. **Fileverse**: Upload formatted markdown report (encrypted, IPFS-synced)
123
- 7. **npm**: Publish tarball (automation token or OTP for 2FA)
124
- 8. **Registry**: `registerPackage()` stores checksum, signature, ENS name, report URI
91
+ ```bash
92
+ opm register-agent --name sentinel --model anthropic/claude-sonnet-4-20250514
93
+ ```
125
94
 
126
- ### `opm install` Verification Pipeline
95
+ Runs 10 benchmark cases, generates ZK proof, registers on-chain if 100% accurate.
127
96
 
128
- 1. Resolve version against on-chain registry
129
- 2. Query OSV API for CVE/GHSA advisories (CRITICAL blocks install)
130
- 3. Fetch on-chain risk score and agent consensus
131
- 4. Verify ECDSA signature against tarball checksum
132
- 5. ChainPatrol API fallback for packages not in registry
133
- 6. Delegate to `npm install` if all gates pass
97
+ All standard npm commands (`init`, `run`, `test`, `build`, `start`) pass through transparently.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opmsec",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -23,17 +24,17 @@
23
24
  "ethers": "^6.13.0",
24
25
  "ink": "^5.2.1",
25
26
  "react": "^18.3.1",
26
- "react-devtools-core": "^5.3.2",
27
+ "react-devtools-core": "^7.0.1",
27
28
  "terminal-image": "^4.2.0",
28
- "viem": "^2.47.2"
29
+ "viem": "^2.47.4"
29
30
  },
30
31
  "devDependencies": {
31
32
  "bun-types": "latest"
32
33
  },
33
34
  "opm": {
34
- "signature": "0xacf623c584df3b03a13588107cb0024212b50a20078a74d3a19394ea7378f2b23c679f3e351fd27393c5212f09ead8c826af1f103f535b9a802944e27b5ffa081b",
35
+ "signature": "0xe975b8c26bb6dc450a51c01fa1e5cb2f04b35d7535cde3432def8b4ee209c31158a4743c7477d45b1da6445c3ad11608e515fff40850aba8848d22d01aaf64621b",
35
36
  "author": "0x2a3942EbDd8c5ea3E66D3fC4301F56d0F15d4bE2",
36
37
  "ensName": "djpaiethg.eth",
37
- "checksum": "0x5e73c81a9f22b1381766bbea30dee60a945ece4d320f3c4f65597beb0bc19269"
38
+ "checksum": "0x2633d7353118b1451f120ebb71a31c2c18901388f37ac9943090bb57a3c5e454"
38
39
  }
39
40
  }
@@ -1,12 +1,13 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { classifyRisk, truncateAddress } from '@opm/core';
4
- import type { AuthorProfile } from '@opm/core';
4
+ import type { AuthorProfile, OPMENSRecords } 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
9
  import { resolveAddress, getENSTextRecords, type ENSProfile } from '../services/ens';
10
+ import { readOPMRecords, readENSContenthash, decodeContenthash } from '../services/ens-records';
10
11
  import {
11
12
  getAuthorProfile,
12
13
  getPackagesByAuthor, getPackagesByAuthorDirect, type AuthorPackageSummary,
@@ -20,6 +21,7 @@ interface Steps {
20
21
  onchain: StepStatus;
21
22
  profile: StepStatus;
22
23
  packages: StepStatus;
24
+ opmRecords: StepStatus;
23
25
  }
24
26
 
25
27
  interface AuthorViewProps {
@@ -28,11 +30,13 @@ interface AuthorViewProps {
28
30
 
29
31
  export function AuthorViewCommand({ ensName }: AuthorViewProps) {
30
32
  const [steps, setSteps] = useState<Steps>({
31
- resolve: 'pending', onchain: 'pending', profile: 'pending', packages: 'pending',
33
+ resolve: 'pending', onchain: 'pending', profile: 'pending', packages: 'pending', opmRecords: 'pending',
32
34
  });
33
35
  const [address, setAddress] = useState<string | null>(null);
34
36
  const [author, setAuthor] = useState<AuthorProfile | null>(null);
35
37
  const [ensProfile, setEnsProfile] = useState<ENSProfile | null>(null);
38
+ const [opmRecords, setOpmRecords] = useState<OPMENSRecords>({});
39
+ const [contenthash, setContenthash] = useState<string | null>(null);
36
40
  const [packages, setPackages] = useState<AuthorPackageSummary[]>([]);
37
41
  const [avatarArt, setAvatarArt] = useState<string | null>(null);
38
42
  const [error, setError] = useState<string | null>(null);
@@ -105,6 +109,21 @@ export function AuthorViewCommand({ ensName }: AuthorViewProps) {
105
109
  update('packages', 'skip');
106
110
  }
107
111
 
112
+ update('opmRecords', 'running');
113
+ try {
114
+ const [recs, ch] = await Promise.allSettled([
115
+ readOPMRecords(ensName),
116
+ readENSContenthash(ensName),
117
+ ]);
118
+ if (recs.status === 'fulfilled') setOpmRecords(recs.value);
119
+ if (ch.status === 'fulfilled' && ch.value) setContenthash(ch.value);
120
+ const hasData = (recs.status === 'fulfilled' && Object.keys(recs.value).length > 0)
121
+ || (ch.status === 'fulfilled' && ch.value);
122
+ update('opmRecords', hasData ? 'done' : 'skip');
123
+ } catch {
124
+ update('opmRecords', 'skip');
125
+ }
126
+
108
127
  const art = await avatarPromise;
109
128
  if (art) setAvatarArt(art);
110
129
 
@@ -126,6 +145,8 @@ export function AuthorViewCommand({ ensName }: AuthorViewProps) {
126
145
  detail={steps.onchain === 'done' ? 'registered author' : steps.onchain === 'error' ? 'not found' : undefined} />
127
146
  <StatusLine label="Fetch packages" status={steps.packages}
128
147
  detail={steps.packages === 'done' ? `${packages.length} package(s)` : undefined} />
148
+ <StatusLine label="OPM ENS records" status={steps.opmRecords}
149
+ detail={steps.opmRecords === 'done' ? `${Object.keys(opmRecords).length} records` : steps.opmRecords === 'skip' ? 'none' : undefined} />
129
150
 
130
151
  {done && (
131
152
  <Box flexDirection="column" marginTop={1}>
@@ -198,6 +219,70 @@ export function AuthorViewCommand({ ensName }: AuthorViewProps) {
198
219
  </>
199
220
  )}
200
221
 
222
+ {(Object.keys(opmRecords).length > 0 || contenthash) && (
223
+ <>
224
+ <Text> </Text>
225
+ <Text color="white" bold> OPM ENS Records</Text>
226
+ <Box flexDirection="column" marginLeft={2}>
227
+ {contenthash && (() => {
228
+ const decoded = decodeContenthash(contenthash);
229
+ return decoded ? (
230
+ <Box>
231
+ <Text color="gray">contenthash: </Text>
232
+ <Text color="magenta">{decoded.length > 60 ? decoded.slice(0, 60) + '...' : decoded}</Text>
233
+ </Box>
234
+ ) : null;
235
+ })()}
236
+ {opmRecords.version && (
237
+ <Box>
238
+ <Text color="gray">opm.version: </Text>
239
+ <Text color="cyan">{opmRecords.version}</Text>
240
+ </Box>
241
+ )}
242
+ {opmRecords.checksum && (
243
+ <Box>
244
+ <Text color="gray">opm.checksum: </Text>
245
+ <Text color="cyan">{truncateAddress(opmRecords.checksum)}</Text>
246
+ </Box>
247
+ )}
248
+ {opmRecords.fileverse && (
249
+ <Box>
250
+ <Text color="gray">opm.fileverse: </Text>
251
+ {opmRecords.fileverse.startsWith('http') ? (
252
+ <Hyperlink url={opmRecords.fileverse} label={opmRecords.fileverse.length > 50 ? opmRecords.fileverse.slice(0, 50) + '...' : opmRecords.fileverse} color="green" />
253
+ ) : (
254
+ <Text color="green">{opmRecords.fileverse}</Text>
255
+ )}
256
+ </Box>
257
+ )}
258
+ {opmRecords.riskScore && (
259
+ <Box>
260
+ <Text color="gray">opm.risk_score: </Text>
261
+ <Text color="cyan">{opmRecords.riskScore}/100</Text>
262
+ </Box>
263
+ )}
264
+ {opmRecords.signature && (
265
+ <Box>
266
+ <Text color="gray">opm.signature: </Text>
267
+ <Text color="cyan">{truncateAddress(opmRecords.signature)}</Text>
268
+ </Box>
269
+ )}
270
+ {opmRecords.contract && (
271
+ <Box>
272
+ <Text color="gray">opm.contract: </Text>
273
+ <Hyperlink url={`https://sepolia.basescan.org/address/${opmRecords.contract}`} label={truncateAddress(opmRecords.contract)} color="cyan" />
274
+ </Box>
275
+ )}
276
+ {opmRecords.packages && (
277
+ <Box>
278
+ <Text color="gray">opm.packages: </Text>
279
+ <Text color="white">{opmRecords.packages}</Text>
280
+ </Box>
281
+ )}
282
+ </Box>
283
+ </>
284
+ )}
285
+
201
286
  {packages.length > 0 && (
202
287
  <>
203
288
  <Text> </Text>
@@ -19,6 +19,7 @@ export function CheckCommand() {
19
19
  const [phase, setPhase] = useState<Phase>('scanning');
20
20
  const [report, setReport] = useState<CheckReport | null>(null);
21
21
  const [reportLink, setReportLink] = useState<string | null>(null);
22
+ const [uploadError, setUploadError] = useState<string | null>(null);
22
23
  const [error, setError] = useState<string | null>(null);
23
24
 
24
25
  useEffect(() => {
@@ -120,10 +121,21 @@ export function CheckCommand() {
120
121
  setReport(checkReport);
121
122
 
122
123
  setPhase('upload');
123
- try {
124
- const link = await uploadCheckReportToFileverse(checkReport);
125
- setReportLink(link);
126
- } catch { /* no Fileverse key — skip */ }
124
+ if (!process.env.FILEVERSE_API_KEY) {
125
+ setUploadError('FILEVERSE_API_KEY not set');
126
+ } else {
127
+ try {
128
+ const uploadResult = await uploadCheckReportToFileverse(checkReport);
129
+ setReportLink(uploadResult.link);
130
+ } catch (e: any) {
131
+ const msg = String(e?.message || e);
132
+ if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED')) {
133
+ setUploadError('Fileverse API not running — start with: npx @fileverse/api --apiKey <key>');
134
+ } else {
135
+ setUploadError(msg.length > 120 ? msg.slice(0, 120) + '...' : msg);
136
+ }
137
+ }
138
+ }
127
139
 
128
140
  setPhase('done');
129
141
  }
@@ -154,7 +166,8 @@ export function CheckCommand() {
154
166
 
155
167
  {(phase === 'upload' || phase === 'done') && (
156
168
  <StatusLine label="Upload report to Fileverse"
157
- status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'} />
169
+ status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'}
170
+ detail={uploadError || undefined} />
158
171
  )}
159
172
 
160
173
  {phase === 'done' && report && (
@@ -28,6 +28,7 @@ export function FixCommand() {
28
28
  const [total, setTotal] = useState(0);
29
29
  const [applied, setApplied] = useState(false);
30
30
  const [reportLink, setReportLink] = useState<string | null>(null);
31
+ const [uploadError, setUploadError] = useState<string | null>(null);
31
32
  const [error, setError] = useState<string | null>(null);
32
33
 
33
34
  useEffect(() => {
@@ -182,17 +183,28 @@ export function FixCommand() {
182
183
  }
183
184
 
184
185
  setPhase('upload');
185
- try {
186
- const checkReport: CheckReport = {
187
- project: projectName,
188
- timestamp: new Date().toISOString(),
189
- totalDeps: allEntries.length,
190
- deps: depResults,
191
- agents: agentResults,
192
- };
193
- const link = await uploadCheckReportToFileverse(checkReport);
194
- setReportLink(link);
195
- } catch { /* no Fileverse key — skip */ }
186
+ const checkReport: CheckReport = {
187
+ project: projectName,
188
+ timestamp: new Date().toISOString(),
189
+ totalDeps: allEntries.length,
190
+ deps: depResults,
191
+ agents: agentResults,
192
+ };
193
+ if (!process.env.FILEVERSE_API_KEY) {
194
+ setUploadError('FILEVERSE_API_KEY not set');
195
+ } else {
196
+ try {
197
+ const uploadResult = await uploadCheckReportToFileverse(checkReport);
198
+ setReportLink(uploadResult.link);
199
+ } catch (e: any) {
200
+ const msg = String(e?.message || e);
201
+ if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED')) {
202
+ setUploadError('Fileverse API not running — start with: npx @fileverse/api --apiKey <key>');
203
+ } else {
204
+ setUploadError(msg.length > 120 ? msg.slice(0, 120) + '...' : msg);
205
+ }
206
+ }
207
+ }
196
208
 
197
209
  setPhase('done');
198
210
  }
@@ -213,7 +225,8 @@ export function FixCommand() {
213
225
 
214
226
  {(phase === 'upload' || phase === 'done') && (
215
227
  <StatusLine label="Upload report to Fileverse"
216
- status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'} />
228
+ status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'}
229
+ detail={uploadError || undefined} />
217
230
  )}
218
231
 
219
232
  {phase === 'done' && fixes.length === 0 && (
@@ -1,14 +1,17 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import { truncateAddress } from '@opm/core';
4
+ import type { OnChainPackageInfo, ScanReport as ScanReportType, OPMENSRecords } from '@opm/core';
3
5
  import { Header } from '../components/Header';
4
6
  import { PackageCard } from '../components/PackageCard';
5
- import { StatusLine } from '../components/StatusLine';
7
+ import { StatusLine, type Status } from '../components/StatusLine';
8
+ import { Hyperlink } from '../components/Hyperlink';
6
9
  import { getPackageInfo, getSafestVersion, getVersions } from '../services/contract';
7
- import { getENSProfile, type ENSProfile } from '../services/ens';
10
+ import { getENSProfile, resolveENSName, type ENSProfile } from '../services/ens';
11
+ import { readOPMRecords, readPackageENSRecords, readENSContenthash, decodeContenthash } from '../services/ens-records';
8
12
  import { fetchReportFromFileverse } from '../services/fileverse';
9
13
  import { queryOSV, type OSVVulnerability } from '../services/osv';
10
14
  import { resolveVersion } from '../services/version';
11
- import type { OnChainPackageInfo, ScanReport as ScanReportType } from '@opm/core';
12
15
 
13
16
  interface InfoCommandProps {
14
17
  packageName: string;
@@ -24,6 +27,9 @@ export function InfoCommand({ packageName, version }: InfoCommandProps) {
24
27
  const [versions, setVersions] = useState<string[]>([]);
25
28
  const [safest, setSafest] = useState<string | undefined>();
26
29
  const [cves, setCves] = useState<OSVVulnerability[]>([]);
30
+ const [ensRecords, setEnsRecords] = useState<OPMENSRecords>({});
31
+ const [pkgEnsRecords, setPkgEnsRecords] = useState<OPMENSRecords>({});
32
+ const [contenthash, setContenthash] = useState<string | null>(null);
27
33
  const [error, setError] = useState<string | null>(null);
28
34
 
29
35
  useEffect(() => {
@@ -31,7 +37,8 @@ export function InfoCommand({ packageName, version }: InfoCommandProps) {
31
37
  }, []);
32
38
 
33
39
  async function run() {
34
- const ver = await resolveVersion(packageName, version || 'latest');
40
+ const resolved = await resolveVersion(packageName, version || 'latest');
41
+ const ver = resolved.version;
35
42
  setResolvedVer(ver);
36
43
 
37
44
  const pkgInfo = await getPackageInfo(packageName, ver);
@@ -55,9 +62,28 @@ export function InfoCommand({ packageName, version }: InfoCommandProps) {
55
62
  setVersions(vers.status === 'fulfilled' ? vers.value : []);
56
63
  setSafest(safe.status === 'fulfilled' ? safe.value : undefined);
57
64
  setCves(osvResult.status === 'fulfilled' ? osvResult.value : []);
65
+
66
+ const authorEns = ep.status === 'fulfilled' && ep.value?.name
67
+ ? ep.value.name
68
+ : await resolveENSName(pkgInfo.author).catch(() => null);
69
+
70
+ if (authorEns) {
71
+ const [opmRecs, pkgRecs, ch] = await Promise.allSettled([
72
+ readOPMRecords(authorEns),
73
+ readPackageENSRecords(authorEns, packageName),
74
+ readENSContenthash(authorEns),
75
+ ]);
76
+ if (opmRecs.status === 'fulfilled') setEnsRecords(opmRecs.value);
77
+ if (pkgRecs.status === 'fulfilled') setPkgEnsRecords(pkgRecs.value);
78
+ if (ch.status === 'fulfilled' && ch.value) setContenthash(ch.value);
79
+ }
80
+
58
81
  setStatus('done');
59
82
  }
60
83
 
84
+ const hasEnsData = Object.keys(ensRecords).length > 0 || Object.keys(pkgEnsRecords).length > 0 || contenthash;
85
+ const fvHash = pkgEnsRecords.fileverse || ensRecords.fileverse;
86
+
61
87
  return (
62
88
  <Box flexDirection="column">
63
89
  <Header subtitle="info" />
@@ -96,6 +122,68 @@ export function InfoCommand({ packageName, version }: InfoCommandProps) {
96
122
  <Text color="green" bold>{safest}</Text>
97
123
  </Box>
98
124
  )}
125
+
126
+ {hasEnsData && (
127
+ <Box flexDirection="column" marginTop={1}>
128
+ <Text color="gray">────────────────────────────────────────</Text>
129
+ <Text color="white" bold> ENS Records</Text>
130
+ {contenthash && (() => {
131
+ const decoded = decodeContenthash(contenthash);
132
+ return decoded ? (
133
+ <Box marginLeft={2}>
134
+ <Text color="gray">contenthash: </Text>
135
+ <Text color="magenta">{decoded.length > 60 ? decoded.slice(0, 60) + '...' : decoded}</Text>
136
+ </Box>
137
+ ) : null;
138
+ })()}
139
+ {(pkgEnsRecords.version || ensRecords.version) && (
140
+ <Box marginLeft={2}>
141
+ <Text color="gray">opm.version: </Text>
142
+ <Text color="cyan">{pkgEnsRecords.version || ensRecords.version}</Text>
143
+ </Box>
144
+ )}
145
+ {(pkgEnsRecords.checksum || ensRecords.checksum) && (
146
+ <Box marginLeft={2}>
147
+ <Text color="gray">opm.checksum: </Text>
148
+ <Text color="cyan">{truncateAddress(pkgEnsRecords.checksum || ensRecords.checksum || '')}</Text>
149
+ </Box>
150
+ )}
151
+ {fvHash && (
152
+ <Box marginLeft={2}>
153
+ <Text color="gray">opm.fileverse: </Text>
154
+ {fvHash.startsWith('http') ? (
155
+ <Hyperlink url={fvHash} label={fvHash.length > 50 ? fvHash.slice(0, 50) + '...' : fvHash} color="green" />
156
+ ) : (
157
+ <Text color="green">{fvHash}</Text>
158
+ )}
159
+ </Box>
160
+ )}
161
+ {(pkgEnsRecords.riskScore || ensRecords.riskScore) && (
162
+ <Box marginLeft={2}>
163
+ <Text color="gray">opm.risk_score: </Text>
164
+ <Text color="cyan">{pkgEnsRecords.riskScore || ensRecords.riskScore}/100</Text>
165
+ </Box>
166
+ )}
167
+ {(pkgEnsRecords.signature || ensRecords.signature) && (
168
+ <Box marginLeft={2}>
169
+ <Text color="gray">opm.signature: </Text>
170
+ <Text color="cyan">{truncateAddress(pkgEnsRecords.signature || ensRecords.signature || '')}</Text>
171
+ </Box>
172
+ )}
173
+ {ensRecords.contract && (
174
+ <Box marginLeft={2}>
175
+ <Text color="gray">opm.contract: </Text>
176
+ <Text color="cyan">{truncateAddress(ensRecords.contract)}</Text>
177
+ </Box>
178
+ )}
179
+ {ensRecords.packages && (
180
+ <Box marginLeft={2}>
181
+ <Text color="gray">opm.packages: </Text>
182
+ <Text color="white">{ensRecords.packages}</Text>
183
+ </Box>
184
+ )}
185
+ </Box>
186
+ )}
99
187
  </Box>
100
188
  ) : (
101
189
  <Box marginLeft={2} marginTop={1}>