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/mesh/jhilke.js ADDED
@@ -0,0 +1,651 @@
1
+ /**
2
+ * JHILKE — Just Hidden In-band Legitimate Key Exchange
3
+ * झिल्के (jhilke) — the sound of crickets
4
+ *
5
+ * Like D-Day cricket clickers: a signal meaningful only to those who know.
6
+ * Like Navajo codetalkers: a language outsiders can't parse.
7
+ *
8
+ * JHILKE provides two critical functions:
9
+ *
10
+ * 1. DETERMINISTIC BOOTSTRAP: Both nodes derive the same initial symmetric
11
+ * key from their shared code hash (verification phrase anchor). This
12
+ * eliminates plaintext KEM exchange — traffic is encrypted from message #1.
13
+ *
14
+ * 2. STEGANOGRAPHIC REKEY: Ongoing key rotations are coordinated via hidden
15
+ * signals embedded in mesh_entropy messages. No explicit REKEY messages
16
+ * are ever sent — an observer sees only entropy exchange.
17
+ *
18
+ * Security properties:
19
+ * - Bootstrap key is derived from code hash + node IDs (deterministic)
20
+ * - Bootstrap key is NOT a long-term secret (upgrades to KEM immediately)
21
+ * - Cricket signals use HKDF dialect derived from codebase hash
22
+ * - Only nodes with identical codebase can encode/decode signals
23
+ * - SST Fibonacci 24-cycle modulates signal encoding (rotational variety)
24
+ * - Ternary state machine: PREPARE (+1) → READY (0) → SWITCH (-1)
25
+ *
26
+ * @module mesh/jhilke
27
+ * @license MIT
28
+ * @copyright 2026 YAKMESH™ Contributors
29
+ */
30
+
31
+ import { randomBytes } from 'crypto';
32
+ import { sha3_256 } from '@noble/hashes/sha3.js';
33
+ import { hkdf } from '@noble/hashes/hkdf.js';
34
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
35
+ import { createLogger } from '../utils/logger.js';
36
+
37
+ // TRIBHUJ ternary primitives
38
+ import { POSITIVE, NEUTRAL, NEGATIVE } from '../oracle/tribhuj.js';
39
+
40
+ // SST Fibonacci 24-cycle for signal modulation
41
+ import { fibonacciRoot } from '../oracle/sst.js';
42
+
43
+ const log = createLogger('mesh:jhilke');
44
+
45
+ // ═══ JHILKE Configuration ═══
46
+ const JHILKE_CONFIG = {
47
+ // Bootstrap — deterministic key from verification phrase anchor
48
+ bootstrapSalt: 'YAKMESH-JHILKE-BOOTSTRAP-2026',
49
+ bootstrapInfo: 'yakmesh-jhilke-bootstrap-key-v1',
50
+
51
+ // Rekey — deterministic key rotation (no KEM round-trip)
52
+ rekeySalt: 'YAKMESH-JHILKE-REKEY-2026',
53
+ rekeyInfo: 'yakmesh-jhilke-rekey-v1',
54
+
55
+ // Dialect — steganographic signal encoding
56
+ dialectSalt: 'jhilke-cricket-salt-2026',
57
+ dialectInfo: 'yakmesh-jhilke-dialect-v1',
58
+
59
+ // Signal parameters
60
+ signalInfo: 'jhilke-signal-v1',
61
+ signalSize: 8, // 8 bytes of HKDF output per signal
62
+ paddingMin: 16, // minimum random padding bytes
63
+ paddingMax: 64, // maximum random padding bytes
64
+
65
+ // Timing
66
+ tickInterval: 1000, // 1 second ticks
67
+ preparePhase: 3, // ticks in PREPARE before moving to READY
68
+ switchDelay: 3, // ticks of SWITCH signaling before executing (must receive peer SWITCH too)
69
+ retryAfterTicks: 24, // "tag, you're it" retry timeout
70
+ abortAfterCycles: 3, // abort after 3 retry cycles (72 ticks total)
71
+ tickTolerance: 1, // ±1 tick tolerance for signal decoding
72
+
73
+ // Ternary intents (TRIBHUJ trits)
74
+ INTENT_PREPARE: POSITIVE, // +1: I'm preparing new keys
75
+ INTENT_READY: NEUTRAL, // 0: My new key is ready
76
+ INTENT_SWITCH: NEGATIVE, // -1: Switching to new key now
77
+ };
78
+
79
+ /**
80
+ * Per-peer coordination state.
81
+ * Tracks where we are in the cricket chirp dance with each peer.
82
+ */
83
+ class JhilkeSession {
84
+ constructor(peerId) {
85
+ this.peerId = peerId;
86
+ this.state = 'idle'; // idle | prepare | ready | switch | exchanging
87
+ this.tick = 0; // tick when this coordination started
88
+ this.switchTick = 0; // tick when we entered switch state
89
+ this.retryCount = 0; // how many retry cycles
90
+ this.initiator = false; // did WE request this rekey?
91
+ this.peerReady = false; // has peer signaled READY?
92
+ this.ourReady = false; // have WE signaled READY?
93
+ this.peerSwitchReceived = false; // has peer signaled SWITCH?
94
+ this.startedAt = null;
95
+ this.lastSignalSent = null;
96
+ this.lastSignalReceived = null;
97
+ }
98
+
99
+ reset() {
100
+ this.state = 'idle';
101
+ this.tick = 0;
102
+ this.switchTick = 0;
103
+ this.retryCount = 0;
104
+ this.initiator = false;
105
+ this.peerReady = false;
106
+ this.ourReady = false;
107
+ this.peerSwitchReceived = false;
108
+ this.startedAt = null;
109
+ this.lastSignalSent = null;
110
+ this.lastSignalReceived = null;
111
+ }
112
+ }
113
+
114
+
115
+ // ═══════════════════════════════════════════════════════════════════════
116
+ // JHILKE Coordinator — the cricket chorus conductor
117
+ // ═══════════════════════════════════════════════════════════════════════
118
+
119
+ export class JhilkeCoordinator {
120
+ constructor(options) {
121
+ this.codeHash = options.codeHash; // Oracle code hash (same for all valid nodes)
122
+ this.nodeId = options.nodeId; // Our node ID
123
+ this.annex = options.annex; // ANNEX instance
124
+ this.mesh = options.mesh; // Mesh instance (for sendTo)
125
+
126
+ // Derived seeds (deterministic from code hash — same for all nodes)
127
+ this.dialectSeed = this._deriveDialectSeed();
128
+
129
+ // Per-peer sessions
130
+ this.sessions = new Map(); // peerId -> JhilkeSession
131
+
132
+ // Tick timer
133
+ this._tickTimer = null;
134
+ this._globalTick = 0;
135
+
136
+ // Stats
137
+ this.stats = {
138
+ bootstrapKeysDerived: 0,
139
+ signalsSent: 0,
140
+ signalsReceived: 0,
141
+ signalsDecoded: 0,
142
+ rekeyCoordinations: 0,
143
+ rekeySuccesses: 0,
144
+ rekeyAborts: 0,
145
+ };
146
+
147
+ log.info('JHILKE coordinator initialized', {
148
+ dialectFingerprint: bytesToHex(this.dialectSeed).slice(0, 16) + '...',
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Shared time reference for steganographic signal encoding/decoding.
154
+ * Uses wall-clock seconds (Unix time) so both nodes agree on the tick
155
+ * regardless of when they started. GPS-synchronized via MA-902 Stratum 1.
156
+ * The per-node _globalTick is still used for session age tracking.
157
+ */
158
+ _sharedTick() {
159
+ return Math.floor(Date.now() / 1000);
160
+ }
161
+
162
+ // ══════════════════════════════════════════════════════════════════
163
+ // BOOTSTRAP: Deterministic initial key from shared code hash
164
+ // "Both nodes arrive at the same conclusion because of the
165
+ // anchoring verification phrase" — the code hash IS the anchor.
166
+ // ══════════════════════════════════════════════════════════════════
167
+
168
+ /**
169
+ * Derive a deterministic bootstrap encryption key for a peer.
170
+ * Both nodes independently compute the SAME key because they share
171
+ * the same code hash (enforced by the Validation Oracle + network
172
+ * fingerprint check in HELLO/WELCOME).
173
+ *
174
+ * Key = HKDF(sha3_256, codeHash, sort(nodeA, nodeB), bootstrapInfo, 32)
175
+ *
176
+ * This key is NOT a long-term secret — anyone with source code + both
177
+ * node IDs could theoretically compute it. It exists to:
178
+ * 1. Eliminate plaintext KEM exchange messages entirely
179
+ * 2. Buy seconds for JHILKE to coordinate a proper KEM upgrade
180
+ * 3. Make ALL traffic encrypted from the very first post-handshake message
181
+ *
182
+ * The bootstrap key is replaced by a proper KEM key almost immediately.
183
+ */
184
+ deriveBootstrapKey(peerId) {
185
+ const hashBytes = hexToBytes(this.codeHash);
186
+
187
+ // Sort node IDs so both sides derive the same key (order-independent)
188
+ const [first, second] = [this.nodeId, peerId].sort();
189
+ const salt = utf8ToBytes(`${JHILKE_CONFIG.bootstrapSalt}:${first}:${second}`);
190
+ const info = utf8ToBytes(JHILKE_CONFIG.bootstrapInfo);
191
+
192
+ const key = hkdf(sha3_256, hashBytes, salt, info, 32);
193
+
194
+ this.stats.bootstrapKeysDerived++;
195
+ log.debug('Bootstrap key derived (verification phrase anchor)', {
196
+ peer: peerId.slice(0, 16),
197
+ keyFingerprint: bytesToHex(key).slice(0, 8) + '...',
198
+ });
199
+
200
+ return Buffer.from(key);
201
+ }
202
+
203
+ // ══════════════════════════════════════════════════════════════════
204
+ // REKEY: Deterministic key rotation — both nodes derive the SAME
205
+ // new key independently, no KEM round-trip, no race condition.
206
+ // ══════════════════════════════════════════════════════════════════
207
+
208
+ /**
209
+ * Derive a deterministic rekey encryption key for a peer.
210
+ * Both nodes independently compute the SAME new key because they share:
211
+ * - codeHash (verified by Oracle)
212
+ * - currentKey (established via initial KEM exchange)
213
+ * - epoch (incremented together after cricket coordination)
214
+ *
215
+ * Key = HKDF(sha3_256, SHA3(codeHash || currentKey), salt(nodeIds + epoch), rekeyInfo, 32)
216
+ *
217
+ * Security properties:
218
+ * - PFS: depends on currentKey (random from initial KEM), which is discarded after
219
+ * - Forward secure: each epoch is a one-way derivation from the previous
220
+ * - Not publicly derivable: requires knowledge of currentKey
221
+ * - Deterministic: no network round-trip, both sides compute simultaneously
222
+ */
223
+ deriveRekeyKey(peerId, epoch, currentKey) {
224
+ // IKM = SHA3(codeHash || currentKey) — binds network identity to session secret
225
+ const ikm = sha3_256.create()
226
+ .update(hexToBytes(this.codeHash))
227
+ .update(currentKey)
228
+ .digest();
229
+
230
+ // Sort node IDs so both sides derive the same key (order-independent)
231
+ const [first, second] = [this.nodeId, peerId].sort();
232
+ const salt = utf8ToBytes(`${JHILKE_CONFIG.rekeySalt}:${first}:${second}:${epoch}`);
233
+ const info = utf8ToBytes(JHILKE_CONFIG.rekeyInfo);
234
+
235
+ const key = hkdf(sha3_256, ikm, salt, info, 32);
236
+
237
+ log.debug('Deterministic rekey derived', {
238
+ peer: peerId.slice(0, 16),
239
+ epoch,
240
+ keyFingerprint: bytesToHex(key).slice(0, 8) + '...',
241
+ });
242
+
243
+ return Buffer.from(key);
244
+ }
245
+
246
+ // ══════════════════════════════════════════════════════════════════
247
+ // DIALECT: Steganographic signal encoding
248
+ // Only nodes with the same codebase can speak this language.
249
+ // ══════════════════════════════════════════════════════════════════
250
+
251
+ _deriveDialectSeed() {
252
+ const hashBytes = hexToBytes(this.codeHash);
253
+ const salt = utf8ToBytes(JHILKE_CONFIG.dialectSalt);
254
+ const info = utf8ToBytes(JHILKE_CONFIG.dialectInfo);
255
+ return hkdf(sha3_256, hashBytes, salt, info, 32);
256
+ }
257
+
258
+ /**
259
+ * Generate a steganographic signal encoding a specific intent.
260
+ * The signal is an 8-byte HKDF output that encodes:
261
+ * - both node IDs (order-sorted for determinism)
262
+ * - current tick
263
+ * - SST Fibonacci 24-cycle position (rotational modulation)
264
+ * - the ternary intent (+1, 0, -1)
265
+ *
266
+ * Only nodes sharing the dialect seed can produce or decode this signal.
267
+ */
268
+ _generateSignal(peerId, tick, intent) {
269
+ const [first, second] = [this.nodeId, peerId].sort();
270
+
271
+ // SST modulation: Fibonacci 24-cycle digital root at this tick
272
+ const fibPos = tick % 24;
273
+ const fibRoot = fibonacciRoot(fibPos);
274
+
275
+ const context = utf8ToBytes(
276
+ `${first}:${second}:${tick}:${fibPos}:${fibRoot}:${intent}`
277
+ );
278
+
279
+ return hkdf(sha3_256, this.dialectSeed, context,
280
+ utf8ToBytes(JHILKE_CONFIG.signalInfo), JHILKE_CONFIG.signalSize);
281
+ }
282
+
283
+ /**
284
+ * Decode an incoming signal by brute-forcing all 3 intents
285
+ * across ±1 tick tolerance (9 total attempts).
286
+ * Trivial for nodes sharing the dialect, impossible without it.
287
+ */
288
+ _decodeSignal(peerId, signalBytes, currentTick) {
289
+ const intents = [
290
+ JHILKE_CONFIG.INTENT_PREPARE,
291
+ JHILKE_CONFIG.INTENT_READY,
292
+ JHILKE_CONFIG.INTENT_SWITCH,
293
+ ];
294
+
295
+ const signalHex = bytesToHex(signalBytes);
296
+
297
+ for (let offset = -JHILKE_CONFIG.tickTolerance; offset <= JHILKE_CONFIG.tickTolerance; offset++) {
298
+ const testTick = currentTick + offset;
299
+ if (testTick < 0) continue;
300
+
301
+ for (const intent of intents) {
302
+ const expected = this._generateSignal(peerId, testTick, intent);
303
+ if (bytesToHex(expected) === signalHex) {
304
+ return { intent, tick: testTick, offset };
305
+ }
306
+ }
307
+ }
308
+
309
+ return null; // Not a JHILKE signal (genuine entropy)
310
+ }
311
+
312
+
313
+ // ══════════════════════════════════════════════════════════════════
314
+ // COORDINATION: State machine + tick loop
315
+ // ══════════════════════════════════════════════════════════════════
316
+
317
+ /**
318
+ * Start the tick loop (1-second heartbeat for cricket coordination)
319
+ */
320
+ start() {
321
+ if (this._tickTimer) return;
322
+ this._tickTimer = setInterval(() => this._tick(), JHILKE_CONFIG.tickInterval);
323
+ log.info('JHILKE tick loop started (crickets chirping)');
324
+ }
325
+
326
+ /**
327
+ * Stop the tick loop and clean up all sessions
328
+ */
329
+ stop() {
330
+ if (this._tickTimer) {
331
+ clearInterval(this._tickTimer);
332
+ this._tickTimer = null;
333
+ }
334
+ this.sessions.clear();
335
+ log.info('JHILKE tick loop stopped');
336
+ }
337
+
338
+ /**
339
+ * Request a rekey for a specific peer.
340
+ * Called by ANNEX when needsRekey() returns true.
341
+ * Instead of sending a plaintext REKEY, we begin the cricket dance.
342
+ */
343
+ initiateRekey(peerId) {
344
+ let session = this.sessions.get(peerId);
345
+ if (session && session.state !== 'idle') {
346
+ log.debug('JHILKE rekey already in progress', { peer: peerId.slice(0, 16) });
347
+ return;
348
+ }
349
+
350
+ if (!session) {
351
+ session = new JhilkeSession(peerId);
352
+ this.sessions.set(peerId, session);
353
+ }
354
+
355
+ session.state = 'prepare';
356
+ session.initiator = true;
357
+ session.startedAt = Date.now();
358
+ session.tick = this._globalTick;
359
+
360
+ this.stats.rekeyCoordinations++;
361
+ log.info('JHILKE rekey initiated (cricket chirping begins)', {
362
+ peer: peerId.slice(0, 16),
363
+ tick: this._globalTick,
364
+ });
365
+ }
366
+
367
+ /**
368
+ * Handle incoming mesh_entropy message — check for hidden cricket signals
369
+ */
370
+ handleIncoming(fromPeerId, message) {
371
+ if (!message.jhilke) return;
372
+
373
+ this.stats.signalsReceived++;
374
+
375
+ let signalBytes;
376
+ try {
377
+ signalBytes = hexToBytes(message.jhilke);
378
+ } catch {
379
+ return; // Malformed hex, ignore
380
+ }
381
+
382
+ const decoded = this._decodeSignal(fromPeerId, signalBytes, this._sharedTick());
383
+ if (!decoded) return; // Not a valid signal for us
384
+
385
+ this.stats.signalsDecoded++;
386
+
387
+ log.debug('JHILKE cricket decoded', {
388
+ peer: fromPeerId.slice(0, 16),
389
+ intent: decoded.intent === JHILKE_CONFIG.INTENT_PREPARE ? 'PREPARE' :
390
+ decoded.intent === JHILKE_CONFIG.INTENT_READY ? 'READY' : 'SWITCH',
391
+ tick: decoded.tick,
392
+ offset: decoded.offset,
393
+ });
394
+
395
+ this._processSignal(fromPeerId, decoded);
396
+ }
397
+
398
+ /**
399
+ * Process a decoded signal through the ternary state machine
400
+ */
401
+ _processSignal(peerId, decoded) {
402
+ let session = this.sessions.get(peerId);
403
+
404
+ if (!session) {
405
+ session = new JhilkeSession(peerId);
406
+ this.sessions.set(peerId, session);
407
+ }
408
+
409
+ session.lastSignalReceived = Date.now();
410
+ const { intent } = decoded;
411
+
412
+ switch (intent) {
413
+ case JHILKE_CONFIG.INTENT_PREPARE:
414
+ // Peer is preparing for rekey
415
+ if (session.state === 'idle') {
416
+ // They initiated — we join the coordination
417
+ session.state = 'prepare';
418
+ session.initiator = false;
419
+ session.startedAt = Date.now();
420
+ session.tick = this._globalTick;
421
+ this.stats.rekeyCoordinations++;
422
+ log.info('JHILKE: peer initiated rekey, joining dance', {
423
+ peer: peerId.slice(0, 16),
424
+ });
425
+ }
426
+ break;
427
+
428
+ case JHILKE_CONFIG.INTENT_READY:
429
+ // Peer's new key material is ready
430
+ if (session.state === 'prepare' || session.state === 'ready') {
431
+ session.peerReady = true;
432
+ log.debug('JHILKE: peer ready', { peer: peerId.slice(0, 16) });
433
+
434
+ // Both sides ready → move to switch phase
435
+ if (session.ourReady && session.peerReady) {
436
+ session.state = 'switch';
437
+ session.switchTick = this._globalTick; // Track when we entered switch state
438
+ log.info('JHILKE: both ready, entering switch phase', { peer: peerId.slice(0, 16) });
439
+ }
440
+ }
441
+ break;
442
+
443
+ case JHILKE_CONFIG.INTENT_SWITCH:
444
+ // Peer is switching — mark peer switch received
445
+ session.peerSwitchReceived = true;
446
+ // Don't execute here — let _tick() handle it after switchDelay
447
+ if (session.state === 'ready' && session.ourReady && session.peerReady) {
448
+ session.state = 'switch';
449
+ session.switchTick = this._globalTick; // Track when we entered switch state
450
+ }
451
+ break;
452
+ }
453
+ }
454
+
455
+
456
+ /**
457
+ * Main tick handler — drives the state machine forward.
458
+ * Called every 1 second. Sends cricket signals and manages transitions.
459
+ */
460
+ _tick() {
461
+ this._globalTick++;
462
+
463
+ for (const [peerId, session] of this.sessions) {
464
+ if (session.state === 'idle') continue;
465
+
466
+ const sessionAge = this._globalTick - session.tick;
467
+
468
+ switch (session.state) {
469
+ case 'prepare':
470
+ // Send PREPARE chirp
471
+ this._sendSignal(peerId, JHILKE_CONFIG.INTENT_PREPARE);
472
+
473
+ // After preparePhase ticks, our key material is "ready"
474
+ if (sessionAge >= JHILKE_CONFIG.preparePhase) {
475
+ session.ourReady = true;
476
+ session.state = 'ready';
477
+ log.debug('JHILKE: our key ready, signaling READY', {
478
+ peer: peerId.slice(0, 16),
479
+ });
480
+ }
481
+ break;
482
+
483
+ case 'ready':
484
+ // Send READY chirp
485
+ this._sendSignal(peerId, JHILKE_CONFIG.INTENT_READY);
486
+
487
+ // Check if both sides are ready
488
+ if (session.ourReady && session.peerReady) {
489
+ session.state = 'switch';
490
+ }
491
+
492
+ // Retry timeout: "tag, you're it"
493
+ if (sessionAge > JHILKE_CONFIG.retryAfterTicks) {
494
+ session.retryCount++;
495
+ if (session.retryCount >= JHILKE_CONFIG.abortAfterCycles) {
496
+ log.warn('JHILKE: rekey coordination timed out', {
497
+ peer: peerId.slice(0, 16),
498
+ retries: session.retryCount,
499
+ });
500
+ session.reset();
501
+ this.stats.rekeyAborts++;
502
+ } else {
503
+ // Reset and retry
504
+ session.tick = this._globalTick;
505
+ session.peerReady = false;
506
+ session.ourReady = false;
507
+ session.state = 'prepare';
508
+ log.debug('JHILKE: retry cycle', {
509
+ peer: peerId.slice(0, 16),
510
+ retry: session.retryCount,
511
+ });
512
+ }
513
+ }
514
+ break;
515
+
516
+ case 'switch':
517
+ // Send SWITCH chirp, then execute after switchDelay ticks
518
+ this._sendSignal(peerId, JHILKE_CONFIG.INTENT_SWITCH);
519
+
520
+ // Execute ONLY after:
521
+ // 1. We've been in switch state for switchDelay ticks
522
+ // 2. AND we received peer's SWITCH signal
523
+ const switchAge = this._globalTick - (session.switchTick || session.tick);
524
+ if (switchAge >= JHILKE_CONFIG.switchDelay && session.peerSwitchReceived) {
525
+ this._executeSwitch(peerId, session);
526
+ }
527
+ break;
528
+
529
+ case 'exchanging':
530
+ // KEM exchange in progress via ANNEX internals — nothing to do
531
+ break;
532
+ }
533
+ }
534
+
535
+ // Periodic check: scan ANNEX sessions for rekey needs
536
+ // (every 30 ticks = 30 seconds, matches ANNEX ping interval)
537
+ if (this._globalTick % 30 === 0) {
538
+ this.checkAnnexRekeys();
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Send a steganographic signal (cricket chirp) to a peer
544
+ */
545
+ _sendSignal(peerId, intent) {
546
+ const signal = this._generateSignal(peerId, this._sharedTick(), intent);
547
+
548
+ // Random padding to vary message size (camouflage)
549
+ const paddingSize = JHILKE_CONFIG.paddingMin +
550
+ Math.floor(Math.random() * (JHILKE_CONFIG.paddingMax - JHILKE_CONFIG.paddingMin));
551
+ const padding = bytesToHex(randomBytes(paddingSize));
552
+
553
+ // Send as mesh_entropy — blends with normal entropy exchange traffic
554
+ this.mesh.sendTo(peerId, {
555
+ type: 'mesh_entropy',
556
+ entropy: bytesToHex(randomBytes(32)), // Genuine entropy contribution
557
+ jhilke: bytesToHex(signal), // Hidden cricket signal
558
+ pad: padding, // Variable-size camouflage
559
+ t: Date.now(),
560
+ });
561
+
562
+ this.stats.signalsSent++;
563
+
564
+ const session = this.sessions.get(peerId);
565
+ if (session) session.lastSignalSent = Date.now();
566
+ }
567
+
568
+ /**
569
+ * Execute the actual key switch after both sides confirmed ready.
570
+ *
571
+ * DETERMINISTIC: Both nodes derive the SAME new key independently.
572
+ * No KEM round-trip, no initiator/responder asymmetry, no race condition.
573
+ * The cricket dance (PREPARE → READY → SWITCH) ensures both nodes
574
+ * execute this at the same moment — then both compute the same key.
575
+ */
576
+ async _executeSwitch(peerId, session) {
577
+ if (session.state === 'exchanging') return;
578
+ session.state = 'exchanging';
579
+
580
+ try {
581
+ const annexSession = this.annex.sessions.get(peerId);
582
+ if (!annexSession || !annexSession.encryptionKey) {
583
+ log.warn('JHILKE: no ANNEX session for rekey switch', {
584
+ peer: peerId.slice(0, 16),
585
+ });
586
+ this.stats.rekeyAborts++;
587
+ return;
588
+ }
589
+
590
+ const epoch = annexSession.rekeyEpoch + 1;
591
+ const newKey = this.deriveRekeyKey(peerId, epoch, annexSession.encryptionKey);
592
+
593
+ // Both nodes arrive here simultaneously — switch key, no round-trip.
594
+ annexSession.deterministicRekey(newKey, epoch);
595
+
596
+ this.stats.rekeySuccesses++;
597
+ log.info('JHILKE: deterministic rekey complete', {
598
+ peer: peerId.slice(0, 16),
599
+ epoch,
600
+ ticks: this._globalTick - session.tick,
601
+ });
602
+ } catch (err) {
603
+ log.error('JHILKE: rekey execution failed', {
604
+ peer: peerId.slice(0, 16),
605
+ error: err.message,
606
+ });
607
+ this.stats.rekeyAborts++;
608
+ } finally {
609
+ session.reset();
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Check all ANNEX sessions for rekey needs.
615
+ * Called periodically by the tick loop.
616
+ */
617
+ checkAnnexRekeys() {
618
+ if (!this.annex) return;
619
+
620
+ for (const [peerId, annexSession] of this.annex.sessions) {
621
+ if (annexSession.needsRekey()) {
622
+ this.initiateRekey(peerId);
623
+ }
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Clean up session for a disconnected peer
629
+ */
630
+ removePeer(peerId) {
631
+ this.sessions.delete(peerId);
632
+ }
633
+
634
+ /**
635
+ * Get JHILKE stats
636
+ */
637
+ getStats() {
638
+ return {
639
+ ...this.stats,
640
+ activeSessions: this.sessions.size,
641
+ activeCoordinations: [...this.sessions.values()].filter(s => s.state !== 'idle').length,
642
+ globalTick: this._globalTick,
643
+ };
644
+ }
645
+ }
646
+
647
+ // Export config for testing
648
+ export { JHILKE_CONFIG };
649
+
650
+ export default JhilkeCoordinator;
651
+