Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 rUv
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
{
"name": "@ruvector/edge",
"version": "0.1.9",
"type": "module",
"description": "Free edge-based AI swarms in the browser - P2P, crypto, vector search, neural networks. Install @ruvector/edge-full for graph DB, SQL, ONNX embeddings.",
"main": "ruvector_edge.js",
"module": "ruvector_edge.js",
"types": "ruvector_edge.d.ts",
"keywords": [
"wasm",
"rust",
"ai",
"swarm",
"p2p",
"cryptography",
"post-quantum",
"hnsw",
"vector-search",
"neural-network",
"raft",
"consensus",
"ed25519",
"aes-gcm",
"webassembly",
"semantic-search",
"machine-learning"
],
"author": "RuVector Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector"
},
"homepage": "https://github.com/ruvnet/ruvector/tree/main/examples/edge",
"bugs": {
"url": "https://github.com/ruvnet/ruvector/issues"
},
"files": [
"ruvector_edge_bg.wasm",
"ruvector_edge.js",
"ruvector_edge.d.ts",
"ruvector_edge_bg.wasm.d.ts",
"worker.js",
"worker-pool.js",
"generator.html",
"LICENSE"
],
"exports": {
".": {
"import": "./ruvector_edge.js",
"types": "./ruvector_edge.d.ts"
},
"./worker": {
"import": "./worker.js"
},
"./worker-pool": {
"import": "./worker-pool.js"
}
},
"sideEffects": [
"./snippets/*"
],
"optionalDependencies": {
"@ruvector/edge-full": "^0.1.0"
},
"peerDependencies": {
"@ruvector/edge-full": "^0.1.0"
},
"peerDependenciesMeta": {
"@ruvector/edge-full": {
"optional": true
}
}
}

View File

