opmsec 0.1.0 → 0.1.3
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.
- package/.env.example +23 -13
- package/README.md +256 -173
- package/docs/architecture/agents.mdx +77 -0
- package/docs/architecture/benchmarks.mdx +65 -0
- package/docs/architecture/overview.mdx +58 -0
- package/docs/architecture/scanner.mdx +53 -0
- package/docs/cli/audit.mdx +35 -0
- package/docs/cli/check.mdx +44 -0
- package/docs/cli/fix.mdx +49 -0
- package/docs/cli/info.mdx +44 -0
- package/docs/cli/install.mdx +71 -0
- package/docs/cli/push.mdx +99 -0
- package/docs/cli/register-agent.mdx +80 -0
- package/docs/cli/view.mdx +52 -0
- package/docs/concepts/multi-agent-consensus.mdx +58 -0
- package/docs/concepts/on-chain-registry.mdx +74 -0
- package/docs/concepts/security-model.mdx +76 -0
- package/docs/concepts/zk-agent-verification.mdx +82 -0
- package/docs/configuration.mdx +82 -0
- package/docs/contract/deployment.mdx +57 -0
- package/docs/contract/events.mdx +115 -0
- package/docs/contract/functions.mdx +220 -0
- package/docs/contract/overview.mdx +58 -0
- package/docs/favicon.svg +5 -0
- package/docs/introduction.mdx +43 -0
- package/docs/logo/dark.svg +5 -0
- package/docs/logo/light.svg +5 -0
- package/docs/mint.json +106 -0
- package/docs/quickstart.mdx +133 -0
- package/package.json +3 -3
- package/packages/cli/src/commands/author-view.tsx +9 -1
- package/packages/cli/src/commands/check.tsx +318 -0
- package/packages/cli/src/commands/fix.tsx +294 -0
- package/packages/cli/src/commands/install.tsx +229 -33
- package/packages/cli/src/commands/push.tsx +53 -22
- package/packages/cli/src/commands/register-agent.tsx +227 -0
- package/packages/cli/src/components/AgentScores.tsx +20 -6
- package/packages/cli/src/components/Hyperlink.tsx +30 -0
- package/packages/cli/src/components/ScanReport.tsx +3 -2
- package/packages/cli/src/index.tsx +41 -5
- package/packages/cli/src/services/avatar.ts +43 -6
- package/packages/cli/src/services/chainpatrol.ts +20 -17
- package/packages/cli/src/services/contract.ts +41 -8
- package/packages/cli/src/services/ens.ts +3 -5
- package/packages/cli/src/services/fileverse.ts +12 -13
- package/packages/cli/src/services/typosquat.ts +166 -0
- package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
- package/packages/contracts/contracts/OPMRegistry.sol +63 -0
- package/packages/contracts/scripts/deploy.ts +22 -3
- package/packages/core/src/abi.ts +221 -0
- package/packages/core/src/benchmarks.ts +450 -0
- package/packages/core/src/constants.ts +20 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/model-rankings.ts +115 -0
- package/packages/core/src/prompt.ts +58 -0
- package/packages/core/src/types.ts +41 -0
- package/packages/core/src/utils.ts +7 -3
- package/packages/scanner/src/agents/base-agent.ts +13 -3
- package/packages/scanner/src/index.ts +5 -2
- package/packages/scanner/src/queue/memory-queue.ts +8 -3
- package/packages/scanner/src/services/benchmark-runner.ts +114 -0
- package/packages/scanner/src/services/contract-writer.ts +2 -3
- package/packages/scanner/src/services/fileverse.ts +26 -7
- package/packages/scanner/src/services/openrouter.ts +46 -0
- package/packages/scanner/src/services/report-formatter.ts +122 -3
- package/packages/scanner/src/services/zk-verifier.ts +118 -0
- package/packages/web/.next/app-build-manifest.json +15 -0
- package/packages/web/.next/build-manifest.json +20 -0
- package/packages/web/.next/package.json +1 -0
- package/packages/web/.next/prerender-manifest.json +11 -0
- package/packages/web/.next/react-loadable-manifest.json +1 -0
- package/packages/web/.next/routes-manifest.json +1 -0
- package/packages/web/.next/server/app/page.js +272 -0
- package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
- package/packages/web/.next/server/app-paths-manifest.json +3 -0
- package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/packages/web/.next/server/middleware-build-manifest.js +22 -0
- package/packages/web/.next/server/middleware-manifest.json +6 -0
- package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/packages/web/.next/server/next-font-manifest.js +1 -0
- package/packages/web/.next/server/next-font-manifest.json +1 -0
- package/packages/web/.next/server/pages-manifest.json +1 -0
- package/packages/web/.next/server/server-reference-manifest.js +1 -0
- package/packages/web/.next/server/server-reference-manifest.json +5 -0
- package/packages/web/.next/server/vendor-chunks/@swc.js +55 -0
- package/packages/web/.next/server/vendor-chunks/next.js +3010 -0
- package/packages/web/.next/server/webpack-runtime.js +209 -0
- package/packages/web/.next/static/chunks/app/layout.js +39 -0
- package/packages/web/.next/static/chunks/app/page.js +61 -0
- package/packages/web/.next/static/chunks/app-pages-internals.js +182 -0
- package/packages/web/.next/static/chunks/main-app.js +1882 -0
- package/packages/web/.next/static/chunks/polyfills.js +1 -0
- package/packages/web/.next/static/chunks/webpack.js +1393 -0
- package/packages/web/.next/static/css/app/layout.css +1237 -0
- package/packages/web/.next/static/development/_buildManifest.js +1 -0
- package/packages/web/.next/static/development/_ssgManifest.js +1 -0
- package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +12 -0
- package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +12 -0
- package/packages/web/.next/trace +5 -0
- package/packages/web/.next/types/app/layout.ts +84 -0
- package/packages/web/.next/types/app/page.ts +84 -0
- package/packages/web/.next/types/cache-life.d.ts +141 -0
- package/packages/web/.next/types/package.json +1 -0
- package/packages/web/.next/types/routes.d.ts +57 -0
- package/packages/web/.next/types/validator.ts +61 -0
- package/packages/web/app/globals.css +75 -0
- package/packages/web/app/layout.tsx +26 -0
- package/packages/web/app/page.tsx +358 -0
- package/packages/web/bun.lock +300 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +5 -0
- package/packages/web/package.json +26 -0
- package/packages/web/postcss.config.mjs +8 -0
- package/packages/web/public/favicon.svg +5 -0
- package/packages/web/public/logo.svg +7 -0
- package/packages/web/tailwind.config.ts +48 -0
- package/packages/web/tsconfig.json +21 -0
|
@@ -17,13 +17,27 @@ export function AgentScores({ agents }: AgentScoresProps) {
|
|
|
17
17
|
const level = classifyRisk(agent.result.risk_score);
|
|
18
18
|
const color = RISK_COLORS[level];
|
|
19
19
|
const connector = i === agents.length - 1 ? '└──' : '├──';
|
|
20
|
+
const intel = agent.model_intelligence || 0;
|
|
21
|
+
const coding = agent.model_coding || 0;
|
|
22
|
+
const weight = agent.model_weight || 0;
|
|
20
23
|
return (
|
|
21
|
-
<Box key={agent.agent_id}>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
<Box key={agent.agent_id} flexDirection="column">
|
|
25
|
+
<Box>
|
|
26
|
+
<Text color="gray">{connector} </Text>
|
|
27
|
+
<Text color="cyan">{agent.agent_id}</Text>
|
|
28
|
+
<Text color="gray"> ({agent.model}) </Text>
|
|
29
|
+
<Text color={color} bold>{agent.result.risk_score}/100</Text>
|
|
30
|
+
<Text color="gray"> {agent.result.recommendation}</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
{(intel > 0 || coding > 0) && (
|
|
33
|
+
<Box marginLeft={4}>
|
|
34
|
+
<Text color="magenta">AI: {intel}</Text>
|
|
35
|
+
<Text color="gray"> | </Text>
|
|
36
|
+
<Text color="blue">Code: {coding}</Text>
|
|
37
|
+
<Text color="gray"> | </Text>
|
|
38
|
+
<Text color="cyan">W: {weight}</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
)}
|
|
27
41
|
</Box>
|
|
28
42
|
);
|
|
29
43
|
})}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface HyperlinkProps {
|
|
5
|
+
url: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Hyperlink({ url, label, color = 'cyan' }: HyperlinkProps) {
|
|
11
|
+
const display = label || shortenUrl(url);
|
|
12
|
+
const ansi = `\x1b]8;;${url}\x07${display}\x1b]8;;\x07`;
|
|
13
|
+
return <Text color={color as any}>{ansi}</Text>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shortenUrl(url: string): string {
|
|
17
|
+
try {
|
|
18
|
+
const u = new URL(url);
|
|
19
|
+
const pathParts = u.pathname.split('/').filter(Boolean);
|
|
20
|
+
const hash = u.hash;
|
|
21
|
+
if (pathParts.length >= 2) {
|
|
22
|
+
const id = pathParts[pathParts.length - 1];
|
|
23
|
+
const shortHash = hash.length > 20 ? hash.slice(0, 20) + '...' : hash;
|
|
24
|
+
return `${u.host}/.../${id}${shortHash}`;
|
|
25
|
+
}
|
|
26
|
+
return url.length > 60 ? url.slice(0, 57) + '...' : url;
|
|
27
|
+
} catch {
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import type { ScanReport as ScanReportType } from '@opm/core';
|
|
4
|
+
import { Hyperlink } from './Hyperlink';
|
|
4
5
|
|
|
5
6
|
interface ScanReportProps {
|
|
6
7
|
report?: ScanReportType | null;
|
|
@@ -21,10 +22,10 @@ export function ScanReport({ report, reportURI }: ScanReportProps) {
|
|
|
21
22
|
return (
|
|
22
23
|
<Box flexDirection="column" marginLeft={2}>
|
|
23
24
|
<Text bold color="white"> Scan Report</Text>
|
|
24
|
-
{reportURI && (
|
|
25
|
+
{reportURI && !reportURI.startsWith('local://') && (
|
|
25
26
|
<Box>
|
|
26
27
|
<Text color="gray"> Link: </Text>
|
|
27
|
-
<
|
|
28
|
+
<Hyperlink url={reportURI} />
|
|
28
29
|
</Box>
|
|
29
30
|
)}
|
|
30
31
|
{report && (
|
|
@@ -6,7 +6,10 @@ import { InstallCommand } from './commands/install';
|
|
|
6
6
|
import { AuditCommand } from './commands/audit';
|
|
7
7
|
import { InfoCommand } from './commands/info';
|
|
8
8
|
import { AuthorViewCommand } from './commands/author-view';
|
|
9
|
+
import { CheckCommand } from './commands/check';
|
|
10
|
+
import { FixCommand } from './commands/fix';
|
|
9
11
|
import { PassthroughCommand } from './commands/passthrough';
|
|
12
|
+
import { RegisterAgentCommand } from './commands/register-agent';
|
|
10
13
|
import { Header } from './components/Header';
|
|
11
14
|
|
|
12
15
|
const args = process.argv.slice(2);
|
|
@@ -56,11 +59,38 @@ function App() {
|
|
|
56
59
|
if (!name) return <Help />;
|
|
57
60
|
return <InfoCommand packageName={name} version={version} />;
|
|
58
61
|
}
|
|
62
|
+
case 'check':
|
|
63
|
+
return <CheckCommand />;
|
|
64
|
+
case 'fix':
|
|
65
|
+
return <FixCommand />;
|
|
59
66
|
case 'whois': {
|
|
60
67
|
if (!rest[0]) return <Help />;
|
|
61
68
|
const ensArg = rest[0].endsWith('.eth') ? rest[0] : `${rest[0]}.eth`;
|
|
62
69
|
return <AuthorViewCommand ensName={ensArg} />;
|
|
63
70
|
}
|
|
71
|
+
case 'register-agent': {
|
|
72
|
+
const nameIdx = rest.findIndex((a) => a === '--name');
|
|
73
|
+
const modelIdx = rest.findIndex((a) => a === '--model');
|
|
74
|
+
const promptIdx = rest.findIndex((a) => a === '--system-prompt');
|
|
75
|
+
const agentName = nameIdx >= 0 ? rest[nameIdx + 1] : undefined;
|
|
76
|
+
const agentModel = modelIdx >= 0 ? rest[modelIdx + 1] : undefined;
|
|
77
|
+
const systemPrompt = promptIdx >= 0 ? rest[promptIdx + 1] : undefined;
|
|
78
|
+
if (!agentName || !agentModel) {
|
|
79
|
+
return (
|
|
80
|
+
<Box flexDirection="column">
|
|
81
|
+
<Header />
|
|
82
|
+
<Text color="red">Usage: opm register-agent --name {'<name>'} --model {'<model>'} [--system-prompt {'<prompt>'}]</Text>
|
|
83
|
+
<Text color="gray"> --name Agent identifier (e.g. my-security-agent)</Text>
|
|
84
|
+
<Text color="gray"> --model LLM model to use (e.g. anthropic/claude-sonnet-4-20250514)</Text>
|
|
85
|
+
<Text color="gray"> --system-prompt Custom system prompt (defaults to OPM security auditor prompt)</Text>
|
|
86
|
+
<Text> </Text>
|
|
87
|
+
<Text color="gray">The agent will be benchmarked against 10 labeled security test cases.</Text>
|
|
88
|
+
<Text color="gray">A ZK proof of 100% accuracy is required for registration.</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return <RegisterAgentCommand agentName={agentName} model={agentModel} systemPrompt={systemPrompt} />;
|
|
93
|
+
}
|
|
64
94
|
default:
|
|
65
95
|
if (command && PASSTHROUGH.has(command)) {
|
|
66
96
|
return <PassthroughCommand command={command} args={rest} />;
|
|
@@ -77,11 +107,19 @@ function Help() {
|
|
|
77
107
|
<Text color="cyan" bold>Security commands:</Text>
|
|
78
108
|
<Text> opm push [--token t] [--otp c] Sign, scan, publish, register</Text>
|
|
79
109
|
<Text> opm install [pkg] Install with on-chain security verification</Text>
|
|
110
|
+
<Text> opm check Scan all deps: typosquats, CVEs, AI analysis</Text>
|
|
111
|
+
<Text> opm fix Auto-fix typosquats and vulnerable versions</Text>
|
|
80
112
|
<Text> opm audit Scan all deps against on-chain security data</Text>
|
|
81
113
|
<Text> opm info {'<pkg>'} Show on-chain security info for a package</Text>
|
|
82
114
|
<Text> opm view {'<name.eth>'} Show author profile, packages, and risk scores</Text>
|
|
83
115
|
<Text> opm whois {'<name>'} Look up an ENS identity on OPM</Text>
|
|
84
116
|
<Text> </Text>
|
|
117
|
+
<Text color="cyan" bold>Agent commands:</Text>
|
|
118
|
+
<Text> opm register-agent Register a new security agent (ZK-verified)</Text>
|
|
119
|
+
<Text> --name {'<name>'} Agent identifier</Text>
|
|
120
|
+
<Text> --model {'<model>'} LLM model (e.g. anthropic/claude-sonnet-4-20250514)</Text>
|
|
121
|
+
<Text> --system-prompt {'<p>'} Custom system prompt (optional)</Text>
|
|
122
|
+
<Text> </Text>
|
|
85
123
|
<Text color="cyan" bold>Standard commands (npm passthrough):</Text>
|
|
86
124
|
<Text> opm init Initialize a new package</Text>
|
|
87
125
|
<Text> opm run {'<script>'} Run a package script</Text>
|
|
@@ -98,11 +136,9 @@ function Help() {
|
|
|
98
136
|
<Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
|
|
99
137
|
<Text color="gray"> view name.eth → author profile, view pkg → info</Text>
|
|
100
138
|
<Text> </Text>
|
|
101
|
-
<Text color="cyan" bold>Environment:</Text>
|
|
102
|
-
<Text>
|
|
103
|
-
<Text>
|
|
104
|
-
<Text> OPENAI_API_KEY For AI security scanning</Text>
|
|
105
|
-
<Text> NPM_TOKEN npm automation token (alt to --token)</Text>
|
|
139
|
+
<Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
|
|
140
|
+
<Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>
|
|
141
|
+
<Text> NPM_TOKEN npm automation token (for push only)</Text>
|
|
106
142
|
</Box>
|
|
107
143
|
</Box>
|
|
108
144
|
);
|
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Jimp, intToRGBA } from 'jimp';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const PIXEL = '\u2584';
|
|
4
|
+
|
|
5
|
+
export async function renderAvatar(url: string, width = 24): Promise<string | null> {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
4
8
|
try {
|
|
5
|
-
const res = await fetch(url, { redirect: 'follow' });
|
|
9
|
+
const res = await fetch(url, { redirect: 'follow', signal: controller.signal });
|
|
10
|
+
clearTimeout(timeout);
|
|
6
11
|
if (!res.ok) return null;
|
|
7
12
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
8
|
-
return
|
|
9
|
-
} catch {
|
|
10
|
-
|
|
13
|
+
return renderImageToAnsi(buffer, width);
|
|
14
|
+
} catch {
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function renderImageToAnsi(buffer: Buffer, targetWidth: number): Promise<string | null> {
|
|
21
|
+
const image = await Jimp.fromBuffer(buffer);
|
|
22
|
+
const { width: origW, height: origH } = image;
|
|
23
|
+
|
|
24
|
+
const ratio = origH / origW;
|
|
25
|
+
const w = targetWidth;
|
|
26
|
+
const h = Math.max(2, Math.round(w * ratio));
|
|
27
|
+
|
|
28
|
+
image.resize({ w, h });
|
|
29
|
+
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
for (let y = 0; y < h - 1; y += 2) {
|
|
32
|
+
let line = '';
|
|
33
|
+
for (let x = 0; x < w; x++) {
|
|
34
|
+
const top = intToRGBA(image.getPixelColor(x, y));
|
|
35
|
+
const bot = intToRGBA(image.getPixelColor(x, y + 1));
|
|
36
|
+
|
|
37
|
+
if (top.a === 0) {
|
|
38
|
+
line += `\x1b[0m\x1b[38;2;${bot.r};${bot.g};${bot.b}m${PIXEL}\x1b[0m`;
|
|
39
|
+
} else {
|
|
40
|
+
line += `\x1b[48;2;${top.r};${top.g};${top.b}m\x1b[38;2;${bot.r};${bot.g};${bot.b}m${PIXEL}\x1b[0m`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
lines.push(line);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { CHAINPATROL_API_URL
|
|
1
|
+
import { CHAINPATROL_API_URL } from '@opm/core';
|
|
2
2
|
import type { ChainPatrolResult } from '@opm/core';
|
|
3
3
|
|
|
4
4
|
export async function checkPackageWithChainPatrol(packageName: string): Promise<ChainPatrolResult> {
|
|
5
|
-
const apiKey =
|
|
5
|
+
const apiKey = process.env.CHAINPATROL_API_KEY;
|
|
6
|
+
if (!apiKey) return { status: 'UNKNOWN', source: 'skipped' };
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(`${CHAINPATROL_API_URL}/asset/check`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'X-API-KEY': apiKey,
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({ content: `npm:${packageName}` }),
|
|
16
|
+
});
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
if (!res.ok) return { status: 'UNKNOWN', source: 'error' };
|
|
19
|
+
|
|
20
|
+
const data = await res.json() as { status: string; source: string };
|
|
21
|
+
return {
|
|
22
|
+
status: (data.status as ChainPatrolResult['status']) || 'UNKNOWN',
|
|
23
|
+
source: data.source || 'chainpatrol',
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
17
26
|
return { status: 'UNKNOWN', source: 'error' };
|
|
18
27
|
}
|
|
19
|
-
|
|
20
|
-
const data = await res.json() as { status: string; source: string };
|
|
21
|
-
return {
|
|
22
|
-
status: (data.status as ChainPatrolResult['status']) || 'UNKNOWN',
|
|
23
|
-
source: data.source || 'chainpatrol',
|
|
24
|
-
};
|
|
25
28
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC } from '@opm/core';
|
|
2
|
+
import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC, DEFAULT_CONTRACT_ADDRESS } from '@opm/core';
|
|
3
3
|
import type { OnChainPackageInfo, AuthorProfile } from '@opm/core';
|
|
4
4
|
|
|
5
|
+
function getContractAddress(): string {
|
|
6
|
+
return getEnvOrDefault('CONTRACT_ADDRESS', DEFAULT_CONTRACT_ADDRESS);
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
function getReadContract() {
|
|
6
10
|
const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
|
|
7
11
|
const provider = new ethers.JsonRpcProvider(rpc);
|
|
8
|
-
|
|
9
|
-
return new ethers.Contract(address, OPM_REGISTRY_ABI, provider);
|
|
12
|
+
return new ethers.Contract(getContractAddress(), OPM_REGISTRY_ABI, provider);
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
function getWriteContract() {
|
|
13
16
|
const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
|
|
14
17
|
const provider = new ethers.JsonRpcProvider(rpc);
|
|
15
|
-
const wallet = new ethers.Wallet(getEnvOrThrow('OPM_PRIVATE_KEY'), provider);
|
|
16
|
-
|
|
17
|
-
return new ethers.Contract(address, OPM_REGISTRY_ABI, wallet);
|
|
18
|
+
const wallet = new ethers.Wallet(getEnvOrThrow('OPM_SIGNING_KEY', 'OPM_PRIVATE_KEY'), provider);
|
|
19
|
+
return new ethers.Contract(getContractAddress(), OPM_REGISTRY_ABI, wallet);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export async function getPackageInfo(name: string, version: string): Promise<OnChainPackageInfo> {
|
|
@@ -105,8 +107,7 @@ export interface AuthorPackageSummary {
|
|
|
105
107
|
export async function getPackagesByAuthor(authorAddress: string): Promise<AuthorPackageSummary[]> {
|
|
106
108
|
const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
|
|
107
109
|
const provider = new ethers.JsonRpcProvider(rpc);
|
|
108
|
-
const
|
|
109
|
-
const contract = new ethers.Contract(contractAddr, OPM_REGISTRY_ABI, provider);
|
|
110
|
+
const contract = new ethers.Contract(getContractAddress(), OPM_REGISTRY_ABI, provider);
|
|
110
111
|
|
|
111
112
|
const packageMap = new Map<string, { name: string; version: string }>();
|
|
112
113
|
|
|
@@ -154,6 +155,38 @@ export async function getPackagesByAuthor(authorAddress: string): Promise<Author
|
|
|
154
155
|
return results;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
export async function registerAgentOnChain(
|
|
159
|
+
name: string,
|
|
160
|
+
model: string,
|
|
161
|
+
systemPromptHash: string,
|
|
162
|
+
proofHash: string,
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
|
|
165
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
166
|
+
const wallet = new ethers.Wallet(getEnvOrThrow('AGENT_PRIVATE_KEY'), provider);
|
|
167
|
+
const contract = new ethers.Contract(getContractAddress(), OPM_REGISTRY_ABI, wallet);
|
|
168
|
+
|
|
169
|
+
const tx = await contract.registerAgent(
|
|
170
|
+
name,
|
|
171
|
+
model,
|
|
172
|
+
ethers.keccak256(ethers.toUtf8Bytes(systemPromptHash)),
|
|
173
|
+
ethers.keccak256(ethers.toUtf8Bytes(proofHash)),
|
|
174
|
+
);
|
|
175
|
+
const receipt = await tx.wait();
|
|
176
|
+
return receipt.hash;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function getRegisteredAgent(agentAddress: string): Promise<any> {
|
|
180
|
+
const contract = getReadContract();
|
|
181
|
+
return contract.getRegisteredAgent(agentAddress);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function getAgentCount(): Promise<number> {
|
|
185
|
+
const contract = getReadContract();
|
|
186
|
+
const count = await contract.getAgentCount();
|
|
187
|
+
return Number(count);
|
|
188
|
+
}
|
|
189
|
+
|
|
157
190
|
export async function getPackagesByAuthorDirect(
|
|
158
191
|
authorAddress: string,
|
|
159
192
|
knownPackageNames: string[],
|
|
@@ -4,21 +4,19 @@ import { addEnsContracts } from '@ensdomains/ensjs';
|
|
|
4
4
|
import { getName } from '@ensdomains/ensjs/public';
|
|
5
5
|
import { getRecords } from '@ensdomains/ensjs/public';
|
|
6
6
|
import { getAddressRecord } from '@ensdomains/ensjs/public';
|
|
7
|
-
import { getEnvOrDefault } from '@opm/core';
|
|
7
|
+
import { getEnvOrDefault, ETH_SEPOLIA_RPC, ETH_MAINNET_RPC } from '@opm/core';
|
|
8
8
|
|
|
9
9
|
function getSepoliaClient() {
|
|
10
|
-
const rpc = getEnvOrDefault('ETH_SEPOLIA_RPC_URL', 'https://ethereum-sepolia-rpc.publicnode.com');
|
|
11
10
|
return createPublicClient({
|
|
12
11
|
chain: addEnsContracts(sepolia),
|
|
13
|
-
transport: http(
|
|
12
|
+
transport: http(getEnvOrDefault('ETH_SEPOLIA_RPC_URL', ETH_SEPOLIA_RPC)),
|
|
14
13
|
});
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
function getMainnetClient() {
|
|
18
|
-
const rpc = getEnvOrDefault('ETH_MAINNET_RPC_URL', 'https://eth.llamarpc.com');
|
|
19
17
|
return createPublicClient({
|
|
20
18
|
chain: addEnsContracts(mainnet),
|
|
21
|
-
transport: http(
|
|
19
|
+
transport: http(getEnvOrDefault('ETH_MAINNET_RPC_URL', ETH_MAINNET_RPC)),
|
|
22
20
|
});
|
|
23
21
|
}
|
|
24
22
|
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import type { ScanReport } from '@opm/core';
|
|
2
|
-
import { getEnvOrDefault, safeJsonParse } from '@opm/core';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_API_URL = 'http://localhost:8001';
|
|
2
|
+
import { getEnvOrDefault, safeJsonParse, FILEVERSE_DEFAULT_URL } from '@opm/core';
|
|
5
3
|
|
|
6
4
|
export async function fetchReportFromFileverse(reportURI: string): Promise<ScanReport | null> {
|
|
7
5
|
if (!reportURI || reportURI.startsWith('local://')) return null;
|
|
8
6
|
|
|
9
7
|
const apiKey = process.env.FILEVERSE_API_KEY;
|
|
10
|
-
|
|
8
|
+
if (!apiKey) return null;
|
|
11
9
|
|
|
10
|
+
const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', FILEVERSE_DEFAULT_URL);
|
|
12
11
|
const ddocId = extractDdocId(reportURI);
|
|
13
|
-
if (ddocId
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}
|
|
12
|
+
if (!ddocId) return null;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${apiUrl}/api/ddocs/${ddocId}?apiKey=${encodeURIComponent(apiKey)}`);
|
|
16
|
+
if (res.ok) {
|
|
17
|
+
const doc = await res.json() as { content: string };
|
|
18
|
+
return safeJsonParse<ScanReport>(doc.content);
|
|
19
|
+
}
|
|
20
|
+
} catch { /* local API not running */ }
|
|
22
21
|
|
|
23
22
|
return null;
|
|
24
23
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const NPM_BULK_DL = 'https://api.npmjs.org/downloads/point/last-week';
|
|
2
|
+
const NPM_SEARCH = 'https://registry.npmjs.org/-/v1/search';
|
|
3
|
+
|
|
4
|
+
export interface TyposquatResult {
|
|
5
|
+
suspect: string;
|
|
6
|
+
likelyTarget: string | null;
|
|
7
|
+
confidence: 'high' | 'medium' | 'low' | 'none';
|
|
8
|
+
reason: string;
|
|
9
|
+
targetDownloads: number;
|
|
10
|
+
suspectDownloads: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function detectTyposquatBatch(names: string[]): Promise<TyposquatResult[]> {
|
|
14
|
+
if (names.length === 0) return [];
|
|
15
|
+
|
|
16
|
+
const dlMap = await fetchBulkDownloads(names);
|
|
17
|
+
|
|
18
|
+
const searchResults = await Promise.all(
|
|
19
|
+
names.map((n) => searchSimilar(stripScope(n)).catch(() => [] as SearchHit[])),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const candidateNames = new Set<string>();
|
|
23
|
+
for (const hits of searchResults) {
|
|
24
|
+
for (const h of hits) candidateNames.add(h.name);
|
|
25
|
+
}
|
|
26
|
+
const extraNames = [...candidateNames].filter((n) => !dlMap.has(n));
|
|
27
|
+
if (extraNames.length > 0) {
|
|
28
|
+
const extra = await fetchBulkDownloads(extraNames);
|
|
29
|
+
for (const [k, v] of extra) dlMap.set(k, v);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return names.map((name, idx) => {
|
|
33
|
+
const bare = stripScope(name);
|
|
34
|
+
const suspectDl = dlMap.get(name) || 0;
|
|
35
|
+
const hits = searchResults[idx] || [];
|
|
36
|
+
let best: TyposquatResult = {
|
|
37
|
+
suspect: name, likelyTarget: null, confidence: 'none',
|
|
38
|
+
reason: '', targetDownloads: 0, suspectDownloads: suspectDl,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const h of hits) {
|
|
42
|
+
if (h.name === name || h.name === bare) continue;
|
|
43
|
+
const cBare = stripScope(h.name);
|
|
44
|
+
const d = levenshtein(bare, cBare);
|
|
45
|
+
if (d === 0 || d > 2) continue;
|
|
46
|
+
if (d === 2 && bare.length < 5) continue;
|
|
47
|
+
|
|
48
|
+
const cDl = dlMap.get(h.name) || 0;
|
|
49
|
+
const ratio = cDl / Math.max(suspectDl, 1);
|
|
50
|
+
|
|
51
|
+
if (d === 1 && ratio >= 100) {
|
|
52
|
+
return {
|
|
53
|
+
suspect: name, likelyTarget: h.name, confidence: 'high' as const,
|
|
54
|
+
reason: `${h.name} has ${fmt(cDl)} weekly downloads vs ${fmt(suspectDl)}`,
|
|
55
|
+
targetDownloads: cDl, suspectDownloads: suspectDl,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (d <= 2 && ratio >= 500 && rankHigher(best, 'medium')) {
|
|
59
|
+
best = {
|
|
60
|
+
suspect: name, likelyTarget: h.name, confidence: 'medium',
|
|
61
|
+
reason: `similar to ${h.name} (${fmt(cDl)} weekly downloads)`,
|
|
62
|
+
targetDownloads: cDl, suspectDownloads: suspectDl,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (d === 1 && ratio >= 10 && rankHigher(best, 'low')) {
|
|
66
|
+
best = {
|
|
67
|
+
suspect: name, likelyTarget: h.name, confidence: 'low',
|
|
68
|
+
reason: `name close to ${h.name}`,
|
|
69
|
+
targetDownloads: cDl, suspectDownloads: suspectDl,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (detectSeparatorTrick(bare, cBare) && ratio >= 50 && rankHigher(best, 'medium')) {
|
|
73
|
+
best = {
|
|
74
|
+
suspect: name, likelyTarget: h.name, confidence: 'medium',
|
|
75
|
+
reason: `separator variation of ${h.name} (${fmt(cDl)} downloads)`,
|
|
76
|
+
targetDownloads: cDl, suspectDownloads: suspectDl,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return best;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface SearchHit { name: string }
|
|
85
|
+
|
|
86
|
+
async function searchSimilar(name: string): Promise<SearchHit[]> {
|
|
87
|
+
const res = await fetch(`${NPM_SEARCH}?text=${encodeURIComponent(name)}&size=10`);
|
|
88
|
+
if (!res.ok) return [];
|
|
89
|
+
const data = await res.json() as {
|
|
90
|
+
objects: Array<{ package: { name: string } }>;
|
|
91
|
+
};
|
|
92
|
+
return data.objects.map((o) => ({ name: o.package.name }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function fetchBulkDownloads(names: string[]): Promise<Map<string, number>> {
|
|
96
|
+
const map = new Map<string, number>();
|
|
97
|
+
const scoped: string[] = [];
|
|
98
|
+
const unscoped: string[] = [];
|
|
99
|
+
for (const n of names) (n.startsWith('@') ? scoped : unscoped).push(n);
|
|
100
|
+
|
|
101
|
+
if (unscoped.length > 0) {
|
|
102
|
+
const chunks = chunkArray(unscoped, 128);
|
|
103
|
+
const fetches = await Promise.allSettled(
|
|
104
|
+
chunks.map(async (chunk) => {
|
|
105
|
+
const res = await fetch(`${NPM_BULK_DL}/${chunk.join(',')}`);
|
|
106
|
+
if (!res.ok) return;
|
|
107
|
+
const data = await res.json() as Record<string, { downloads: number } | null>;
|
|
108
|
+
for (const [pkg, info] of Object.entries(data)) {
|
|
109
|
+
if (info?.downloads) map.set(pkg, info.downloads);
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scopedFetches = await Promise.allSettled(
|
|
116
|
+
scoped.map(async (n) => {
|
|
117
|
+
const res = await fetch(`${NPM_BULK_DL}/${encodeURIComponent(n)}`);
|
|
118
|
+
if (!res.ok) return;
|
|
119
|
+
const data = await res.json() as { downloads: number };
|
|
120
|
+
if (data.downloads) map.set(n, data.downloads);
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return map;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function chunkArray<T>(arr: T[], size: number): T[][] {
|
|
128
|
+
const result: T[][] = [];
|
|
129
|
+
for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stripScope(name: string): string {
|
|
134
|
+
return name.startsWith('@') ? name.split('/').pop() || name : name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function levenshtein(a: string, b: string): number {
|
|
138
|
+
const m = a.length, n = b.length;
|
|
139
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
140
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
141
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
142
|
+
for (let i = 1; i <= m; i++) {
|
|
143
|
+
for (let j = 1; j <= n; j++) {
|
|
144
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
145
|
+
? dp[i - 1][j - 1]
|
|
146
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return dp[m][n];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function detectSeparatorTrick(a: string, b: string): boolean {
|
|
153
|
+
const norm = (s: string) => s.replace(/[-_.]/g, '');
|
|
154
|
+
return norm(a) === norm(b) && a !== b;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const RANK = { none: 0, low: 1, medium: 2, high: 3 } as const;
|
|
158
|
+
function rankHigher(current: TyposquatResult, level: TyposquatResult['confidence']): boolean {
|
|
159
|
+
return RANK[level] > RANK[current.confidence];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function fmt(n: number): string {
|
|
163
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
164
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
165
|
+
return String(n);
|
|
166
|
+
}
|