| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const fs = require("node:fs"); |
| const path = require("node:path"); |
| const crypto = require("node:crypto"); |
| const https = require("node:https"); |
| const pdfParse = require("pdf-parse"); |
| const notifier = require("./notifier"); |
|
|
| |
| |
| |
| const NVIDIA_API_KEY = process.env.NVIDIA_API_KEY; |
| const NVIDIA_API_BASE = "https://integrate.api.nvidia.com/v1"; |
| const MODEL = "deepseek-ai/deepseek-v4-pro"; |
|
|
| const STATE_FILE = path.resolve( |
| __dirname, |
| "..", |
| ".local", |
| "state", |
| "prover-state.json" |
| ); |
| const AUDIT_REPORT = path.resolve(__dirname, "..", "demo", "audit-realT.md"); |
| const CACHE_DIR = path.resolve(__dirname, "..", ".local", "state", "cache"); |
|
|
| |
| const GATEWAYS = [ |
| "https://ipfs.io", |
| "https://cloudflare-ipfs.com", |
| "https://dweb.link", |
| ]; |
|
|
| |
| |
| |
| async function extractTextFromPDF(buffer) { |
| try { |
| |
| const str = buffer.toString("utf-8"); |
| if (!str.startsWith("%PDF-")) { |
| throw new Error("Not a valid PDF"); |
| } |
|
|
| |
| const data = await pdfParse(buffer); |
|
|
| if (data.text && data.text.trim().length > 0) { |
| return data.text.trim(); |
| } else { |
| return "[No extractable text - PDF may be image-based]"; |
| } |
| } catch (error) { |
| console.error(`[auditor] PDF extraction error: ${error.message}`); |
| return `[PDF extraction failed: ${error.message}]`; |
| } |
| } |
|
|
| |
| |
| |
| async function fetchFromIPFS(cid) { |
| for (const gateway of GATEWAYS) { |
| const url = gateway + '/ipfs/' + cid; |
| try { |
| const buffer = await httpGet(url); |
| return buffer; |
| } catch (err) { |
| console.warn(`[auditor] Gateway ${gateway} failed for ${cid}: ${err.message}`); |
| } |
| } |
| throw new Error(`All gateways failed for CID ${cid}`); |
| } |
|
|
| function httpGet(url) { |
| return new Promise((resolve, reject) => { |
| https |
| .get(url, (res) => { |
| if (res.statusCode !== 200) { |
| reject(new Error(`HTTP ${res.statusCode}`)); |
| return; |
| } |
| const chunks = []; |
| res.on("data", (chunk) => chunks.push(chunk)); |
| res.on("end", () => resolve(Buffer.concat(chunks))); |
| }) |
| .on("error", reject); |
| }); |
| } |
|
|
| |
| |
| |
| function getCachedText(cid) { |
| const cacheFile = path.join(CACHE_DIR, `${cid}.txt`); |
| if (fs.existsSync(cacheFile)) { |
| return fs.readFileSync(cacheFile, "utf-8"); |
| } |
| return null; |
| } |
|
|
| function setCachedText(cid, text) { |
| if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true }); |
| fs.writeFileSync(path.join(CACHE_DIR, `${cid}.txt`), text, "utf-8"); |
| } |
|
|
| |
| |
| |
| async function callNVIDIA(prompt, options = {}) { |
| if (!NVIDIA_API_KEY) throw new Error("NVIDIA_API_KEY is not set"); |
|
|
| const body = JSON.stringify({ |
| model: MODEL, |
| messages: [ |
| { |
| role: "user", |
| content: prompt, |
| }, |
| ], |
| max_tokens: options.max_tokens || 2000, |
| temperature: options.temperature || 0.1, |
| stream: false, |
| }); |
|
|
| return new Promise((resolve, reject) => { |
| const req = https.request( |
| `${NVIDIA_API_BASE}/chat/completions`, |
| { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| "Authorization": `Bearer ${NVIDIA_API_KEY}`, |
| }, |
| }, |
| (res) => { |
| let data = ""; |
| res.on("data", (chunk) => (data += chunk)); |
| res.on("end", () => { |
| if (res.statusCode !== 200) { |
| reject(new Error(`NVIDIA API error ${res.statusCode}: ${data}`)); |
| return; |
| } |
| try { |
| const parsed = JSON.parse(data); |
| resolve(parsed.choices[0].message.content); |
| } catch (e) { |
| reject(e); |
| } |
| }); |
| } |
| ); |
| req.on("error", reject); |
| req.write(body); |
| req.end(); |
| }); |
| } |
|
|
| |
| |
| |
| function buildPrompt(assetId, expectedHash, actualHash, status, deedText) { |
| return `You are a forensic auditor. Analyze this property deed document text and compare it to the expected cryptographic hash: ${expectedHash}. |
| |
| The current status is: ${status} (fresh means hash matches, mismatch means potential tampering). |
| |
| Highlight any discrepant clauses, legal issues, or irregularities that could affect tokenization or RWA security. Focus on property description, vesting, encumbrances, and title validity. |
| |
| Produce a two-paragraph summary suitable for an issuer’s legal team, including recommendations if issues are found. |
| |
| Document text: |
| ${deedText.slice(0, 8000)}`; |
| } |
|
|
| |
| |
| |
| async function runAudit() { |
| console.log("[auditor] Starting ghost‑risk audit..."); |
|
|
| |
| if (!fs.existsSync(STATE_FILE)) { |
| throw new Error("Prover state file not found. Run fetcher first."); |
| } |
| const proverState = JSON.parse(fs.readFileSync(STATE_FILE, "utf-8")); |
| if ( |
| !proverState.results || |
| !Array.isArray(proverState.results) || |
| proverState.results.length === 0 |
| ) { |
| throw new Error("No prover results. Run fetcher first."); |
| } |
|
|
| |
| let report = `# ProofBridge Liner - Ghost-Risk Audit Report\n\n`; |
| report += `Generated: ${new Date().toISOString()}\n\n`; |
|
|
| |
| for (const result of proverState.results) { |
| const { assetId, ipfsCid, expectedHash, actualHash, status } = result; |
| console.log(`[auditor] Processing asset ${assetId} (${status})...`); |
|
|
| |
| let deedText = getCachedText(ipfsCid); |
| if (!deedText) { |
| try { |
| const pdfBuffer = await fetchFromIPFS(ipfsCid); |
| deedText = await extractTextFromPDF(pdfBuffer); |
| setCachedText(ipfsCid, deedText); |
| } catch (err) { |
| console.error( |
| `[auditor] Failed to fetch/extract text for ${ipfsCid}: ${err.message}` |
| ); |
| deedText = `[Error: ${err.message}]`; |
| } |
| } |
|
|
| |
| let aiAnalysis = ""; |
| if (NVIDIA_API_KEY && deedText && deedText.length > 10 && !deedText.startsWith("[")) { |
| const prompt = buildPrompt( |
| assetId, |
| expectedHash, |
| actualHash, |
| status, |
| deedText |
| ); |
| try { |
| aiAnalysis = await callNVIDIA(prompt); |
| } catch (err) { |
| console.error(`[auditor] AI call failed for ${assetId}: ${err.message}`); |
| aiAnalysis = "_AI analysis unavailable due to API error._"; |
| } |
| } else { |
| aiAnalysis = "_AI analysis skipped: missing API key or no extractable text._"; |
| } |
|
|
| |
| report += `## Asset ID: ${assetId}\n\n`; |
| report += `**Status:** ${status}\n`; |
| report += `**Expected Hash:** ${expectedHash}\n`; |
| report += `**Actual Hash:** ${actualHash}\n`; |
| report += `**IPFS CID:** ${ipfsCid}\n\n`; |
| report += `${aiAnalysis}\n\n`; |
| } |
|
|
| |
| const reportDir = path.dirname(AUDIT_REPORT); |
| if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true }); |
| fs.writeFileSync(AUDIT_REPORT, report, "utf-8"); |
| console.log(`[auditor] Report written to ${AUDIT_REPORT}`); |
|
|
| |
| try { |
| notifier.auditComplete({ |
| assetsAnalyzed: proverState.results.length, |
| reportPath: AUDIT_REPORT, |
| aiUsed: !!NVIDIA_API_KEY, |
| }); |
| } catch (_) {} |
| } |
|
|
| |
| |
| |
| runAudit() |
| .then(() => process.exit(0)) |
| .catch((err) => { |
| try { notifier.systemError({ error: err.message, component: 'auditor' }); } catch (_) {} |
| console.error(`[auditor] Fatal: ${err.message}`); |
| process.exit(1); |
| }); |