opmsec 0.1.0

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 (57) hide show
  1. package/.env.example +14 -0
  2. package/.pnp.cjs +9953 -0
  3. package/.pnp.loader.mjs +2126 -0
  4. package/README.md +266 -0
  5. package/bun.lock +620 -0
  6. package/bunfig.toml +6 -0
  7. package/docker-compose.yml +10 -0
  8. package/package.json +39 -0
  9. package/packages/cli/package.json +7 -0
  10. package/packages/cli/src/commands/audit.tsx +142 -0
  11. package/packages/cli/src/commands/author-view.tsx +247 -0
  12. package/packages/cli/src/commands/info.tsx +109 -0
  13. package/packages/cli/src/commands/install.tsx +362 -0
  14. package/packages/cli/src/commands/passthrough.tsx +36 -0
  15. package/packages/cli/src/commands/push.tsx +321 -0
  16. package/packages/cli/src/components/AgentScores.tsx +32 -0
  17. package/packages/cli/src/components/AuthorInfo.tsx +45 -0
  18. package/packages/cli/src/components/Header.tsx +24 -0
  19. package/packages/cli/src/components/PackageCard.tsx +48 -0
  20. package/packages/cli/src/components/RiskBadge.tsx +32 -0
  21. package/packages/cli/src/components/ScanReport.tsx +50 -0
  22. package/packages/cli/src/components/StatusLine.tsx +30 -0
  23. package/packages/cli/src/index.tsx +111 -0
  24. package/packages/cli/src/services/avatar.ts +10 -0
  25. package/packages/cli/src/services/chainpatrol.ts +25 -0
  26. package/packages/cli/src/services/contract.ts +182 -0
  27. package/packages/cli/src/services/ens.ts +143 -0
  28. package/packages/cli/src/services/fileverse.ts +36 -0
  29. package/packages/cli/src/services/osv.ts +141 -0
  30. package/packages/cli/src/services/signature.ts +22 -0
  31. package/packages/cli/src/services/version.ts +10 -0
  32. package/packages/contracts/contracts/OPMRegistry.sol +253 -0
  33. package/packages/contracts/hardhat.config.ts +32 -0
  34. package/packages/contracts/package-lock.json +7772 -0
  35. package/packages/contracts/package.json +10 -0
  36. package/packages/contracts/scripts/deploy.ts +28 -0
  37. package/packages/contracts/test/OPMRegistry.test.ts +101 -0
  38. package/packages/contracts/tsconfig.json +11 -0
  39. package/packages/core/package.json +7 -0
  40. package/packages/core/src/abi.ts +629 -0
  41. package/packages/core/src/constants.ts +30 -0
  42. package/packages/core/src/index.ts +5 -0
  43. package/packages/core/src/prompt.ts +111 -0
  44. package/packages/core/src/types.ts +104 -0
  45. package/packages/core/src/utils.ts +50 -0
  46. package/packages/scanner/package.json +6 -0
  47. package/packages/scanner/src/agents/agent-configs.ts +24 -0
  48. package/packages/scanner/src/agents/base-agent.ts +75 -0
  49. package/packages/scanner/src/index.ts +25 -0
  50. package/packages/scanner/src/queue/memory-queue.ts +91 -0
  51. package/packages/scanner/src/services/contract-writer.ts +34 -0
  52. package/packages/scanner/src/services/fileverse.ts +89 -0
  53. package/packages/scanner/src/services/npm-registry.ts +159 -0
  54. package/packages/scanner/src/services/openrouter.ts +86 -0
  55. package/packages/scanner/src/services/osv.ts +87 -0
  56. package/packages/scanner/src/services/report-formatter.ts +134 -0
  57. package/tsconfig.json +23 -0
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bun
2
+ import React from 'react';
3
+ import { render, Box, Text } from 'ink';
4
+ import { PushCommand } from './commands/push';
5
+ import { InstallCommand } from './commands/install';
6
+ import { AuditCommand } from './commands/audit';
7
+ import { InfoCommand } from './commands/info';
8
+ import { AuthorViewCommand } from './commands/author-view';
9
+ import { PassthroughCommand } from './commands/passthrough';
10
+ import { Header } from './components/Header';
11
+
12
+ const args = process.argv.slice(2);
13
+ const command = args[0];
14
+ const rest = args.slice(1);
15
+
16
+ function parsePackageArg(pkg?: string) {
17
+ if (!pkg) return {};
18
+ const atIdx = pkg.lastIndexOf('@');
19
+ if (atIdx > 0) return { name: pkg.slice(0, atIdx), version: pkg.slice(atIdx + 1) };
20
+ return { name: pkg };
21
+ }
22
+
23
+ function isENSName(arg?: string): boolean {
24
+ return !!arg && arg.endsWith('.eth');
25
+ }
26
+
27
+ const PASSTHROUGH = new Set(['init', 'run', 'test', 'uninstall', 'remove', 'rm', 'ls', 'list', 'outdated', 'update', 'link', 'pack', 'start', 'build']);
28
+
29
+ function App() {
30
+ switch (command) {
31
+ case 'push': {
32
+ const tokenIdx = rest.findIndex((a) => a === '--token' || a === '--access-token');
33
+ const npmToken = tokenIdx >= 0 ? rest[tokenIdx + 1] : undefined;
34
+ const otpIdx = rest.findIndex((a) => a === '--otp');
35
+ const otp = otpIdx >= 0 ? rest[otpIdx + 1] : undefined;
36
+ return <PushCommand npmToken={npmToken} otp={otp} />;
37
+ }
38
+ case 'install':
39
+ case 'i':
40
+ case 'add': {
41
+ const { name, version } = parsePackageArg(rest[0]);
42
+ return <InstallCommand packageName={name} version={version} />;
43
+ }
44
+ case 'audit':
45
+ return <AuditCommand />;
46
+ case 'info': {
47
+ const { name, version } = parsePackageArg(rest[0]);
48
+ if (!name) return <Help />;
49
+ return <InfoCommand packageName={name} version={version} />;
50
+ }
51
+ case 'view': {
52
+ if (isENSName(rest[0])) {
53
+ return <AuthorViewCommand ensName={rest[0]} />;
54
+ }
55
+ const { name, version } = parsePackageArg(rest[0]);
56
+ if (!name) return <Help />;
57
+ return <InfoCommand packageName={name} version={version} />;
58
+ }
59
+ case 'whois': {
60
+ if (!rest[0]) return <Help />;
61
+ const ensArg = rest[0].endsWith('.eth') ? rest[0] : `${rest[0]}.eth`;
62
+ return <AuthorViewCommand ensName={ensArg} />;
63
+ }
64
+ default:
65
+ if (command && PASSTHROUGH.has(command)) {
66
+ return <PassthroughCommand command={command} args={rest} />;
67
+ }
68
+ return <Help />;
69
+ }
70
+ }
71
+
72
+ function Help() {
73
+ return (
74
+ <Box flexDirection="column">
75
+ <Header />
76
+ <Box flexDirection="column" marginLeft={2}>
77
+ <Text color="cyan" bold>Security commands:</Text>
78
+ <Text> opm push [--token t] [--otp c] Sign, scan, publish, register</Text>
79
+ <Text> opm install [pkg] Install with on-chain security verification</Text>
80
+ <Text> opm audit Scan all deps against on-chain security data</Text>
81
+ <Text> opm info {'<pkg>'} Show on-chain security info for a package</Text>
82
+ <Text> opm view {'<name.eth>'} Show author profile, packages, and risk scores</Text>
83
+ <Text> opm whois {'<name>'} Look up an ENS identity on OPM</Text>
84
+ <Text> </Text>
85
+ <Text color="cyan" bold>Standard commands (npm passthrough):</Text>
86
+ <Text> opm init Initialize a new package</Text>
87
+ <Text> opm run {'<script>'} Run a package script</Text>
88
+ <Text> opm test Run tests</Text>
89
+ <Text> opm start Run start script</Text>
90
+ <Text> opm build Run build script</Text>
91
+ <Text> opm uninstall {'<pkg>'} Remove a dependency</Text>
92
+ <Text> opm outdated Check for outdated deps</Text>
93
+ <Text> opm update Update dependencies</Text>
94
+ <Text> opm list List installed packages</Text>
95
+ <Text> opm link Symlink a package</Text>
96
+ <Text> opm pack Create a tarball</Text>
97
+ <Text> </Text>
98
+ <Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
99
+ <Text color="gray"> view name.eth → author profile, view pkg → info</Text>
100
+ <Text> </Text>
101
+ <Text color="cyan" bold>Environment:</Text>
102
+ <Text> OPM_PRIVATE_KEY Author signing key</Text>
103
+ <Text> CONTRACT_ADDRESS OPMRegistry contract on Base Sepolia</Text>
104
+ <Text> OPENAI_API_KEY For AI security scanning</Text>
105
+ <Text> NPM_TOKEN npm automation token (alt to --token)</Text>
106
+ </Box>
107
+ </Box>
108
+ );
109
+ }
110
+
111
+ render(<App />);
@@ -0,0 +1,10 @@
1
+ import terminalImage from 'terminal-image';
2
+
3
+ export async function renderAvatar(url: string, width = 16): Promise<string | null> {
4
+ try {
5
+ const res = await fetch(url, { redirect: 'follow' });
6
+ if (!res.ok) return null;
7
+ const buffer = Buffer.from(await res.arrayBuffer());
8
+ return await terminalImage.buffer(buffer, { width, preserveAspectRatio: true, preferNativeRender: false });
9
+ } catch { return null; }
10
+ }
@@ -0,0 +1,25 @@
1
+ import { CHAINPATROL_API_URL, getEnvOrThrow } from '@opm/core';
2
+ import type { ChainPatrolResult } from '@opm/core';
3
+
4
+ export async function checkPackageWithChainPatrol(packageName: string): Promise<ChainPatrolResult> {
5
+ const apiKey = getEnvOrThrow('CHAINPATROL_API_KEY');
6
+
7
+ const res = await fetch(`${CHAINPATROL_API_URL}/asset/check`, {
8
+ method: 'POST',
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ 'X-API-KEY': apiKey,
12
+ },
13
+ body: JSON.stringify({ content: `npm:${packageName}` }),
14
+ });
15
+
16
+ if (!res.ok) {
17
+ return { status: 'UNKNOWN', source: 'error' };
18
+ }
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
+ }
@@ -0,0 +1,182 @@
1
+ import { ethers } from 'ethers';
2
+ import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC } from '@opm/core';
3
+ import type { OnChainPackageInfo, AuthorProfile } from '@opm/core';
4
+
5
+ function getReadContract() {
6
+ const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
7
+ const provider = new ethers.JsonRpcProvider(rpc);
8
+ const address = getEnvOrThrow('CONTRACT_ADDRESS');
9
+ return new ethers.Contract(address, OPM_REGISTRY_ABI, provider);
10
+ }
11
+
12
+ function getWriteContract() {
13
+ const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
14
+ const provider = new ethers.JsonRpcProvider(rpc);
15
+ const wallet = new ethers.Wallet(getEnvOrThrow('OPM_PRIVATE_KEY'), provider);
16
+ const address = getEnvOrThrow('CONTRACT_ADDRESS');
17
+ return new ethers.Contract(address, OPM_REGISTRY_ABI, wallet);
18
+ }
19
+
20
+ export async function getPackageInfo(name: string, version: string): Promise<OnChainPackageInfo> {
21
+ const contract = getReadContract();
22
+ const [author, checksum, sig, ensName, reportURI, aggregateScore, exists] =
23
+ await contract.getPackageInfo(name, version);
24
+
25
+ let scores: OnChainPackageInfo['scores'] = [];
26
+ if (exists) {
27
+ const rawScores = await contract.getScores(name, version);
28
+ scores = rawScores.map((s: { agent: string; riskScore: number; reasoning: string }) => ({
29
+ agent: s.agent,
30
+ riskScore: Number(s.riskScore),
31
+ reasoning: s.reasoning,
32
+ }));
33
+ }
34
+
35
+ return {
36
+ author,
37
+ checksum,
38
+ signature: ethers.hexlify(sig),
39
+ ensName,
40
+ reportURI,
41
+ scores,
42
+ aggregateScore: Number(aggregateScore),
43
+ exists,
44
+ };
45
+ }
46
+
47
+ export async function registerPackageOnChain(
48
+ name: string,
49
+ version: string,
50
+ checksum: string,
51
+ signature: Uint8Array,
52
+ ensName: string,
53
+ ): Promise<string> {
54
+ const contract = getWriteContract();
55
+ const tx = await contract.registerPackage(name, version, checksum, signature, ensName);
56
+ const receipt = await tx.wait();
57
+ return receipt.hash;
58
+ }
59
+
60
+ export async function getVersions(name: string): Promise<string[]> {
61
+ const contract = getReadContract();
62
+ return contract.getVersions(name);
63
+ }
64
+
65
+ export async function getSafestVersion(name: string, lookback: number = 3): Promise<string> {
66
+ const contract = getReadContract();
67
+ return contract.getSafestVersion(name, lookback);
68
+ }
69
+
70
+ export async function getAuthorProfile(address: string): Promise<AuthorProfile> {
71
+ const contract = getReadContract();
72
+ const p = await contract.getAuthorByAddress(address);
73
+ return {
74
+ addr: p.addr,
75
+ ensName: p.ensName,
76
+ reputationScore: p.reputationCount > 0
77
+ ? Number(p.reputationTotal) / Number(p.reputationCount)
78
+ : 0,
79
+ packagesPublished: Number(p.packagesPublished),
80
+ };
81
+ }
82
+
83
+ export async function getAuthorByENS(ensName: string): Promise<AuthorProfile> {
84
+ const contract = getReadContract();
85
+ const p = await contract.getAuthorByENS(ensName);
86
+ return {
87
+ addr: p.addr,
88
+ ensName: p.ensName,
89
+ reputationScore: p.reputationCount > 0
90
+ ? Number(p.reputationTotal) / Number(p.reputationCount)
91
+ : 0,
92
+ packagesPublished: Number(p.packagesPublished),
93
+ };
94
+ }
95
+
96
+ export interface AuthorPackageSummary {
97
+ name: string;
98
+ version: string;
99
+ aggregateScore: number;
100
+ reportURI: string;
101
+ checksum: string;
102
+ signature: string;
103
+ }
104
+
105
+ export async function getPackagesByAuthor(authorAddress: string): Promise<AuthorPackageSummary[]> {
106
+ const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
107
+ const provider = new ethers.JsonRpcProvider(rpc);
108
+ const contractAddr = getEnvOrThrow('CONTRACT_ADDRESS');
109
+ const contract = new ethers.Contract(contractAddr, OPM_REGISTRY_ABI, provider);
110
+
111
+ const packageMap = new Map<string, { name: string; version: string }>();
112
+
113
+ try {
114
+ const latest = await provider.getBlockNumber();
115
+ const CHUNK = 5_000;
116
+ const startBlock = Math.max(0, latest - 50_000);
117
+
118
+ for (let from = startBlock; from <= latest; from += CHUNK) {
119
+ const to = Math.min(from + CHUNK - 1, latest);
120
+ try {
121
+ const events = await contract.queryFilter(
122
+ contract.filters.PackageRegistered(), from, to,
123
+ );
124
+ for (const event of events) {
125
+ const parsed = contract.interface.parseLog({
126
+ topics: event.topics as string[], data: event.data,
127
+ });
128
+ if (!parsed) continue;
129
+ const [name, version, author] = parsed.args;
130
+ if (author.toLowerCase() === authorAddress.toLowerCase()) {
131
+ packageMap.set(`${name}@${version}`, { name, version });
132
+ }
133
+ }
134
+ } catch { /* chunk failed, continue */ }
135
+ }
136
+ } catch { /* event scan failed entirely */ }
137
+
138
+ const results: AuthorPackageSummary[] = [];
139
+ for (const { name, version } of packageMap.values()) {
140
+ try {
141
+ const info = await getPackageInfo(name, version);
142
+ if (info.exists) {
143
+ results.push({
144
+ name, version,
145
+ aggregateScore: info.aggregateScore,
146
+ reportURI: info.reportURI,
147
+ checksum: info.checksum,
148
+ signature: info.signature,
149
+ });
150
+ }
151
+ } catch { /* skip */ }
152
+ }
153
+
154
+ return results;
155
+ }
156
+
157
+ export async function getPackagesByAuthorDirect(
158
+ authorAddress: string,
159
+ knownPackageNames: string[],
160
+ ): Promise<AuthorPackageSummary[]> {
161
+ const results: AuthorPackageSummary[] = [];
162
+
163
+ for (const name of knownPackageNames) {
164
+ try {
165
+ const versions = await getVersions(name);
166
+ for (const version of versions) {
167
+ const info = await getPackageInfo(name, version);
168
+ if (info.exists && info.author.toLowerCase() === authorAddress.toLowerCase()) {
169
+ results.push({
170
+ name, version,
171
+ aggregateScore: info.aggregateScore,
172
+ reportURI: info.reportURI,
173
+ checksum: info.checksum,
174
+ signature: info.signature,
175
+ });
176
+ }
177
+ }
178
+ } catch { /* skip */ }
179
+ }
180
+
181
+ return results;
182
+ }
@@ -0,0 +1,143 @@
1
+ import { createPublicClient, http } from 'viem';
2
+ import { mainnet, sepolia } from 'viem/chains';
3
+ import { addEnsContracts } from '@ensdomains/ensjs';
4
+ import { getName } from '@ensdomains/ensjs/public';
5
+ import { getRecords } from '@ensdomains/ensjs/public';
6
+ import { getAddressRecord } from '@ensdomains/ensjs/public';
7
+ import { getEnvOrDefault } from '@opm/core';
8
+
9
+ function getSepoliaClient() {
10
+ const rpc = getEnvOrDefault('ETH_SEPOLIA_RPC_URL', 'https://ethereum-sepolia-rpc.publicnode.com');
11
+ return createPublicClient({
12
+ chain: addEnsContracts(sepolia),
13
+ transport: http(rpc),
14
+ });
15
+ }
16
+
17
+ function getMainnetClient() {
18
+ const rpc = getEnvOrDefault('ETH_MAINNET_RPC_URL', 'https://eth.llamarpc.com');
19
+ return createPublicClient({
20
+ chain: addEnsContracts(mainnet),
21
+ transport: http(rpc),
22
+ });
23
+ }
24
+
25
+ export async function resolveENSName(address: string): Promise<string | null> {
26
+ const addr = address as `0x${string}`;
27
+ const clients = [
28
+ { client: getSepoliaClient(), label: 'sepolia' },
29
+ { client: getMainnetClient(), label: 'mainnet' },
30
+ ];
31
+
32
+ const results = await Promise.allSettled(
33
+ clients.map(({ client }) =>
34
+ getName(client as any, { address: addr, allowMismatch: true }),
35
+ ),
36
+ );
37
+
38
+ for (const result of results) {
39
+ if (result.status === 'fulfilled' && result.value?.name) {
40
+ return result.value.name;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ export async function resolveAddress(ensName: string): Promise<string | null> {
47
+ const clients = [
48
+ { client: getSepoliaClient(), label: 'sepolia' },
49
+ { client: getMainnetClient(), label: 'mainnet' },
50
+ ];
51
+
52
+ const results = await Promise.allSettled(
53
+ clients.map(({ client }) =>
54
+ getAddressRecord(client as any, { name: ensName, coin: 'ETH' }),
55
+ ),
56
+ );
57
+
58
+ for (const result of results) {
59
+ if (result.status === 'fulfilled' && result.value?.value) {
60
+ return result.value.value;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ export interface ENSProfile {
67
+ name: string | null;
68
+ avatar: string | null;
69
+ url: string | null;
70
+ github: string | null;
71
+ twitter: string | null;
72
+ description: string | null;
73
+ email: string | null;
74
+ }
75
+
76
+ export async function getENSProfile(address: string): Promise<ENSProfile> {
77
+ const profile: ENSProfile = {
78
+ name: null, avatar: null, url: null,
79
+ github: null, twitter: null, description: null, email: null,
80
+ };
81
+
82
+ const name = await resolveENSName(address);
83
+ if (!name) return profile;
84
+ profile.name = name;
85
+
86
+ const clients = [
87
+ getSepoliaClient(),
88
+ getMainnetClient(),
89
+ ];
90
+
91
+ for (const client of clients) {
92
+ try {
93
+ const records = await getRecords(client as any, {
94
+ name,
95
+ texts: ['avatar', 'url', 'com.github', 'com.twitter', 'description', 'email'],
96
+ contentHash: false,
97
+ abi: false,
98
+ });
99
+
100
+ if (records?.texts?.length > 0) {
101
+ for (const t of records.texts) {
102
+ if (t.key === 'avatar' && t.value) profile.avatar = t.value;
103
+ if (t.key === 'url' && t.value) profile.url = t.value;
104
+ if (t.key === 'com.github' && t.value) profile.github = t.value;
105
+ if (t.key === 'com.twitter' && t.value) profile.twitter = t.value;
106
+ if (t.key === 'description' && t.value) profile.description = t.value;
107
+ if (t.key === 'email' && t.value) profile.email = t.value;
108
+ }
109
+ if (records.texts.some((t) => t.value)) return profile;
110
+ }
111
+ } catch { /* try next client */ }
112
+ }
113
+
114
+ return profile;
115
+ }
116
+
117
+ export async function getENSTextRecords(
118
+ ensName: string,
119
+ keys: string[],
120
+ ): Promise<Record<string, string>> {
121
+ const clients = [getSepoliaClient(), getMainnetClient()];
122
+
123
+ for (const client of clients) {
124
+ try {
125
+ const records = await getRecords(client as any, {
126
+ name: ensName,
127
+ texts: keys,
128
+ contentHash: false,
129
+ abi: false,
130
+ });
131
+
132
+ if (records?.texts?.length > 0) {
133
+ const result: Record<string, string> = {};
134
+ for (const t of records.texts) {
135
+ if (t.value) result[t.key] = t.value;
136
+ }
137
+ if (Object.keys(result).length > 0) return result;
138
+ }
139
+ } catch { /* try next client */ }
140
+ }
141
+
142
+ return {};
143
+ }
@@ -0,0 +1,36 @@
1
+ import type { ScanReport } from '@opm/core';
2
+ import { getEnvOrDefault, safeJsonParse } from '@opm/core';
3
+
4
+ const DEFAULT_API_URL = 'http://localhost:8001';
5
+
6
+ export async function fetchReportFromFileverse(reportURI: string): Promise<ScanReport | null> {
7
+ if (!reportURI || reportURI.startsWith('local://')) return null;
8
+
9
+ const apiKey = process.env.FILEVERSE_API_KEY;
10
+ const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', DEFAULT_API_URL);
11
+
12
+ const ddocId = extractDdocId(reportURI);
13
+ if (ddocId && apiKey) {
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 */ }
21
+ }
22
+
23
+ return null;
24
+ }
25
+
26
+ function extractDdocId(link: string): string | null {
27
+ try {
28
+ const url = new URL(link);
29
+ const parts = url.pathname.split('/');
30
+ const dIdx = parts.indexOf('d');
31
+ if (dIdx >= 0 && parts[dIdx + 1]) return parts[dIdx + 1];
32
+ const pendingIdx = parts.indexOf('pending');
33
+ if (pendingIdx >= 0 && parts[pendingIdx + 1]) return parts[pendingIdx + 1];
34
+ } catch { /* not a URL */ }
35
+ return null;
36
+ }
@@ -0,0 +1,141 @@
1
+ const OSV_API = 'https://api.osv.dev/v1/query';
2
+
3
+ export interface OSVVulnerability {
4
+ id: string;
5
+ summary: string;
6
+ details: string;
7
+ severity: Array<{ type: string; score: string }>;
8
+ affected: Array<{
9
+ ranges: Array<{ events: Array<{ introduced?: string; fixed?: string }> }>;
10
+ }>;
11
+ references: Array<{ type: string; url: string }>;
12
+ database_specific?: { severity?: string; [key: string]: unknown };
13
+ }
14
+
15
+ export async function queryOSV(packageName: string, version: string): Promise<OSVVulnerability[]> {
16
+ if (!version || version === 'latest') return [];
17
+
18
+ try {
19
+ const res = await fetch(OSV_API, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({
23
+ package: { name: packageName, ecosystem: 'npm' },
24
+ version,
25
+ }),
26
+ });
27
+ if (!res.ok) return [];
28
+ const data = await res.json() as { vulns?: OSVVulnerability[] };
29
+ return data.vulns || [];
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ export function getOSVSeverity(vuln: OSVVulnerability): string {
36
+ const dbSev = vuln.database_specific?.severity;
37
+ if (typeof dbSev === 'string' && dbSev.length > 0) {
38
+ const norm = dbSev.toUpperCase();
39
+ if (norm === 'CRITICAL') return 'CRITICAL';
40
+ if (norm === 'HIGH') return 'HIGH';
41
+ if (norm === 'MODERATE' || norm === 'MEDIUM') return 'MEDIUM';
42
+ if (norm === 'LOW') return 'LOW';
43
+ }
44
+
45
+ if (vuln.severity?.length > 0) {
46
+ const cvss = vuln.severity.find((s) => s.type === 'CVSS_V3' || s.type === 'CVSS_V4');
47
+ if (cvss?.score) {
48
+ const numericScore = computeCVSSv3BaseScore(cvss.score);
49
+ if (numericScore !== null) {
50
+ if (numericScore >= 9.0) return 'CRITICAL';
51
+ if (numericScore >= 7.0) return 'HIGH';
52
+ if (numericScore >= 4.0) return 'MEDIUM';
53
+ return 'LOW';
54
+ }
55
+ }
56
+ }
57
+
58
+ return 'UNKNOWN';
59
+ }
60
+
61
+ const CVSS_V3_METRICS: Record<string, Record<string, number>> = {
62
+ AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.20 },
63
+ AC: { L: 0.77, H: 0.44 },
64
+ PR_U: { N: 0.85, L: 0.62, H: 0.27 },
65
+ PR_C: { N: 0.85, L: 0.68, H: 0.50 },
66
+ UI: { N: 0.85, R: 0.62 },
67
+ C: { H: 0.56, L: 0.22, N: 0 },
68
+ I: { H: 0.56, L: 0.22, N: 0 },
69
+ A: { H: 0.56, L: 0.22, N: 0 },
70
+ };
71
+
72
+ function computeCVSSv3BaseScore(vector: string): number | null {
73
+ const parts = vector.split('/');
74
+ const metrics: Record<string, string> = {};
75
+ for (const part of parts) {
76
+ const [key, val] = part.split(':');
77
+ if (key && val) metrics[key] = val;
78
+ }
79
+
80
+ const scope = metrics['S'];
81
+ if (!scope || !metrics['AV'] || !metrics['AC'] || !metrics['PR'] || !metrics['UI']) return null;
82
+ if (!metrics['C'] || !metrics['I'] || !metrics['A']) return null;
83
+
84
+ const av = CVSS_V3_METRICS.AV[metrics['AV']];
85
+ const ac = CVSS_V3_METRICS.AC[metrics['AC']];
86
+ const prTable = scope === 'C' ? CVSS_V3_METRICS.PR_C : CVSS_V3_METRICS.PR_U;
87
+ const pr = prTable[metrics['PR']];
88
+ const ui = CVSS_V3_METRICS.UI[metrics['UI']];
89
+ const c = CVSS_V3_METRICS.C[metrics['C']];
90
+ const i = CVSS_V3_METRICS.I[metrics['I']];
91
+ const a = CVSS_V3_METRICS.A[metrics['A']];
92
+
93
+ if ([av, ac, pr, ui, c, i, a].some((v) => v === undefined)) return null;
94
+
95
+ const iscBase = 1 - (1 - c) * (1 - i) * (1 - a);
96
+ const impact = scope === 'U'
97
+ ? 6.42 * iscBase
98
+ : 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(Math.max(iscBase - 0.02, 0), 15);
99
+
100
+ if (impact <= 0) return 0;
101
+
102
+ const exploitability = 8.22 * av * ac * pr * ui;
103
+ const raw = scope === 'U'
104
+ ? Math.min(impact + exploitability, 10)
105
+ : Math.min(1.08 * (impact + exploitability), 10);
106
+
107
+ return Math.ceil(raw * 10) / 10;
108
+ }
109
+
110
+ export function getFixedVersion(vuln: OSVVulnerability, forVersion?: string): string | null {
111
+ const majorMinor = forVersion ? getMajorMinor(forVersion) : null;
112
+ let bestMatch: string | null = null;
113
+ let anyFix: string | null = null;
114
+
115
+ for (const aff of vuln.affected || []) {
116
+ for (const range of aff.ranges || []) {
117
+ let introduced: string | null = null;
118
+ let fixed: string | null = null;
119
+ for (const event of range.events || []) {
120
+ if (event.introduced) introduced = event.introduced;
121
+ if (event.fixed) fixed = event.fixed;
122
+ }
123
+ if (!fixed) continue;
124
+ if (!anyFix) anyFix = fixed;
125
+
126
+ if (majorMinor && introduced) {
127
+ const introMM = getMajorMinor(introduced);
128
+ if (introMM === majorMinor) {
129
+ bestMatch = fixed;
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ return bestMatch || anyFix;
136
+ }
137
+
138
+ function getMajorMinor(version: string): string {
139
+ const parts = version.replace(/^v/, '').split('.');
140
+ return parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0];
141
+ }
@@ -0,0 +1,22 @@
1
+ import { ethers } from 'ethers';
2
+ import * as fs from 'fs';
3
+
4
+ export function computeChecksum(filePath: string): string {
5
+ const data = fs.readFileSync(filePath);
6
+ return ethers.keccak256(data);
7
+ }
8
+
9
+ export async function signChecksumAsync(checksum: string, privateKey: string): Promise<{ signature: string; address: string }> {
10
+ const wallet = new ethers.Wallet(privateKey);
11
+ const signature = await wallet.signMessage(ethers.getBytes(checksum));
12
+ return { signature, address: wallet.address };
13
+ }
14
+
15
+ export function verifyChecksum(checksum: string, signature: string, expectedAddress: string): boolean {
16
+ try {
17
+ const recovered = ethers.verifyMessage(ethers.getBytes(checksum), signature);
18
+ return recovered.toLowerCase() === expectedAddress.toLowerCase();
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ import { getVersions } from './contract';
2
+
3
+ export async function resolveVersion(name: string, version: string): Promise<string> {
4
+ if (version && version !== 'latest') return version;
5
+ try {
6
+ const versions = await getVersions(name);
7
+ if (versions.length > 0) return versions[versions.length - 1];
8
+ } catch { /* no versions on-chain */ }
9
+ return version;
10
+ }