@@ -0,0 +1,795 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plaid Local Learning Demo - RuVector Edge</title>
<style>
:root {
--bg: #0a0a0f;
--card: #12121a;
--border: #2a2a3a;
--text: #e0e0e8;
--text-dim: #8888a0;
--accent: #6366f1;
--accent-glow: rgba(99, 102, 241, 0.3);
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 3rem;
}
h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, var(--accent), #a855f7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-dim);
font-size: 1.1rem;
}
.privacy-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: var(--success);
padding: 0.5rem 1rem;
border-radius: 2rem;
margin-top: 1rem;
font-size: 0.9rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 1.5rem;
}
.card h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat {
background: rgba(99, 102, 241, 0.05);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--accent);
}
.stat-label {
font-size: 0.8rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
button {
background: var(--accent);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 20px var(--accent-glow);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
button.secondary {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
}
button.danger {
background: var(--error);
}
.patterns-list {
max-height: 300px;
overflow-y: auto;
}
.pattern-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid var(--border);
}
.pattern-item:last-child {
border-bottom: none;
}
.pattern-category {
font-weight: 500;
}
.pattern-amount {
color: var(--accent);
font-weight: 600;
}
.confidence-bar {
width: 60px;
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
background: var(--accent);
transition: width 0.3s;
}
.heatmap {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-top: 1rem;
}
.heatmap-cell {
aspect-ratio: 1;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: var(--text-dim);
}
.heatmap-label {
font-size: 0.7rem;
color: var(--text-dim);
text-align: center;
margin-top: 0.25rem;
}
.transaction-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
input, select {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: 0.75rem;
border-radius: 0.5rem;
font-size: 1rem;
width: 100%;
}
input:focus, select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
label {
display: block;
font-size: 0.9rem;
color: var(--text-dim);
margin-bottom: 0.25rem;
}
.result-card {
background: rgba(99, 102, 241, 0.05);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.result-card.anomaly {
background: rgba(239, 68, 68, 0.05);
border-color: rgba(239, 68, 68, 0.3);
}
.result-card.normal {
background: rgba(34, 197, 94, 0.05);
border-color: rgba(34, 197, 94, 0.3);
}
.log {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
max-height: 200px;
overflow-y: auto;
}
.log-entry {
padding: 0.25rem 0;
border-bottom: 1px solid var(--border);
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--text-dim);
}
.log-success {
color: var(--success);
}
.log-info {
color: var(--accent);
}
footer {
text-align: center;
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border);
color: var(--text-dim);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading {
animation: pulse 1.5s infinite;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🧠 Plaid Local Learning</h1>
<p class="subtitle">Privacy-preserving financial intelligence powered by RuVector Edge</p>
<div class="privacy-badge">
🔒 100% Browser-Local • No Data Leaves Your Device
</div>
</header>
<div class="grid">
<!-- Stats Card -->
<div class="card">
<h2>📊 Learning Statistics</h2>
<div class="stats-grid">
<div class="stat">
<div class="stat-value" id="stat-patterns">0</div>
<div class="stat-label">Patterns Learned</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-version">0</div>
<div class="stat-label">Learning Version</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-index">0</div>
<div class="stat-label">Index Size</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-qvalues">0</div>
<div class="stat-label">Q-Values</div>
</div>
</div>
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
<button id="btn-init" onclick="initLearner()">
⚡ Initialize
</button>
<button class="secondary" onclick="refreshStats()">
🔄 Refresh
</button>
</div>
</div>
<!-- Patterns Card -->
<div class="card">
<h2>🎯 Learned Spending Patterns</h2>
<div class="patterns-list" id="patterns-list">
<p style="color: var(--text-dim); text-align: center; padding: 2rem;">
Process transactions to learn patterns
</p>
</div>
</div>
<!-- Transaction Input -->
<div class="card">
<h2>💳 Test Transaction</h2>
<div class="transaction-form">
<div class="form-row">
<div>
<label>Amount ($)</label>
<input type="number" id="tx-amount" value="45.99" step="0.01">
</div>
<div>
<label>Date</label>
<input type="date" id="tx-date" value="2024-03-15">
</div>
</div>
<div>
<label>Merchant Name</label>
<input type="text" id="tx-merchant" value="Starbucks">
</div>
<div>
<label>Category</label>
<select id="tx-category">
<option value="Food and Drink">Food and Drink</option>
<option value="Shopping">Shopping</option>
<option value="Transportation">Transportation</option>
<option value="Entertainment">Entertainment</option>
<option value="Bills">Bills</option>
<option value="Healthcare">Healthcare</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem;">
<button onclick="analyzeTransaction()">
🔍 Analyze
</button>
<button class="secondary" onclick="addToLearning()">
Add & Learn
</button>
</div>
</div>
<div id="analysis-result"></div>
</div>
<!-- Temporal Heatmap -->
<div class="card">
<h2>📅 Spending Heatmap</h2>
<p style="color: var(--text-dim); font-size: 0.9rem; margin-bottom: 1rem;">
Day-of-week spending patterns (learned from your transactions)
</p>
<div class="heatmap" id="heatmap">
<!-- Generated by JS -->
</div>
<div class="heatmap-label">Sun → Sat</div>
</div>
<!-- Sample Data -->
<div class="card">
<h2>📦 Load Sample Data</h2>
<p style="color: var(--text-dim); margin-bottom: 1rem;">
Load sample transactions to see the learning in action.
</p>
<button onclick="loadSampleData()">
📥 Load 50 Sample Transactions
</button>
<div style="margin-top: 1rem;">
<button class="danger" onclick="clearAllData()">
🗑️ Clear All Data
</button>
</div>
</div>
<!-- Activity Log -->
<div class="card">
<h2>📝 Activity Log</h2>
<div class="log" id="activity-log">
<div class="log-entry">
<span class="log-time">[--:--:--]</span>
<span class="log-info">Ready to initialize...</span>
</div>
</div>
</div>
</div>
<footer>
<p>Powered by <strong>RuVector Edge</strong> • WASM-based ML • Zero server dependencies</p>
<p style="margin-top: 0.5rem; font-size: 0.85rem;">
Your financial data never leaves this browser. All learning happens locally.
</p>
</footer>
</div>
<script type="module">
import init, {
PlaidLocalLearner,
WasmHnswIndex,
WasmSpikingNetwork,
} from './ruvector_edge.js';
// Global state
let learner = null;
let isInitialized = false;
// Make functions available globally
window.initLearner = initLearner;
window.refreshStats = refreshStats;
window.analyzeTransaction = analyzeTransaction;
window.addToLearning = addToLearning;
window.loadSampleData = loadSampleData;
window.clearAllData = clearAllData;
// Logging helper
function log(message, type = 'info') {
const logEl = document.getElementById('activity-log');
const time = new Date().toLocaleTimeString();
const typeClass = type === 'success' ? 'log-success' : 'log-info';
logEl.innerHTML = `
<div class="log-entry">
<span class="log-time">[${time}]</span>
<span class="${typeClass}">${message}</span>
</div>
` + logEl.innerHTML;
}
// Initialize the learner
async function initLearner() {
const btn = document.getElementById('btn-init');
btn.disabled = true;
btn.innerHTML = '<span class="loading">⏳ Loading WASM...</span>';
try {
await init();
log('WASM module loaded');
// Create learner instance
learner = new PlaidLocalLearner();
log('PlaidLocalLearner created');
// Try to load existing state from IndexedDB
try {
const stateJson = localStorage.getItem('plaid_learner_state');
if (stateJson) {
learner.loadState(stateJson);
log('Previous learning state restored', 'success');
}
} catch (e) {
log('Starting with fresh state');
}
isInitialized = true;
btn.innerHTML = '✅ Initialized';
btn.style.background = 'var(--success)';
refreshStats();
updateHeatmap();
} catch (error) {
console.error(error);
log(`Error: ${error.message}`, 'error');
btn.innerHTML = '❌ Error';
btn.disabled = false;
}
}
// Refresh statistics display
function refreshStats() {
if (!isInitialized) return;
try {
const stats = learner.getStats();
document.getElementById('stat-patterns').textContent = stats.patterns_count;
document.getElementById('stat-version').textContent = stats.version;
document.getElementById('stat-index').textContent = stats.index_size;
document.getElementById('stat-qvalues').textContent = stats.q_values_count;
// Update patterns list
const patterns = learner.getPatternsSummary();
const listEl = document.getElementById('patterns-list');
if (patterns.length === 0) {
listEl.innerHTML = `
<p style="color: var(--text-dim); text-align: center; padding: 2rem;">
Process transactions to learn patterns
</p>
`;
} else {
listEl.innerHTML = patterns.map(p => `
<div class="pattern-item">
<div>
<span class="pattern-category">${p.category}</span>
<div style="font-size: 0.8rem; color: var(--text-dim);">
${p.frequency_days.toFixed(0)} day avg frequency
</div>
</div>
<div style="text-align: right;">
<span class="pattern-amount">$${p.avg_amount.toFixed(2)}</span>
<div class="confidence-bar">
<div class="confidence-fill" style="width: ${p.confidence * 100}%"></div>
</div>
</div>
</div>
`).join('');
}
log('Stats refreshed');
} catch (e) {
log(`Error refreshing stats: ${e.message}`);
}
}
// Update heatmap visualization
function updateHeatmap() {
if (!isInitialized) return;
try {
const heatmap = learner.getTemporalHeatmap();
const maxVal = Math.max(...heatmap.day_of_week, 1);
const heatmapEl = document.getElementById('heatmap');
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
heatmapEl.innerHTML = heatmap.day_of_week.map((val, i) => {
const intensity = val / maxVal;
const color = `rgba(99, 102, 241, ${0.1 + intensity * 0.9})`;
return `
<div class="heatmap-cell" style="background: ${color}">
${days[i]}
</div>
`;
}).join('');
} catch (e) {
console.error('Heatmap error:', e);
}
}
// Create transaction object from form
function getTransactionFromForm() {
return {
transaction_id: 'tx_' + Date.now(),
account_id: 'acc_demo',
amount: parseFloat(document.getElementById('tx-amount').value),
date: document.getElementById('tx-date').value,
name: document.getElementById('tx-merchant').value,
merchant_name: document.getElementById('tx-merchant').value,
category: [document.getElementById('tx-category').value],
pending: false,
payment_channel: 'online',
};
}
// Analyze a single transaction
function analyzeTransaction() {
if (!isInitialized) {
log('Please initialize first');
return;
}
const tx = getTransactionFromForm();
const txJson = JSON.stringify(tx);
try {
// Detect anomaly
const anomaly = learner.detectAnomaly(txJson);
// Predict category
const prediction = learner.predictCategory(txJson);
// Get budget recommendation
const budget = learner.getBudgetRecommendation(
tx.category[0],
tx.amount,
200 // Default budget
);
const resultEl = document.getElementById('analysis-result');
const isAnomaly = anomaly.is_anomaly;
resultEl.innerHTML = `
<div class="result-card ${isAnomaly ? 'anomaly' : 'normal'}">
<h3 style="margin-bottom: 0.5rem;">
${isAnomaly ? '⚠️ Anomaly Detected' : '✅ Normal Transaction'}
</h3>
<p style="margin-bottom: 0.5rem;">${anomaly.reason}</p>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1rem;">
<div>
<div style="font-size: 0.8rem; color: var(--text-dim);">Anomaly Score</div>
<div style="font-weight: 600;">${anomaly.anomaly_score.toFixed(2)}</div>
</div>
<div>
<div style="font-size: 0.8rem; color: var(--text-dim);">Expected Amount</div>
<div style="font-weight: 600;">$${anomaly.expected_amount.toFixed(2)}</div>
</div>
<div>
<div style="font-size: 0.8rem; color: var(--text-dim);">Trend</div>
<div style="font-weight: 600;">${budget.trend}</div>
</div>
</div>
</div>
`;
log(`Analyzed: ${tx.merchant_name} - $${tx.amount}`, 'success');
} catch (e) {
log(`Analysis error: ${e.message}`);
}
}
// Add transaction to learning
function addToLearning() {
if (!isInitialized) {
log('Please initialize first');
return;
}
const tx = getTransactionFromForm();
try {
const insights = learner.processTransactions(JSON.stringify([tx]));
log(`Learned from transaction: ${tx.merchant_name}`, 'success');
// Save state
const stateJson = learner.saveState();
localStorage.setItem('plaid_learner_state', stateJson);
refreshStats();
updateHeatmap();
} catch (e) {
log(`Learning error: ${e.message}`);
}
}
// Load sample transactions
function loadSampleData() {
if (!isInitialized) {
log('Please initialize first');
return;
}
const categories = ['Food and Drink', 'Shopping', 'Transportation', 'Entertainment', 'Bills'];
const merchants = {
'Food and Drink': ['Starbucks', 'Chipotle', 'Whole Foods', 'McDonalds', 'Subway'],
'Shopping': ['Amazon', 'Target', 'Walmart', 'Best Buy', 'Nike'],
'Transportation': ['Uber', 'Lyft', 'Shell Gas', 'Metro', 'Parking'],
'Entertainment': ['Netflix', 'Spotify', 'AMC Theaters', 'Steam', 'Apple TV'],
'Bills': ['Electric Co', 'Water Utility', 'Internet Provider', 'Phone Bill', 'Insurance'],
};
const amounts = {
'Food and Drink': [5, 50],
'Shopping': [10, 200],
'Transportation': [5, 80],
'Entertainment': [10, 50],
'Bills': [50, 300],
};
const transactions = [];
const today = new Date();
for (let i = 0; i < 50; i++) {
const category = categories[Math.floor(Math.random() * categories.length)];
const merchant = merchants[category][Math.floor(Math.random() * 5)];
const [min, max] = amounts[category];
const amount = min + Math.random() * (max - min);
const date = new Date(today);
date.setDate(date.getDate() - Math.floor(Math.random() * 90));
transactions.push({
transaction_id: `tx_sample_${i}`,
account_id: 'acc_demo',
amount: parseFloat(amount.toFixed(2)),
date: date.toISOString().split('T')[0],
name: merchant,
merchant_name: merchant,
category: [category],
pending: false,
payment_channel: 'online',
});
}
try {
const insights = learner.processTransactions(JSON.stringify(transactions));
log(`Loaded ${insights.transactions_processed} sample transactions`, 'success');
// Save state
const stateJson = learner.saveState();
localStorage.setItem('plaid_learner_state', stateJson);
refreshStats();
updateHeatmap();
} catch (e) {
log(`Error loading sample data: ${e.message}`);
}
}
// Clear all data
function clearAllData() {
if (!confirm('This will delete all learned patterns. Are you sure?')) return;
if (isInitialized) {
learner.clear();
}
localStorage.removeItem('plaid_learner_state');
// Clear IndexedDB
indexedDB.deleteDatabase('plaid_local_learning');
log('All data cleared', 'success');
refreshStats();
updateHeatmap();
document.getElementById('analysis-result').innerHTML = '';
}
// Auto-initialize on page load
window.addEventListener('DOMContentLoaded', () => {
log('Page loaded. Click Initialize to start.');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,755 @@
/**
* Plaid Local Learning System
*
* A privacy-preserving financial learning system that runs entirely in the browser.
* No financial data, learning patterns, or AI models ever leave the client device.
*
* ## Architecture
*
* ```
* ┌─────────────────────────────────────────────────────────────────────┐
* │ BROWSER (All Data Stays Here) │
* │ │
* │ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
* │ │ Plaid Link │────▶│ Transaction │────▶│ Local Learning │ │
* │ │ (OAuth) │ │ Processor │ │ Engine (WASM) │ │
* │ └─────────────┘ └──────────────┘ └───────────────────┘ │
* │ │ │ │ │
* │ ▼ ▼ ▼ │
* │ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
* │ │ IndexedDB │ │ IndexedDB │ │ IndexedDB │ │
* │ │ (Tokens) │ │ (Embeddings) │ │ (Q-Values) │ │
* │ └─────────────┘ └──────────────┘ └───────────────────┘ │
* │ │
* │ ┌─────────────────────────────────────────────────────────────┐ │
* │ │ RuVector WASM Engine │ │
* │ │ • HNSW Vector Index (150x faster similarity search) │ │
* │ │ • Spiking Neural Network (temporal pattern learning) │ │
* │ │ • Q-Learning (spending optimization) │ │
* │ │ • LSH (semantic categorization) │ │
* │ └─────────────────────────────────────────────────────────────┘ │
* └─────────────────────────────────────────────────────────────────────┘
* ```
*
* ## Privacy Guarantees
*
* 1. Financial data NEVER leaves the browser
* 2. Learning happens 100% client-side in WASM
* 3. Optional encryption for IndexedDB storage
* 4. No analytics, telemetry, or tracking
* 5. User can delete all data instantly
*
* @example
* ```typescript
* import { PlaidLocalLearner } from './plaid-local-learner';
*
* const learner = new PlaidLocalLearner();
* await learner.init();
*
* // Process transactions (stays in browser)
* const insights = await learner.processTransactions(transactions);
*
* // Get predictions (computed locally)
* const category = await learner.predictCategory(newTransaction);
* const anomaly = await learner.detectAnomaly(newTransaction);
*
* // All data persisted to IndexedDB
* await learner.save();
* ```
*/
import init, {
PlaidLocalLearner as WasmLearner,
WasmHnswIndex,
WasmCrypto,
WasmSpikingNetwork,
} from './ruvector_edge';
// Database constants
const DB_NAME = 'plaid_local_learning';
const DB_VERSION = 1;
const STORES = {
STATE: 'learning_state',
TOKENS: 'plaid_tokens',
TRANSACTIONS: 'transactions',
INSIGHTS: 'insights',
};
/**
* Transaction from Plaid API
*/
export interface Transaction {
transaction_id: string;
account_id: string;
amount: number;
date: string;
name: string;
merchant_name?: string;
category: string[];
pending: boolean;
payment_channel: string;
}
/**
* Spending pattern learned from transactions
*/
export interface SpendingPattern {
pattern_id: string;
category: string;
avg_amount: number;
frequency_days: number;
confidence: number;
last_seen: number;
}
/**
* Category prediction result
*/
export interface CategoryPrediction {
category: string;
confidence: number;
similar_transactions: string[];
}
/**
* Anomaly detection result
*/
export interface AnomalyResult {
is_anomaly: boolean;
anomaly_score: number;
reason: string;
expected_amount: number;
}
/**
* Budget recommendation
*/
export interface BudgetRecommendation {
category: string;
recommended_limit: number;
current_avg: number;
trend: 'increasing' | 'stable' | 'decreasing';
confidence: number;
}
/**
* Processing insights from batch
*/
export interface ProcessingInsights {
transactions_processed: number;
total_amount: number;
patterns_learned: number;
state_version: number;
}
/**
* Learning statistics
*/
export interface LearningStats {
version: number;
patterns_count: number;
q_values_count: number;
embeddings_count: number;
index_size: number;
}
/**
* Temporal spending heatmap
*/
export interface TemporalHeatmap {
day_of_week: number[]; // 7 values (Sun-Sat)
day_of_month: number[]; // 31 values
}
/**
* Plaid Link configuration
*/
export interface PlaidConfig {
clientId?: string;
environment: 'sandbox' | 'development' | 'production';
products: string[];
countryCodes: string[];
language: string;
}
/**
* Browser-local financial learning engine
*
* All data processing happens in the browser using WebAssembly.
* Financial data is never transmitted to any server.
*/
export class PlaidLocalLearner {
private wasmLearner: WasmLearner | null = null;
private db: IDBDatabase | null = null;
private initialized = false;
private encryptionKey: CryptoKey | null = null;
/**
* Initialize the local learner
*
* - Loads WASM module
* - Opens IndexedDB
* - Restores previous learning state
*/
async init(encryptionPassword?: string): Promise<void> {
if (this.initialized) return;
// Initialize WASM
await init();
// Create WASM learner
this.wasmLearner = new WasmLearner();
// Open IndexedDB
this.db = await this.openDatabase();
// Setup encryption if password provided
if (encryptionPassword) {
this.encryptionKey = await this.deriveKey(encryptionPassword);
}
// Load previous state
await this.load();
this.initialized = true;
console.log('🧠 PlaidLocalLearner initialized (100% browser-local)');
}
/**
* Open IndexedDB database
*/
private openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Create object stores
if (!db.objectStoreNames.contains(STORES.STATE)) {
db.createObjectStore(STORES.STATE);
}
if (!db.objectStoreNames.contains(STORES.TOKENS)) {
db.createObjectStore(STORES.TOKENS);
}
if (!db.objectStoreNames.contains(STORES.TRANSACTIONS)) {
const store = db.createObjectStore(STORES.TRANSACTIONS, {
keyPath: 'transaction_id',
});
store.createIndex('date', 'date');
store.createIndex('category', 'category', { multiEntry: true });
}
if (!db.objectStoreNames.contains(STORES.INSIGHTS)) {
db.createObjectStore(STORES.INSIGHTS);
}
};
});
}
/**
* Derive encryption key from password
*
* Uses a unique salt per installation stored in IndexedDB.
* This prevents rainbow table attacks across different users.
*/
private async deriveKey(password: string): Promise<CryptoKey> {
const encoder = new TextEncoder();
// Get or create unique salt for this installation
const salt = await this.getOrCreateSalt();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
/**
* Get or create a unique salt for this installation
*
* Salt is stored in IndexedDB and persists across sessions.
* Each browser/device gets a unique salt.
*/
private async getOrCreateSalt(): Promise<Uint8Array> {
const SALT_KEY = '_encryption_salt';
return new Promise(async (resolve, reject) => {
const transaction = this.db!.transaction([STORES.STATE], 'readwrite');
const store = transaction.objectStore(STORES.STATE);
// Try to get existing salt
const getRequest = store.get(SALT_KEY);
getRequest.onsuccess = () => {
if (getRequest.result) {
// Use existing salt
resolve(new Uint8Array(getRequest.result));
} else {
// Generate new random salt (32 bytes)
const newSalt = crypto.getRandomValues(new Uint8Array(32));
// Store it for future use
const putRequest = store.put(newSalt.buffer, SALT_KEY);
putRequest.onsuccess = () => resolve(newSalt);
putRequest.onerror = () => reject(putRequest.error);
}
};
getRequest.onerror = () => reject(getRequest.error);
});
}
/**
* Encrypt data for storage
*/
private async encrypt(data: string): Promise<ArrayBuffer> {
if (!this.encryptionKey) {
return new TextEncoder().encode(data);
}
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.encryptionKey,
new TextEncoder().encode(data)
);
// Prepend IV to encrypted data
const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);
return result.buffer;
}
/**
* Decrypt data from storage
*/
private async decrypt(data: ArrayBuffer): Promise<string> {
if (!this.encryptionKey) {
return new TextDecoder().decode(data);
}
const dataArray = new Uint8Array(data);
const iv = dataArray.slice(0, 12);
const encrypted = dataArray.slice(12);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
this.encryptionKey,
encrypted
);
return new TextDecoder().decode(decrypted);
}
/**
* Save learning state to IndexedDB
*/
async save(): Promise<void> {
this.ensureInitialized();
const stateJson = this.wasmLearner!.saveState();
const encrypted = await this.encrypt(stateJson);
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.STATE], 'readwrite');
const store = transaction.objectStore(STORES.STATE);
const request = store.put(encrypted, 'main');
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
/**
* Load learning state from IndexedDB
*/
async load(): Promise<void> {
this.ensureInitialized();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.STATE], 'readonly');
const store = transaction.objectStore(STORES.STATE);
const request = store.get('main');
request.onerror = () => reject(request.error);
request.onsuccess = async () => {
if (request.result) {
try {
const stateJson = await this.decrypt(request.result);
this.wasmLearner!.loadState(stateJson);
} catch (e) {
console.warn('Failed to load state, starting fresh:', e);
}
}
resolve();
};
});
}
/**
* Process a batch of transactions
*
* All processing happens locally in WASM. No data is transmitted.
*/
async processTransactions(transactions: Transaction[]): Promise<ProcessingInsights> {
this.ensureInitialized();
// Store transactions locally
await this.storeTransactions(transactions);
// Process in WASM
const insights = this.wasmLearner!.processTransactions(
JSON.stringify(transactions)
) as ProcessingInsights;
// Auto-save state
await this.save();
return insights;
}
/**
* Store transactions in IndexedDB
*/
private async storeTransactions(transactions: Transaction[]): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.TRANSACTIONS], 'readwrite');
const store = transaction.objectStore(STORES.TRANSACTIONS);
transactions.forEach((tx) => {
store.put(tx);
});
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
/**
* Predict category for a transaction
*/
predictCategory(transaction: Transaction): CategoryPrediction {
this.ensureInitialized();
return this.wasmLearner!.predictCategory(
JSON.stringify(transaction)
) as CategoryPrediction;
}
/**
* Detect if a transaction is anomalous
*/
detectAnomaly(transaction: Transaction): AnomalyResult {
this.ensureInitialized();
return this.wasmLearner!.detectAnomaly(
JSON.stringify(transaction)
) as AnomalyResult;
}
/**
* Get budget recommendation for a category
*/
getBudgetRecommendation(
category: string,
currentSpending: number,
budget: number
): BudgetRecommendation {
this.ensureInitialized();
return this.wasmLearner!.getBudgetRecommendation(
category,
currentSpending,
budget
) as BudgetRecommendation;
}
/**
* Record spending outcome for Q-learning
*
* @param category - Spending category
* @param action - 'under_budget', 'at_budget', or 'over_budget'
* @param reward - Reward value (-1 to 1)
*/
recordOutcome(
category: string,
action: 'under_budget' | 'at_budget' | 'over_budget',
reward: number
): void {
this.ensureInitialized();
this.wasmLearner!.recordOutcome(category, action, reward);
}
/**
* Get all learned spending patterns
*/
getPatterns(): SpendingPattern[] {
this.ensureInitialized();
return this.wasmLearner!.getPatternsSummary() as SpendingPattern[];
}
/**
* Get temporal spending heatmap
*/
getTemporalHeatmap(): TemporalHeatmap {
this.ensureInitialized();
return this.wasmLearner!.getTemporalHeatmap() as TemporalHeatmap;
}
/**
* Find similar transactions
*/
findSimilar(transaction: Transaction, k: number = 5): { id: string; distance: number }[] {
this.ensureInitialized();
return this.wasmLearner!.findSimilarTransactions(
JSON.stringify(transaction),
k
) as { id: string; distance: number }[];
}
/**
* Get learning statistics
*/
getStats(): LearningStats {
this.ensureInitialized();
return this.wasmLearner!.getStats() as LearningStats;
}
/**
* Clear all learned data
*
* Privacy feature: completely wipes all local learning data.
*/
async clearAllData(): Promise<void> {
this.ensureInitialized();
// Clear WASM state
this.wasmLearner!.clear();
// Clear IndexedDB
const stores = [STORES.STATE, STORES.TRANSACTIONS, STORES.INSIGHTS];
for (const storeName of stores) {
await new Promise<void>((resolve, reject) => {
const transaction = this.db!.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
console.log('🗑️ All local learning data cleared');
}
/**
* Get stored transactions from IndexedDB
*/
async getStoredTransactions(
options: {
startDate?: string;
endDate?: string;
category?: string;
limit?: number;
} = {}
): Promise<Transaction[]> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.TRANSACTIONS], 'readonly');
const store = transaction.objectStore(STORES.TRANSACTIONS);
let request: IDBRequest;
if (options.startDate && options.endDate) {
const index = store.index('date');
request = index.getAll(IDBKeyRange.bound(options.startDate, options.endDate));
} else if (options.category) {
const index = store.index('category');
request = index.getAll(options.category);
} else {
request = store.getAll();
}
request.onerror = () => reject(request.error);
request.onsuccess = () => {
let results = request.result as Transaction[];
if (options.limit) {
results = results.slice(0, options.limit);
}
resolve(results);
};
});
}
/**
* Export all data for backup
*
* Returns encrypted data that can be imported later.
*/
async exportData(): Promise<ArrayBuffer> {
this.ensureInitialized();
const exportData = {
state: this.wasmLearner!.saveState(),
transactions: await this.getStoredTransactions(),
exportedAt: new Date().toISOString(),
version: 1,
};
return this.encrypt(JSON.stringify(exportData));
}
/**
* Import data from backup
*/
async importData(encryptedData: ArrayBuffer): Promise<void> {
this.ensureInitialized();
const json = await this.decrypt(encryptedData);
const importData = JSON.parse(json);
// Load state
this.wasmLearner!.loadState(importData.state);
// Store transactions
if (importData.transactions) {
await this.storeTransactions(importData.transactions);
}
await this.save();
}
/**
* Ensure learner is initialized
*/
private ensureInitialized(): void {
if (!this.initialized || !this.wasmLearner || !this.db) {
throw new Error('PlaidLocalLearner not initialized. Call init() first.');
}
}
/**
* Close database connection
*/
close(): void {
if (this.db) {
this.db.close();
this.db = null;
}
this.initialized = false;
}
}
/**
* Plaid Link integration helper
*
* Handles Plaid Link flow while keeping tokens local.
*/
export class PlaidLinkHandler {
private db: IDBDatabase | null = null;
constructor(private config: PlaidConfig) {}
/**
* Initialize handler
*/
async init(): Promise<void> {
this.db = await this.openDatabase();
}
private openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Store access token locally
*
* Token never leaves the browser.
*/
async storeToken(itemId: string, accessToken: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.TOKENS], 'readwrite');
const store = transaction.objectStore(STORES.TOKENS);
// Store encrypted (in production, use proper encryption)
const request = store.put(
{
accessToken,
storedAt: Date.now(),
},
itemId
);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
/**
* Get stored token
*/
async getToken(itemId: string): Promise<string | null> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.TOKENS], 'readonly');
const store = transaction.objectStore(STORES.TOKENS);
const request = store.get(itemId);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
resolve(request.result?.accessToken ?? null);
};
});
}
/**
* Delete token
*/
async deleteToken(itemId: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.TOKENS], 'readwrite');
const store = transaction.objectStore(STORES.TOKENS);
const request = store.delete(itemId);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
/**
* List all stored item IDs
*/
async listItems(): Promise<string[]> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORES.TOKENS], 'readonly');
const store = transaction.objectStore(STORES.TOKENS);
const request = store.getAllKeys();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result as string[]);
});
}
}
// Export default instance
export default PlaidLocalLearner;

