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.
- package/.env.example +1 -0
- package/.husky/pre-commit +1 -0
- package/README.md +71 -275
- package/bun.lock +5 -5
- package/docs/architecture/agents.mdx +11 -59
- package/docs/architecture/benchmarks.mdx +20 -46
- package/docs/architecture/overview.mdx +31 -38
- package/docs/architecture/scanner.mdx +11 -37
- package/docs/cli/audit.mdx +9 -12
- package/docs/cli/check.mdx +12 -26
- package/docs/cli/fix.mdx +10 -30
- package/docs/cli/info.mdx +12 -19
- package/docs/cli/install.mdx +27 -39
- package/docs/cli/push.mdx +40 -57
- package/docs/cli/register-agent.mdx +21 -53
- package/docs/cli/view.mdx +12 -29
- package/docs/concepts/ens-records.mdx +44 -0
- package/docs/concepts/multi-agent-consensus.mdx +18 -36
- package/docs/concepts/on-chain-registry.mdx +22 -49
- package/docs/concepts/security-model.mdx +20 -52
- package/docs/concepts/zk-agent-verification.mdx +26 -64
- package/docs/contract/events.mdx +13 -74
- package/docs/contract/functions.mdx +40 -126
- package/docs/contract/overview.mdx +17 -36
- package/docs/introduction.mdx +22 -25
- package/docs/mint.json +3 -2
- package/docs/quickstart.mdx +34 -70
- package/docs/system-design.png +0 -0
- package/package.json +7 -6
- package/packages/cli/src/commands/author-view.tsx +87 -2
- package/packages/cli/src/commands/check.tsx +18 -5
- package/packages/cli/src/commands/fix.tsx +25 -12
- package/packages/cli/src/commands/info.tsx +92 -4
- package/packages/cli/src/commands/install.tsx +327 -23
- package/packages/cli/src/commands/push.tsx +112 -0
- package/packages/cli/src/commands/register-agent.tsx +72 -31
- package/packages/cli/src/index.tsx +7 -5
- package/packages/cli/src/services/ens-records.ts +525 -0
- package/packages/cli/src/services/version.ts +156 -5
- package/packages/core/src/benchmarks.ts +116 -0
- package/packages/core/src/constants.ts +18 -6
- package/packages/core/src/model-rankings.ts +40 -15
- package/packages/core/src/types.ts +10 -0
- package/packages/core/src/utils.ts +136 -1
- package/packages/scanner/src/index.ts +2 -1
- package/packages/scanner/src/queue/memory-queue.ts +7 -2
- package/packages/scanner/src/services/benchmark-runner.ts +86 -1
- package/packages/scanner/src/services/fileverse.ts +61 -12
- package/packages/scanner/src/services/openrouter.ts +18 -7
- package/packages/web/.next/BUILD_ID +1 -0
- package/packages/web/.next/app-path-routes-manifest.json +4 -0
- package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
- package/packages/web/.next/diagnostics/framework.json +1 -0
- package/packages/web/.next/export-marker.json +6 -0
- package/packages/web/.next/images-manifest.json +58 -0
- package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
- package/packages/web/.next/next-server.js.nft.json +1 -0
- package/packages/web/.next/prerender-manifest.json +54 -4
- package/packages/web/.next/required-server-files.json +320 -0
- package/packages/web/.next/routes-manifest.json +53 -1
- package/packages/web/.next/server/app/_not-found/page.js +2 -0
- package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/packages/web/.next/server/app/_not-found.html +1 -0
- package/packages/web/.next/server/app/_not-found.meta +8 -0
- package/packages/web/.next/server/app/_not-found.rsc +18 -0
- package/packages/web/.next/server/app/index.html +6 -0
- package/packages/web/.next/server/app/index.meta +7 -0
- package/packages/web/.next/server/app/index.rsc +22 -0
- package/packages/web/.next/server/app/page.js +24 -24
- package/packages/web/.next/server/app/page.js.nft.json +1 -0
- package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/server/chunks/611.js +6 -0
- package/packages/web/.next/server/chunks/778.js +30 -0
- package/packages/web/.next/server/functions-config-manifest.json +4 -0
- package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -1
- package/packages/web/.next/server/next-font-manifest.js +1 -1
- package/packages/web/.next/server/next-font-manifest.json +1 -1
- package/packages/web/.next/server/pages/404.html +1 -0
- package/packages/web/.next/server/pages/500.html +1 -0
- package/packages/web/.next/server/pages/_app.js +1 -0
- package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
- package/packages/web/.next/server/pages/_document.js +1 -0
- package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
- package/packages/web/.next/server/pages/_error.js +19 -0
- package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
- package/packages/web/.next/server/webpack-runtime.js +2 -2
- package/packages/web/.next/static/0esGzFBCzREfVwijEGDfL/_buildManifest.js +1 -0
- package/packages/web/.next/static/0esGzFBCzREfVwijEGDfL/_ssgManifest.js +1 -0
- package/packages/web/.next/static/chunks/174-5b5efcb3b8efcc01.js +1 -0
- package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
- package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
- package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
- package/packages/web/.next/static/chunks/app/layout-de8e841104500505.js +1 -0
- package/packages/web/.next/static/chunks/app/layout.js +37 -7
- package/packages/web/.next/static/chunks/app/page-7e086379698b9fb0.js +1 -0
- package/packages/web/.next/static/chunks/app/page.js +297 -1
- package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
- package/packages/web/.next/static/chunks/main-4e8d71b5ef7ee7e3.js +1 -0
- package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
- package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/packages/web/.next/static/chunks/webpack-0dcd67569eb46132.js +1 -0
- package/packages/web/.next/static/chunks/webpack.js +2 -2
- package/packages/web/.next/static/css/102562cf2d0ae9b0.css +3 -0
- package/packages/web/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/packages/web/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/packages/web/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/packages/web/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/packages/web/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/packages/web/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- package/packages/web/.next/static/webpack/16f18baa938a434c.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/5fe9fe8578f9c3d2.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/73c7d02260cc80e4.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/a2d85d19aa028de1.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/app/{layout.73e341375c8d429e.hot-update.js → layout.16f18baa938a434c.hot-update.js} +1 -1
- package/packages/web/.next/static/webpack/app/{layout.6fee6306e0f98869.hot-update.js → layout.5fe9fe8578f9c3d2.hot-update.js} +1 -1
- package/packages/web/.next/static/webpack/app/layout.653e365406c0d9ac.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/layout.6800169a899e3a8b.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/layout.73c7d02260cc80e4.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/layout.a2d85d19aa028de1.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.653e365406c0d9ac.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.6800169a899e3a8b.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.73c7d02260cc80e4.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.a2d85d19aa028de1.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/{webpack.6fee6306e0f98869.hot-update.js → webpack.16f18baa938a434c.hot-update.js} +2 -2
- package/packages/web/.next/static/webpack/{webpack.73e341375c8d429e.hot-update.js → webpack.5fe9fe8578f9c3d2.hot-update.js} +2 -2
- package/packages/web/.next/static/webpack/webpack.653e365406c0d9ac.hot-update.js +12 -0
- package/packages/web/.next/static/webpack/webpack.6800169a899e3a8b.hot-update.js +12 -0
- package/packages/web/.next/static/webpack/webpack.73c7d02260cc80e4.hot-update.js +12 -0
- package/packages/web/.next/static/webpack/webpack.a2d85d19aa028de1.hot-update.js +12 -0
- package/packages/web/.next/trace +2 -5
- package/packages/web/app/globals.css +197 -51
- package/packages/web/app/layout.tsx +6 -3
- package/packages/web/app/page.tsx +791 -309
- package/packages/web/bun.lock +66 -105
- package/packages/web/next.config.ts +8 -1
- package/packages/web/package.json +5 -2
- package/packages/web/postcss.config.mjs +2 -2
- package/packages/web/public/apple-icon.png +1 -0
- package/packages/web/public/dependency-bottleneck.png +0 -0
- package/packages/web/public/icon-dark-32x32.png +1 -0
- package/packages/web/public/icon-light-32x32.png +1 -0
- package/packages/web/public/icon.svg +1 -0
- package/packages/web/public/nextjs-cve-announcement.png +0 -0
- package/packages/web/public/phantomraven-npm-attack.png +0 -0
- package/packages/web/public/placeholder-logo.png +1 -0
- package/packages/web/public/placeholder-logo.svg +1 -0
- package/packages/web/public/placeholder-user.jpg +1 -0
- package/packages/web/public/placeholder.jpg +1 -0
- package/packages/web/public/placeholder.svg +1 -0
- package/packages/web/public/react-cve-meme.png +0 -0
- package/packages/web/public/wallet-drain-exploit.png +0 -0
- package/packages/web/styles/globals.css +125 -0
- package/packages/web/.next/server/vendor-chunks/@swc.js +0 -55
- package/packages/web/.next/server/vendor-chunks/next.js +0 -3010
- package/packages/web/.next/static/chunks/app-pages-internals.js +0 -182
- package/packages/web/.next/static/chunks/main-app.js +0 -1882
- package/packages/web/.next/static/css/app/layout.css +0 -1237
- package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +0 -22
- package/packages/web/tailwind.config.ts +0 -48
- /package/packages/web/.next/static/chunks/{polyfills.js → polyfills-42372ed130431b0a.js} +0 -0
- /package/packages/web/.next/static/webpack/{6fee6306e0f98869.webpack.hot-update.json → 653e365406c0d9ac.webpack.hot-update.json} +0 -0
- /package/packages/web/.next/static/webpack/{73e341375c8d429e.webpack.hot-update.json → 6800169a899e3a8b.webpack.hot-update.json} +0 -0
|
@@ -448,3 +448,119 @@ export function evaluateBenchmark(
|
|
|
448
448
|
reason: passed ? 'Correctly classified' : reason,
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
|
+
|
|
452
|
+
/* ─── Batch (single-call) benchmark for fast agent registration ─── */
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Response-format-only instructions appended after the candidate's own system prompt.
|
|
456
|
+
* This tells the model HOW to respond, but the candidate's prompt determines
|
|
457
|
+
* the actual analysis behaviour.
|
|
458
|
+
*/
|
|
459
|
+
export const BATCH_RESPONSE_FORMAT_INSTRUCTIONS = `
|
|
460
|
+
|
|
461
|
+
For this benchmark you will be given a series of npm packages (labelled Case 1 through Case N). For each package you must decide whether it should be FLAGGED or SAFE.
|
|
462
|
+
|
|
463
|
+
You MUST respond with a valid JSON object matching this exact schema — no markdown, no explanation outside the JSON:
|
|
464
|
+
|
|
465
|
+
{
|
|
466
|
+
"answers": [
|
|
467
|
+
{ "case": 1, "flagged": <boolean> },
|
|
468
|
+
{ "case": 2, "flagged": <boolean> },
|
|
469
|
+
...
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
FLAGGED = true means the package is malicious, dangerous, or high/critical risk.
|
|
474
|
+
SAFE = false means the package is benign and low risk.`;
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Build the full system prompt used during batch benchmarking.
|
|
478
|
+
* If the candidate supplied a custom system prompt it is used as-is,
|
|
479
|
+
* with the response-format instructions appended.
|
|
480
|
+
* If no custom prompt is provided, fall back to the default OPM security
|
|
481
|
+
* auditor prompt so the model still knows its role.
|
|
482
|
+
*/
|
|
483
|
+
export function buildBatchSystemPrompt(candidateSystemPrompt?: string): string {
|
|
484
|
+
const base = candidateSystemPrompt || SYSTEM_PROMPT;
|
|
485
|
+
return base + BATCH_RESPONSE_FORMAT_INSTRUCTIONS;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface BatchBenchmarkAnswer {
|
|
489
|
+
case: number;
|
|
490
|
+
flagged: boolean;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface BatchBenchmarkResponse {
|
|
494
|
+
answers: BatchBenchmarkAnswer[];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export interface BatchBenchmarkResult {
|
|
498
|
+
caseId: string;
|
|
499
|
+
category: string;
|
|
500
|
+
expectedFlagged: boolean;
|
|
501
|
+
actualFlagged: boolean;
|
|
502
|
+
passed: boolean;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Determine the expected "flagged" label for a benchmark case.
|
|
507
|
+
* HIGH / CRITICAL → flagged=true, LOW / MEDIUM → flagged=false.
|
|
508
|
+
*/
|
|
509
|
+
export function expectedFlagForCase(bench: BenchmarkCase): boolean {
|
|
510
|
+
return bench.expected.risk_level === 'HIGH' || bench.expected.risk_level === 'CRITICAL';
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Build a single prompt containing all benchmark cases so the agent
|
|
515
|
+
* can return all answers in one LLM call.
|
|
516
|
+
*/
|
|
517
|
+
export function buildBatchBenchmarkPrompt(cases: BenchmarkCase[]): string {
|
|
518
|
+
const sections = cases.map((bench, i) => {
|
|
519
|
+
const depsStr = Object.entries(bench.metadata.dependencies || {})
|
|
520
|
+
.map(([k, v]) => `${k}@${v}`)
|
|
521
|
+
.join(', ') || 'none';
|
|
522
|
+
|
|
523
|
+
const scriptsStr = ['preinstall', 'postinstall', 'prepare']
|
|
524
|
+
.map((s) => `${s}: ${(bench.metadata.scripts as Record<string, string>)?.[s] || 'none'}`)
|
|
525
|
+
.join(', ');
|
|
526
|
+
|
|
527
|
+
const codeStr = bench.sourceFiles
|
|
528
|
+
.map((f) => ` File: ${f.path} (${f.size} bytes)\n \`\`\`\n ${f.content}\n \`\`\``)
|
|
529
|
+
.join('\n');
|
|
530
|
+
|
|
531
|
+
const cveStr = bench.knownCVEs.length > 0
|
|
532
|
+
? ` Known CVEs: ${bench.knownCVEs.map((c) => `${c.id}: ${c.summary}`).join('; ')}\n`
|
|
533
|
+
: '';
|
|
534
|
+
|
|
535
|
+
return `### Case ${i + 1}
|
|
536
|
+
- Name: ${bench.metadata.name}@${bench.metadata.version}
|
|
537
|
+
- Author: ${bench.metadata.author || 'unknown'}
|
|
538
|
+
- License: ${bench.metadata.license || 'none'}
|
|
539
|
+
- Dependencies: ${depsStr}
|
|
540
|
+
- Install scripts: ${scriptsStr}
|
|
541
|
+
${cveStr}${codeStr}`;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
return `Analyze each of the following ${cases.length} npm packages and decide if it should be FLAGGED or SAFE. Respond with the JSON schema from your system instructions.\n\n${sections.join('\n\n')}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Evaluate batch answers against ground-truth labels.
|
|
549
|
+
*/
|
|
550
|
+
export function evaluateBatchBenchmark(
|
|
551
|
+
cases: BenchmarkCase[],
|
|
552
|
+
answers: BatchBenchmarkAnswer[],
|
|
553
|
+
): BatchBenchmarkResult[] {
|
|
554
|
+
return cases.map((bench, i) => {
|
|
555
|
+
const expected = expectedFlagForCase(bench);
|
|
556
|
+
const answer = answers.find((a) => a.case === i + 1);
|
|
557
|
+
const actual = answer?.flagged ?? false;
|
|
558
|
+
return {
|
|
559
|
+
caseId: bench.id,
|
|
560
|
+
category: bench.category,
|
|
561
|
+
expectedFlagged: expected,
|
|
562
|
+
actualFlagged: actual,
|
|
563
|
+
passed: expected === actual,
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
}
|
|
@@ -2,15 +2,15 @@ export const HIGH_RISK_THRESHOLD = 70;
|
|
|
2
2
|
export const MEDIUM_RISK_THRESHOLD = 40;
|
|
3
3
|
|
|
4
4
|
export const OPENROUTER_MODELS = {
|
|
5
|
-
agent1: 'anthropic/claude-
|
|
6
|
-
agent2: 'google/gemini-
|
|
7
|
-
agent3: 'deepseek/deepseek-
|
|
5
|
+
agent1: 'anthropic/claude-opus-4.6',
|
|
6
|
+
agent2: 'google/gemini-3.1-pro-preview',
|
|
7
|
+
agent3: 'deepseek/deepseek-v3.2',
|
|
8
8
|
} as const;
|
|
9
9
|
|
|
10
10
|
export const OPENAI_MODELS = {
|
|
11
|
-
agent1: 'gpt-4
|
|
12
|
-
agent2: 'gpt-
|
|
13
|
-
agent3: 'gpt-
|
|
11
|
+
agent1: 'gpt-5.4',
|
|
12
|
+
agent2: 'gpt-5.3-codex',
|
|
13
|
+
agent3: 'gpt-5.2',
|
|
14
14
|
} as const;
|
|
15
15
|
|
|
16
16
|
export const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
@@ -48,3 +48,15 @@ export const SCANNABLE_EXTENSIONS = ['.js', '.ts', '.mjs', '.cjs', '.json'];
|
|
|
48
48
|
export const MAX_FILE_SIZE_BYTES = 100_000;
|
|
49
49
|
export const MAX_TOTAL_CODE_CHARS = 200_000;
|
|
50
50
|
export const VERSION_LOOKBACK = 3;
|
|
51
|
+
|
|
52
|
+
export const ENS_REGISTRY_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e';
|
|
53
|
+
|
|
54
|
+
export const OPM_ENS_KEYS = {
|
|
55
|
+
version: 'opm.version',
|
|
56
|
+
checksum: 'opm.checksum',
|
|
57
|
+
fileverse: 'opm.fileverse',
|
|
58
|
+
riskScore: 'opm.risk_score',
|
|
59
|
+
packages: 'opm.packages',
|
|
60
|
+
signature: 'opm.signature',
|
|
61
|
+
contract: 'opm.contract',
|
|
62
|
+
} as const;
|
|
@@ -53,26 +53,51 @@ export async function fetchModelRankings(): Promise<ModelRanking[]> {
|
|
|
53
53
|
|
|
54
54
|
export function getDefaultRankings(): ModelRanking[] {
|
|
55
55
|
return [
|
|
56
|
-
{ id: '1', name: '
|
|
57
|
-
{ id: '2', name: 'GPT-4
|
|
58
|
-
{ id: '3', name: '
|
|
59
|
-
{ id: '4', name: '
|
|
60
|
-
{ id: '5', name: '
|
|
61
|
-
{ id: '6', name: 'GPT-
|
|
56
|
+
{ id: '1', name: 'Gemini 3.1 Pro Preview', slug: 'gemini-3.1-pro-preview', intelligenceIndex: 57, codingIndex: 55 },
|
|
57
|
+
{ id: '2', name: 'GPT-5.4', slug: 'gpt-5.4', intelligenceIndex: 57, codingIndex: 55 },
|
|
58
|
+
{ id: '3', name: 'GPT-5.3 Codex', slug: 'gpt-5.3-codex', intelligenceIndex: 54, codingIndex: 52 },
|
|
59
|
+
{ id: '4', name: 'Claude Opus 4.6', slug: 'claude-opus-4-6', intelligenceIndex: 53, codingIndex: 51 },
|
|
60
|
+
{ id: '5', name: 'Claude Sonnet 4.6', slug: 'claude-sonnet-4-6', intelligenceIndex: 52, codingIndex: 50 },
|
|
61
|
+
{ id: '6', name: 'GPT-5.2', slug: 'gpt-5.2', intelligenceIndex: 51, codingIndex: 49 },
|
|
62
|
+
{ id: '7', name: 'GLM-5', slug: 'glm-5', intelligenceIndex: 50, codingIndex: 48 },
|
|
63
|
+
{ id: '8', name: 'Grok 4.2 Beta 0309', slug: 'grok-4-2-beta-0309', intelligenceIndex: 48, codingIndex: 46 },
|
|
64
|
+
{ id: '9', name: 'Kimi K2.5', slug: 'kimi-k2-5', intelligenceIndex: 47, codingIndex: 45 },
|
|
65
|
+
{ id: '10', name: 'Gemini 3 Flash', slug: 'gemini-3-flash', intelligenceIndex: 46, codingIndex: 44 },
|
|
66
|
+
{ id: '11', name: 'Qwen 3.5', slug: 'qwen-3-5', intelligenceIndex: 45, codingIndex: 43 },
|
|
67
|
+
{ id: '12', name: 'MiniMax-M2.5', slug: 'minimax-m2-5', intelligenceIndex: 42, codingIndex: 40 },
|
|
68
|
+
{ id: '13', name: 'DeepSeek V3.2', slug: 'deepseek-v3-2', intelligenceIndex: 42, codingIndex: 40 },
|
|
69
|
+
{ id: '14', name: 'MiMo V2 Flash Feb 2026', slug: 'mimo-v2-flash-feb-2026', intelligenceIndex: 41, codingIndex: 39 },
|
|
70
|
+
{ id: '15', name: 'Grok 4.1 Fast', slug: 'grok-4-1-fast', intelligenceIndex: 39, codingIndex: 37 },
|
|
71
|
+
{ id: '16', name: 'Claude 4.5 Haiku', slug: 'claude-4-5-haiku', intelligenceIndex: 37, codingIndex: 35 },
|
|
72
|
+
{ id: '17', name: 'NVIDIA Nemotron 3 Super', slug: 'nvidia-nemotron-3-super', intelligenceIndex: 36, codingIndex: 34 },
|
|
73
|
+
{ id: '18', name: 'Nova 2.0 Pro Preview', slug: 'nova-2-0-pro-preview', intelligenceIndex: 36, codingIndex: 34 },
|
|
74
|
+
{ id: '19', name: 'Gemini 3.1 Flash Lite Preview', slug: 'gemini-3-1-flash-lite-preview', intelligenceIndex: 34, codingIndex: 32 },
|
|
75
|
+
{ id: '20', name: 'gpt-oss-120B', slug: 'gpt-oss-120b', intelligenceIndex: 33, codingIndex: 31 },
|
|
76
|
+
{ id: '21', name: 'K-EXAONE', slug: 'k-exaone', intelligenceIndex: 32, codingIndex: 30 },
|
|
77
|
+
{ id: '22', name: 'gpt-oss-20B', slug: 'gpt-oss-20b', intelligenceIndex: 24, codingIndex: 22 },
|
|
78
|
+
{ id: '23', name: 'NVIDIA Nemotron 3 Nano', slug: 'nvidia-nemotron-3-nano', intelligenceIndex: 24, codingIndex: 22 },
|
|
79
|
+
{ id: '24', name: 'K2 Think V2', slug: 'k2-think-v2', intelligenceIndex: 24, codingIndex: 22 },
|
|
80
|
+
{ id: '25', name: 'Mi:dm K 2.5 Pro', slug: 'midm-k-2-5-pro', intelligenceIndex: 23, codingIndex: 21 },
|
|
81
|
+
{ id: '26', name: 'Mistral Large 3', slug: 'mistral-large-3', intelligenceIndex: 23, codingIndex: 21 },
|
|
82
|
+
{ id: '27', name: 'Llama 4 Maverick', slug: 'llama-4-maverick', intelligenceIndex: 18, codingIndex: 16 },
|
|
62
83
|
];
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
const MODEL_SLUGS: Record<string, string> = {
|
|
66
|
-
'anthropic/claude-
|
|
67
|
-
'anthropic/claude-sonnet-4': 'claude-sonnet-4',
|
|
87
|
+
'anthropic/claude-opus-4.6': 'claude-opus-4-6',
|
|
88
|
+
'anthropic/claude-sonnet-4.6': 'claude-sonnet-4-6',
|
|
89
|
+
'anthropic/claude-opus-4': 'claude-opus-4-6',
|
|
90
|
+
'google/gemini-3.1-pro-preview': 'gemini-3.1-pro-preview',
|
|
91
|
+
'google/gemini-3-flash': 'gemini-3-flash',
|
|
68
92
|
'google/gemini-2.5-flash': 'gemini-2.5-flash',
|
|
69
|
-
'deepseek/deepseek-
|
|
70
|
-
'
|
|
71
|
-
'gpt-4
|
|
72
|
-
'
|
|
73
|
-
'gpt-
|
|
74
|
-
'
|
|
75
|
-
'gpt-
|
|
93
|
+
'deepseek/deepseek-v3.2': 'deepseek-v3-2',
|
|
94
|
+
'deepseek/deepseek-chat': 'deepseek-v3-2',
|
|
95
|
+
'openai/gpt-5.4': 'gpt-5.4',
|
|
96
|
+
'gpt-5.4': 'gpt-5.4',
|
|
97
|
+
'openai/gpt-5.3-codex': 'gpt-5.3-codex',
|
|
98
|
+
'gpt-5.3-codex': 'gpt-5.3-codex',
|
|
99
|
+
'openai/gpt-5.2': 'gpt-5.2',
|
|
100
|
+
'gpt-5.2': 'gpt-5.2',
|
|
76
101
|
};
|
|
77
102
|
|
|
78
103
|
function findModel(rankings: ModelRanking[], modelSlug: string): ModelRanking | undefined {
|
|
@@ -143,3 +143,13 @@ export interface CheckReport {
|
|
|
143
143
|
deps: CheckDepResult[];
|
|
144
144
|
agents: CheckAgentResult[];
|
|
145
145
|
}
|
|
146
|
+
|
|
147
|
+
export interface OPMENSRecords {
|
|
148
|
+
version?: string;
|
|
149
|
+
checksum?: string;
|
|
150
|
+
fileverse?: string;
|
|
151
|
+
riskScore?: string;
|
|
152
|
+
packages?: string;
|
|
153
|
+
signature?: string;
|
|
154
|
+
contract?: string;
|
|
155
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HIGH_RISK_THRESHOLD, MEDIUM_RISK_THRESHOLD } from './constants';
|
|
2
|
-
import type { RiskLevel, AgentScanResult } from './types';
|
|
2
|
+
import type { RiskLevel, AgentScanResult, SupplyChainIndicators, VersionAnalysis } from './types';
|
|
3
3
|
|
|
4
4
|
export function classifyRisk(score: number): RiskLevel {
|
|
5
5
|
if (score >= HIGH_RISK_THRESHOLD) return 'HIGH';
|
|
@@ -45,6 +45,141 @@ export function validateScanResult(obj: unknown): obj is AgentScanResult {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
const VALID_RISK_LEVELS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;
|
|
49
|
+
const VALID_RECOMMENDATIONS = ['SAFE', 'CAUTION', 'WARN', 'BLOCK'] as const;
|
|
50
|
+
const SCORE_KEYS = ['risk_score', 'score', 'riskScore', 'risk_rating'];
|
|
51
|
+
const LEVEL_KEYS = ['risk_level', 'riskLevel', 'level', 'severity', 'verdict', 'rating'];
|
|
52
|
+
const TEXT_KEYS = ['reasoning', 'summary', 'explanation', 'description', 'analysis', 'one_line_summary', 'one_liner'];
|
|
53
|
+
|
|
54
|
+
function deepFind(obj: Record<string, any>, keys: string[], type: 'number' | 'string', depth = 0): any {
|
|
55
|
+
if (depth > 4 || !obj || typeof obj !== 'object') return undefined;
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const val = obj[key];
|
|
58
|
+
if (val !== undefined && val !== null) {
|
|
59
|
+
if (type === 'number') {
|
|
60
|
+
if (typeof val === 'number') return val;
|
|
61
|
+
if (typeof val === 'string' && !isNaN(parseFloat(val))) return parseFloat(val);
|
|
62
|
+
} else if (type === 'string' && typeof val === 'string' && val.length > 0) {
|
|
63
|
+
return val;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const key of Object.keys(obj)) {
|
|
68
|
+
const val = obj[key];
|
|
69
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
70
|
+
const found = deepFind(val, keys, type, depth + 1);
|
|
71
|
+
if (found !== undefined) return found;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deepFindArray(obj: Record<string, any>, keys: string[], depth = 0): any[] | undefined {
|
|
78
|
+
if (depth > 4 || !obj || typeof obj !== 'object') return undefined;
|
|
79
|
+
for (const key of keys) {
|
|
80
|
+
if (Array.isArray(obj[key])) return obj[key];
|
|
81
|
+
}
|
|
82
|
+
for (const key of Object.keys(obj)) {
|
|
83
|
+
const val = obj[key];
|
|
84
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
85
|
+
const found = deepFindArray(val, keys, depth + 1);
|
|
86
|
+
if (found) return found;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function deepFindObj(obj: Record<string, any>, keys: string[], depth = 0): Record<string, any> | undefined {
|
|
93
|
+
if (depth > 3 || !obj || typeof obj !== 'object') return undefined;
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
const val = obj[key];
|
|
96
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) return val;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeRiskLevel(val: unknown): RiskLevel {
|
|
102
|
+
if (typeof val !== 'string') return 'MEDIUM';
|
|
103
|
+
const upper = val.toUpperCase().trim();
|
|
104
|
+
if (VALID_RISK_LEVELS.includes(upper as RiskLevel)) return upper as RiskLevel;
|
|
105
|
+
if (upper === 'SAFE' || upper === 'NONE' || upper === 'INFO') return 'LOW';
|
|
106
|
+
if (upper === 'MODERATE' || upper === 'SUSPICIOUS') return 'MEDIUM';
|
|
107
|
+
if (upper === 'DANGEROUS' || upper === 'SEVERE') return 'CRITICAL';
|
|
108
|
+
return 'MEDIUM';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeRecommendation(val: unknown, riskLevel: RiskLevel): string {
|
|
112
|
+
if (typeof val === 'string') {
|
|
113
|
+
const upper = val.toUpperCase().trim();
|
|
114
|
+
if (VALID_RECOMMENDATIONS.includes(upper as any)) return upper;
|
|
115
|
+
}
|
|
116
|
+
const map: Record<RiskLevel, string> = { LOW: 'SAFE', MEDIUM: 'CAUTION', HIGH: 'WARN', CRITICAL: 'BLOCK' };
|
|
117
|
+
return map[riskLevel] || 'CAUTION';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Recursively searches an arbitrarily-shaped LLM response for risk_score,
|
|
122
|
+
* risk_level, reasoning, etc. and assembles a valid AgentScanResult.
|
|
123
|
+
*/
|
|
124
|
+
export function normalizeScanResult(raw: unknown): AgentScanResult | null {
|
|
125
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
126
|
+
const o = raw as Record<string, any>;
|
|
127
|
+
|
|
128
|
+
const riskScore = deepFind(o, SCORE_KEYS, 'number');
|
|
129
|
+
if (riskScore === undefined || isNaN(riskScore)) return null;
|
|
130
|
+
|
|
131
|
+
const rawLevel = deepFind(o, LEVEL_KEYS, 'string');
|
|
132
|
+
const riskLevel = normalizeRiskLevel(rawLevel);
|
|
133
|
+
|
|
134
|
+
const reasoning = deepFind(o, TEXT_KEYS, 'string') ?? `Risk score: ${riskScore}`;
|
|
135
|
+
|
|
136
|
+
const rawVulns = deepFindArray(o, ['vulnerabilities', 'findings', 'issues', 'alerts', 'concerns']);
|
|
137
|
+
const vulnerabilities = rawVulns
|
|
138
|
+
? rawVulns.map((f: any) => ({
|
|
139
|
+
severity: normalizeRiskLevel(f.severity ?? f.level ?? f.risk_level),
|
|
140
|
+
category: f.category || f.type || f.issue || 'unknown',
|
|
141
|
+
description: f.description || f.message || f.detail || f.title || '',
|
|
142
|
+
file: f.file || f.location || f.path || '',
|
|
143
|
+
evidence: f.evidence || f.code || f.snippet || '',
|
|
144
|
+
}))
|
|
145
|
+
: [];
|
|
146
|
+
|
|
147
|
+
const sci = deepFindObj(o, ['supply_chain_indicators', 'supplyChainIndicators', 'indicators']);
|
|
148
|
+
const supply_chain_indicators: SupplyChainIndicators = (sci as SupplyChainIndicators) ?? {
|
|
149
|
+
has_install_scripts: false,
|
|
150
|
+
has_native_bindings: false,
|
|
151
|
+
has_obfuscated_code: false,
|
|
152
|
+
has_network_calls: false,
|
|
153
|
+
has_filesystem_access: false,
|
|
154
|
+
has_process_spawn: false,
|
|
155
|
+
has_eval_usage: false,
|
|
156
|
+
accesses_env_variables: false,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const va = deepFindObj(o, ['version_analysis', 'versionAnalysis']);
|
|
160
|
+
const version_analysis: VersionAnalysis = (va as VersionAnalysis) ?? {
|
|
161
|
+
version_reviewed: deepFind(o, ['version', 'version_reviewed'], 'string') ?? '',
|
|
162
|
+
previous_versions_reviewed: [],
|
|
163
|
+
changelog_risk: 'NONE',
|
|
164
|
+
changelog_reasoning: '',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const recommendation = normalizeRecommendation(
|
|
168
|
+
deepFind(o, ['recommendation', 'action', 'verdict'], 'string'),
|
|
169
|
+
riskLevel,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
risk_score: Math.max(0, Math.min(100, Math.round(riskScore))),
|
|
174
|
+
risk_level: riskLevel,
|
|
175
|
+
reasoning,
|
|
176
|
+
vulnerabilities,
|
|
177
|
+
supply_chain_indicators,
|
|
178
|
+
version_analysis,
|
|
179
|
+
recommendation: recommendation as any,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
48
183
|
export function safeJsonParse<T>(raw: string): T | null {
|
|
49
184
|
try {
|
|
50
185
|
return JSON.parse(raw) as T;
|
|
@@ -8,8 +8,9 @@ export { callLLM, callLLMRaw, getLLMProvider } from './services/openrouter';
|
|
|
8
8
|
export { fetchPackageData, extractMetadata, buildVersionHistory, fetchSourceFiles, extractLocalSourceFiles, buildLocalPackageData } from './services/npm-registry';
|
|
9
9
|
export { submitScoreOnChain, setReportURIOnChain } from './services/contract-writer';
|
|
10
10
|
export { uploadReportToFileverse, uploadCheckReportToFileverse, fetchReportFromFileverse } from './services/fileverse';
|
|
11
|
+
export type { FileverseUploadResult } from './services/fileverse';
|
|
11
12
|
export { formatCheckReportAsMarkdown } from './services/report-formatter';
|
|
12
|
-
export { runBenchmarkSuite, type AgentCandidate, type BenchmarkRunResult } from './services/benchmark-runner';
|
|
13
|
+
export { runBenchmarkSuite, runBatchBenchmarkSuite, type AgentCandidate, type BenchmarkRunResult, type BatchBenchmarkRunResult } from './services/benchmark-runner';
|
|
13
14
|
export { generateProof, verifyProof, generateCommitment, proofToOnChainBytes, type ZKProof } from './services/zk-verifier';
|
|
14
15
|
|
|
15
16
|
if (import.meta.main) {
|
|
@@ -8,6 +8,7 @@ import { uploadReportToFileverse } from '../services/fileverse';
|
|
|
8
8
|
export interface ScanJobResult {
|
|
9
9
|
report: ScanReport;
|
|
10
10
|
reportURI: string;
|
|
11
|
+
ipfsHash?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const activeJobs = new Map<string, Promise<ScanJobResult>>();
|
|
@@ -77,9 +78,13 @@ async function executeScan(
|
|
|
77
78
|
|
|
78
79
|
log('Uploading report to Fileverse...');
|
|
79
80
|
let reportURI: string;
|
|
81
|
+
let ipfsHash: string | undefined;
|
|
80
82
|
try {
|
|
81
|
-
|
|
83
|
+
const uploadResult = await uploadReportToFileverse(report);
|
|
84
|
+
reportURI = uploadResult.link;
|
|
85
|
+
ipfsHash = uploadResult.ipfsHash;
|
|
82
86
|
log(`Report uploaded: ${reportURI}`);
|
|
87
|
+
if (ipfsHash) log(`IPFS content hash: ${ipfsHash}`);
|
|
83
88
|
} catch (err) {
|
|
84
89
|
log(`Fileverse upload failed: ${err}`);
|
|
85
90
|
reportURI = `local://report-${packageName}-${version}`;
|
|
@@ -92,5 +97,5 @@ async function executeScan(
|
|
|
92
97
|
log(`On-chain report URI: ${err?.shortMessage || err?.message || 'failed'}`);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
return { report, reportURI };
|
|
100
|
+
return { report, reportURI, ipfsHash };
|
|
96
101
|
}
|
|
@@ -3,10 +3,16 @@ import {
|
|
|
3
3
|
buildBenchmarkPrompt,
|
|
4
4
|
evaluateBenchmark,
|
|
5
5
|
SYSTEM_PROMPT,
|
|
6
|
+
buildBatchSystemPrompt,
|
|
7
|
+
buildBatchBenchmarkPrompt,
|
|
8
|
+
evaluateBatchBenchmark,
|
|
9
|
+
expectedFlagForCase,
|
|
6
10
|
type BenchmarkCase,
|
|
7
11
|
type BenchmarkResult,
|
|
12
|
+
type BatchBenchmarkResponse,
|
|
13
|
+
type BatchBenchmarkResult,
|
|
8
14
|
} from '@opm/core';
|
|
9
|
-
import { callLLM } from './openrouter';
|
|
15
|
+
import { callLLM, callLLMRaw } from './openrouter';
|
|
10
16
|
import {
|
|
11
17
|
generateCommitment,
|
|
12
18
|
generateProof,
|
|
@@ -32,6 +38,85 @@ export interface BenchmarkRunResult {
|
|
|
32
38
|
failureReasons: string[];
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
/** Result type for the fast single-call batch benchmark */
|
|
42
|
+
export interface BatchBenchmarkRunResult {
|
|
43
|
+
candidate: AgentCandidate;
|
|
44
|
+
results: BatchBenchmarkResult[];
|
|
45
|
+
passed: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
total: number;
|
|
48
|
+
accuracyPct: number;
|
|
49
|
+
zkProof: ZKProof;
|
|
50
|
+
verified: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fast single-call benchmark: sends all 10 cases in one prompt,
|
|
55
|
+
* gets back 10 flagged/safe answers, compares to ground truth,
|
|
56
|
+
* generates ZK proof. ~10x faster than runBenchmarkSuite.
|
|
57
|
+
*/
|
|
58
|
+
export async function runBatchBenchmarkSuite(
|
|
59
|
+
candidate: AgentCandidate,
|
|
60
|
+
onStatus?: (msg: string) => void,
|
|
61
|
+
): Promise<BatchBenchmarkRunResult> {
|
|
62
|
+
const log = onStatus || console.log;
|
|
63
|
+
const benchmarks = generateBenchmarkDataset();
|
|
64
|
+
|
|
65
|
+
// Expected verdicts: 1 = flagged, 0 = safe
|
|
66
|
+
const expectedVerdicts = benchmarks.map((b) => expectedFlagForCase(b) ? 1 : 0);
|
|
67
|
+
|
|
68
|
+
log(`Generating commitment for ${benchmarks.length} test cases...`);
|
|
69
|
+
const commitment = generateCommitment(expectedVerdicts);
|
|
70
|
+
log(`Commitment: ${commitment.expectedHash.slice(0, 16)}…`);
|
|
71
|
+
|
|
72
|
+
log(`Sending ${benchmarks.length} cases to ${candidate.model} (single call)...`);
|
|
73
|
+
const userPrompt = buildBatchBenchmarkPrompt(benchmarks);
|
|
74
|
+
// Use the candidate's own system prompt so a bad prompt actually fails the benchmark
|
|
75
|
+
const systemPrompt = buildBatchSystemPrompt(candidate.systemPrompt);
|
|
76
|
+
|
|
77
|
+
const response = await callLLMRaw<BatchBenchmarkResponse>(
|
|
78
|
+
candidate.model,
|
|
79
|
+
systemPrompt,
|
|
80
|
+
userPrompt,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!response?.answers || !Array.isArray(response.answers)) {
|
|
84
|
+
throw new Error(`Agent returned invalid batch response — expected { answers: [...] }`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
log(`Received ${response.answers.length} answers, evaluating...`);
|
|
88
|
+
|
|
89
|
+
const results = evaluateBatchBenchmark(benchmarks, response.answers);
|
|
90
|
+
const actualVerdicts = results.map((r) => r.actualFlagged ? 1 : 0);
|
|
91
|
+
|
|
92
|
+
const passed = results.filter((r) => r.passed).length;
|
|
93
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
94
|
+
const accuracyPct = Math.round((passed / results.length) * 100);
|
|
95
|
+
|
|
96
|
+
results.forEach((r, i) => {
|
|
97
|
+
const icon = r.passed ? '✓' : '✗';
|
|
98
|
+
log(` ${icon} Case ${i + 1} (${r.category}): ${r.actualFlagged ? 'FLAGGED' : 'SAFE'} — expected ${r.expectedFlagged ? 'FLAGGED' : 'SAFE'}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
log(`Accuracy: ${passed}/${results.length} (${accuracyPct}%)`);
|
|
102
|
+
log('Generating ZK proof...');
|
|
103
|
+
|
|
104
|
+
const zkProof = generateProof(commitment, expectedVerdicts, actualVerdicts);
|
|
105
|
+
const verified = verifyProof(zkProof);
|
|
106
|
+
log(`ZK proof ${verified ? 'verified ✓' : 'INVALID ✗'}`);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
candidate,
|
|
110
|
+
results,
|
|
111
|
+
passed,
|
|
112
|
+
failed,
|
|
113
|
+
total: results.length,
|
|
114
|
+
accuracyPct,
|
|
115
|
+
zkProof,
|
|
116
|
+
verified,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
35
120
|
export async function runBenchmarkSuite(
|
|
36
121
|
candidate: AgentCandidate,
|
|
37
122
|
onStatus?: (msg: string) => void,
|
|
@@ -11,7 +11,49 @@ function getApiConfig() {
|
|
|
11
11
|
return { apiUrl, apiKey };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export
|
|
14
|
+
export interface FileverseUploadResult {
|
|
15
|
+
link: string;
|
|
16
|
+
ipfsHash?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DdocResponse {
|
|
20
|
+
ddocId: string;
|
|
21
|
+
syncStatus: string;
|
|
22
|
+
link?: string;
|
|
23
|
+
contentHash?: string;
|
|
24
|
+
ipfsHash?: string;
|
|
25
|
+
cid?: string;
|
|
26
|
+
ipfsCid?: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractIPFSHash(data: DdocResponse, log?: (msg: string) => void): string | undefined {
|
|
31
|
+
const knownFields = ['contentHash', 'ipfsHash', 'cid', 'ipfsCid', 'hash', 'ipfs', 'contentId'];
|
|
32
|
+
for (const field of knownFields) {
|
|
33
|
+
const val = data[field];
|
|
34
|
+
if (typeof val === 'string' && val.length > 0) return val;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [key, val] of Object.entries(data)) {
|
|
38
|
+
if (typeof val === 'string' && (val.startsWith('Qm') || val.startsWith('bafy'))) {
|
|
39
|
+
return val;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (data.link) {
|
|
44
|
+
const ipfsMatch = data.link.match(/\/ipfs\/(Qm[A-Za-z0-9]{44,}|bafy[a-z2-7]{50,})/);
|
|
45
|
+
if (ipfsMatch) return ipfsMatch[1];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (log) {
|
|
49
|
+
const keys = Object.keys(data).filter((k) => !['ddocId', 'syncStatus', 'link'].includes(k));
|
|
50
|
+
if (keys.length > 0) log(`Fileverse extra fields: ${keys.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function uploadReportToFileverse(report: ScanReport): Promise<FileverseUploadResult> {
|
|
15
57
|
const { apiUrl, apiKey } = getApiConfig();
|
|
16
58
|
|
|
17
59
|
const title = `OPM Security Report: ${report.package}@${report.version}`;
|
|
@@ -28,16 +70,19 @@ export async function uploadReportToFileverse(report: ScanReport): Promise<strin
|
|
|
28
70
|
throw new Error(`Fileverse create failed (${res.status}): ${body}`);
|
|
29
71
|
}
|
|
30
72
|
|
|
31
|
-
const { data } = await res.json() as { data:
|
|
73
|
+
const { data } = await res.json() as { data: DdocResponse };
|
|
32
74
|
const ddocId = data.ddocId;
|
|
75
|
+
let ipfsHash = extractIPFSHash(data);
|
|
33
76
|
|
|
34
|
-
if (data.syncStatus === 'synced' && data.link)
|
|
77
|
+
if (data.syncStatus === 'synced' && data.link) {
|
|
78
|
+
return { link: data.link, ipfsHash };
|
|
79
|
+
}
|
|
35
80
|
|
|
36
|
-
const
|
|
37
|
-
return link;
|
|
81
|
+
const syncResult = await pollForSync(apiUrl, apiKey, ddocId);
|
|
82
|
+
return { link: syncResult.link, ipfsHash: ipfsHash || syncResult.ipfsHash };
|
|
38
83
|
}
|
|
39
84
|
|
|
40
|
-
async function pollForSync(apiUrl: string, apiKey: string, ddocId: string): Promise<
|
|
85
|
+
async function pollForSync(apiUrl: string, apiKey: string, ddocId: string): Promise<FileverseUploadResult> {
|
|
41
86
|
const start = Date.now();
|
|
42
87
|
|
|
43
88
|
while (Date.now() - start < POLL_TIMEOUT_MS) {
|
|
@@ -46,15 +91,17 @@ async function pollForSync(apiUrl: string, apiKey: string, ddocId: string): Prom
|
|
|
46
91
|
const res = await fetch(`${apiUrl}/api/ddocs/${ddocId}?apiKey=${encodeURIComponent(apiKey)}`);
|
|
47
92
|
if (!res.ok) continue;
|
|
48
93
|
|
|
49
|
-
const doc = await res.json() as
|
|
50
|
-
if (doc.syncStatus === 'synced' && doc.link)
|
|
94
|
+
const doc = await res.json() as DdocResponse;
|
|
95
|
+
if (doc.syncStatus === 'synced' && doc.link) {
|
|
96
|
+
return { link: doc.link, ipfsHash: extractIPFSHash(doc) };
|
|
97
|
+
}
|
|
51
98
|
if (doc.syncStatus === 'failed') throw new Error('Fileverse blockchain sync failed');
|
|
52
99
|
}
|
|
53
100
|
|
|
54
|
-
return `https://ddocs.new/pending/${ddocId}
|
|
101
|
+
return { link: `https://ddocs.new/pending/${ddocId}` };
|
|
55
102
|
}
|
|
56
103
|
|
|
57
|
-
export async function uploadCheckReportToFileverse(report: CheckReport): Promise<
|
|
104
|
+
export async function uploadCheckReportToFileverse(report: CheckReport): Promise<FileverseUploadResult> {
|
|
58
105
|
const { apiUrl, apiKey } = getApiConfig();
|
|
59
106
|
const title = `OPM Check Report: ${report.project} (${report.totalDeps} deps)`;
|
|
60
107
|
const content = formatCheckReportAsMarkdown(report);
|
|
@@ -70,8 +117,10 @@ export async function uploadCheckReportToFileverse(report: CheckReport): Promise
|
|
|
70
117
|
throw new Error(`Fileverse create failed (${res.status}): ${body}`);
|
|
71
118
|
}
|
|
72
119
|
|
|
73
|
-
const { data } = await res.json() as { data:
|
|
74
|
-
if (data.syncStatus === 'synced' && data.link)
|
|
120
|
+
const { data } = await res.json() as { data: DdocResponse };
|
|
121
|
+
if (data.syncStatus === 'synced' && data.link) {
|
|
122
|
+
return { link: data.link, ipfsHash: extractIPFSHash(data) };
|
|
123
|
+
}
|
|
75
124
|
return pollForSync(apiUrl, apiKey, data.ddocId);
|
|
76
125
|
}
|
|
77
126
|
|