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,159 @@
1
+ import { NPM_REGISTRY_URL, SCANNABLE_EXTENSIONS, MAX_FILE_SIZE_BYTES, MAX_TOTAL_CODE_CHARS, VERSION_LOOKBACK } from '@opm/core';
2
+ import type { PackageMetadata, VersionHistoryEntry, SourceFile } from '@opm/core';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { execSync } from 'child_process';
6
+ import { randomUUID } from 'crypto';
7
+ import * as os from 'os';
8
+
9
+ export interface NpmPackageData {
10
+ name: string;
11
+ versions: Record<string, {
12
+ version: string;
13
+ description?: string;
14
+ author?: string | { name?: string };
15
+ license?: string;
16
+ dependencies?: Record<string, string>;
17
+ scripts?: Record<string, string>;
18
+ dist: { tarball: string; fileCount?: number; unpackedSize?: number };
19
+ _npmUser?: { name: string };
20
+ }>;
21
+ time: Record<string, string>;
22
+ 'dist-tags': Record<string, string>;
23
+ }
24
+
25
+ export async function fetchPackageData(packageName: string): Promise<NpmPackageData> {
26
+ const res = await fetch(`${NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}`);
27
+ if (!res.ok) throw new Error(`npm registry ${res.status} for ${packageName}`);
28
+ return res.json() as Promise<NpmPackageData>;
29
+ }
30
+
31
+ export function buildLocalPackageData(pkgJsonPath: string): NpmPackageData {
32
+ const raw = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
33
+ const version = raw.version || '0.0.0';
34
+ return {
35
+ name: raw.name || 'unknown',
36
+ 'dist-tags': { latest: version },
37
+ time: { [version]: new Date().toISOString() },
38
+ versions: {
39
+ [version]: {
40
+ version,
41
+ description: raw.description || '',
42
+ author: raw.author || '',
43
+ license: raw.license || '',
44
+ dependencies: raw.dependencies || {},
45
+ scripts: raw.scripts || {},
46
+ dist: { tarball: '', fileCount: 0, unpackedSize: 0 },
47
+ },
48
+ },
49
+ };
50
+ }
51
+
52
+ export function extractMetadata(data: NpmPackageData, version: string): PackageMetadata {
53
+ const v = data.versions[version];
54
+ if (!v) throw new Error(`Version ${version} not found for ${data.name}`);
55
+ const authorStr = typeof v.author === 'string' ? v.author : v.author?.name || '';
56
+ return {
57
+ name: data.name,
58
+ version: v.version,
59
+ description: v.description || '',
60
+ author: authorStr,
61
+ license: v.license || '',
62
+ dependencies: v.dependencies || {},
63
+ scripts: v.scripts || {},
64
+ };
65
+ }
66
+
67
+ export function buildVersionHistory(data: NpmPackageData, currentVersion: string): VersionHistoryEntry[] {
68
+ const allVersions = Object.keys(data.versions);
69
+ const currentIdx = allVersions.indexOf(currentVersion);
70
+ if (currentIdx === -1) return [];
71
+
72
+ const start = Math.max(0, currentIdx - VERSION_LOOKBACK);
73
+ const slice = allVersions.slice(start, currentIdx + 1);
74
+
75
+ return slice.map((ver, i) => {
76
+ const v = data.versions[ver];
77
+ const prev = i > 0 ? data.versions[slice[i - 1]] : null;
78
+ const prevDeps = prev?.dependencies || {};
79
+ const curDeps = v.dependencies || {};
80
+ const depsChanged = Object.keys(curDeps)
81
+ .filter((d) => prevDeps[d] !== curDeps[d])
82
+ .join(', ') || 'none';
83
+
84
+ const prevSize = prev?.dist?.unpackedSize || 0;
85
+ const curSize = v.dist?.unpackedSize || 0;
86
+ const sizeDelta = prev ? `${curSize - prevSize} bytes` : 'N/A';
87
+
88
+ const prevMaintainer = prev?._npmUser?.name || '';
89
+ const curMaintainer = v._npmUser?.name || '';
90
+
91
+ return {
92
+ version: ver,
93
+ published: data.time[ver] || 'unknown',
94
+ depsChanged,
95
+ filesChanged: prev ? `~${Math.abs((v.dist?.fileCount || 0) - (prev.dist?.fileCount || 0))} files` : 'N/A',
96
+ sizeDelta,
97
+ newMaintainer: prev ? curMaintainer !== prevMaintainer : false,
98
+ };
99
+ });
100
+ }
101
+
102
+ function extractFilesFromTarball(tarballPath: string): SourceFile[] {
103
+ const tmpDir = path.join(os.tmpdir(), `opm-extract-${randomUUID()}`);
104
+ fs.mkdirSync(tmpDir, { recursive: true });
105
+
106
+ try {
107
+ execSync(`tar -xzf "${tarballPath}" -C "${tmpDir}"`, { stdio: 'pipe' });
108
+ return walkAndCollect(tmpDir, tmpDir);
109
+ } finally {
110
+ fs.rmSync(tmpDir, { recursive: true, force: true });
111
+ }
112
+ }
113
+
114
+ function walkAndCollect(dir: string, root: string): SourceFile[] {
115
+ const files: SourceFile[] = [];
116
+ let totalChars = 0;
117
+
118
+ function walk(current: string) {
119
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
120
+ if (totalChars >= MAX_TOTAL_CODE_CHARS) return;
121
+ const fullPath = path.join(current, entry.name);
122
+
123
+ if (entry.isDirectory()) {
124
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
125
+ walk(fullPath);
126
+ } else if (SCANNABLE_EXTENSIONS.includes(path.extname(entry.name))) {
127
+ const stat = fs.statSync(fullPath);
128
+ if (stat.size > MAX_FILE_SIZE_BYTES) continue;
129
+
130
+ const content = fs.readFileSync(fullPath, 'utf-8');
131
+ if (totalChars + content.length > MAX_TOTAL_CODE_CHARS) continue;
132
+ totalChars += content.length;
133
+
134
+ const relPath = path.relative(root, fullPath).replace(/^package\//, '');
135
+ files.push({ path: relPath, size: content.length, content });
136
+ }
137
+ }
138
+ }
139
+
140
+ walk(dir);
141
+ return files;
142
+ }
143
+
144
+ export async function fetchSourceFiles(_packageName: string, _version: string, tarballUrl: string): Promise<SourceFile[]> {
145
+ const res = await fetch(tarballUrl);
146
+ if (!res.ok) throw new Error(`Failed to fetch tarball: ${res.status}`);
147
+
148
+ const tmpFile = path.join(os.tmpdir(), `opm-dl-${randomUUID()}.tgz`);
149
+ try {
150
+ fs.writeFileSync(tmpFile, Buffer.from(await res.arrayBuffer()));
151
+ return extractFilesFromTarball(tmpFile);
152
+ } finally {
153
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
154
+ }
155
+ }
156
+
157
+ export async function extractLocalSourceFiles(tarballPath: string): Promise<SourceFile[]> {
158
+ return extractFilesFromTarball(tarballPath);
159
+ }
@@ -0,0 +1,86 @@
1
+ import { OPENROUTER_API_URL, OPENAI_API_URL } from '@opm/core';
2
+ import type { AgentScanResult } from '@opm/core';
3
+ import { validateScanResult, safeJsonParse } from '@opm/core';
4
+
5
+ function getProvider(): { apiUrl: string; apiKey: string; kind: 'openai' | 'openrouter' } {
6
+ const forcedProvider = process.env.LLM_PROVIDER;
7
+
8
+ if (forcedProvider === 'openrouter') {
9
+ const orKey = process.env.OPENROUTER_API_KEY;
10
+ if (!orKey) throw new Error('OPENROUTER_API_KEY required when LLM_PROVIDER=openrouter');
11
+ return { apiUrl: OPENROUTER_API_URL, apiKey: orKey, kind: 'openrouter' };
12
+ }
13
+
14
+ if (forcedProvider === 'openai') {
15
+ const openaiKey = process.env.OPENAI_API_KEY;
16
+ if (!openaiKey) throw new Error('OPENAI_API_KEY required when LLM_PROVIDER=openai');
17
+ return { apiUrl: OPENAI_API_URL, apiKey: openaiKey, kind: 'openai' };
18
+ }
19
+
20
+ const orKey = process.env.OPENROUTER_API_KEY;
21
+ if (orKey) return { apiUrl: OPENROUTER_API_URL, apiKey: orKey, kind: 'openrouter' };
22
+
23
+ const openaiKey = process.env.OPENAI_API_KEY;
24
+ if (openaiKey) return { apiUrl: OPENAI_API_URL, apiKey: openaiKey, kind: 'openai' };
25
+
26
+ throw new Error('Set OPENROUTER_API_KEY (for diverse models) or OPENAI_API_KEY');
27
+ }
28
+
29
+ export function getLLMProvider(): 'openai' | 'openrouter' {
30
+ const forcedProvider = process.env.LLM_PROVIDER;
31
+ if (forcedProvider === 'openrouter') return 'openrouter';
32
+ if (forcedProvider === 'openai') return 'openai';
33
+
34
+ if (process.env.OPENROUTER_API_KEY) return 'openrouter';
35
+ if (process.env.OPENAI_API_KEY) return 'openai';
36
+ throw new Error('Set OPENROUTER_API_KEY (for diverse models) or OPENAI_API_KEY');
37
+ }
38
+
39
+ export async function callLLM(
40
+ model: string,
41
+ systemPrompt: string,
42
+ userPrompt: string,
43
+ ): Promise<AgentScanResult> {
44
+ const { apiUrl, apiKey, kind } = getProvider();
45
+
46
+ const headers: Record<string, string> = {
47
+ 'Authorization': `Bearer ${apiKey}`,
48
+ 'Content-Type': 'application/json',
49
+ };
50
+
51
+ if (kind === 'openrouter') {
52
+ headers['HTTP-Referer'] = 'https://opm.dev';
53
+ headers['X-Title'] = 'OPM Security Scanner';
54
+ }
55
+
56
+ const res = await fetch(apiUrl, {
57
+ method: 'POST',
58
+ headers,
59
+ body: JSON.stringify({
60
+ model,
61
+ messages: [
62
+ { role: 'system', content: systemPrompt },
63
+ { role: 'user', content: userPrompt },
64
+ ],
65
+ response_format: { type: 'json_object' },
66
+ temperature: 0.1,
67
+ max_tokens: 4096,
68
+ }),
69
+ });
70
+
71
+ if (!res.ok) {
72
+ const body = await res.text();
73
+ throw new Error(`${kind} ${res.status}: ${body}`);
74
+ }
75
+
76
+ const data = await res.json() as { choices: Array<{ message: { content: string } }> };
77
+ const raw = data.choices?.[0]?.message?.content;
78
+ if (!raw) throw new Error(`Empty response from ${kind}/${model}`);
79
+
80
+ const parsed = safeJsonParse<AgentScanResult>(raw);
81
+ if (!parsed || !validateScanResult(parsed)) {
82
+ throw new Error(`Invalid scan result JSON from ${model}: ${raw.slice(0, 200)}`);
83
+ }
84
+
85
+ return parsed;
86
+ }
@@ -0,0 +1,87 @@
1
+ const OSV_API = 'https://api.osv.dev/v1/query';
2
+
3
+ export interface OSVAffectedRange {
4
+ type: string;
5
+ events: Array<{ introduced?: string; fixed?: string }>;
6
+ }
7
+
8
+ export interface OSVAffected {
9
+ ranges?: OSVAffectedRange[];
10
+ }
11
+
12
+ export interface OSVVulnerability {
13
+ id: string;
14
+ summary: string;
15
+ details: string;
16
+ severity: Array<{ type: string; score: string }>;
17
+ references: Array<{ type: string; url: string }>;
18
+ database_specific?: { severity?: string; [key: string]: unknown };
19
+ affected?: OSVAffected[];
20
+ }
21
+
22
+ function parseVersion(v: string): number[] {
23
+ return v.replace(/^v/, '').split('.').map((p) => {
24
+ const num = parseInt(p, 10);
25
+ return isNaN(num) ? 0 : num;
26
+ });
27
+ }
28
+
29
+ function compareVersions(v1: string, v2: string): number {
30
+ const p1 = parseVersion(v1);
31
+ const p2 = parseVersion(v2);
32
+ const len = Math.max(p1.length, p2.length);
33
+ for (let i = 0; i < len; i++) {
34
+ const n1 = p1[i] ?? 0;
35
+ const n2 = p2[i] ?? 0;
36
+ if (n1 !== n2) return n1 - n2;
37
+ }
38
+ return 0;
39
+ }
40
+
41
+ function isFixedInVersionOrBefore(vuln: OSVVulnerability, currentVersion: string): boolean {
42
+ const affected = vuln.affected;
43
+ if (!affected || affected.length === 0) return false;
44
+
45
+ for (const entry of affected) {
46
+ const ranges = entry.ranges;
47
+ if (!ranges || ranges.length === 0) continue;
48
+
49
+ for (const range of ranges) {
50
+ const events = range.events;
51
+ if (!events) continue;
52
+
53
+ for (const event of events) {
54
+ if (event.fixed) {
55
+ const fixedVersion = event.fixed;
56
+ if (compareVersions(currentVersion, fixedVersion) >= 0) {
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+
66
+ export async function queryOSV(packageName: string, version: string): Promise<OSVVulnerability[]> {
67
+ if (!version || version === 'latest') return [];
68
+
69
+ try {
70
+ const res = await fetch(OSV_API, {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({
74
+ package: { name: packageName, ecosystem: 'npm' },
75
+ version,
76
+ }),
77
+ });
78
+ if (!res.ok) return [];
79
+ const data = await res.json() as { vulns?: OSVVulnerability[] };
80
+ const vulns = data.vulns || [];
81
+
82
+ const unfixedVulns = vulns.filter((v) => !isFixedInVersionOrBefore(v, version));
83
+ return unfixedVulns;
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
@@ -0,0 +1,134 @@
1
+ import type { ScanReport, AgentEntry, Vulnerability, SupplyChainIndicators } from '@opm/core';
2
+
3
+ function riskEmoji(score: number): string {
4
+ if (score >= 70) return '🔴';
5
+ if (score >= 40) return '🟡';
6
+ return '🟢';
7
+ }
8
+
9
+ function riskBar(score: number): string {
10
+ const filled = Math.round(score / 5);
11
+ const empty = 20 - filled;
12
+ return `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
13
+ }
14
+
15
+ function formatIndicators(indicators: SupplyChainIndicators): string {
16
+ const checks: Array<[keyof SupplyChainIndicators, string]> = [
17
+ ['has_install_scripts', 'Install scripts'],
18
+ ['has_native_bindings', 'Native bindings'],
19
+ ['has_obfuscated_code', 'Obfuscated code'],
20
+ ['has_network_calls', 'Network calls'],
21
+ ['has_filesystem_access', 'Filesystem access'],
22
+ ['has_process_spawn', 'Process spawning'],
23
+ ['has_eval_usage', 'eval() usage'],
24
+ ['accesses_env_variables', 'Environment variable access'],
25
+ ];
26
+
27
+ return checks
28
+ .map(([key, label]) => `- ${indicators[key] ? '⚠️' : '✅'} ${label}`)
29
+ .join('\n');
30
+ }
31
+
32
+ function formatVulnerabilities(vulns: Vulnerability[]): string {
33
+ if (vulns.length === 0) return '*No vulnerabilities detected.*\n';
34
+
35
+ return vulns.map((v, i) => {
36
+ const sev = v.severity === 'HIGH' || v.severity === 'CRITICAL' ? '🔴' : v.severity === 'MEDIUM' ? '🟡' : '🟢';
37
+ return [
38
+ `**${sev} ${i + 1}. ${v.category}** (${v.severity})`,
39
+ '',
40
+ `> ${v.description}`,
41
+ '',
42
+ `- **File:** \`${v.file}\``,
43
+ v.evidence ? `- **Evidence:** \`${v.evidence}\`` : '',
44
+ ].filter(Boolean).join('\n');
45
+ }).join('\n\n');
46
+ }
47
+
48
+ function formatAgent(agent: AgentEntry, index: number): string {
49
+ const { result } = agent;
50
+ const emoji = riskEmoji(result.risk_score);
51
+
52
+ return [
53
+ `### Agent ${index + 1}: \`${agent.agent_id}\``,
54
+ '',
55
+ `- **Model:** \`${agent.model}\``,
56
+ `- **Risk Score:** ${emoji} **${result.risk_score}/100** (${result.risk_level})`,
57
+ `- **Recommendation:** ${result.recommendation}`,
58
+ '',
59
+ '#### Analysis',
60
+ '',
61
+ result.reasoning,
62
+ '',
63
+ '#### Supply Chain Indicators',
64
+ '',
65
+ formatIndicators(result.supply_chain_indicators),
66
+ '',
67
+ '#### Vulnerabilities',
68
+ '',
69
+ formatVulnerabilities(result.vulnerabilities),
70
+ '',
71
+ '#### Version Analysis',
72
+ '',
73
+ `- **Version reviewed:** ${result.version_analysis.version_reviewed}`,
74
+ result.version_analysis.previous_versions_reviewed.length > 0
75
+ ? `- **Previous versions:** ${result.version_analysis.previous_versions_reviewed.join(', ')}`
76
+ : '',
77
+ `- **Changelog risk:** ${result.version_analysis.changelog_risk}`,
78
+ result.version_analysis.changelog_reasoning
79
+ ? `- ${result.version_analysis.changelog_reasoning}`
80
+ : '',
81
+ ].filter(Boolean).join('\n');
82
+ }
83
+
84
+ export function formatReportAsMarkdown(report: ScanReport): string {
85
+ const emoji = riskEmoji(report.aggregate_risk_score);
86
+ const timestamp = new Date(report.scan_timestamp).toLocaleString('en-US', {
87
+ dateStyle: 'long',
88
+ timeStyle: 'short',
89
+ });
90
+
91
+ const sections = [
92
+ `# ${emoji} OPM Security Report`,
93
+ '',
94
+ `## ${report.package}@${report.version}`,
95
+ '',
96
+ `- **Package:** ${report.package}`,
97
+ `- **Version:** ${report.version}`,
98
+ `- **Scanned:** ${timestamp}`,
99
+ `- **Versions analyzed:** ${report.versions_analyzed.join(', ')}`,
100
+ `- **Agents:** ${report.agents.length}`,
101
+ '',
102
+ '---',
103
+ '',
104
+ '## Aggregate Risk',
105
+ '',
106
+ `\`${riskBar(report.aggregate_risk_score)}\` **${report.aggregate_risk_score}/100** — ${report.consensus}`,
107
+ '',
108
+ report.aggregate_risk_score < 40
109
+ ? '> ✅ This package appears safe based on multi-agent consensus.'
110
+ : report.aggregate_risk_score < 70
111
+ ? '> ⚠️ This package has medium risk indicators. Review the agent findings below.'
112
+ : '> 🚨 This package has HIGH risk. Installation is not recommended.',
113
+ '',
114
+ '---',
115
+ '',
116
+ '## Agent Reports',
117
+ '',
118
+ ...report.agents.map((agent, i) => formatAgent(agent, i)),
119
+ '',
120
+ '---',
121
+ '',
122
+ '## Raw JSON',
123
+ '',
124
+ '```json',
125
+ JSON.stringify(report, null, 2),
126
+ '```',
127
+ '',
128
+ '---',
129
+ '',
130
+ `*Report generated by [OPM](https://github.com/dhananjaypai08/opm) — On-chain Package Manager*`,
131
+ ];
132
+
133
+ return sections.join('\n');
134
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": true,
12
+ "sourceMap": true,
13
+ "outDir": "dist",
14
+ "jsx": "react-jsx",
15
+ "types": ["bun-types"],
16
+ "paths": {
17
+ "@opm/core": ["./packages/core/src/index.ts"],
18
+ "@opm/scanner": ["./packages/scanner/src/index.ts"]
19
+ },
20
+ "baseUrl": "."
21
+ },
22
+ "exclude": ["node_modules", "dist", "packages/contracts"]
23
+ }