View File

@@ -0,0 +1,351 @@
/* tslint:disable */
/* eslint-disable */
export class WasmAdaptiveCompressor {
free(): void;
[Symbol.dispose](): void;
/**
* Update network metrics (bandwidth in Mbps, latency in ms)
*/
updateMetrics(bandwidth_mbps: number, latency_ms: number): void;
/**
* Create new adaptive compressor
*/
constructor();
/**
* Compress vector based on network conditions
*/
compress(data: Float32Array): any;
/**
* Get current network condition
*/
condition(): string;
}
export class WasmCrypto {
private constructor();
free(): void;
[Symbol.dispose](): void;
/**
* Generate a local CID for data
*/
static generateCid(data: Uint8Array): string;
/**
* SHA-256 hash of string as hex
*/
static sha256String(text: string): string;
/**
* SHA-256 hash as hex string
*/
static sha256(data: Uint8Array): string;
/**
* Decrypt data with AES-256-GCM
*/
static decrypt(encrypted: any, key_hex: string): Uint8Array;
/**
* Encrypt data with AES-256-GCM (key as hex)
*/
static encrypt(data: Uint8Array, key_hex: string): any;
}
export class WasmHnswIndex {
free(): void;
[Symbol.dispose](): void;
/**
* Create with custom parameters (m = connections per node, ef = search width)
*/
static withParams(m: number, ef_construction: number): WasmHnswIndex;
/**
* Get number of vectors in index
*/
len(): number;
/**
* Create new HNSW index with default parameters
*/
constructor();
/**
* Insert a vector with an ID
*/
insert(id: string, vector: Float32Array): void;
/**
* Search for k nearest neighbors, returns JSON array of {id, distance}
*/
search(query: Float32Array, k: number): any;
/**
* Check if index is empty
*/
isEmpty(): boolean;
}
export class WasmHybridKeyPair {
free(): void;
[Symbol.dispose](): void;
/**
* Get public key bytes as hex
*/
publicKeyHex(): string;
/**
* Generate new hybrid keypair
*/
constructor();
/**
* Sign message with hybrid signature
*/
sign(message: Uint8Array): string;
/**
* Verify hybrid signature (pubkey and signature both as JSON)
*/
static verify(public_key_json: string, message: Uint8Array, signature_json: string): boolean;
}
export class WasmIdentity {
free(): void;
[Symbol.dispose](): void;
/**
* Sign raw bytes and return signature as hex
*/
signBytes(data: Uint8Array): string;
/**
* Generate a random nonce
*/
static generateNonce(): string;
/**
* Get Ed25519 public key as hex string
*/
publicKeyHex(): string;
/**
* Create a signed registration for this identity
*/
createRegistration(agent_id: string, capabilities: any): any;
/**
* Get X25519 public key as hex string (for key exchange)
*/
x25519PublicKeyHex(): string;
/**
* Create a new identity with generated keys
*/
constructor();
/**
* Sign a message and return signature as hex
*/
sign(message: string): string;
/**
* Verify a signature (static method)
*/
static verify(public_key_hex: string, message: string, signature_hex: string): boolean;
}
export class WasmQuantizer {
private constructor();
free(): void;
[Symbol.dispose](): void;
/**
* Binary quantize a vector (32x compression)
*/
static binaryQuantize(vector: Float32Array): Uint8Array;
/**
* Scalar quantize a vector (4x compression)
*/
static scalarQuantize(vector: Float32Array): any;
/**
* Compute hamming distance between binary quantized vectors
*/
static hammingDistance(a: Uint8Array, b: Uint8Array): number;
/**
* Reconstruct from scalar quantized
*/
static scalarDequantize(quantized: any): Float32Array;
}
export class WasmRaftNode {
free(): void;
[Symbol.dispose](): void;
/**
* Append entry to log (leader only), returns log index or null
*/
appendEntry(data: Uint8Array): any;
/**
* Get log length
*/
getLogLength(): number;
/**
* Start an election (returns vote request as JSON)
*/
startElection(): any;
/**
* Get commit index
*/
getCommitIndex(): bigint;
/**
* Handle a vote request (returns vote response as JSON)
*/
handleVoteRequest(request: any): any;
/**
* Handle a vote response (returns true if we became leader)
*/
handleVoteResponse(response: any): boolean;
/**
* Create new Raft node with cluster members
*/
constructor(node_id: string, members: any);
/**
* Get current term
*/
term(): bigint;
/**
* Get current state (Follower, Candidate, Leader)
*/
state(): string;
/**
* Check if this node is the leader
*/
isLeader(): boolean;
}
export class WasmSemanticMatcher {
free(): void;
[Symbol.dispose](): void;
/**
* Get number of registered agents
*/
agentCount(): number;
/**
* Find best matching agent for a task, returns {agentId, score} or null
*/
matchAgent(task_description: string): any;
/**
* Register an agent with capability description
*/
registerAgent(agent_id: string, capabilities: string): void;
/**
* Create new semantic matcher
*/
constructor();
}
export class WasmSpikingNetwork {
free(): void;
[Symbol.dispose](): void;
/**
* Apply STDP learning rule
*/
stdpUpdate(pre: Uint8Array, post: Uint8Array, learning_rate: number): void;
/**
* Create new spiking network
*/
constructor(input_size: number, hidden_size: number, output_size: number);
/**
* Reset network state
*/
reset(): void;
/**
* Process input spikes and return output spikes
*/
forward(inputs: Uint8Array): Uint8Array;
}
/**
* Initialize the WASM module (call once on load)
*/
export function init(): void;
/**
* Get library version
*/
export function version(): string;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly __wbg_wasmadaptivecompressor_free: (a: number, b: number) => void;
readonly __wbg_wasmcrypto_free: (a: number, b: number) => void;
readonly __wbg_wasmhnswindex_free: (a: number, b: number) => void;
readonly __wbg_wasmhybridkeypair_free: (a: number, b: number) => void;
readonly __wbg_wasmidentity_free: (a: number, b: number) => void;
readonly __wbg_wasmquantizer_free: (a: number, b: number) => void;
readonly __wbg_wasmraftnode_free: (a: number, b: number) => void;
readonly __wbg_wasmsemanticmatcher_free: (a: number, b: number) => void;
readonly __wbg_wasmspikingnetwork_free: (a: number, b: number) => void;
readonly version: () => [number, number];
readonly wasmadaptivecompressor_compress: (a: number, b: number, c: number) => any;
readonly wasmadaptivecompressor_condition: (a: number) => [number, number];
readonly wasmadaptivecompressor_new: () => number;
readonly wasmadaptivecompressor_updateMetrics: (a: number, b: number, c: number) => void;
readonly wasmcrypto_decrypt: (a: any, b: number, c: number) => [number, number, number, number];
readonly wasmcrypto_encrypt: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly wasmcrypto_generateCid: (a: number, b: number) => [number, number];
readonly wasmcrypto_sha256: (a: number, b: number) => [number, number];
readonly wasmhnswindex_insert: (a: number, b: number, c: number, d: number, e: number) => void;
readonly wasmhnswindex_isEmpty: (a: number) => number;
readonly wasmhnswindex_len: (a: number) => number;
readonly wasmhnswindex_new: () => number;
readonly wasmhnswindex_search: (a: number, b: number, c: number, d: number) => any;
readonly wasmhnswindex_withParams: (a: number, b: number) => number;
readonly wasmhybridkeypair_new: () => number;
readonly wasmhybridkeypair_publicKeyHex: (a: number) => [number, number];
readonly wasmhybridkeypair_sign: (a: number, b: number, c: number) => [number, number];
readonly wasmhybridkeypair_verify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
readonly wasmidentity_createRegistration: (a: number, b: number, c: number, d: any) => [number, number, number];
readonly wasmidentity_generateNonce: () => [number, number];
readonly wasmidentity_new: () => number;
readonly wasmidentity_publicKeyHex: (a: number) => [number, number];
readonly wasmidentity_sign: (a: number, b: number, c: number) => [number, number];
readonly wasmidentity_verify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
readonly wasmidentity_x25519PublicKeyHex: (a: number) => [number, number];
readonly wasmquantizer_binaryQuantize: (a: number, b: number) => [number, number];
readonly wasmquantizer_hammingDistance: (a: number, b: number, c: number, d: number) => number;
readonly wasmquantizer_scalarDequantize: (a: any) => [number, number, number, number];
readonly wasmquantizer_scalarQuantize: (a: number, b: number) => any;
readonly wasmraftnode_appendEntry: (a: number, b: number, c: number) => any;
readonly wasmraftnode_getCommitIndex: (a: number) => bigint;
readonly wasmraftnode_getLogLength: (a: number) => number;
readonly wasmraftnode_handleVoteRequest: (a: number, b: any) => [number, number, number];
readonly wasmraftnode_handleVoteResponse: (a: number, b: any) => [number, number, number];
readonly wasmraftnode_isLeader: (a: number) => number;
readonly wasmraftnode_new: (a: number, b: number, c: any) => [number, number, number];
readonly wasmraftnode_startElection: (a: number) => any;
readonly wasmraftnode_state: (a: number) => [number, number];
readonly wasmraftnode_term: (a: number) => bigint;
readonly wasmsemanticmatcher_agentCount: (a: number) => number;
readonly wasmsemanticmatcher_matchAgent: (a: number, b: number, c: number) => any;
readonly wasmsemanticmatcher_new: () => number;
readonly wasmsemanticmatcher_registerAgent: (a: number, b: number, c: number, d: number, e: number) => void;
readonly wasmspikingnetwork_forward: (a: number, b: number, c: number) => [number, number];
readonly wasmspikingnetwork_new: (a: number, b: number, c: number) => number;
readonly wasmspikingnetwork_reset: (a: number) => void;
readonly wasmspikingnetwork_stdpUpdate: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly init: () => void;
readonly wasmidentity_signBytes: (a: number, b: number, c: number) => [number, number];
readonly wasmcrypto_sha256String: (a: number, b: number) => [number, number];
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_externrefs: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,71 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const __wbg_wasmadaptivecompressor_free: (a: number, b: number) => void;
export const __wbg_wasmcrypto_free: (a: number, b: number) => void;
export const __wbg_wasmhnswindex_free: (a: number, b: number) => void;
export const __wbg_wasmhybridkeypair_free: (a: number, b: number) => void;
export const __wbg_wasmidentity_free: (a: number, b: number) => void;
export const __wbg_wasmquantizer_free: (a: number, b: number) => void;
export const __wbg_wasmraftnode_free: (a: number, b: number) => void;
export const __wbg_wasmsemanticmatcher_free: (a: number, b: number) => void;
export const __wbg_wasmspikingnetwork_free: (a: number, b: number) => void;
export const version: () => [number, number];
export const wasmadaptivecompressor_compress: (a: number, b: number, c: number) => any;
export const wasmadaptivecompressor_condition: (a: number) => [number, number];
export const wasmadaptivecompressor_new: () => number;
export const wasmadaptivecompressor_updateMetrics: (a: number, b: number, c: number) => void;
export const wasmcrypto_decrypt: (a: any, b: number, c: number) => [number, number, number, number];
export const wasmcrypto_encrypt: (a: number, b: number, c: number, d: number) => [number, number, number];
export const wasmcrypto_generateCid: (a: number, b: number) => [number, number];
export const wasmcrypto_sha256: (a: number, b: number) => [number, number];
export const wasmhnswindex_insert: (a: number, b: number, c: number, d: number, e: number) => void;
export const wasmhnswindex_isEmpty: (a: number) => number;
export const wasmhnswindex_len: (a: number) => number;
export const wasmhnswindex_new: () => number;
export const wasmhnswindex_search: (a: number, b: number, c: number, d: number) => any;
export const wasmhnswindex_withParams: (a: number, b: number) => number;
export const wasmhybridkeypair_new: () => number;
export const wasmhybridkeypair_publicKeyHex: (a: number) => [number, number];
export const wasmhybridkeypair_sign: (a: number, b: number, c: number) => [number, number];
export const wasmhybridkeypair_verify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
export const wasmidentity_createRegistration: (a: number, b: number, c: number, d: any) => [number, number, number];
export const wasmidentity_generateNonce: () => [number, number];
export const wasmidentity_new: () => number;
export const wasmidentity_publicKeyHex: (a: number) => [number, number];
export const wasmidentity_sign: (a: number, b: number, c: number) => [number, number];
export const wasmidentity_verify: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
export const wasmidentity_x25519PublicKeyHex: (a: number) => [number, number];
export const wasmquantizer_binaryQuantize: (a: number, b: number) => [number, number];
export const wasmquantizer_hammingDistance: (a: number, b: number, c: number, d: number) => number;
export const wasmquantizer_scalarDequantize: (a: any) => [number, number, number, number];
export const wasmquantizer_scalarQuantize: (a: number, b: number) => any;
export const wasmraftnode_appendEntry: (a: number, b: number, c: number) => any;
export const wasmraftnode_getCommitIndex: (a: number) => bigint;
export const wasmraftnode_getLogLength: (a: number) => number;
export const wasmraftnode_handleVoteRequest: (a: number, b: any) => [number, number, number];
export const wasmraftnode_handleVoteResponse: (a: number, b: any) => [number, number, number];
export const wasmraftnode_isLeader: (a: number) => number;
export const wasmraftnode_new: (a: number, b: number, c: any) => [number, number, number];
export const wasmraftnode_startElection: (a: number) => any;
export const wasmraftnode_state: (a: number) => [number, number];
export const wasmraftnode_term: (a: number) => bigint;
export const wasmsemanticmatcher_agentCount: (a: number) => number;
export const wasmsemanticmatcher_matchAgent: (a: number, b: number, c: number) => any;
export const wasmsemanticmatcher_new: () => number;
export const wasmsemanticmatcher_registerAgent: (a: number, b: number, c: number, d: number, e: number) => void;
export const wasmspikingnetwork_forward: (a: number, b: number, c: number) => [number, number];
export const wasmspikingnetwork_new: (a: number, b: number, c: number) => number;
export const wasmspikingnetwork_reset: (a: number) => void;
export const wasmspikingnetwork_stdpUpdate: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const init: () => void;
export const wasmidentity_signBytes: (a: number, b: number, c: number) => [number, number];
export const wasmcrypto_sha256String: (a: number, b: number) => [number, number];
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_externrefs: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_start: () => void;

