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
@@ -23,12 +23,21 @@
23
23
  * @version 2.6.0
24
24
  */
25
25
 
26
- import { sha3_256 } from '@noble/hashes/sha3.js';
26
+ import { sha3_256 as _nobleSha3 } from '@noble/hashes/sha3.js';
27
27
  import { bytesToHex } from '@noble/hashes/utils.js';
28
28
  import { createLogger } from '../utils/logger.js';
29
29
 
30
+ // 144T ternary addressing for message IDs (eliminates hex "666" patterns)
31
+ import { TritAddress } from '../oracle/ternary-144t.js';
32
+
33
+ // ACCEL: Hardware-accelerated SHA3-256 (OpenSSL/SHA-NI — 4.6x faster)
34
+ import { sha3_256 } from '../utils/accel.js';
35
+
30
36
  const log = createLogger('mantra:protocol');
31
37
 
38
+ /** Extract unique peer suffix from nodeId (e.g. 'node-net-name-pq-kEEU' → 'kEEU') */
39
+ const peerTag = (id) => id?.split('-pq-').pop() || id?.slice?.(-8) || String(id);
40
+
32
41
  // Message types for MANTRA protocol
33
42
  // (Maintains GOSSIP_ prefix for backward compatibility with existing mesh messages)
34
43
  export const MantraMessageType = {
@@ -36,11 +45,11 @@ export const MantraMessageType = {
36
45
  HELLO: 'GOSSIP_HELLO', // Announce self to network (prayer wheel spin)
37
46
  PEERS: 'GOSSIP_PEERS', // Share known peers (community)
38
47
  WANT_PEERS: 'GOSSIP_WANT_PEERS', // Request peer list (seeking guidance)
39
-
48
+
40
49
  // Rumor mongering (MANTRA spreading)
41
50
  RUMOR: 'GOSSIP_RUMOR', // New data to propagate (mantra to spread)
42
51
  SEEN: 'GOSSIP_SEEN', // Acknowledge receipt (mantra received)
43
-
52
+
44
53
  // Anti-entropy (KARMA balance)
45
54
  DIGEST: 'GOSSIP_DIGEST', // Summary of known data (karma digest)
46
55
  DIFF: 'GOSSIP_DIFF', // Missing data request (karma balance)
@@ -89,8 +98,9 @@ class BloomFilter {
89
98
  }
90
99
 
91
100
  // Reset when filter gets too full (false positive rate increases)
101
+ // 70% fill keeps FP rate manageable; 50% was too aggressive (wasted capacity)
92
102
  shouldReset() {
93
- return this.count > this.size * 0.5;
103
+ return this.count > this.size * 0.7;
94
104
  }
95
105
 
96
106
  reset() {
@@ -109,7 +119,14 @@ export class MantraProtocol {
109
119
  constructor(mesh, identity, options = {}) {
110
120
  this.mesh = mesh;
111
121
  this.identity = identity;
112
-
122
+
123
+ // Relay info callback — lets gossip ask the node about active relay endpoints
124
+ // Returns { relayEndpoints: ['https://...'], relayNodeIds: Set } or null
125
+ this._getRelayInfo = options.getRelayInfo || null;
126
+ // Relay connect callback — lets gossip tell the node to register with a relay
127
+ // Signature: (relayEndpoint, nodeId) => Promise<void>
128
+ this._connectRelay = options.connectRelay || null;
129
+
113
130
  // Configuration
114
131
  this.config = {
115
132
  fanout: options.fanout || 3, // Peers to spread mantra to
@@ -125,10 +142,15 @@ export class MantraProtocol {
125
142
  this.knownPeers = new Map(); // nodeId -> { info, lastSeen, endpoint }
126
143
  this.seenMessages = new BloomFilter();
127
144
  this.pendingRumors = new Map(); // messageId -> { rumor, attempts, targets }
128
-
145
+
146
+ // Recent rumors buffer (for HTTP polling by MeshBridge)
147
+ this.recentRumors = []; // { topic, data, origin, timestamp, messageId }
148
+ this.maxRecentRumors = 500; // Keep last 500 rumors
149
+ this.rumorRetentionMs = 300000; // 5 min retention
150
+
129
151
  // Intervals
130
152
  this.intervals = [];
131
-
153
+
132
154
  // Bind handlers
133
155
  this._handleGossipMessage = this._handleGossipMessage.bind(this);
134
156
  }
@@ -138,7 +160,7 @@ export class MantraProtocol {
138
160
  */
139
161
  start() {
140
162
  log.info('MANTRA protocol started - prayer wheel spinning');
141
-
163
+
142
164
  // Register message handler with mesh
143
165
  this.mesh.on('gossip', this._handleGossipMessage);
144
166
 
@@ -177,7 +199,7 @@ export class MantraProtocol {
177
199
  */
178
200
  spreadRumor(topic, data) {
179
201
  const messageId = this._generateMessageId(topic, data);
180
-
202
+
181
203
  if (this.seenMessages.has(messageId)) {
182
204
  return; // Already seen
183
205
  }
@@ -192,31 +214,69 @@ export class MantraProtocol {
192
214
  timestamp: Date.now(),
193
215
  };
194
216
 
217
+ // Sign the rumor (ML-DSA-65) — excludes TTL since it decrements during propagation
218
+ const sigPayload = JSON.stringify({
219
+ messageId: rumor.messageId,
220
+ topic: rumor.topic,
221
+ data: rumor.data,
222
+ origin: rumor.origin,
223
+ timestamp: rumor.timestamp,
224
+ });
225
+ rumor.signature = this.identity.sign(sigPayload);
226
+
195
227
  this.seenMessages.add(messageId);
228
+ this._bufferRumor(rumor);
196
229
  this._propagateRumor(rumor);
197
-
230
+
231
+ // Also emit for HTTP relay bridge — locally-generated rumors must reach
232
+ // relay peers (nodes connected via HTTP polling, not WebSocket).
233
+ // _propagateRumor only sends to mesh.getPeers() (WS peers), so relay-only
234
+ // nodes (e.g. behind firewalls) would never propagate their own rumors
235
+ // without this. The server layer's outbound-gossip handler queues the
236
+ // message in the relay outbox for delivery on the next poll cycle.
237
+ this._emitForRelayBridge(rumor);
238
+
198
239
  return messageId;
199
240
  }
200
241
 
242
+ /**
243
+ * Emit a rumor as an outbound-gossip event for the HTTP relay bridge.
244
+ * This ensures locally-generated rumors reach relay peers (nodes connected
245
+ * via HTTP polling rather than direct WebSocket). Without this, nodes that
246
+ * only have relay connections would generate rumors that never leave the node.
247
+ */
248
+ _emitForRelayBridge(rumor) {
249
+ const meshMsg = {
250
+ type: 'gossip',
251
+ payload: { gossip: rumor },
252
+ id: rumor.messageId,
253
+ origin: rumor.origin,
254
+ ttl: rumor.ttl,
255
+ };
256
+ // Exclude our own nodeId — we're the origin, no need to relay back to self
257
+ this.mesh.emit('outbound-gossip', meshMsg, [rumor.origin]);
258
+ }
259
+
201
260
  /**
202
261
  * Get known peers (for peer discovery)
203
262
  */
204
263
  getKnownPeers() {
205
264
  const now = Date.now();
206
265
  const peers = [];
207
-
266
+
208
267
  for (const [nodeId, info] of this.knownPeers) {
209
268
  if (now - info.lastSeen < this.config.peerTTL) {
210
269
  peers.push({
211
270
  nodeId,
212
271
  name: info.name,
213
272
  endpoint: info.endpoint,
273
+ relayEndpoints: info.relayEndpoints || [],
214
274
  region: info.region,
215
275
  lastSeen: info.lastSeen,
216
276
  });
217
277
  }
218
278
  }
219
-
279
+
220
280
  return peers;
221
281
  }
222
282
 
@@ -260,14 +320,26 @@ export class MantraProtocol {
260
320
  region: this.identity.identity.region,
261
321
  capabilities: this.identity.identity.capabilities,
262
322
  endpoint: this.mesh.getPublicEndpoint?.() || null,
323
+ publicKey: this.identity.identity.publicKey, // ML-DSA-65 public key for signature verification
263
324
  timestamp: Date.now(),
264
325
  };
265
326
 
327
+ // Include relay endpoints we're registered with so peers can discover relay paths
328
+ // This is how relay knowledge propagates organically through the mesh
329
+ if (this._getRelayInfo) {
330
+ try {
331
+ const relayInfo = this._getRelayInfo();
332
+ if (relayInfo?.relayEndpoints?.length > 0) {
333
+ hello.relayEndpoints = relayInfo.relayEndpoints;
334
+ }
335
+ } catch { /* relay info unavailable — that's ok */ }
336
+ }
337
+
266
338
  this.mesh.broadcast({ gossip: hello });
267
-
339
+
268
340
  // Also request peers
269
- this.mesh.broadcast({
270
- gossip: { type: GossipMessageType.WANT_PEERS }
341
+ this.mesh.broadcast({
342
+ gossip: { type: GossipMessageType.WANT_PEERS }
271
343
  });
272
344
  }
273
345
 
@@ -275,15 +347,38 @@ export class MantraProtocol {
275
347
  * Handle HELLO from a peer
276
348
  */
277
349
  _handleHello(message, fromNodeId) {
350
+ const isNewPeer = !this.knownPeers.has(message.nodeId);
351
+
278
352
  this.knownPeers.set(message.nodeId, {
279
353
  name: message.name,
280
354
  region: message.region,
281
355
  capabilities: message.capabilities,
282
356
  endpoint: message.endpoint,
357
+ relayEndpoints: message.relayEndpoints || [],
358
+ publicKey: message.publicKey || null, // Store for gossip signature verification
283
359
  lastSeen: Date.now(),
284
360
  });
285
361
 
286
- log.info('Discovered peer', { name: message.name, nodeId: message.nodeId.slice(0, 16) });
362
+ // Store public key in mesh._relayPeerKeys for signature verification
363
+ // This allows relay-discovered peers to verify each other's gossip signatures
364
+ if (message.publicKey && message.nodeId && this.mesh?._relayPeerKeys) {
365
+ this.mesh._relayPeerKeys.set(message.nodeId, message.publicKey);
366
+ } else if (message.publicKey && message.nodeId && this.mesh) {
367
+ // Initialize the map if it doesn't exist
368
+ if (!this.mesh._relayPeerKeys) this.mesh._relayPeerKeys = new Map();
369
+ this.mesh._relayPeerKeys.set(message.nodeId, message.publicKey);
370
+ }
371
+
372
+ // Only log when we actually discover a new peer
373
+ if (isNewPeer) {
374
+ log.info('Discovered peer', { name: message.name, peer: peerTag(message.nodeId) });
375
+ }
376
+
377
+ // If peer advertises relay endpoints and we have no direct connection to the
378
+ // node behind that relay, auto-register — this is how relay knowledge spreads
379
+ if (message.relayEndpoints?.length > 0 && this._connectRelay) {
380
+ this._tryRelayConnect(message.relayEndpoints, message.nodeId);
381
+ }
287
382
 
288
383
  // Respond with our peer list
289
384
  this._sendPeerList(fromNodeId);
@@ -320,26 +415,54 @@ export class MantraProtocol {
320
415
  _handlePeers(message) {
321
416
  for (const peer of message.peers) {
322
417
  if (peer.nodeId === this.identity.identity.nodeId) continue;
323
-
418
+
324
419
  if (!this.knownPeers.has(peer.nodeId)) {
325
420
  this.knownPeers.set(peer.nodeId, {
326
421
  name: peer.name,
327
422
  region: peer.region,
328
423
  endpoint: peer.endpoint,
424
+ relayEndpoints: peer.relayEndpoints || [],
329
425
  lastSeen: peer.lastSeen,
330
426
  });
331
-
427
+
332
428
  // Try to connect if we have an endpoint
333
429
  if (peer.endpoint && !this.mesh.isConnectedTo(peer.nodeId)) {
334
430
  log.debug('Attempting connection to discovered peer', { name: peer.name });
335
431
  this.mesh.connectToPeer(peer.endpoint).catch(() => {
336
- // Connection failed, that's ok
432
+ // Connection failed try relay if available
433
+ if (peer.relayEndpoints?.length > 0 && this._connectRelay) {
434
+ this._tryRelayConnect(peer.relayEndpoints, peer.nodeId);
435
+ }
337
436
  });
437
+ } else if (!peer.endpoint && peer.relayEndpoints?.length > 0 && this._connectRelay) {
438
+ // No WS endpoint but has relay — connect via relay
439
+ this._tryRelayConnect(peer.relayEndpoints, peer.nodeId);
338
440
  }
339
441
  }
340
442
  }
341
443
  }
342
444
 
445
+ /**
446
+ * Try connecting to a peer via relay endpoints discovered through gossip.
447
+ * Fire-and-forget — failure is silent (relay is a fallback, not primary path).
448
+ */
449
+ _tryRelayConnect(relayEndpoints, nodeId) {
450
+ if (!this._connectRelay || !relayEndpoints?.length) return;
451
+
452
+ // Don't relay-connect to ourselves
453
+ if (nodeId === this.identity.identity.nodeId) return;
454
+
455
+ for (const endpoint of relayEndpoints) {
456
+ if (typeof endpoint !== 'string' || !endpoint.startsWith('http')) continue;
457
+
458
+ log.info(`GOSSIP relay discovery → ${endpoint} for ${peerTag(nodeId)}`);
459
+ this._connectRelay(endpoint, nodeId).catch(err => {
460
+ log.debug(`GOSSIP relay connect failed: ${endpoint} — ${err.message}`);
461
+ });
462
+ break; // Only try first viable relay endpoint
463
+ }
464
+ }
465
+
343
466
  /**
344
467
  * Handle incoming rumor
345
468
  */
@@ -355,18 +478,43 @@ export class MantraProtocol {
355
478
  return;
356
479
  }
357
480
 
481
+ // Verify origin's ML-DSA-65 signature before trusting the rumor
482
+ if (!rumor.signature) {
483
+ log.warn('Dropping unsigned rumor', { origin: peerTag(rumor.origin), messageId });
484
+ return;
485
+ }
486
+ const originPubKey = this._getPeerPublicKey(rumor.origin);
487
+ if (!originPubKey) {
488
+ log.warn('Dropping rumor from unknown origin (no public key)', { origin: peerTag(rumor.origin), messageId });
489
+ return;
490
+ }
491
+ const sigPayload = JSON.stringify({
492
+ messageId: rumor.messageId,
493
+ topic: rumor.topic,
494
+ data: rumor.data,
495
+ origin: rumor.origin,
496
+ timestamp: rumor.timestamp,
497
+ });
498
+ if (!this.identity.verify(sigPayload, rumor.signature, originPubKey)) {
499
+ log.warn('Dropping rumor with invalid signature', { origin: peerTag(rumor.origin), messageId });
500
+ return;
501
+ }
502
+
358
503
  // Mark as seen
359
504
  this.seenMessages.add(messageId);
360
-
505
+
361
506
  // Check bloom filter health
362
507
  if (this.seenMessages.shouldReset()) {
363
508
  this.seenMessages.reset();
364
509
  }
365
510
 
511
+ // Buffer for HTTP API consumers
512
+ this._bufferRumor(rumor);
513
+
366
514
  // Emit event for application layer
367
515
  this.mesh.emit('rumor', rumor.topic, rumor.data, rumor.origin);
368
516
 
369
- log.debug('Received rumor', { topic: rumor.topic, origin: rumor.origin.slice(0, 16) });
517
+ log.debug('Received rumor', { topic: rumor.topic, origin: peerTag(rumor.origin) });
370
518
 
371
519
  // Propagate if TTL allows
372
520
  if (ttl > 1) {
@@ -396,13 +544,11 @@ export class MantraProtocol {
396
544
  .filter(p => p.nodeId !== excludeNodeId && p.nodeId !== rumor.origin);
397
545
 
398
546
  if (peers.length === 0) {
399
- log.warn('No peers to propagate rumor to');
400
547
  return;
401
548
  }
402
549
 
403
550
  // Select random subset based on fanout
404
551
  const targets = this._selectRandom(peers, this.config.fanout);
405
- log.debug('Propagating rumor', { targetCount: targets.length, targets: targets.map(t => t.nodeId.slice(0, 12)) });
406
552
 
407
553
  for (const target of targets) {
408
554
  // Use broadcast format so the mesh routes it correctly
@@ -434,7 +580,7 @@ export class MantraProtocol {
434
580
 
435
581
  // Pick a random peer for anti-entropy
436
582
  const target = peers[Math.floor(Math.random() * peers.length)];
437
-
583
+
438
584
  this.mesh.sendTo(target.nodeId, {
439
585
  gossip: { type: GossipMessageType.WANT_PEERS }
440
586
  });
@@ -470,21 +616,95 @@ export class MantraProtocol {
470
616
  }
471
617
 
472
618
  /**
473
- * Generate deterministic message ID
619
+ * Generate deterministic message ID using 144T ternary format
620
+ * Eliminates hex "666" patterns while maintaining collision resistance
621
+ * Returns tier 1 (36 trits) as compact string: "TT00TTT00:TTT00TTT0:0TTT00TTT:00TTT00TT"
474
622
  */
475
623
  _generateMessageId(topic, data) {
476
624
  const payload = JSON.stringify({ topic, data, origin: this.identity.identity.nodeId, ts: Date.now() });
477
- return bytesToHex(sha3_256(new TextEncoder().encode(payload))).slice(0, 32);
625
+ const hex = bytesToHex(sha3_256(new TextEncoder().encode(payload)));
626
+ // Convert to 144T ternary address, extract tier 1 as compact string
627
+ const tritAddr = TritAddress.fromHex(hex);
628
+ return tritAddr.toString().split('.')[0]; // First tier only
478
629
  }
479
630
 
480
631
  /**
481
632
  * Select random items from array
482
633
  */
483
634
  _selectRandom(array, count) {
484
- const shuffled = [...array].sort(() => Math.random() - 0.5);
635
+ // Fisher-Yates shuffle unbiased (sort-based shuffle is statistically skewed)
636
+ const shuffled = [...array];
637
+ for (let i = shuffled.length - 1; i > 0; i--) {
638
+ const j = Math.floor(Math.random() * (i + 1));
639
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
640
+ }
485
641
  return shuffled.slice(0, Math.min(count, array.length));
486
642
  }
487
643
 
644
+ /**
645
+ * Buffer a rumor for HTTP API retrieval
646
+ */
647
+ _bufferRumor(rumor) {
648
+ this.recentRumors.push({
649
+ messageId: rumor.messageId,
650
+ topic: rumor.topic,
651
+ data: rumor.data,
652
+ origin: rumor.origin,
653
+ timestamp: rumor.timestamp || Date.now(),
654
+ });
655
+
656
+ // Evict old entries
657
+ const cutoff = Date.now() - this.rumorRetentionMs;
658
+ while (this.recentRumors.length > this.maxRecentRumors ||
659
+ (this.recentRumors.length > 0 && this.recentRumors[0].timestamp < cutoff)) {
660
+ this.recentRumors.shift();
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Get recent rumors (for HTTP API polling)
666
+ * @param {number} since - Timestamp to filter from (exclusive)
667
+ * @param {string} [topic] - Optional topic filter
668
+ * @returns {Array} Matching rumors
669
+ */
670
+ getRecentRumors(since = 0, topic = null) {
671
+ return this.recentRumors.filter(r => {
672
+ if (r.timestamp <= since) return false;
673
+ if (topic && r.topic !== topic) return false;
674
+ return true;
675
+ });
676
+ }
677
+
678
+ /**
679
+ * Resolve a peer's public key from mesh state.
680
+ * Checks WS peers, relay keys, knownPeers, SHERPA registry, and self.
681
+ */
682
+ _getPeerPublicKey(nodeId) {
683
+ // Self
684
+ if (nodeId === this.identity.identity.nodeId) {
685
+ return this.identity.identity.publicKey;
686
+ }
687
+ // WS peer info
688
+ if (this.mesh?.peers) {
689
+ const peer = this.mesh.peers.get(nodeId);
690
+ if (peer?.identity?.publicKey) return peer.identity.publicKey;
691
+ }
692
+ // Relay peer keys (stored during signed registration)
693
+ if (this.mesh?._relayPeerKeys) {
694
+ const key = this.mesh._relayPeerKeys.get(nodeId);
695
+ if (key) return key;
696
+ }
697
+ // knownPeers from HELLO messages (learned via gossip)
698
+ const knownPeer = this.knownPeers.get(nodeId);
699
+ if (knownPeer?.publicKey) return knownPeer.publicKey;
700
+ // SHERPA registry
701
+ if (this.mesh?.sherpa?.registry) {
702
+ const regPeer = this.mesh.sherpa.registry.get(nodeId);
703
+ if (regPeer?.publicKey) return regPeer.publicKey;
704
+ }
705
+ return null;
706
+ }
707
+
488
708
  /**
489
709
  * Get MANTRA statistics (prayer wheel metrics)
490
710
  */