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.
Files changed (232) hide show
  1. package/CHANGELOG.md +637 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/Caddyfile +77 -0
  4. package/README.md +119 -29
  5. package/adapters/adapter-mlv-bible/README.md +124 -0
  6. package/adapters/adapter-mlv-bible/index.js +400 -0
  7. package/adapters/chat-mod-adapter.js +532 -0
  8. package/adapters/content-adapter.js +273 -0
  9. package/content/api.js +50 -41
  10. package/content/index.js +2 -2
  11. package/content/store.js +355 -173
  12. package/dashboard/index.html +19 -3
  13. package/database/replication.js +117 -37
  14. package/docs/CRYPTO-AGILITY.md +204 -0
  15. package/docs/MTLS-RESEARCH.md +367 -0
  16. package/docs/NAMCHE-SPEC.md +681 -0
  17. package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
  18. package/docs/PRECISION-DISCLOSURE.md +96 -0
  19. package/docs/README.md +76 -0
  20. package/docs/ROADMAP-2.4.0.md +447 -0
  21. package/docs/ROADMAP-2.5.0.md +244 -0
  22. package/docs/SECURITY-AUDIT-REPORT.md +306 -0
  23. package/docs/SST-INTEGRATION.md +712 -0
  24. package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
  25. package/docs/TERNARY-AUDIT-REPORT.md +247 -0
  26. package/docs/TME-FAQ.md +221 -0
  27. package/docs/WHITEPAPER.md +623 -0
  28. package/docs/adapters.html +1001 -0
  29. package/docs/advanced-systems.html +1045 -0
  30. package/docs/annex.html +1046 -0
  31. package/docs/api.html +970 -0
  32. package/docs/business/response-templates.md +160 -0
  33. package/docs/c2c.html +1225 -0
  34. package/docs/cli.html +1332 -0
  35. package/docs/configuration.html +1248 -0
  36. package/docs/darshan.html +1085 -0
  37. package/docs/dharma.html +966 -0
  38. package/docs/docs-bundle.html +1075 -0
  39. package/docs/docs.css +3120 -0
  40. package/docs/docs.js +556 -0
  41. package/docs/doko.html +969 -0
  42. package/docs/geo-proof.html +858 -0
  43. package/docs/getting-started.html +840 -0
  44. package/docs/gumba-tutorial.html +1144 -0
  45. package/docs/gumba.html +1098 -0
  46. package/docs/index.html +914 -0
  47. package/docs/jhilke.html +1312 -0
  48. package/docs/karma.html +1100 -0
  49. package/docs/katha.html +1037 -0
  50. package/docs/lama.html +978 -0
  51. package/docs/mandala.html +1067 -0
  52. package/docs/mani.html +964 -0
  53. package/docs/mantra.html +967 -0
  54. package/docs/mesh.html +1409 -0
  55. package/docs/nakpak.html +869 -0
  56. package/docs/namche.html +928 -0
  57. package/docs/nav-order.json +53 -0
  58. package/docs/prahari.html +1043 -0
  59. package/docs/prism-bash.min.js +1 -0
  60. package/docs/prism-javascript.min.js +1 -0
  61. package/docs/prism-json.min.js +1 -0
  62. package/docs/prism-tomorrow.min.css +1 -0
  63. package/docs/prism.min.js +1 -0
  64. package/docs/privacy.html +699 -0
  65. package/docs/quick-reference.html +1181 -0
  66. package/docs/sakshi.html +1402 -0
  67. package/docs/sandboxing.md +386 -0
  68. package/docs/seva.html +911 -0
  69. package/docs/sherpa.html +871 -0
  70. package/docs/studio.html +860 -0
  71. package/docs/stupa.html +995 -0
  72. package/docs/tailwind.min.css +2 -0
  73. package/docs/tattva.html +1332 -0
  74. package/docs/terms.html +686 -0
  75. package/docs/time-server-deployment.md +166 -0
  76. package/docs/time-sources.html +1392 -0
  77. package/docs/tivra.html +1127 -0
  78. package/docs/trademark-policy.html +686 -0
  79. package/docs/tribhuj.html +1183 -0
  80. package/docs/trust-security.html +1029 -0
  81. package/docs/tutorials/backup-recovery.html +654 -0
  82. package/docs/tutorials/dashboard.html +604 -0
  83. package/docs/tutorials/domain-setup.html +605 -0
  84. package/docs/tutorials/host-website.html +456 -0
  85. package/docs/tutorials/mesh-network.html +505 -0
  86. package/docs/tutorials/mobile-access.html +445 -0
  87. package/docs/tutorials/privacy.html +467 -0
  88. package/docs/tutorials/raspberry-pi.html +600 -0
  89. package/docs/tutorials/security-basics.html +539 -0
  90. package/docs/tutorials/share-files.html +431 -0
  91. package/docs/tutorials/troubleshooting.html +637 -0
  92. package/docs/tutorials/trust-karma.html +419 -0
  93. package/docs/tutorials/yak-protocol.html +456 -0
  94. package/docs/tutorials.html +1034 -0
  95. package/docs/vani.html +1270 -0
  96. package/docs/webserver.html +809 -0
  97. package/docs/yak-protocol.html +940 -0
  98. package/docs/yak-timeserver-design.md +475 -0
  99. package/docs/yakapp.html +1015 -0
  100. package/docs/ypc27.html +1069 -0
  101. package/docs/yurt.html +1344 -0
  102. package/embedded-docs/bundle.js +334 -74
  103. package/gossip/protocol.js +247 -27
  104. package/identity/key-resolver.js +262 -0
  105. package/identity/machine-seed.js +632 -0
  106. package/identity/node-key.js +669 -368
  107. package/identity/tribhuj-ratchet.js +506 -0
  108. package/knowledge-base.js +37 -8
  109. package/launcher/yakmesh.bat +62 -0
  110. package/launcher/yakmesh.sh +70 -0
  111. package/mesh/annex.js +462 -108
  112. package/mesh/beacon-broadcast.js +113 -1
  113. package/mesh/darshan.js +1718 -0
  114. package/mesh/gumba.js +1567 -0
  115. package/mesh/jhilke.js +651 -0
  116. package/mesh/katha.js +1012 -0
  117. package/mesh/nakpak-routing.js +8 -5
  118. package/mesh/network.js +724 -34
  119. package/mesh/pulse-sync.js +4 -1
  120. package/mesh/rate-limiter.js +127 -15
  121. package/mesh/seva.js +526 -0
  122. package/mesh/sherpa-discovery.js +89 -8
  123. package/mesh/sybil-defense.js +19 -5
  124. package/mesh/temporal-encoder.js +4 -3
  125. package/mesh/vani.js +1364 -0
  126. package/mesh/yurt.js +1340 -0
  127. package/models/entropy-sentinel.onnx +0 -0
  128. package/models/karma-trust.onnx +0 -0
  129. package/models/manifest.json +43 -0
  130. package/models/sakshi-anomaly.onnx +0 -0
  131. package/oracle/code-proof-protocol.js +7 -6
  132. package/oracle/codebase-lock.js +257 -28
  133. package/oracle/index.js +74 -15
  134. package/oracle/ma902-snmp.js +678 -0
  135. package/oracle/module-sealer.js +5 -3
  136. package/oracle/network-identity.js +16 -0
  137. package/oracle/packet-checksum.js +201 -0
  138. package/oracle/sst.js +579 -0
  139. package/oracle/ternary-144t.js +714 -0
  140. package/oracle/ternary-ml.js +481 -0
  141. package/oracle/time-api.js +239 -0
  142. package/oracle/time-source.js +137 -47
  143. package/oracle/validation-oracle-hardened.js +1111 -1071
  144. package/oracle/validation-oracle.js +4 -2
  145. package/oracle/ypc27.js +211 -0
  146. package/package.json +20 -3
  147. package/protocol/yak-handler.js +35 -9
  148. package/protocol/yak-protocol.js +28 -13
  149. package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
  150. package/reference/cpp/yakmesh_ypc27.cpp +179 -0
  151. package/sbom.json +87 -0
  152. package/scripts/security-audit.mjs +264 -0
  153. package/scripts/update-docs-nav.js +194 -0
  154. package/scripts/update-docs-sidebar.cjs +164 -0
  155. package/security/crypto-config.js +4 -3
  156. package/security/dharma-moderation.js +517 -0
  157. package/security/doko-identity.js +193 -143
  158. package/security/domain-consensus.js +86 -85
  159. package/security/fs-hardening.js +620 -0
  160. package/security/hardware-attestation.js +5 -3
  161. package/security/hybrid-trust.js +227 -87
  162. package/security/karma-rate-limiter.js +692 -0
  163. package/security/khata-protocol.js +22 -21
  164. package/security/khata-trust-integration.js +277 -150
  165. package/security/memory-safety.js +635 -0
  166. package/security/mesh-auth.js +11 -10
  167. package/security/mesh-revocation.js +373 -5
  168. package/security/namche-gateway.js +298 -69
  169. package/security/sakshi.js +460 -3
  170. package/security/sangha.js +770 -0
  171. package/security/secure-config.js +473 -0
  172. package/security/silicon-parity.js +13 -10
  173. package/security/steadywatch.js +1142 -0
  174. package/security/strike-system.js +32 -3
  175. package/security/temporal-signing.js +488 -0
  176. package/security/trit-commitment.js +464 -0
  177. package/server/crypto/annex.js +247 -0
  178. package/server/darshan-api.js +343 -0
  179. package/server/index.js +3259 -362
  180. package/server/komm-api.js +668 -0
  181. package/utils/accel.js +2273 -0
  182. package/utils/ternary-id.js +79 -0
  183. package/utils/verify-worker.js +57 -0
  184. package/webserver/index.js +95 -5
  185. package/assets/yakmesh-logo.png +0 -0
  186. package/assets/yakmesh-logo.svg +0 -80
  187. package/assets/yakmesh-logo2.png +0 -0
  188. package/assets/yakmesh-logo2sm.png +0 -0
  189. package/assets/ymsm.png +0 -0
  190. package/website/assets/silhouettes/adapters.svg +0 -107
  191. package/website/assets/silhouettes/api-endpoints.svg +0 -115
  192. package/website/assets/silhouettes/atomic-clock.svg +0 -83
  193. package/website/assets/silhouettes/base-camp.svg +0 -81
  194. package/website/assets/silhouettes/bridge.svg +0 -69
  195. package/website/assets/silhouettes/docs-bundle.svg +0 -113
  196. package/website/assets/silhouettes/doko-basket.svg +0 -70
  197. package/website/assets/silhouettes/fortress.svg +0 -93
  198. package/website/assets/silhouettes/gateway.svg +0 -54
  199. package/website/assets/silhouettes/gears.svg +0 -93
  200. package/website/assets/silhouettes/globe-satellite.svg +0 -67
  201. package/website/assets/silhouettes/karma-wheel.svg +0 -137
  202. package/website/assets/silhouettes/lama-council.svg +0 -141
  203. package/website/assets/silhouettes/mandala-network.svg +0 -169
  204. package/website/assets/silhouettes/mani-stones.svg +0 -149
  205. package/website/assets/silhouettes/mantra-wheel.svg +0 -116
  206. package/website/assets/silhouettes/mesh-nodes.svg +0 -113
  207. package/website/assets/silhouettes/nakpak.svg +0 -56
  208. package/website/assets/silhouettes/peak-lightning.svg +0 -73
  209. package/website/assets/silhouettes/sherpa.svg +0 -69
  210. package/website/assets/silhouettes/stupa-tower.svg +0 -119
  211. package/website/assets/silhouettes/tattva-eye.svg +0 -78
  212. package/website/assets/silhouettes/terminal.svg +0 -74
  213. package/website/assets/silhouettes/webserver.svg +0 -145
  214. package/website/assets/silhouettes/yak.svg +0 -78
  215. package/website/assets/yakmesh-logo.png +0 -0
  216. package/website/assets/yakmesh-logo.webp +0 -0
  217. package/website/assets/yakmesh-logo128x140.webp +0 -0
  218. package/website/assets/yakmesh-logo2.png +0 -0
  219. package/website/assets/yakmesh-logo2.svg +0 -51
  220. package/website/assets/yakmesh-logo40x44.webp +0 -0
  221. package/website/assets/yakmesh.gif +0 -0
  222. package/website/assets/yakmesh.ico +0 -0
  223. package/website/assets/yakmesh.jpg +0 -0
  224. package/website/assets/yakmesh.pdf +0 -0
  225. package/website/assets/yakmesh.png +0 -0
  226. package/website/assets/yakmesh.svg +0 -70
  227. package/website/assets/yakmesh128.webp +0 -0
  228. package/website/assets/yakmesh32.png +0 -0
  229. package/website/assets/yakmesh32.svg +0 -65
  230. package/website/assets/yakmesh32o.ico +0 -2
  231. package/website/assets/yakmesh32o.svg +0 -65
  232. 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 - Autonomous Network Negotiated Encrypted eXchange
