chain-insights 0.2.16
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/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +10 -0
- package/bin/install.cjs +252 -0
- package/bin/mcp-proxy.cjs +10 -0
- package/dist/active-BSrxLKwn.mjs +50 -0
- package/dist/active-BSrxLKwn.mjs.map +1 -0
- package/dist/active-Dv7Tu-O4.cjs +68 -0
- package/dist/app-BjjuQM0B.mjs +155 -0
- package/dist/app-BjjuQM0B.mjs.map +1 -0
- package/dist/app-Dq1TdB6p.cjs +161 -0
- package/dist/artifact-server-DoxJ7fCx.cjs +47 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs +48 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs.map +1 -0
- package/dist/assets/bg-pattern.png +0 -0
- package/dist/assets/logo.png +0 -0
- package/dist/call-args-DQA2QcRA.cjs +27 -0
- package/dist/call-args-Lk_wOJxd.mjs +29 -0
- package/dist/call-args-Lk_wOJxd.mjs.map +1 -0
- package/dist/capabilities-CB97WMA5.cjs +83 -0
- package/dist/capabilities-DliMBim-.mjs +84 -0
- package/dist/capabilities-DliMBim-.mjs.map +1 -0
- package/dist/cases-By7INiOa.mjs +6 -0
- package/dist/cases-CDcNU91B.cjs +9 -0
- package/dist/chunk-CZWwpsFl.cjs +43 -0
- package/dist/cli.cjs +752 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +753 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-D4Bq0rp9.mjs +111 -0
- package/dist/client-D4Bq0rp9.mjs.map +1 -0
- package/dist/client-D4fZgIaO.cjs +132 -0
- package/dist/config-Bmdl5hdk.cjs +67 -0
- package/dist/config-BwrBYmiC.mjs +44 -0
- package/dist/config-BwrBYmiC.mjs.map +1 -0
- package/dist/data-extractor-BNGj7ECT.cjs +347 -0
- package/dist/data-extractor-DFzsa5CS.mjs +336 -0
- package/dist/data-extractor-DFzsa5CS.mjs.map +1 -0
- package/dist/dossier-BsroDgD3.mjs +76 -0
- package/dist/dossier-BsroDgD3.mjs.map +1 -0
- package/dist/dossier-DtxREpPm.cjs +76 -0
- package/dist/evidence-BGcdKxuV.cjs +200 -0
- package/dist/evidence-BhvFW-y_.mjs +195 -0
- package/dist/evidence-BhvFW-y_.mjs.map +1 -0
- package/dist/format-Ce1RObVl.mjs +22 -0
- package/dist/format-Ce1RObVl.mjs.map +1 -0
- package/dist/format-DOrPvXEr.cjs +20 -0
- package/dist/frontmatter-D8wWCeOa.mjs +26 -0
- package/dist/frontmatter-D8wWCeOa.mjs.map +1 -0
- package/dist/frontmatter-DgAuai7E.cjs +35 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs +130 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs.map +1 -0
- package/dist/graph-normalizer-DeIj6Ses.cjs +133 -0
- package/dist/graph-reports-C4TBjCkM.mjs +63 -0
- package/dist/graph-reports-C4TBjCkM.mjs.map +1 -0
- package/dist/graph-reports-DU05YCei.cjs +64 -0
- package/dist/html-generator-CAv81IWH.cjs +85 -0
- package/dist/html-generator-V6Bp0uRb.mjs +68 -0
- package/dist/html-generator-V6Bp0uRb.mjs.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +9 -0
- package/dist/init-BjuFt54X.cjs +232 -0
- package/dist/init-CaOsHTIo.mjs +232 -0
- package/dist/init-CaOsHTIo.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +1257 -0
- package/dist/mcp-proxy.d.cts +12 -0
- package/dist/mcp-proxy.d.cts.map +1 -0
- package/dist/mcp-proxy.d.mts +12 -0
- package/dist/mcp-proxy.d.mts.map +1 -0
- package/dist/mcp-proxy.mjs +1255 -0
- package/dist/mcp-proxy.mjs.map +1 -0
- package/dist/output-root-CFYms3ad.cjs +43 -0
- package/dist/output-root-CmWM7aV2.mjs +33 -0
- package/dist/output-root-CmWM7aV2.mjs.map +1 -0
- package/dist/parser-BUIWW1OH.cjs +182 -0
- package/dist/parser-DO0_SssG.mjs +182 -0
- package/dist/parser-DO0_SssG.mjs.map +1 -0
- package/dist/public-tools-D4UI-Zb0.mjs +2554 -0
- package/dist/public-tools-D4UI-Zb0.mjs.map +1 -0
- package/dist/public-tools-XSpkz2ky.cjs +2556 -0
- package/dist/resolver-C2ZS7oC8.mjs +201 -0
- package/dist/resolver-C2ZS7oC8.mjs.map +1 -0
- package/dist/resolver-zYbu4wDV.cjs +203 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/runner-1Eq55OYb.cjs +148 -0
- package/dist/runner-BhUHbiHG.mjs +149 -0
- package/dist/runner-BhUHbiHG.mjs.map +1 -0
- package/dist/schema-4XpzDFQM.cjs +55 -0
- package/dist/schema-8d0rVIdZ.mjs +37 -0
- package/dist/schema-8d0rVIdZ.mjs.map +1 -0
- package/dist/schema-cache-9CksD7tX.mjs +34 -0
- package/dist/schema-cache-9CksD7tX.mjs.map +1 -0
- package/dist/schema-cache-CgWRCN2N.cjs +36 -0
- package/dist/selector-CkFcTXzz.cjs +10 -0
- package/dist/selector-xjm6NTHI.mjs +12 -0
- package/dist/selector-xjm6NTHI.mjs.map +1 -0
- package/dist/server-BkM5xrXb.mjs +45 -0
- package/dist/server-BkM5xrXb.mjs.map +1 -0
- package/dist/server-DXowbpfi.cjs +54 -0
- package/dist/session-BpNylyuJ.cjs +115 -0
- package/dist/session-CcTgYxsj.mjs +115 -0
- package/dist/session-CcTgYxsj.mjs.map +1 -0
- package/dist/setup-DOpKPrlx.cjs +81 -0
- package/dist/setup-DyrWHuwQ.mjs +80 -0
- package/dist/setup-DyrWHuwQ.mjs.map +1 -0
- package/dist/store-BiUhQOIf.cjs +230 -0
- package/dist/store-BoWE-Gtl.mjs +225 -0
- package/dist/store-BoWE-Gtl.mjs.map +1 -0
- package/dist/templates/graph.html +1406 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs +28 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs.map +1 -0
- package/dist/tool-visibility-CwgY205r.cjs +36 -0
- package/dist/tools-Cp2jAAAb.mjs +100 -0
- package/dist/tools-Cp2jAAAb.mjs.map +1 -0
- package/dist/tools-f_vJUZAF.cjs +139 -0
- package/dist/topup-server-BZuQifvh.cjs +940 -0
- package/dist/topup-server-DUjyFftI.mjs +919 -0
- package/dist/topup-server-DUjyFftI.mjs.map +1 -0
- package/dist/version-1gP19Lhi.mjs +8 -0
- package/dist/version-1gP19Lhi.mjs.map +1 -0
- package/dist/version-BNGtdpmH.cjs +18 -0
- package/dist/viz-BlCJe6Tk.mjs +35 -0
- package/dist/viz-BlCJe6Tk.mjs.map +1 -0
- package/dist/viz-ClezVXrJ.cjs +44 -0
- package/dist/wallet-BMelXBYP.mjs +104 -0
- package/dist/wallet-BMelXBYP.mjs.map +1 -0
- package/dist/wallet-RnvvSpV2.cjs +146 -0
- package/docs/architecture.md +145 -0
- package/docs/contributing.md +68 -0
- package/docs/debugging.md +68 -0
- package/docs/development.md +44 -0
- package/docs/graph-tools.md +251 -0
- package/docs/images/graph-mcp-iframe.png +0 -0
- package/docs/images/graph-visualization.png +0 -0
- package/docs/images/topup-page.png +0 -0
- package/docs/investigation-workspaces.md +151 -0
- package/docs/mcp-proxy.md +180 -0
- package/package.json +59 -0
- package/skills/chain-insights-developer-experience/SKILL.md +101 -0
- package/skills/chain-insights-investigation/SKILL.md +285 -0
- package/skills/chain-insights-investigation/agents/openai.yaml +4 -0
- package/skills/chain-insights-investigation/scripts/run-target-uat.sh +197 -0
- package/skills/chain-insights-trace-funds/SKILL.md +249 -0
- package/skills/ci-case/SKILL.md +43 -0
- package/skills/ci-status/SKILL.md +45 -0
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +75 -0
- package/skills/test-chain-insights-graphrag-mcp/agents/openai.yaml +4 -0
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +414 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-extractor-DFzsa5CS.mjs","names":[],"sources":["../src/viz/graph-model.ts","../src/viz/data-extractor.ts"],"sourcesContent":["import * as z from 'zod'\n\nexport const EntityType = z.enum(['eoa', 'contract', 'exchange', 'mixer', 'unknown'])\nexport type EntityType = z.infer<typeof EntityType>\n\nexport const RiskLevel = z.enum(['low', 'medium', 'high', 'critical', 'unknown'])\nexport type RiskLevel = z.infer<typeof RiskLevel>\n\nexport const GraphNode = z.object({\n id: z.string().min(1),\n label: z.string().optional(),\n entityType: EntityType.default('unknown'),\n riskLevel: RiskLevel.default('unknown'),\n totalIn: z.number().default(0),\n totalOut: z.number().default(0),\n txCount: z.number().int().default(0),\n firstSeen: z.string().optional(),\n lastSeen: z.string().optional(),\n})\nexport type GraphNode = z.infer<typeof GraphNode>\n\nexport const GraphEdge = z.object({\n source: z.string().min(1),\n target: z.string().min(1),\n value: z.number(),\n txHash: z.string().optional(),\n blockNumber: z.number().int().optional(),\n timestamp: z.string().optional(),\n})\nexport type GraphEdge = z.infer<typeof GraphEdge>\n\nexport const GraphData = z.object({\n nodes: z.array(GraphNode),\n edges: z.array(GraphEdge),\n metadata: z.object({\n caseId: z.string().optional(),\n title: z.string().default('Money Flow'),\n generatedAt: z.string(),\n truncated: z.boolean().default(false),\n totalNodes: z.number().int().optional(),\n hiddenNodes: z.number().int().optional(),\n }),\n})\nexport type GraphData = z.infer<typeof GraphData>\n\nconst MAX_NODES = 100\n\nexport function truncateGraph(data: GraphData): GraphData {\n if (data.nodes.length <= MAX_NODES) return data\n const sorted = [...data.nodes].sort((a, b) => (b.totalIn + b.totalOut) - (a.totalIn + a.totalOut))\n const kept = sorted.slice(0, MAX_NODES)\n const keptIds = new Set(kept.map(n => n.id))\n const filteredEdges = data.edges.filter(e => keptIds.has(e.source) && keptIds.has(e.target))\n return {\n nodes: kept,\n edges: filteredEdges,\n metadata: {\n ...data.metadata,\n truncated: true,\n totalNodes: data.nodes.length,\n hiddenNodes: data.nodes.length - MAX_NODES,\n },\n }\n}\n","import { readFile, readdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { GraphData, GraphNode, GraphEdge } from './graph-model.js'\nimport { parseFrontmatter } from '../cases/frontmatter.js'\nimport { activeCasesRoot } from '../workspace/active.js'\n\nfunction caseDir(caseId: string): string {\n if (/[/\\\\]|^\\.\\.?$/.test(caseId)) throw new Error(`Invalid case ID: ${caseId}`)\n return path.join(activeCasesRoot(), caseId)\n}\n\n/**\n * Extracts all items from ```json code blocks in a markdown string.\n * If the parsed value is an array, spreads all items.\n * If the parsed value has 'nodes' and 'edges', wraps it as a single item.\n * Returns empty array if no JSON blocks found or parsing fails.\n */\nexport function parseEvidenceJson(markdown: string): unknown[] {\n const results: unknown[] = []\n const re = /```json\\s*\\n([\\s\\S]*?)```/g\n let match: RegExpExecArray | null\n while ((match = re.exec(markdown)) !== null) {\n const raw = match[1]\n if (!raw) continue\n try {\n const parsed: unknown = JSON.parse(raw)\n if (Array.isArray(parsed)) {\n results.push(...parsed)\n } else if (\n parsed !== null &&\n typeof parsed === 'object' &&\n 'nodes' in (parsed as object) &&\n 'edges' in (parsed as object)\n ) {\n // Full GraphData object embedded in evidence\n results.push(parsed)\n }\n // Ignore other object types\n } catch {\n // Malformed JSON in evidence — skip gracefully (T-04-06: no crash)\n }\n }\n if (results.length > 0) return results\n\n const rawJson = extractEmbeddedJson(markdown)\n if (rawJson) {\n try {\n const parsed: unknown = JSON.parse(rawJson)\n if (Array.isArray(parsed)) return parsed\n if (parsed !== null && typeof parsed === 'object') return [parsed]\n } catch {\n // Ignore non-JSON evidence bodies.\n }\n }\n return results\n}\n\nfunction extractEmbeddedJson(text: string): string | null {\n const trimmed = text.trim()\n const start = [...trimmed]\n .map((char, index) => (char === '{' || char === '[' ? index : -1))\n .find(index => index >= 0)\n if (start === undefined) return null\n return trimmed.slice(start)\n}\n\ntype SimpleTx = { from: string; to: string; value: number; txHash?: string; blockNumber?: number; timestamp?: string }\n\ntype CompactEvidence = {\n schema?: string\n outgoing_flows?: Array<{\n src?: string\n dst?: string\n amount_sum?: number\n tx_count?: number\n first_tx_id?: string\n }>\n}\n\nfunction isSimpleTx(item: unknown): item is SimpleTx {\n return (\n item !== null &&\n typeof item === 'object' &&\n 'from' in (item as object) &&\n 'to' in (item as object) &&\n 'value' in (item as object)\n )\n}\n\nfunction isGraphDataLike(input: unknown): input is { nodes: unknown[]; edges: unknown[] } {\n return (\n input !== null &&\n typeof input === 'object' &&\n Array.isArray((input as Record<string, unknown>)['nodes']) &&\n Array.isArray((input as Record<string, unknown>)['edges'])\n )\n}\n\nfunction compactEvidenceToSimpleTxs(item: unknown): SimpleTx[] {\n const compact = item as CompactEvidence\n if (\n !compact ||\n typeof compact !== 'object' ||\n compact.schema !== 'chain-insights.compact_evidence.v1' ||\n !Array.isArray(compact.outgoing_flows)\n ) {\n return []\n }\n\n return compact.outgoing_flows\n .filter(flow => typeof flow.src === 'string' && typeof flow.dst === 'string' && typeof flow.amount_sum === 'number')\n .map(flow => ({\n from: flow.src!,\n to: flow.dst!,\n value: flow.amount_sum!,\n txHash: flow.first_tx_id,\n }))\n}\n\n/**\n * Converts simple [{from, to, value}] transaction arrays into graph nodes.\n * Computes totalIn, totalOut, txCount per node from edges.\n */\nfunction buildGraphFromSimpleTxs(items: SimpleTx[]): { nodes: GraphNode[]; edges: GraphEdge[] } {\n // Build edges first\n const edges: GraphEdge[] = items.map(tx => GraphEdge.parse({\n source: tx.from,\n target: tx.to,\n value: tx.value,\n txHash: tx.txHash,\n blockNumber: tx.blockNumber,\n timestamp: tx.timestamp,\n }))\n\n // Collect unique addresses\n const addresses = new Set<string>()\n for (const tx of items) {\n addresses.add(tx.from)\n addresses.add(tx.to)\n }\n\n // Compute totals per address\n const totals: Record<string, { totalIn: number; totalOut: number; txCount: number }> = {}\n for (const addr of addresses) {\n totals[addr] = { totalIn: 0, totalOut: 0, txCount: 0 }\n }\n for (const tx of items) {\n const out = totals[tx.from]\n const inp = totals[tx.to]\n if (out) {\n out.totalOut += tx.value\n out.txCount += 1\n }\n if (inp) {\n inp.totalIn += tx.value\n inp.txCount += 1\n }\n }\n\n const nodes: GraphNode[] = [...addresses].map(addr =>\n GraphNode.parse({\n id: addr,\n entityType: 'unknown',\n riskLevel: 'unknown',\n totalIn: totals[addr]?.totalIn ?? 0,\n totalOut: totals[addr]?.totalOut ?? 0,\n txCount: totals[addr]?.txCount ?? 0,\n })\n )\n\n return { nodes, edges }\n}\n\n/**\n * Handles two input formats:\n * 1. Full GraphData object (has nodes + edges) — parse with Zod\n * 2. Array of {from, to, value, ...} transaction objects — auto-derive nodes\n *\n * Throws \"Invalid transaction data\" for any other input.\n */\nexport function extractGraphFromJson(input: unknown): GraphData {\n if (isGraphDataLike(input)) {\n // Full GraphData: parse and validate through Zod\n return GraphData.parse(input)\n }\n\n if (Array.isArray(input)) {\n const simpleTxs: SimpleTx[] = []\n for (const item of input) {\n if (isSimpleTx(item)) {\n simpleTxs.push(item)\n } else {\n simpleTxs.push(...compactEvidenceToSimpleTxs(item))\n }\n }\n const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxs)\n return GraphData.parse({\n nodes,\n edges,\n metadata: {\n title: 'Money Flow',\n generatedAt: new Date().toISOString(),\n },\n })\n }\n\n throw new Error(\n 'Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.'\n )\n}\n\n/**\n * Merges two sets of nodes, deduplicating by id.\n * For duplicate nodes, sums totalIn, totalOut, txCount; keeps earliest firstSeen, latest lastSeen.\n */\nfunction mergeNodes(existing: GraphNode[], incoming: GraphNode[]): GraphNode[] {\n const map = new Map<string, GraphNode>()\n for (const node of existing) {\n map.set(node.id, { ...node })\n }\n for (const node of incoming) {\n const prev = map.get(node.id)\n if (prev) {\n map.set(node.id, {\n ...prev,\n totalIn: prev.totalIn + node.totalIn,\n totalOut: prev.totalOut + node.totalOut,\n txCount: prev.txCount + node.txCount,\n firstSeen: pickEarlier(prev.firstSeen, node.firstSeen),\n lastSeen: pickLater(prev.lastSeen, node.lastSeen),\n })\n } else {\n map.set(node.id, { ...node })\n }\n }\n return [...map.values()]\n}\n\nfunction pickEarlier(a: string | undefined, b: string | undefined): string | undefined {\n if (!a) return b\n if (!b) return a\n return a < b ? a : b\n}\n\nfunction pickLater(a: string | undefined, b: string | undefined): string | undefined {\n if (!a) return b\n if (!b) return a\n return a > b ? a : b\n}\n\n/**\n * Aggregates edges: for duplicate (source, target) pairs, sums value; keeps last txHash/timestamp.\n */\nfunction aggregateEdges(edges: GraphEdge[]): GraphEdge[] {\n const map = new Map<string, GraphEdge>()\n for (const edge of edges) {\n const key = `${edge.source}::${edge.target}`\n const prev = map.get(key)\n if (prev) {\n map.set(key, { ...edge, value: prev.value + edge.value })\n } else {\n map.set(key, { ...edge })\n }\n }\n return [...map.values()]\n}\n\n/**\n * Reads evidence files from a case directory, extracts JSON transaction data\n * from markdown code blocks, enriches nodes with entity types from dossiers,\n * and returns a merged GraphData.\n */\nexport async function extractGraphFromCase(caseId: string): Promise<GraphData> {\n const evidenceDir = path.join(caseDir(caseId), 'evidence')\n\n let files: string[] = []\n try {\n const all = await readdir(evidenceDir)\n files = all.filter(f => f.endsWith('.md'))\n } catch {\n // Evidence directory missing — return empty graph (graceful, not an error)\n return GraphData.parse({\n nodes: [],\n edges: [],\n metadata: {\n caseId,\n title: `${caseId} - Money Flow`,\n generatedAt: new Date().toISOString(),\n },\n })\n }\n\n let allNodes: GraphNode[] = []\n let allEdges: GraphEdge[] = []\n\n for (const file of files) {\n const raw = await readFile(path.join(evidenceDir, file), 'utf-8')\n const { body } = parseFrontmatter(raw)\n const items = parseEvidenceJson(body)\n if (items.length === 0) continue\n\n // Check if items contain a full GraphData object\n const graphDataItems = items.filter(item => isGraphDataLike(item))\n const simpleTxItems = items.flatMap(item => {\n if (isSimpleTx(item)) return [item]\n return compactEvidenceToSimpleTxs(item)\n }) as SimpleTx[]\n\n if (graphDataItems.length > 0) {\n // Merge full GraphData objects\n for (const gd of graphDataItems) {\n try {\n const parsed = GraphData.parse(gd)\n allNodes = mergeNodes(allNodes, parsed.nodes)\n allEdges = [...allEdges, ...parsed.edges]\n } catch {\n // Invalid GraphData in evidence — skip (T-04-06)\n }\n }\n // Also process any simple tx items alongside\n if (simpleTxItems.length > 0) {\n const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxItems)\n allNodes = mergeNodes(allNodes, nodes)\n allEdges = [...allEdges, ...edges]\n }\n } else if (simpleTxItems.length > 0) {\n const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxItems)\n allNodes = mergeNodes(allNodes, nodes)\n allEdges = [...allEdges, ...edges]\n }\n }\n\n // Aggregate edges with same source-target pair\n allEdges = aggregateEdges(allEdges)\n\n // Enrich nodes with entity types from dossiers\n try {\n const { DossierStore } = await import('../cases/index.js')\n const dossiers = await DossierStore.listSummaries(caseId)\n const dossierMap = new Map<string, string>()\n for (const d of dossiers) {\n dossierMap.set(d.address, d.type)\n }\n allNodes = allNodes.map(node => {\n const dossierType = dossierMap.get(node.id)\n if (dossierType && dossierType !== 'unknown') {\n // Validate against EntityType enum — default to 'unknown' if invalid (T-04-07)\n const validTypes = ['eoa', 'contract', 'exchange', 'mixer', 'unknown'] as const\n type ValidType = (typeof validTypes)[number]\n const entityType: ValidType = validTypes.includes(dossierType as ValidType)\n ? (dossierType as ValidType)\n : 'unknown'\n return { ...node, entityType }\n }\n return node\n })\n } catch {\n // Dossier enrichment is best-effort — don't fail extraction if dossiers unavailable\n }\n\n return GraphData.parse({\n nodes: allNodes,\n edges: allEdges,\n metadata: {\n caseId,\n title: `${caseId} - Money Flow`,\n generatedAt: new Date().toISOString(),\n },\n })\n}\n\nexport const DataExtractor = {\n extractGraphFromCase,\n extractGraphFromJson,\n parseEvidenceJson,\n}\n"],"mappings":";;;;;;;AAEA,MAAa,aAAa,EAAE,KAAK;CAAC;CAAO;CAAY;CAAY;CAAS;CAAU,CAAC;AAGrF,MAAa,YAAY,EAAE,KAAK;CAAC;CAAO;CAAU;CAAQ;CAAY;CAAU,CAAC;AAGjF,MAAa,YAAY,EAAE,OAAO;CAChC,IAAY,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC7B,OAAY,EAAE,QAAQ,CAAC,UAAU;CACjC,YAAY,WAAW,QAAQ,UAAU;CACzC,WAAY,UAAU,QAAQ,UAAU;CACxC,SAAY,EAAE,QAAQ,CAAC,QAAQ,EAAE;CACjC,UAAY,EAAE,QAAQ,CAAC,QAAQ,EAAE;CACjC,SAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE;CACvC,WAAY,EAAE,QAAQ,CAAC,UAAU;CACjC,UAAY,EAAE,QAAQ,CAAC,UAAU;CAClC,CAAC;AAGF,MAAa,YAAY,EAAE,OAAO;CAChC,QAAa,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC9B,QAAa,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC9B,OAAa,EAAE,QAAQ;CACvB,QAAa,EAAE,QAAQ,CAAC,UAAU;CAClC,aAAa,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CACxC,WAAa,EAAE,QAAQ,CAAC,UAAU;CACnC,CAAC;AAGF,MAAa,YAAY,EAAE,OAAO;CAChC,OAAO,EAAE,MAAM,UAAU;CACzB,OAAO,EAAE,MAAM,UAAU;CACzB,UAAU,EAAE,OAAO;EACjB,QAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,OAAa,EAAE,QAAQ,CAAC,QAAQ,aAAa;EAC7C,aAAa,EAAE,QAAQ;EACvB,WAAa,EAAE,SAAS,CAAC,QAAQ,MAAM;EACvC,YAAa,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;EACxC,aAAa,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;EACzC,CAAC;CACH,CAAC;AAGF,MAAM,YAAY;AAElB,SAAgB,cAAc,MAA4B;AACxD,KAAI,KAAK,MAAM,UAAU,UAAW,QAAO;CAE3C,MAAM,OADS,CAAC,GAAG,KAAK,MAAM,CAAC,MAAM,GAAG,MAAO,EAAE,UAAU,EAAE,YAAa,EAAE,UAAU,EAAE,UACrE,CAAC,MAAM,GAAG,UAAU;CACvC,MAAM,UAAU,IAAI,IAAI,KAAK,KAAI,MAAK,EAAE,GAAG,CAAC;AAE5C,QAAO;EACL,OAAO;EACP,OAHoB,KAAK,MAAM,QAAO,MAAK,QAAQ,IAAI,EAAE,OAAO,IAAI,QAAQ,IAAI,EAAE,OAAO,CAGrE;EACpB,UAAU;GACR,GAAG,KAAK;GACR,WAAW;GACX,YAAY,KAAK,MAAM;GACvB,aAAa,KAAK,MAAM,SAAS;GAClC;EACF;;;;;;;;;ACxDH,SAAS,QAAQ,QAAwB;AACvC,KAAI,gBAAgB,KAAK,OAAO,CAAE,OAAM,IAAI,MAAM,oBAAoB,SAAS;AAC/E,QAAO,KAAK,KAAK,iBAAiB,EAAE,OAAO;;;;;;;;AAS7C,SAAgB,kBAAkB,UAA6B;CAC7D,MAAM,UAAqB,EAAE;CAC7B,MAAM,KAAK;CACX,IAAI;AACJ,SAAQ,QAAQ,GAAG,KAAK,SAAS,MAAM,MAAM;EAC3C,MAAM,MAAM,MAAM;AAClB,MAAI,CAAC,IAAK;AACV,MAAI;GACF,MAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,OAAI,MAAM,QAAQ,OAAO,CACvB,SAAQ,KAAK,GAAG,OAAO;YAEvB,WAAW,QACX,OAAO,WAAW,YAClB,WAAY,UACZ,WAAY,OAGZ,SAAQ,KAAK,OAAO;UAGhB;;AAIV,KAAI,QAAQ,SAAS,EAAG,QAAO;CAE/B,MAAM,UAAU,oBAAoB,SAAS;AAC7C,KAAI,QACF,KAAI;EACF,MAAM,SAAkB,KAAK,MAAM,QAAQ;AAC3C,MAAI,MAAM,QAAQ,OAAO,CAAE,QAAO;AAClC,MAAI,WAAW,QAAQ,OAAO,WAAW,SAAU,QAAO,CAAC,OAAO;SAC5D;AAIV,QAAO;;AAGT,SAAS,oBAAoB,MAA6B;CACxD,MAAM,UAAU,KAAK,MAAM;CAC3B,MAAM,QAAQ,CAAC,GAAG,QAAQ,CACvB,KAAK,MAAM,UAAW,SAAS,OAAO,SAAS,MAAM,QAAQ,GAAI,CACjE,MAAK,UAAS,SAAS,EAAE;AAC5B,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,QAAO,QAAQ,MAAM,MAAM;;AAgB7B,SAAS,WAAW,MAAiC;AACnD,QACE,SAAS,QACT,OAAO,SAAS,YAChB,UAAW,QACX,QAAS,QACT,WAAY;;AAIhB,SAAS,gBAAgB,OAAiE;AACxF,QACE,UAAU,QACV,OAAO,UAAU,YACjB,MAAM,QAAS,MAAkC,SAAS,IAC1D,MAAM,QAAS,MAAkC,SAAS;;AAI9D,SAAS,2BAA2B,MAA2B;CAC7D,MAAM,UAAU;AAChB,KACE,CAAC,WACD,OAAO,YAAY,YACnB,QAAQ,WAAW,wCACnB,CAAC,MAAM,QAAQ,QAAQ,eAAe,CAEtC,QAAO,EAAE;AAGX,QAAO,QAAQ,eACZ,QAAO,SAAQ,OAAO,KAAK,QAAQ,YAAY,OAAO,KAAK,QAAQ,YAAY,OAAO,KAAK,eAAe,SAAS,CACnH,KAAI,UAAS;EACZ,MAAM,KAAK;EACX,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,QAAQ,KAAK;EACd,EAAE;;;;;;AAOP,SAAS,wBAAwB,OAA+D;CAE9F,MAAM,QAAqB,MAAM,KAAI,OAAM,UAAU,MAAM;EACzD,QAAQ,GAAG;EACX,QAAQ,GAAG;EACX,OAAO,GAAG;EACV,QAAQ,GAAG;EACX,aAAa,GAAG;EAChB,WAAW,GAAG;EACf,CAAC,CAAC;CAGH,MAAM,4BAAY,IAAI,KAAa;AACnC,MAAK,MAAM,MAAM,OAAO;AACtB,YAAU,IAAI,GAAG,KAAK;AACtB,YAAU,IAAI,GAAG,GAAG;;CAItB,MAAM,SAAiF,EAAE;AACzF,MAAK,MAAM,QAAQ,UACjB,QAAO,QAAQ;EAAE,SAAS;EAAG,UAAU;EAAG,SAAS;EAAG;AAExD,MAAK,MAAM,MAAM,OAAO;EACtB,MAAM,MAAM,OAAO,GAAG;EACtB,MAAM,MAAM,OAAO,GAAG;AACtB,MAAI,KAAK;AACP,OAAI,YAAY,GAAG;AACnB,OAAI,WAAW;;AAEjB,MAAI,KAAK;AACP,OAAI,WAAW,GAAG;AAClB,OAAI,WAAW;;;AAenB,QAAO;EAAE,OAXkB,CAAC,GAAG,UAAU,CAAC,KAAI,SAC5C,UAAU,MAAM;GACd,IAAI;GACJ,YAAY;GACZ,WAAW;GACX,SAAS,OAAO,OAAO,WAAW;GAClC,UAAU,OAAO,OAAO,YAAY;GACpC,SAAS,OAAO,OAAO,WAAW;GACnC,CAAC,CAGU;EAAE;EAAO;;;;;;;;;AAUzB,SAAgB,qBAAqB,OAA2B;AAC9D,KAAI,gBAAgB,MAAM,CAExB,QAAO,UAAU,MAAM,MAAM;AAG/B,KAAI,MAAM,QAAQ,MAAM,EAAE;EACxB,MAAM,YAAwB,EAAE;AAChC,OAAK,MAAM,QAAQ,MACjB,KAAI,WAAW,KAAK,CAClB,WAAU,KAAK,KAAK;MAEpB,WAAU,KAAK,GAAG,2BAA2B,KAAK,CAAC;EAGvD,MAAM,EAAE,OAAO,UAAU,wBAAwB,UAAU;AAC3D,SAAO,UAAU,MAAM;GACrB;GACA;GACA,UAAU;IACR,OAAO;IACP,8BAAa,IAAI,MAAM,EAAC,aAAa;IACtC;GACF,CAAC;;AAGJ,OAAM,IAAI,MACR,mIACD;;;;;;AAOH,SAAS,WAAW,UAAuB,UAAoC;CAC7E,MAAM,sBAAM,IAAI,KAAwB;AACxC,MAAK,MAAM,QAAQ,SACjB,KAAI,IAAI,KAAK,IAAI,EAAE,GAAG,MAAM,CAAC;AAE/B,MAAK,MAAM,QAAQ,UAAU;EAC3B,MAAM,OAAO,IAAI,IAAI,KAAK,GAAG;AAC7B,MAAI,KACF,KAAI,IAAI,KAAK,IAAI;GACf,GAAG;GACH,SAAS,KAAK,UAAU,KAAK;GAC7B,UAAU,KAAK,WAAW,KAAK;GAC/B,SAAS,KAAK,UAAU,KAAK;GAC7B,WAAW,YAAY,KAAK,WAAW,KAAK,UAAU;GACtD,UAAU,UAAU,KAAK,UAAU,KAAK,SAAS;GAClD,CAAC;MAEF,KAAI,IAAI,KAAK,IAAI,EAAE,GAAG,MAAM,CAAC;;AAGjC,QAAO,CAAC,GAAG,IAAI,QAAQ,CAAC;;AAG1B,SAAS,YAAY,GAAuB,GAA2C;AACrF,KAAI,CAAC,EAAG,QAAO;AACf,KAAI,CAAC,EAAG,QAAO;AACf,QAAO,IAAI,IAAI,IAAI;;AAGrB,SAAS,UAAU,GAAuB,GAA2C;AACnF,KAAI,CAAC,EAAG,QAAO;AACf,KAAI,CAAC,EAAG,QAAO;AACf,QAAO,IAAI,IAAI,IAAI;;;;;AAMrB,SAAS,eAAe,OAAiC;CACvD,MAAM,sBAAM,IAAI,KAAwB;AACxC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,GAAG,KAAK,OAAO,IAAI,KAAK;EACpC,MAAM,OAAO,IAAI,IAAI,IAAI;AACzB,MAAI,KACF,KAAI,IAAI,KAAK;GAAE,GAAG;GAAM,OAAO,KAAK,QAAQ,KAAK;GAAO,CAAC;MAEzD,KAAI,IAAI,KAAK,EAAE,GAAG,MAAM,CAAC;;AAG7B,QAAO,CAAC,GAAG,IAAI,QAAQ,CAAC;;;;;;;AAQ1B,eAAsB,qBAAqB,QAAoC;CAC7E,MAAM,cAAc,KAAK,KAAK,QAAQ,OAAO,EAAE,WAAW;CAE1D,IAAI,QAAkB,EAAE;AACxB,KAAI;AAEF,WAAQ,MADU,QAAQ,YAAY,EAC1B,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC;SACpC;AAEN,SAAO,UAAU,MAAM;GACrB,OAAO,EAAE;GACT,OAAO,EAAE;GACT,UAAU;IACR;IACA,OAAO,GAAG,OAAO;IACjB,8BAAa,IAAI,MAAM,EAAC,aAAa;IACtC;GACF,CAAC;;CAGJ,IAAI,WAAwB,EAAE;CAC9B,IAAI,WAAwB,EAAE;AAE9B,MAAK,MAAM,QAAQ,OAAO;EAExB,MAAM,EAAE,SAAS,iBAAiB,MADhB,SAAS,KAAK,KAAK,aAAa,KAAK,EAAE,QAAQ,CAC3B;EACtC,MAAM,QAAQ,kBAAkB,KAAK;AACrC,MAAI,MAAM,WAAW,EAAG;EAGxB,MAAM,iBAAiB,MAAM,QAAO,SAAQ,gBAAgB,KAAK,CAAC;EAClE,MAAM,gBAAgB,MAAM,SAAQ,SAAQ;AAC1C,OAAI,WAAW,KAAK,CAAE,QAAO,CAAC,KAAK;AACnC,UAAO,2BAA2B,KAAK;IACvC;AAEF,MAAI,eAAe,SAAS,GAAG;AAE7B,QAAK,MAAM,MAAM,eACf,KAAI;IACF,MAAM,SAAS,UAAU,MAAM,GAAG;AAClC,eAAW,WAAW,UAAU,OAAO,MAAM;AAC7C,eAAW,CAAC,GAAG,UAAU,GAAG,OAAO,MAAM;WACnC;AAKV,OAAI,cAAc,SAAS,GAAG;IAC5B,MAAM,EAAE,OAAO,UAAU,wBAAwB,cAAc;AAC/D,eAAW,WAAW,UAAU,MAAM;AACtC,eAAW,CAAC,GAAG,UAAU,GAAG,MAAM;;aAE3B,cAAc,SAAS,GAAG;GACnC,MAAM,EAAE,OAAO,UAAU,wBAAwB,cAAc;AAC/D,cAAW,WAAW,UAAU,MAAM;AACtC,cAAW,CAAC,GAAG,UAAU,GAAG,MAAM;;;AAKtC,YAAW,eAAe,SAAS;AAGnC,KAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EACtC,MAAM,WAAW,MAAM,aAAa,cAAc,OAAO;EACzD,MAAM,6BAAa,IAAI,KAAqB;AAC5C,OAAK,MAAM,KAAK,SACd,YAAW,IAAI,EAAE,SAAS,EAAE,KAAK;AAEnC,aAAW,SAAS,KAAI,SAAQ;GAC9B,MAAM,cAAc,WAAW,IAAI,KAAK,GAAG;AAC3C,OAAI,eAAe,gBAAgB,WAAW;IAI5C,MAAM,aAAwB;KAFV;KAAO;KAAY;KAAY;KAAS;KAEpB,CAAC,SAAS,YAAyB,GACtE,cACD;AACJ,WAAO;KAAE,GAAG;KAAM;KAAY;;AAEhC,UAAO;IACP;SACI;AAIR,QAAO,UAAU,MAAM;EACrB,OAAO;EACP,OAAO;EACP,UAAU;GACR;GACA,OAAO,GAAG,OAAO;GACjB,8BAAa,IAAI,MAAM,EAAC,aAAa;GACtC;EACF,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { n as serializeFrontmatter, t as parseFrontmatter } from "./frontmatter-D8wWCeOa.mjs";
|
|
2
|
+
import { n as workspaceOutputPaths } from "./output-root-CmWM7aV2.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import "node:crypto";
|
|
6
|
+
//#region src/cases/dossier.ts
|
|
7
|
+
function caseDir(caseId) {
|
|
8
|
+
return path.join(workspaceOutputPaths().casesRoot, caseId);
|
|
9
|
+
}
|
|
10
|
+
function sanitizeAddress(address) {
|
|
11
|
+
return address.replace(/[^a-zA-Z0-9]/g, "").slice(0, 66);
|
|
12
|
+
}
|
|
13
|
+
const DossierStore = {
|
|
14
|
+
async appendFinding(caseId, address, finding, entityType = "unknown") {
|
|
15
|
+
const safeAddr = sanitizeAddress(address);
|
|
16
|
+
const dossierDir = path.join(caseDir(caseId), "dossiers");
|
|
17
|
+
await mkdir(dossierDir, { recursive: true });
|
|
18
|
+
const filePath = path.join(dossierDir, `${safeAddr}.md`);
|
|
19
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20
|
+
let raw;
|
|
21
|
+
let isNew = false;
|
|
22
|
+
try {
|
|
23
|
+
raw = await readFile(filePath, "utf8");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.code !== "ENOENT") throw err;
|
|
26
|
+
raw = serializeFrontmatter({
|
|
27
|
+
address,
|
|
28
|
+
type: entityType,
|
|
29
|
+
firstSeen: now,
|
|
30
|
+
lastSeen: now,
|
|
31
|
+
riskTags: ""
|
|
32
|
+
}, `# Entity: ${address}\n\n## Summary\n\n${entityType === "exchange" ? "Exchange-labeled entity observed in this case." : entityType === "contract" ? "Contract entity observed in this case." : entityType === "mixer" ? "Mixer-labeled entity observed in this case." : "Address/entity observed in this case."}\n\n## Findings\n\n`);
|
|
33
|
+
isNew = true;
|
|
34
|
+
}
|
|
35
|
+
if (!isNew && raw.includes(finding)) return;
|
|
36
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
37
|
+
frontmatter["lastSeen"] = now;
|
|
38
|
+
if (!isNew) frontmatter["type"] = entityType;
|
|
39
|
+
const findingEntry = `- [${now}] ${finding}\n`;
|
|
40
|
+
await writeFile(filePath, serializeFrontmatter(frontmatter, body.replace("## Findings\n", `## Findings\n\n${findingEntry}`)), { mode: 384 });
|
|
41
|
+
},
|
|
42
|
+
async get(caseId, address) {
|
|
43
|
+
const safeAddr = sanitizeAddress(address);
|
|
44
|
+
const filePath = path.join(caseDir(caseId), "dossiers", `${safeAddr}.md`);
|
|
45
|
+
try {
|
|
46
|
+
return parseFrontmatter(await readFile(filePath, "utf8"));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.code === "ENOENT") return null;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async listSummaries(caseId) {
|
|
53
|
+
const dossierDir = path.join(caseDir(caseId), "dossiers");
|
|
54
|
+
try {
|
|
55
|
+
const files = await readdir(dossierDir);
|
|
56
|
+
const summaries = [];
|
|
57
|
+
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
58
|
+
const { frontmatter } = parseFrontmatter(await readFile(path.join(dossierDir, file), "utf8"));
|
|
59
|
+
summaries.push({
|
|
60
|
+
address: frontmatter["address"] ?? file.replace(".md", ""),
|
|
61
|
+
type: frontmatter["type"] ?? "unknown",
|
|
62
|
+
riskTags: frontmatter["riskTags"] ?? "",
|
|
63
|
+
firstSeen: frontmatter["firstSeen"] ?? "",
|
|
64
|
+
lastSeen: frontmatter["lastSeen"] ?? ""
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return summaries;
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { DossierStore };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=dossier-BsroDgD3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dossier-BsroDgD3.mjs","names":["nodeErr"],"sources":["../src/cases/dossier.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport { createHash } from 'node:crypto'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { parseFrontmatter, serializeFrontmatter } from './frontmatter.js'\n\nfunction caseDir(caseId: string): string {\n return path.join(workspaceOutputPaths().casesRoot, caseId)\n}\n\nfunction sanitizeAddress(address: string): string {\n // Security T-03-06: prevent path traversal by stripping all non-alphanumeric chars\n return address.replace(/[^a-zA-Z0-9]/g, '').slice(0, 66)\n}\n\nfunction contentHash(text: string): string {\n return createHash('sha256').update(text).digest('hex')\n}\n\nexport const DossierStore = {\n async appendFinding(\n caseId: string,\n address: string,\n finding: string,\n entityType: 'eoa' | 'contract' | 'exchange' | 'mixer' | 'unknown' = 'unknown'\n ): Promise<void> {\n const safeAddr = sanitizeAddress(address)\n const dossierDir = path.join(caseDir(caseId), 'dossiers')\n await mkdir(dossierDir, { recursive: true })\n const filePath = path.join(dossierDir, `${safeAddr}.md`)\n const now = new Date().toISOString()\n\n let raw: string\n let isNew = false\n try {\n raw = await readFile(filePath, 'utf8')\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code !== 'ENOENT') throw err\n // New dossier — create template\n const fm: Record<string, string> = {\n address,\n type: entityType,\n firstSeen: now,\n lastSeen: now,\n riskTags: '',\n }\n const summary = entityType === 'exchange'\n ? 'Exchange-labeled entity observed in this case.'\n : entityType === 'contract'\n ? 'Contract entity observed in this case.'\n : entityType === 'mixer'\n ? 'Mixer-labeled entity observed in this case.'\n : 'Address/entity observed in this case.'\n const body = `# Entity: ${address}\\n\\n## Summary\\n\\n${summary}\\n\\n## Findings\\n\\n`\n raw = serializeFrontmatter(fm, body)\n isNew = true\n }\n\n // Content-hash deduplication — skip if finding already present (text presence check)\n if (!isNew && raw.includes(finding)) {\n return\n }\n\n // Update frontmatter lastSeen\n const { frontmatter, body } = parseFrontmatter(raw)\n frontmatter['lastSeen'] = now\n if (!isNew) {\n frontmatter['type'] = entityType\n }\n\n // Append finding to ## Findings section\n const findingEntry = `- [${now}] ${finding}\\n`\n const updatedBody = body.replace('## Findings\\n', `## Findings\\n\\n${findingEntry}`)\n\n await writeFile(filePath, serializeFrontmatter(frontmatter, updatedBody), { mode: 0o600 })\n },\n\n async get(caseId: string, address: string): Promise<{ frontmatter: Record<string, string>; body: string } | null> {\n const safeAddr = sanitizeAddress(address)\n const filePath = path.join(caseDir(caseId), 'dossiers', `${safeAddr}.md`)\n try {\n const raw = await readFile(filePath, 'utf8')\n return parseFrontmatter(raw)\n } catch (err: unknown) {\n const nodeErr = err as NodeJS.ErrnoException\n if (nodeErr.code === 'ENOENT') return null\n throw err\n }\n },\n\n async listSummaries(caseId: string): Promise<Array<{ address: string; type: string; riskTags: string; firstSeen: string; lastSeen: string }>> {\n const dossierDir = path.join(caseDir(caseId), 'dossiers')\n try {\n const files = await readdir(dossierDir)\n const summaries = []\n for (const file of files.filter(f => f.endsWith('.md'))) {\n const raw = await readFile(path.join(dossierDir, file), 'utf8')\n const { frontmatter } = parseFrontmatter(raw)\n summaries.push({\n address: frontmatter['address'] ?? file.replace('.md', ''),\n type: frontmatter['type'] ?? 'unknown',\n riskTags: frontmatter['riskTags'] ?? '',\n firstSeen: frontmatter['firstSeen'] ?? '',\n lastSeen: frontmatter['lastSeen'] ?? '',\n })\n }\n return summaries\n } catch {\n return []\n }\n },\n}\n"],"mappings":";;;;;;AAMA,SAAS,QAAQ,QAAwB;AACvC,QAAO,KAAK,KAAK,sBAAsB,CAAC,WAAW,OAAO;;AAG5D,SAAS,gBAAgB,SAAyB;AAEhD,QAAO,QAAQ,QAAQ,iBAAiB,GAAG,CAAC,MAAM,GAAG,GAAG;;AAO1D,MAAa,eAAe;CAC1B,MAAM,cACJ,QACA,SACA,SACA,aAAoE,WACrD;EACf,MAAM,WAAW,gBAAgB,QAAQ;EACzC,MAAM,aAAa,KAAK,KAAK,QAAQ,OAAO,EAAE,WAAW;AACzD,QAAM,MAAM,YAAY,EAAE,WAAW,MAAM,CAAC;EAC5C,MAAM,WAAW,KAAK,KAAK,YAAY,GAAG,SAAS,KAAK;EACxD,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,IAAI;EACJ,IAAI,QAAQ;AACZ,MAAI;AACF,SAAM,MAAM,SAAS,UAAU,OAAO;WAC/B,KAAc;AAErB,OAAIA,IAAQ,SAAS,SAAU,OAAM;AAiBrC,SAAM,qBAAqB;IAdzB;IACA,MAAM;IACN,WAAW;IACX,UAAU;IACV,UAAU;IAUiB,EAAE,aADL,QAAQ,oBAPlB,eAAe,aAC3B,mDACA,eAAe,aACb,2CACA,eAAe,UACb,gDACA,wCACsD,qBAC1B;AACpC,WAAQ;;AAIV,MAAI,CAAC,SAAS,IAAI,SAAS,QAAQ,CACjC;EAIF,MAAM,EAAE,aAAa,SAAS,iBAAiB,IAAI;AACnD,cAAY,cAAc;AAC1B,MAAI,CAAC,MACH,aAAY,UAAU;EAIxB,MAAM,eAAe,MAAM,IAAI,IAAI,QAAQ;AAG3C,QAAM,UAAU,UAAU,qBAAqB,aAF3B,KAAK,QAAQ,iBAAiB,kBAAkB,eAEG,CAAC,EAAE,EAAE,MAAM,KAAO,CAAC;;CAG5F,MAAM,IAAI,QAAgB,SAAwF;EAChH,MAAM,WAAW,gBAAgB,QAAQ;EACzC,MAAM,WAAW,KAAK,KAAK,QAAQ,OAAO,EAAE,YAAY,GAAG,SAAS,KAAK;AACzE,MAAI;AAEF,UAAO,iBAAiB,MADN,SAAS,UAAU,OAAO,CAChB;WACrB,KAAc;AAErB,OAAIA,IAAQ,SAAS,SAAU,QAAO;AACtC,SAAM;;;CAIV,MAAM,cAAc,QAA0H;EAC5I,MAAM,aAAa,KAAK,KAAK,QAAQ,OAAO,EAAE,WAAW;AACzD,MAAI;GACF,MAAM,QAAQ,MAAM,QAAQ,WAAW;GACvC,MAAM,YAAY,EAAE;AACpB,QAAK,MAAM,QAAQ,MAAM,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,EAAE;IAEvD,MAAM,EAAE,gBAAgB,iBAAiB,MADvB,SAAS,KAAK,KAAK,YAAY,KAAK,EAAE,OAAO,CAClB;AAC7C,cAAU,KAAK;KACb,SAAS,YAAY,cAAc,KAAK,QAAQ,OAAO,GAAG;KAC1D,MAAM,YAAY,WAAW;KAC7B,UAAU,YAAY,eAAe;KACrC,WAAW,YAAY,gBAAgB;KACvC,UAAU,YAAY,eAAe;KACtC,CAAC;;AAEJ,UAAO;UACD;AACN,UAAO,EAAE;;;CAGd"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
const require_frontmatter = require("./frontmatter-DgAuai7E.cjs");
|
|
3
|
+
const require_output_root = require("./output-root-CFYms3ad.cjs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
require("node:crypto");
|
|
8
|
+
//#region src/cases/dossier.ts
|
|
9
|
+
function caseDir(caseId) {
|
|
10
|
+
return node_path.default.join(require_output_root.workspaceOutputPaths().casesRoot, caseId);
|
|
11
|
+
}
|
|
12
|
+
function sanitizeAddress(address) {
|
|
13
|
+
return address.replace(/[^a-zA-Z0-9]/g, "").slice(0, 66);
|
|
14
|
+
}
|
|
15
|
+
const DossierStore = {
|
|
16
|
+
async appendFinding(caseId, address, finding, entityType = "unknown") {
|
|
17
|
+
const safeAddr = sanitizeAddress(address);
|
|
18
|
+
const dossierDir = node_path.default.join(caseDir(caseId), "dossiers");
|
|
19
|
+
await (0, node_fs_promises.mkdir)(dossierDir, { recursive: true });
|
|
20
|
+
const filePath = node_path.default.join(dossierDir, `${safeAddr}.md`);
|
|
21
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22
|
+
let raw;
|
|
23
|
+
let isNew = false;
|
|
24
|
+
try {
|
|
25
|
+
raw = await (0, node_fs_promises.readFile)(filePath, "utf8");
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code !== "ENOENT") throw err;
|
|
28
|
+
raw = require_frontmatter.serializeFrontmatter({
|
|
29
|
+
address,
|
|
30
|
+
type: entityType,
|
|
31
|
+
firstSeen: now,
|
|
32
|
+
lastSeen: now,
|
|
33
|
+
riskTags: ""
|
|
34
|
+
}, `# Entity: ${address}\n\n## Summary\n\n${entityType === "exchange" ? "Exchange-labeled entity observed in this case." : entityType === "contract" ? "Contract entity observed in this case." : entityType === "mixer" ? "Mixer-labeled entity observed in this case." : "Address/entity observed in this case."}\n\n## Findings\n\n`);
|
|
35
|
+
isNew = true;
|
|
36
|
+
}
|
|
37
|
+
if (!isNew && raw.includes(finding)) return;
|
|
38
|
+
const { frontmatter, body } = require_frontmatter.parseFrontmatter(raw);
|
|
39
|
+
frontmatter["lastSeen"] = now;
|
|
40
|
+
if (!isNew) frontmatter["type"] = entityType;
|
|
41
|
+
const findingEntry = `- [${now}] ${finding}\n`;
|
|
42
|
+
await (0, node_fs_promises.writeFile)(filePath, require_frontmatter.serializeFrontmatter(frontmatter, body.replace("## Findings\n", `## Findings\n\n${findingEntry}`)), { mode: 384 });
|
|
43
|
+
},
|
|
44
|
+
async get(caseId, address) {
|
|
45
|
+
const safeAddr = sanitizeAddress(address);
|
|
46
|
+
const filePath = node_path.default.join(caseDir(caseId), "dossiers", `${safeAddr}.md`);
|
|
47
|
+
try {
|
|
48
|
+
return require_frontmatter.parseFrontmatter(await (0, node_fs_promises.readFile)(filePath, "utf8"));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err.code === "ENOENT") return null;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async listSummaries(caseId) {
|
|
55
|
+
const dossierDir = node_path.default.join(caseDir(caseId), "dossiers");
|
|
56
|
+
try {
|
|
57
|
+
const files = await (0, node_fs_promises.readdir)(dossierDir);
|
|
58
|
+
const summaries = [];
|
|
59
|
+
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
60
|
+
const { frontmatter } = require_frontmatter.parseFrontmatter(await (0, node_fs_promises.readFile)(node_path.default.join(dossierDir, file), "utf8"));
|
|
61
|
+
summaries.push({
|
|
62
|
+
address: frontmatter["address"] ?? file.replace(".md", ""),
|
|
63
|
+
type: frontmatter["type"] ?? "unknown",
|
|
64
|
+
riskTags: frontmatter["riskTags"] ?? "",
|
|
65
|
+
firstSeen: frontmatter["firstSeen"] ?? "",
|
|
66
|
+
lastSeen: frontmatter["lastSeen"] ?? ""
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return summaries;
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
exports.DossierStore = DossierStore;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
const require_frontmatter = require("./frontmatter-DgAuai7E.cjs");
|
|
3
|
+
const require_output_root = require("./output-root-CFYms3ad.cjs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
8
|
+
//#region src/cases/evidence.ts
|
|
9
|
+
const MAX_INLINE_JSON_BYTES = 8 * 1024;
|
|
10
|
+
function caseDir(caseId) {
|
|
11
|
+
return node_path.default.join(require_output_root.workspaceOutputPaths().casesRoot, caseId);
|
|
12
|
+
}
|
|
13
|
+
function sanitizeSource(source) {
|
|
14
|
+
return source.replace(/[^a-z0-9_-]/gi, "").slice(0, 40);
|
|
15
|
+
}
|
|
16
|
+
function formatTimestamp() {
|
|
17
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "").slice(0, 15);
|
|
18
|
+
}
|
|
19
|
+
function parseJsonContent(content) {
|
|
20
|
+
const trimmed = content.trim();
|
|
21
|
+
if (trimmed.startsWith("```")) return null;
|
|
22
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(trimmed);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function compactJsonValue(value) {
|
|
30
|
+
if (Array.isArray(value)) return value.map(compactJsonValue);
|
|
31
|
+
if (!value || typeof value !== "object") return value;
|
|
32
|
+
const compact = {};
|
|
33
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
34
|
+
if (entry === null || entry === void 0) continue;
|
|
35
|
+
compact[key] = compactJsonValue(entry);
|
|
36
|
+
}
|
|
37
|
+
return compact;
|
|
38
|
+
}
|
|
39
|
+
function summarizeJsonValue(value) {
|
|
40
|
+
if (Array.isArray(value)) return {
|
|
41
|
+
kind: "array",
|
|
42
|
+
count: value.length,
|
|
43
|
+
sample: value.slice(0, 3).map(compactJsonValue)
|
|
44
|
+
};
|
|
45
|
+
if (!value || typeof value !== "object") return {
|
|
46
|
+
kind: typeof value,
|
|
47
|
+
value
|
|
48
|
+
};
|
|
49
|
+
const record = compactJsonValue(value);
|
|
50
|
+
const summary = {
|
|
51
|
+
kind: "object",
|
|
52
|
+
keys: Object.keys(record).slice(0, 50)
|
|
53
|
+
};
|
|
54
|
+
for (const key of [
|
|
55
|
+
"schema",
|
|
56
|
+
"source",
|
|
57
|
+
"tool",
|
|
58
|
+
"network",
|
|
59
|
+
"seed_address",
|
|
60
|
+
"address"
|
|
61
|
+
]) if (typeof record[key] === "string") summary[key] = record[key];
|
|
62
|
+
for (const key of [
|
|
63
|
+
"files",
|
|
64
|
+
"outputs",
|
|
65
|
+
"facts"
|
|
66
|
+
]) {
|
|
67
|
+
const entry = record[key];
|
|
68
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) summary[key] = compactJsonValue(entry);
|
|
69
|
+
}
|
|
70
|
+
const counts = Object.fromEntries(Object.entries(record).filter(([, entry]) => Array.isArray(entry)).map(([key, entry]) => [key, entry.length]));
|
|
71
|
+
if (Object.keys(counts).length > 0) summary["array_counts"] = counts;
|
|
72
|
+
return summary;
|
|
73
|
+
}
|
|
74
|
+
async function formatEvidenceContent(evidenceId, source, timestamp, content) {
|
|
75
|
+
const parsedJson = parseJsonContent(content);
|
|
76
|
+
if (parsedJson === null) return content;
|
|
77
|
+
const compactJson = compactJsonValue(parsedJson);
|
|
78
|
+
const prettyJson = JSON.stringify(compactJson, null, 2);
|
|
79
|
+
if (Buffer.byteLength(prettyJson, "utf8") <= MAX_INLINE_JSON_BYTES) return `\`\`\`json\n${prettyJson}\n\`\`\``;
|
|
80
|
+
const paths = require_output_root.workspaceOutputPaths();
|
|
81
|
+
await (0, node_fs_promises.mkdir)(paths.reportTablesRoot, {
|
|
82
|
+
recursive: true,
|
|
83
|
+
mode: 448
|
|
84
|
+
});
|
|
85
|
+
const tableFilename = `${evidenceId}_${sanitizeSource(source) || "evidence"}_${timestamp}_${Math.random().toString(36).slice(2, 8)}.json`;
|
|
86
|
+
const tablePath = node_path.default.join(paths.reportTablesRoot, tableFilename);
|
|
87
|
+
await (0, node_fs_promises.writeFile)(tablePath, prettyJson + "\n", {
|
|
88
|
+
mode: 384,
|
|
89
|
+
flag: "wx"
|
|
90
|
+
});
|
|
91
|
+
const relativeTablePath = node_path.default.relative(paths.root, tablePath);
|
|
92
|
+
const summary = {
|
|
93
|
+
schema: "chain-insights.evidence_summary.v1",
|
|
94
|
+
omitted_inline_json: true,
|
|
95
|
+
stored_json: relativeTablePath,
|
|
96
|
+
summary: summarizeJsonValue(compactJson)
|
|
97
|
+
};
|
|
98
|
+
return [
|
|
99
|
+
"Large JSON evidence was stored as an analyst table extract instead of inline Markdown.",
|
|
100
|
+
"",
|
|
101
|
+
`Stored JSON: \`${relativeTablePath}\``,
|
|
102
|
+
"",
|
|
103
|
+
"```json",
|
|
104
|
+
JSON.stringify(summary, null, 2),
|
|
105
|
+
"```"
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
function hashContent(content) {
|
|
109
|
+
return (0, node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
110
|
+
}
|
|
111
|
+
async function appendToManifest(manifestPath, entry) {
|
|
112
|
+
const existing = JSON.parse(await (0, node_fs_promises.readFile)(manifestPath, "utf8").catch(() => "{\"entries\":[]}"));
|
|
113
|
+
existing.entries.push(entry);
|
|
114
|
+
await (0, node_fs_promises.writeFile)(manifestPath, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
|
|
115
|
+
}
|
|
116
|
+
const EvidenceStore = {
|
|
117
|
+
async append(caseId, input) {
|
|
118
|
+
const dir = caseDir(caseId);
|
|
119
|
+
const evidenceDir = node_path.default.join(dir, "evidence");
|
|
120
|
+
await (0, node_fs_promises.mkdir)(evidenceDir, { recursive: true });
|
|
121
|
+
const safeSource = sanitizeSource(input.source);
|
|
122
|
+
const timestamp = formatTimestamp();
|
|
123
|
+
let seq = 1;
|
|
124
|
+
try {
|
|
125
|
+
seq = (await (0, node_fs_promises.readdir)(evidenceDir)).filter((f) => f.endsWith(".md")).length + 1;
|
|
126
|
+
} catch {
|
|
127
|
+
seq = 1;
|
|
128
|
+
}
|
|
129
|
+
const seqStr = String(seq).padStart(3, "0");
|
|
130
|
+
let filename = `${seqStr}_${safeSource}_${timestamp}.md`;
|
|
131
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
132
|
+
const fm = {
|
|
133
|
+
id: `${caseId}_ev${seqStr}`,
|
|
134
|
+
caseId,
|
|
135
|
+
source: input.source,
|
|
136
|
+
timestamp: now,
|
|
137
|
+
queryParams: input.queryParams
|
|
138
|
+
};
|
|
139
|
+
const formattedContent = await formatEvidenceContent(`${caseId}_ev${seqStr}`, input.source, timestamp, input.content);
|
|
140
|
+
const fileContent = require_frontmatter.serializeFrontmatter(fm, [
|
|
141
|
+
`## Evidence: ${input.source}`,
|
|
142
|
+
"",
|
|
143
|
+
`**Source:** ${input.source}`,
|
|
144
|
+
`**Captured:** ${now}`,
|
|
145
|
+
"",
|
|
146
|
+
formattedContent,
|
|
147
|
+
""
|
|
148
|
+
].join("\n"));
|
|
149
|
+
const filePath = node_path.default.join(evidenceDir, filename);
|
|
150
|
+
try {
|
|
151
|
+
await (0, node_fs_promises.writeFile)(filePath, fileContent, {
|
|
152
|
+
mode: 384,
|
|
153
|
+
flag: "wx"
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err.code === "EEXIST") {
|
|
157
|
+
filename = `${seqStr}_${safeSource}_${timestamp}_${Math.random().toString(36).slice(2, 6)}.md`;
|
|
158
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(evidenceDir, filename), fileContent, {
|
|
159
|
+
mode: 384,
|
|
160
|
+
flag: "wx"
|
|
161
|
+
});
|
|
162
|
+
} else throw err;
|
|
163
|
+
}
|
|
164
|
+
const sha256 = hashContent(fileContent);
|
|
165
|
+
await appendToManifest(node_path.default.join(dir, "manifest.json"), {
|
|
166
|
+
file: filename,
|
|
167
|
+
sha256
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
filename,
|
|
171
|
+
sha256
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
async verifyManifest(caseId) {
|
|
175
|
+
const dir = caseDir(caseId);
|
|
176
|
+
const manifestPath = node_path.default.join(dir, "manifest.json");
|
|
177
|
+
const manifest = JSON.parse(await (0, node_fs_promises.readFile)(manifestPath, "utf8").catch(() => "{\"entries\":[]}"));
|
|
178
|
+
const tampered = [];
|
|
179
|
+
for (const entry of manifest.entries) {
|
|
180
|
+
const filePath = node_path.default.join(dir, "evidence", entry.file);
|
|
181
|
+
try {
|
|
182
|
+
if (hashContent(await (0, node_fs_promises.readFile)(filePath, "utf8")) !== entry.sha256) tampered.push(entry.file);
|
|
183
|
+
} catch {
|
|
184
|
+
tampered.push(entry.file);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
ok: tampered.length === 0,
|
|
189
|
+
count: manifest.entries.length,
|
|
190
|
+
...tampered.length > 0 ? { tampered } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
Object.defineProperty(exports, "EvidenceStore", {
|
|
196
|
+
enumerable: true,
|
|
197
|
+
get: function() {
|
|
198
|
+
return EvidenceStore;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { n as serializeFrontmatter } from "./frontmatter-D8wWCeOa.mjs";
|
|
2
|
+
import { n as workspaceOutputPaths } from "./output-root-CmWM7aV2.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/cases/evidence.ts
|
|
7
|
+
const MAX_INLINE_JSON_BYTES = 8 * 1024;
|
|
8
|
+
function caseDir(caseId) {
|
|
9
|
+
return path.join(workspaceOutputPaths().casesRoot, caseId);
|
|
10
|
+
}
|
|
11
|
+
function sanitizeSource(source) {
|
|
12
|
+
return source.replace(/[^a-z0-9_-]/gi, "").slice(0, 40);
|
|
13
|
+
}
|
|
14
|
+
function formatTimestamp() {
|
|
15
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "").slice(0, 15);
|
|
16
|
+
}
|
|
17
|
+
function parseJsonContent(content) {
|
|
18
|
+
const trimmed = content.trim();
|
|
19
|
+
if (trimmed.startsWith("```")) return null;
|
|
20
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(trimmed);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function compactJsonValue(value) {
|
|
28
|
+
if (Array.isArray(value)) return value.map(compactJsonValue);
|
|
29
|
+
if (!value || typeof value !== "object") return value;
|
|
30
|
+
const compact = {};
|
|
31
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
32
|
+
if (entry === null || entry === void 0) continue;
|
|
33
|
+
compact[key] = compactJsonValue(entry);
|
|
34
|
+
}
|
|
35
|
+
return compact;
|
|
36
|
+
}
|
|
37
|
+
function summarizeJsonValue(value) {
|
|
38
|
+
if (Array.isArray(value)) return {
|
|
39
|
+
kind: "array",
|
|
40
|
+
count: value.length,
|
|
41
|
+
sample: value.slice(0, 3).map(compactJsonValue)
|
|
42
|
+
};
|
|
43
|
+
if (!value || typeof value !== "object") return {
|
|
44
|
+
kind: typeof value,
|
|
45
|
+
value
|
|
46
|
+
};
|
|
47
|
+
const record = compactJsonValue(value);
|
|
48
|
+
const summary = {
|
|
49
|
+
kind: "object",
|
|
50
|
+
keys: Object.keys(record).slice(0, 50)
|
|
51
|
+
};
|
|
52
|
+
for (const key of [
|
|
53
|
+
"schema",
|
|
54
|
+
"source",
|
|
55
|
+
"tool",
|
|
56
|
+
"network",
|
|
57
|
+
"seed_address",
|
|
58
|
+
"address"
|
|
59
|
+
]) if (typeof record[key] === "string") summary[key] = record[key];
|
|
60
|
+
for (const key of [
|
|
61
|
+
"files",
|
|
62
|
+
"outputs",
|
|
63
|
+
"facts"
|
|
64
|
+
]) {
|
|
65
|
+
const entry = record[key];
|
|
66
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) summary[key] = compactJsonValue(entry);
|
|
67
|
+
}
|
|
68
|
+
const counts = Object.fromEntries(Object.entries(record).filter(([, entry]) => Array.isArray(entry)).map(([key, entry]) => [key, entry.length]));
|
|
69
|
+
if (Object.keys(counts).length > 0) summary["array_counts"] = counts;
|
|
70
|
+
return summary;
|
|
71
|
+
}
|
|
72
|
+
async function formatEvidenceContent(evidenceId, source, timestamp, content) {
|
|
73
|
+
const parsedJson = parseJsonContent(content);
|
|
74
|
+
if (parsedJson === null) return content;
|
|
75
|
+
const compactJson = compactJsonValue(parsedJson);
|
|
76
|
+
const prettyJson = JSON.stringify(compactJson, null, 2);
|
|
77
|
+
if (Buffer.byteLength(prettyJson, "utf8") <= MAX_INLINE_JSON_BYTES) return `\`\`\`json\n${prettyJson}\n\`\`\``;
|
|
78
|
+
const paths = workspaceOutputPaths();
|
|
79
|
+
await mkdir(paths.reportTablesRoot, {
|
|
80
|
+
recursive: true,
|
|
81
|
+
mode: 448
|
|
82
|
+
});
|
|
83
|
+
const tableFilename = `${evidenceId}_${sanitizeSource(source) || "evidence"}_${timestamp}_${Math.random().toString(36).slice(2, 8)}.json`;
|
|
84
|
+
const tablePath = path.join(paths.reportTablesRoot, tableFilename);
|
|
85
|
+
await writeFile(tablePath, prettyJson + "\n", {
|
|
86
|
+
mode: 384,
|
|
87
|
+
flag: "wx"
|
|
88
|
+
});
|
|
89
|
+
const relativeTablePath = path.relative(paths.root, tablePath);
|
|
90
|
+
const summary = {
|
|
91
|
+
schema: "chain-insights.evidence_summary.v1",
|
|
92
|
+
omitted_inline_json: true,
|
|
93
|
+
stored_json: relativeTablePath,
|
|
94
|
+
summary: summarizeJsonValue(compactJson)
|
|
95
|
+
};
|
|
96
|
+
return [
|
|
97
|
+
"Large JSON evidence was stored as an analyst table extract instead of inline Markdown.",
|
|
98
|
+
"",
|
|
99
|
+
`Stored JSON: \`${relativeTablePath}\``,
|
|
100
|
+
"",
|
|
101
|
+
"```json",
|
|
102
|
+
JSON.stringify(summary, null, 2),
|
|
103
|
+
"```"
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
function hashContent(content) {
|
|
107
|
+
return createHash("sha256").update(content).digest("hex");
|
|
108
|
+
}
|
|
109
|
+
async function appendToManifest(manifestPath, entry) {
|
|
110
|
+
const existing = JSON.parse(await readFile(manifestPath, "utf8").catch(() => "{\"entries\":[]}"));
|
|
111
|
+
existing.entries.push(entry);
|
|
112
|
+
await writeFile(manifestPath, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
|
|
113
|
+
}
|
|
114
|
+
const EvidenceStore = {
|
|
115
|
+
async append(caseId, input) {
|
|
116
|
+
const dir = caseDir(caseId);
|
|
117
|
+
const evidenceDir = path.join(dir, "evidence");
|
|
118
|
+
await mkdir(evidenceDir, { recursive: true });
|
|
119
|
+
const safeSource = sanitizeSource(input.source);
|
|
120
|
+
const timestamp = formatTimestamp();
|
|
121
|
+
let seq = 1;
|
|
122
|
+
try {
|
|
123
|
+
seq = (await readdir(evidenceDir)).filter((f) => f.endsWith(".md")).length + 1;
|
|
124
|
+
} catch {
|
|
125
|
+
seq = 1;
|
|
126
|
+
}
|
|
127
|
+
const seqStr = String(seq).padStart(3, "0");
|
|
128
|
+
let filename = `${seqStr}_${safeSource}_${timestamp}.md`;
|
|
129
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
130
|
+
const fm = {
|
|
131
|
+
id: `${caseId}_ev${seqStr}`,
|
|
132
|
+
caseId,
|
|
133
|
+
source: input.source,
|
|
134
|
+
timestamp: now,
|
|
135
|
+
queryParams: input.queryParams
|
|
136
|
+
};
|
|
137
|
+
const formattedContent = await formatEvidenceContent(`${caseId}_ev${seqStr}`, input.source, timestamp, input.content);
|
|
138
|
+
const fileContent = serializeFrontmatter(fm, [
|
|
139
|
+
`## Evidence: ${input.source}`,
|
|
140
|
+
"",
|
|
141
|
+
`**Source:** ${input.source}`,
|
|
142
|
+
`**Captured:** ${now}`,
|
|
143
|
+
"",
|
|
144
|
+
formattedContent,
|
|
145
|
+
""
|
|
146
|
+
].join("\n"));
|
|
147
|
+
const filePath = path.join(evidenceDir, filename);
|
|
148
|
+
try {
|
|
149
|
+
await writeFile(filePath, fileContent, {
|
|
150
|
+
mode: 384,
|
|
151
|
+
flag: "wx"
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err.code === "EEXIST") {
|
|
155
|
+
filename = `${seqStr}_${safeSource}_${timestamp}_${Math.random().toString(36).slice(2, 6)}.md`;
|
|
156
|
+
await writeFile(path.join(evidenceDir, filename), fileContent, {
|
|
157
|
+
mode: 384,
|
|
158
|
+
flag: "wx"
|
|
159
|
+
});
|
|
160
|
+
} else throw err;
|
|
161
|
+
}
|
|
162
|
+
const sha256 = hashContent(fileContent);
|
|
163
|
+
await appendToManifest(path.join(dir, "manifest.json"), {
|
|
164
|
+
file: filename,
|
|
165
|
+
sha256
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
filename,
|
|
169
|
+
sha256
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
async verifyManifest(caseId) {
|
|
173
|
+
const dir = caseDir(caseId);
|
|
174
|
+
const manifestPath = path.join(dir, "manifest.json");
|
|
175
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8").catch(() => "{\"entries\":[]}"));
|
|
176
|
+
const tampered = [];
|
|
177
|
+
for (const entry of manifest.entries) {
|
|
178
|
+
const filePath = path.join(dir, "evidence", entry.file);
|
|
179
|
+
try {
|
|
180
|
+
if (hashContent(await readFile(filePath, "utf8")) !== entry.sha256) tampered.push(entry.file);
|
|
181
|
+
} catch {
|
|
182
|
+
tampered.push(entry.file);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
ok: tampered.length === 0,
|
|
187
|
+
count: manifest.entries.length,
|
|
188
|
+
...tampered.length > 0 ? { tampered } : {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
//#endregion
|
|
193
|
+
export { EvidenceStore as t };
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=evidence-BhvFW-y_.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evidence-BhvFW-y_.mjs","names":["e"],"sources":["../src/cases/evidence.ts"],"sourcesContent":["import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'\nimport { createHash } from 'node:crypto'\nimport path from 'node:path'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { serializeFrontmatter } from './frontmatter.js'\n\nconst MAX_INLINE_JSON_BYTES = 8 * 1024\n\nfunction caseDir(caseId: string): string {\n return path.join(workspaceOutputPaths().casesRoot, caseId)\n}\n\nfunction sanitizeSource(source: string): string {\n return source.replace(/[^a-z0-9_-]/gi, '').slice(0, 40)\n}\n\nfunction formatTimestamp(): string {\n // Returns timestamp like 20260511T142300 (no colons, no dots)\n return new Date().toISOString().replace(/[-:]/g, '').replace(/\\.\\d{3}/, '').slice(0, 15)\n}\n\nfunction parseJsonContent(content: string): unknown | null {\n const trimmed = content.trim()\n if (trimmed.startsWith('```')) return null\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null\n try {\n return JSON.parse(trimmed)\n } catch {\n return null\n }\n}\n\nfunction compactJsonValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(compactJsonValue)\n if (!value || typeof value !== 'object') return value\n\n const compact: Record<string, unknown> = {}\n for (const [key, entry] of Object.entries(value)) {\n if (entry === null || entry === undefined) continue\n compact[key] = compactJsonValue(entry)\n }\n return compact\n}\n\nfunction summarizeJsonValue(value: unknown): Record<string, unknown> {\n if (Array.isArray(value)) {\n return {\n kind: 'array',\n count: value.length,\n sample: value.slice(0, 3).map(compactJsonValue),\n }\n }\n\n if (!value || typeof value !== 'object') {\n return { kind: typeof value, value }\n }\n\n const record = compactJsonValue(value) as Record<string, unknown>\n const summary: Record<string, unknown> = {\n kind: 'object',\n keys: Object.keys(record).slice(0, 50),\n }\n for (const key of ['schema', 'source', 'tool', 'network', 'seed_address', 'address']) {\n if (typeof record[key] === 'string') summary[key] = record[key]\n }\n for (const key of ['files', 'outputs', 'facts']) {\n const entry = record[key]\n if (entry && typeof entry === 'object' && !Array.isArray(entry)) summary[key] = compactJsonValue(entry)\n }\n const counts = Object.fromEntries(\n Object.entries(record)\n .filter(([, entry]) => Array.isArray(entry))\n .map(([key, entry]) => [key, (entry as unknown[]).length]),\n )\n if (Object.keys(counts).length > 0) summary['array_counts'] = counts\n return summary\n}\n\nasync function formatEvidenceContent(\n evidenceId: string,\n source: string,\n timestamp: string,\n content: string,\n): Promise<string> {\n const parsedJson = parseJsonContent(content)\n if (parsedJson === null) return content\n\n const compactJson = compactJsonValue(parsedJson)\n const prettyJson = JSON.stringify(compactJson, null, 2)\n if (Buffer.byteLength(prettyJson, 'utf8') <= MAX_INLINE_JSON_BYTES) {\n return `\\`\\`\\`json\\n${prettyJson}\\n\\`\\`\\``\n }\n\n const paths = workspaceOutputPaths()\n await mkdir(paths.reportTablesRoot, { recursive: true, mode: 0o700 })\n const safeSource = sanitizeSource(source) || 'evidence'\n const tableFilename = `${evidenceId}_${safeSource}_${timestamp}_${Math.random().toString(36).slice(2, 8)}.json`\n const tablePath = path.join(paths.reportTablesRoot, tableFilename)\n await writeFile(tablePath, prettyJson + '\\n', { mode: 0o600, flag: 'wx' })\n const relativeTablePath = path.relative(paths.root, tablePath)\n const summary = {\n schema: 'chain-insights.evidence_summary.v1',\n omitted_inline_json: true,\n stored_json: relativeTablePath,\n summary: summarizeJsonValue(compactJson),\n }\n return [\n 'Large JSON evidence was stored as an analyst table extract instead of inline Markdown.',\n '',\n `Stored JSON: \\`${relativeTablePath}\\``,\n '',\n '```json',\n JSON.stringify(summary, null, 2),\n '```',\n ].join('\\n')\n}\n\nexport function hashContent(content: string): string {\n return createHash('sha256').update(content).digest('hex')\n}\n\nasync function appendToManifest(\n manifestPath: string,\n entry: { file: string; sha256: string }\n): Promise<void> {\n const existing = JSON.parse(\n await readFile(manifestPath, 'utf8').catch(() => '{\"entries\":[]}')\n ) as { caseId?: string; entries: Array<{ file: string; sha256: string }> }\n existing.entries.push(entry)\n await writeFile(manifestPath, JSON.stringify(existing, null, 2) + '\\n', { mode: 0o600 })\n}\n\nexport const EvidenceStore = {\n async append(\n caseId: string,\n input: { source: string; content: string; queryParams: string }\n ): Promise<{ filename: string; sha256: string }> {\n const dir = caseDir(caseId)\n const evidenceDir = path.join(dir, 'evidence')\n await mkdir(evidenceDir, { recursive: true })\n const safeSource = sanitizeSource(input.source)\n const timestamp = formatTimestamp()\n\n // Determine sequence number\n let seq = 1\n try {\n const files = await readdir(evidenceDir)\n const evidenceFiles = files.filter(f => f.endsWith('.md'))\n seq = evidenceFiles.length + 1\n } catch {\n seq = 1\n }\n const seqStr = String(seq).padStart(3, '0')\n let filename = `${seqStr}_${safeSource}_${timestamp}.md`\n\n // Build file content\n const now = new Date().toISOString()\n const fm: Record<string, string> = {\n id: `${caseId}_ev${seqStr}`,\n caseId,\n source: input.source,\n timestamp: now,\n queryParams: input.queryParams,\n }\n const evidenceId = `${caseId}_ev${seqStr}`\n const formattedContent = await formatEvidenceContent(evidenceId, input.source, timestamp, input.content)\n const body = [\n `## Evidence: ${input.source}`,\n '',\n `**Source:** ${input.source}`,\n `**Captured:** ${now}`,\n '',\n formattedContent,\n '',\n ].join('\\n')\n const fileContent = serializeFrontmatter(fm, body)\n\n // Write with exclusive flag to prevent sequence collision (Pitfall 4)\n const filePath = path.join(evidenceDir, filename)\n try {\n await writeFile(filePath, fileContent, { mode: 0o600, flag: 'wx' })\n } catch (err: unknown) {\n const e = err as NodeJS.ErrnoException\n if (e.code === 'EEXIST') {\n // Retry with timestamp-unique suffix\n filename = `${seqStr}_${safeSource}_${timestamp}_${Math.random().toString(36).slice(2, 6)}.md`\n await writeFile(path.join(evidenceDir, filename), fileContent, { mode: 0o600, flag: 'wx' })\n } else {\n throw err\n }\n }\n\n // Compute SHA-256 of written content and append to manifest\n const sha256 = hashContent(fileContent)\n await appendToManifest(path.join(dir, 'manifest.json'), { file: filename, sha256 })\n\n return { filename, sha256 }\n },\n\n async verifyManifest(caseId: string): Promise<{ ok: boolean; count: number; tampered?: string[] }> {\n const dir = caseDir(caseId)\n const manifestPath = path.join(dir, 'manifest.json')\n const manifest = JSON.parse(\n await readFile(manifestPath, 'utf8').catch(() => '{\"entries\":[]}')\n ) as { entries: Array<{ file: string; sha256: string }> }\n\n const tampered: string[] = []\n for (const entry of manifest.entries) {\n const filePath = path.join(dir, 'evidence', entry.file)\n try {\n const content = await readFile(filePath, 'utf8')\n const actual = hashContent(content)\n if (actual !== entry.sha256) {\n tampered.push(entry.file)\n }\n } catch {\n tampered.push(entry.file) // File missing = tampered\n }\n }\n\n return {\n ok: tampered.length === 0,\n count: manifest.entries.length,\n ...(tampered.length > 0 ? { tampered } : {}),\n }\n },\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/mcp/format.ts
|
|
2
|
+
const NAME_WIDTH = 30;
|
|
3
|
+
const DESC_MAX = 60;
|
|
4
|
+
/**
|
|
5
|
+
* Formats an array of MCP tools as a plain text table string.
|
|
6
|
+
* Returns "No tools available." for an empty array.
|
|
7
|
+
* Caller controls output — use console.log(formatToolsTable(tools)).
|
|
8
|
+
*/
|
|
9
|
+
function formatToolsTable(tools) {
|
|
10
|
+
if (tools.length === 0) return "No tools available.";
|
|
11
|
+
return [
|
|
12
|
+
`${"Tool".padEnd(NAME_WIDTH)} Description`,
|
|
13
|
+
"-".repeat(NAME_WIDTH) + " " + "-".repeat(DESC_MAX),
|
|
14
|
+
...tools.map((t) => {
|
|
15
|
+
return `${t.name.padEnd(NAME_WIDTH)} ${(t.description ?? "").slice(0, DESC_MAX)}`;
|
|
16
|
+
})
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { formatToolsTable };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=format-Ce1RObVl.mjs.map
|