yakmesh 2.8.2 → 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/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- 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 +334 -74
- 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 +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- 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/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- 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/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -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 +28 -13
- 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-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- 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 +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -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/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
package/server/index.js
CHANGED
|
@@ -13,10 +13,20 @@
|
|
|
13
13
|
|
|
14
14
|
import express from 'express';
|
|
15
15
|
import rateLimit from 'express-rate-limit';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
16
17
|
import { existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { networkInterfaces } from 'os';
|
|
20
|
+
import { WebSocketServer } from 'ws';
|
|
17
21
|
import { createLogger } from '../utils/logger.js';
|
|
22
|
+
import * as accel from '../utils/accel.js';
|
|
23
|
+
import * as steadywatch from '../security/steadywatch.js';
|
|
24
|
+
|
|
25
|
+
// Embedded Caddy web server for HTTPS/443 reverse proxy
|
|
26
|
+
import { YakmeshWebServer } from '../webserver/index.js';
|
|
18
27
|
|
|
19
28
|
const log = createLogger('server:main');
|
|
29
|
+
const peerTag = (id) => id?.split('-pq-').pop() || id?.slice?.(-8) || String(id);
|
|
20
30
|
import { NodeIdentity } from '../identity/node-key.js';
|
|
21
31
|
import { MeshNetwork } from '../mesh/network.js';
|
|
22
32
|
import { ReplicationEngine } from '../database/replication.js';
|
|
@@ -28,16 +38,17 @@ import { ContentStore, createContentAPI } from '../content/index.js';
|
|
|
28
38
|
// Embedded documentation (hardcoded, hash-verified)
|
|
29
39
|
import { getDocsFile, serveDocsFile, getBundleInfo } from '../embedded-docs/index.js';
|
|
30
40
|
|
|
31
|
-
// Annex
|
|
32
|
-
|
|
41
|
+
// Annex lives in mesh/network.js — single instance, no duplication
|
|
42
|
+
// ServerAnnexSession for client-facing WS (KOMM channel encryption)
|
|
43
|
+
import { ServerAnnexSession, ANNEX_HANDSHAKE_TYPE } from './crypto/annex.js';
|
|
33
44
|
|
|
34
45
|
// SHERPA - Secure Hidden Endpoint Resolution Path Architecture
|
|
35
46
|
import { SherpaDiscovery, createBeaconMiddleware } from '../mesh/sherpa-discovery.js';
|
|
36
47
|
|
|
37
48
|
// Oracle system imports
|
|
38
|
-
import {
|
|
39
|
-
getOracle,
|
|
40
|
-
CodeProofProtocol,
|
|
49
|
+
import {
|
|
50
|
+
getOracle,
|
|
51
|
+
CodeProofProtocol,
|
|
41
52
|
ConsensusEngine,
|
|
42
53
|
ContentState,
|
|
43
54
|
GenesisNetworkV2,
|
|
@@ -45,11 +56,13 @@ import {
|
|
|
45
56
|
lockCodebase,
|
|
46
57
|
unlockCodebase,
|
|
47
58
|
setupUnlockOnExit,
|
|
59
|
+
onTamper,
|
|
60
|
+
getTamperEvents,
|
|
48
61
|
} from '../oracle/index.js';
|
|
49
62
|
|
|
50
63
|
// Time source imports
|
|
51
|
-
import {
|
|
52
|
-
TimeSourceDetector,
|
|
64
|
+
import {
|
|
65
|
+
TimeSourceDetector,
|
|
53
66
|
getTimeSourceDetector,
|
|
54
67
|
createPhaseConfig,
|
|
55
68
|
detectTimeSources,
|
|
@@ -57,11 +70,11 @@ import {
|
|
|
57
70
|
import { setTimeSourceConfig, getActiveConfig } from '../oracle/phase-epoch.js';
|
|
58
71
|
|
|
59
72
|
// v2.0 Security imports - NAMCHE and DOKO
|
|
60
|
-
import NamcheGateway, {
|
|
73
|
+
import NamcheGateway, {
|
|
61
74
|
DOKO_TYPES as NAMCHE_DOKO_TYPES,
|
|
62
|
-
VERIFY_RESULT
|
|
75
|
+
VERIFY_RESULT
|
|
63
76
|
} from '../security/namche-gateway.js';
|
|
64
|
-
import {
|
|
77
|
+
import {
|
|
65
78
|
DOKO_TYPES as DOKOTypes,
|
|
66
79
|
DOKODocument,
|
|
67
80
|
DOKOGenerator,
|
|
@@ -87,7 +100,7 @@ const GATE_NAMES = [
|
|
|
87
100
|
];
|
|
88
101
|
|
|
89
102
|
// YAK:// Protocol Handler
|
|
90
|
-
import YakProtocolHandler, {
|
|
103
|
+
import YakProtocolHandler, {
|
|
91
104
|
createProtocolEndpoints,
|
|
92
105
|
parseYakUrl,
|
|
93
106
|
yakToHttp,
|
|
@@ -96,13 +109,91 @@ import YakProtocolHandler, {
|
|
|
96
109
|
BUILTIN_ROUTES
|
|
97
110
|
} from '../protocol/yak-protocol.js';
|
|
98
111
|
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
113
|
+
// KOMM STACK — Chat, Voice, Rooms, Access Control
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
// KATHA — Chat messaging (text, reactions, typing, threads, read receipts)
|
|
117
|
+
import { KathaHub, KATHA_CONFIG } from '../mesh/katha.js';
|
|
118
|
+
|
|
119
|
+
// VANI — WebRTC voice/video calling with mesh signaling
|
|
120
|
+
import { VaniHub, VANI_CONFIG, MEDIA_TYPE, CALL_STATE } from '../mesh/vani.js';
|
|
121
|
+
|
|
122
|
+
// YURT — Decentralized room directory and discovery
|
|
123
|
+
import { YurtHub, YURT_CONFIG } from '../mesh/yurt.js';
|
|
124
|
+
|
|
125
|
+
// GUMBA — Cryptographic access control (proofs, not keys)
|
|
126
|
+
import { GumbaHub, GUMBA_CONFIG } from '../mesh/gumba.js';
|
|
127
|
+
|
|
128
|
+
// KOMM API router and gossip wiring
|
|
129
|
+
import { createKommAPI, wireKommGossip } from './komm-api.js';
|
|
130
|
+
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
// DARSHAN — Content Streaming (view, don't copy)
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
134
|
+
import { DarshanGateway, DARSHAN_CONFIG } from '../mesh/darshan.js';
|
|
135
|
+
import { createDarshanAPI, wireDarshanGossip } from './darshan-api.js';
|
|
136
|
+
|
|
137
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
// NAKPAK — Post-Quantum Onion Routing
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
140
|
+
import { NakpakRouter, NAKPAK_CONFIG } from '../mesh/nakpak-routing.js';
|
|
141
|
+
|
|
142
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
143
|
+
// SAKSHI — Observational Witness Consensus
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
145
|
+
import { NodeWitness, ObservationResult, BehaviorVelocityMonitor, BEHAVIOR_DIMENSION, VELOCITY_ALERT } from '../security/sakshi.js';
|
|
146
|
+
|
|
147
|
+
// KARMA Trust Model — SAKSHI observations feed into trust assessment
|
|
148
|
+
import { KarmaTrustModel, KarmaLevel } from '../security/hybrid-trust.js';
|
|
149
|
+
|
|
150
|
+
// SANGHA — Unified Component Attestation (collective security)
|
|
151
|
+
import { getSangha, joinSangha, SANGHA_COMPONENT } from '../security/sangha.js';
|
|
152
|
+
|
|
153
|
+
// FS Hardening — File integrity with SANGHA-FS integration
|
|
154
|
+
import { getFSHardening, PROTECTION_LEVEL } from '../security/fs-hardening.js';
|
|
155
|
+
|
|
156
|
+
// Memory Safety — Circulating canaries for memory integrity
|
|
157
|
+
import { getMemorySafety } from '../security/memory-safety.js';
|
|
158
|
+
|
|
159
|
+
// Temporal Signing — GPS-bound code signatures with auto-expiry
|
|
160
|
+
import { getTemporalSigner, TemporalSignature } from '../security/temporal-signing.js';
|
|
161
|
+
|
|
162
|
+
// KARMA Rate Limiter — KARMA-adaptive rate limiting with input validation
|
|
163
|
+
import { getKarmaRateLimiter, KARMA_TIERS, SIZE_LIMITS } from '../security/karma-rate-limiter.js';
|
|
164
|
+
|
|
165
|
+
// Secure Config — Oracle-attested configuration management
|
|
166
|
+
import { getSecureConfig, PROFILE_LEVEL, SECURE_DEFAULTS } from '../security/secure-config.js';
|
|
167
|
+
|
|
168
|
+
// TRIBHUJ — Balanced ternary for KARMA trit mapping
|
|
169
|
+
import { POSITIVE, NEUTRAL, NEGATIVE, TritState } from '../oracle/tribhuj.js';
|
|
170
|
+
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
172
|
+
// TERNARY HARMONIZATION — SST × YPC-27 × 144T × ML
|
|
173
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
174
|
+
|
|
175
|
+
// YPC27_SST — SST seed rotation for YPC-27 checksums
|
|
176
|
+
import { YPC27_SST } from '../oracle/ypc27.js';
|
|
177
|
+
|
|
178
|
+
// Batch checksum verification (GPU-accelerated)
|
|
179
|
+
import { batchChecksumVerifier, BatchChecksumVerifier } from '../oracle/packet-checksum.js';
|
|
180
|
+
|
|
181
|
+
// Ternary ML — quantized inference & trust classification
|
|
182
|
+
import { TernaryInferenceAdapter } from '../oracle/ternary-ml.js';
|
|
183
|
+
|
|
184
|
+
// 144T — Hierarchical ternary mesh addressing
|
|
185
|
+
import { TritAddress, TernaryRoutingTable, hexIdToAddress, TierName } from '../oracle/ternary-144t.js';
|
|
186
|
+
|
|
187
|
+
// Time API — HTTP bridge to MA-902 GPS time server (serves on port 3099)
|
|
188
|
+
import { startTimeApi, stopTimeApi } from '../oracle/time-api.js';
|
|
189
|
+
|
|
99
190
|
// Helper: Format uptime in human-readable format
|
|
100
191
|
function formatUptime(seconds) {
|
|
101
192
|
const days = Math.floor(seconds / 86400);
|
|
102
193
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
103
194
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
104
195
|
const secs = seconds % 60;
|
|
105
|
-
|
|
196
|
+
|
|
106
197
|
if (days > 0) return `${days}d ${hours}h ${mins}m`;
|
|
107
198
|
if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
|
|
108
199
|
if (mins > 0) return `${mins}m ${secs}s`;
|
|
@@ -133,35 +224,92 @@ const DEFAULT_CONFIG = {
|
|
|
133
224
|
};
|
|
134
225
|
|
|
135
226
|
/**
|
|
136
|
-
* Load configuration
|
|
227
|
+
* Load configuration.
|
|
228
|
+
*
|
|
229
|
+
* SECURITY: Config MUST be loaded from the codebase-local yakmesh.config.js.
|
|
230
|
+
* The Validation Oracle hashes ALL .js files — config included — so any node
|
|
231
|
+
* loading a different config file would compute a different genesis hash and
|
|
232
|
+
* be rejected by the mesh. Never allow runtime config file injection.
|
|
233
|
+
*
|
|
234
|
+
* Resolution order:
|
|
235
|
+
* 1. CLI argument: --config <path> (for production deployments)
|
|
236
|
+
* 2. Default: ./yakmesh.config.js (byte-identical on every node)
|
|
237
|
+
*
|
|
238
|
+
* Runtime overrides via env vars (applied AFTER config load, never touch files):
|
|
239
|
+
* YAKMESH_HTTP_PORT — override network.httpPort
|
|
240
|
+
* YAKMESH_WS_PORT — override network.wsPort
|
|
241
|
+
* YAKMESH_DATA_DIR — override database.path directory
|
|
242
|
+
* YAKMESH_BOOTSTRAP — override bootstrap peer list (comma-separated ws:// URLs)
|
|
243
|
+
* YAKMESH_RELAY_PEERS — auto-register with relay endpoints at startup (comma-separated https:// URLs)
|
|
137
244
|
*/
|
|
138
245
|
async function loadConfig() {
|
|
139
|
-
// Check for --config argument
|
|
246
|
+
// 1. Check for --config CLI argument (operator-controlled, not env-injectable)
|
|
140
247
|
const configArgIndex = process.argv.findIndex(arg => arg === '--config' || arg === '-c');
|
|
141
248
|
let configPath = './yakmesh.config.js';
|
|
142
|
-
|
|
249
|
+
|
|
143
250
|
if (configArgIndex !== -1 && process.argv[configArgIndex + 1]) {
|
|
144
251
|
configPath = process.argv[configArgIndex + 1];
|
|
145
|
-
log.info(`📋
|
|
252
|
+
log.info(`📋 Config source: CLI --config ${configPath}`);
|
|
146
253
|
}
|
|
147
|
-
|
|
148
|
-
|
|
254
|
+
|
|
255
|
+
let config = { ...DEFAULT_CONFIG };
|
|
256
|
+
|
|
257
|
+
// Load config from the resolved path
|
|
149
258
|
if (existsSync(configPath)) {
|
|
150
259
|
// Handle both absolute and relative paths
|
|
151
260
|
const isAbsolute = configPath.startsWith('/') || /^[A-Z]:/i.test(configPath);
|
|
152
|
-
const importPath = isAbsolute
|
|
261
|
+
const importPath = isAbsolute
|
|
153
262
|
? `file://${configPath.replace(/\\/g, '/')}`
|
|
154
263
|
: `../${configPath.replace('./', '')}`;
|
|
155
264
|
const { default: userConfig } = await import(importPath);
|
|
156
|
-
|
|
265
|
+
config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
266
|
+
} else {
|
|
267
|
+
log.warn(`⚠️ Config file not found: ${configPath} — using defaults`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Apply env var overrides (allows multi-node on same machine
|
|
271
|
+
// WITHOUT modifying the config file — config MUST stay byte-identical
|
|
272
|
+
// for oracle hash integrity)
|
|
273
|
+
if (process.env.YAKMESH_HTTP_PORT) {
|
|
274
|
+
config.network = { ...config.network, httpPort: parseInt(process.env.YAKMESH_HTTP_PORT, 10) };
|
|
275
|
+
}
|
|
276
|
+
if (process.env.YAKMESH_WS_PORT) {
|
|
277
|
+
config.network = { ...config.network, wsPort: parseInt(process.env.YAKMESH_WS_PORT, 10) };
|
|
278
|
+
}
|
|
279
|
+
if (process.env.YAKMESH_DATA_DIR) {
|
|
280
|
+
config.database = { ...config.database, path: `${process.env.YAKMESH_DATA_DIR}/yakmesh.db` };
|
|
281
|
+
}
|
|
282
|
+
if (process.env.YAKMESH_BOOTSTRAP) {
|
|
283
|
+
// Comma-separated WS URLs, e.g. ws://localhost:9011,ws://localhost:9012
|
|
284
|
+
config.bootstrap = process.env.YAKMESH_BOOTSTRAP
|
|
285
|
+
.split(',')
|
|
286
|
+
.map(s => s.trim())
|
|
287
|
+
.filter(Boolean);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (process.env.YAKMESH_RELAY_PEERS) {
|
|
291
|
+
// Comma-separated HTTPS relay URLs, e.g. https://yakmesh.dev/mesh/relay.php
|
|
292
|
+
config.relayPeers = process.env.YAKMESH_RELAY_PEERS
|
|
293
|
+
.split(',')
|
|
294
|
+
.map(s => s.trim())
|
|
295
|
+
.filter(Boolean);
|
|
157
296
|
}
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
297
|
+
|
|
298
|
+
// Caddy web server config (HTTPS/443 reverse proxy)
|
|
299
|
+
// YAKMESH_DOMAIN=yakmesh.dev enables auto-HTTPS via Let's Encrypt
|
|
300
|
+
// YAKMESH_ACME_EMAIL=admin@yakmesh.dev for cert notifications
|
|
301
|
+
if (process.env.YAKMESH_DOMAIN) {
|
|
302
|
+
config.caddy = {
|
|
303
|
+
enabled: true,
|
|
304
|
+
domain: process.env.YAKMESH_DOMAIN,
|
|
305
|
+
autoHttps: true,
|
|
306
|
+
acmeEmail: process.env.YAKMESH_ACME_EMAIL || null,
|
|
307
|
+
nodeHttpPort: config.network?.httpPort || 3080,
|
|
308
|
+
nodeWsPort: config.network?.wsPort || 9080,
|
|
309
|
+
};
|
|
163
310
|
}
|
|
164
|
-
|
|
311
|
+
|
|
312
|
+
return config;
|
|
165
313
|
}
|
|
166
314
|
|
|
167
315
|
/**
|
|
@@ -178,34 +326,73 @@ export class YakmeshNode {
|
|
|
178
326
|
this.http = null;
|
|
179
327
|
this.boundHttpPort = null; // Actual bound port (may differ if fallback used)
|
|
180
328
|
this.app = null; // Store Express app for PeerQuanta endpoints
|
|
181
|
-
|
|
329
|
+
|
|
182
330
|
// Oracle system
|
|
183
331
|
this.oracle = null;
|
|
184
332
|
this.codeProof = null;
|
|
185
333
|
this.consensus = null;
|
|
186
|
-
|
|
334
|
+
|
|
187
335
|
// Content store for public delivery
|
|
188
336
|
this.contentStore = null;
|
|
189
|
-
|
|
190
|
-
// Annex
|
|
191
|
-
|
|
192
|
-
|
|
337
|
+
|
|
338
|
+
// Annex lives in mesh.annex — single instance managed by mesh layer
|
|
339
|
+
|
|
340
|
+
// KOMM Stack — chat, voice, rooms, access control
|
|
341
|
+
this.kathaHub = null;
|
|
342
|
+
this.vaniHub = null;
|
|
343
|
+
this.yurtHub = null;
|
|
344
|
+
this.gumbaHub = null;
|
|
345
|
+
|
|
346
|
+
// DARSHAN — content streaming
|
|
347
|
+
this.darshanGateway = null;
|
|
348
|
+
|
|
349
|
+
// NAKPAK — onion routing
|
|
350
|
+
this.nakpakRouter = null;
|
|
351
|
+
|
|
352
|
+
// SAKSHI — witness consensus
|
|
353
|
+
this.sakshiWitness = null;
|
|
354
|
+
this.velocityMonitor = null;
|
|
355
|
+
|
|
356
|
+
// KARMA — trust model (fed by SAKSHI observations)
|
|
357
|
+
this.karmaModel = null;
|
|
358
|
+
|
|
359
|
+
// KOMM WebSocket (real-time KATHA/VANI)
|
|
360
|
+
this.kommWss = null;
|
|
361
|
+
|
|
193
362
|
// Time source detector
|
|
194
363
|
this.timeSource = null;
|
|
195
|
-
|
|
364
|
+
|
|
196
365
|
// Geographic proof service (v2.5.0)
|
|
197
366
|
this.geoProofService = null;
|
|
198
|
-
|
|
367
|
+
|
|
199
368
|
// iO Network Identity (hash obfuscation)
|
|
200
369
|
this.genesisNetwork = null;
|
|
201
|
-
|
|
370
|
+
|
|
371
|
+
// SANGHA — collective component attestation
|
|
372
|
+
this.sangha = null;
|
|
373
|
+
|
|
374
|
+
// FS Hardening — file integrity with SANGHA-FS
|
|
375
|
+
this.fsHardening = null;
|
|
376
|
+
|
|
377
|
+
// Memory Safety — circulating canaries
|
|
378
|
+
this.memorySafety = null;
|
|
379
|
+
|
|
380
|
+
// Temporal Signing — GPS-bound code signatures
|
|
381
|
+
this.temporalSigner = null;
|
|
382
|
+
|
|
383
|
+
// KARMA Rate Limiter — adaptive rate limiting
|
|
384
|
+
this.rateLimiter = null;
|
|
385
|
+
|
|
386
|
+
// Secure Config — oracle-attested configuration
|
|
387
|
+
this.secureConfig = null;
|
|
388
|
+
|
|
202
389
|
// Codebase lock status
|
|
203
390
|
this.codebaseLocked = false;
|
|
204
391
|
}
|
|
205
392
|
|
|
206
393
|
async start() {
|
|
207
394
|
log.info('\n🦬 Starting Yakmesh Node...\n');
|
|
208
|
-
|
|
395
|
+
|
|
209
396
|
// Record start time for uptime tracking
|
|
210
397
|
this._startTime = Date.now();
|
|
211
398
|
|
|
@@ -217,24 +404,95 @@ export class YakmeshNode {
|
|
|
217
404
|
this.codebaseLocked = true;
|
|
218
405
|
setupUnlockOnExit(); // Ensure cleanup on process exit
|
|
219
406
|
log.info(`✓ Codebase locked: ${lockResult.fileCount} source files protected`);
|
|
407
|
+
|
|
408
|
+
// Subscribe to tampering events
|
|
409
|
+
onTamper((event) => {
|
|
410
|
+
log.error('🚨 SECURITY ALERT: Tampering detected!', {
|
|
411
|
+
type: event.type,
|
|
412
|
+
path: event.path,
|
|
413
|
+
time: event.isoTime,
|
|
414
|
+
});
|
|
415
|
+
// Could broadcast to mesh here for visibility
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (lockResult.watchdogActive) {
|
|
419
|
+
log.info('✓ Watchdog active: monitoring for tampering attempts');
|
|
420
|
+
}
|
|
220
421
|
} else {
|
|
221
422
|
log.warn(`⚠️ Codebase lock failed: ${lockResult.error}`);
|
|
222
423
|
log.warn(' Node will continue but source files are not protected');
|
|
223
424
|
}
|
|
224
425
|
|
|
426
|
+
// 0b. Initialize ACCEL — hardware-accelerated crypto & inference
|
|
427
|
+
// Probes CPU SIMD (AVX-512/VAES/SHA-NI), NVIDIA GPU (CUDA), AMD NPU (XDNA/DirectML)
|
|
428
|
+
// Must happen before any crypto operations so native paths are available
|
|
429
|
+
log.info('⚡ Initializing ACCEL (hardware acceleration)...');
|
|
430
|
+
const accelResult = await accel.initialize();
|
|
431
|
+
this._accel = accelResult;
|
|
432
|
+
const caps = [];
|
|
433
|
+
if (accel.HW.nativeSha3) caps.push('SHA3-native');
|
|
434
|
+
if (accel.HW.avx512) caps.push('AVX-512');
|
|
435
|
+
if (accel.HW.vaes) caps.push('VAES');
|
|
436
|
+
if (accel.HW.shaNI) caps.push('SHA-NI');
|
|
437
|
+
if (accel.HW.nvGpu) caps.push(`GPU:${accel.HW.nvGpuName}`);
|
|
438
|
+
if (accel.HW.amdNpu) caps.push(`NPU:${accel.HW.amdNpuTops}TOPS`);
|
|
439
|
+
if (accel.HW.nativePQ) caps.push(`PQ:${accel.HW.nativePQBackend}`);
|
|
440
|
+
log.info(`✓ ACCEL: ${caps.length > 0 ? caps.join(' | ') : 'pure-JS fallback'}`);
|
|
441
|
+
|
|
442
|
+
// 0b½. Load ONNX models — NPU/GPU-accelerated security inference
|
|
443
|
+
// These models are used by EntropySentinel, SAKSHI, and KARMA subsystems.
|
|
444
|
+
// If onnxruntime-node is not installed, loadModel silently returns false
|
|
445
|
+
// and all subsystems fall back to CPU-only heuristic paths.
|
|
446
|
+
const modelsDir = join(import.meta.dirname, '..', 'models');
|
|
447
|
+
const ONNX_MODELS = [
|
|
448
|
+
{ name: 'entropy-sentinel', file: 'entropy-sentinel.onnx' },
|
|
449
|
+
{ name: 'sakshi-anomaly', file: 'sakshi-anomaly.onnx' },
|
|
450
|
+
{ name: 'karma-trust', file: 'karma-trust.onnx' },
|
|
451
|
+
];
|
|
452
|
+
let modelsLoaded = 0;
|
|
453
|
+
for (const { name, file } of ONNX_MODELS) {
|
|
454
|
+
const modelPath = join(modelsDir, file);
|
|
455
|
+
if (existsSync(modelPath)) {
|
|
456
|
+
const ok = await accel.inference.loadModel(name, modelPath);
|
|
457
|
+
if (ok) modelsLoaded++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (modelsLoaded > 0) {
|
|
461
|
+
log.info(`✓ ONNX models: ${modelsLoaded}/${ONNX_MODELS.length} loaded (${accel.inference._preferredProvider || 'CPU'})`);
|
|
462
|
+
} else {
|
|
463
|
+
log.info('○ ONNX models: none loaded (CPU heuristic fallback active)');
|
|
464
|
+
}
|
|
465
|
+
// 0c. Initialize STEADYWATCH — quantum-hardware-validated entropy seeds
|
|
466
|
+
// Hurwitz quaternion seeds (IBM ibm_marrakesh) for ANNEX ML-KEM-768 keygen.
|
|
467
|
+
// Two-source extractor: STEADYWATCH seed ⊕ CSPRNG → hybrid entropy.
|
|
468
|
+
// Uses ACCEL SHA3-native for seed fingerprinting, InferenceEngine for Entropy Sentinel.
|
|
469
|
+
log.info('🛰️ Initializing STEADYWATCH (quantum entropy)...');
|
|
470
|
+
const steadywatchResult = await steadywatch.initialize({
|
|
471
|
+
seedFile: this.config.steadywatch?.seedFile,
|
|
472
|
+
nodeIndex: this.config.steadywatch?.nodeIndex,
|
|
473
|
+
prime: this.config.steadywatch?.prime || 5,
|
|
474
|
+
generateTest: this.config.steadywatch?.generateTest ?? true,
|
|
475
|
+
inferenceEngine: accel.inference,
|
|
476
|
+
});
|
|
477
|
+
if (steadywatchResult.initialized) {
|
|
478
|
+
log.info(`✓ STEADYWATCH: ${steadywatchResult.seedCount} satellite seeds loaded (Sentinel: ${steadywatchResult.sentinel ? 'NPU' : 'CPU'})`);
|
|
479
|
+
} else {
|
|
480
|
+
log.warn('⚠️ STEADYWATCH: no seeds loaded, ANNEX will use pure CSPRNG');
|
|
481
|
+
}
|
|
482
|
+
|
|
225
483
|
// 1. Initialize the Oracle system FIRST (provides codebase hash for identity)
|
|
226
484
|
// This MUST happen before identity initialization
|
|
227
485
|
this._initOracle();
|
|
228
|
-
|
|
229
|
-
// 1b. Initialize time source detection
|
|
230
|
-
this._initTimeSource();
|
|
486
|
+
|
|
487
|
+
// 1b. Initialize time source detection (async — MA-902 SNMP init)
|
|
488
|
+
await this._initTimeSource();
|
|
231
489
|
|
|
232
490
|
// 2. Initialize identity - extract directory from database path
|
|
233
491
|
// Pass the oracle so it can derive network name from codebase hash
|
|
234
492
|
const dbDir = this.config.database.path.replace(/[/\\\\][^/\\\\]+\.db$/, '');
|
|
235
493
|
this.identity = new NodeIdentity(dbDir);
|
|
236
494
|
await this.identity.init(this.config.node.name, this.config.node.region, this.oracle);
|
|
237
|
-
|
|
495
|
+
|
|
238
496
|
// 2b. Update codeProof and consensus with the initialized identity
|
|
239
497
|
if (this.codeProof) {
|
|
240
498
|
this.codeProof.nodeId = this.identity.identity?.nodeId;
|
|
@@ -246,6 +504,8 @@ export class YakmeshNode {
|
|
|
246
504
|
// Pass network identity for peer verification
|
|
247
505
|
networkId: this.genesisNetwork?.networkId,
|
|
248
506
|
networkFingerprint: this.genesisNetwork?.fingerprint,
|
|
507
|
+
// JHILKE: Pass oracle code hash for deterministic bootstrap key derivation
|
|
508
|
+
codeHash: this.oracle?.selfHash,
|
|
249
509
|
});
|
|
250
510
|
await this.mesh.start();
|
|
251
511
|
|
|
@@ -261,69 +521,89 @@ export class YakmeshNode {
|
|
|
261
521
|
this.gossip = new GossipProtocol(this.mesh, this.identity, {
|
|
262
522
|
fanout: 3,
|
|
263
523
|
helloInterval: 30000,
|
|
524
|
+
// Relay info callback — gossip includes our relay endpoints in HELLO broadcasts
|
|
525
|
+
getRelayInfo: () => this._getActiveRelayInfo(),
|
|
526
|
+
// Relay connect callback — gossip tells us to register with a discovered relay
|
|
527
|
+
connectRelay: (endpoint, nodeId) => this._registerWithRelay({ relayEndpoint: endpoint, nodeId: nodeId || `relay-${Date.now()}` }),
|
|
264
528
|
});
|
|
265
529
|
this.gossip.start();
|
|
266
530
|
|
|
267
531
|
// Handle incoming rumors (data from other nodes)
|
|
268
532
|
this.mesh.on('rumor', (topic, data, origin) => {
|
|
269
|
-
log.debug(`📨 Rumor [${topic}] from ${origin
|
|
270
|
-
|
|
533
|
+
log.debug(`📨 Rumor [${topic}] from ${peerTag(origin)}`);
|
|
534
|
+
|
|
271
535
|
// Handle different rumor topics
|
|
272
536
|
if (topic === 'data_update') {
|
|
273
537
|
this.replication.recordChange(data.table, data.rowId, data.operation, data.data);
|
|
274
538
|
}
|
|
275
|
-
|
|
539
|
+
|
|
276
540
|
// Handle code proof challenges
|
|
277
541
|
if (topic === 'code_proof_challenge') {
|
|
278
542
|
const response = this.codeProof.respondToChallenge(data);
|
|
279
543
|
this.gossip.spreadRumor('code_proof_response', response);
|
|
280
544
|
}
|
|
281
|
-
|
|
545
|
+
|
|
282
546
|
// Handle code proof responses
|
|
283
547
|
if (topic === 'code_proof_response') {
|
|
284
548
|
this.codeProof.verifyResponse(data);
|
|
285
549
|
}
|
|
286
|
-
|
|
550
|
+
|
|
287
551
|
// Handle oracle-validated content
|
|
288
552
|
if (topic === 'oracle_content') {
|
|
289
553
|
this._handleOracleContent(data, origin);
|
|
290
554
|
}
|
|
291
|
-
|
|
555
|
+
|
|
292
556
|
// Handle iO network handshakes
|
|
293
557
|
if (topic === 'network_handshake') {
|
|
294
558
|
this._handleNetworkHandshake(data, origin);
|
|
295
559
|
}
|
|
296
|
-
|
|
560
|
+
|
|
297
561
|
// Handle content gossip (for public content delivery)
|
|
298
562
|
if (topic === 'content') {
|
|
299
563
|
if (this.contentStore) {
|
|
300
564
|
this.contentStore._handleContentGossip(data, origin);
|
|
301
565
|
}
|
|
302
566
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (this.annex) {
|
|
308
|
-
this.annex._handleAnnexMessage(data.annex || data, origin);
|
|
567
|
+
|
|
568
|
+
// Handle time heartbeat gossip (MANI grandmaster time propagation)
|
|
569
|
+
if (topic === 'time:heartbeat') {
|
|
570
|
+
this._handleTimeHeartbeat(data, origin);
|
|
309
571
|
}
|
|
310
572
|
});
|
|
311
573
|
|
|
574
|
+
// 4b. Start periodic time heartbeat gossip broadcast
|
|
575
|
+
this._startTimeHeartbeat();
|
|
576
|
+
|
|
577
|
+
// Annex messages handled directly in mesh._handleMessage() — no separate routing needed
|
|
578
|
+
|
|
312
579
|
// 5. Initialize content store for public delivery
|
|
313
580
|
this.contentStore = new ContentStore({
|
|
314
581
|
dataDir: this.config.database?.contentPath || './data/content',
|
|
315
|
-
quorumSize: 2,
|
|
316
582
|
});
|
|
317
583
|
await this.contentStore.init(this);
|
|
318
|
-
|
|
319
|
-
// 5b.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
584
|
+
|
|
585
|
+
// 5b. Annex — initialized inside mesh.start(), no duplicate instance needed
|
|
586
|
+
log.info('✓ Annex channel ready (single instance in mesh layer)');
|
|
587
|
+
|
|
588
|
+
// 5c. Initialize KOMM stack (KATHA + VANI + YURT + GUMBA)
|
|
589
|
+
this._initKommStack();
|
|
590
|
+
|
|
591
|
+
// 5d. Initialize DARSHAN content streaming gateway
|
|
592
|
+
this._initDarshan();
|
|
593
|
+
|
|
594
|
+
// 5e. Initialize NAKPAK onion routing
|
|
595
|
+
this._initNakpak();
|
|
596
|
+
|
|
597
|
+
// 5f. Initialize SAKSHI witness consensus
|
|
598
|
+
this._initSakshi();
|
|
599
|
+
|
|
600
|
+
// 5g. Initialize KARMA trust model (fed by SAKSHI)
|
|
601
|
+
this._initKarma();
|
|
602
|
+
|
|
603
|
+
// 5h. Initialize ternary harmonization stack (SST × YPC-27 × 144T × ML)
|
|
604
|
+
await this._initTernaryHarmonization();
|
|
605
|
+
|
|
606
|
+
// 5i. Initialize SHERPA for decentralized peer discovery
|
|
327
607
|
this.sherpa = new SherpaDiscovery({
|
|
328
608
|
nodeId: this.identity.identity.nodeId,
|
|
329
609
|
networkName: this.genesisNetwork?.networkName,
|
|
@@ -332,27 +612,101 @@ export class YakmeshNode {
|
|
|
332
612
|
verifyFn: (data, sig, pubKey) => this.identity.verify(data, sig, pubKey),
|
|
333
613
|
selfEndpoint: this.config.sherpa?.selfEndpoint || null,
|
|
334
614
|
wsEndpoint: this.config.sherpa?.wsEndpoint || null,
|
|
615
|
+
relayEndpoint: this.config.sherpa?.relayEndpoint || null,
|
|
335
616
|
capabilities: {
|
|
336
617
|
wsPort: this.config.network.wsPort,
|
|
337
618
|
httpPort: this.config.network.httpPort,
|
|
338
619
|
supportsAnnex: true,
|
|
339
|
-
supportsNakpak:
|
|
620
|
+
supportsNakpak: !!this.nakpakRouter,
|
|
621
|
+
supportsKomm: !!(this.kathaHub && this.gumbaHub),
|
|
622
|
+
supportsDarshan: !!this.darshanGateway,
|
|
340
623
|
supportsGossip: true,
|
|
341
624
|
},
|
|
342
625
|
seedEndpoints: this.config.sherpa?.seeds || [],
|
|
343
626
|
});
|
|
344
|
-
|
|
627
|
+
|
|
628
|
+
// Expose SHERPA registry on mesh so ANNEX can look up relay peer public keys
|
|
629
|
+
this.mesh.sherpa = this.sherpa;
|
|
630
|
+
|
|
345
631
|
// Start SHERPA if seeds are configured or selfEndpoint is set
|
|
346
632
|
if (this.config.sherpa?.enabled !== false) {
|
|
633
|
+
// Wire SHERPA auto-connect: when crawl discovers peers, connect outbound
|
|
634
|
+
this.sherpa.on('crawl-complete', ({ peersFound }) => {
|
|
635
|
+
if (peersFound > 0) {
|
|
636
|
+
this._sherpaAutoConnect();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
347
639
|
this.sherpa.start();
|
|
348
640
|
log.info('✓ SHERPA discovery initialized (decentralized peer discovery)');
|
|
349
641
|
}
|
|
350
|
-
|
|
642
|
+
|
|
643
|
+
// 5i. Wire mesh → HTTP relay bridge
|
|
644
|
+
// Route direct messages (sendTo) via relay when no WS connection
|
|
645
|
+
this.mesh.on('outbound-relay', (targetNodeId, msg) => {
|
|
646
|
+
if ((this._relayPollers && this._relayPollers.has(targetNodeId)) ||
|
|
647
|
+
(this._relayClients && this._relayClients.has(targetNodeId))) {
|
|
648
|
+
this._queueRelayMessage(targetNodeId, msg);
|
|
649
|
+
} else {
|
|
650
|
+
log.debug(`No relay path to ${peerTag(targetNodeId)}`);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Wire gossip broadcasts → relay bridge
|
|
655
|
+
// This covers both directions:
|
|
656
|
+
// - _relayPollers: nodes WE poll (we initiated relay connection)
|
|
657
|
+
// - _relayClients: nodes that poll US (they registered with our relay)
|
|
658
|
+
this.mesh.on('outbound-gossip', (msg, excludeNodeIds = []) => {
|
|
659
|
+
const excludeSet = new Set(excludeNodeIds);
|
|
660
|
+
|
|
661
|
+
// Queue for nodes we actively poll (outbound relay connections)
|
|
662
|
+
if (this._relayPollers && this._relayPollers.size > 0) {
|
|
663
|
+
for (const [relayNodeId] of this._relayPollers) {
|
|
664
|
+
if (!excludeSet.has(relayNodeId) && relayNodeId !== msg.origin) {
|
|
665
|
+
this._queueRelayMessage(relayNodeId, msg);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Queue for nodes that registered to poll us (inbound relay clients)
|
|
671
|
+
if (this._relayClients && this._relayClients.size > 0) {
|
|
672
|
+
for (const [clientNodeId] of this._relayClients) {
|
|
673
|
+
if (!excludeSet.has(clientNodeId) && clientNodeId !== msg.origin) {
|
|
674
|
+
this._queueRelayMessage(clientNodeId, msg);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// 5j. Expire stale relay clients (no poll for 5 minutes)
|
|
681
|
+
setInterval(() => {
|
|
682
|
+
if (!this._relayClients || this._relayClients.size === 0) return;
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
const RELAY_CLIENT_TTL = 5 * 60 * 1000; // 5 minutes
|
|
685
|
+
for (const [clientNodeId, lastSeen] of this._relayClients) {
|
|
686
|
+
if (now - lastSeen > RELAY_CLIENT_TTL) {
|
|
687
|
+
this._relayClients.delete(clientNodeId);
|
|
688
|
+
// Also clear any queued messages and cached keys for expired client
|
|
689
|
+
if (this._relayOutbox) this._relayOutbox.delete(clientNodeId);
|
|
690
|
+
if (this.mesh?._relayPeerKeys) this.mesh._relayPeerKeys.delete(clientNodeId);
|
|
691
|
+
log.debug(`Relay client expired: ${peerTag(clientNodeId)}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}, 60000); // Check every minute
|
|
695
|
+
|
|
696
|
+
// 5k. Start scheduled ML workloads through ComputeScheduler
|
|
697
|
+
this._startScheduledWorkloads();
|
|
698
|
+
|
|
351
699
|
// 6. Start HTTP server
|
|
352
700
|
await this._startHttpServer();
|
|
353
701
|
|
|
354
|
-
//
|
|
355
|
-
|
|
702
|
+
// 6b. Attach KOMM WebSocket upgrade paths to HTTP server
|
|
703
|
+
this._initKommWebSocket();
|
|
704
|
+
|
|
705
|
+
// 7. Connect to bootstrap nodes (non-blocking — runs in background)
|
|
706
|
+
this._connectToBootstrap();
|
|
707
|
+
|
|
708
|
+
// 7b. Auto-register with relay peers from YAKMESH_RELAY_PEERS env var
|
|
709
|
+
this._connectToRelayPeers();
|
|
356
710
|
|
|
357
711
|
// 7. Initialize PeerQuanta integration (if enabled)
|
|
358
712
|
if (this.config.peerquanta?.enabled) {
|
|
@@ -386,12 +740,53 @@ export class YakmeshNode {
|
|
|
386
740
|
const stats = this.contentStore.getStats();
|
|
387
741
|
log.info(` Content: ${stats.totalObjects} objects (${stats.verified} verified)`);
|
|
388
742
|
}
|
|
389
|
-
if (this.annex) {
|
|
743
|
+
if (this.mesh?.annex) {
|
|
390
744
|
log.info(` Annex: ✓ Encrypted P2P ready`);
|
|
391
745
|
}
|
|
746
|
+
if (this.kathaHub) {
|
|
747
|
+
log.info(` KOMM: ✓ KATHA + VANI + YURT + GUMBA at /komm/`);
|
|
748
|
+
}
|
|
749
|
+
if (this.darshanGateway) {
|
|
750
|
+
log.info(` DARSHAN: ✓ Content streaming at /darshan/`);
|
|
751
|
+
}
|
|
752
|
+
if (this.nakpakRouter) {
|
|
753
|
+
log.info(` NAKPAK: ✓ Onion routing active (${this.nakpakRouter.knownNodes.size} known nodes)`);
|
|
754
|
+
}
|
|
755
|
+
if (this.sakshiWitness) {
|
|
756
|
+
log.info(` SAKSHI: ✓ Witness consensus active`);
|
|
757
|
+
}
|
|
758
|
+
if (this.karmaModel) {
|
|
759
|
+
log.info(` KARMA: ✓ Trust model active (SAKSHI → trust pipeline)`);
|
|
760
|
+
}
|
|
761
|
+
if (this.kommWss) {
|
|
762
|
+
log.info(` KOMM WS: ✓ Real-time at ws://localhost:${this.boundHttpPort || this.config.network.httpPort}/komm/ws`);
|
|
763
|
+
}
|
|
392
764
|
if (this.sherpa) {
|
|
393
765
|
log.info(` SHERPA: ✓ Beacon at /.well-known/yakmesh/beacon`);
|
|
394
766
|
}
|
|
767
|
+
// ACCEL status line
|
|
768
|
+
{
|
|
769
|
+
const a = accel.HW;
|
|
770
|
+
const accelParts = [];
|
|
771
|
+
if (a.nativeSha3) accelParts.push('SHA3');
|
|
772
|
+
if (a.avx512) accelParts.push('AVX-512');
|
|
773
|
+
if (a.nvGpu) accelParts.push(`GPU(${a.nvGpuName}, ${a.nvGpuTops}T)`);
|
|
774
|
+
if (a.amdNpu) accelParts.push(`NPU(${a.amdNpuTops}T)`);
|
|
775
|
+
if (a.totalTops > 0) accelParts.push(`∑${a.totalTops}TOPS`);
|
|
776
|
+
if (a.nativePQ) accelParts.push(`PQ(${a.nativePQBackend})`);
|
|
777
|
+
if (accelParts.length > 0) {
|
|
778
|
+
log.info(` ACCEL: ⚡ ${accelParts.join(' + ')}`);
|
|
779
|
+
} else {
|
|
780
|
+
log.info(` ACCEL: ○ pure-JS (install liboqs-node / onnxruntime-node for acceleration)`);
|
|
781
|
+
}
|
|
782
|
+
// Scheduler status
|
|
783
|
+
const sched = accel.scheduler.getStatus();
|
|
784
|
+
if (sched.initialized) {
|
|
785
|
+
const devNames = Object.keys(sched.devices).map(d => d.toUpperCase()).join('+');
|
|
786
|
+
const totalSlots = Object.values(sched.devices).reduce((s, d) => s + d.queue.capacity, 0);
|
|
787
|
+
log.info(` SCHEDULER: ✓ ${devNames} heterogeneous (${totalSlots} queue slots, ${sched.routing.mode} routing)`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
395
790
|
if (this.adapter) {
|
|
396
791
|
log.info(` Adapter: ✓ Enabled`);
|
|
397
792
|
}
|
|
@@ -400,30 +795,75 @@ export class YakmeshNode {
|
|
|
400
795
|
}
|
|
401
796
|
log.info('');
|
|
402
797
|
|
|
798
|
+
// 10. Start Time API (HTTP bridge to MA-902 GPS time server)
|
|
799
|
+
try {
|
|
800
|
+
await startTimeApi();
|
|
801
|
+
log.info(' TIME API: ✓ GPS telemetry at http://localhost:3099/api/time');
|
|
802
|
+
} catch (err) {
|
|
803
|
+
log.warn(` TIME API: ⚠️ Failed to start: ${err.message}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// 11. Start Caddy reverse proxy (HTTPS/443 with auto Let's Encrypt)
|
|
807
|
+
// Only starts if YAKMESH_DOMAIN env var is set
|
|
808
|
+
if (this.config.caddy?.enabled && this.config.caddy?.domain) {
|
|
809
|
+
try {
|
|
810
|
+
this.webServer = new YakmeshWebServer({
|
|
811
|
+
domain: this.config.caddy.domain,
|
|
812
|
+
autoHttps: this.config.caddy.autoHttps !== false,
|
|
813
|
+
acmeEmail: this.config.caddy.acmeEmail,
|
|
814
|
+
nodeProxy: true,
|
|
815
|
+
nodeHttpPort: this.boundHttpPort || this.config.network.httpPort,
|
|
816
|
+
nodeWsPort: this.mesh.boundPort || this.config.network.wsPort,
|
|
817
|
+
root: './htdocs',
|
|
818
|
+
logPath: './logs',
|
|
819
|
+
});
|
|
820
|
+
await this.webServer.start();
|
|
821
|
+
log.info(` CADDY: ✓ HTTPS reverse proxy at https://${this.config.caddy.domain}`);
|
|
822
|
+
log.info(` → HTTP:${this.boundHttpPort || this.config.network.httpPort} WS:${this.mesh.boundPort || this.config.network.wsPort}`);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
log.warn(` CADDY: ⚠️ Failed to start: ${err.message}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
403
828
|
return this;
|
|
404
829
|
}
|
|
405
830
|
|
|
406
831
|
async stop() {
|
|
407
832
|
log.info('\n🛑 Stopping Yakmesh Node...');
|
|
408
|
-
|
|
833
|
+
|
|
409
834
|
this.adapter?.stopSync();
|
|
410
835
|
this.timeSource?.stop(); // Stop time source monitoring
|
|
836
|
+
await stopTimeApi().catch(() => { }); // Stop time API server
|
|
411
837
|
this.consensus?.stop(); // Stop consensus engine
|
|
412
|
-
this.
|
|
838
|
+
this.yurtHub?.stop(); // Stop YURT room gossip
|
|
839
|
+
this.velocityMonitor?.stop?.(); // Stop velocity monitoring
|
|
840
|
+
this.karmaModel?.stopPromotionChecks?.(); // Stop KARMA auto-promotion
|
|
841
|
+
this.nakpakRouter?.cleanupCircuits?.(); // Cleanup NAKPAK circuits
|
|
842
|
+
this.kommWss?.close(); // Close KOMM WebSocket server
|
|
843
|
+
// Stop Caddy web server
|
|
844
|
+
if (this.webServer) {
|
|
845
|
+
await this.webServer.stop().catch(() => { });
|
|
846
|
+
}
|
|
847
|
+
// Stop scheduled workloads
|
|
848
|
+
if (this._entropyCheckTimer) clearInterval(this._entropyCheckTimer);
|
|
849
|
+
if (this._peerAssessTimer) clearInterval(this._peerAssessTimer);
|
|
850
|
+
if (this._timeHeartbeatInterval) clearInterval(this._timeHeartbeatInterval);
|
|
851
|
+
await accel.scheduler.shutdown(); // Drain compute scheduler queues
|
|
852
|
+
// Annex channels cleaned up by mesh.stop()
|
|
413
853
|
this.gossip?.stop();
|
|
414
854
|
this.replication?.stopSync();
|
|
415
855
|
await this.mesh?.stop();
|
|
416
|
-
|
|
856
|
+
|
|
417
857
|
if (this.http) {
|
|
418
858
|
this.http.close();
|
|
419
859
|
}
|
|
420
|
-
|
|
860
|
+
|
|
421
861
|
// Unlock codebase - allow modifications again
|
|
422
862
|
if (this.codebaseLocked) {
|
|
423
863
|
unlockCodebase();
|
|
424
864
|
this.codebaseLocked = false;
|
|
425
865
|
}
|
|
426
|
-
|
|
866
|
+
|
|
427
867
|
log.info('✓ Yakmesh Node stopped\n');
|
|
428
868
|
}
|
|
429
869
|
|
|
@@ -434,34 +874,34 @@ export class YakmeshNode {
|
|
|
434
874
|
*/
|
|
435
875
|
_initOracle() {
|
|
436
876
|
log.info('🔮 Initializing Oracle System...');
|
|
437
|
-
|
|
877
|
+
|
|
438
878
|
// Get the singleton oracle instance (computes codebase hash)
|
|
439
879
|
this.oracle = getOracle();
|
|
440
|
-
|
|
880
|
+
|
|
441
881
|
// Initialize code proof protocol (identity will be set later)
|
|
442
882
|
this.codeProof = new CodeProofProtocol({ identity: null });
|
|
443
|
-
|
|
883
|
+
|
|
444
884
|
// Initialize consensus engine (identity will be set later)
|
|
445
885
|
this.consensus = new ConsensusEngine({ identity: null }, {
|
|
446
886
|
minAttestations: this.config.oracle?.minAttestations || 1,
|
|
447
887
|
});
|
|
448
|
-
|
|
888
|
+
|
|
449
889
|
// Listen for consensus events
|
|
450
890
|
this.consensus.on('consensus', (event) => {
|
|
451
891
|
log.info(`✓ Consensus reached for ${event.contentType}: ${event.contentHash.slice(0, 16)}...`);
|
|
452
892
|
});
|
|
453
|
-
|
|
893
|
+
|
|
454
894
|
this.consensus.on('conflict-resolved', (event) => {
|
|
455
895
|
log.info(`⚖️ Conflict resolved: ${event.winnerHash.slice(0, 16)}... won`);
|
|
456
896
|
});
|
|
457
|
-
|
|
897
|
+
|
|
458
898
|
// Note: Raw oracle hash now hidden - use network identity instead
|
|
459
899
|
log.info(`✓ Oracle initialized`);
|
|
460
|
-
|
|
900
|
+
|
|
461
901
|
// Initialize iO-inspired network identity (hash obfuscation)
|
|
462
902
|
this._initGenesisNetwork();
|
|
463
903
|
}
|
|
464
|
-
|
|
904
|
+
|
|
465
905
|
/**
|
|
466
906
|
* Initialize the iO-inspired Genesis Network Identity
|
|
467
907
|
* This derives a human-readable network name from the oracle hash
|
|
@@ -469,52 +909,57 @@ export class YakmeshNode {
|
|
|
469
909
|
*/
|
|
470
910
|
_initGenesisNetwork() {
|
|
471
911
|
log.info('🌐 Initializing iO Network Identity...');
|
|
472
|
-
|
|
912
|
+
|
|
473
913
|
// Create GenesisNetworkV2 from the oracle
|
|
474
914
|
this.genesisNetwork = createGenesisNetworkV2(this.oracle);
|
|
475
|
-
|
|
915
|
+
|
|
476
916
|
// Update consensus engine with network fingerprint for security
|
|
477
917
|
if (this.consensus) {
|
|
478
918
|
this.consensus.networkFingerprint = this.genesisNetwork.fingerprint;
|
|
479
919
|
}
|
|
480
|
-
|
|
920
|
+
|
|
481
921
|
// Update code proof protocol with fingerprint
|
|
482
922
|
if (this.codeProof) {
|
|
483
923
|
this.codeProof.networkFingerprint = this.genesisNetwork.fingerprint;
|
|
484
924
|
}
|
|
485
|
-
|
|
925
|
+
|
|
486
926
|
log.debug(` Network Name: ${this.genesisNetwork.networkName}`);
|
|
487
927
|
log.debug(` Network ID: ${this.genesisNetwork.networkId}`);
|
|
488
928
|
log.debug(` Verify: "${this.genesisNetwork.verificationPhrase}"`);
|
|
489
929
|
log.info('✓ Genesis Network initialized (iO hash obfuscation active)');
|
|
490
930
|
}
|
|
491
|
-
|
|
931
|
+
|
|
492
932
|
/**
|
|
493
933
|
* Initialize time source detection
|
|
494
934
|
* Detects precision time sources and configures phase epochs accordingly
|
|
495
935
|
*/
|
|
496
|
-
_initTimeSource() {
|
|
936
|
+
async _initTimeSource() {
|
|
497
937
|
log.info('⏰ Initializing Time Source Detection...');
|
|
498
|
-
|
|
938
|
+
|
|
499
939
|
// Get or create global time source detector
|
|
940
|
+
// MA-902/S-C1 GPS Time Server on LAN — provides satellite telemetry via SNMP
|
|
500
941
|
this.timeSource = getTimeSourceDetector({
|
|
501
942
|
detectHardware: true,
|
|
502
943
|
checkNtp: true,
|
|
503
944
|
refreshInterval: 60000, // Re-check every minute
|
|
504
945
|
verbose: false,
|
|
946
|
+
ma902: {
|
|
947
|
+
host: '192.168.1.30', // MA-902/S-C1 Gigabit PTP Time Server
|
|
948
|
+
pollInterval: 10000, // Poll SNMP telemetry every 10s
|
|
949
|
+
},
|
|
505
950
|
});
|
|
506
|
-
|
|
951
|
+
|
|
507
952
|
// Perform initial detection
|
|
508
953
|
const results = this.timeSource.detect();
|
|
509
|
-
|
|
954
|
+
|
|
510
955
|
// Configure phase epochs based on detected time source
|
|
511
956
|
if (results.trustLevel) {
|
|
512
957
|
setTimeSourceConfig(results.trustLevel);
|
|
513
958
|
}
|
|
514
|
-
|
|
515
|
-
// Start continuous monitoring
|
|
516
|
-
this.timeSource.start();
|
|
517
|
-
|
|
959
|
+
|
|
960
|
+
// Start continuous monitoring (async — initialises MA-902 SNMP session)
|
|
961
|
+
await this.timeSource.start();
|
|
962
|
+
|
|
518
963
|
// Log initial detection
|
|
519
964
|
const trustIcons = {
|
|
520
965
|
atomic: '🔬',
|
|
@@ -523,132 +968,1455 @@ export class YakmeshNode {
|
|
|
523
968
|
ntp: '🌐',
|
|
524
969
|
unsync: '⚠️',
|
|
525
970
|
};
|
|
526
|
-
|
|
971
|
+
|
|
527
972
|
log.debug(` Trust Level: ${trustIcons[results.trustLevel] || '?'} ${results.trustLevel.toUpperCase()}`);
|
|
528
973
|
log.debug(` Tolerance: ±${results.phaseTolerance}ms`);
|
|
529
974
|
log.debug(` Primary: ${results.primarySource || 'none'}`);
|
|
530
|
-
|
|
975
|
+
|
|
976
|
+
// Track last known trust level to only log on actual changes
|
|
977
|
+
let lastKnownTrustLevel = results.trustLevel;
|
|
978
|
+
|
|
531
979
|
// Listen for trust level changes
|
|
532
980
|
this.timeSource.on('detected', (newResults) => {
|
|
533
|
-
if (newResults.trustLevel !==
|
|
534
|
-
log.info(`⏰ Time source changed: ${newResults.trustLevel.toUpperCase()}`);
|
|
981
|
+
if (newResults.trustLevel !== lastKnownTrustLevel) {
|
|
982
|
+
log.info(`⏰ Time source changed: ${lastKnownTrustLevel.toUpperCase()} → ${newResults.trustLevel.toUpperCase()}`);
|
|
983
|
+
lastKnownTrustLevel = newResults.trustLevel;
|
|
535
984
|
setTimeSourceConfig(newResults.trustLevel);
|
|
536
985
|
}
|
|
537
986
|
});
|
|
538
|
-
|
|
987
|
+
|
|
539
988
|
log.info('✓ Time Source initialized');
|
|
540
989
|
}
|
|
541
|
-
|
|
990
|
+
|
|
991
|
+
// =========================================
|
|
992
|
+
// MANI Time Heartbeat Gossip
|
|
993
|
+
// =========================================
|
|
994
|
+
// Broadcasts local time source status via MANTRA gossip so that every
|
|
995
|
+
// mesh peer receives grandmaster-quality timing data even if it only
|
|
996
|
+
// has system-clock NTP. On the LAN node this carries MA-902 GPS
|
|
997
|
+
// satellite telemetry; on Hostinger (or any peer) the incoming
|
|
998
|
+
// heartbeats populate `this.meshTimeReference` — the best-known
|
|
999
|
+
// atomic/GPS time from the mesh.
|
|
1000
|
+
//
|
|
1001
|
+
// Public NTP: time.yakmesh.dev (UDP 123 → MA-902 GPS grandmaster)
|
|
1002
|
+
// =========================================
|
|
1003
|
+
|
|
542
1004
|
/**
|
|
543
|
-
*
|
|
1005
|
+
* Start periodic time heartbeat gossip.
|
|
1006
|
+
* Called once after gossip + timeSource are both initialized.
|
|
544
1007
|
*/
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1008
|
+
_startTimeHeartbeat() {
|
|
1009
|
+
// Mesh time reference — best peer time we've received via gossip
|
|
1010
|
+
this.meshTimeReference = null;
|
|
1011
|
+
|
|
1012
|
+
const HEARTBEAT_INTERVAL = 30_000; // 30 s (matches relay poll cadence)
|
|
1013
|
+
|
|
1014
|
+
const broadcast = () => {
|
|
1015
|
+
if (!this.gossip || !this.timeSource) return;
|
|
1016
|
+
|
|
1017
|
+
const status = this.timeSource.getStatus();
|
|
1018
|
+
const sats = status.ma902?.satellites || status.satellites || {};
|
|
1019
|
+
const locked = status.trustLevel === 'gps' || status.trustLevel === 'atomic';
|
|
1020
|
+
|
|
1021
|
+
const heartbeat = {
|
|
1022
|
+
// Node identity
|
|
1023
|
+
nodeId: this.identity.identity.nodeId,
|
|
1024
|
+
nodeName: this.identity.identity.name,
|
|
1025
|
+
// Time quality
|
|
1026
|
+
trustLevel: status.trustLevel,
|
|
1027
|
+
stratum: status.stratum ?? (locked ? 1 : 2),
|
|
1028
|
+
accuracy_ms: locked ? 1 : 50,
|
|
1029
|
+
phaseTolerance: status.phaseTolerance,
|
|
1030
|
+
primarySource: status.primarySource,
|
|
1031
|
+
// Satellite telemetry (only meaningful on GPS-backed nodes)
|
|
1032
|
+
satellites: {
|
|
1033
|
+
visible: sats.visible ?? 0,
|
|
1034
|
+
used: sats.used ?? 0,
|
|
1035
|
+
tracking: sats.tracking ?? 0,
|
|
1036
|
+
constellations: sats.constellations ?? [],
|
|
1037
|
+
},
|
|
1038
|
+
lock: locked,
|
|
1039
|
+
quality: locked ? 'excellent' : 'degraded',
|
|
1040
|
+
offset_ns: status.offset ?? 0,
|
|
1041
|
+
reference_id: locked ? 'GPS' : 'SYS',
|
|
1042
|
+
// MA-902 enrichment (when available)
|
|
1043
|
+
ma902: status.ma902 ? {
|
|
1044
|
+
host: status.ma902.host,
|
|
1045
|
+
locked: status.ma902.locked,
|
|
1046
|
+
gpsTime: status.ma902.gpsTimeISO,
|
|
1047
|
+
clockDelta: status.ma902.clockDeltaSeconds,
|
|
1048
|
+
alarm: status.ma902.alarm,
|
|
1049
|
+
quality: status.ma902.qualityIndicator,
|
|
1050
|
+
} : null,
|
|
1051
|
+
// Public NTP endpoint (resolvable from anywhere on the internet)
|
|
1052
|
+
publicNtp: locked ? 'time.yakmesh.dev' : null,
|
|
1053
|
+
// Timestamp of this heartbeat (local clock)
|
|
1054
|
+
timestamp: Date.now(),
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
this.gossip.spreadRumor('time:heartbeat', heartbeat);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// First heartbeat after a short delay (let relay connect)
|
|
1061
|
+
setTimeout(broadcast, 5_000);
|
|
1062
|
+
// Then every 30 s
|
|
1063
|
+
this._timeHeartbeatInterval = setInterval(broadcast, HEARTBEAT_INTERVAL);
|
|
1064
|
+
|
|
1065
|
+
log.info('⏰ MANI time heartbeat gossip started (every 30 s)');
|
|
573
1066
|
}
|
|
574
1067
|
|
|
575
1068
|
/**
|
|
576
|
-
* Handle
|
|
577
|
-
*
|
|
1069
|
+
* Handle an incoming time:heartbeat rumor from a peer.
|
|
1070
|
+
* Keeps track of the best (lowest stratum) grandmaster in the mesh.
|
|
578
1071
|
*/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
1072
|
+
_handleTimeHeartbeat(data, origin) {
|
|
1073
|
+
// Ignore our own heartbeats
|
|
1074
|
+
if (origin === this.identity.identity.nodeId) return;
|
|
1075
|
+
|
|
1076
|
+
const peerStratum = data.stratum ?? 16;
|
|
1077
|
+
const peerLocked = !!data.lock;
|
|
1078
|
+
const currentBest = this.meshTimeReference;
|
|
1079
|
+
|
|
1080
|
+
// Accept if: no current reference, OR this peer has a lower (better) stratum,
|
|
1081
|
+
// OR same stratum but this one is locked and current isn't
|
|
1082
|
+
const dominated =
|
|
1083
|
+
!currentBest ||
|
|
1084
|
+
peerStratum < (currentBest.stratum ?? 16) ||
|
|
1085
|
+
(peerStratum === (currentBest.stratum ?? 16) && peerLocked && !currentBest.lock);
|
|
1086
|
+
|
|
1087
|
+
if (dominated) {
|
|
1088
|
+
this.meshTimeReference = {
|
|
1089
|
+
...data,
|
|
1090
|
+
receivedAt: Date.now(),
|
|
1091
|
+
fromNodeId: origin,
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
log.info(`⏰ Mesh time reference updated — ${data.nodeName || peerTag(origin)} ` +
|
|
1095
|
+
`stratum ${peerStratum}, lock=${peerLocked}, ` +
|
|
1096
|
+
`sats=${data.satellites?.used ?? 0}/${data.satellites?.visible ?? 0}` +
|
|
1097
|
+
(data.publicNtp ? `, ntp=${data.publicNtp}` : ''));
|
|
1098
|
+
} else if (currentBest && origin === currentBest.fromNodeId) {
|
|
1099
|
+
// Same grandmaster, refresh its data
|
|
1100
|
+
this.meshTimeReference = {
|
|
1101
|
+
...data,
|
|
1102
|
+
receivedAt: Date.now(),
|
|
1103
|
+
fromNodeId: origin,
|
|
1104
|
+
};
|
|
602
1105
|
}
|
|
603
1106
|
}
|
|
604
1107
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1108
|
+
/**
|
|
1109
|
+
* Initialize the KOMM Stack (KATHA + VANI + YURT + GUMBA)
|
|
1110
|
+
* This provides the chat, voice, room, and access control backend.
|
|
1111
|
+
*/
|
|
1112
|
+
_initKommStack() {
|
|
1113
|
+
log.info('💬 Initializing KOMM Stack...');
|
|
1114
|
+
|
|
1115
|
+
// KATHA — Chat messaging hub
|
|
1116
|
+
this.kathaHub = new KathaHub();
|
|
1117
|
+
log.debug(' KATHA: Chat messaging hub ready');
|
|
1118
|
+
|
|
1119
|
+
// GUMBA — Access control (initialized before YURT, which depends on it)
|
|
1120
|
+
this.gumbaHub = new GumbaHub(this.identity, this.mesh?.annex, {});
|
|
1121
|
+
log.debug(' GUMBA: Access control hub ready');
|
|
1122
|
+
|
|
1123
|
+
// YURT — Room directory (depends on identity, gumbaHub, mesh)
|
|
1124
|
+
this.yurtHub = new YurtHub(this.identity, this.gumbaHub, this.mesh, {});
|
|
1125
|
+
this.yurtHub.start();
|
|
1126
|
+
log.debug(' YURT: Room directory + gossip started');
|
|
1127
|
+
|
|
1128
|
+
// VANI — Voice/video calling
|
|
1129
|
+
this.vaniHub = new VaniHub({
|
|
1130
|
+
localPeerId: this.identity.identity.nodeId,
|
|
1131
|
+
onSignal: (signal) => {
|
|
1132
|
+
// Forward WebRTC signals through mesh gossip
|
|
1133
|
+
this.gossip.spreadRumor('vani:signal', {
|
|
1134
|
+
signal,
|
|
1135
|
+
origin: this.identity.identity.nodeId,
|
|
1136
|
+
});
|
|
1137
|
+
},
|
|
1138
|
+
});
|
|
1139
|
+
log.debug(' VANI: Voice/video signaling hub ready');
|
|
1140
|
+
|
|
1141
|
+
// Wire KOMM gossip handlers (incoming KATHA/VANI/YURT/GUMBA rumors)
|
|
1142
|
+
wireKommGossip(this.mesh, this.kathaHub, this.vaniHub, this.yurtHub, this.gumbaHub);
|
|
1143
|
+
|
|
1144
|
+
log.info('✓ KOMM Stack initialized (KATHA + VANI + YURT + GUMBA)');
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Initialize DARSHAN content streaming gateway
|
|
1149
|
+
*/
|
|
1150
|
+
_initDarshan() {
|
|
1151
|
+
log.info('📺 Initializing DARSHAN...');
|
|
1152
|
+
|
|
1153
|
+
this.darshanGateway = new DarshanGateway(this.identity, {
|
|
1154
|
+
maxBandwidth: this.config.darshan?.maxBandwidth || Infinity,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// Wire DARSHAN gossip handlers
|
|
1158
|
+
wireDarshanGossip(this.mesh, this.darshanGateway);
|
|
1159
|
+
|
|
1160
|
+
log.info('✓ DARSHAN initialized (content streaming gateway)');
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Initialize NAKPAK onion routing
|
|
1165
|
+
* Provides post-quantum anonymous routing for sensitive messages.
|
|
1166
|
+
*/
|
|
1167
|
+
_initNakpak() {
|
|
1168
|
+
log.info('🧅 Initializing NAKPAK...');
|
|
1169
|
+
|
|
1170
|
+
this.nakpakRouter = new NakpakRouter({
|
|
1171
|
+
nodeId: this.identity.identity.nodeId,
|
|
1172
|
+
onMessageReceived: (message) => {
|
|
1173
|
+
log.debug(`📦 NAKPAK message received: ${message.id?.slice(0, 16) || 'unknown'}...`);
|
|
1174
|
+
this.mesh.emit('nakpak:message', message);
|
|
1175
|
+
},
|
|
1176
|
+
onForward: (packet) => {
|
|
1177
|
+
// Forward the packet to the next hop via mesh
|
|
1178
|
+
const nextHop = packet.nextHop;
|
|
1179
|
+
if (nextHop && this.mesh.sendTo) {
|
|
1180
|
+
this.mesh.sendTo(nextHop, {
|
|
1181
|
+
type: 'nakpak:relay',
|
|
1182
|
+
packet,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// Register known peers as NAKPAK nodes
|
|
1189
|
+
// Re-register whenever new peers connect
|
|
1190
|
+
this.mesh.on('peer:connected', (peerId, peerInfo) => {
|
|
1191
|
+
if (peerInfo?.publicKey) {
|
|
1192
|
+
this.nakpakRouter.registerNode(peerId, peerInfo.publicKey);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// Handle incoming NAKPAK relay packets
|
|
1197
|
+
this.mesh.on('rumor', (topic, data, origin) => {
|
|
1198
|
+
if (topic === 'nakpak:relay' && data.packet) {
|
|
1199
|
+
this.nakpakRouter.relay.handlePacket(data.packet);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
log.info('✓ NAKPAK initialized (post-quantum onion routing)');
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Initialize SAKSHI witness consensus
|
|
1208
|
+
* Observational capability system for node behavior monitoring.
|
|
1209
|
+
*/
|
|
1210
|
+
_initSakshi() {
|
|
1211
|
+
log.info('👁️ Initializing SAKSHI...');
|
|
1212
|
+
|
|
1213
|
+
this.sakshiWitness = new NodeWitness({
|
|
1214
|
+
nodeId: this.identity.identity.nodeId,
|
|
1215
|
+
...this.config.sakshi,
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// Behavior velocity monitor (detects rapid state changes / anomalies)
|
|
1219
|
+
this.velocityMonitor = new BehaviorVelocityMonitor({
|
|
1220
|
+
nodeId: this.identity.identity.nodeId,
|
|
1221
|
+
inferenceEngine: accel.inference,
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// Track connection churn per peer via velocity monitor
|
|
1225
|
+
this.mesh.on('peer:connected', (peerId) => {
|
|
1226
|
+
this.velocityMonitor.observe(
|
|
1227
|
+
peerId,
|
|
1228
|
+
BEHAVIOR_DIMENSION.CONNECTION_CHURN,
|
|
1229
|
+
1 // connect event = +1
|
|
1230
|
+
);
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
this.mesh.on('peer:disconnected', (peerId) => {
|
|
1234
|
+
this.velocityMonitor.observe(
|
|
1235
|
+
peerId,
|
|
1236
|
+
BEHAVIOR_DIMENSION.CONNECTION_CHURN,
|
|
1237
|
+
-1 // disconnect event = -1
|
|
1238
|
+
);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// Track gossip message rates per origin
|
|
1242
|
+
if (this.gossip && this.mesh) {
|
|
1243
|
+
let messageCountWindow = new Map(); // peerId -> count in current window
|
|
1244
|
+
|
|
1245
|
+
this.mesh.on('rumor', (topic, data, origin) => {
|
|
1246
|
+
const rumor = { origin };
|
|
1247
|
+
if (!rumor.origin) return;
|
|
1248
|
+
const count = (messageCountWindow.get(rumor.origin) || 0) + 1;
|
|
1249
|
+
messageCountWindow.set(rumor.origin, count);
|
|
1250
|
+
this.velocityMonitor.observe(
|
|
1251
|
+
rumor.origin,
|
|
1252
|
+
BEHAVIOR_DIMENSION.MESSAGE_RATE,
|
|
1253
|
+
count
|
|
1254
|
+
);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// Reset message count window every minute
|
|
1258
|
+
setInterval(() => { messageCountWindow.clear(); }, 60000);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
log.info('✓ SAKSHI initialized (witness consensus + velocity monitoring)');
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Initialize KARMA trust model
|
|
1266
|
+
* SAKSHI velocity alerts feed into KARMA trust assessments.
|
|
1267
|
+
*/
|
|
1268
|
+
_initKarma() {
|
|
1269
|
+
log.info('☯️ Initializing KARMA...');
|
|
1270
|
+
|
|
1271
|
+
this.karmaModel = new KarmaTrustModel({
|
|
1272
|
+
...this.config.karma,
|
|
1273
|
+
inferenceEngine: accel.inference,
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// Wire SAKSHI velocity alerts → KARMA trust adjustments (ternary: NEGATIVE/NEUTRAL/ignored)
|
|
1277
|
+
if (this.velocityMonitor) {
|
|
1278
|
+
this.velocityMonitor.onAlert((alert) => {
|
|
1279
|
+
const { nodeId, level, dimension, zScore } = alert;
|
|
1280
|
+
|
|
1281
|
+
// ═══ TRIBHUJ ternary mapping ═══
|
|
1282
|
+
// CRITICAL → NEGATIVE karma (record as failed verification)
|
|
1283
|
+
// WARNING → NEUTRAL observation (beacon sighting — keeps node active)
|
|
1284
|
+
// ELEVATED → ignored (normal variance — no karmic consequence)
|
|
1285
|
+
if (level === VELOCITY_ALERT.CRITICAL) {
|
|
1286
|
+
log.warn(`☯️ KARMA: Critical velocity alert for ${peerTag(nodeId)} (${dimension}, z=${zScore.toFixed(1)}) → NEGATIVE`);
|
|
1287
|
+
// Record negative evidence — failed behavioral verification
|
|
1288
|
+
this.karmaModel.recordDokoVerification(nodeId, {
|
|
1289
|
+
passed: false,
|
|
1290
|
+
reason: `Critical velocity anomaly: ${dimension} (z-score ${zScore.toFixed(1)})`,
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
// Schedule deep NPU anomaly assessment via ComputeScheduler (HIGH)
|
|
1294
|
+
const karmaEvidence = this.karmaModel.getEvidence(nodeId);
|
|
1295
|
+
this._scheduledAnomalyAssessment(nodeId, {
|
|
1296
|
+
karmaScore: karmaEvidence?.trustScore ? karmaEvidence.trustScore / 100 : 0.5,
|
|
1297
|
+
}).then(({ result }) => {
|
|
1298
|
+
if (result?.anomalyScore > 0.7) {
|
|
1299
|
+
log.warn(`👁️ SAKSHI: Deep assessment confirms anomaly for ${peerTag(nodeId)} (score=${result.anomalyScore.toFixed(3)})`);
|
|
1300
|
+
}
|
|
1301
|
+
}).catch(() => { }); // Non-fatal — scheduler may reject under load
|
|
1302
|
+
|
|
1303
|
+
} else if (level === VELOCITY_ALERT.WARNING) {
|
|
1304
|
+
log.debug(`☯️ KARMA: Warning velocity alert for ${peerTag(nodeId)} (${dimension}) → NEUTRAL`);
|
|
1305
|
+
// Record beacon sighting (neutral — keeps node active, doesn't penalize)
|
|
1306
|
+
this.karmaModel.recordBeaconSighting(nodeId);
|
|
1307
|
+
}
|
|
1308
|
+
// ELEVATED → no karmic consequence (positive path: absence of negative)
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Wire mesh peer events → KARMA beacon sightings (positive karma accumulation)
|
|
1313
|
+
this.mesh.on('peer:connected', (peerId) => {
|
|
1314
|
+
this.karmaModel.recordBeaconSighting(peerId);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
// Wire KARMA trust level changes → scheduled NPU trust prediction (second opinion)
|
|
1318
|
+
this.karmaModel.on('promoted', ({ nodeId, from, to, reason }) => {
|
|
1319
|
+
const nid = String(nodeId ?? 'unknown');
|
|
1320
|
+
log.info(`☯️ KARMA: Node ${peerTag(nid)} promoted ${from}→${to} (${reason})`);
|
|
1321
|
+
// Schedule NPU trust prediction for the promoted node
|
|
1322
|
+
const evidence = this.karmaModel.getEvidence(nid);
|
|
1323
|
+
if (evidence) {
|
|
1324
|
+
this._scheduledTrustPrediction(evidence).then(({ result }) => {
|
|
1325
|
+
if (result?.predicted) {
|
|
1326
|
+
const agrees = result.predicted === ['UNTRUSTED', 'SEEKING', 'AWAKENED', 'ENLIGHTENED'][to];
|
|
1327
|
+
log.debug(`☯️ KARMA NPU: ${result.source} predicts ${result.predicted} (${agrees ? 'agrees' : 'disagrees'} with rule-based ${to})`);
|
|
1328
|
+
}
|
|
1329
|
+
}).catch(() => { }); // Non-fatal
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
this.karmaModel.on('demoted', ({ nodeId, from, to, reason }) => {
|
|
1334
|
+
const nid = String(nodeId ?? 'unknown');
|
|
1335
|
+
log.warn(`☯️ KARMA: Node ${peerTag(nid)} demoted ${from}→${to} (${reason})`);
|
|
1336
|
+
// Schedule NPU trust prediction for the demoted node
|
|
1337
|
+
const evidence = this.karmaModel.getEvidence(nid);
|
|
1338
|
+
if (evidence) {
|
|
1339
|
+
this._scheduledTrustPrediction(evidence).then(({ result }) => {
|
|
1340
|
+
if (result?.predicted) {
|
|
1341
|
+
const agrees = result.predicted === ['UNTRUSTED', 'SEEKING', 'AWAKENED', 'ENLIGHTENED'][to];
|
|
1342
|
+
log.debug(`☯️ KARMA NPU: ${result.source} predicts ${result.predicted} (${agrees ? 'agrees' : 'disagrees'} with rule-based ${to})`);
|
|
1343
|
+
}
|
|
1344
|
+
}).catch(() => { }); // Non-fatal
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
log.info('✓ KARMA trust model initialized (SAKSHI → trust assessment pipeline)');
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// =========================================================================
|
|
1352
|
+
// TERNARY HARMONIZATION — SST × YPC-27 × 144T × ML unification
|
|
1353
|
+
// =========================================================================
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Initialize the ternary harmonization stack.
|
|
1357
|
+
* Wires together SST-rotated YPC-27 checksums, batch verification,
|
|
1358
|
+
* ternary ML inference, and 144T hierarchical addressing.
|
|
1359
|
+
*
|
|
1360
|
+
* Call after _initKarma() since it depends on the trust model.
|
|
1361
|
+
*/
|
|
1362
|
+
async _initTernaryHarmonization() {
|
|
1363
|
+
log.info('◬ Initializing ternary harmonization stack...');
|
|
1364
|
+
|
|
1365
|
+
// ── 1. 144T Address — derive from node identity ──
|
|
1366
|
+
const nodeId = this.identity?.publicKeyHex || crypto.randomBytes(32).toString('hex');
|
|
1367
|
+
this.tritAddress = hexIdToAddress(nodeId, {
|
|
1368
|
+
galaxy: 0, // Galaxy 0 = default mesh
|
|
1369
|
+
});
|
|
1370
|
+
log.info(`◬ 144T address: ${this.tritAddress.toString()}`);
|
|
1371
|
+
|
|
1372
|
+
// ── 2. Ternary routing table ──
|
|
1373
|
+
this.ternaryRouter = new TernaryRoutingTable(this.tritAddress, 6);
|
|
1374
|
+
|
|
1375
|
+
// Wire mesh peer connections → ternary routing table
|
|
1376
|
+
this.mesh.on('peer:connected', (peerId) => {
|
|
1377
|
+
try {
|
|
1378
|
+
const peerAddress = hexIdToAddress(peerId);
|
|
1379
|
+
this.ternaryRouter.addPeer(peerId, peerAddress);
|
|
1380
|
+
log.debug(`◬ 144T: Added peer ${peerTag(peerId)} (tier distance: ${this.tritAddress.tierDistance(peerAddress)})`);
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
log.debug(`◬ 144T: Could not add peer address: ${err.message}`);
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
this.mesh.on('peer:disconnected', (peerId) => {
|
|
1387
|
+
this.ternaryRouter.removePeer(peerId);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// ── 3. Ternary inference adapter (bridges TRIBHUJ → ONNX) ──
|
|
1391
|
+
this.ternaryInference = new TernaryInferenceAdapter(accel.inference);
|
|
1392
|
+
|
|
1393
|
+
// Wire KARMA trust changes → ternary trust classification (second opinion)
|
|
1394
|
+
if (this.karmaModel) {
|
|
1395
|
+
this.karmaModel.on('promoted', ({ nodeId, to }) => {
|
|
1396
|
+
const evidence = this.karmaModel.getEvidence(nodeId);
|
|
1397
|
+
if (evidence) {
|
|
1398
|
+
this._scheduledTernaryTrustClassification(nodeId, evidence.trustScore || 50)
|
|
1399
|
+
.catch(() => { }); // Non-fatal
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
this.karmaModel.on('demoted', ({ nodeId, to }) => {
|
|
1404
|
+
const evidence = this.karmaModel.getEvidence(nodeId);
|
|
1405
|
+
if (evidence) {
|
|
1406
|
+
this._scheduledTernaryTrustClassification(nodeId, evidence.trustScore || 50)
|
|
1407
|
+
.catch(() => { }); // Non-fatal
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// ── 4. Batch checksum verifier — start auto-flush ──
|
|
1413
|
+
// The BatchChecksumVerifier uses ComputeScheduler internally
|
|
1414
|
+
log.info(`◬ Batch checksum verifier ready (flush threshold: ${batchChecksumVerifier.batchSize})`);
|
|
1415
|
+
|
|
1416
|
+
log.info('✓ Ternary harmonization stack initialized (YPC27_SST + BatchVerify + TernaryML + 144T)');
|
|
1417
|
+
|
|
1418
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1419
|
+
// SANGHA — Unified Component Attestation (collective security)
|
|
1420
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1421
|
+
// SANGHA creates cryptographic synapses between components for mutual
|
|
1422
|
+
// attestation. Unlike isolation (each stands alone), SANGHA components
|
|
1423
|
+
// protect each other — no component can be compromised silently.
|
|
1424
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1425
|
+
log.info('🔗 Initializing SANGHA (collective attestation)...');
|
|
1426
|
+
|
|
1427
|
+
const sangha = getSangha();
|
|
1428
|
+
|
|
1429
|
+
// Bind time source for temporal attestations
|
|
1430
|
+
if (this.timeSource) {
|
|
1431
|
+
sangha.bindTimeSource(this.timeSource);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Register core components with the collective
|
|
1435
|
+
// Each component provides a state getter for antibody circulation
|
|
1436
|
+
joinSangha(SANGHA_COMPONENT.CRYPTO, accel, () => ({
|
|
1437
|
+
initialized: accel.initialized,
|
|
1438
|
+
nativeSha3: accel.HW.nativeSha3,
|
|
1439
|
+
gpuAvailable: accel.HW.gpuAvailable,
|
|
1440
|
+
npuAvailable: accel.HW.npuAvailable,
|
|
1441
|
+
}));
|
|
1442
|
+
|
|
1443
|
+
joinSangha(SANGHA_COMPONENT.ORACLE, this.oracle, () => ({
|
|
1444
|
+
network: this.oracle?.getNetworkId?.() || 'unknown',
|
|
1445
|
+
epoch: this.consensus?.getCurrentEpoch?.() || 0,
|
|
1446
|
+
timeSource: this.timeSource?.getSourceType?.() || 'unknown',
|
|
1447
|
+
}));
|
|
1448
|
+
|
|
1449
|
+
joinSangha(SANGHA_COMPONENT.MESH, this.mesh, () => ({
|
|
1450
|
+
peerId: this.identity?.peerId || 'unknown',
|
|
1451
|
+
peerCount: this.mesh?.getPeerCount?.() || 0,
|
|
1452
|
+
annexActive: this.mesh?.annex?.enabled || false,
|
|
1453
|
+
}));
|
|
1454
|
+
|
|
1455
|
+
joinSangha(SANGHA_COMPONENT.HTTP, this.app, () => ({
|
|
1456
|
+
port: this.boundHttpPort || this.config.httpPort,
|
|
1457
|
+
routes: this.app?._router?.stack?.length || 0,
|
|
1458
|
+
}));
|
|
1459
|
+
|
|
1460
|
+
joinSangha(SANGHA_COMPONENT.IDENTITY, this.identity, () => ({
|
|
1461
|
+
peerId: this.identity?.peerId || 'unknown',
|
|
1462
|
+
keyAlgorithm: 'ML-DSA-65',
|
|
1463
|
+
hasPrivateKey: !!this.identity?.privateKey,
|
|
1464
|
+
}));
|
|
1465
|
+
|
|
1466
|
+
// Start the collective (antibody circulation every 5s)
|
|
1467
|
+
sangha.start({ circulationIntervalMs: 5000 });
|
|
1468
|
+
|
|
1469
|
+
// Subscribe to collective events
|
|
1470
|
+
sangha.on('anomalyDetected', (anomalies) => {
|
|
1471
|
+
log.error('🚨 SANGHA: Anomalies detected in component collective', {
|
|
1472
|
+
count: anomalies.length,
|
|
1473
|
+
types: anomalies.map(a => a.type),
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
sangha.on('collectiveResponse', (response) => {
|
|
1478
|
+
log.warn('🛡️ SANGHA: Collective response triggered', response);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
this.sangha = sangha;
|
|
1482
|
+
log.info('✓ SANGHA initialized (collective attestation active)');
|
|
1483
|
+
|
|
1484
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1485
|
+
// FS HARDENING — File Integrity with SANGHA-FS Integration
|
|
1486
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1487
|
+
// File guardians protect critical files and join the SANGHA collective.
|
|
1488
|
+
// Tampering triggers collective response — no silent compromise possible.
|
|
1489
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1490
|
+
log.info('[FS] Initializing FS Hardening (file guardians)...');
|
|
1491
|
+
|
|
1492
|
+
const fsHardening = getFSHardening(this.dataDir);
|
|
1493
|
+
await fsHardening.init();
|
|
1494
|
+
|
|
1495
|
+
// Bind to SANGHA for collective response
|
|
1496
|
+
fsHardening.bindSangha(sangha);
|
|
1497
|
+
|
|
1498
|
+
// Register FS as a SANGHA component
|
|
1499
|
+
joinSangha('fs', fsHardening, async () => {
|
|
1500
|
+
const status = fsHardening.getStatus();
|
|
1501
|
+
return {
|
|
1502
|
+
guardianCount: status.files.length,
|
|
1503
|
+
allLocked: status.files.every(f => f.locked),
|
|
1504
|
+
sanghaConnected: status.sanghaConnected,
|
|
1505
|
+
};
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
// Forward tamper events to SANGHA
|
|
1509
|
+
fsHardening.on('tamper', (event) => {
|
|
1510
|
+
log.error('[!] FS TAMPER DETECTED - alerting SANGHA', event);
|
|
1511
|
+
// The collective will respond via anomalyDetected event
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// Start periodic verification (30s interval)
|
|
1515
|
+
fsHardening.start(30000);
|
|
1516
|
+
|
|
1517
|
+
this.fsHardening = fsHardening;
|
|
1518
|
+
log.info('✓ FS Hardening initialized', { guardians: fsHardening.getStatus().files.length });
|
|
1519
|
+
|
|
1520
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1521
|
+
// MEMORY SAFETY — Circulating Canaries
|
|
1522
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1523
|
+
// Canaries are strategically-placed memory regions with known content.
|
|
1524
|
+
// During SANGHA circulation, canaries are checksummed and attested.
|
|
1525
|
+
// Corruption (buffer overflow, use-after-free) is detected in one cycle.
|
|
1526
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1527
|
+
log.info('[MEM] Initializing Memory Safety (circulating canaries)...');
|
|
1528
|
+
|
|
1529
|
+
const memorySafety = getMemorySafety();
|
|
1530
|
+
memorySafety.init();
|
|
1531
|
+
|
|
1532
|
+
// Bind to SANGHA for collective response
|
|
1533
|
+
memorySafety.bindSangha(sangha);
|
|
1534
|
+
|
|
1535
|
+
// Register as SANGHA component
|
|
1536
|
+
joinSangha('memory', memorySafety, async () => {
|
|
1537
|
+
return memorySafety.getState();
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
// Forward corruption events
|
|
1541
|
+
memorySafety.on('corruption', (corruptions) => {
|
|
1542
|
+
log.error('[!] MEMORY CORRUPTION - alerting SANGHA', { count: corruptions.length });
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
// Start monitoring (sync with SANGHA circulation)
|
|
1546
|
+
memorySafety.start(5000);
|
|
1547
|
+
|
|
1548
|
+
this.memorySafety = memorySafety;
|
|
1549
|
+
log.info('✓ Memory Safety initialized', {
|
|
1550
|
+
canaries: memorySafety.getStatus().heapCanaries +
|
|
1551
|
+
memorySafety.getStatus().closureCanaries +
|
|
1552
|
+
memorySafety.getStatus().nativeCanaries,
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1556
|
+
// TEMPORAL CODE SIGNING — GPS-bound signatures with auto-expiry
|
|
1557
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1558
|
+
// Traditional code signing: sign once, valid forever (until compromise).
|
|
1559
|
+
// Temporal signing: signatures BREATHE — bound to GPS time, auto-expire.
|
|
1560
|
+
//
|
|
1561
|
+
// This forces:
|
|
1562
|
+
// - Regular re-attestation of releases
|
|
1563
|
+
// - Leaked/stolen signatures become useless after expiry
|
|
1564
|
+
// - Nodes reject code signed outside the trust window
|
|
1565
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1566
|
+
log.info('[SIGN] Initializing Temporal Code Signing...');
|
|
1567
|
+
|
|
1568
|
+
const temporalSigner = getTemporalSigner({
|
|
1569
|
+
timeSource: this.timeSourceDetector,
|
|
1570
|
+
networkId: this.networkId || this._identity?.network?.name || 'yakmesh',
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
// Bind GPS time source if available
|
|
1574
|
+
if (this.timeSourceDetector) {
|
|
1575
|
+
temporalSigner.bindTimeSource(this.timeSourceDetector);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Register as SANGHA component (signer participates in collective)
|
|
1579
|
+
joinSangha('sign', temporalSigner, async () => {
|
|
1580
|
+
return temporalSigner.getStatus();
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
this.temporalSigner = temporalSigner;
|
|
1584
|
+
log.info('✓ Temporal Signing initialized', temporalSigner.getStatus());
|
|
1585
|
+
|
|
1586
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1587
|
+
// KARMA RATE LIMITER — Trust-adaptive rate limiting + input validation
|
|
1588
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1589
|
+
// Traditional rate limiting: Fixed thresholds for everyone.
|
|
1590
|
+
// KARMA-adaptive: Throughput scales with earned reputation.
|
|
1591
|
+
//
|
|
1592
|
+
// - Unknown peers: 10 req/min (strict)
|
|
1593
|
+
// - Hostile (KARMA 0-10): 2 req/min (almost blocked)
|
|
1594
|
+
// - Low (KARMA 11-30): 25 req/min
|
|
1595
|
+
// - Medium (KARMA 31-60): 50 req/min
|
|
1596
|
+
// - High (KARMA 61-85): 100 req/min
|
|
1597
|
+
// - Excellent (KARMA 86-100): 200 req/min
|
|
1598
|
+
//
|
|
1599
|
+
// This creates economic incentive: good behavior → higher throughput.
|
|
1600
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1601
|
+
log.info('[RATE] Initializing KARMA Rate Limiter...');
|
|
1602
|
+
|
|
1603
|
+
const rateLimiter = getKarmaRateLimiter();
|
|
1604
|
+
|
|
1605
|
+
// Bind to KARMA trust model for reputation lookups
|
|
1606
|
+
if (this.karmaTrust) {
|
|
1607
|
+
rateLimiter.bindKarmaTrust(this.karmaTrust);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Bind to SANGHA for collective response
|
|
1611
|
+
rateLimiter.bindSangha(sangha);
|
|
1612
|
+
|
|
1613
|
+
// Register as SANGHA component
|
|
1614
|
+
joinSangha('rate', rateLimiter, async () => {
|
|
1615
|
+
return rateLimiter.getState();
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
// Forward block events
|
|
1619
|
+
rateLimiter.on('blocked', ({ peerId, reason }) => {
|
|
1620
|
+
log.warn('[BLOCKED] Peer rate-limited', { peerId: peerId.slice(0, 16), reason });
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// Periodic cleanup of stale buckets
|
|
1624
|
+
setInterval(() => rateLimiter.cleanup(), 300000); // Every 5 minutes
|
|
1625
|
+
|
|
1626
|
+
this.rateLimiter = rateLimiter;
|
|
1627
|
+
log.info('✓ KARMA Rate Limiter initialized', rateLimiter.getStatus());
|
|
1628
|
+
|
|
1629
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1630
|
+
// SECURE CONFIG — Oracle-attested configuration management
|
|
1631
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1632
|
+
// Traditional secure defaults: Ship with good defaults, hope they stick.
|
|
1633
|
+
// Oracle-attested config: Configuration is hashed and cryptographically
|
|
1634
|
+
// verified. Any deviation from the secure profile is detected.
|
|
1635
|
+
//
|
|
1636
|
+
// Profiles:
|
|
1637
|
+
// - PARANOID: Maximum security, minimal attack surface
|
|
1638
|
+
// - HARDENED: Production-ready security (default)
|
|
1639
|
+
// - STANDARD: Balanced security
|
|
1640
|
+
// - DEVELOPMENT: Relaxed for local dev (warnings only)
|
|
1641
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1642
|
+
log.info('[CFG] Initializing Secure Config...');
|
|
1643
|
+
|
|
1644
|
+
const secureConfig = getSecureConfig(); // Uses YAKMESH_SECURITY_PROFILE env or HARDENED
|
|
1645
|
+
|
|
1646
|
+
// Bind to SANGHA for collective verification
|
|
1647
|
+
secureConfig.bindSangha(sangha);
|
|
1648
|
+
|
|
1649
|
+
// Register as SANGHA component
|
|
1650
|
+
joinSangha('config', secureConfig, async () => {
|
|
1651
|
+
return secureConfig.getState();
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
// Forward deviation events
|
|
1655
|
+
secureConfig.on('deviation', ({ profileLevel, deviations }) => {
|
|
1656
|
+
log.warn('[WARN] Config deviation from secure profile', {
|
|
1657
|
+
profile: profileLevel,
|
|
1658
|
+
deviations: deviations.length,
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
this.secureConfig = secureConfig;
|
|
1663
|
+
log.info('✓ Secure Config initialized', secureConfig.getStatus());
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// =========================================================================
|
|
1667
|
+
// SCHEDULED WORKLOADS — route ML inference through ComputeScheduler
|
|
1668
|
+
// =========================================================================
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Schedule a ternary trust classification through the compute scheduler.
|
|
1672
|
+
* Routes KARMA trust scores through the TernaryInferenceAdapter for
|
|
1673
|
+
* SST-family-aware classification (NEGATIVE/NEUTRAL/POSITIVE mapping).
|
|
1674
|
+
*
|
|
1675
|
+
* NORMAL priority — enrichment task, not security-critical path.
|
|
1676
|
+
*
|
|
1677
|
+
* @param {string} nodeId — peer to classify
|
|
1678
|
+
* @param {number} trustScore — 0-100 trust score from KARMA
|
|
1679
|
+
* @returns {Promise<{ outcome, device, result, execMs, waitMs }>}
|
|
1680
|
+
*/
|
|
1681
|
+
_scheduledTernaryTrustClassification(nodeId, trustScore) {
|
|
1682
|
+
const executor = () => this.ternaryInference.classifyTrust(trustScore);
|
|
1683
|
+
return accel.scheduler.submit({
|
|
1684
|
+
type: 'ternary-trust',
|
|
1685
|
+
priority: accel.Priority.NORMAL,
|
|
1686
|
+
affinity: accel.Affinity.NPU_PREFERRED,
|
|
1687
|
+
timeoutMs: 2000,
|
|
1688
|
+
inputSize: 4,
|
|
1689
|
+
executors: { npu: executor, gpu: executor, cpu: executor },
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Schedule a batch YPC-27 checksum verification via the compute scheduler.
|
|
1695
|
+
* Wraps the BatchChecksumVerifier for protocol-level packet integrity.
|
|
1696
|
+
*
|
|
1697
|
+
* HIGH priority — checksum verification is integrity-critical.
|
|
1698
|
+
*
|
|
1699
|
+
* @param {string} domain — protocol domain (e.g., 'STUPA', 'NAKPAK')
|
|
1700
|
+
* @param {string} nodeId — peer node ID for seed derivation
|
|
1701
|
+
* @param {Uint8Array} data — packet data to verify
|
|
1702
|
+
* @param {Object} checksum — expected checksum from wire
|
|
1703
|
+
* @returns {Promise<boolean>}
|
|
1704
|
+
*/
|
|
1705
|
+
async _scheduledChecksumVerify(domain, nodeId, data, checksum) {
|
|
1706
|
+
return batchChecksumVerifier.enqueue(domain, nodeId, data, checksum);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Schedule a STEADYWATCH entropy quality check through the compute scheduler.
|
|
1711
|
+
* CRITICAL priority — entropy degradation is a security emergency.
|
|
1712
|
+
*
|
|
1713
|
+
* @param {Uint8Array} data — raw bytes to score
|
|
1714
|
+
* @returns {Promise<{ outcome, device, result, execMs, waitMs }>}
|
|
1715
|
+
*/
|
|
1716
|
+
_scheduledEntropyCheck(data) {
|
|
1717
|
+
const executor = () => steadywatch.scoreEntropy(data);
|
|
1718
|
+
return accel.scheduler.submit({
|
|
1719
|
+
type: 'entropy-sentinel',
|
|
1720
|
+
priority: accel.Priority.CRITICAL,
|
|
1721
|
+
affinity: accel.Affinity.NPU_PREFERRED,
|
|
1722
|
+
timeoutMs: 2000,
|
|
1723
|
+
inputSize: data?.length || 256,
|
|
1724
|
+
executors: { npu: executor, gpu: executor, cpu: executor },
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Schedule a SAKSHI anomaly assessment through the compute scheduler.
|
|
1730
|
+
* HIGH priority — anomaly detection is security-sensitive.
|
|
1731
|
+
*
|
|
1732
|
+
* @param {string} nodeId — peer to assess
|
|
1733
|
+
* @param {Object} context — additional context features for the ONNX model
|
|
1734
|
+
* @returns {Promise<{ outcome, device, result, execMs, waitMs }>}
|
|
1735
|
+
*/
|
|
1736
|
+
_scheduledAnomalyAssessment(nodeId, context = {}) {
|
|
1737
|
+
const executor = () => this.velocityMonitor.assessNode(nodeId, context);
|
|
1738
|
+
return accel.scheduler.submit({
|
|
1739
|
+
type: 'sakshi-anomaly',
|
|
1740
|
+
priority: accel.Priority.HIGH,
|
|
1741
|
+
affinity: accel.Affinity.NPU_PREFERRED,
|
|
1742
|
+
timeoutMs: 3000,
|
|
1743
|
+
inputSize: 48, // 12 × float32
|
|
1744
|
+
executors: { npu: executor, gpu: executor, cpu: executor },
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Schedule a KARMA trust prediction through the compute scheduler.
|
|
1750
|
+
* HIGH priority — trust decisions affect network security.
|
|
1751
|
+
*
|
|
1752
|
+
* @param {Object} evidence — KarmaEvidence instance
|
|
1753
|
+
* @returns {Promise<{ outcome, device, result, execMs, waitMs }>}
|
|
1754
|
+
*/
|
|
1755
|
+
_scheduledTrustPrediction(evidence) {
|
|
1756
|
+
const executor = () => this.karmaModel.predictTrustLevel(evidence);
|
|
1757
|
+
return accel.scheduler.submit({
|
|
1758
|
+
type: 'karma-trust',
|
|
1759
|
+
priority: accel.Priority.HIGH,
|
|
1760
|
+
affinity: accel.Affinity.NPU_PREFERRED,
|
|
1761
|
+
timeoutMs: 3000,
|
|
1762
|
+
inputSize: 56, // 14 × float32
|
|
1763
|
+
executors: { npu: executor, gpu: executor, cpu: executor },
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Schedule batch ML-DSA-65 signature verification through the scheduler.
|
|
1769
|
+
* HIGH priority — signature verification is security-critical.
|
|
1770
|
+
*
|
|
1771
|
+
* @param {Uint8Array} signature
|
|
1772
|
+
* @param {Uint8Array} message
|
|
1773
|
+
* @param {Uint8Array} publicKey
|
|
1774
|
+
* @returns {Promise<{ outcome, device, result, execMs, waitMs }>}
|
|
1775
|
+
*/
|
|
1776
|
+
_scheduledBatchVerify(signature, message, publicKey) {
|
|
1777
|
+
const executor = () => accel.batchVerify.enqueue(signature, message, publicKey);
|
|
1778
|
+
return accel.scheduler.submit({
|
|
1779
|
+
type: 'batch-verify',
|
|
1780
|
+
priority: accel.Priority.HIGH,
|
|
1781
|
+
affinity: accel.Affinity.GPU_PREFERRED,
|
|
1782
|
+
timeoutMs: 5000,
|
|
1783
|
+
inputSize: (signature?.length || 0) + (message?.length || 0) + (publicKey?.length || 0),
|
|
1784
|
+
executors: { gpu: executor, npu: executor, cpu: executor },
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Start periodic scheduled workloads that exercise the compute scheduler.
|
|
1790
|
+
* Called once during boot after all subsystems are initialized.
|
|
1791
|
+
*/
|
|
1792
|
+
_startScheduledWorkloads() {
|
|
1793
|
+
// ── Periodic entropy health check (every 30s) ──
|
|
1794
|
+
// Generates fresh random bytes and scores them through STEADYWATCH sentinel.
|
|
1795
|
+
// Detects entropy source degradation before it impacts ANNEX keygen.
|
|
1796
|
+
this._entropyCheckTimer = setInterval(async () => {
|
|
1797
|
+
try {
|
|
1798
|
+
const sample = crypto.randomBytes(256);
|
|
1799
|
+
const { result } = await this._scheduledEntropyCheck(sample);
|
|
1800
|
+
if (result && result.score < 0.6) {
|
|
1801
|
+
log.warn(`⚠️ STEADYWATCH: Entropy quality degraded (score=${result.score.toFixed(3)}, verdict=${result.verdict})`);
|
|
1802
|
+
}
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
// Scheduler rejection (queue full) is fine — non-fatal
|
|
1805
|
+
if (err?.outcome !== 'rejected') {
|
|
1806
|
+
log.debug(`Entropy check error: ${err.message || err.reason || 'unknown'}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}, 30_000);
|
|
1810
|
+
if (this._entropyCheckTimer.unref) this._entropyCheckTimer.unref();
|
|
1811
|
+
|
|
1812
|
+
// ── Periodic peer assessment sweep (every 60s) ──
|
|
1813
|
+
// Deep-assesses the 5 most active peers via SAKSHI anomaly model.
|
|
1814
|
+
// Catches slow-burn attacks that velocity z-scores alone miss.
|
|
1815
|
+
this._peerAssessTimer = setInterval(async () => {
|
|
1816
|
+
if (!this.velocityMonitor || !this.mesh) return;
|
|
1817
|
+
const peers = this.mesh.getPeers ? this.mesh.getPeers() : [];
|
|
1818
|
+
// Assess up to 5 peers per sweep — don't flood the scheduler
|
|
1819
|
+
const batch = peers.slice(0, 5);
|
|
1820
|
+
for (const peerId of batch) {
|
|
1821
|
+
try {
|
|
1822
|
+
const karmaEvidence = this.karmaModel?.getEvidence(peerId);
|
|
1823
|
+
const context = {
|
|
1824
|
+
karmaScore: karmaEvidence?.trustScore ? karmaEvidence.trustScore / 100 : 0.5,
|
|
1825
|
+
uptimePercent: 0.5,
|
|
1826
|
+
networkAgeDays: karmaEvidence ? (Date.now() - (karmaEvidence.firstSeen || Date.now())) / 86400000 : 0,
|
|
1827
|
+
};
|
|
1828
|
+
const { result } = await this._scheduledAnomalyAssessment(peerId, context);
|
|
1829
|
+
if (result && result.anomalyScore > 0.7) {
|
|
1830
|
+
log.warn(`👁️ SAKSHI: High anomaly score for ${peerTag(peerId)} (score=${result.anomalyScore.toFixed(3)}, source=${result.source})`);
|
|
1831
|
+
}
|
|
1832
|
+
} catch {
|
|
1833
|
+
// Non-fatal — scheduler may have rejected the task
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}, 60_000);
|
|
1837
|
+
if (this._peerAssessTimer.unref) this._peerAssessTimer.unref();
|
|
1838
|
+
|
|
1839
|
+
log.info('✓ Scheduled workloads: entropy-sentinel(30s) + peer-assessment(60s) via ComputeScheduler');
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Initialize KOMM WebSocket upgrade on the HTTP server
|
|
1844
|
+
* Provides real-time KATHA messages and VANI signaling over WS.
|
|
1845
|
+
*
|
|
1846
|
+
* Clients connect to:
|
|
1847
|
+
* ws://host:port/komm/ws — unified KOMM channel
|
|
1848
|
+
* Messages are JSON: { type: 'katha:event'|'katha:typing'|'vani:signal'|..., data: {...} }
|
|
1849
|
+
*/
|
|
1850
|
+
_initKommWebSocket() {
|
|
1851
|
+
if (!this.http || !this.kathaHub) return;
|
|
1852
|
+
|
|
1853
|
+
this.kommWss = new WebSocketServer({ noServer: true, maxPayload: 1048576 }); // 1MB max message size
|
|
1854
|
+
|
|
1855
|
+
// Per-client ANNEX sessions for PQ encryption
|
|
1856
|
+
const kommAnnexSessions = new Map(); // ws → ServerAnnexSession
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* secureSend — encrypt outbound KOMM messages via ANNEX when session exists
|
|
1860
|
+
*/
|
|
1861
|
+
const secureSend = (ws, data) => {
|
|
1862
|
+
if (ws.readyState !== 1) return; // OPEN
|
|
1863
|
+
const session = kommAnnexSessions.get(ws);
|
|
1864
|
+
if (session && !session.isExpired()) {
|
|
1865
|
+
try {
|
|
1866
|
+
const encrypted = session.encrypt(typeof data === 'string' ? data : JSON.stringify(data));
|
|
1867
|
+
ws.send(JSON.stringify({ type: ANNEX_HANDSHAKE_TYPE.ENCRYPTED, payload: encrypted }));
|
|
1868
|
+
} catch {
|
|
1869
|
+
// Encryption failed — drop message (no plaintext fallback)
|
|
1870
|
+
log.warn('KOMM ANNEX encrypt failed — message dropped');
|
|
1871
|
+
}
|
|
1872
|
+
} else {
|
|
1873
|
+
// No ANNEX session yet — send plaintext (only during handshake/migration)
|
|
1874
|
+
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
// Handle upgrade requests for /komm/ws path
|
|
1879
|
+
this.http.on('upgrade', (request, socket, head) => {
|
|
1880
|
+
// Catch TCP errors on the raw socket during upgrade — prevents
|
|
1881
|
+
// ECONNRESET from bubbling up as an uncaught exception
|
|
1882
|
+
socket.on('error', (err) => {
|
|
1883
|
+
log.debug('Upgrade socket error (benign)', { code: err.code, msg: err.message });
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
const url = new URL(request.url, `http://${request.headers.host}`);
|
|
1887
|
+
|
|
1888
|
+
if (url.pathname === '/komm/ws') {
|
|
1889
|
+
this.kommWss.handleUpgrade(request, socket, head, (ws) => {
|
|
1890
|
+
this.kommWss.emit('connection', ws, request);
|
|
1891
|
+
});
|
|
1892
|
+
} else {
|
|
1893
|
+
// Not our path — let other upgrade handlers (mesh WS) deal with it
|
|
1894
|
+
// If no handler, the socket just hangs. Destroy it if unhandled.
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
// Track connected KOMM WebSocket clients
|
|
1899
|
+
const kommClients = new Set();
|
|
1900
|
+
|
|
1901
|
+
this.kommWss.on('connection', (ws, request) => {
|
|
1902
|
+
kommClients.add(ws);
|
|
1903
|
+
log.debug('📡 KOMM WS client connected');
|
|
1904
|
+
|
|
1905
|
+
ws.on('close', () => {
|
|
1906
|
+
kommClients.delete(ws);
|
|
1907
|
+
// Destroy ANNEX session on close — zero key material
|
|
1908
|
+
const session = kommAnnexSessions.get(ws);
|
|
1909
|
+
if (session) { session.destroy(); kommAnnexSessions.delete(ws); }
|
|
1910
|
+
log.debug('📡 KOMM WS client disconnected');
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
ws.on('error', () => {
|
|
1914
|
+
kommClients.delete(ws);
|
|
1915
|
+
const session = kommAnnexSessions.get(ws);
|
|
1916
|
+
if (session) { session.destroy(); kommAnnexSessions.delete(ws); }
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
// Handle incoming messages from client
|
|
1920
|
+
ws.on('message', (raw) => {
|
|
1921
|
+
try {
|
|
1922
|
+
const msg = JSON.parse(raw.toString());
|
|
1923
|
+
|
|
1924
|
+
// ── ANNEX handshake layer (before any application logic) ──
|
|
1925
|
+
if (msg.type === ANNEX_HANDSHAKE_TYPE.PUBLIC_KEY) {
|
|
1926
|
+
const session = new ServerAnnexSession({
|
|
1927
|
+
localId: peerTag(this.identity.identity.nodeId),
|
|
1928
|
+
remoteId: msg.clientId || 'komm-client',
|
|
1929
|
+
});
|
|
1930
|
+
const result = session.handlePublicKey(msg.publicKey);
|
|
1931
|
+
kommAnnexSessions.set(ws, session);
|
|
1932
|
+
ws.send(JSON.stringify({
|
|
1933
|
+
type: ANNEX_HANDSHAKE_TYPE.ENCAPSULATED,
|
|
1934
|
+
ciphertext: result.ciphertext,
|
|
1935
|
+
serverId: peerTag(this.identity.identity.nodeId),
|
|
1936
|
+
sessionId: msg.sessionId,
|
|
1937
|
+
}));
|
|
1938
|
+
log.debug('📡 KOMM ANNEX handshake complete (ML-KEM-768)');
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
if (msg.type === 'annex:rekey_ack') {
|
|
1943
|
+
const session = kommAnnexSessions.get(ws);
|
|
1944
|
+
if (session) {
|
|
1945
|
+
session.rekey(msg.publicKey);
|
|
1946
|
+
log.debug('📡 KOMM ANNEX rekeyed');
|
|
1947
|
+
}
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
if (msg.type === ANNEX_HANDSHAKE_TYPE.ENCRYPTED) {
|
|
1952
|
+
const session = kommAnnexSessions.get(ws);
|
|
1953
|
+
if (!session) return;
|
|
1954
|
+
const plaintext = session.decrypt(msg.payload);
|
|
1955
|
+
const decrypted = JSON.parse(plaintext);
|
|
1956
|
+
// Check if rekey needed
|
|
1957
|
+
if (session.isNearingExpiry()) {
|
|
1958
|
+
secureSend(ws, { type: 'annex:rekey', reason: 'threshold' });
|
|
1959
|
+
}
|
|
1960
|
+
this._handleKommWsMessage(decrypted, ws, secureSend);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Plaintext fallback (backward compat during migration)
|
|
1965
|
+
this._handleKommWsMessage(msg, ws, secureSend);
|
|
1966
|
+
} catch {
|
|
1967
|
+
secureSend(ws, { error: 'Invalid message' });
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// Send welcome (may be plaintext if ANNEX not yet established)
|
|
1972
|
+
secureSend(ws, {
|
|
1973
|
+
type: 'welcome',
|
|
1974
|
+
nodeId: peerTag(this.identity.identity.nodeId),
|
|
1975
|
+
capabilities: ['katha', 'vani', 'yurt'],
|
|
1976
|
+
});
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
// Broadcast helper — now encrypts per-client via ANNEX
|
|
1980
|
+
const broadcastKomm = (type, data) => {
|
|
1981
|
+
const payload = { type, data, ts: Date.now() };
|
|
1982
|
+
for (const client of kommClients) {
|
|
1983
|
+
secureSend(client, payload);
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
// Wire KATHA events → WS broadcast
|
|
1988
|
+
if (this.kathaHub) {
|
|
1989
|
+
this.kathaHub.on?.('message', (msg) => broadcastKomm('katha:message', msg));
|
|
1990
|
+
this.kathaHub.on?.('typing', (data) => broadcastKomm('katha:typing', data));
|
|
1991
|
+
this.kathaHub.on?.('reaction', (data) => broadcastKomm('katha:reaction', data));
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Wire VANI signals → WS broadcast
|
|
1995
|
+
if (this.vaniHub) {
|
|
1996
|
+
this.vaniHub.on?.('signal', (signal) => broadcastKomm('vani:signal', signal));
|
|
1997
|
+
this.vaniHub.on?.('callStateChanged', (state) => broadcastKomm('vani:callState', state));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// Wire YURT room events → WS broadcast
|
|
2001
|
+
if (this.yurtHub) {
|
|
2002
|
+
this.yurtHub.on?.('roomRegistered', (room) => broadcastKomm('yurt:registered', room));
|
|
2003
|
+
this.yurtHub.on?.('roomUnregistered', (room) => broadcastKomm('yurt:unregistered', room));
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Also broadcast gossip-received KATHA/VANI events
|
|
2007
|
+
if (this.mesh) {
|
|
2008
|
+
this.mesh.on('rumor', (topic, data, origin) => {
|
|
2009
|
+
if (topic === 'katha:event' || topic === 'katha:typing' ||
|
|
2010
|
+
topic === 'vani:signal') {
|
|
2011
|
+
broadcastKomm(topic, data);
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
log.info('✓ KOMM WebSocket initialized at /komm/ws (ANNEX PQ-encrypted)');
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Handle incoming KOMM WS messages from clients
|
|
2021
|
+
*/
|
|
2022
|
+
_handleKommWsMessage(msg, ws, secureSend) {
|
|
2023
|
+
const { type, data } = msg;
|
|
2024
|
+
|
|
2025
|
+
switch (type) {
|
|
2026
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2027
|
+
// KATHA (Chat) Handlers
|
|
2028
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2029
|
+
|
|
2030
|
+
case 'katha:auth':
|
|
2031
|
+
// Client authentication — store username for this connection
|
|
2032
|
+
ws._kathaUser = {
|
|
2033
|
+
username: msg.username || data?.username || 'anon',
|
|
2034
|
+
userId: msg.userId || data?.userId || `user_${Date.now()}`,
|
|
2035
|
+
clientType: msg.clientType || 'web',
|
|
2036
|
+
};
|
|
2037
|
+
secureSend(ws, {
|
|
2038
|
+
type: 'katha:auth-ok',
|
|
2039
|
+
userId: ws._kathaUser.userId,
|
|
2040
|
+
username: ws._kathaUser.username,
|
|
2041
|
+
});
|
|
2042
|
+
log.debug(`📡 KOMM client authenticated: ${ws._kathaUser.username}`);
|
|
2043
|
+
break;
|
|
2044
|
+
|
|
2045
|
+
case 'katha:list-channels':
|
|
2046
|
+
// Return list of channels
|
|
2047
|
+
const channels = [];
|
|
2048
|
+
if (this.kathaHub?.channels) {
|
|
2049
|
+
for (const [id, channel] of this.kathaHub.channels) {
|
|
2050
|
+
channels.push({
|
|
2051
|
+
id,
|
|
2052
|
+
name: channel.name || id,
|
|
2053
|
+
type: channel.type || 'text',
|
|
2054
|
+
memberCount: channel.members?.size || 0,
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// Add default channels if none exist
|
|
2059
|
+
if (channels.length === 0) {
|
|
2060
|
+
channels.push(
|
|
2061
|
+
{ id: 'general', name: 'general', type: 'text', memberCount: 1 },
|
|
2062
|
+
{ id: 'random', name: 'random', type: 'text', memberCount: 0 },
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
secureSend(ws, { type: 'katha:channels', channels });
|
|
2066
|
+
break;
|
|
2067
|
+
|
|
2068
|
+
case 'katha:join':
|
|
2069
|
+
// Join a channel
|
|
2070
|
+
const channelId = data?.channelId || msg.channelId;
|
|
2071
|
+
if (channelId && this.kathaHub?.join) {
|
|
2072
|
+
this.kathaHub.join(channelId, ws._kathaUser);
|
|
2073
|
+
}
|
|
2074
|
+
// Get channel messages
|
|
2075
|
+
const channel = this.kathaHub?.channels?.get(channelId);
|
|
2076
|
+
const messages = channel?.getMessages?.({ limit: 50 }) || [];
|
|
2077
|
+
const members = channel?.members ? Array.from(channel.members.values()) : [];
|
|
2078
|
+
secureSend(ws, {
|
|
2079
|
+
type: 'katha:joined',
|
|
2080
|
+
channelId,
|
|
2081
|
+
messages,
|
|
2082
|
+
members,
|
|
2083
|
+
});
|
|
2084
|
+
break;
|
|
2085
|
+
|
|
2086
|
+
case 'katha:send':
|
|
2087
|
+
// Process and broadcast chat message
|
|
2088
|
+
const sendData = {
|
|
2089
|
+
channelId: data.channelId,
|
|
2090
|
+
messageId: data.messageId || `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2091
|
+
userId: ws._kathaUser?.userId || data.userId,
|
|
2092
|
+
username: ws._kathaUser?.username || data.username,
|
|
2093
|
+
content: data.content,
|
|
2094
|
+
timestamp: new Date().toISOString(),
|
|
2095
|
+
type: data.type || 'katha:text',
|
|
2096
|
+
};
|
|
2097
|
+
|
|
2098
|
+
// Store message (if kathaHub supports it)
|
|
2099
|
+
if (this.kathaHub?.send) {
|
|
2100
|
+
this.kathaHub.send(sendData);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// Broadcast to all KOMM clients (including sender for confirmation)
|
|
2104
|
+
this.kommWss.clients.forEach(client => {
|
|
2105
|
+
if (client.readyState === WebSocket.OPEN && client._kathaUser) {
|
|
2106
|
+
client.send(JSON.stringify({ type: 'katha:message', data: sendData }));
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
break;
|
|
2110
|
+
|
|
2111
|
+
case 'katha:typing':
|
|
2112
|
+
// Broadcast typing indicator to channel members
|
|
2113
|
+
const typingData = {
|
|
2114
|
+
channelId: data.channelId,
|
|
2115
|
+
userId: ws._kathaUser?.userId || data.userId,
|
|
2116
|
+
username: ws._kathaUser?.username || data.username,
|
|
2117
|
+
isTyping: data.isTyping !== false,
|
|
2118
|
+
};
|
|
2119
|
+
|
|
2120
|
+
if (this.kathaHub?.setTyping) {
|
|
2121
|
+
this.kathaHub.setTyping(typingData);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Broadcast to all KOMM clients in the same channel (except sender)
|
|
2125
|
+
this.kommWss.clients.forEach(client => {
|
|
2126
|
+
if (client !== ws && client.readyState === WebSocket.OPEN && client._kathaUser) {
|
|
2127
|
+
client.send(JSON.stringify({ type: 'katha:typing', data: typingData }));
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
break;
|
|
2131
|
+
|
|
2132
|
+
case 'katha:reaction':
|
|
2133
|
+
// Toggle reaction on a message
|
|
2134
|
+
const reactionData = {
|
|
2135
|
+
channelId: data.channelId,
|
|
2136
|
+
messageId: data.messageId,
|
|
2137
|
+
emoji: data.emoji,
|
|
2138
|
+
userId: ws._kathaUser?.userId || data.userId,
|
|
2139
|
+
};
|
|
2140
|
+
|
|
2141
|
+
// Store reaction (if kathaHub supports it)
|
|
2142
|
+
if (this.kathaHub?.toggleReaction) {
|
|
2143
|
+
this.kathaHub.toggleReaction(reactionData);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Broadcast to all KOMM clients (including sender for confirmation)
|
|
2147
|
+
this.kommWss.clients.forEach(client => {
|
|
2148
|
+
if (client.readyState === WebSocket.OPEN && client._kathaUser) {
|
|
2149
|
+
client.send(JSON.stringify({ type: 'katha:reaction', data: reactionData }));
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
break;
|
|
2153
|
+
|
|
2154
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2155
|
+
// YURT (Rooms) Handlers
|
|
2156
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2157
|
+
|
|
2158
|
+
case 'yurt:browse':
|
|
2159
|
+
// Browse available rooms
|
|
2160
|
+
const rooms = [];
|
|
2161
|
+
if (this.yurtHub?.directory?.entries) {
|
|
2162
|
+
for (const [id, entry] of this.yurtHub.directory.entries) {
|
|
2163
|
+
rooms.push({
|
|
2164
|
+
id,
|
|
2165
|
+
name: entry.name || id,
|
|
2166
|
+
description: entry.description || '',
|
|
2167
|
+
memberCount: entry.memberCount || 0,
|
|
2168
|
+
isPublic: entry.isPublic !== false,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
secureSend(ws, { type: 'yurt:rooms', rooms });
|
|
2173
|
+
break;
|
|
2174
|
+
|
|
2175
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2176
|
+
// VANI (Voice/Video) Handlers
|
|
2177
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2178
|
+
|
|
2179
|
+
case 'vani:signal':
|
|
2180
|
+
if (this.vaniHub?.signal) {
|
|
2181
|
+
this.vaniHub.signal(data);
|
|
2182
|
+
}
|
|
2183
|
+
break;
|
|
2184
|
+
|
|
2185
|
+
case 'vani:call':
|
|
2186
|
+
if (this.vaniHub?.initiateCall) {
|
|
2187
|
+
this.vaniHub.initiateCall(data).then(result => {
|
|
2188
|
+
secureSend(ws, { type: 'vani:callResult', data: result });
|
|
2189
|
+
}).catch(() => { });
|
|
2190
|
+
}
|
|
2191
|
+
break;
|
|
2192
|
+
|
|
2193
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2194
|
+
// General
|
|
2195
|
+
// ══════════════════════════════════════════════════════════════════
|
|
2196
|
+
|
|
2197
|
+
case 'ping':
|
|
2198
|
+
secureSend(ws, { type: 'pong', ts: Date.now() });
|
|
2199
|
+
break;
|
|
2200
|
+
|
|
2201
|
+
default:
|
|
2202
|
+
log.debug(`📡 Unknown KOMM message type: ${type}`);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Handle oracle-validated content from peers
|
|
2208
|
+
*/
|
|
2209
|
+
_handleOracleContent(data, origin) {
|
|
2210
|
+
const { sealedPackage, attestations } = data;
|
|
2211
|
+
|
|
2212
|
+
// Verify the peer is running valid code
|
|
2213
|
+
if (!this.codeProof.isPeerVerified(origin)) {
|
|
2214
|
+
log.warn(`⚠️ Received content from unverified peer ${peerTag(origin)}`);
|
|
2215
|
+
// Challenge the peer
|
|
2216
|
+
const challenge = this.codeProof.generateChallenge(origin);
|
|
2217
|
+
this.gossip.spreadRumor('code_proof_challenge', challenge);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// Submit to consensus engine
|
|
2222
|
+
const result = this.consensus.receivePackage(data);
|
|
2223
|
+
|
|
2224
|
+
if (result.accepted) {
|
|
2225
|
+
log.info(`✓ Oracle content accepted: ${result.contentHash?.slice(0, 16)}...`);
|
|
2226
|
+
|
|
2227
|
+
// Record in replication for persistence
|
|
2228
|
+
this.replication.recordChange(
|
|
2229
|
+
`oracle_${sealedPackage.type}`,
|
|
2230
|
+
sealedPackage.contentHash,
|
|
2231
|
+
'UPSERT',
|
|
2232
|
+
sealedPackage.content
|
|
2233
|
+
);
|
|
2234
|
+
} else {
|
|
2235
|
+
log.warn(`✗ Oracle content rejected: ${result.reason}`);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* Handle iO network handshake from peer
|
|
2241
|
+
* Verifies network compatibility using fingerprints (no hash exposed)
|
|
2242
|
+
*/
|
|
2243
|
+
_handleNetworkHandshake(data, origin) {
|
|
2244
|
+
if (!this.genesisNetwork) return;
|
|
2245
|
+
|
|
2246
|
+
const { handshake, nodeId } = data;
|
|
2247
|
+
const verification = this.genesisNetwork.verifyHandshake(handshake);
|
|
2248
|
+
|
|
2249
|
+
// Register the peer
|
|
2250
|
+
const compatible = this.genesisNetwork.registerPeer(nodeId || origin, handshake);
|
|
2251
|
+
|
|
2252
|
+
if (compatible) {
|
|
2253
|
+
log.debug(`🌐 Peer ${peerTag(origin)} verified on same network: ${handshake.name}`);
|
|
2254
|
+
} else {
|
|
2255
|
+
log.debug(`⚠️ Peer ${peerTag(origin)} on different network: ${handshake.name} (${handshake.shortId})`);
|
|
2256
|
+
log.debug(` Our network: ${this.genesisNetwork.networkName} (${this.genesisNetwork.networkId})`);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// Optionally broadcast our handshake back
|
|
2260
|
+
if (compatible && !data.isResponse) {
|
|
2261
|
+
this.gossip.spreadRumor('network_handshake', {
|
|
2262
|
+
handshake: this.genesisNetwork.createHandshake(),
|
|
2263
|
+
nodeId: this.identity.identity.nodeId,
|
|
2264
|
+
isResponse: true,
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* Resolve a peer's ML-DSA-65 public key from mesh registries.
|
|
2271
|
+
* Mirrors the annex._getPeerPublicKey() pattern:
|
|
2272
|
+
* 1. WS peers (direct connections)
|
|
2273
|
+
* 2. Relay peer keys (signed relay registration)
|
|
2274
|
+
* 3. SHERPA registry (discovered peers)
|
|
2275
|
+
*
|
|
2276
|
+
* Returns hex public key string or null if unknown peer.
|
|
2277
|
+
*/
|
|
2278
|
+
_resolvePeerPublicKey(nodeId) {
|
|
2279
|
+
// 1. Direct WS peer (most trusted — active connection with verified HELLO)
|
|
2280
|
+
if (this.mesh?.peers) {
|
|
2281
|
+
const peer = this.mesh.peers.get(nodeId);
|
|
2282
|
+
if (peer?.identity?.publicKey) return peer.identity.publicKey;
|
|
2283
|
+
}
|
|
2284
|
+
// 2. Relay registration keys (signed during relay handshake)
|
|
2285
|
+
if (this.mesh?._relayPeerKeys) {
|
|
2286
|
+
const key = this.mesh._relayPeerKeys.get(nodeId);
|
|
2287
|
+
if (key) return key;
|
|
2288
|
+
}
|
|
2289
|
+
// 3. SHERPA discovery registry (populated during beacon exchange)
|
|
2290
|
+
if (this.mesh?.sherpa?.registry) {
|
|
2291
|
+
const regPeer = this.mesh.sherpa.registry.get(nodeId);
|
|
2292
|
+
if (regPeer?.publicKey) return regPeer.publicKey;
|
|
2293
|
+
}
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
async _startHttpServer() {
|
|
2298
|
+
const app = express();
|
|
2299
|
+
this.app = app; // Store for PeerQuanta endpoints
|
|
2300
|
+
|
|
2301
|
+
// Enable strict routing: /docs and /docs/ are different routes
|
|
2302
|
+
app.set('strict routing', true);
|
|
2303
|
+
|
|
2304
|
+
// SECURITY: Do NOT set 'trust proxy'. This is a P2P mesh node, not
|
|
2305
|
+
// behind a reverse proxy. Setting trust proxy lets remote attackers
|
|
2306
|
+
// forge req.ip via X-Forwarded-For headers. Rate limiting uses
|
|
2307
|
+
// validate: { xForwardedForHeader: false } to avoid this class of attack.
|
|
2308
|
+
// If deployed behind a known proxy, configure trustedProxies explicitly.
|
|
2309
|
+
|
|
2310
|
+
app.use(express.json({ limit: '1mb' })); // Limit payload size
|
|
2311
|
+
|
|
2312
|
+
// =========================================
|
|
2313
|
+
// SECURITY: Rate Limiting (DoS Protection)
|
|
2314
|
+
// =========================================
|
|
2315
|
+
|
|
2316
|
+
// General rate limit: 100 requests per minute per IP
|
|
2317
|
+
const generalLimiter = rateLimit({
|
|
2318
|
+
windowMs: 60 * 1000, // 1 minute
|
|
2319
|
+
max: 100,
|
|
2320
|
+
message: { error: 'Too many requests, please try again later' },
|
|
2321
|
+
standardHeaders: true,
|
|
2322
|
+
legacyHeaders: false,
|
|
2323
|
+
validate: { xForwardedForHeader: false },
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
// Strict rate limit for write operations: 20 per minute
|
|
2327
|
+
const writeLimiter = rateLimit({
|
|
2328
|
+
windowMs: 60 * 1000,
|
|
2329
|
+
max: 20,
|
|
2330
|
+
message: { error: 'Too many write requests, please slow down' },
|
|
632
2331
|
standardHeaders: true,
|
|
633
2332
|
legacyHeaders: false,
|
|
2333
|
+
validate: { xForwardedForHeader: false },
|
|
634
2334
|
});
|
|
635
|
-
|
|
2335
|
+
|
|
636
2336
|
// Apply general limiter to all routes
|
|
637
2337
|
app.use(generalLimiter);
|
|
638
|
-
|
|
639
|
-
// CORS
|
|
2338
|
+
|
|
2339
|
+
// CORS — restricted to localhost and configured origins
|
|
2340
|
+
const allowedOrigins = new Set([
|
|
2341
|
+
'http://localhost:3000',
|
|
2342
|
+
'http://localhost:3090',
|
|
2343
|
+
'http://127.0.0.1:3000',
|
|
2344
|
+
'http://127.0.0.1:3090',
|
|
2345
|
+
...(this.config.cors?.allowedOrigins || []),
|
|
2346
|
+
]);
|
|
2347
|
+
|
|
640
2348
|
app.use((req, res, next) => {
|
|
641
|
-
|
|
2349
|
+
const origin = req.headers.origin;
|
|
2350
|
+
if (origin && allowedOrigins.has(origin)) {
|
|
2351
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
2352
|
+
}
|
|
642
2353
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
643
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
|
2354
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
2355
|
+
res.header('Vary', 'Origin');
|
|
644
2356
|
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
|
645
2357
|
next();
|
|
646
2358
|
});
|
|
647
|
-
|
|
2359
|
+
|
|
2360
|
+
// =========================================
|
|
2361
|
+
// SECURITY: Peer authentication middleware
|
|
2362
|
+
// =========================================
|
|
2363
|
+
// Validates that write requests from peers include a valid ML-DSA-65 signature.
|
|
2364
|
+
// Uses real socket address — NOT req.ip — to prevent X-Forwarded-For spoofing.
|
|
2365
|
+
// Public key resolved from mesh peer registry, not from nodeId string.
|
|
2366
|
+
const requirePeerAuth = (req, res, next) => {
|
|
2367
|
+
const nodeId = req.headers['x-node-id'];
|
|
2368
|
+
const sig = req.headers['x-node-signature'];
|
|
2369
|
+
const ts = req.headers['x-node-timestamp'];
|
|
2370
|
+
|
|
2371
|
+
// Use the RAW socket address, immune to X-Forwarded-For spoofing.
|
|
2372
|
+
// req.ip respects 'trust proxy' and can be forged — never use it for auth.
|
|
2373
|
+
const rawIP = req.socket?.remoteAddress || req.connection?.remoteAddress;
|
|
2374
|
+
const isLocal = rawIP === '127.0.0.1' || rawIP === '::1' || rawIP === '::ffff:127.0.0.1';
|
|
2375
|
+
if (isLocal) {
|
|
2376
|
+
return next();
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// Require node identity headers for remote requests
|
|
2380
|
+
if (!nodeId || !sig || !ts) {
|
|
2381
|
+
return res.status(401).json({ error: 'Missing peer authentication headers' });
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// Reject stale timestamps — tightened from 30s to 10s
|
|
2385
|
+
// With TRIBHUJ ratchet and SSE push, nodes maintain tighter time sync.
|
|
2386
|
+
// 10s allows for reasonable network latency while preventing replay attacks.
|
|
2387
|
+
const MAX_AUTH_DRIFT_MS = 10000;
|
|
2388
|
+
const drift = Math.abs(Date.now() - parseInt(ts, 10));
|
|
2389
|
+
if (isNaN(drift) || drift > MAX_AUTH_DRIFT_MS) {
|
|
2390
|
+
return res.status(401).json({ error: 'Request timestamp too old or invalid' });
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Resolve the ACTUAL public key for this nodeId from mesh peer registry.
|
|
2394
|
+
// The annex._getPeerPublicKey pattern: peers → _relayPeerKeys → sherpa.registry
|
|
2395
|
+
const peerPublicKey = this._resolvePeerPublicKey(nodeId);
|
|
2396
|
+
if (!peerPublicKey) {
|
|
2397
|
+
return res.status(403).json({ error: 'Unknown peer — no public key on record' });
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// Verify ML-DSA-65 signature over (nodeId + timestamp + body hash)
|
|
2401
|
+
try {
|
|
2402
|
+
const bodyStr = JSON.stringify(req.body || {});
|
|
2403
|
+
const payload = `${nodeId}:${ts}:${bodyStr}`;
|
|
2404
|
+
const verified = this.identity.verify(payload, sig, peerPublicKey);
|
|
2405
|
+
if (!verified) {
|
|
2406
|
+
return res.status(403).json({ error: 'Invalid peer signature' });
|
|
2407
|
+
}
|
|
2408
|
+
req.authenticatedPeer = nodeId;
|
|
2409
|
+
req.authenticatedPeerKey = peerPublicKey;
|
|
2410
|
+
next();
|
|
2411
|
+
} catch (e) {
|
|
2412
|
+
return res.status(403).json({ error: 'Signature verification failed' });
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
|
|
648
2416
|
// =========================================
|
|
649
2417
|
// SECURITY: Input Validation Helpers
|
|
650
2418
|
// =========================================
|
|
651
|
-
|
|
2419
|
+
|
|
652
2420
|
const validateUrl = (url) => {
|
|
653
2421
|
if (!url || typeof url !== 'string') return false;
|
|
654
2422
|
try {
|
|
@@ -656,40 +2424,168 @@ export class YakmeshNode {
|
|
|
656
2424
|
return ['ws:', 'wss:', 'http:', 'https:'].includes(parsed.protocol);
|
|
657
2425
|
} catch { return false; }
|
|
658
2426
|
};
|
|
659
|
-
|
|
2427
|
+
|
|
660
2428
|
const validateString = (str, maxLen = 1000) => {
|
|
661
2429
|
return str && typeof str === 'string' && str.length <= maxLen;
|
|
662
2430
|
};
|
|
663
|
-
|
|
2431
|
+
|
|
664
2432
|
const validateObject = (obj) => {
|
|
665
2433
|
return obj && typeof obj === 'object' && !Array.isArray(obj);
|
|
666
2434
|
};
|
|
667
|
-
|
|
2435
|
+
|
|
668
2436
|
// =========================================
|
|
669
2437
|
// PUBLIC CONTENT API (No Auth for reads)
|
|
670
2438
|
// =========================================
|
|
671
|
-
|
|
2439
|
+
|
|
672
2440
|
// Mount content API at /content
|
|
673
2441
|
const contentAPI = createContentAPI(this.contentStore, {
|
|
674
2442
|
writeLimiter,
|
|
675
2443
|
readLimiter: generalLimiter,
|
|
676
2444
|
validateString,
|
|
2445
|
+
requirePeerAuth,
|
|
677
2446
|
});
|
|
678
2447
|
app.use('/content', contentAPI);
|
|
679
|
-
|
|
2448
|
+
|
|
2449
|
+
// =========================================
|
|
2450
|
+
// KOMM STACK API (KATHA/VANI/YURT/GUMBA)
|
|
2451
|
+
// Backend for yakapp (GUI) and terminal (CLI) clients
|
|
2452
|
+
// =========================================
|
|
2453
|
+
|
|
2454
|
+
if (this.kathaHub) {
|
|
2455
|
+
const kommRouter = createKommAPI({
|
|
2456
|
+
kathaHub: this.kathaHub,
|
|
2457
|
+
vaniHub: this.vaniHub,
|
|
2458
|
+
yurtHub: this.yurtHub,
|
|
2459
|
+
gumbaHub: this.gumbaHub,
|
|
2460
|
+
gossip: this.gossip,
|
|
2461
|
+
identity: this.identity,
|
|
2462
|
+
writeLimiter,
|
|
2463
|
+
requirePeerAuth,
|
|
2464
|
+
});
|
|
2465
|
+
app.use('/komm', kommRouter);
|
|
2466
|
+
log.info('📡 KOMM API mounted at /komm');
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// =========================================
|
|
2470
|
+
// DARSHAN Content Streaming API
|
|
2471
|
+
// View-don't-copy content delivery
|
|
2472
|
+
// =========================================
|
|
2473
|
+
|
|
2474
|
+
if (this.darshanGateway) {
|
|
2475
|
+
const darshanRouter = createDarshanAPI({
|
|
2476
|
+
darshanGateway: this.darshanGateway,
|
|
2477
|
+
gossip: this.gossip,
|
|
2478
|
+
identity: this.identity,
|
|
2479
|
+
writeLimiter,
|
|
2480
|
+
requirePeerAuth,
|
|
2481
|
+
});
|
|
2482
|
+
app.use('/darshan', darshanRouter);
|
|
2483
|
+
log.info('📡 DARSHAN API mounted at /darshan');
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// =========================================
|
|
2487
|
+
// NAKPAK Status Endpoint
|
|
2488
|
+
// =========================================
|
|
2489
|
+
|
|
2490
|
+
if (this.nakpakRouter) {
|
|
2491
|
+
app.get('/nakpak/status', (req, res) => {
|
|
2492
|
+
const circuits = this.nakpakRouter.circuits || new Map();
|
|
2493
|
+
const relays = this.nakpakRouter.relays || new Map();
|
|
2494
|
+
res.json({
|
|
2495
|
+
active: true,
|
|
2496
|
+
circuits: circuits.size,
|
|
2497
|
+
relays: relays.size,
|
|
2498
|
+
nodeId: peerTag(this.identity.identity.nodeId),
|
|
2499
|
+
});
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// =========================================
|
|
2504
|
+
// SAKSHI Witness + KARMA Status Endpoint
|
|
2505
|
+
// =========================================
|
|
2506
|
+
|
|
2507
|
+
if (this.sakshiWitness) {
|
|
2508
|
+
app.get('/sakshi/status', (req, res) => {
|
|
2509
|
+
const velocityStats = this.velocityMonitor?.getStats?.() || {};
|
|
2510
|
+
const karmaStats = this.karmaModel?.getStats?.() || {};
|
|
2511
|
+
|
|
2512
|
+
res.json({
|
|
2513
|
+
active: true,
|
|
2514
|
+
witness: this.sakshiWitness.toJSON(),
|
|
2515
|
+
velocity: {
|
|
2516
|
+
active: !!this.velocityMonitor,
|
|
2517
|
+
...velocityStats,
|
|
2518
|
+
activeAlerts: this.velocityMonitor?.getActiveAlerts?.() || [],
|
|
2519
|
+
},
|
|
2520
|
+
karma: {
|
|
2521
|
+
active: !!this.karmaModel,
|
|
2522
|
+
...karmaStats,
|
|
2523
|
+
},
|
|
2524
|
+
});
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// =========================================
|
|
2529
|
+
// ANNEX + JHILKE Status Endpoint
|
|
2530
|
+
// =========================================
|
|
2531
|
+
|
|
2532
|
+
app.get('/annex/status', (req, res) => {
|
|
2533
|
+
const annex = this.mesh?.annex;
|
|
2534
|
+
const jhilke = this.mesh?.jhilke;
|
|
2535
|
+
|
|
2536
|
+
if (!annex) {
|
|
2537
|
+
return res.json({ active: false, reason: 'ANNEX not initialized' });
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
const sessions = annex.listAnnexes().map(s => ({
|
|
2541
|
+
...s,
|
|
2542
|
+
nodeId: peerTag(s.nodeId),
|
|
2543
|
+
}));
|
|
2544
|
+
|
|
2545
|
+
const jhilkeStats = jhilke?.getStats() || null;
|
|
2546
|
+
if (jhilkeStats?.activeSessions !== undefined) {
|
|
2547
|
+
// Tag any peer IDs in jhilke session data
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
res.json({
|
|
2551
|
+
active: true,
|
|
2552
|
+
nodeId: peerTag(this.identity.identity.nodeId),
|
|
2553
|
+
stats: annex.getStats(),
|
|
2554
|
+
sessions,
|
|
2555
|
+
jhilke: jhilkeStats ? {
|
|
2556
|
+
...jhilkeStats,
|
|
2557
|
+
coordinatorActive: true,
|
|
2558
|
+
} : { coordinatorActive: false },
|
|
2559
|
+
});
|
|
2560
|
+
});
|
|
2561
|
+
|
|
2562
|
+
// =========================================
|
|
2563
|
+
// Ternary Harmonization Status Endpoint
|
|
2564
|
+
// =========================================
|
|
2565
|
+
|
|
2566
|
+
app.get('/ternary/status', (req, res) => {
|
|
2567
|
+
res.json({
|
|
2568
|
+
active: true,
|
|
2569
|
+
address144t: this.tritAddress?.toString() || null,
|
|
2570
|
+
routing: this.ternaryRouter?.getStatus() || null,
|
|
2571
|
+
batchChecksum: batchChecksumVerifier.telemetry,
|
|
2572
|
+
ternaryInference: !!this.ternaryInference,
|
|
2573
|
+
});
|
|
2574
|
+
});
|
|
2575
|
+
|
|
680
2576
|
// =========================================
|
|
681
2577
|
// Embedded Documentation (hardcoded, hash-verified)
|
|
682
2578
|
// Accessible via yak://docs or http://localhost:PORT/docs/
|
|
683
2579
|
// =========================================
|
|
684
|
-
|
|
2580
|
+
|
|
685
2581
|
app.get('/docs', (req, res) => {
|
|
686
2582
|
res.redirect('/docs/');
|
|
687
2583
|
});
|
|
688
|
-
|
|
2584
|
+
|
|
689
2585
|
app.get('/docs/', (req, res) => {
|
|
690
2586
|
serveDocsFile('index.html', res);
|
|
691
2587
|
});
|
|
692
|
-
|
|
2588
|
+
|
|
693
2589
|
app.get('/docs/_bundle', (req, res) => {
|
|
694
2590
|
try {
|
|
695
2591
|
const info = getBundleInfo();
|
|
@@ -698,12 +2594,12 @@ export class YakmeshNode {
|
|
|
698
2594
|
res.status(500).json({ error: 'Bundle info unavailable' });
|
|
699
2595
|
}
|
|
700
2596
|
});
|
|
701
|
-
|
|
2597
|
+
|
|
702
2598
|
app.get('/docs/:file(*)', (req, res) => {
|
|
703
2599
|
const file = req.params.file || 'index.html';
|
|
704
2600
|
serveDocsFile(file, res);
|
|
705
2601
|
});
|
|
706
|
-
|
|
2602
|
+
|
|
707
2603
|
// Serve dashboard
|
|
708
2604
|
app.get('/dashboard', (req, res) => {
|
|
709
2605
|
res.sendFile('dashboard/index.html', { root: import.meta.dirname + '/..' });
|
|
@@ -711,15 +2607,36 @@ export class YakmeshNode {
|
|
|
711
2607
|
|
|
712
2608
|
// Health check
|
|
713
2609
|
app.get('/health', (req, res) => {
|
|
2610
|
+
const wsPeers = this.mesh.getPeers();
|
|
2611
|
+
const relayPollCount = this._relayPollers?.size || 0;
|
|
2612
|
+
const relayClientCount = this._relayClients?.size || 0;
|
|
2613
|
+
const relayOutboxSize = this._relayOutbox
|
|
2614
|
+
? [...this._relayOutbox.values()].reduce((sum, q) => sum + q.length, 0)
|
|
2615
|
+
: 0;
|
|
2616
|
+
|
|
714
2617
|
res.json({
|
|
715
2618
|
status: 'ok',
|
|
716
2619
|
nodeId: this.identity.identity.nodeId,
|
|
717
|
-
|
|
2620
|
+
persistentId: this.identity.getPersistentId(), // 144T identity across code upgrades
|
|
2621
|
+
peers: wsPeers.length,
|
|
2622
|
+
relayPeers: relayPollCount + relayClientCount,
|
|
2623
|
+
relayPollers: relayPollCount,
|
|
2624
|
+
relayClients: relayClientCount,
|
|
2625
|
+
relayOutbox: relayOutboxSize,
|
|
2626
|
+
totalPeers: wsPeers.length + relayPollCount + relayClientCount,
|
|
718
2627
|
algorithm: 'ML-DSA-65',
|
|
719
2628
|
network: this.genesisNetwork ? {
|
|
720
2629
|
name: this.genesisNetwork.networkName,
|
|
721
2630
|
id: this.genesisNetwork.networkId,
|
|
722
2631
|
} : null,
|
|
2632
|
+
sherpa: this.sherpa ? {
|
|
2633
|
+
registry: this.sherpa.registry?.size() || 0,
|
|
2634
|
+
candidates: this.sherpa.getConnectionCandidates(10).length,
|
|
2635
|
+
} : null,
|
|
2636
|
+
accel: accel.getStatus(),
|
|
2637
|
+
steadywatch: steadywatch.getStatus(),
|
|
2638
|
+
timeSource: this.timeSource ? this.timeSource.getStatus() : null,
|
|
2639
|
+
security: this.mesh.getSecurityStats(),
|
|
723
2640
|
});
|
|
724
2641
|
});
|
|
725
2642
|
|
|
@@ -736,7 +2653,7 @@ export class YakmeshNode {
|
|
|
736
2653
|
// =========================================
|
|
737
2654
|
// SHERPA: Decentralized Peer Discovery
|
|
738
2655
|
// =========================================
|
|
739
|
-
|
|
2656
|
+
|
|
740
2657
|
// Beacon endpoint for SHERPA peer discovery
|
|
741
2658
|
// This allows other nodes to discover us and our known peers
|
|
742
2659
|
if (this.sherpa) {
|
|
@@ -759,6 +2676,220 @@ export class YakmeshNode {
|
|
|
759
2676
|
res.json(this.sherpa.getConnectionCandidates(10));
|
|
760
2677
|
});
|
|
761
2678
|
|
|
2679
|
+
// =========================================
|
|
2680
|
+
// ACCEL: Hardware Acceleration Status
|
|
2681
|
+
// =========================================
|
|
2682
|
+
app.get('/accel', (req, res) => {
|
|
2683
|
+
res.json(accel.getStatus());
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
app.get('/accel/telemetry', (req, res) => {
|
|
2687
|
+
res.json(accel.getTelemetry());
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2690
|
+
// =========================================
|
|
2691
|
+
// COMPUTE SCHEDULER: Heterogeneous GPU/NPU/CPU
|
|
2692
|
+
// =========================================
|
|
2693
|
+
app.get('/scheduler', (req, res) => {
|
|
2694
|
+
res.json(accel.scheduler.getStatus());
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
app.get('/scheduler/training-data', (req, res) => {
|
|
2698
|
+
const n = Math.min(parseInt(req.query.n) || 100, 5000);
|
|
2699
|
+
res.json(accel.scheduler.getTrainingData(n));
|
|
2700
|
+
});
|
|
2701
|
+
|
|
2702
|
+
// =========================================
|
|
2703
|
+
// STEADYWATCH: Quantum Entropy Status
|
|
2704
|
+
// =========================================
|
|
2705
|
+
app.get('/steadywatch', (req, res) => {
|
|
2706
|
+
res.json(steadywatch.getStatus());
|
|
2707
|
+
});
|
|
2708
|
+
|
|
2709
|
+
// =========================================
|
|
2710
|
+
// SHERPA HTTP Relay: Mesh messaging over HTTP
|
|
2711
|
+
// =========================================
|
|
2712
|
+
// Allows nodes behind firewalls to exchange mesh messages via HTTP POST
|
|
2713
|
+
// instead of WebSocket. The PHP bridge on yakmesh.dev proxies to this.
|
|
2714
|
+
// Message flow: Remote Node → HTTPS POST yakmesh.dev/mesh/relay → PHP → localhost:<httpPort>/mesh/relay
|
|
2715
|
+
|
|
2716
|
+
// Accept inbound mesh messages via HTTP (signed, verified)
|
|
2717
|
+
app.post('/mesh/relay', writeLimiter, (req, res) => {
|
|
2718
|
+
// Handle relay registration (action: 'register') through the same endpoint
|
|
2719
|
+
// so it works through the PHP bridge which only proxies POST /mesh/relay
|
|
2720
|
+
if (req.body.action === 'register') {
|
|
2721
|
+
const { nodeId, networkName, publicKey, capabilities, signature, timestamp } = req.body;
|
|
2722
|
+
if (!nodeId || !networkName) {
|
|
2723
|
+
return res.status(400).json({ error: 'nodeId and networkName required for register' });
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// Timestamp is REQUIRED for replay protection — reject if missing or stale
|
|
2727
|
+
if (!timestamp || typeof timestamp !== 'number') {
|
|
2728
|
+
return res.status(400).json({ error: 'timestamp required for registration (replay protection)' });
|
|
2729
|
+
}
|
|
2730
|
+
if (Math.abs(Date.now() - timestamp) > 300000) {
|
|
2731
|
+
return res.status(403).json({ error: 'Registration timestamp too old (replay protection)' });
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// Verify ML-DSA-65 registration signature — no unsigned registrations
|
|
2735
|
+
if (!signature || !publicKey) {
|
|
2736
|
+
return res.status(403).json({ error: 'Signed registration required (signature + publicKey)' });
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// SECURITY: For FIRST registration, we must trust the supplied publicKey
|
|
2740
|
+
// since the peer is unknown. On subsequent registrations, verify against
|
|
2741
|
+
// the STORED key to prevent identity takeover.
|
|
2742
|
+
const knownKey = this._resolvePeerPublicKey(nodeId);
|
|
2743
|
+
const verifyKey = knownKey || publicKey; // Trust first contact, verify thereafter
|
|
2744
|
+
|
|
2745
|
+
try {
|
|
2746
|
+
const sigData = JSON.stringify({ action: 'register', nodeId, networkName, timestamp });
|
|
2747
|
+
const valid = this.identity.verify(sigData, signature, verifyKey);
|
|
2748
|
+
if (!valid) {
|
|
2749
|
+
return res.status(403).json({ error: 'Invalid registration signature' });
|
|
2750
|
+
}
|
|
2751
|
+
} catch {
|
|
2752
|
+
return res.status(403).json({ error: 'Registration signature verification failed' });
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// If we had a stored key and the supplied key differs, reject (identity conflict)
|
|
2756
|
+
if (knownKey && publicKey !== knownKey) {
|
|
2757
|
+
return res.status(403).json({ error: 'Public key mismatch — identity conflict' });
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
if (this.sherpa) {
|
|
2761
|
+
this.sherpa.registry.upsert({
|
|
2762
|
+
nodeId,
|
|
2763
|
+
endpoint: null,
|
|
2764
|
+
wsEndpoint: null,
|
|
2765
|
+
relayEndpoint: null,
|
|
2766
|
+
networkName,
|
|
2767
|
+
publicKey,
|
|
2768
|
+
capabilities: { ...capabilities, httpRelay: true },
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Store publicKey for relay peers (used by ANNEX signature verification)
|
|
2773
|
+
// Attach to mesh so ANNEX._getPeerPublicKey() can find relay peer keys
|
|
2774
|
+
if (!this.mesh._relayPeerKeys) this.mesh._relayPeerKeys = new Map();
|
|
2775
|
+
this.mesh._relayPeerKeys.set(nodeId, publicKey);
|
|
2776
|
+
|
|
2777
|
+
// Track relay clients as Map {nodeId → lastSeen} for expiry
|
|
2778
|
+
if (!this._relayClients) this._relayClients = new Map();
|
|
2779
|
+
this._relayClients.set(nodeId, Date.now());
|
|
2780
|
+
|
|
2781
|
+
log.info(`HTTP relay peer registered (verified): ${peerTag(nodeId)}`);
|
|
2782
|
+
log.info(` ⚠ Relay peers use HTTP polling (30s cadence) — reduced throughput & latency vs WebSocket`);
|
|
2783
|
+
log.info(` ⚠ Relay is a firewall-traversal fallback, not the intended full-duplex mesh connection`);
|
|
2784
|
+
return res.json({
|
|
2785
|
+
success: true,
|
|
2786
|
+
nodeId: this.identity.identity.nodeId,
|
|
2787
|
+
publicKey: this.identity.identity.publicKey,
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
const { messages, senderNodeId, signature, publicKey } = req.body;
|
|
2792
|
+
|
|
2793
|
+
if (!Array.isArray(messages)) {
|
|
2794
|
+
return res.status(400).json({ error: 'messages array required' });
|
|
2795
|
+
}
|
|
2796
|
+
if (messages.length > 50) {
|
|
2797
|
+
return res.status(400).json({ error: 'Max 50 messages per relay batch' });
|
|
2798
|
+
}
|
|
2799
|
+
if (!senderNodeId || typeof senderNodeId !== 'string') {
|
|
2800
|
+
return res.status(400).json({ error: 'senderNodeId required' });
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// Require ML-DSA-65 batch signature — verified against KNOWN peer key
|
|
2804
|
+
if (!signature) {
|
|
2805
|
+
return res.status(403).json({ error: 'Signed relay batch required' });
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// SECURITY: Look up the sender's STORED public key from our registry.
|
|
2809
|
+
// Never verify against an attacker-supplied publicKey in the body.
|
|
2810
|
+
const knownBatchKey = this._resolvePeerPublicKey(senderNodeId);
|
|
2811
|
+
if (!knownBatchKey) {
|
|
2812
|
+
return res.status(403).json({ error: 'Unknown relay peer — register first' });
|
|
2813
|
+
}
|
|
2814
|
+
try {
|
|
2815
|
+
const sigData = JSON.stringify({ messages, senderNodeId });
|
|
2816
|
+
const valid = this.identity.verify(sigData, signature, knownBatchKey);
|
|
2817
|
+
if (!valid) {
|
|
2818
|
+
return res.status(403).json({ error: 'Invalid batch signature' });
|
|
2819
|
+
}
|
|
2820
|
+
} catch {
|
|
2821
|
+
return res.status(403).json({ error: 'Batch signature verification failed' });
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// Process each message through the mesh layer
|
|
2825
|
+
let accepted = 0;
|
|
2826
|
+
for (const msg of messages) {
|
|
2827
|
+
if (msg && typeof msg === 'object' && msg.type) {
|
|
2828
|
+
try {
|
|
2829
|
+
// Dispatch by msg.type (e.g., 'gossip') — not 'message'
|
|
2830
|
+
this.mesh.emit(msg.type, msg, null, senderNodeId);
|
|
2831
|
+
// Route ANNEX messages arriving via relay
|
|
2832
|
+
if (msg.annex && this.mesh.annex) {
|
|
2833
|
+
this.mesh.annex._handleAnnexMessage(msg.annex, senderNodeId).catch(() => { });
|
|
2834
|
+
}
|
|
2835
|
+
accepted++;
|
|
2836
|
+
} catch {
|
|
2837
|
+
// Skip malformed messages
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// Refresh relay client last-seen on poll
|
|
2843
|
+
if (this._relayClients && this._relayClients.has(senderNodeId)) {
|
|
2844
|
+
this._relayClients.set(senderNodeId, Date.now());
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// Return our own pending outbound messages for this sender (bi-directional relay)
|
|
2848
|
+
const outbound = this._drainRelayOutbox(senderNodeId);
|
|
2849
|
+
|
|
2850
|
+
res.json({
|
|
2851
|
+
accepted,
|
|
2852
|
+
outbound,
|
|
2853
|
+
nodeId: this.identity.identity.nodeId,
|
|
2854
|
+
publicKey: this.identity.identity.publicKey,
|
|
2855
|
+
timestamp: Date.now(),
|
|
2856
|
+
});
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
// Retrieve pending relay messages for a specific node (pull-based)
|
|
2860
|
+
app.get('/mesh/relay/:nodeId', (req, res) => {
|
|
2861
|
+
const outbound = this._drainRelayOutbox(req.params.nodeId);
|
|
2862
|
+
res.json({
|
|
2863
|
+
messages: outbound,
|
|
2864
|
+
nodeId: this.identity.identity.nodeId,
|
|
2865
|
+
timestamp: Date.now(),
|
|
2866
|
+
});
|
|
2867
|
+
});
|
|
2868
|
+
|
|
2869
|
+
// Register as an HTTP-relay peer (for nodes that can't do WS)
|
|
2870
|
+
// SECURITY: Requires peer auth — prevents phantom peer registration
|
|
2871
|
+
app.post('/mesh/relay/register', writeLimiter, requirePeerAuth, (req, res) => {
|
|
2872
|
+
const { nodeId, relayEndpoint, publicKey, capabilities } = req.body;
|
|
2873
|
+
|
|
2874
|
+
if (!nodeId || !relayEndpoint) {
|
|
2875
|
+
return res.status(400).json({ error: 'nodeId and relayEndpoint required' });
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// Register in SHERPA registry as an HTTP-relay peer
|
|
2879
|
+
if (this.sherpa) {
|
|
2880
|
+
this.sherpa.registry.upsert({
|
|
2881
|
+
nodeId,
|
|
2882
|
+
endpoint: relayEndpoint,
|
|
2883
|
+
wsEndpoint: null, // No WS — HTTP relay only
|
|
2884
|
+
networkName: this.genesisNetwork?.networkName,
|
|
2885
|
+
capabilities: { ...capabilities, httpRelay: true },
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
log.info(`HTTP relay peer registered: ${peerTag(nodeId)} via ${relayEndpoint}`);
|
|
2890
|
+
res.json({ success: true, nodeId: this.identity.identity.nodeId });
|
|
2891
|
+
});
|
|
2892
|
+
|
|
762
2893
|
// Replication stats
|
|
763
2894
|
app.get('/replication', (req, res) => {
|
|
764
2895
|
res.json(this.replication.getStats());
|
|
@@ -766,19 +2897,48 @@ export class YakmeshNode {
|
|
|
766
2897
|
|
|
767
2898
|
// Connect to a peer dynamically
|
|
768
2899
|
// SECURITY: Rate limited + URL validation
|
|
769
|
-
app.post('/connect', writeLimiter, async (req, res) => {
|
|
2900
|
+
app.post('/connect', writeLimiter, requirePeerAuth, async (req, res) => {
|
|
770
2901
|
const { address } = req.body;
|
|
771
|
-
|
|
2902
|
+
|
|
772
2903
|
if (!validateUrl(address)) {
|
|
773
2904
|
return res.status(400).json({ error: 'Valid WebSocket URL required (ws:// or wss://)' });
|
|
774
2905
|
}
|
|
775
|
-
|
|
2906
|
+
|
|
776
2907
|
try {
|
|
777
2908
|
await this.mesh.connectToPeer(address);
|
|
778
|
-
res.json({
|
|
779
|
-
success: true,
|
|
2909
|
+
res.json({
|
|
2910
|
+
success: true,
|
|
780
2911
|
message: `Connecting to ${address}`,
|
|
781
|
-
peers: this.mesh.getPeers().length
|
|
2912
|
+
peers: this.mesh.getPeers().length
|
|
2913
|
+
});
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
res.status(500).json({ error: error.message });
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
|
|
2919
|
+
// Connect to a peer via HTTP relay (firewall traversal fallback)
|
|
2920
|
+
app.post('/connect/relay', writeLimiter, requirePeerAuth, async (req, res) => {
|
|
2921
|
+
const { relayEndpoint, nodeId } = req.body;
|
|
2922
|
+
|
|
2923
|
+
if (!relayEndpoint || typeof relayEndpoint !== 'string') {
|
|
2924
|
+
return res.status(400).json({ error: 'relayEndpoint URL required (e.g. https://yakmesh.dev/mesh/relay.php)' });
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// nodeId is optional — we'll learn it from the registration response
|
|
2928
|
+
const candidate = {
|
|
2929
|
+
nodeId: nodeId || `relay-${Date.now()}`,
|
|
2930
|
+
relayEndpoint,
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2933
|
+
try {
|
|
2934
|
+
await this._registerWithRelay(candidate);
|
|
2935
|
+
const relayPollCount = this._relayPollers?.size || 0;
|
|
2936
|
+
const relayClientCount = this._relayClients?.size || 0;
|
|
2937
|
+
res.json({
|
|
2938
|
+
success: true,
|
|
2939
|
+
message: `Relay connection established to ${relayEndpoint}`,
|
|
2940
|
+
relayPeers: relayPollCount + relayClientCount,
|
|
2941
|
+
totalPeers: this.mesh.getPeers().length + relayPollCount + relayClientCount,
|
|
782
2942
|
});
|
|
783
2943
|
} catch (error) {
|
|
784
2944
|
res.status(500).json({ error: error.message });
|
|
@@ -787,9 +2947,9 @@ export class YakmeshNode {
|
|
|
787
2947
|
|
|
788
2948
|
// Simple API endpoint for testing replication
|
|
789
2949
|
// SECURITY: Rate limited + input validation
|
|
790
|
-
app.post('/data', writeLimiter, (req, res) => {
|
|
2950
|
+
app.post('/data', writeLimiter, requirePeerAuth, (req, res) => {
|
|
791
2951
|
const { table, data } = req.body;
|
|
792
|
-
|
|
2952
|
+
|
|
793
2953
|
// Validate inputs
|
|
794
2954
|
if (!validateString(table, 64)) {
|
|
795
2955
|
return res.status(400).json({ error: 'Valid table name required (max 64 chars)' });
|
|
@@ -797,11 +2957,11 @@ export class YakmeshNode {
|
|
|
797
2957
|
if (!validateObject(data)) {
|
|
798
2958
|
return res.status(400).json({ error: 'Data must be an object' });
|
|
799
2959
|
}
|
|
800
|
-
|
|
2960
|
+
|
|
801
2961
|
// Record the change for replication
|
|
802
2962
|
const rowId = data.id || Date.now();
|
|
803
2963
|
this.replication.recordChange(table, rowId, 'INSERT', data);
|
|
804
|
-
|
|
2964
|
+
|
|
805
2965
|
// Spread via gossip protocol
|
|
806
2966
|
this.gossip.spreadRumor('data_update', {
|
|
807
2967
|
table,
|
|
@@ -809,7 +2969,7 @@ export class YakmeshNode {
|
|
|
809
2969
|
operation: 'INSERT',
|
|
810
2970
|
data,
|
|
811
2971
|
});
|
|
812
|
-
|
|
2972
|
+
|
|
813
2973
|
res.json({ success: true, rowId });
|
|
814
2974
|
});
|
|
815
2975
|
|
|
@@ -825,20 +2985,70 @@ export class YakmeshNode {
|
|
|
825
2985
|
|
|
826
2986
|
// Spread a rumor
|
|
827
2987
|
// SECURITY: Rate limited + input validation
|
|
828
|
-
app.post('/rumor', writeLimiter, (req, res) => {
|
|
2988
|
+
app.post('/rumor', writeLimiter, requirePeerAuth, (req, res) => {
|
|
829
2989
|
const { topic, data } = req.body;
|
|
830
|
-
|
|
2990
|
+
|
|
831
2991
|
if (!validateString(topic, 64)) {
|
|
832
2992
|
return res.status(400).json({ error: 'Valid topic required (max 64 chars)' });
|
|
833
2993
|
}
|
|
834
2994
|
if (!validateObject(data)) {
|
|
835
2995
|
return res.status(400).json({ error: 'Data must be an object' });
|
|
836
2996
|
}
|
|
837
|
-
|
|
2997
|
+
|
|
838
2998
|
const messageId = this.gossip.spreadRumor(topic, data);
|
|
839
2999
|
res.json({ success: true, messageId });
|
|
840
3000
|
});
|
|
841
3001
|
|
|
3002
|
+
// Retrieve recent rumors (for MeshBridge HTTP polling)
|
|
3003
|
+
// Supports ?since=<timestamp>&topic=<topic> filters
|
|
3004
|
+
app.get('/rumors', (req, res) => {
|
|
3005
|
+
const since = parseInt(req.query.since) || 0;
|
|
3006
|
+
const topic = req.query.topic || null;
|
|
3007
|
+
const rumors = this.gossip.getRecentRumors(since, topic);
|
|
3008
|
+
res.json({ rumors, serverTime: Date.now() });
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
// SSE endpoint: real-time push of rumors (replaces polling for MeshBridge)
|
|
3012
|
+
// GET /rumors/subscribe?topic=<optional> — Server-Sent Events stream
|
|
3013
|
+
// SECURITY: Restricted to localhost (mesh topology leaks if exposed)
|
|
3014
|
+
app.get('/rumors/subscribe', (req, res) => {
|
|
3015
|
+
// Only allow connections from localhost — SSE is for local MeshBridge, not remote clients
|
|
3016
|
+
const remoteAddr = req.socket?.remoteAddress || '';
|
|
3017
|
+
const isLocal = remoteAddr === '127.0.0.1' || remoteAddr === '::1' || remoteAddr === '::ffff:127.0.0.1';
|
|
3018
|
+
if (!isLocal) {
|
|
3019
|
+
return res.status(403).json({ error: 'SSE subscribe restricted to localhost' });
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
const topicFilter = req.query.topic || null;
|
|
3023
|
+
|
|
3024
|
+
res.writeHead(200, {
|
|
3025
|
+
'Content-Type': 'text/event-stream',
|
|
3026
|
+
'Cache-Control': 'no-cache',
|
|
3027
|
+
Connection: 'keep-alive',
|
|
3028
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
3029
|
+
});
|
|
3030
|
+
res.write('retry: 5000\n\n'); // Auto-reconnect after 5s
|
|
3031
|
+
|
|
3032
|
+
// Listener that forwards matching rumors (origin stripped to prevent topology leak)
|
|
3033
|
+
const onRumor = (topic, data, _origin) => {
|
|
3034
|
+
if (topicFilter && topic !== topicFilter) return;
|
|
3035
|
+
const event = JSON.stringify({ topic, data, timestamp: Date.now() });
|
|
3036
|
+
res.write(`data: ${event}\n\n`);
|
|
3037
|
+
};
|
|
3038
|
+
|
|
3039
|
+
// Heartbeat to keep connection alive through proxies
|
|
3040
|
+
const heartbeat = setInterval(() => {
|
|
3041
|
+
res.write(': heartbeat\n\n');
|
|
3042
|
+
}, 15000);
|
|
3043
|
+
|
|
3044
|
+
this.mesh.on('rumor', onRumor);
|
|
3045
|
+
|
|
3046
|
+
req.on('close', () => {
|
|
3047
|
+
this.mesh.off('rumor', onRumor);
|
|
3048
|
+
clearInterval(heartbeat);
|
|
3049
|
+
});
|
|
3050
|
+
});
|
|
3051
|
+
|
|
842
3052
|
// =========================================
|
|
843
3053
|
// Oracle Endpoints - Self-Verifying Trust
|
|
844
3054
|
// =========================================
|
|
@@ -851,10 +3061,10 @@ export class YakmeshNode {
|
|
|
851
3061
|
}
|
|
852
3062
|
|
|
853
3063
|
const integrity = this.oracle.verifySelfIntegrity();
|
|
854
|
-
|
|
3064
|
+
|
|
855
3065
|
// Use network identity fingerprint instead of raw hash
|
|
856
3066
|
const networkFingerprint = this.genesisNetwork?.fingerprint || 'not-initialized';
|
|
857
|
-
|
|
3067
|
+
|
|
858
3068
|
res.json({
|
|
859
3069
|
status: integrity.valid ? 'healthy' : 'compromised',
|
|
860
3070
|
integrity: {
|
|
@@ -899,8 +3109,8 @@ export class YakmeshNode {
|
|
|
899
3109
|
});
|
|
900
3110
|
|
|
901
3111
|
// Verify a peer's handshake
|
|
902
|
-
// SECURITY: Input validation
|
|
903
|
-
app.post('/network/verify', (req, res) => {
|
|
3112
|
+
// SECURITY: Input validation + peer auth
|
|
3113
|
+
app.post('/network/verify', writeLimiter, requirePeerAuth, (req, res) => {
|
|
904
3114
|
if (!this.genesisNetwork) {
|
|
905
3115
|
return res.status(503).json({ error: 'Genesis network not initialized' });
|
|
906
3116
|
}
|
|
@@ -928,7 +3138,7 @@ export class YakmeshNode {
|
|
|
928
3138
|
|
|
929
3139
|
// Register a peer via handshake
|
|
930
3140
|
// SECURITY: Rate limited + input validation
|
|
931
|
-
app.post('/network/register-peer', writeLimiter, (req, res) => {
|
|
3141
|
+
app.post('/network/register-peer', writeLimiter, requirePeerAuth, (req, res) => {
|
|
932
3142
|
if (!this.genesisNetwork) {
|
|
933
3143
|
return res.status(503).json({ error: 'Genesis network not initialized' });
|
|
934
3144
|
}
|
|
@@ -950,10 +3160,10 @@ export class YakmeshNode {
|
|
|
950
3160
|
});
|
|
951
3161
|
|
|
952
3162
|
// Initiate code-proof challenge for a peer
|
|
953
|
-
// SECURITY: Rate limited + input validation
|
|
954
|
-
app.post('/oracle/challenge', writeLimiter, (req, res) => {
|
|
3163
|
+
// SECURITY: Rate limited + peer auth + input validation
|
|
3164
|
+
app.post('/oracle/challenge', writeLimiter, requirePeerAuth, (req, res) => {
|
|
955
3165
|
const { peerId } = req.body;
|
|
956
|
-
|
|
3166
|
+
|
|
957
3167
|
if (!validateString(peerId, 128)) {
|
|
958
3168
|
return res.status(400).json({ error: 'Valid peerId required (max 128 chars)' });
|
|
959
3169
|
}
|
|
@@ -963,14 +3173,14 @@ export class YakmeshNode {
|
|
|
963
3173
|
}
|
|
964
3174
|
|
|
965
3175
|
const challenge = this.codeProof.generateChallenge(peerId);
|
|
966
|
-
|
|
3176
|
+
|
|
967
3177
|
// Spread challenge via gossip
|
|
968
3178
|
this.gossip.spreadRumor('code_proof_challenge', challenge);
|
|
969
|
-
|
|
3179
|
+
|
|
970
3180
|
res.json({
|
|
971
3181
|
success: true,
|
|
972
3182
|
challengeId: challenge.challengeId,
|
|
973
|
-
message: `Challenge sent to peer ${peerId
|
|
3183
|
+
message: `Challenge sent to peer ${peerTag(peerId)}`
|
|
974
3184
|
});
|
|
975
3185
|
});
|
|
976
3186
|
|
|
@@ -987,10 +3197,10 @@ export class YakmeshNode {
|
|
|
987
3197
|
});
|
|
988
3198
|
|
|
989
3199
|
// Submit oracle-validated content
|
|
990
|
-
// SECURITY: Rate limited + input validation + hash obfuscation
|
|
991
|
-
app.post('/oracle/submit', writeLimiter, async (req, res) => {
|
|
3200
|
+
// SECURITY: Rate limited + peer auth + input validation + hash obfuscation
|
|
3201
|
+
app.post('/oracle/submit', writeLimiter, requirePeerAuth, async (req, res) => {
|
|
992
3202
|
const { type, content } = req.body;
|
|
993
|
-
|
|
3203
|
+
|
|
994
3204
|
if (!validateString(type, 64)) {
|
|
995
3205
|
return res.status(400).json({ error: 'Valid type required (max 64 chars)' });
|
|
996
3206
|
}
|
|
@@ -1005,7 +3215,7 @@ export class YakmeshNode {
|
|
|
1005
3215
|
try {
|
|
1006
3216
|
// Validate through oracle
|
|
1007
3217
|
const validation = await this.oracle.validate(type, content);
|
|
1008
|
-
|
|
3218
|
+
|
|
1009
3219
|
if (!validation.valid) {
|
|
1010
3220
|
return res.status(400).json({
|
|
1011
3221
|
success: false,
|
|
@@ -1054,7 +3264,8 @@ export class YakmeshNode {
|
|
|
1054
3264
|
});
|
|
1055
3265
|
|
|
1056
3266
|
// Resolve conflicts manually (admin endpoint)
|
|
1057
|
-
|
|
3267
|
+
// SECURITY: Peer auth required — admin action
|
|
3268
|
+
app.post('/oracle/resolve', writeLimiter, requirePeerAuth, (req, res) => {
|
|
1058
3269
|
if (!this.consensus) {
|
|
1059
3270
|
return res.status(503).json({ error: 'Consensus engine not initialized' });
|
|
1060
3271
|
}
|
|
@@ -1070,28 +3281,29 @@ export class YakmeshNode {
|
|
|
1070
3281
|
// =========================================
|
|
1071
3282
|
// Metrics Endpoint - Dashboard Data
|
|
1072
3283
|
// =========================================
|
|
1073
|
-
|
|
3284
|
+
|
|
1074
3285
|
app.get('/metrics', (req, res) => {
|
|
1075
3286
|
const startTime = this._startTime || Date.now();
|
|
1076
3287
|
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
|
1077
|
-
|
|
3288
|
+
|
|
1078
3289
|
// Crypto configuration (imported at top of file)
|
|
1079
3290
|
let cryptoInfo = null;
|
|
1080
3291
|
try {
|
|
1081
3292
|
// Dynamic import not needed - use the imported module
|
|
1082
|
-
cryptoInfo = this._cryptoSummary || {
|
|
3293
|
+
cryptoInfo = this._cryptoSummary || {
|
|
1083
3294
|
levelName: 'NIST Level 3',
|
|
1084
3295
|
signatureAlgorithm: 'ML-DSA-65',
|
|
1085
3296
|
backupSignatureAlgorithm: 'SLH-DSA-SHA2-192f',
|
|
1086
3297
|
kemAlgorithm: 'ML-KEM-768',
|
|
1087
3298
|
classicalSecurity: '192-bit',
|
|
1088
3299
|
quantumSecurity: '128-bit',
|
|
3300
|
+
routingSecurity: '256-bit (144T)',
|
|
1089
3301
|
nistStandards: ['FIPS 203 (ML-KEM)', 'FIPS 204 (ML-DSA)', 'FIPS 205 (SLH-DSA)'],
|
|
1090
3302
|
};
|
|
1091
3303
|
} catch (e) {
|
|
1092
3304
|
cryptoInfo = { error: 'Could not load crypto config' };
|
|
1093
3305
|
}
|
|
1094
|
-
|
|
3306
|
+
|
|
1095
3307
|
// Time source info
|
|
1096
3308
|
let timeInfo = null;
|
|
1097
3309
|
if (this.timeSource) {
|
|
@@ -1104,7 +3316,7 @@ export class YakmeshNode {
|
|
|
1104
3316
|
hasHighPrecisionTime: this.timeSource.hasHighPrecisionTime(),
|
|
1105
3317
|
};
|
|
1106
3318
|
}
|
|
1107
|
-
|
|
3319
|
+
|
|
1108
3320
|
// Oracle status
|
|
1109
3321
|
let oracleInfo = null;
|
|
1110
3322
|
if (this.oracle) {
|
|
@@ -1118,11 +3330,11 @@ export class YakmeshNode {
|
|
|
1118
3330
|
verifiedPeers: this.codeProof?.getVerifiedPeers()?.length || 0,
|
|
1119
3331
|
};
|
|
1120
3332
|
}
|
|
1121
|
-
|
|
3333
|
+
|
|
1122
3334
|
// Mesh stats
|
|
1123
3335
|
const peerCount = this.mesh?.getPeers()?.length || 0;
|
|
1124
3336
|
const gossipStats = this.gossip?.getStats() || null;
|
|
1125
|
-
|
|
3337
|
+
|
|
1126
3338
|
// NAMCHE security gate status (v2.0)
|
|
1127
3339
|
let namcheInfo = null;
|
|
1128
3340
|
if (this.namcheGateway) {
|
|
@@ -1135,7 +3347,7 @@ export class YakmeshNode {
|
|
|
1135
3347
|
gateCount: 7,
|
|
1136
3348
|
};
|
|
1137
3349
|
}
|
|
1138
|
-
|
|
3350
|
+
|
|
1139
3351
|
// DOKO identity status (v2.0)
|
|
1140
3352
|
let dokoInfo = null;
|
|
1141
3353
|
if (this.dokoRegistry) {
|
|
@@ -1146,13 +3358,16 @@ export class YakmeshNode {
|
|
|
1146
3358
|
types: Object.keys(DOKOTypes),
|
|
1147
3359
|
};
|
|
1148
3360
|
}
|
|
1149
|
-
|
|
3361
|
+
|
|
1150
3362
|
// Website adapter status (v2.0)
|
|
1151
3363
|
let websiteInfo = null;
|
|
1152
3364
|
if (this.websiteAdapter) {
|
|
3365
|
+
// Count unique websites by domain (not replicated manifests)
|
|
3366
|
+
const uniqueSites = this.websiteAdapter.domains.size || 1; // At least our own site
|
|
1153
3367
|
websiteInfo = {
|
|
1154
3368
|
status: 'active',
|
|
1155
|
-
websites: this.websiteAdapter.manifests.size,
|
|
3369
|
+
websites: this.websiteAdapter.manifests.size, // Total manifests (replicas from all nodes)
|
|
3370
|
+
uniqueSites, // Actual unique sites by domain
|
|
1156
3371
|
domains: this.websiteAdapter.domains.size,
|
|
1157
3372
|
filesServed: this.websiteAdapter.stats.filesServed,
|
|
1158
3373
|
bytesServed: this.websiteAdapter.stats.bytesServed,
|
|
@@ -1160,12 +3375,12 @@ export class YakmeshNode {
|
|
|
1160
3375
|
} else {
|
|
1161
3376
|
websiteInfo = { status: 'uninitialized' };
|
|
1162
3377
|
}
|
|
1163
|
-
|
|
3378
|
+
|
|
1164
3379
|
res.json({
|
|
1165
3380
|
node: {
|
|
1166
3381
|
id: this.identity?.identity?.nodeId || null,
|
|
1167
3382
|
name: this.config?.node?.name || 'unknown',
|
|
1168
|
-
version: '2.0
|
|
3383
|
+
version: '2.9.0',
|
|
1169
3384
|
uptime,
|
|
1170
3385
|
uptimeFormatted: formatUptime(uptime),
|
|
1171
3386
|
},
|
|
@@ -1205,7 +3420,7 @@ export class YakmeshNode {
|
|
|
1205
3420
|
}
|
|
1206
3421
|
|
|
1207
3422
|
const results = this.timeSource.detect();
|
|
1208
|
-
|
|
3423
|
+
|
|
1209
3424
|
// Update phase config if trust level changed
|
|
1210
3425
|
if (results.trustLevel) {
|
|
1211
3426
|
setTimeSourceConfig(results.trustLevel);
|
|
@@ -1220,42 +3435,196 @@ export class YakmeshNode {
|
|
|
1220
3435
|
return res.status(503).json({ error: 'Time source detector not initialized' });
|
|
1221
3436
|
}
|
|
1222
3437
|
|
|
1223
|
-
const phaseConfig = createPhaseConfig(this.timeSource);
|
|
1224
|
-
const activeConfig = getActiveConfig();
|
|
3438
|
+
const phaseConfig = createPhaseConfig(this.timeSource);
|
|
3439
|
+
const activeConfig = getActiveConfig();
|
|
3440
|
+
|
|
3441
|
+
res.json({
|
|
3442
|
+
...phaseConfig,
|
|
3443
|
+
activePhaseConfig: activeConfig,
|
|
3444
|
+
});
|
|
3445
|
+
});
|
|
3446
|
+
|
|
3447
|
+
// Get time source capabilities
|
|
3448
|
+
app.get('/time/capabilities', (req, res) => {
|
|
3449
|
+
if (!this.timeSource) {
|
|
3450
|
+
return res.status(503).json({ error: 'Time source detector not initialized' });
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
const status = this.timeSource.getStatus();
|
|
3454
|
+
const phaseConfig = createPhaseConfig(this.timeSource);
|
|
3455
|
+
|
|
3456
|
+
res.json({
|
|
3457
|
+
trustLevel: status.trustLevel,
|
|
3458
|
+
stratum: status.stratum,
|
|
3459
|
+
canBeTimeOracle: phaseConfig.capabilities.canBeTimeOracle,
|
|
3460
|
+
canValidateTightPhase: phaseConfig.capabilities.canValidateTightPhase,
|
|
3461
|
+
canParticipateInConsensus: phaseConfig.capabilities.canParticipateInConsensus,
|
|
3462
|
+
hasAtomicTime: this.timeSource.hasAtomicTime(),
|
|
3463
|
+
hasHighPrecisionTime: this.timeSource.hasHighPrecisionTime(),
|
|
3464
|
+
phaseTolerance: status.phaseTolerance,
|
|
3465
|
+
epochDuration: phaseConfig.epochDurationHours,
|
|
3466
|
+
gracePeriod: phaseConfig.gracePeriodMinutes,
|
|
3467
|
+
});
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
// =========================================
|
|
3471
|
+
// Public Time API — GPS Time for the World
|
|
3472
|
+
// =========================================
|
|
3473
|
+
// These endpoints serve live GPS time from the MA-902 grandmaster clock.
|
|
3474
|
+
// On the LAN node: data comes directly from SNMP. On meshed Hostinger node:
|
|
3475
|
+
// data arrives via mesh peering with the LAN grandmaster.
|
|
3476
|
+
// The landing page at yakmesh.dev/time/ polls these endpoints.
|
|
3477
|
+
|
|
3478
|
+
app.get('/api/time', (req, res) => {
|
|
3479
|
+
const now = Date.now();
|
|
3480
|
+
const status = this.timeSource?.getStatus() || {};
|
|
3481
|
+
const sats = status.satellites || status.ma902?.satellites || {};
|
|
3482
|
+
const locked = status.trustLevel === 'gps' || status.trustLevel === 'atomic';
|
|
3483
|
+
|
|
3484
|
+
// Mesh grandmaster reference (received via time:heartbeat gossip)
|
|
3485
|
+
const meshRef = this.meshTimeReference;
|
|
3486
|
+
const hasMeshGrandmaster = meshRef && meshRef.lock && (Date.now() - meshRef.receivedAt < 120_000);
|
|
3487
|
+
|
|
3488
|
+
// Effective source: local GPS if available, else mesh grandmaster, else system
|
|
3489
|
+
const effectiveStratum = locked ? 1 : (hasMeshGrandmaster ? meshRef.stratum : 2);
|
|
3490
|
+
const effectiveSource = locked ? 'MA-902/S-C1 GPS' :
|
|
3491
|
+
(hasMeshGrandmaster ? `mesh/${meshRef.nodeName || peerTag(meshRef.fromNodeId)}` : 'system');
|
|
3492
|
+
const effectiveAccuracy = locked ? 1 : (hasMeshGrandmaster ? (meshRef.accuracy_ms ?? 5) : 50);
|
|
3493
|
+
const effectiveQuality = locked ? 'excellent' : (hasMeshGrandmaster ? 'mesh-synced' : 'degraded');
|
|
3494
|
+
|
|
3495
|
+
res.set({
|
|
3496
|
+
'Access-Control-Allow-Origin': '*',
|
|
3497
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
|
3498
|
+
'X-Yakmesh-Time': (now / 1000).toFixed(3),
|
|
3499
|
+
'X-Yakmesh-Stratum': String(effectiveStratum),
|
|
3500
|
+
'X-Yakmesh-Source': effectiveSource,
|
|
3501
|
+
});
|
|
3502
|
+
|
|
3503
|
+
const body = {
|
|
3504
|
+
iso: new Date(now).toISOString(),
|
|
3505
|
+
unix: now / 1000,
|
|
3506
|
+
unix_ms: now,
|
|
3507
|
+
stratum: effectiveStratum,
|
|
3508
|
+
source: effectiveSource,
|
|
3509
|
+
accuracy_ms: effectiveAccuracy,
|
|
3510
|
+
leap_indicator: 0,
|
|
3511
|
+
satellites: {
|
|
3512
|
+
visible: sats.visible ?? 0,
|
|
3513
|
+
used: sats.used ?? 0,
|
|
3514
|
+
tracking: sats.tracking ?? 0,
|
|
3515
|
+
constellations: sats.constellations ?? [],
|
|
3516
|
+
},
|
|
3517
|
+
lock: locked,
|
|
3518
|
+
quality: effectiveQuality,
|
|
3519
|
+
offset_ns: status.offset ?? 0,
|
|
3520
|
+
reference_id: locked ? 'GPS' : (hasMeshGrandmaster ? 'MESH' : 'SYS'),
|
|
3521
|
+
// Public NTP server (always available — points to MA-902 grandmaster)
|
|
3522
|
+
public_ntp: 'time.yakmesh.dev',
|
|
3523
|
+
};
|
|
3524
|
+
|
|
3525
|
+
// If this node isn't GPS-backed but has a mesh grandmaster, include its data
|
|
3526
|
+
if (!locked && hasMeshGrandmaster) {
|
|
3527
|
+
body.mesh_grandmaster = {
|
|
3528
|
+
nodeId: meshRef.fromNodeId,
|
|
3529
|
+
nodeName: meshRef.nodeName,
|
|
3530
|
+
stratum: meshRef.stratum,
|
|
3531
|
+
lock: meshRef.lock,
|
|
3532
|
+
satellites: meshRef.satellites,
|
|
3533
|
+
ma902: meshRef.ma902 || null,
|
|
3534
|
+
trustLevel: meshRef.trustLevel,
|
|
3535
|
+
publicNtp: meshRef.publicNtp,
|
|
3536
|
+
age_ms: Date.now() - meshRef.receivedAt,
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
res.json(body);
|
|
3541
|
+
});
|
|
3542
|
+
|
|
3543
|
+
app.get('/api/time/simple', (req, res) => {
|
|
3544
|
+
const now = Date.now();
|
|
3545
|
+
const status = this.timeSource?.getStatus() || {};
|
|
3546
|
+
const locked = status.trustLevel === 'gps' || status.trustLevel === 'atomic';
|
|
3547
|
+
const meshRef = this.meshTimeReference;
|
|
3548
|
+
const hasMeshGM = meshRef && meshRef.lock && (Date.now() - meshRef.receivedAt < 120_000);
|
|
3549
|
+
const eff = locked ? 1 : (hasMeshGM ? meshRef.stratum : 2);
|
|
3550
|
+
const q = locked ? 'excellent' : (hasMeshGM ? 'mesh-synced' : 'degraded');
|
|
3551
|
+
res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' });
|
|
3552
|
+
res.json({ t: now, s: eff, q, ntp: 'time.yakmesh.dev' });
|
|
3553
|
+
});
|
|
3554
|
+
|
|
3555
|
+
app.get('/api/health', (req, res) => {
|
|
3556
|
+
const status = this.timeSource?.getStatus() || {};
|
|
3557
|
+
const sats = status.satellites || status.ma902?.satellites || {};
|
|
3558
|
+
const locked = status.trustLevel === 'gps' || status.trustLevel === 'atomic';
|
|
3559
|
+
const meshRef = this.meshTimeReference;
|
|
3560
|
+
const hasMeshGM = meshRef && meshRef.lock && (Date.now() - meshRef.receivedAt < 120_000);
|
|
3561
|
+
const effectiveStatus = locked ? 'healthy' : (hasMeshGM ? 'mesh-synced' : 'degraded');
|
|
1225
3562
|
|
|
3563
|
+
res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' });
|
|
1226
3564
|
res.json({
|
|
1227
|
-
|
|
1228
|
-
|
|
3565
|
+
status: effectiveStatus,
|
|
3566
|
+
lock: locked,
|
|
3567
|
+
satellites_visible: sats.visible ?? 0,
|
|
3568
|
+
satellites_used: sats.used ?? 0,
|
|
3569
|
+
constellations: sats.constellations ?? [],
|
|
3570
|
+
alarm: status.alarm ?? false,
|
|
3571
|
+
quality: locked ? 'excellent' : (hasMeshGM ? 'mesh-synced' : 'degraded'),
|
|
3572
|
+
trust_level: status.trustLevel ?? 'unknown',
|
|
3573
|
+
mesh_grandmaster: hasMeshGM ? {
|
|
3574
|
+
nodeName: meshRef.nodeName,
|
|
3575
|
+
stratum: meshRef.stratum,
|
|
3576
|
+
lock: meshRef.lock,
|
|
3577
|
+
satellites_used: meshRef.satellites?.used ?? 0,
|
|
3578
|
+
publicNtp: meshRef.publicNtp,
|
|
3579
|
+
age_ms: Date.now() - meshRef.receivedAt,
|
|
3580
|
+
} : null,
|
|
3581
|
+
public_ntp: 'time.yakmesh.dev',
|
|
1229
3582
|
});
|
|
1230
3583
|
});
|
|
1231
3584
|
|
|
1232
|
-
//
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
return res.status(503).json({ error: 'Time source detector not initialized' });
|
|
1236
|
-
}
|
|
3585
|
+
// =========================================
|
|
3586
|
+
// SANGHA Collective Status (v3.0)
|
|
3587
|
+
// =========================================
|
|
1237
3588
|
|
|
1238
|
-
|
|
1239
|
-
|
|
3589
|
+
// Get SANGHA collective status
|
|
3590
|
+
app.get('/api/sangha', (req, res) => {
|
|
3591
|
+
if (!this.sangha) {
|
|
3592
|
+
return res.status(503).json({ error: 'SANGHA not initialized' });
|
|
3593
|
+
}
|
|
3594
|
+
res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' });
|
|
3595
|
+
res.json(this.sangha.getStatus());
|
|
3596
|
+
});
|
|
1240
3597
|
|
|
3598
|
+
// Get recent antibody circulations
|
|
3599
|
+
app.get('/api/sangha/circulations', (req, res) => {
|
|
3600
|
+
if (!this.sangha) {
|
|
3601
|
+
return res.status(503).json({ error: 'SANGHA not initialized' });
|
|
3602
|
+
}
|
|
3603
|
+
const count = Math.min(parseInt(req.query.count) || 10, 100);
|
|
3604
|
+
res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' });
|
|
1241
3605
|
res.json({
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
canBeTimeOracle: phaseConfig.capabilities.canBeTimeOracle,
|
|
1245
|
-
canValidateTightPhase: phaseConfig.capabilities.canValidateTightPhase,
|
|
1246
|
-
canParticipateInConsensus: phaseConfig.capabilities.canParticipateInConsensus,
|
|
1247
|
-
hasAtomicTime: this.timeSource.hasAtomicTime(),
|
|
1248
|
-
hasHighPrecisionTime: this.timeSource.hasHighPrecisionTime(),
|
|
1249
|
-
phaseTolerance: status.phaseTolerance,
|
|
1250
|
-
epochDuration: phaseConfig.epochDurationHours,
|
|
1251
|
-
gracePeriod: phaseConfig.gracePeriodMinutes,
|
|
3606
|
+
circulations: this.sangha.getRecentCirculations(count),
|
|
3607
|
+
status: this.sangha.getStatus(),
|
|
1252
3608
|
});
|
|
1253
3609
|
});
|
|
1254
3610
|
|
|
3611
|
+
// Trigger manual circulation (for testing)
|
|
3612
|
+
app.post('/api/sangha/circulate', async (req, res) => {
|
|
3613
|
+
if (!this.sangha) {
|
|
3614
|
+
return res.status(503).json({ error: 'SANGHA not initialized' });
|
|
3615
|
+
}
|
|
3616
|
+
try {
|
|
3617
|
+
const result = await this.sangha.circulate();
|
|
3618
|
+
res.json({ success: true, result });
|
|
3619
|
+
} catch (e) {
|
|
3620
|
+
res.status(500).json({ error: e.message });
|
|
3621
|
+
}
|
|
3622
|
+
});
|
|
3623
|
+
|
|
1255
3624
|
// =========================================
|
|
1256
3625
|
// NAMCHE Security Gate Endpoints (v2.0)
|
|
1257
3626
|
// =========================================
|
|
1258
|
-
|
|
3627
|
+
|
|
1259
3628
|
// Get all gate statuses
|
|
1260
3629
|
app.get('/security/namche/gates', (req, res) => {
|
|
1261
3630
|
if (!this.namcheGateway) {
|
|
@@ -1271,18 +3640,19 @@ export class YakmeshNode {
|
|
|
1271
3640
|
}
|
|
1272
3641
|
res.json(this.namcheGateway.getStatus());
|
|
1273
3642
|
});
|
|
1274
|
-
|
|
3643
|
+
|
|
1275
3644
|
// Verify a specific gate
|
|
1276
|
-
|
|
3645
|
+
// SECURITY: Peer auth — only known peers can trigger gate verification
|
|
3646
|
+
app.post('/security/namche/verify/:gate', writeLimiter, requirePeerAuth, (req, res) => {
|
|
1277
3647
|
const gateNum = parseInt(req.params.gate);
|
|
1278
3648
|
if (gateNum < 1 || gateNum > 7) {
|
|
1279
3649
|
return res.status(400).json({ error: 'Gate must be 1-7' });
|
|
1280
3650
|
}
|
|
1281
|
-
|
|
3651
|
+
|
|
1282
3652
|
if (!this.namcheGateway) {
|
|
1283
3653
|
return res.status(503).json({ error: 'NAMCHE gateway not initialized' });
|
|
1284
3654
|
}
|
|
1285
|
-
|
|
3655
|
+
|
|
1286
3656
|
const result = this.namcheGateway.verifyGate(gateNum, req.body);
|
|
1287
3657
|
res.json({
|
|
1288
3658
|
gate: gateNum,
|
|
@@ -1290,11 +3660,11 @@ export class YakmeshNode {
|
|
|
1290
3660
|
...result
|
|
1291
3661
|
});
|
|
1292
3662
|
});
|
|
1293
|
-
|
|
3663
|
+
|
|
1294
3664
|
// Get comprehensive security status
|
|
1295
3665
|
app.get('/security/status', (req, res) => {
|
|
1296
3666
|
const oracleIntegrity = this.oracle?.verifySelfIntegrity();
|
|
1297
|
-
|
|
3667
|
+
|
|
1298
3668
|
res.json({
|
|
1299
3669
|
namche: this.namcheGateway?.getStatus() || { status: 'uninitialized' },
|
|
1300
3670
|
doko: this.dokoRegistry?.getStats() || { status: 'uninitialized' },
|
|
@@ -1315,7 +3685,7 @@ export class YakmeshNode {
|
|
|
1315
3685
|
// =========================================
|
|
1316
3686
|
// DOKO Identity Endpoints (v2.0)
|
|
1317
3687
|
// =========================================
|
|
1318
|
-
|
|
3688
|
+
|
|
1319
3689
|
// Get DOKO registry stats
|
|
1320
3690
|
app.get('/security/doko/stats', (req, res) => {
|
|
1321
3691
|
if (!this.dokoRegistry) {
|
|
@@ -1327,13 +3697,13 @@ export class YakmeshNode {
|
|
|
1327
3697
|
}
|
|
1328
3698
|
res.json(this.dokoRegistry.getStats());
|
|
1329
3699
|
});
|
|
1330
|
-
|
|
3700
|
+
|
|
1331
3701
|
// List identities (limited info)
|
|
1332
3702
|
app.get('/security/doko/identities', (req, res) => {
|
|
1333
3703
|
if (!this.dokoRegistry) {
|
|
1334
3704
|
return res.status(503).json({ error: 'DOKO registry not initialized' });
|
|
1335
3705
|
}
|
|
1336
|
-
|
|
3706
|
+
|
|
1337
3707
|
const type = req.query.type || null;
|
|
1338
3708
|
const identities = this.dokoRegistry.list(type);
|
|
1339
3709
|
res.json({
|
|
@@ -1347,18 +3717,19 @@ export class YakmeshNode {
|
|
|
1347
3717
|
}))
|
|
1348
3718
|
});
|
|
1349
3719
|
});
|
|
1350
|
-
|
|
3720
|
+
|
|
1351
3721
|
// Verify an identity
|
|
1352
|
-
|
|
3722
|
+
// SECURITY: Peer auth — only known peers can request identity verification
|
|
3723
|
+
app.post('/security/doko/verify', writeLimiter, requirePeerAuth, (req, res) => {
|
|
1353
3724
|
if (!this.dokoRegistry) {
|
|
1354
3725
|
return res.status(503).json({ error: 'DOKO registry not initialized' });
|
|
1355
3726
|
}
|
|
1356
|
-
|
|
3727
|
+
|
|
1357
3728
|
const { id, challenge, signature } = req.body;
|
|
1358
3729
|
if (!id || !challenge || !signature) {
|
|
1359
3730
|
return res.status(400).json({ error: 'Missing id, challenge, or signature' });
|
|
1360
3731
|
}
|
|
1361
|
-
|
|
3732
|
+
|
|
1362
3733
|
const result = this.dokoRegistry.verify(id, challenge, signature);
|
|
1363
3734
|
res.json(result);
|
|
1364
3735
|
});
|
|
@@ -1367,11 +3738,11 @@ export class YakmeshNode {
|
|
|
1367
3738
|
// Geographic Proof Endpoints (v2.5.0)
|
|
1368
3739
|
// Speed-of-Light Exclusion Zones
|
|
1369
3740
|
// =========================================
|
|
1370
|
-
|
|
3741
|
+
|
|
1371
3742
|
// Get geo proof status
|
|
1372
3743
|
app.get('/geo/status', (req, res) => {
|
|
1373
3744
|
const timeSourceStatus = this.timeSource?.getStatus() || null;
|
|
1374
|
-
|
|
3745
|
+
|
|
1375
3746
|
// Initialize geo proof service lazily if needed
|
|
1376
3747
|
if (!this.geoProofService && this.timeSource && this.identity) {
|
|
1377
3748
|
this.geoProofService = new GeoProofService({
|
|
@@ -1379,9 +3750,9 @@ export class YakmeshNode {
|
|
|
1379
3750
|
timeSourceDetector: this.timeSource,
|
|
1380
3751
|
});
|
|
1381
3752
|
}
|
|
1382
|
-
|
|
3753
|
+
|
|
1383
3754
|
const service = this.geoProofService;
|
|
1384
|
-
|
|
3755
|
+
|
|
1385
3756
|
res.json({
|
|
1386
3757
|
timeSource: timeSourceStatus ? {
|
|
1387
3758
|
type: timeSourceStatus.trustLevel,
|
|
@@ -1409,7 +3780,7 @@ export class YakmeshNode {
|
|
|
1409
3780
|
},
|
|
1410
3781
|
});
|
|
1411
3782
|
});
|
|
1412
|
-
|
|
3783
|
+
|
|
1413
3784
|
// List landmarks
|
|
1414
3785
|
app.get('/geo/landmarks', (req, res) => {
|
|
1415
3786
|
// Initialize geo proof service lazily if needed
|
|
@@ -1419,23 +3790,23 @@ export class YakmeshNode {
|
|
|
1419
3790
|
timeSourceDetector: this.timeSource,
|
|
1420
3791
|
});
|
|
1421
3792
|
}
|
|
1422
|
-
|
|
3793
|
+
|
|
1423
3794
|
const service = this.geoProofService;
|
|
1424
3795
|
if (!service) {
|
|
1425
3796
|
return res.json({ landmarks: [], message: 'Geographic proof service not initialized' });
|
|
1426
3797
|
}
|
|
1427
|
-
|
|
3798
|
+
|
|
1428
3799
|
const verifiedOnly = req.query.verified === 'true';
|
|
1429
3800
|
let landmarks = Array.from(service.landmarkRegistry.landmarks.values());
|
|
1430
|
-
|
|
3801
|
+
|
|
1431
3802
|
if (verifiedOnly) {
|
|
1432
3803
|
landmarks = landmarks.filter(l => l.verified);
|
|
1433
3804
|
}
|
|
1434
|
-
|
|
3805
|
+
|
|
1435
3806
|
res.json({
|
|
1436
3807
|
landmarks: landmarks.map(lm => ({
|
|
1437
3808
|
nodeId: lm.nodeId,
|
|
1438
|
-
name: lm.name || `Landmark ${lm.nodeId
|
|
3809
|
+
name: lm.name || `Landmark ${peerTag(lm.nodeId)}`,
|
|
1439
3810
|
lat: lm.lat,
|
|
1440
3811
|
lon: lm.lon,
|
|
1441
3812
|
tier: lm.tier,
|
|
@@ -1446,18 +3817,19 @@ export class YakmeshNode {
|
|
|
1446
3817
|
count: landmarks.length,
|
|
1447
3818
|
});
|
|
1448
3819
|
});
|
|
1449
|
-
|
|
3820
|
+
|
|
1450
3821
|
// Add a landmark
|
|
1451
|
-
|
|
3822
|
+
// SECURITY: Peer auth — prevent phantom landmark injection
|
|
3823
|
+
app.post('/geo/landmarks', writeLimiter, requirePeerAuth, (req, res) => {
|
|
1452
3824
|
const { name, lat, lon, nodeId, endpoint } = req.body;
|
|
1453
|
-
|
|
3825
|
+
|
|
1454
3826
|
if (typeof lat !== 'number' || typeof lon !== 'number') {
|
|
1455
3827
|
return res.status(400).json({ error: 'lat and lon must be numbers' });
|
|
1456
3828
|
}
|
|
1457
3829
|
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
|
|
1458
3830
|
return res.status(400).json({ error: 'Invalid coordinates' });
|
|
1459
3831
|
}
|
|
1460
|
-
|
|
3832
|
+
|
|
1461
3833
|
// Initialize geo proof service lazily if needed
|
|
1462
3834
|
if (!this.geoProofService && this.timeSource && this.identity) {
|
|
1463
3835
|
this.geoProofService = new GeoProofService({
|
|
@@ -1465,12 +3837,12 @@ export class YakmeshNode {
|
|
|
1465
3837
|
timeSourceDetector: this.timeSource,
|
|
1466
3838
|
});
|
|
1467
3839
|
}
|
|
1468
|
-
|
|
3840
|
+
|
|
1469
3841
|
const service = this.geoProofService;
|
|
1470
3842
|
if (!service) {
|
|
1471
3843
|
return res.status(503).json({ error: 'Geographic proof service not initialized' });
|
|
1472
3844
|
}
|
|
1473
|
-
|
|
3845
|
+
|
|
1474
3846
|
const landmarkId = nodeId || `landmark-${Date.now()}`;
|
|
1475
3847
|
service.landmarkRegistry.addLandmark(landmarkId, lat, lon, {
|
|
1476
3848
|
name,
|
|
@@ -1478,21 +3850,21 @@ export class YakmeshNode {
|
|
|
1478
3850
|
verified: false,
|
|
1479
3851
|
addedManually: true,
|
|
1480
3852
|
});
|
|
1481
|
-
|
|
3853
|
+
|
|
1482
3854
|
res.json({ success: true, landmarkId, name, lat, lon });
|
|
1483
3855
|
});
|
|
1484
|
-
|
|
3856
|
+
|
|
1485
3857
|
// List exclusion zones
|
|
1486
3858
|
app.get('/geo/zones', (req, res) => {
|
|
1487
3859
|
if (!this.geoProofService) {
|
|
1488
3860
|
return res.json({ zones: [], message: 'No geographic proof established' });
|
|
1489
3861
|
}
|
|
1490
|
-
|
|
3862
|
+
|
|
1491
3863
|
const proof = this.geoProofService.myProof;
|
|
1492
3864
|
if (!proof || !proof.zones) {
|
|
1493
3865
|
return res.json({ zones: [], message: 'No exclusion zones established' });
|
|
1494
3866
|
}
|
|
1495
|
-
|
|
3867
|
+
|
|
1496
3868
|
const zones = proof.zones.map(zone => {
|
|
1497
3869
|
const landmark = this.geoProofService.landmarkRegistry.getLandmark(zone.landmarkId);
|
|
1498
3870
|
return {
|
|
@@ -1505,14 +3877,15 @@ export class YakmeshNode {
|
|
|
1505
3877
|
measuredAt: zone.measuredAt,
|
|
1506
3878
|
};
|
|
1507
3879
|
});
|
|
1508
|
-
|
|
3880
|
+
|
|
1509
3881
|
res.json({ zones, count: zones.length });
|
|
1510
3882
|
});
|
|
1511
|
-
|
|
3883
|
+
|
|
1512
3884
|
// Generate geographic proof
|
|
1513
|
-
|
|
3885
|
+
// SECURITY: Peer auth required for proof generation
|
|
3886
|
+
app.post('/geo/prove', writeLimiter, requirePeerAuth, async (req, res) => {
|
|
1514
3887
|
const { force } = req.body || {};
|
|
1515
|
-
|
|
3888
|
+
|
|
1516
3889
|
// Initialize geo proof service lazily if needed
|
|
1517
3890
|
if (!this.geoProofService && this.timeSource && this.identity) {
|
|
1518
3891
|
this.geoProofService = new GeoProofService({
|
|
@@ -1520,20 +3893,20 @@ export class YakmeshNode {
|
|
|
1520
3893
|
timeSourceDetector: this.timeSource,
|
|
1521
3894
|
});
|
|
1522
3895
|
}
|
|
1523
|
-
|
|
3896
|
+
|
|
1524
3897
|
const service = this.geoProofService;
|
|
1525
3898
|
if (!service) {
|
|
1526
|
-
return res.status(503).json({
|
|
1527
|
-
success: false,
|
|
3899
|
+
return res.status(503).json({
|
|
3900
|
+
success: false,
|
|
1528
3901
|
error: 'Geographic proof service not initialized',
|
|
1529
3902
|
reason: 'Time source or identity not available'
|
|
1530
3903
|
});
|
|
1531
3904
|
}
|
|
1532
|
-
|
|
3905
|
+
|
|
1533
3906
|
try {
|
|
1534
3907
|
// Get all landmarks to measure
|
|
1535
3908
|
const landmarks = Array.from(service.landmarkRegistry.landmarks.values());
|
|
1536
|
-
|
|
3909
|
+
|
|
1537
3910
|
if (landmarks.length === 0) {
|
|
1538
3911
|
return res.json({
|
|
1539
3912
|
success: false,
|
|
@@ -1541,7 +3914,7 @@ export class YakmeshNode {
|
|
|
1541
3914
|
reason: 'Add landmarks via KHATA gossip or manually with POST /geo/landmarks'
|
|
1542
3915
|
});
|
|
1543
3916
|
}
|
|
1544
|
-
|
|
3917
|
+
|
|
1545
3918
|
// Measure RTT to each landmark (simulated for now - real implementation uses WebSocket)
|
|
1546
3919
|
const measurements = [];
|
|
1547
3920
|
for (const lm of landmarks) {
|
|
@@ -1555,10 +3928,10 @@ export class YakmeshNode {
|
|
|
1555
3928
|
measuredAt: Date.now(),
|
|
1556
3929
|
});
|
|
1557
3930
|
}
|
|
1558
|
-
|
|
3931
|
+
|
|
1559
3932
|
// Create proof from measurements
|
|
1560
3933
|
const proof = service.createProof(measurements);
|
|
1561
|
-
|
|
3934
|
+
|
|
1562
3935
|
res.json({
|
|
1563
3936
|
success: true,
|
|
1564
3937
|
proof: {
|
|
@@ -1569,7 +3942,7 @@ export class YakmeshNode {
|
|
|
1569
3942
|
zones: (proof.zones || []).map(z => {
|
|
1570
3943
|
const lm = service.landmarkRegistry.getLandmark(z.landmarkId);
|
|
1571
3944
|
return {
|
|
1572
|
-
landmarkName: lm?.name || z.landmarkId
|
|
3945
|
+
landmarkName: lm?.name || peerTag(z.landmarkId),
|
|
1573
3946
|
radiusKm: z.minDistanceKm,
|
|
1574
3947
|
};
|
|
1575
3948
|
}),
|
|
@@ -1579,38 +3952,119 @@ export class YakmeshNode {
|
|
|
1579
3952
|
res.status(500).json({ success: false, error: error.message });
|
|
1580
3953
|
}
|
|
1581
3954
|
});
|
|
1582
|
-
|
|
1583
|
-
// Verify another node's geographic claims
|
|
3955
|
+
|
|
3956
|
+
// Verify another node's geographic claims using PRAMAAN physics
|
|
3957
|
+
// Accepts either a nodeId (lookup cached proof) or a full proof payload
|
|
1584
3958
|
app.post('/geo/verify', writeLimiter, async (req, res) => {
|
|
1585
|
-
const { nodeId } = req.body;
|
|
1586
|
-
|
|
1587
|
-
if (!nodeId) {
|
|
1588
|
-
return res.status(400).json({ error: 'nodeId
|
|
3959
|
+
const { nodeId, proof: proofData } = req.body;
|
|
3960
|
+
|
|
3961
|
+
if (!nodeId && !proofData) {
|
|
3962
|
+
return res.status(400).json({ error: 'nodeId or proof required' });
|
|
1589
3963
|
}
|
|
1590
|
-
|
|
3964
|
+
|
|
1591
3965
|
if (!this.geoProofService) {
|
|
1592
|
-
return res.status(503).json({
|
|
1593
|
-
verified: false,
|
|
1594
|
-
reason: 'Geographic proof service not initialized'
|
|
3966
|
+
return res.status(503).json({
|
|
3967
|
+
verified: false,
|
|
3968
|
+
reason: 'Geographic proof service not initialized'
|
|
1595
3969
|
});
|
|
1596
3970
|
}
|
|
1597
|
-
|
|
3971
|
+
|
|
1598
3972
|
try {
|
|
1599
|
-
//
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
3973
|
+
// Deserialize the peer's proof
|
|
3974
|
+
let peerProof;
|
|
3975
|
+
if (proofData) {
|
|
3976
|
+
// Direct proof submission — deserialize and verify
|
|
3977
|
+
peerProof = GeographicProof.deserialize(proofData);
|
|
3978
|
+
} else {
|
|
3979
|
+
// Look up cached proof from gossip
|
|
3980
|
+
peerProof = this.geoProofService.proofs.get(nodeId);
|
|
3981
|
+
if (!peerProof) {
|
|
3982
|
+
return res.json({
|
|
3983
|
+
verified: false,
|
|
3984
|
+
nodeId,
|
|
3985
|
+
reason: 'No geo-proof available for this node. Request via gossip first.',
|
|
3986
|
+
confidence: 0,
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
// ── PRAMAAN Verification: Physics-based consistency checks ──
|
|
3992
|
+
const verificationResults = [];
|
|
3993
|
+
let sharedLandmarks = 0;
|
|
3994
|
+
let physicsViolations = 0;
|
|
3995
|
+
const ourMeasurements = this.geoProofService.measurementCache;
|
|
3996
|
+
|
|
3997
|
+
for (const zone of peerProof.exclusionZones) {
|
|
3998
|
+
const result = {
|
|
3999
|
+
landmarkId: zone.landmarkId,
|
|
4000
|
+
landmarkName: zone.landmarkName,
|
|
4001
|
+
claimedRttMs: zone.rttMs,
|
|
4002
|
+
claimedMinDistanceKm: zone.minDistanceKm,
|
|
4003
|
+
valid: true,
|
|
4004
|
+
checks: [],
|
|
4005
|
+
};
|
|
4006
|
+
|
|
4007
|
+
// Check 1: RTT must be positive and physically plausible
|
|
4008
|
+
if (zone.rttMs == null || zone.rttMs <= 0) {
|
|
4009
|
+
result.valid = false;
|
|
4010
|
+
result.checks.push('FAIL: RTT must be positive');
|
|
4011
|
+
physicsViolations++;
|
|
4012
|
+
} else {
|
|
4013
|
+
result.checks.push('PASS: RTT positive');
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
// Check 2: Claimed distance must equal calculateMinDistance(rtt) within precision
|
|
4017
|
+
if (zone.rttMs > 0) {
|
|
4018
|
+
const expectedMinDist = calculateMinDistance(zone.rttMs, 'fiber');
|
|
4019
|
+
const tolerance = zone.precisionKm || 50; // precision from time source
|
|
4020
|
+
if (Math.abs(zone.minDistanceKm - expectedMinDist) > tolerance) {
|
|
4021
|
+
result.valid = false;
|
|
4022
|
+
result.checks.push(
|
|
4023
|
+
`FAIL: Distance ${zone.minDistanceKm.toFixed(1)}km inconsistent with RTT ${zone.rttMs.toFixed(1)}ms (expected ~${expectedMinDist.toFixed(1)}km)`
|
|
4024
|
+
);
|
|
4025
|
+
physicsViolations++;
|
|
4026
|
+
} else {
|
|
4027
|
+
result.checks.push('PASS: Distance consistent with RTT (speed-of-light)');
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
// Check 3: Cross-reference with OUR measurements to the same landmark
|
|
4032
|
+
const ourMeasurement = ourMeasurements.get(zone.landmarkId);
|
|
4033
|
+
if (ourMeasurement) {
|
|
4034
|
+
sharedLandmarks++;
|
|
4035
|
+
const ourRtt = ourMeasurement.getMinRTT();
|
|
4036
|
+
if (ourRtt !== null) {
|
|
4037
|
+
// Triangle inequality: |peerRTT - ourRTT| should be <= sum
|
|
4038
|
+
// (both should be positive, and wildly different RTTs to the same
|
|
4039
|
+
// landmark are suspicious but not impossible — different continents)
|
|
4040
|
+
result.checks.push(
|
|
4041
|
+
`INFO: Our RTT to ${zone.landmarkName}: ${ourRtt.toFixed(1)}ms vs peer ${zone.rttMs?.toFixed(1)}ms`
|
|
4042
|
+
);
|
|
4043
|
+
result.ourRttMs = ourRtt;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
verificationResults.push(result);
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// Compute overall confidence
|
|
4051
|
+
const totalZones = peerProof.exclusionZones.length;
|
|
4052
|
+
const validZones = verificationResults.filter(r => r.valid).length;
|
|
4053
|
+
const confidence = totalZones > 0 ? validZones / totalZones : 0;
|
|
4054
|
+
const verified = physicsViolations === 0 && totalZones >= GEO_PROOF_CONFIG.minLandmarks;
|
|
4055
|
+
|
|
1607
4056
|
res.json({
|
|
1608
|
-
verified
|
|
1609
|
-
nodeId,
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
4057
|
+
verified,
|
|
4058
|
+
nodeId: peerProof.nodeId || nodeId,
|
|
4059
|
+
dokoId: peerProof.dokoId,
|
|
4060
|
+
validZones,
|
|
4061
|
+
totalZones,
|
|
4062
|
+
sharedLandmarks,
|
|
4063
|
+
physicsViolations,
|
|
4064
|
+
confidence,
|
|
4065
|
+
timeSource: peerProof.timeSource,
|
|
4066
|
+
proofTimestamp: peerProof.timestamp,
|
|
4067
|
+
zones: verificationResults,
|
|
1614
4068
|
});
|
|
1615
4069
|
} catch (error) {
|
|
1616
4070
|
res.status(500).json({ verified: false, reason: error.message });
|
|
@@ -1620,7 +4074,7 @@ export class YakmeshNode {
|
|
|
1620
4074
|
return new Promise(async (resolve, reject) => {
|
|
1621
4075
|
const basePort = this.config.network.httpPort;
|
|
1622
4076
|
const maxRetries = 10;
|
|
1623
|
-
|
|
4077
|
+
|
|
1624
4078
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1625
4079
|
const port = basePort + attempt;
|
|
1626
4080
|
try {
|
|
@@ -1649,25 +4103,468 @@ export class YakmeshNode {
|
|
|
1649
4103
|
const server = app.listen(port);
|
|
1650
4104
|
server.on('listening', () => {
|
|
1651
4105
|
this.http = server;
|
|
4106
|
+
|
|
4107
|
+
// Handle TCP-level client errors (ECONNRESET, EPIPE, etc.)
|
|
4108
|
+
// These occur when clients disconnect abruptly — normal in P2P mesh.
|
|
4109
|
+
// Without this handler, they bubble up as uncaught exceptions.
|
|
4110
|
+
server.on('clientError', (err, socket) => {
|
|
4111
|
+
if (socket.writable) {
|
|
4112
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
4113
|
+
}
|
|
4114
|
+
socket.destroy();
|
|
4115
|
+
});
|
|
4116
|
+
|
|
1652
4117
|
resolve();
|
|
1653
4118
|
});
|
|
1654
4119
|
server.on('error', reject);
|
|
1655
4120
|
});
|
|
1656
4121
|
}
|
|
1657
4122
|
|
|
1658
|
-
|
|
4123
|
+
/**
|
|
4124
|
+
* Auto-register with relay peers from YAKMESH_RELAY_PEERS config.
|
|
4125
|
+
* Non-blocking — runs after server is up, fire-and-forget like bootstrap.
|
|
4126
|
+
*/
|
|
4127
|
+
_connectToRelayPeers() {
|
|
4128
|
+
const relayPeers = this.config.relayPeers || [];
|
|
4129
|
+
if (relayPeers.length === 0) return;
|
|
4130
|
+
|
|
4131
|
+
log.info(`RELAY: ${relayPeers.length} relay peer(s) from config — registering in background`);
|
|
4132
|
+
|
|
4133
|
+
// Delay slightly to let identity and mesh fully initialize
|
|
4134
|
+
setTimeout(async () => {
|
|
4135
|
+
for (const endpoint of relayPeers) {
|
|
4136
|
+
if (!endpoint || typeof endpoint !== 'string' || !endpoint.startsWith('http')) {
|
|
4137
|
+
log.warn(`RELAY: skipping invalid endpoint: ${endpoint}`);
|
|
4138
|
+
continue;
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
const candidate = {
|
|
4142
|
+
nodeId: `relay-${Date.now()}`,
|
|
4143
|
+
relayEndpoint: endpoint,
|
|
4144
|
+
};
|
|
4145
|
+
|
|
4146
|
+
try {
|
|
4147
|
+
await this._registerWithRelay(candidate);
|
|
4148
|
+
log.info(`RELAY: ✓ registered with ${endpoint}`);
|
|
4149
|
+
} catch (err) {
|
|
4150
|
+
log.warn(`RELAY: ${endpoint} registration failed — ${err.message}`);
|
|
4151
|
+
// Retry after 30s (once) — relay peers may not be up yet
|
|
4152
|
+
setTimeout(async () => {
|
|
4153
|
+
try {
|
|
4154
|
+
await this._registerWithRelay(candidate);
|
|
4155
|
+
log.info(`RELAY: ✓ registered with ${endpoint} (retry)`);
|
|
4156
|
+
} catch (e) {
|
|
4157
|
+
log.warn(`RELAY: ${endpoint} retry failed — ${e.message}`);
|
|
4158
|
+
}
|
|
4159
|
+
}, 30000);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
}, 3000);
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
/**
|
|
4166
|
+
* Get active relay info for gossip propagation.
|
|
4167
|
+
* Returns list of relay endpoints this node is registered with,
|
|
4168
|
+
* so peers can discover relay paths through HELLO broadcasts.
|
|
4169
|
+
*/
|
|
4170
|
+
_getActiveRelayInfo() {
|
|
4171
|
+
const endpoints = [];
|
|
4172
|
+
if (this._relayPollers) {
|
|
4173
|
+
for (const [nodeId, _interval] of this._relayPollers) {
|
|
4174
|
+
// Find the relay endpoint URL for this poller
|
|
4175
|
+
// We track candidates when we register — check _relayEndpoints map
|
|
4176
|
+
if (this._relayEndpoints?.has(nodeId)) {
|
|
4177
|
+
endpoints.push(this._relayEndpoints.get(nodeId));
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
return { relayEndpoints: endpoints };
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
/**
|
|
4185
|
+
* Bootstrap — SEED ONLY mechanism for initial network join.
|
|
4186
|
+
*
|
|
4187
|
+
* Connection priority (proper flow):
|
|
4188
|
+
* 1. DirectWS — known peers from gossip, saved state, inbound connections
|
|
4189
|
+
* 2. Bootstrap — initial network discovery when no peers exist
|
|
4190
|
+
* 3. Beacon Relays — NAT traversal fallback
|
|
4191
|
+
* 4. Crawlers — active network discovery
|
|
4192
|
+
* 5. Gossip — passive peer exchange (MANTRA)
|
|
4193
|
+
*
|
|
4194
|
+
* Bootstrap ONLY activates when we have zero peers. Once connected to the
|
|
4195
|
+
* network, we rely on gossip for peer exchange. This prevents duplicate
|
|
4196
|
+
* connections and race conditions.
|
|
4197
|
+
*/
|
|
4198
|
+
_connectToBootstrap() {
|
|
4199
|
+
// ── Build bootstrap peer list once at startup ──
|
|
4200
|
+
if (!this._bootstrapPeers) {
|
|
4201
|
+
this._buildBootstrapPeerList();
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
if (this._bootstrapPeers.length === 0) {
|
|
4205
|
+
log.info('BOOTSTRAP: no remote peers configured');
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// ── Check if we actually need bootstrap (zero peers) ──
|
|
4210
|
+
const currentPeers = this.mesh?.getPeers?.() || [];
|
|
4211
|
+
if (currentPeers.length > 0) {
|
|
4212
|
+
log.debug(`BOOTSTRAP: skipping — already have ${currentPeers.length} peer(s), using gossip for discovery`);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
log.info(`BOOTSTRAP: no peers — seeding network from ${this._bootstrapPeers.length} configured peer(s)`);
|
|
4217
|
+
|
|
4218
|
+
// ── Try all bootstrap peers concurrently ──
|
|
4219
|
+
this._tryBootstrapConnections();
|
|
4220
|
+
|
|
4221
|
+
// ── Setup recovery watcher (only runs when we lose all peers) ──
|
|
4222
|
+
if (!this._bootstrapRecoverySetup) {
|
|
4223
|
+
this._bootstrapRecoverySetup = true;
|
|
4224
|
+
this.mesh.on('peer:disconnected', () => {
|
|
4225
|
+
// Check if we lost ALL peers — if so, trigger bootstrap
|
|
4226
|
+
setTimeout(() => {
|
|
4227
|
+
const peers = this.mesh?.getPeers?.() || [];
|
|
4228
|
+
if (peers.length === 0) {
|
|
4229
|
+
log.info('BOOTSTRAP: lost all peers — re-seeding network');
|
|
4230
|
+
this._tryBootstrapConnections();
|
|
4231
|
+
}
|
|
4232
|
+
}, 2000); // Small delay to allow reconnects
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
/**
|
|
4238
|
+
* Build the filtered list of bootstrap peers (run once at startup).
|
|
4239
|
+
*/
|
|
4240
|
+
_buildBootstrapPeerList() {
|
|
4241
|
+
const localAddrs = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']);
|
|
4242
|
+
const ifaces = networkInterfaces();
|
|
4243
|
+
for (const addrs of Object.values(ifaces)) {
|
|
4244
|
+
for (const addr of addrs) localAddrs.add(addr.address);
|
|
4245
|
+
}
|
|
4246
|
+
const ourWsPort = this.mesh.boundPort || this.config.network.wsPort;
|
|
4247
|
+
|
|
4248
|
+
this._bootstrapPeers = [];
|
|
1659
4249
|
for (const endpoint of this.config.bootstrap) {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
4250
|
+
let url;
|
|
4251
|
+
try { url = new URL(endpoint); } catch {
|
|
4252
|
+
log.warn(`BOOTSTRAP: invalid endpoint: ${endpoint}`);
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
const epPort = parseInt(url.port, 10);
|
|
4256
|
+
if (epPort === ourWsPort && localAddrs.has(url.hostname)) {
|
|
4257
|
+
log.debug(`BOOTSTRAP: skipping self: ${endpoint}`);
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
this._bootstrapPeers.push({
|
|
4261
|
+
endpoint,
|
|
4262
|
+
failures: 0,
|
|
4263
|
+
lastTry: 0,
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
if (this._bootstrapPeers.length > 0) {
|
|
4268
|
+
log.info(`BOOTSTRAP: ${this._bootstrapPeers.length} seed peer(s) configured`);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
/**
|
|
4273
|
+
* Attempt to connect to bootstrap peers for initial network seeding.
|
|
4274
|
+
* Only called when we have zero peers (not as a maintenance loop).
|
|
4275
|
+
*/
|
|
4276
|
+
_tryBootstrapConnections() {
|
|
4277
|
+
if (!this._bootstrapPeers) return;
|
|
4278
|
+
|
|
4279
|
+
// Double-check we still need to seed (another peer might have connected)
|
|
4280
|
+
const currentPeers = this.mesh?.getPeers?.() || [];
|
|
4281
|
+
if (currentPeers.length > 0) {
|
|
4282
|
+
log.debug('BOOTSTRAP: peer connected during seeding, stopping');
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
for (const peer of this._bootstrapPeers) {
|
|
4287
|
+
// Simple backoff: 5s minimum between attempts to same peer
|
|
4288
|
+
if (Date.now() - peer.lastTry < 5000) continue;
|
|
4289
|
+
peer.lastTry = Date.now();
|
|
4290
|
+
|
|
4291
|
+
// Fire-and-forget with 5s timeout
|
|
4292
|
+
this._connectWithTimeout(peer.endpoint, 5_000)
|
|
4293
|
+
.then(() => {
|
|
4294
|
+
log.info(`BOOTSTRAP: ✓ seeded from ${peer.endpoint}`);
|
|
4295
|
+
peer.failures = 0;
|
|
4296
|
+
})
|
|
4297
|
+
.catch(() => {
|
|
4298
|
+
peer.failures++;
|
|
4299
|
+
log.debug(`BOOTSTRAP: ${peer.endpoint} unreachable (attempt ${peer.failures})`);
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
/**
|
|
4305
|
+
* Connect to a peer with an explicit timeout.
|
|
4306
|
+
* Rejects if the connection hasn't completed within `ms` milliseconds,
|
|
4307
|
+
* instead of waiting for the OS TCP timeout (21-30s on Windows).
|
|
4308
|
+
*/
|
|
4309
|
+
_connectWithTimeout(endpoint, ms) {
|
|
4310
|
+
return new Promise((resolve, reject) => {
|
|
4311
|
+
let settled = false;
|
|
4312
|
+
const timer = setTimeout(() => {
|
|
4313
|
+
if (!settled) {
|
|
4314
|
+
settled = true;
|
|
4315
|
+
reject(new Error(`timeout after ${ms}ms`));
|
|
4316
|
+
}
|
|
4317
|
+
}, ms);
|
|
4318
|
+
|
|
4319
|
+
this.mesh.connect(endpoint)
|
|
4320
|
+
.then((result) => {
|
|
4321
|
+
if (!settled) {
|
|
4322
|
+
settled = true;
|
|
4323
|
+
clearTimeout(timer);
|
|
4324
|
+
resolve(result);
|
|
4325
|
+
}
|
|
4326
|
+
})
|
|
4327
|
+
.catch((err) => {
|
|
4328
|
+
if (!settled) {
|
|
4329
|
+
settled = true;
|
|
4330
|
+
clearTimeout(timer);
|
|
4331
|
+
reject(err);
|
|
4332
|
+
}
|
|
4333
|
+
});
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
/**
|
|
4338
|
+
* SHERPA Auto-Connect: Automatically connect to peers discovered via beacon crawling.
|
|
4339
|
+
*
|
|
4340
|
+
* This is the missing link that makes SHERPA a complete discovery+connection system.
|
|
4341
|
+
* When crawl-complete fires, we check discovered peers for wsEndpoints we're not
|
|
4342
|
+
* already connected to, and initiate OUTBOUND WebSocket connections.
|
|
4343
|
+
*
|
|
4344
|
+
* This solves the firewall problem: nodes that can't receive inbound connections
|
|
4345
|
+
* (e.g., behind shared hosting firewalls) discover peers via HTTP beacons (port 443)
|
|
4346
|
+
* and OUTBOUND connect to them. WebSocket is bidirectional once established.
|
|
4347
|
+
*/
|
|
4348
|
+
async _sherpaAutoConnect() {
|
|
4349
|
+
if (!this.sherpa || !this.mesh) return;
|
|
4350
|
+
|
|
4351
|
+
const candidates = this.sherpa.getConnectionCandidates(10);
|
|
4352
|
+
const currentPeers = new Set(this.mesh.getPeers().map(p => p.nodeId));
|
|
4353
|
+
const selfNodeId = this.identity.identity.nodeId;
|
|
4354
|
+
|
|
4355
|
+
for (const candidate of candidates) {
|
|
4356
|
+
// Skip self and already-connected peers
|
|
4357
|
+
if (candidate.nodeId === selfNodeId) continue;
|
|
4358
|
+
if (currentPeers.has(candidate.nodeId)) continue;
|
|
4359
|
+
|
|
4360
|
+
// Try WebSocket first (preferred — full duplex)
|
|
4361
|
+
if (candidate.wsEndpoint) {
|
|
4362
|
+
try {
|
|
4363
|
+
log.info(`SHERPA auto-connect WS → ${candidate.wsEndpoint} (${peerTag(candidate.nodeId)})`);
|
|
4364
|
+
await this.mesh.connect(candidate.wsEndpoint);
|
|
4365
|
+
this.sherpa.markConnected(candidate.nodeId);
|
|
4366
|
+
log.info(`SHERPA auto-connect ✓ ${peerTag(candidate.nodeId)} via WS`);
|
|
4367
|
+
continue; // Success — no need for relay fallback
|
|
4368
|
+
} catch (e) {
|
|
4369
|
+
log.debug(`SHERPA WS failed: ${candidate.wsEndpoint} — ${e.message}`);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
// Fall back to HTTP relay (half-duplex, firewall traversal)
|
|
4374
|
+
if (candidate.relayEndpoint) {
|
|
4375
|
+
try {
|
|
4376
|
+
log.info(`SHERPA relay register → ${candidate.relayEndpoint} (${peerTag(candidate.nodeId)})`);
|
|
4377
|
+
await this._registerWithRelay(candidate);
|
|
4378
|
+
log.info(`SHERPA relay registered ✓ ${peerTag(candidate.nodeId)}`);
|
|
4379
|
+
} catch (e) {
|
|
4380
|
+
this.sherpa.markDisconnected(candidate.nodeId);
|
|
4381
|
+
log.debug(`SHERPA relay failed: ${candidate.relayEndpoint} — ${e.message}`);
|
|
4382
|
+
}
|
|
4383
|
+
} else {
|
|
4384
|
+
// Neither WS nor relay available
|
|
4385
|
+
this.sherpa.markDisconnected(candidate.nodeId);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
/**
|
|
4391
|
+
* Register with a peer's HTTP relay endpoint for store-and-forward messaging.
|
|
4392
|
+
* Starts periodic polling to pull inbound messages.
|
|
4393
|
+
*/
|
|
4394
|
+
async _registerWithRelay(candidate) {
|
|
4395
|
+
const relayUrl = candidate.relayEndpoint;
|
|
4396
|
+
const selfNodeId = this.identity.identity.nodeId;
|
|
4397
|
+
|
|
4398
|
+
// Register with the relay via same POST /mesh/relay endpoint (action: 'register')
|
|
4399
|
+
// This works through the PHP bridge which only proxies POST to /mesh/relay
|
|
4400
|
+
// ML-DSA-65 signed registration — relay receiver verifies before accepting
|
|
4401
|
+
const regPayload = {
|
|
4402
|
+
action: 'register',
|
|
4403
|
+
nodeId: selfNodeId,
|
|
4404
|
+
networkName: this.genesisNetwork?.networkName,
|
|
4405
|
+
publicKey: this.identity.identity.publicKey,
|
|
4406
|
+
timestamp: Date.now(),
|
|
4407
|
+
};
|
|
4408
|
+
const regSignature = this.identity.sign(JSON.stringify({
|
|
4409
|
+
action: regPayload.action,
|
|
4410
|
+
nodeId: regPayload.nodeId,
|
|
4411
|
+
networkName: regPayload.networkName,
|
|
4412
|
+
timestamp: regPayload.timestamp,
|
|
4413
|
+
}));
|
|
4414
|
+
regPayload.signature = regSignature;
|
|
4415
|
+
|
|
4416
|
+
const resp = await fetch(relayUrl, {
|
|
4417
|
+
method: 'POST',
|
|
4418
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4419
|
+
body: JSON.stringify(regPayload),
|
|
4420
|
+
signal: AbortSignal.timeout(10000),
|
|
4421
|
+
});
|
|
4422
|
+
|
|
4423
|
+
if (!resp.ok) throw new Error(`Relay register HTTP ${resp.status}`);
|
|
4424
|
+
|
|
4425
|
+
// Learn the remote node's actual nodeId from the registration response
|
|
4426
|
+
const regResult = await resp.json();
|
|
4427
|
+
if (regResult.nodeId && candidate.nodeId.startsWith('relay-')) {
|
|
4428
|
+
candidate.nodeId = regResult.nodeId;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// Store remote node's public key for signature verification (gossip, ANNEX)
|
|
4432
|
+
// Without this, relay-only peers can't verify each other's rumor signatures
|
|
4433
|
+
if (regResult.publicKey && regResult.nodeId) {
|
|
4434
|
+
if (!this.mesh._relayPeerKeys) this.mesh._relayPeerKeys = new Map();
|
|
4435
|
+
this.mesh._relayPeerKeys.set(regResult.nodeId, regResult.publicKey);
|
|
4436
|
+
if (this.sherpa) {
|
|
4437
|
+
this.sherpa.registry.upsert({
|
|
4438
|
+
nodeId: regResult.nodeId,
|
|
4439
|
+
publicKey: regResult.publicKey,
|
|
4440
|
+
capabilities: { httpRelay: true },
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
// Start polling for inbound messages if not already polling
|
|
4446
|
+
if (!this._relayPollers) this._relayPollers = new Map();
|
|
4447
|
+
if (!this._relayEndpoints) this._relayEndpoints = new Map();
|
|
4448
|
+
|
|
4449
|
+
if (!this._relayPollers.has(candidate.nodeId)) {
|
|
4450
|
+
const pollInterval = setInterval(async () => {
|
|
4451
|
+
try {
|
|
4452
|
+
await this._pollRelay(candidate);
|
|
4453
|
+
} catch (e) {
|
|
4454
|
+
log.debug(`Relay poll error ${peerTag(candidate.nodeId)}: ${e.message}`);
|
|
4455
|
+
}
|
|
4456
|
+
}, 30000); // Poll every 30 seconds
|
|
4457
|
+
|
|
4458
|
+
this._relayPollers.set(candidate.nodeId, pollInterval);
|
|
4459
|
+
this._relayEndpoints.set(candidate.nodeId, relayUrl);
|
|
4460
|
+
this.sherpa.markConnected(candidate.nodeId);
|
|
4461
|
+
log.warn(`Relay peer ${peerTag(candidate.nodeId)} connected via HTTP polling (30s cadence)`);
|
|
4462
|
+
log.warn(` ⚠ Relay connections have reduced throughput & higher latency vs direct WebSocket`);
|
|
4463
|
+
log.warn(` ⚠ This is a firewall-traversal fallback — useful for emergency mesh connectivity`);
|
|
4464
|
+
log.warn(` ⚠ Gossip propagation, ANNEX encryption, and consensus still function but may lag`);
|
|
4465
|
+
|
|
4466
|
+
// Also do an immediate poll
|
|
4467
|
+
await this._pollRelay(candidate);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
/**
|
|
4472
|
+
* Poll a relay endpoint for inbound messages.
|
|
4473
|
+
*/
|
|
4474
|
+
async _pollRelay(candidate) {
|
|
4475
|
+
const selfNodeId = this.identity.identity.nodeId;
|
|
4476
|
+
const relayUrl = candidate.relayEndpoint;
|
|
4477
|
+
|
|
4478
|
+
// Send any queued outbound messages and receive inbound
|
|
4479
|
+
const outbound = this._drainRelayOutbox(candidate.nodeId);
|
|
4480
|
+
|
|
4481
|
+
// ML-DSA-65 signed batch — relay receiver verifies before processing
|
|
4482
|
+
const batchPayload = { messages: outbound, senderNodeId: selfNodeId };
|
|
4483
|
+
const batchSignature = this.identity.sign(JSON.stringify(batchPayload));
|
|
4484
|
+
|
|
4485
|
+
const resp = await fetch(relayUrl, {
|
|
4486
|
+
method: 'POST',
|
|
4487
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4488
|
+
body: JSON.stringify({
|
|
4489
|
+
...batchPayload,
|
|
4490
|
+
signature: batchSignature,
|
|
4491
|
+
publicKey: this.identity.identity.publicKey,
|
|
4492
|
+
}),
|
|
4493
|
+
signal: AbortSignal.timeout(15000),
|
|
4494
|
+
});
|
|
4495
|
+
|
|
4496
|
+
if (!resp.ok) throw new Error(`Relay poll HTTP ${resp.status}`);
|
|
4497
|
+
|
|
4498
|
+
const data = await resp.json();
|
|
4499
|
+
|
|
4500
|
+
// Learn/refresh remote node's public key from poll response
|
|
4501
|
+
// Ensures relay-only peers can verify each other's gossip signatures
|
|
4502
|
+
if (data.publicKey && data.nodeId) {
|
|
4503
|
+
if (!this.mesh._relayPeerKeys) this.mesh._relayPeerKeys = new Map();
|
|
4504
|
+
this.mesh._relayPeerKeys.set(data.nodeId, data.publicKey);
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
// Process inbound messages from relay
|
|
4508
|
+
if (data.outbound && Array.isArray(data.outbound)) {
|
|
4509
|
+
for (const msg of data.outbound) {
|
|
4510
|
+
try {
|
|
4511
|
+
// Dispatch by msg.type (e.g., 'gossip', 'hello') — not 'message'
|
|
4512
|
+
if (msg && msg.type) {
|
|
4513
|
+
this.mesh.emit(msg.type, msg, null, candidate.nodeId);
|
|
4514
|
+
// Route ANNEX messages arriving via relay
|
|
4515
|
+
if (msg.annex && this.mesh.annex) {
|
|
4516
|
+
this.mesh.annex._handleAnnexMessage(msg.annex, candidate.nodeId).catch(() => { });
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
} catch (e) {
|
|
4520
|
+
log.debug(`Relay message process error: ${e.message}`);
|
|
4521
|
+
}
|
|
1667
4522
|
}
|
|
1668
4523
|
}
|
|
1669
4524
|
}
|
|
1670
4525
|
|
|
4526
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4527
|
+
// HTTP Relay Outbox — Store-and-forward messages for HTTP relay peers
|
|
4528
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4529
|
+
|
|
4530
|
+
/**
|
|
4531
|
+
* Queue a message for delivery via HTTP relay to a specific node.
|
|
4532
|
+
* Used when no WebSocket connection exists but the peer has registered
|
|
4533
|
+
* an HTTP relay endpoint via SHERPA.
|
|
4534
|
+
*/
|
|
4535
|
+
_queueRelayMessage(targetNodeId, message) {
|
|
4536
|
+
if (!this._relayOutbox) this._relayOutbox = new Map();
|
|
4537
|
+
|
|
4538
|
+
let queue = this._relayOutbox.get(targetNodeId);
|
|
4539
|
+
if (!queue) {
|
|
4540
|
+
queue = [];
|
|
4541
|
+
this._relayOutbox.set(targetNodeId, queue);
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
queue.push({ ...message, _relayTs: Date.now() });
|
|
4545
|
+
|
|
4546
|
+
// Cap at 500 messages per peer, evict oldest
|
|
4547
|
+
if (queue.length > 500) {
|
|
4548
|
+
queue.splice(0, queue.length - 500);
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
/**
|
|
4553
|
+
* Drain (retrieve and clear) outbox messages for a specific relay peer.
|
|
4554
|
+
* Called when the peer polls via GET /mesh/relay/:nodeId or during
|
|
4555
|
+
* bi-directional POST /mesh/relay exchange.
|
|
4556
|
+
*/
|
|
4557
|
+
_drainRelayOutbox(targetNodeId) {
|
|
4558
|
+
if (!this._relayOutbox) return [];
|
|
4559
|
+
const queue = this._relayOutbox.get(targetNodeId);
|
|
4560
|
+
if (!queue || queue.length === 0) return [];
|
|
4561
|
+
|
|
4562
|
+
// Drain and return
|
|
4563
|
+
const messages = [...queue];
|
|
4564
|
+
queue.length = 0;
|
|
4565
|
+
return messages;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
1671
4568
|
async _initAdapter() {
|
|
1672
4569
|
try {
|
|
1673
4570
|
// Dynamic import of PeerQuanta integration
|
|
@@ -1679,18 +4576,18 @@ export class YakmeshNode {
|
|
|
1679
4576
|
this,
|
|
1680
4577
|
this.config.peerquanta.phpbbDatabase
|
|
1681
4578
|
);
|
|
1682
|
-
|
|
4579
|
+
|
|
1683
4580
|
await this.adapter.init();
|
|
1684
|
-
|
|
4581
|
+
|
|
1685
4582
|
// Register PeerQuanta API endpoints on existing HTTP app
|
|
1686
4583
|
if (this.app && createAdapterEndpoints) {
|
|
1687
4584
|
createAdapterEndpoints(this.app, this.adapter);
|
|
1688
4585
|
}
|
|
1689
|
-
|
|
4586
|
+
|
|
1690
4587
|
if (this.config.peerquanta.syncInterval) {
|
|
1691
4588
|
this.adapter.startSync(this.config.peerquanta.syncInterval);
|
|
1692
4589
|
}
|
|
1693
|
-
|
|
4590
|
+
|
|
1694
4591
|
log.info('✓ PeerQuanta integration enabled');
|
|
1695
4592
|
} catch (error) {
|
|
1696
4593
|
log.error('Failed to initialize PeerQuanta:', { error: error.message });
|
|
@@ -1703,19 +4600,19 @@ export class YakmeshNode {
|
|
|
1703
4600
|
async _initWebsiteAdapter() {
|
|
1704
4601
|
try {
|
|
1705
4602
|
const { default: WebsiteAdapter } = await import('../adapters/adapter-website/index.js');
|
|
1706
|
-
|
|
4603
|
+
|
|
1707
4604
|
// Get source directory from config or default
|
|
1708
4605
|
const sourceDir = this.config.website?.sourceDir || '../website';
|
|
1709
|
-
|
|
4606
|
+
|
|
1710
4607
|
this.websiteAdapter = new WebsiteAdapter(this, {
|
|
1711
4608
|
sourceDir,
|
|
1712
4609
|
cacheDir: './data/websites',
|
|
1713
4610
|
mountPath: '/site',
|
|
1714
4611
|
yakDomains: true,
|
|
1715
4612
|
});
|
|
1716
|
-
|
|
4613
|
+
|
|
1717
4614
|
await this.websiteAdapter.init();
|
|
1718
|
-
|
|
4615
|
+
|
|
1719
4616
|
// Register the yakmesh.yak domain if website exists
|
|
1720
4617
|
if (this.websiteAdapter.manifests.size > 0) {
|
|
1721
4618
|
const firstManifest = this.websiteAdapter.manifests.values().next().value;
|
|
@@ -1727,7 +4624,7 @@ export class YakmeshNode {
|
|
|
1727
4624
|
}
|
|
1728
4625
|
}
|
|
1729
4626
|
}
|
|
1730
|
-
|
|
4627
|
+
|
|
1731
4628
|
log.info('✓ Website Adapter enabled');
|
|
1732
4629
|
log.info(` Site: http://localhost:${this.boundHttpPort}/site/`);
|
|
1733
4630
|
} catch (error) {
|
|
@@ -1747,15 +4644,15 @@ export class YakmeshNode {
|
|
|
1747
4644
|
port: this.boundHttpPort || this.config.network.httpPort,
|
|
1748
4645
|
nodePath: process.cwd(),
|
|
1749
4646
|
});
|
|
1750
|
-
|
|
4647
|
+
|
|
1751
4648
|
// Register protocol endpoints on Express app
|
|
1752
4649
|
createProtocolEndpoints(this.app, this.protocolHandler);
|
|
1753
|
-
|
|
4650
|
+
|
|
1754
4651
|
// Auto-register protocol if configured
|
|
1755
4652
|
if (this.config.protocol?.autoRegister) {
|
|
1756
4653
|
await this.protocolHandler.register();
|
|
1757
4654
|
}
|
|
1758
|
-
|
|
4655
|
+
|
|
1759
4656
|
log.info('✓ YAK:// Protocol handler initialized');
|
|
1760
4657
|
} catch (error) {
|
|
1761
4658
|
// Protocol handler is optional
|
|
@@ -1767,18 +4664,18 @@ export class YakmeshNode {
|
|
|
1767
4664
|
// Run if executed directly (works on Windows and Unix)
|
|
1768
4665
|
import { fileURLToPath } from 'url';
|
|
1769
4666
|
const __filename = fileURLToPath(import.meta.url);
|
|
1770
|
-
const isMainModule = process.argv[1] === __filename ||
|
|
1771
|
-
|
|
4667
|
+
const isMainModule = process.argv[1] === __filename ||
|
|
4668
|
+
process.argv[1]?.replace(/\\/g, '/') === __filename.replace(/\\/g, '/');
|
|
1772
4669
|
if (isMainModule) {
|
|
1773
4670
|
const config = await loadConfig();
|
|
1774
4671
|
const node = new YakmeshNode(config);
|
|
1775
|
-
|
|
4672
|
+
|
|
1776
4673
|
// Handle shutdown
|
|
1777
4674
|
process.on('SIGINT', async () => {
|
|
1778
4675
|
await node.stop();
|
|
1779
4676
|
process.exit(0);
|
|
1780
4677
|
});
|
|
1781
|
-
|
|
4678
|
+
|
|
1782
4679
|
await node.start();
|
|
1783
4680
|
}
|
|
1784
4681
|
|