32
- import { Annex } from '../mesh/annex.js';
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(`📋 Using config: ${configPath}`);
252
+ log.info(`📋 Config source: CLI --config ${configPath}`);
146
253
  }
147
-
148
- // Try to load config file
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
- return { ...DEFAULT_CONFIG, ...userConfig };
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
- // Fallback to default yakmesh.config.js
160
- if (existsSync('./yakmesh.config.js')) {
161
- const { default: userConfig } = await import('../yakmesh.config.js');
162
- return { ...DEFAULT_CONFIG, ...userConfig };
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
- return DEFAULT_CONFIG;
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 - encrypted point-to-point messaging
191
- this.annex = null;
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.slice(0, 16)}...`);
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
- // Handle Annex (encrypted direct messages) - separate from gossip
306
- this.mesh.on('annex', (data, origin) => {
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. Initialize Annex for encrypted point-to-point messaging
320
- this.annex = new Annex({
321
- identity: this.identity,
322
- mesh: this.mesh,
323
- });
324
- log.info('✓ Annex channel initialized (encrypted P2P messaging)');
325
-
326
- // 5c. Initialize SHERPA for decentralized peer discovery
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: true,
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
- // 7. Connect to bootstrap nodes
355
- await this._connectToBootstrap();
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.annex = null; // Clear annex channels
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 !== results.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
- * Handle oracle-validated content from peers
1005
+ * Start periodic time heartbeat gossip.
1006
+ * Called once after gossip + timeSource are both initialized.
544
1007
  */
545
- _handleOracleContent(data, origin) {
546
- const { sealedPackage, attestations } = data;
547
-
548
- // Verify the peer is running valid code
549
- if (!this.codeProof.isPeerVerified(origin)) {
550
- log.warn(`⚠️ Received content from unverified peer ${origin.slice(0, 16)}...`);
551
- // Challenge the peer
552
- const challenge = this.codeProof.generateChallenge(origin);
553
- this.gossip.spreadRumor('code_proof_challenge', challenge);
554
- return;
555
- }
556
-
557
- // Submit to consensus engine
558
- const result = this.consensus.receivePackage(data);
559
-
560
- if (result.accepted) {
561
- log.info(`✓ Oracle content accepted: ${result.contentHash?.slice(0, 16)}...`);
562
-
563
- // Record in replication for persistence
564
- this.replication.recordChange(
565
- `oracle_${sealedPackage.type}`,
566
- sealedPackage.contentHash,
567
- 'UPSERT',
568
- sealedPackage.content
569
- );
570
- } else {
571
- log.warn(`✗ Oracle content rejected: ${result.reason}`);
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 iO network handshake from peer
577
- * Verifies network compatibility using fingerprints (no hash exposed)
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
- _handleNetworkHandshake(data, origin) {
580
- if (!this.genesisNetwork) return;
581
-
582
- const { handshake, nodeId } = data;
583
- const verification = this.genesisNetwork.verifyHandshake(handshake);
584
-
585
- // Register the peer
586
- const compatible = this.genesisNetwork.registerPeer(nodeId || origin, handshake);
587
-
588
- if (compatible) {
589
- log.debug(`🌐 Peer ${origin.slice(0, 16)}... verified on same network: ${handshake.name}`);
590
- } else {
591
- log.debug(`⚠️ Peer ${origin.slice(0, 16)}... on different network: ${handshake.name} (${handshake.shortId})`);
592
- log.debug(` Our network: ${this.genesisNetwork.networkName} (${this.genesisNetwork.networkId})`);
593
- }
594
-
595
- // Optionally broadcast our handshake back
596
- if (compatible && !data.isResponse) {
597
- this.gossip.spreadRumor('network_handshake', {
598
- handshake: this.genesisNetwork.createHandshake(),
599
- nodeId: this.identity.identity.nodeId,
600
- isResponse: true,
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
- async _startHttpServer() {
606
- const app = express();
607
- this.app = app; // Store for PeerQuanta endpoints
608
-
609
- // Enable strict routing: /docs and /docs/ are different routes
610
- app.set('strict routing', true);
611
-
612
- app.use(express.json({ limit: '1mb' })); // Limit payload size
613
-
614
- // =========================================
615
- // SECURITY: Rate Limiting (DoS Protection)
616
- // =========================================
617
-
618
- // General rate limit: 100 requests per minute per IP
619
- const generalLimiter = rateLimit({
620
- windowMs: 60 * 1000, // 1 minute
621
- max: 100,
622
- message: { error: 'Too many requests, please try again later' },
623
- standardHeaders: true,
624
- legacyHeaders: false,
625
- });
626
-
627
- // Strict rate limit for write operations: 20 per minute
628
- const writeLimiter = rateLimit({
629
- windowMs: 60 * 1000,
630
- max: 20,
631
- message: { error: 'Too many write requests, please slow down' },
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 for dashboard
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
- res.header('Access-Control-Allow-Origin', '*');
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
- peers: this.mesh.getPeers().length,
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.slice(0, 16)}...`
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
- app.post('/oracle/resolve', (req, res) => {
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.1',
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
- ...phaseConfig,
1228
- activePhaseConfig: activeConfig,
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
- // Get time source capabilities
1233
- app.get('/time/capabilities', (req, res) => {
1234
- if (!this.timeSource) {
1235
- return res.status(503).json({ error: 'Time source detector not initialized' });
1236
- }
3585
+ // =========================================
3586
+ // SANGHA Collective Status (v3.0)
3587
+ // =========================================
1237
3588
 
1238
- const status = this.timeSource.getStatus();
1239
- const phaseConfig = createPhaseConfig(this.timeSource);
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
- trustLevel: status.trustLevel,
1243
- stratum: status.stratum,
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
- app.post('/security/namche/verify/:gate', (req, res) => {
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
- app.post('/security/doko/verify', (req, res) => {
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.slice(0, 8)}`,
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
- app.post('/geo/landmarks', writeLimiter, (req, res) => {
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
- app.post('/geo/prove', writeLimiter, async (req, res) => {
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.slice(0, 16),
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 is required' });
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
- // In real implementation, this would:
1600
- // 1. Request the node's geo proof via gossip
1601
- // 2. Verify each exclusion zone by checking our own RTT to the same landmarks
1602
- // 3. Confirm the claimed distances are physically possible
1603
-
1604
- // For now, return a placeholder response
1605
- // The real verification happens in khata-trust-integration.js via gossip
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: true,
1609
- nodeId,
1610
- validZones: 0,
1611
- totalZones: 0,
1612
- confidence: 0,
1613
- message: 'Verification requires active gossip network. Use KHATA integration for real-time verification.',
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
- async _connectToBootstrap() {
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
- // Don't connect to ourselves
1661
- if (endpoint.includes(`:${this.config.network.wsPort}`)) continue;
1662
-
1663
- try {
1664
- await this.mesh.connect(endpoint);
1665
- } catch (e) {
1666
- log.debug(` (bootstrap ${endpoint} not available)`);
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
- process.argv[1]?.replace(/\\/g, '/') === __filename.replace(/\\/g, '/');
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