yakmesh 2.9.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +637 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/content/api.js +50 -41
- package/content/index.js +1 -2
- package/content/store.js +323 -177
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +274 -114
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +4 -1
- package/mesh/darshan.js +17 -5
- package/mesh/gumba.js +47 -13
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +5 -2
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/yurt.js +72 -17
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/packet-checksum.js +201 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +6 -5
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +4 -3
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +18 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +102 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/scripts/update-docs-nav.cjs +0 -194
- package/update-docs-nav.cjs +0 -18
- package/update-nav.ps1 +0 -16
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yakmesh KARMA-Adaptive Rate Limiting + Input Validation
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* PHILOSOPHY: TRUST ENABLES THROUGHPUT
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Traditional rate limiting: Fixed thresholds (e.g., 100 req/min for everyone).
|
|
9
|
+
* KARMA-adaptive: Throughput scales with earned trust.
|
|
10
|
+
*
|
|
11
|
+
* - Unknown peers: Strict limits (10 req/min)
|
|
12
|
+
* - Low KARMA (0-30): Cautious (25 req/min)
|
|
13
|
+
* - Medium KARMA (31-60): Standard (50 req/min)
|
|
14
|
+
* - High KARMA (61-85): Elevated (100 req/min)
|
|
15
|
+
* - Excellent KARMA (86-100): Trusted (200 req/min)
|
|
16
|
+
*
|
|
17
|
+
* This creates economic incentive: good behavior → higher throughput capacity.
|
|
18
|
+
*
|
|
19
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
* INTEGRATION WITH SANGHA
|
|
21
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
*
|
|
23
|
+
* The rate limiter participates in SANGHA collective attestation:
|
|
24
|
+
* - Reports current load and block counts during circulation
|
|
25
|
+
* - Can trigger collective response to coordinated flood attacks
|
|
26
|
+
* - Receives warnings from other components about suspicious peers
|
|
27
|
+
*
|
|
28
|
+
* @module security/karma-rate-limiter
|
|
29
|
+
* @version 1.0.0
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createLogger } from '../utils/logger.js';
|
|
33
|
+
import { EventEmitter } from 'events';
|
|
34
|
+
|
|
35
|
+
const log = createLogger('security:karma-rate-limiter');
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// CONSTANTS
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/** Rate limit tiers based on KARMA score */
|
|
42
|
+
export const KARMA_TIERS = {
|
|
43
|
+
UNKNOWN: { min: -1, max: -1, limit: 10, window: 60000, label: 'Unknown' },
|
|
44
|
+
HOSTILE: { min: 0, max: 10, limit: 2, window: 60000, label: 'Hostile' },
|
|
45
|
+
LOW: { min: 11, max: 30, limit: 25, window: 60000, label: 'Low' },
|
|
46
|
+
MEDIUM: { min: 31, max: 60, limit: 50, window: 60000, label: 'Medium' },
|
|
47
|
+
HIGH: { min: 61, max: 85, limit: 100, window: 60000, label: 'High' },
|
|
48
|
+
EXCELLENT: { min: 86, max: 100, limit: 200, window: 60000, label: 'Excellent' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Request size limits by content type */
|
|
52
|
+
export const SIZE_LIMITS = {
|
|
53
|
+
json: 256 * 1024, // 256 KB for JSON payloads
|
|
54
|
+
binary: 16 * 1024 * 1024, // 16 MB for binary content
|
|
55
|
+
websocket: 64 * 1024, // 64 KB per WebSocket message
|
|
56
|
+
gossip: 4 * 1024, // 4 KB for gossip messages
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Burst multiplier (allows short bursts above rate limit) */
|
|
60
|
+
const BURST_MULTIPLIER = 2;
|
|
61
|
+
|
|
62
|
+
/** Block duration after exceeding limits (ms) */
|
|
63
|
+
const BLOCK_DURATION = 60000;
|
|
64
|
+
|
|
65
|
+
/** Escalation: each violation increases block duration */
|
|
66
|
+
const BLOCK_ESCALATION = 2;
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// RATE BUCKET
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* RateBucket — Token bucket for a single peer
|
|
74
|
+
*/
|
|
75
|
+
class RateBucket {
|
|
76
|
+
peerId;
|
|
77
|
+
karma;
|
|
78
|
+
tier;
|
|
79
|
+
tokens;
|
|
80
|
+
lastRefill;
|
|
81
|
+
violations;
|
|
82
|
+
blockedUntil;
|
|
83
|
+
|
|
84
|
+
constructor(peerId, karma = -1) {
|
|
85
|
+
this.peerId = peerId;
|
|
86
|
+
this.karma = karma;
|
|
87
|
+
this.tier = this.#getTier(karma);
|
|
88
|
+
this.tokens = this.tier.limit * BURST_MULTIPLIER;
|
|
89
|
+
this.lastRefill = Date.now();
|
|
90
|
+
this.violations = 0;
|
|
91
|
+
this.blockedUntil = 0;
|
|
92
|
+
|
|
93
|
+
Object.seal(this);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#getTier(karma) {
|
|
97
|
+
if (karma < 0) return KARMA_TIERS.UNKNOWN;
|
|
98
|
+
if (karma <= 10) return KARMA_TIERS.HOSTILE;
|
|
99
|
+
if (karma <= 30) return KARMA_TIERS.LOW;
|
|
100
|
+
if (karma <= 60) return KARMA_TIERS.MEDIUM;
|
|
101
|
+
if (karma <= 85) return KARMA_TIERS.HIGH;
|
|
102
|
+
return KARMA_TIERS.EXCELLENT;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Update KARMA score (may change tier)
|
|
107
|
+
*/
|
|
108
|
+
updateKarma(newKarma) {
|
|
109
|
+
const oldTier = this.tier;
|
|
110
|
+
this.karma = newKarma;
|
|
111
|
+
this.tier = this.#getTier(newKarma);
|
|
112
|
+
|
|
113
|
+
if (oldTier !== this.tier) {
|
|
114
|
+
log.debug('Peer tier changed', {
|
|
115
|
+
peerId: this.peerId.slice(0, 16),
|
|
116
|
+
oldTier: oldTier.label,
|
|
117
|
+
newTier: this.tier.label,
|
|
118
|
+
newLimit: this.tier.limit,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Refill tokens based on elapsed time
|
|
125
|
+
*/
|
|
126
|
+
#refill() {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const elapsed = now - this.lastRefill;
|
|
129
|
+
const tokensPerMs = this.tier.limit / this.tier.window;
|
|
130
|
+
const newTokens = Math.floor(elapsed * tokensPerMs);
|
|
131
|
+
|
|
132
|
+
if (newTokens > 0) {
|
|
133
|
+
this.tokens = Math.min(
|
|
134
|
+
this.tokens + newTokens,
|
|
135
|
+
this.tier.limit * BURST_MULTIPLIER
|
|
136
|
+
);
|
|
137
|
+
this.lastRefill = now;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Try to consume tokens
|
|
143
|
+
* @param {number} cost - Tokens to consume (default 1)
|
|
144
|
+
* @returns {{ allowed: boolean, remaining: number, reason?: string }}
|
|
145
|
+
*/
|
|
146
|
+
consume(cost = 1) {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
|
|
149
|
+
// Check if blocked
|
|
150
|
+
if (this.blockedUntil > now) {
|
|
151
|
+
return {
|
|
152
|
+
allowed: false,
|
|
153
|
+
remaining: 0,
|
|
154
|
+
reason: `Blocked for ${Math.ceil((this.blockedUntil - now) / 1000)}s`,
|
|
155
|
+
retryAfter: this.blockedUntil - now,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Refill tokens
|
|
160
|
+
this.#refill();
|
|
161
|
+
|
|
162
|
+
// Check if enough tokens
|
|
163
|
+
if (this.tokens < cost) {
|
|
164
|
+
this.violations++;
|
|
165
|
+
const blockDuration = BLOCK_DURATION * Math.pow(BLOCK_ESCALATION, Math.min(this.violations - 1, 5));
|
|
166
|
+
this.blockedUntil = now + blockDuration;
|
|
167
|
+
|
|
168
|
+
log.warn('Rate limit exceeded — peer blocked', {
|
|
169
|
+
peerId: this.peerId.slice(0, 16),
|
|
170
|
+
tier: this.tier.label,
|
|
171
|
+
violations: this.violations,
|
|
172
|
+
blockDuration: Math.round(blockDuration / 1000) + 's',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
allowed: false,
|
|
177
|
+
remaining: 0,
|
|
178
|
+
reason: `Rate limit exceeded (${this.tier.label} tier: ${this.tier.limit}/min)`,
|
|
179
|
+
retryAfter: blockDuration,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.tokens -= cost;
|
|
184
|
+
return {
|
|
185
|
+
allowed: true,
|
|
186
|
+
remaining: this.tokens,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get current status
|
|
192
|
+
*/
|
|
193
|
+
getStatus() {
|
|
194
|
+
this.#refill();
|
|
195
|
+
return {
|
|
196
|
+
peerId: this.peerId,
|
|
197
|
+
karma: this.karma,
|
|
198
|
+
tier: this.tier.label,
|
|
199
|
+
limit: this.tier.limit,
|
|
200
|
+
remaining: Math.floor(this.tokens),
|
|
201
|
+
violations: this.violations,
|
|
202
|
+
blocked: this.blockedUntil > Date.now(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// INPUT VALIDATOR
|
|
209
|
+
// =============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* InputValidator — Validates and sanitizes incoming data
|
|
213
|
+
*/
|
|
214
|
+
export class InputValidator {
|
|
215
|
+
#schemas;
|
|
216
|
+
|
|
217
|
+
constructor() {
|
|
218
|
+
this.#schemas = new Map();
|
|
219
|
+
this.#registerBuiltinSchemas();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Register built-in validation schemas
|
|
224
|
+
*/
|
|
225
|
+
#registerBuiltinSchemas() {
|
|
226
|
+
// Node ID schema
|
|
227
|
+
this.#schemas.set('nodeId', {
|
|
228
|
+
type: 'string',
|
|
229
|
+
pattern: /^node-[a-z0-9-]+-pq-[A-Za-z0-9]{4}$/,
|
|
230
|
+
minLength: 20,
|
|
231
|
+
maxLength: 100,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Public key (hex)
|
|
235
|
+
this.#schemas.set('publicKey', {
|
|
236
|
+
type: 'string',
|
|
237
|
+
pattern: /^[a-f0-9]+$/i,
|
|
238
|
+
minLength: 64,
|
|
239
|
+
maxLength: 4096,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Signature (hex)
|
|
243
|
+
this.#schemas.set('signature', {
|
|
244
|
+
type: 'string',
|
|
245
|
+
pattern: /^[a-f0-9]+$/i,
|
|
246
|
+
minLength: 128,
|
|
247
|
+
maxLength: 8192,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Hash (SHA3-256 hex)
|
|
251
|
+
this.#schemas.set('hash', {
|
|
252
|
+
type: 'string',
|
|
253
|
+
pattern: /^[a-f0-9]{64}$/i,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Timestamp (Unix ms)
|
|
257
|
+
this.#schemas.set('timestamp', {
|
|
258
|
+
type: 'number',
|
|
259
|
+
min: 0,
|
|
260
|
+
max: Date.now() + 86400000, // Max 1 day in future
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Gossip message
|
|
264
|
+
this.#schemas.set('gossipMessage', {
|
|
265
|
+
type: 'object',
|
|
266
|
+
required: ['type', 'from', 'timestamp'],
|
|
267
|
+
properties: {
|
|
268
|
+
type: { type: 'string', maxLength: 50 },
|
|
269
|
+
from: { $ref: 'nodeId' },
|
|
270
|
+
timestamp: { $ref: 'timestamp' },
|
|
271
|
+
payload: { type: 'object', maxSize: SIZE_LIMITS.gossip },
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// DOKO document
|
|
276
|
+
this.#schemas.set('dokoDocument', {
|
|
277
|
+
type: 'object',
|
|
278
|
+
required: ['version', 'type', 'nodeId', 'publicKey', 'signature'],
|
|
279
|
+
properties: {
|
|
280
|
+
version: { type: 'number', min: 1, max: 10 },
|
|
281
|
+
type: { type: 'string', enum: ['node', 'entity', 'content', 'code', 'system'] },
|
|
282
|
+
nodeId: { $ref: 'nodeId' },
|
|
283
|
+
publicKey: { $ref: 'publicKey' },
|
|
284
|
+
signature: { $ref: 'signature' },
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Register a custom schema
|
|
291
|
+
*/
|
|
292
|
+
registerSchema(name, schema) {
|
|
293
|
+
this.#schemas.set(name, schema);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Validate data against a schema
|
|
298
|
+
* @param {any} data - Data to validate
|
|
299
|
+
* @param {string|object} schemaOrName - Schema name or schema object
|
|
300
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
301
|
+
*/
|
|
302
|
+
validate(data, schemaOrName) {
|
|
303
|
+
const schema = typeof schemaOrName === 'string'
|
|
304
|
+
? this.#schemas.get(schemaOrName)
|
|
305
|
+
: schemaOrName;
|
|
306
|
+
|
|
307
|
+
if (!schema) {
|
|
308
|
+
return { valid: false, errors: [`Unknown schema: ${schemaOrName}`] };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const errors = [];
|
|
312
|
+
this.#validateValue(data, schema, '', errors);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
valid: errors.length === 0,
|
|
316
|
+
errors,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#validateValue(value, schema, path, errors) {
|
|
321
|
+
// Handle schema references
|
|
322
|
+
if (schema.$ref) {
|
|
323
|
+
const refSchema = this.#schemas.get(schema.$ref);
|
|
324
|
+
if (!refSchema) {
|
|
325
|
+
errors.push(`${path}: Unknown schema reference: ${schema.$ref}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.#validateValue(value, refSchema, path, errors);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Type checking
|
|
333
|
+
if (schema.type) {
|
|
334
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
335
|
+
if (actualType !== schema.type) {
|
|
336
|
+
errors.push(`${path}: Expected ${schema.type}, got ${actualType}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// String validations
|
|
342
|
+
if (schema.type === 'string' && typeof value === 'string') {
|
|
343
|
+
if (schema.minLength && value.length < schema.minLength) {
|
|
344
|
+
errors.push(`${path}: String too short (min: ${schema.minLength})`);
|
|
345
|
+
}
|
|
346
|
+
if (schema.maxLength && value.length > schema.maxLength) {
|
|
347
|
+
errors.push(`${path}: String too long (max: ${schema.maxLength})`);
|
|
348
|
+
}
|
|
349
|
+
if (schema.pattern && !schema.pattern.test(value)) {
|
|
350
|
+
errors.push(`${path}: String doesn't match pattern`);
|
|
351
|
+
}
|
|
352
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
353
|
+
errors.push(`${path}: Value not in allowed list: ${schema.enum.join(', ')}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Number validations
|
|
358
|
+
if (schema.type === 'number' && typeof value === 'number') {
|
|
359
|
+
if (schema.min !== undefined && value < schema.min) {
|
|
360
|
+
errors.push(`${path}: Number too small (min: ${schema.min})`);
|
|
361
|
+
}
|
|
362
|
+
if (schema.max !== undefined && value > schema.max) {
|
|
363
|
+
errors.push(`${path}: Number too large (max: ${schema.max})`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Object validations
|
|
368
|
+
if (schema.type === 'object' && typeof value === 'object' && value !== null) {
|
|
369
|
+
// Check required properties
|
|
370
|
+
if (schema.required) {
|
|
371
|
+
for (const prop of schema.required) {
|
|
372
|
+
if (!(prop in value)) {
|
|
373
|
+
errors.push(`${path}: Missing required property: ${prop}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Validate properties
|
|
379
|
+
if (schema.properties) {
|
|
380
|
+
for (const [prop, propSchema] of Object.entries(schema.properties)) {
|
|
381
|
+
if (prop in value) {
|
|
382
|
+
this.#validateValue(value[prop], propSchema, `${path}.${prop}`, errors);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Check max size
|
|
388
|
+
if (schema.maxSize) {
|
|
389
|
+
const size = JSON.stringify(value).length;
|
|
390
|
+
if (size > schema.maxSize) {
|
|
391
|
+
errors.push(`${path}: Object too large (max: ${schema.maxSize} bytes)`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Sanitize a string for safe use
|
|
399
|
+
*/
|
|
400
|
+
sanitizeString(input, maxLength = 1000) {
|
|
401
|
+
if (typeof input !== 'string') return '';
|
|
402
|
+
|
|
403
|
+
// Remove null bytes and control characters
|
|
404
|
+
let sanitized = input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
405
|
+
|
|
406
|
+
// Truncate
|
|
407
|
+
if (sanitized.length > maxLength) {
|
|
408
|
+
sanitized = sanitized.slice(0, maxLength);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return sanitized;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Validate request size
|
|
416
|
+
*/
|
|
417
|
+
validateSize(data, type = 'json') {
|
|
418
|
+
const maxSize = SIZE_LIMITS[type] || SIZE_LIMITS.json;
|
|
419
|
+
const size = typeof data === 'string'
|
|
420
|
+
? data.length
|
|
421
|
+
: JSON.stringify(data).length;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
valid: size <= maxSize,
|
|
425
|
+
size,
|
|
426
|
+
maxSize,
|
|
427
|
+
error: size > maxSize ? `Payload too large: ${size} > ${maxSize} bytes` : null,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// =============================================================================
|
|
433
|
+
// KARMA RATE LIMITER
|
|
434
|
+
// =============================================================================
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* KarmaRateLimiter — KARMA-adaptive rate limiting with SANGHA integration
|
|
438
|
+
*/
|
|
439
|
+
export class KarmaRateLimiter extends EventEmitter {
|
|
440
|
+
#buckets;
|
|
441
|
+
#karmaTrust;
|
|
442
|
+
#sangha;
|
|
443
|
+
#validator;
|
|
444
|
+
#stats;
|
|
445
|
+
|
|
446
|
+
constructor() {
|
|
447
|
+
super();
|
|
448
|
+
this.#buckets = new Map();
|
|
449
|
+
this.#karmaTrust = null;
|
|
450
|
+
this.#sangha = null;
|
|
451
|
+
this.#validator = new InputValidator();
|
|
452
|
+
this.#stats = {
|
|
453
|
+
allowed: 0,
|
|
454
|
+
blocked: 0,
|
|
455
|
+
validated: 0,
|
|
456
|
+
rejected: 0,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
Object.seal(this);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Bind KARMA trust model for reputation lookups
|
|
464
|
+
*/
|
|
465
|
+
bindKarmaTrust(karmaTrust) {
|
|
466
|
+
this.#karmaTrust = karmaTrust;
|
|
467
|
+
log.info('Rate limiter bound to KARMA trust model');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Bind SANGHA for collective response
|
|
472
|
+
*/
|
|
473
|
+
bindSangha(sangha) {
|
|
474
|
+
this.#sangha = sangha;
|
|
475
|
+
log.info('Rate limiter bound to SANGHA collective');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get or create rate bucket for a peer
|
|
480
|
+
*/
|
|
481
|
+
#getBucket(peerId) {
|
|
482
|
+
let bucket = this.#buckets.get(peerId);
|
|
483
|
+
|
|
484
|
+
if (!bucket) {
|
|
485
|
+
// Get KARMA score if available
|
|
486
|
+
const karma = this.#karmaTrust?.getTrustScore?.(peerId) ?? -1;
|
|
487
|
+
bucket = new RateBucket(peerId, karma);
|
|
488
|
+
this.#buckets.set(peerId, bucket);
|
|
489
|
+
|
|
490
|
+
log.debug('Created rate bucket', {
|
|
491
|
+
peerId: peerId.slice(0, 16),
|
|
492
|
+
karma,
|
|
493
|
+
tier: bucket.tier.label,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return bucket;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Check if request is allowed
|
|
502
|
+
* @param {string} peerId - Peer making the request
|
|
503
|
+
* @param {number} cost - Request cost (default 1)
|
|
504
|
+
* @returns {{ allowed: boolean, remaining: number, tier: string, reason?: string }}
|
|
505
|
+
*/
|
|
506
|
+
checkLimit(peerId, cost = 1) {
|
|
507
|
+
const bucket = this.#getBucket(peerId);
|
|
508
|
+
|
|
509
|
+
// Update KARMA in case it changed
|
|
510
|
+
if (this.#karmaTrust) {
|
|
511
|
+
const currentKarma = this.#karmaTrust.getTrustScore?.(peerId) ?? -1;
|
|
512
|
+
bucket.updateKarma(currentKarma);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const result = bucket.consume(cost);
|
|
516
|
+
|
|
517
|
+
if (result.allowed) {
|
|
518
|
+
this.#stats.allowed++;
|
|
519
|
+
} else {
|
|
520
|
+
this.#stats.blocked++;
|
|
521
|
+
|
|
522
|
+
// Emit event for monitoring
|
|
523
|
+
this.emit('blocked', { peerId, reason: result.reason });
|
|
524
|
+
|
|
525
|
+
// Alert SANGHA if many peers are being blocked (possible attack)
|
|
526
|
+
if (this.#stats.blocked % 100 === 0) {
|
|
527
|
+
log.warn('High block rate detected — possible flood attack', {
|
|
528
|
+
blocked: this.#stats.blocked,
|
|
529
|
+
allowed: this.#stats.allowed,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
allowed: result.allowed,
|
|
536
|
+
remaining: result.remaining,
|
|
537
|
+
tier: bucket.tier.label,
|
|
538
|
+
reason: result.reason,
|
|
539
|
+
retryAfter: result.retryAfter,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Validate and rate-limit a request
|
|
545
|
+
* @param {string} peerId - Peer making the request
|
|
546
|
+
* @param {any} data - Request data
|
|
547
|
+
* @param {string} schemaName - Schema to validate against
|
|
548
|
+
* @param {number} cost - Request cost
|
|
549
|
+
* @returns {{ allowed: boolean, valid: boolean, errors: string[], tier: string }}
|
|
550
|
+
*/
|
|
551
|
+
validateAndLimit(peerId, data, schemaName, cost = 1) {
|
|
552
|
+
// First check rate limit
|
|
553
|
+
const limitResult = this.checkLimit(peerId, cost);
|
|
554
|
+
if (!limitResult.allowed) {
|
|
555
|
+
return {
|
|
556
|
+
allowed: false,
|
|
557
|
+
valid: false,
|
|
558
|
+
errors: [limitResult.reason],
|
|
559
|
+
tier: limitResult.tier,
|
|
560
|
+
retryAfter: limitResult.retryAfter,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Then validate
|
|
565
|
+
const validation = this.#validator.validate(data, schemaName);
|
|
566
|
+
|
|
567
|
+
if (validation.valid) {
|
|
568
|
+
this.#stats.validated++;
|
|
569
|
+
} else {
|
|
570
|
+
this.#stats.rejected++;
|
|
571
|
+
|
|
572
|
+
log.debug('Validation failed', {
|
|
573
|
+
peerId: peerId.slice(0, 16),
|
|
574
|
+
schema: schemaName,
|
|
575
|
+
errors: validation.errors.slice(0, 3),
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
allowed: true,
|
|
581
|
+
valid: validation.valid,
|
|
582
|
+
errors: validation.errors,
|
|
583
|
+
tier: limitResult.tier,
|
|
584
|
+
remaining: limitResult.remaining,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Check payload size
|
|
590
|
+
* @param {string} peerId
|
|
591
|
+
* @param {any} data
|
|
592
|
+
* @param {string} type
|
|
593
|
+
* @returns {{ allowed: boolean, valid: boolean, error?: string }}
|
|
594
|
+
*/
|
|
595
|
+
checkSize(peerId, data, type = 'json') {
|
|
596
|
+
const limitResult = this.checkLimit(peerId, 0); // Don't consume tokens, just check block
|
|
597
|
+
if (!limitResult.allowed) {
|
|
598
|
+
return { allowed: false, valid: false, error: limitResult.reason };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const sizeCheck = this.#validator.validateSize(data, type);
|
|
602
|
+
return {
|
|
603
|
+
allowed: true,
|
|
604
|
+
valid: sizeCheck.valid,
|
|
605
|
+
error: sizeCheck.error,
|
|
606
|
+
size: sizeCheck.size,
|
|
607
|
+
maxSize: sizeCheck.maxSize,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get the input validator
|
|
613
|
+
*/
|
|
614
|
+
getValidator() {
|
|
615
|
+
return this.#validator;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Get state for SANGHA attestation
|
|
620
|
+
*/
|
|
621
|
+
getState() {
|
|
622
|
+
return {
|
|
623
|
+
component: 'rate-limiter',
|
|
624
|
+
buckets: this.#buckets.size,
|
|
625
|
+
stats: { ...this.#stats },
|
|
626
|
+
tiers: Object.fromEntries(
|
|
627
|
+
Object.entries(KARMA_TIERS).map(([k, v]) => [k, v.limit])
|
|
628
|
+
),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Get status for API
|
|
634
|
+
*/
|
|
635
|
+
getStatus() {
|
|
636
|
+
// Count peers by tier
|
|
637
|
+
const byTier = {};
|
|
638
|
+
for (const tier of Object.values(KARMA_TIERS)) {
|
|
639
|
+
byTier[tier.label] = 0;
|
|
640
|
+
}
|
|
641
|
+
for (const bucket of this.#buckets.values()) {
|
|
642
|
+
byTier[bucket.tier.label]++;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
trackedPeers: this.#buckets.size,
|
|
647
|
+
byTier,
|
|
648
|
+
stats: { ...this.#stats },
|
|
649
|
+
karmaBound: !!this.#karmaTrust,
|
|
650
|
+
sanghaBound: !!this.#sangha,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Cleanup old buckets (call periodically)
|
|
656
|
+
*/
|
|
657
|
+
cleanup(maxAge = 3600000) {
|
|
658
|
+
const now = Date.now();
|
|
659
|
+
let removed = 0;
|
|
660
|
+
|
|
661
|
+
for (const [peerId, bucket] of this.#buckets) {
|
|
662
|
+
// Remove buckets that haven't been used recently and aren't blocked
|
|
663
|
+
if (bucket.lastRefill < now - maxAge && bucket.blockedUntil < now) {
|
|
664
|
+
this.#buckets.delete(peerId);
|
|
665
|
+
removed++;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (removed > 0) {
|
|
670
|
+
log.debug('Cleaned up old rate buckets', { removed, remaining: this.#buckets.size });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// =============================================================================
|
|
676
|
+
// SINGLETON & EXPORTS
|
|
677
|
+
// =============================================================================
|
|
678
|
+
|
|
679
|
+
let _instance = null;
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get the KarmaRateLimiter singleton
|
|
683
|
+
* @returns {KarmaRateLimiter}
|
|
684
|
+
*/
|
|
685
|
+
export function getKarmaRateLimiter() {
|
|
686
|
+
if (!_instance) {
|
|
687
|
+
_instance = new KarmaRateLimiter();
|
|
688
|
+
}
|
|
689
|
+
return _instance;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export default KarmaRateLimiter;
|