Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
21
vendor/ruvector/examples/edge/pkg/LICENSE
vendored
Normal file
21
vendor/ruvector/examples/edge/pkg/LICENSE
vendored
Normal 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.
|
||||
1315
vendor/ruvector/examples/edge/pkg/README.md
vendored
Normal file
1315
vendor/ruvector/examples/edge/pkg/README.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2262
vendor/ruvector/examples/edge/pkg/generator.html
vendored
Normal file
2262
vendor/ruvector/examples/edge/pkg/generator.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
74
vendor/ruvector/examples/edge/pkg/package.json
vendored
Normal file
74
vendor/ruvector/examples/edge/pkg/package.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
795
vendor/ruvector/examples/edge/pkg/plaid-demo.html
vendored
Normal file
795
vendor/ruvector/examples/edge/pkg/plaid-demo.html
vendored
Normal 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>
|
||||
755
vendor/ruvector/examples/edge/pkg/plaid-local-learner.ts
vendored
Normal file
755
vendor/ruvector/examples/edge/pkg/plaid-local-learner.ts
vendored
Normal 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;
|
||||
351
vendor/ruvector/examples/edge/pkg/ruvector_edge.d.ts
vendored
Normal file
351
vendor/ruvector/examples/edge/pkg/ruvector_edge.d.ts
vendored
Normal 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>;
|
||||
1452
vendor/ruvector/examples/edge/pkg/ruvector_edge.js
vendored
Normal file
1452
vendor/ruvector/examples/edge/pkg/ruvector_edge.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
vendor/ruvector/examples/edge/pkg/ruvector_edge_bg.wasm
vendored
Normal file
BIN
vendor/ruvector/examples/edge/pkg/ruvector_edge_bg.wasm
vendored
Normal file
Binary file not shown.
71
vendor/ruvector/examples/edge/pkg/ruvector_edge_bg.wasm.d.ts
vendored
Normal file
71
vendor/ruvector/examples/edge/pkg/ruvector_edge_bg.wasm.d.ts
vendored
Normal 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;
|
||||
254
vendor/ruvector/examples/edge/pkg/worker-pool.js
vendored
Normal file
254
vendor/ruvector/examples/edge/pkg/worker-pool.js
vendored
Normal 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;
|
||||
184
vendor/ruvector/examples/edge/pkg/worker.js
vendored
Normal file
184
vendor/ruvector/examples/edge/pkg/worker.js
vendored
Normal 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
|
||||
});
|
||||
}
|
||||
584
vendor/ruvector/examples/edge/pkg/zk-demo.html
vendored
Normal file
584
vendor/ruvector/examples/edge/pkg/zk-demo.html
vendored
Normal 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>
|
||||
425
vendor/ruvector/examples/edge/pkg/zk-financial-proofs.ts
vendored
Normal file
425
vendor/ruvector/examples/edge/pkg/zk-financial-proofs.ts
vendored
Normal 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 };
|
||||
Reference in New Issue
Block a user