File size: 9,356 Bytes
93c7565 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | // prover/auditor.js
// ProofBridge Liner — Ghost‑Risk Audit Engine
//
// Consumes prover state, fetches deed content, extracts text from PDFs,
// submits forensic prompts to DeepSeek‑V4‑Pro via NVIDIA NIM API,
// and generates issuer‑ready audit reports.
//
// Usage:
// npm run audit after npm run fetch
// node prover/auditor.js directly
// ---------------------------------------------------------------------------
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");
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
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");
// IPFS gateways to try (fallback if the first fails)
const GATEWAYS = [
"https://ipfs.io",
"https://cloudflare-ipfs.com",
"https://dweb.link",
];
// ---------------------------------------------------------------------------
// PDF text extraction using pdf-parse
// ---------------------------------------------------------------------------
async function extractTextFromPDF(buffer) {
try {
// Validate PDF format
const str = buffer.toString("utf-8");
if (!str.startsWith("%PDF-")) {
throw new Error("Not a valid PDF");
}
// Use pdf-parse for proper text extraction
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}]`;
}
}
// ---------------------------------------------------------------------------
// IPFS fetch with gateway fallback
// ---------------------------------------------------------------------------
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);
});
}
// ---------------------------------------------------------------------------
// Caching layer
// ---------------------------------------------------------------------------
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");
}
// ---------------------------------------------------------------------------
// NVIDIA NIM API call (OpenAI‑compatible endpoint)
// ---------------------------------------------------------------------------
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();
});
}
// ---------------------------------------------------------------------------
// Forensic prompt template
// ---------------------------------------------------------------------------
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)}`;
}
// ---------------------------------------------------------------------------
// Main audit routine
// ---------------------------------------------------------------------------
async function runAudit() {
console.log("[auditor] Starting ghost‑risk audit...");
// 1. Load prover state
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.");
}
// 2. Prepare report
let report = `# ProofBridge Liner - Ghost-Risk Audit Report\n\n`;
report += `Generated: ${new Date().toISOString()}\n\n`;
// 3. Process each asset
for (const result of proverState.results) {
const { assetId, ipfsCid, expectedHash, actualHash, status } = result;
console.log(`[auditor] Processing asset ${assetId} (${status})...`);
// Get deed text (cached or fetch)
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}]`;
}
}
// AI analysis
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._";
}
// Append to report
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`;
}
// 4. Write report
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}`);
// Notify completion
try {
notifier.auditComplete({
assetsAnalyzed: proverState.results.length,
reportPath: AUDIT_REPORT,
aiUsed: !!NVIDIA_API_KEY,
});
} catch (_) {}
}
// ---------------------------------------------------------------------------
// CLI entry point
// ---------------------------------------------------------------------------
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);
}); |