View File

@@ -0,0 +1,254 @@
/**
* Web Worker Pool Manager
*
* Manages a pool of workers for parallel vector operations.
* Supports:
* - Round-robin task distribution
* - Load balancing
* - Automatic worker initialization
* - Promise-based API
*/
export class WorkerPool {
constructor(workerUrl, wasmUrl, options = {}) {
this.workerUrl = workerUrl;
this.wasmUrl = wasmUrl;
this.poolSize = options.poolSize || navigator.hardwareConcurrency || 4;
this.workers = [];
this.nextWorker = 0;
this.pendingRequests = new Map();
this.requestId = 0;
this.initialized = false;
this.options = options;
}
/**
* Initialize the worker pool
*/
async init() {
if (this.initialized) return;
console.log(`Initializing worker pool with ${this.poolSize} workers...`);
const initPromises = [];
for (let i = 0; i < this.poolSize; i++) {
const worker = new Worker(this.workerUrl, { type: 'module' });
worker.onmessage = (e) => this.handleMessage(i, e);
worker.onerror = (error) => this.handleError(i, error);
this.workers.push({
worker,
busy: false,
id: i
});
// Initialize worker with WASM
const initPromise = this.sendToWorker(i, 'init', {
wasmUrl: this.wasmUrl,
dimensions: this.options.dimensions,
metric: this.options.metric,
useHnsw: this.options.useHnsw
});
initPromises.push(initPromise);
}
await Promise.all(initPromises);
this.initialized = true;
console.log(`Worker pool initialized successfully`);
}
/**
* Handle message from worker
*/
handleMessage(workerId, event) {
const { type, requestId, data, error } = event.data;
if (type === 'error') {
const request = this.pendingRequests.get(requestId);
if (request) {
request.reject(new Error(error.message));
this.pendingRequests.delete(requestId);
}
return;
}
const request = this.pendingRequests.get(requestId);
if (request) {
this.workers[workerId].busy = false;
request.resolve(data);
this.pendingRequests.delete(requestId);
}
}
/**
* Handle worker error
*/
handleError(workerId, error) {
console.error(`Worker ${workerId} error:`, error);
// Reject all pending requests for this worker
for (const [requestId, request] of this.pendingRequests) {
if (request.workerId === workerId) {
request.reject(error);
this.pendingRequests.delete(requestId);
}
}
}
/**
* Get next available worker (round-robin)
*/
getNextWorker() {
// Try to find an idle worker
for (let i = 0; i < this.workers.length; i++) {
const idx = (this.nextWorker + i) % this.workers.length;
if (!this.workers[idx].busy) {
this.nextWorker = (idx + 1) % this.workers.length;
return idx;
}
}
// All busy, use round-robin
const idx = this.nextWorker;
this.nextWorker = (this.nextWorker + 1) % this.workers.length;
return idx;
}
/**
* Send message to specific worker
*/
sendToWorker(workerId, type, data) {
return new Promise((resolve, reject) => {
const requestId = this.requestId++;
this.pendingRequests.set(requestId, {
resolve,
reject,
workerId,
timestamp: Date.now()
});
this.workers[workerId].busy = true;
this.workers[workerId].worker.postMessage({
type,
data: { ...data, requestId }
});
// Timeout after 30 seconds
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
/**
* Execute operation on next available worker
*/
async execute(type, data) {
if (!this.initialized) {
await this.init();
}
const workerId = this.getNextWorker();
return this.sendToWorker(workerId, type, data);
}
/**
* Insert vector
*/
async insert(vector, id = null, metadata = null) {
return this.execute('insert', { vector, id, metadata });
}
/**
* Insert batch of vectors
*/
async insertBatch(entries) {
// Distribute batch across workers
const chunkSize = Math.ceil(entries.length / this.poolSize);
const chunks = [];
for (let i = 0; i < entries.length; i += chunkSize) {
chunks.push(entries.slice(i, i + chunkSize));
}
const promises = chunks.map((chunk, i) =>
this.sendToWorker(i % this.poolSize, 'insertBatch', { entries: chunk })
);
const results = await Promise.all(promises);
return results.flat();
}
/**
* Search for similar vectors
*/
async search(query, k = 10, filter = null) {
return this.execute('search', { query, k, filter });
}
/**
* Parallel search across multiple queries
*/
async searchBatch(queries, k = 10, filter = null) {
const promises = queries.map((query, i) =>
this.sendToWorker(i % this.poolSize, 'search', { query, k, filter })
);
return Promise.all(promises);
}
/**
* Delete vector
*/
async delete(id) {
return this.execute('delete', { id });
}
/**
* Get vector by ID
*/
async get(id) {
return this.execute('get', { id });
}
/**
* Get database length (from first worker)
*/
async len() {
return this.sendToWorker(0, 'len', {});
}
/**
* Terminate all workers
*/
terminate() {
for (const { worker } of this.workers) {
worker.terminate();
}
this.workers = [];
this.initialized = false;
console.log('Worker pool terminated');
}
/**
* Get pool statistics
*/
getStats() {
return {
poolSize: this.poolSize,
busyWorkers: this.workers.filter(w => w.busy).length,
idleWorkers: this.workers.filter(w => !w.busy).length,
pendingRequests: this.pendingRequests.size
};
}
}
export default WorkerPool;

View File

@@ -0,0 +1,184 @@
/**
* Web Worker for parallel vector search operations
*
* This worker handles:
* - Vector search operations in parallel
* - Batch insert operations
* - Zero-copy transfers via transferable objects
*/
// Import the WASM module
let wasmModule = null;
let vectorDB = null;
/**
* Initialize the worker with WASM module
*/
self.onmessage = async function(e) {
const { type, data } = e.data;
try {
switch (type) {
case 'init':
await initWorker(data);
self.postMessage({ type: 'init', success: true });
break;
case 'insert':
await handleInsert(data);
break;
case 'insertBatch':
await handleInsertBatch(data);
break;
case 'search':
await handleSearch(data);
break;
case 'delete':
await handleDelete(data);
break;
case 'get':
await handleGet(data);
break;
case 'len':
const length = vectorDB.len();
self.postMessage({ type: 'len', data: length });
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
} catch (error) {
self.postMessage({
type: 'error',
error: {
message: error.message,
stack: error.stack
}
});
}
};
/**
* Initialize WASM module and VectorDB
*/
async function initWorker(config) {
const { wasmUrl, dimensions, metric, useHnsw } = config;
// Import WASM module
wasmModule = await import(wasmUrl);
// Initialize WASM
await wasmModule.default();
// Create VectorDB instance
vectorDB = new wasmModule.VectorDB(dimensions, metric, useHnsw);
console.log(`Worker initialized with dimensions=${dimensions}, metric=${metric}, SIMD=${wasmModule.detectSIMD()}`);
}
/**
* Handle single vector insert
*/
async function handleInsert(data) {
const { vector, id, metadata, requestId } = data;
// Convert array to Float32Array if needed
const vectorArray = new Float32Array(vector);
const resultId = vectorDB.insert(vectorArray, id, metadata);
self.postMessage({
type: 'insert',
requestId,
data: resultId
});
}
/**
* Handle batch insert
*/
async function handleInsertBatch(data) {
const { entries, requestId } = data;
// Convert vectors to Float32Array
const processedEntries = entries.map(entry => ({
vector: new Float32Array(entry.vector),
id: entry.id,
metadata: entry.metadata
}));
const ids = vectorDB.insertBatch(processedEntries);
self.postMessage({
type: 'insertBatch',
requestId,
data: ids
});
}
/**
* Handle vector search
*/
async function handleSearch(data) {
const { query, k, filter, requestId } = data;
// Convert query to Float32Array
const queryArray = new Float32Array(query);
const results = vectorDB.search(queryArray, k, filter);
// Convert results to plain objects
const plainResults = results.map(result => ({
id: result.id,
score: result.score,
vector: result.vector ? Array.from(result.vector) : null,
metadata: result.metadata
}));
self.postMessage({
type: 'search',
requestId,
data: plainResults
});
}
/**
* Handle delete operation
*/
async function handleDelete(data) {
const { id, requestId } = data;
const deleted = vectorDB.delete(id);
self.postMessage({
type: 'delete',
requestId,
data: deleted
});
}
/**
* Handle get operation
*/
async function handleGet(data) {
const { id, requestId } = data;
const entry = vectorDB.get(id);
const plainEntry = entry ? {
id: entry.id,
vector: Array.from(entry.vector),
metadata: entry.metadata
} : null;
self.postMessage({
type: 'get',
requestId,
data: plainEntry
});
}

View File

@@ -0,0 +1,584 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZK Financial Proofs Demo - RuVector Edge</title>
<style>
:root {
--bg: #0a0a0f;
--card: #12121a;
--border: #2a2a3a;
--text: #e0e0e8;
--text-dim: #8888a0;
--accent: #8b5cf6;
--accent-glow: rgba(139, 92, 246, 0.3);
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
header { text-align: center; margin-bottom: 3rem; }
h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, var(--accent), #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle { color: var(--text-dim); font-size: 1.1rem; margin-top: 0.5rem; }
.privacy-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.3);
color: var(--accent);
padding: 0.5rem 1rem;
border-radius: 2rem;
margin-top: 1rem;
font-size: 0.9rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 1.5rem;
}
.card h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-group { margin-bottom: 1rem; }
label {
display: block;
font-size: 0.9rem;
color: var(--text-dim);
margin-bottom: 0.25rem;
}
input, select {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: 0.75rem;
border-radius: 0.5rem;
font-size: 1rem;
width: 100%;
}
input:focus, select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
button {
background: var(--accent);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 20px var(--accent-glow);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
button.secondary {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
}
.proof-display {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
.verification-result {
padding: 1rem;
border-radius: 0.5rem;
margin-top: 1rem;
}
.verification-result.valid {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
}
.verification-result.invalid {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.flow-diagram {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
margin-bottom: 1.5rem;
}
.flow-step {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--card);
border-radius: 0.5rem;
margin: 0 0.25rem;
}
.flow-arrow {
color: var(--text-dim);
font-size: 1.5rem;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--border);
color: var(--text-dim);
cursor: pointer;
border-radius: 0.5rem;
}
.tab.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.hidden { display: none; }
.info-box {
background: rgba(139, 92, 246, 0.05);
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
footer {
text-align: center;
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border);
color: var(--text-dim);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🔐 Zero-Knowledge Financial Proofs</h1>
<p class="subtitle">Prove financial statements without revealing actual numbers</p>
<div class="privacy-badge">
🛡️ Your actual income, balance, and transactions are NEVER revealed
</div>
</header>
<!-- Flow Diagram -->
<div class="flow-diagram">
<span class="flow-step">📊 Your Private Data</span>
<span class="flow-arrow"></span>
<span class="flow-step">🔮 ZK Circuit (WASM)</span>
<span class="flow-arrow"></span>
<span class="flow-step">📜 Proof (~1KB)</span>
<span class="flow-arrow"></span>
<span class="flow-step">✅ Verifier</span>
</div>
<div class="grid">
<!-- Prover Panel -->
<div class="card">
<h2>👤 Prover (Your Data - Private)</h2>
<div class="info-box">
<strong>How it works:</strong> Enter your real financial data below.
The ZK system will generate a proof that ONLY reveals the statement is true,
not your actual numbers.
</div>
<div class="form-group">
<label>Monthly Income ($)</label>
<input type="number" id="income" value="6500" placeholder="e.g., 6500">
</div>
<div class="form-group">
<label>Current Savings ($)</label>
<input type="number" id="savings" value="15000" placeholder="e.g., 15000">
</div>
<div class="form-group">
<label>Monthly Rent ($)</label>
<input type="number" id="rent" value="2000" placeholder="e.g., 2000">
</div>
<div class="form-group">
<label>Proof Type</label>
<select id="proof-type">
<option value="affordability">Rental Affordability (Income ≥ 3× Rent)</option>
<option value="income">Income Above Threshold</option>
<option value="savings">Savings Above Threshold</option>
<option value="no-overdraft">No Overdrafts (90 days)</option>
<option value="full-application">Complete Rental Application</option>
</select>
</div>
<button id="generate-btn" onclick="generateProof()">
🔮 Generate ZK Proof
</button>
<div id="prover-result" style="margin-top: 1rem;"></div>
</div>
<!-- Verifier Panel -->
<div class="card">
<h2>🏢 Verifier (Landlord/Bank - No Private Data)</h2>
<div class="info-box">
<strong>What verifier sees:</strong> Only the proof and statement.
Cannot determine actual income, savings, or any other numbers.
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('paste')">Paste Proof</button>
<button class="tab" onclick="showTab('received')">Received Proof</button>
</div>
<div id="tab-paste">
<div class="form-group">
<label>Proof JSON</label>
<textarea id="proof-input" class="proof-display" rows="8"
placeholder="Paste proof JSON here..."></textarea>
</div>
</div>
<div id="tab-received" class="hidden">
<div class="proof-display" id="received-proof">
No proof received yet. Generate one from the Prover panel.
</div>
</div>
<button onclick="verifyProof()">
✅ Verify Proof
</button>
<div id="verification-result"></div>
</div>
<!-- What's Proven vs Hidden -->
<div class="card">
<h2>🔍 What's Proven vs What's Hidden</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid var(--border);">
<th style="padding: 0.5rem; text-align: left;">Statement</th>
<th style="padding: 0.5rem; text-align: center;">Proven</th>
<th style="padding: 0.5rem; text-align: center;">Hidden</th>
</tr>
</thead>
<tbody id="proof-breakdown">
<tr>
<td style="padding: 0.5rem;">Income ≥ 3× Rent</td>
<td style="padding: 0.5rem; text-align: center; color: var(--success);">✓ Yes/No</td>
<td style="padding: 0.5rem; text-align: center; color: var(--error);">🔒 Exact amount</td>
</tr>
</tbody>
</table>
<div style="margin-top: 1rem; padding: 1rem; background: var(--bg); border-radius: 0.5rem;">
<strong>Privacy Guarantee:</strong>
<p style="color: var(--text-dim); margin-top: 0.5rem; font-size: 0.9rem;">
The verifier mathematically CANNOT extract your actual numbers from the proof.
They only learn whether the statement is true or false.
</p>
</div>
</div>
<!-- Use Cases -->
<div class="card">
<h2>💡 Real-World Use Cases</h2>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="padding: 1rem; background: var(--bg); border-radius: 0.5rem;">
<strong>🏠 Rental Applications</strong>
<p style="color: var(--text-dim); font-size: 0.9rem;">
Prove you can afford rent without revealing exact salary
</p>
</div>
<div style="padding: 1rem; background: var(--bg); border-radius: 0.5rem;">
<strong>💳 Credit Applications</strong>
<p style="color: var(--text-dim); font-size: 0.9rem;">
Prove debt-to-income ratio without revealing all debts
</p>
</div>
<div style="padding: 1rem; background: var(--bg); border-radius: 0.5rem;">
<strong>💼 Employment Verification</strong>
<p style="color: var(--text-dim); font-size: 0.9rem;">
Prove you earn above minimum without revealing exact pay
</p>
</div>
<div style="padding: 1rem; background: var(--bg); border-radius: 0.5rem;">
<strong>🏦 Account Stability</strong>
<p style="color: var(--text-dim); font-size: 0.9rem;">
Prove no overdrafts without revealing transaction history
</p>
</div>
</div>
</div>
</div>
<footer>
<p>Powered by <strong>RuVector Edge</strong> • Bulletproofs-style ZK Proofs • 100% Browser-Local</p>
</footer>
</div>
<script type="module">
// Simulated ZK proof generation (in production, uses WASM)
let lastProof = null;
window.generateProof = async function() {
const btn = document.getElementById('generate-btn');
btn.disabled = true;
btn.innerHTML = '⏳ Generating...';
const income = parseFloat(document.getElementById('income').value);
const savings = parseFloat(document.getElementById('savings').value);
const rent = parseFloat(document.getElementById('rent').value);
const proofType = document.getElementById('proof-type').value;
// Simulate proof generation
await new Promise(r => setTimeout(r, 500));
let statement, canProve;
switch (proofType) {
case 'affordability':
canProve = income >= rent * 3;
statement = `Income ≥ 3× monthly rent of $${rent}`;
break;
case 'income':
canProve = income >= 5000;
statement = `Average monthly income ≥ $5,000`;
break;
case 'savings':
canProve = savings >= rent * 2;
statement = `Current savings ≥ $${rent * 2}`;
break;
case 'no-overdraft':
canProve = savings > 0;
statement = `No overdrafts in the past 90 days`;
break;
case 'full-application':
canProve = income >= rent * 3 && savings >= rent * 2;
statement = `Complete rental application for $${rent}/month`;
break;
}
if (!canProve) {
document.getElementById('prover-result').innerHTML = `
<div style="color: var(--error); padding: 1rem; background: rgba(239,68,68,0.1); border-radius: 0.5rem;">
❌ Cannot generate proof: Your data doesn't meet the requirement.
<br><small>Actual numbers never leave your browser.</small>
</div>
`;
btn.disabled = false;
btn.innerHTML = '🔮 Generate ZK Proof';
return;
}
// Generate proof structure
lastProof = {
proof_type: proofType === 'affordability' ? 'Affordability' : 'Range',
proof_data: Array.from({length: 256}, () => Math.floor(Math.random() * 256)),
public_inputs: {
commitments: [{
point: Array.from({length: 32}, () => Math.floor(Math.random() * 256))
}],
bounds: [rent * 100, 3],
statement: statement,
},
generated_at: Math.floor(Date.now() / 1000),
expires_at: Math.floor(Date.now() / 1000) + 86400 * 30,
};
const proofJson = JSON.stringify(lastProof, null, 2);
document.getElementById('prover-result').innerHTML = `
<div style="color: var(--success); margin-bottom: 0.5rem;">
✅ Proof generated successfully!
</div>
<div class="proof-display" style="max-height: 200px;">
${proofJson}
</div>
<button class="secondary" style="margin-top: 0.5rem;" onclick="copyProof()">
📋 Copy Proof
</button>
`;
document.getElementById('received-proof').textContent = proofJson;
document.getElementById('proof-input').value = proofJson;
updateBreakdown(income, savings, rent, proofType);
btn.disabled = false;
btn.innerHTML = '🔮 Generate ZK Proof';
};
window.verifyProof = function() {
const proofJson = document.getElementById('proof-input').value ||
document.getElementById('received-proof').textContent;
if (!proofJson || proofJson.includes('No proof')) {
alert('Please generate or paste a proof first');
return;
}
try {
const proof = JSON.parse(proofJson);
// Simulate verification
const result = {
valid: true,
statement: proof.public_inputs.statement,
verified_at: Math.floor(Date.now() / 1000),
};
document.getElementById('verification-result').innerHTML = `
<div class="verification-result ${result.valid ? 'valid' : 'invalid'}">
<h3>${result.valid ? '✅ Proof Valid' : '❌ Proof Invalid'}</h3>
<p style="margin-top: 0.5rem;"><strong>Statement:</strong> ${result.statement}</p>
<p style="margin-top: 0.5rem; color: var(--text-dim); font-size: 0.9rem;">
${result.valid
? 'The prover has demonstrated the statement is TRUE without revealing actual values.'
: 'The proof could not be verified.'}
</p>
</div>
`;
} catch (e) {
document.getElementById('verification-result').innerHTML = `
<div class="verification-result invalid">
<h3>❌ Invalid Proof Format</h3>
<p>${e.message}</p>
</div>
`;
}
};
window.copyProof = function() {
const proofJson = JSON.stringify(lastProof);
navigator.clipboard.writeText(proofJson);
alert('Proof copied to clipboard!');
};
window.showTab = function(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector(`[onclick="showTab('${tab}')"]`).classList.add('active');
document.getElementById('tab-paste').classList.toggle('hidden', tab !== 'paste');
document.getElementById('tab-received').classList.toggle('hidden', tab !== 'received');
};
function updateBreakdown(income, savings, rent, proofType) {
const tbody = document.getElementById('proof-breakdown');
const rows = {
'affordability': [
['Income ≥ 3× Rent', '✓ True/False', '🔒 $' + income.toLocaleString()],
['Rent amount', '✓ $' + rent.toLocaleString(), '—'],
],
'income': [
['Income ≥ $5,000', '✓ True/False', '🔒 $' + income.toLocaleString()],
],
'savings': [
['Savings ≥ $' + (rent * 2).toLocaleString(), '✓ True/False', '🔒 $' + savings.toLocaleString()],
],
'no-overdraft': [
['No overdrafts (90 days)', '✓ True/False', '🔒 All balances'],
],
'full-application': [
['Income ≥ 3× Rent', '✓ True/False', '🔒 $' + income.toLocaleString()],
['No overdrafts', '✓ True/False', '🔒 All balances'],
['Savings ≥ 2× Rent', '✓ True/False', '🔒 $' + savings.toLocaleString()],
],
};
tbody.innerHTML = (rows[proofType] || rows['affordability']).map(([stmt, proven, hidden]) => `
<tr>
<td style="padding: 0.5rem;">${stmt}</td>
<td style="padding: 0.5rem; text-align: center; color: var(--success);">${proven}</td>
<td style="padding: 0.5rem; text-align: center; color: var(--error);">${hidden}</td>
</tr>
`).join('');
}
// Initialize
updateBreakdown(6500, 15000, 2000, 'affordability');
</script>
</body>
</html>

View File

@@ -0,0 +1,425 @@
/**
* Zero-Knowledge Financial Proofs
*
* Prove financial statements without revealing actual numbers.
* All proof generation happens in the browser - private data never leaves.
*
* @example
* ```typescript
* import { ZkFinancialProver, ZkProofVerifier } from './zk-financial-proofs';
*
* // Prover (you - with private data)
* const prover = new ZkFinancialProver();
* prover.loadIncome([650000, 650000, 680000]); // cents
* prover.loadBalances([500000, 520000, 480000, 510000]);
*
* // Generate proof: "My income is at least 3x the rent"
* const proof = await prover.proveAffordability(200000, 3); // $2000 rent
*
* // Share proof with landlord (contains NO actual numbers)
* const proofJson = JSON.stringify(proof);
*
* // Verifier (landlord - without your private data)
* const result = ZkProofVerifier.verify(proofJson);
* console.log(result.valid); // true
* console.log(result.statement); // "Income ≥ 3× monthly rent of $2000"
* ```
*/
import init, {
ZkFinancialProver as WasmProver,
ZkProofVerifier as WasmVerifier,
ZkUtils,
} from './ruvector_edge';
// ============================================================================
// Types
// ============================================================================
/**
* A zero-knowledge proof
*/
export interface ZkProof {
proof_type: ProofType;
proof_data: number[];
public_inputs: PublicInputs;
generated_at: number;
expires_at?: number;
}
export type ProofType =
| 'Range'
| 'Comparison'
| 'Affordability'
| 'NonNegative'
| 'SumBound'
| 'AverageBound'
| 'SetMembership';
export interface PublicInputs {
commitments: Commitment[];
bounds: number[];
statement: string;
attestation?: Attestation;
}
export interface Commitment {
point: number[];
}
export interface Attestation {
issuer: string;
signature: number[];
timestamp: number;
}
export interface VerificationResult {
valid: boolean;
statement: string;
verified_at: number;
error?: string;
}
export interface RentalApplicationProof {
income_proof: ZkProof;
stability_proof: ZkProof;
savings_proof?: ZkProof;
metadata: ApplicationMetadata;
}
export interface ApplicationMetadata {
applicant_id: string;
property_id?: string;
generated_at: number;
expires_at: number;
}
// ============================================================================
// Prover (Client-Side)
// ============================================================================
/**
* Generate zero-knowledge proofs about financial data.
*
* All proof generation happens locally in WebAssembly.
* Your actual financial numbers are NEVER revealed.
*/
export class ZkFinancialProver {
private wasmProver: WasmProver | null = null;
private initialized = false;
/**
* Initialize the prover
*/
async init(): Promise<void> {
if (this.initialized) return;
await init();
this.wasmProver = new WasmProver();
this.initialized = true;
}
/**
* Load monthly income data
* @param monthlyIncome Array of monthly income in CENTS (e.g., $6500 = 650000)
*/
loadIncome(monthlyIncome: number[]): void {
this.ensureInit();
this.wasmProver!.loadIncome(new BigUint64Array(monthlyIncome.map(BigInt)));
}
/**
* Load expense data for a category
* @param category Category name (e.g., "Food", "Transportation")
* @param monthlyExpenses Array of monthly expenses in CENTS
*/
loadExpenses(category: string, monthlyExpenses: number[]): void {
this.ensureInit();
this.wasmProver!.loadExpenses(category, new BigUint64Array(monthlyExpenses.map(BigInt)));
}
/**
* Load daily balance history
* @param dailyBalances Array of daily balances in CENTS (can be negative)
*/
loadBalances(dailyBalances: number[]): void {
this.ensureInit();
this.wasmProver!.loadBalances(new BigInt64Array(dailyBalances.map(BigInt)));
}
// --------------------------------------------------------------------------
// Proof Generation
// --------------------------------------------------------------------------
/**
* Prove: average income ≥ threshold
*
* Use case: Prove you make at least $X without revealing exact income
*
* @param thresholdDollars Minimum income threshold in dollars
*/
async proveIncomeAbove(thresholdDollars: number): Promise<ZkProof> {
this.ensureInit();
const thresholdCents = Math.round(thresholdDollars * 100);
return this.wasmProver!.proveIncomeAbove(BigInt(thresholdCents));
}
/**
* Prove: income ≥ multiplier × rent
*
* Use case: Prove affordability for apartment application
*
* @param rentDollars Monthly rent in dollars
* @param multiplier Required income multiplier (typically 3)
*/
async proveAffordability(rentDollars: number, multiplier: number): Promise<ZkProof> {
this.ensureInit();
const rentCents = Math.round(rentDollars * 100);
return this.wasmProver!.proveAffordability(BigInt(rentCents), BigInt(multiplier));
}
/**
* Prove: no overdrafts in the past N days
*
* Use case: Prove account stability
*
* @param days Number of days to prove (e.g., 90)
*/
async proveNoOverdrafts(days: number): Promise<ZkProof> {
this.ensureInit();
return this.wasmProver!.proveNoOverdrafts(days);
}
/**
* Prove: current savings ≥ threshold
*
* Use case: Prove you have emergency fund
*
* @param thresholdDollars Minimum savings in dollars
*/
async proveSavingsAbove(thresholdDollars: number): Promise<ZkProof> {
this.ensureInit();
const thresholdCents = Math.round(thresholdDollars * 100);
return this.wasmProver!.proveSavingsAbove(BigInt(thresholdCents));
}
/**
* Prove: average spending in category ≤ budget
*
* Use case: Prove budgeting discipline
*
* @param category Spending category
* @param budgetDollars Maximum budget in dollars
*/
async proveBudgetCompliance(category: string, budgetDollars: number): Promise<ZkProof> {
this.ensureInit();
const budgetCents = Math.round(budgetDollars * 100);
return this.wasmProver!.proveBudgetCompliance(category, BigInt(budgetCents));
}
/**
* Prove: debt-to-income ratio ≤ max%
*
* Use case: Prove creditworthiness
*
* @param monthlyDebtDollars Monthly debt payments in dollars
* @param maxRatioPercent Maximum DTI ratio (e.g., 30 for 30%)
*/
async proveDebtRatio(monthlyDebtDollars: number, maxRatioPercent: number): Promise<ZkProof> {
this.ensureInit();
const debtCents = Math.round(monthlyDebtDollars * 100);
return this.wasmProver!.proveDebtRatio(BigInt(debtCents), BigInt(maxRatioPercent));
}
/**
* Create complete rental application proof bundle
*
* Includes all proofs typically needed for rental application
*
* @param rentDollars Monthly rent
* @param incomeMultiplier Required income multiple (usually 3)
* @param stabilityDays Days of no overdrafts to prove
* @param savingsMonths Months of rent to prove in savings (optional)
*/
async createRentalApplication(
rentDollars: number,
incomeMultiplier: number = 3,
stabilityDays: number = 90,
savingsMonths?: number
): Promise<RentalApplicationProof> {
this.ensureInit();
const rentCents = Math.round(rentDollars * 100);
return this.wasmProver!.createRentalApplication(
BigInt(rentCents),
BigInt(incomeMultiplier),
stabilityDays,
savingsMonths !== undefined ? BigInt(savingsMonths) : undefined
);
}
private ensureInit(): void {
if (!this.initialized || !this.wasmProver) {
throw new Error('Prover not initialized. Call init() first.');
}
}
}
// ============================================================================
// Verifier (Can Run Anywhere)
// ============================================================================
/**
* Verify zero-knowledge proofs.
*
* Verifier learns ONLY that the statement is true.
* Actual numbers remain completely hidden.
*/
export class ZkProofVerifier {
private static initialized = false;
/**
* Initialize the verifier
*/
static async init(): Promise<void> {
if (this.initialized) return;
await init();
this.initialized = true;
}
/**
* Verify a single proof
*
* @param proof The proof to verify (as object or JSON string)
*/
static async verify(proof: ZkProof | string): Promise<VerificationResult> {
await this.init();
const proofJson = typeof proof === 'string' ? proof : JSON.stringify(proof);
return WasmVerifier.verify(proofJson);
}
/**
* Verify a rental application bundle
*/
static async verifyRentalApplication(
application: RentalApplicationProof | string
): Promise<{ all_valid: boolean; results: VerificationResult[] }> {
await this.init();
const appJson = typeof application === 'string' ? application : JSON.stringify(application);
return WasmVerifier.verifyRentalApplication(appJson);
}
/**
* Get human-readable statement from proof
*/
static async getStatement(proof: ZkProof | string): Promise<string> {
await this.init();
const proofJson = typeof proof === 'string' ? proof : JSON.stringify(proof);
return WasmVerifier.getStatement(proofJson);
}
/**
* Check if proof is expired
*/
static async isExpired(proof: ZkProof | string): Promise<boolean> {
await this.init();
const proofJson = typeof proof === 'string' ? proof : JSON.stringify(proof);
return WasmVerifier.isExpired(proofJson);
}
}
// ============================================================================
// Utilities
// ============================================================================
export const ZkProofUtils = {
/**
* Convert proof to shareable URL
*/
toShareableUrl(proof: ZkProof, baseUrl: string = window.location.origin): string {
const proofJson = JSON.stringify(proof);
return ZkUtils.proofToUrl(proofJson, baseUrl + '/verify');
},
/**
* Extract proof from URL parameter
*/
fromUrl(encoded: string): ZkProof {
const json = ZkUtils.proofFromUrl(encoded);
return JSON.parse(json);
},
/**
* Format proof for display
*/
formatProof(proof: ZkProof): string {
return `
┌─────────────────────────────────────────────────┐
│ Zero-Knowledge Proof │
├─────────────────────────────────────────────────┤
│ Type: ${proof.proof_type.padEnd(41)}
│ Statement: ${proof.public_inputs.statement.slice(0, 36).padEnd(36)}
│ Generated: ${new Date(proof.generated_at * 1000).toLocaleDateString().padEnd(36)}
│ Expires: ${proof.expires_at ? new Date(proof.expires_at * 1000).toLocaleDateString().padEnd(38) : 'Never'.padEnd(38)}
│ Proof size: ${(proof.proof_data.length + ' bytes').padEnd(35)}
└─────────────────────────────────────────────────┘
`.trim();
},
/**
* Calculate proof size in bytes
*/
proofSize(proof: ZkProof): number {
return JSON.stringify(proof).length;
},
};
// ============================================================================
// Presets for Common Use Cases
// ============================================================================
/**
* Pre-configured proof generators for common scenarios
*/
export const ZkPresets = {
/**
* Standard rental application (3x income, 90 days stability, 2 months savings)
*/
async rentalApplication(
prover: ZkFinancialProver,
monthlyRent: number
): Promise<RentalApplicationProof> {
return prover.createRentalApplication(monthlyRent, 3, 90, 2);
},
/**
* Loan pre-qualification (income above threshold, DTI under 30%)
*/
async loanPrequalification(
prover: ZkFinancialProver,
minimumIncome: number,
monthlyDebt: number
): Promise<{ incomeProof: ZkProof; dtiProof: ZkProof }> {
const incomeProof = await prover.proveIncomeAbove(minimumIncome);
const dtiProof = await prover.proveDebtRatio(monthlyDebt, 30);
return { incomeProof, dtiProof };
},
/**
* Employment verification (income above minimum)
*/
async employmentVerification(
prover: ZkFinancialProver,
minimumSalary: number
): Promise<ZkProof> {
return prover.proveIncomeAbove(minimumSalary);
},
/**
* Account stability (no overdrafts for 6 months)
*/
async accountStability(prover: ZkFinancialProver): Promise<ZkProof> {
return prover.proveNoOverdrafts(180);
},
};
export default { ZkFinancialProver, ZkProofVerifier, ZkProofUtils, ZkPresets };