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
@@ -0,0 +1,668 @@
1
+ /**
2
+ * KOMM Stack API — KATHA + VANI + YURT + GUMBA
3
+ *
4
+ * Exposes the Himalayan chat/voice/room/access protocols as HTTP + WS endpoints.
5
+ * This is the backend that yakapp's MeshBridge and terminal clients consume.
6
+ *
7
+ * Architecture:
8
+ * yakapp (GUI) ─── HTTP/WS ───┐
9
+ * terminal (CLI) ─── HTTP/WS ───┤── KOMM API ── yakmesh-node protocols
10
+ * external clients ─── HTTP/WS ───┘
11
+ *
12
+ * @module server/komm-api
13
+ * @license MIT
14
+ * @copyright 2026 YAKMESH™ Contributors
15
+ */
16
+
17
+ import { Router } from 'express';
18
+ import { createLogger } from '../utils/logger.js';
19
+
20
+ const log = createLogger('server:komm');
21
+
22
+ /**
23
+ * Create the KOMM stack API router.
24
+ *
25
+ * @param {Object} params
26
+ * @param {Object} params.kathaHub - KathaHub instance (chat messaging)
27
+ * @param {Object} params.vaniHub - VaniHub instance (voice/video)
28
+ * @param {Object} params.yurtHub - YurtHub instance (room directory)
29
+ * @param {Object} params.gumbaHub - GumbaHub instance (access control)
30
+ * @param {Object} params.gossip - GossipProtocol instance (for broadcasting)
31
+ * @param {Object} params.identity - NodeIdentity instance
32
+ * @param {Function} params.writeLimiter - Express rate limiter for writes
33
+ * @param {Function} params.requirePeerAuth - Peer auth middleware
34
+ * @returns {Router} Express router mounted at /komm
35
+ */
36
+ export function createKommAPI({
37
+ kathaHub,
38
+ vaniHub,
39
+ yurtHub,
40
+ gumbaHub,
41
+ gossip,
42
+ identity,
43
+ writeLimiter,
44
+ requirePeerAuth,
45
+ }) {
46
+ const router = Router();
47
+
48
+ // ═══════════════════════════════════════════════════════════════════════════
49
+ // KOMM STATUS
50
+ // ═══════════════════════════════════════════════════════════════════════════
51
+
52
+ router.get('/status', (req, res) => {
53
+ res.json({
54
+ komm: 'operational',
55
+ katha: {
56
+ channels: kathaHub.channels.size,
57
+ },
58
+ vani: {
59
+ activeCalls: vaniHub.calls.size,
60
+ activeCallId: vaniHub.activeCallId,
61
+ },
62
+ yurt: {
63
+ rooms: yurtHub.directory.entries.size,
64
+ ownRooms: yurtHub.directory.ownEntries?.size || 0,
65
+ },
66
+ gumba: {
67
+ bundles: gumbaHub.bundles.size,
68
+ sessions: gumbaHub.sessions.size,
69
+ stats: gumbaHub.stats,
70
+ },
71
+ });
72
+ });
73
+
74
+ // ═══════════════════════════════════════════════════════════════════════════
75
+ // KATHA — Chat Messaging
76
+ // ═══════════════════════════════════════════════════════════════════════════
77
+
78
+ /**
79
+ * GET /komm/katha/channels — List active channels
80
+ */
81
+ router.get('/katha/channels', (req, res) => {
82
+ const channels = [];
83
+ for (const [id, channel] of kathaHub.channels) {
84
+ channels.push({
85
+ channelId: id,
86
+ messageCount: channel.messages?.size ?? channel.messages?.length ?? 0,
87
+ typingUsers: channel.getTypingUsers?.() || [],
88
+ });
89
+ }
90
+ res.json({ channels });
91
+ });
92
+
93
+ /**
94
+ * GET /komm/katha/channel/:channelId — Get channel messages
95
+ */
96
+ router.get('/katha/channel/:channelId', (req, res) => {
97
+ const { channelId } = req.params;
98
+ const since = parseInt(req.query.since) || 0;
99
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
100
+
101
+ const channel = kathaHub.channels.get(channelId);
102
+ if (!channel) {
103
+ return res.json({ messages: [], exists: false });
104
+ }
105
+
106
+ // channel.messages is a Map — use getMessages() or convert to array
107
+ let messages = typeof channel.getMessages === 'function'
108
+ ? channel.getMessages({ after: since || undefined, limit })
109
+ : Array.from(channel.messages?.values?.() || []);
110
+ if (since > 0 && typeof channel.getMessages !== 'function') {
111
+ messages = messages.filter(m => m.timestamp > since);
112
+ }
113
+ messages = messages.slice(-limit);
114
+
115
+ res.json({
116
+ channelId,
117
+ messages: messages.map(m => m.toJSON ? m.toJSON() : m),
118
+ typingUsers: channel.getTypingUsers?.() || [],
119
+ exists: true,
120
+ });
121
+ });
122
+
123
+ /**
124
+ * POST /komm/katha/send — Send a message (broadcast via gossip)
125
+ */
126
+ router.post('/katha/send', writeLimiter, requirePeerAuth, (req, res) => {
127
+ const { channelId, type, content, messageId, userId, emoji, parentId, threadId } = req.body;
128
+
129
+ if (!channelId) {
130
+ return res.status(400).json({ error: 'channelId required' });
131
+ }
132
+
133
+ // Explicit allowlist — never spread raw req.body (proto pollution defense)
134
+ const event = {
135
+ channelId,
136
+ type: type || 'katha:text',
137
+ ...(content !== undefined && { content }),
138
+ ...(messageId !== undefined && { messageId }),
139
+ ...(userId !== undefined && { userId }),
140
+ ...(emoji !== undefined && { emoji }),
141
+ ...(parentId !== undefined && { parentId }),
142
+ ...(threadId !== undefined && { threadId }),
143
+ };
144
+
145
+ // Process locally
146
+ const result = kathaHub.handleEvent(event);
147
+
148
+ // Broadcast to mesh
149
+ gossip.spreadRumor('katha:event', {
150
+ ...event,
151
+ origin: identity.identity.nodeId,
152
+ timestamp: Date.now(),
153
+ });
154
+
155
+ res.json({ success: true, result });
156
+ });
157
+
158
+ /**
159
+ * POST /komm/katha/typing — Typing indicator
160
+ */
161
+ router.post('/katha/typing', requirePeerAuth, (req, res) => {
162
+ const { channelId, userId: bodyUserId, typing } = req.body;
163
+ // Use authenticated peer identity when available; fall back to body for localhost
164
+ const userId = req.authenticatedPeer || bodyUserId;
165
+
166
+ if (!channelId || !userId) {
167
+ return res.status(400).json({ error: 'channelId and userId required' });
168
+ }
169
+
170
+ const channel = kathaHub.getChannel(channelId);
171
+ channel.setTyping(userId, typing !== false);
172
+
173
+ // Ephemeral — broadcast but don't persist
174
+ gossip.spreadRumor('katha:typing', {
175
+ channelId,
176
+ userId,
177
+ typing: typing !== false,
178
+ origin: identity.identity.nodeId,
179
+ });
180
+
181
+ res.json({ success: true });
182
+ });
183
+
184
+ /**
185
+ * POST /komm/katha/reaction — Add/remove reaction
186
+ */
187
+ router.post('/katha/reaction', writeLimiter, requirePeerAuth, (req, res) => {
188
+ const { channelId, messageId, userId: bodyUserId, emoji, remove } = req.body;
189
+ // Use authenticated peer identity when available; fall back to body for localhost
190
+ const userId = req.authenticatedPeer || bodyUserId;
191
+
192
+ if (!channelId || !messageId || !userId || !emoji) {
193
+ return res.status(400).json({ error: 'channelId, messageId, userId, and emoji required' });
194
+ }
195
+
196
+ const event = {
197
+ channelId,
198
+ messageId,
199
+ userId,
200
+ emoji,
201
+ type: remove ? 'katha:reaction:remove' : 'katha:reaction:add',
202
+ };
203
+
204
+ const result = kathaHub.handleEvent(event);
205
+ gossip.spreadRumor('katha:event', { ...event, origin: identity.identity.nodeId });
206
+
207
+ res.json({ success: true, result });
208
+ });
209
+
210
+ /**
211
+ * POST /komm/katha/read — Mark messages as read
212
+ */
213
+ router.post('/katha/read', requirePeerAuth, (req, res) => {
214
+ const { channelId, userId: bodyUserId, lastReadMessageId, lastReadTimestamp } = req.body;
215
+ // Use authenticated peer identity when available; fall back to body for localhost
216
+ const userId = req.authenticatedPeer || bodyUserId;
217
+
218
+ if (!channelId || !userId) {
219
+ return res.status(400).json({ error: 'channelId and userId required' });
220
+ }
221
+
222
+ const event = {
223
+ channelId,
224
+ userId,
225
+ lastReadMessageId,
226
+ lastReadTimestamp: lastReadTimestamp || Date.now(),
227
+ type: 'katha:read',
228
+ };
229
+
230
+ kathaHub.handleEvent(event);
231
+ gossip.spreadRumor('katha:event', { ...event, origin: identity.identity.nodeId });
232
+
233
+ res.json({ success: true });
234
+ });
235
+
236
+ // ═══════════════════════════════════════════════════════════════════════════
237
+ // VANI — Voice/Video Calling
238
+ // ═══════════════════════════════════════════════════════════════════════════
239
+
240
+ /**
241
+ * GET /komm/vani/calls — List active calls
242
+ */
243
+ router.get('/vani/calls', (req, res) => {
244
+ const calls = [];
245
+ for (const [id, call] of vaniHub.calls) {
246
+ calls.push({
247
+ callId: id,
248
+ state: call.state,
249
+ mediaType: call.mediaType,
250
+ participants: call.participants?.size || 0,
251
+ startedAt: call.startedAt,
252
+ });
253
+ }
254
+ res.json({ calls, activeCallId: vaniHub.activeCallId });
255
+ });
256
+
257
+ /**
258
+ * POST /komm/vani/call — Start a new call
259
+ */
260
+ router.post('/vani/call', writeLimiter, requirePeerAuth, (req, res) => {
261
+ const { targetPeerIds, mediaType, bundleId, isGroupCall } = req.body;
262
+
263
+ if (!targetPeerIds || !Array.isArray(targetPeerIds)) {
264
+ return res.status(400).json({ error: 'targetPeerIds array required' });
265
+ }
266
+
267
+ try {
268
+ const call = vaniHub.startCall({
269
+ targetPeerIds,
270
+ mediaType,
271
+ bundleId,
272
+ isGroupCall,
273
+ });
274
+
275
+ res.json({ success: true, callId: call.id });
276
+ } catch (error) {
277
+ res.status(500).json({ error: error.message });
278
+ }
279
+ });
280
+
281
+ /**
282
+ * POST /komm/vani/signal — Forward WebRTC signal
283
+ */
284
+ router.post('/vani/signal', requirePeerAuth, (req, res) => {
285
+ const { signal } = req.body;
286
+
287
+ if (!signal) {
288
+ return res.status(400).json({ error: 'signal object required' });
289
+ }
290
+
291
+ try {
292
+ vaniHub.handleSignal(signal);
293
+
294
+ // Forward signal through mesh gossip
295
+ gossip.spreadRumor('vani:signal', {
296
+ signal,
297
+ origin: identity.identity.nodeId,
298
+ });
299
+
300
+ res.json({ success: true });
301
+ } catch (error) {
302
+ res.status(500).json({ error: error.message });
303
+ }
304
+ });
305
+
306
+ /**
307
+ * POST /komm/vani/call/:callId/end — End a call
308
+ */
309
+ router.post('/vani/call/:callId/end', requirePeerAuth, (req, res) => {
310
+ const { callId } = req.params;
311
+ const { reason } = req.body;
312
+
313
+ const call = vaniHub.calls.get(callId);
314
+ if (!call) {
315
+ return res.status(404).json({ error: 'Call not found' });
316
+ }
317
+
318
+ call.end(reason || 'USER_HANGUP');
319
+ res.json({ success: true });
320
+ });
321
+
322
+ // ═══════════════════════════════════════════════════════════════════════════
323
+ // YURT — Room Directory & Discovery
324
+ // ═══════════════════════════════════════════════════════════════════════════
325
+
326
+ /**
327
+ * GET /komm/yurt/rooms — List discovered rooms
328
+ */
329
+ router.get('/yurt/rooms', (req, res) => {
330
+ const tag = req.query.tag || null;
331
+ const includeExpired = req.query.includeExpired === 'true';
332
+
333
+ // YurtDirectory uses search() — no list() method
334
+ let entries = typeof yurtHub.directory.search === 'function'
335
+ ? yurtHub.directory.search({ limit: 200 })
336
+ : Array.from(yurtHub.directory.entries?.values?.() || []);
337
+
338
+ if (tag) {
339
+ entries = entries.filter(e => e.tags?.includes(tag));
340
+ }
341
+ if (!includeExpired) {
342
+ entries = entries.filter(e => !e.isExpired?.());
343
+ }
344
+
345
+ res.json({
346
+ rooms: entries.map(e => e.toJSON ? e.toJSON() : e),
347
+ total: entries.length,
348
+ });
349
+ });
350
+
351
+ /**
352
+ * GET /komm/yurt/room/:bundleId — Get room details
353
+ */
354
+ router.get('/yurt/room/:bundleId', (req, res) => {
355
+ const { bundleId } = req.params;
356
+ const entry = yurtHub.directory.get(bundleId);
357
+
358
+ if (!entry) {
359
+ return res.status(404).json({ error: 'Room not found' });
360
+ }
361
+
362
+ res.json(entry.toJSON ? entry.toJSON() : entry);
363
+ });
364
+
365
+ /**
366
+ * POST /komm/yurt/publish — Publish a room to the directory
367
+ */
368
+ router.post('/yurt/publish', writeLimiter, requirePeerAuth, (req, res) => {
369
+ const { bundleId, name, description, tags, visibility } = req.body;
370
+
371
+ if (!bundleId) {
372
+ return res.status(400).json({ error: 'bundleId required' });
373
+ }
374
+
375
+ try {
376
+ const result = yurtHub.publishRoom(bundleId, {
377
+ name,
378
+ description,
379
+ tags,
380
+ visibility,
381
+ });
382
+
383
+ res.json(result);
384
+ } catch (error) {
385
+ res.status(500).json({ error: error.message });
386
+ }
387
+ });
388
+
389
+ /**
390
+ * POST /komm/yurt/search — Search for rooms via gossip
391
+ */
392
+ router.post('/yurt/search', requirePeerAuth, (req, res) => {
393
+ const { query, tags, limit } = req.body;
394
+
395
+ try {
396
+ const results = yurtHub.search?.({ query, tags, limit }) ||
397
+ yurtHub.gossip.query?.({ query, tags }) ||
398
+ { results: [] };
399
+ res.json(results);
400
+ } catch (error) {
401
+ res.status(500).json({ error: error.message });
402
+ }
403
+ });
404
+
405
+ /**
406
+ * DELETE /komm/yurt/room/:bundleId — Unpublish a room
407
+ */
408
+ router.delete('/yurt/room/:bundleId', writeLimiter, requirePeerAuth, (req, res) => {
409
+ const { bundleId } = req.params;
410
+
411
+ try {
412
+ yurtHub.unpublishRoom?.(bundleId);
413
+ res.json({ success: true });
414
+ } catch (error) {
415
+ res.status(500).json({ error: error.message });
416
+ }
417
+ });
418
+
419
+ // ═══════════════════════════════════════════════════════════════════════════
420
+ // GUMBA — Access Control (Bundles, Proofs, Membership)
421
+ // ═══════════════════════════════════════════════════════════════════════════
422
+
423
+ /**
424
+ * GET /komm/gumba/bundles — List all bundles
425
+ */
426
+ router.get('/gumba/bundles', (req, res) => {
427
+ res.json({ bundles: gumbaHub.listBundles() });
428
+ });
429
+
430
+ /**
431
+ * GET /komm/gumba/bundle/:bundleId — Get bundle info
432
+ */
433
+ router.get('/gumba/bundle/:bundleId', (req, res) => {
434
+ const { bundleId } = req.params;
435
+ const bundle = gumbaHub.getBundle(bundleId);
436
+
437
+ if (!bundle) {
438
+ return res.status(404).json({ error: 'Bundle not found' });
439
+ }
440
+
441
+ res.json(bundle.getInfo());
442
+ });
443
+
444
+ /**
445
+ * POST /komm/gumba/bundle — Create a new bundle (room)
446
+ */
447
+ router.post('/gumba/bundle', writeLimiter, requirePeerAuth, (req, res) => {
448
+ const { bundleId, name, description, maxMembers } = req.body;
449
+
450
+ if (!bundleId) {
451
+ return res.status(400).json({ error: 'bundleId required' });
452
+ }
453
+
454
+ try {
455
+ const info = gumbaHub.createBundle(bundleId, { name, description, maxMembers });
456
+ res.json({ success: true, bundle: info });
457
+ } catch (error) {
458
+ res.status(400).json({ error: error.message });
459
+ }
460
+ });
461
+
462
+ /**
463
+ * POST /komm/gumba/bundle/:bundleId/access — Request access to a bundle
464
+ */
465
+ router.post('/gumba/bundle/:bundleId/access', writeLimiter, requirePeerAuth, (req, res) => {
466
+ const { bundleId } = req.params;
467
+ const { proof, visitorNodeId } = req.body;
468
+
469
+ if (!proof) {
470
+ return res.status(400).json({ error: 'proof object required' });
471
+ }
472
+
473
+ gumbaHub.handleAccessRequest(
474
+ bundleId,
475
+ proof,
476
+ visitorNodeId || req.authenticatedPeer || 'unknown'
477
+ ).then(result => {
478
+ res.json(result);
479
+ }).catch(error => {
480
+ res.status(500).json({ error: error.message });
481
+ });
482
+ });
483
+
484
+ /**
485
+ * POST /komm/gumba/bundle/:bundleId/member — Add a member
486
+ */
487
+ router.post('/gumba/bundle/:bundleId/member', writeLimiter, requirePeerAuth, (req, res) => {
488
+ const { bundleId } = req.params;
489
+ const { dokoId, role } = req.body;
490
+
491
+ if (!dokoId) {
492
+ return res.status(400).json({ error: 'dokoId required' });
493
+ }
494
+
495
+ const bundle = gumbaHub.getBundle(bundleId);
496
+ if (!bundle) {
497
+ return res.status(404).json({ error: 'Bundle not found' });
498
+ }
499
+
500
+ try {
501
+ const result = bundle.addMember(dokoId, role);
502
+
503
+ gossip.spreadRumor('gumba:member:added', {
504
+ bundleId,
505
+ dokoId,
506
+ role,
507
+ origin: identity.identity.nodeId,
508
+ });
509
+
510
+ res.json({ success: true, result });
511
+ } catch (error) {
512
+ res.status(400).json({ error: error.message });
513
+ }
514
+ });
515
+
516
+ /**
517
+ * DELETE /komm/gumba/bundle/:bundleId/member/:dokoId — Remove a member
518
+ */
519
+ router.delete('/gumba/bundle/:bundleId/member/:dokoId', writeLimiter, requirePeerAuth, (req, res) => {
520
+ const { bundleId, dokoId } = req.params;
521
+
522
+ const bundle = gumbaHub.getBundle(bundleId);
523
+ if (!bundle) {
524
+ return res.status(404).json({ error: 'Bundle not found' });
525
+ }
526
+
527
+ try {
528
+ bundle.removeMember(dokoId);
529
+
530
+ gossip.spreadRumor('gumba:member:removed', {
531
+ bundleId,
532
+ dokoId,
533
+ origin: identity.identity.nodeId,
534
+ });
535
+
536
+ res.json({ success: true });
537
+ } catch (error) {
538
+ res.status(400).json({ error: error.message });
539
+ }
540
+ });
541
+
542
+ /**
543
+ * POST /komm/gumba/bundle/:bundleId/message — Send message to bundle (ANNEX-encrypted)
544
+ */
545
+ router.post('/gumba/bundle/:bundleId/message', writeLimiter, requirePeerAuth, (req, res) => {
546
+ const { bundleId } = req.params;
547
+ const { sessionId, content, contentType } = req.body;
548
+
549
+ const bundle = gumbaHub.getBundle(bundleId);
550
+ if (!bundle) {
551
+ return res.status(404).json({ error: 'Bundle not found' });
552
+ }
553
+
554
+ // Verify session
555
+ const session = gumbaHub.sessions.get(sessionId);
556
+ if (!session || session.bundleId !== bundleId) {
557
+ return res.status(403).json({ error: 'Invalid or expired session' });
558
+ }
559
+ if (session.expiresAt < Date.now()) {
560
+ gumbaHub.sessions.delete(sessionId);
561
+ return res.status(403).json({ error: 'Session expired' });
562
+ }
563
+
564
+ try {
565
+ const result = bundle.addMessage({
566
+ content,
567
+ contentType,
568
+ senderDokoId: session.dokoId,
569
+ role: session.role,
570
+ });
571
+
572
+ // Broadcast encrypted via gossip (GUMBA handles encryption)
573
+ gossip.spreadRumor('gumba:message', {
574
+ bundleId,
575
+ origin: identity.identity.nodeId,
576
+ encrypted: true,
577
+ timestamp: Date.now(),
578
+ });
579
+
580
+ res.json({ success: true, messageId: result?.messageId });
581
+ } catch (error) {
582
+ res.status(500).json({ error: error.message });
583
+ }
584
+ });
585
+
586
+ log.info('✓ KOMM API initialized (KATHA + VANI + YURT + GUMBA)');
587
+
588
+ return router;
589
+ }
590
+
591
+ /**
592
+ * Wire KOMM gossip handlers into the mesh rumor stream.
593
+ * Called once during server initialization.
594
+ *
595
+ * @param {Object} mesh - MeshNetwork instance
596
+ * @param {Object} kathaHub - KathaHub instance
597
+ * @param {Object} vaniHub - VaniHub instance
598
+ * @param {Object} yurtHub - YurtHub instance
599
+ * @param {Object} gumbaHub - GumbaHub instance
600
+ */
601
+ export function wireKommGossip(mesh, kathaHub, vaniHub, yurtHub, gumbaHub) {
602
+ mesh.on('rumor', (topic, data, origin) => {
603
+ // KATHA events (chat messages, reactions, edits, deletes)
604
+ if (topic === 'katha:event') {
605
+ kathaHub.handleEvent(data);
606
+ }
607
+
608
+ // KATHA typing indicators (ephemeral)
609
+ if (topic === 'katha:typing') {
610
+ const channel = kathaHub.getChannel(data.channelId);
611
+ if (channel) {
612
+ channel.setTyping(data.userId, data.typing);
613
+ }
614
+ }
615
+
616
+ // VANI signals (WebRTC signaling for calls)
617
+ if (topic === 'vani:signal') {
618
+ vaniHub.handleSignal(data.signal);
619
+ }
620
+
621
+ // YURT room announcements
622
+ if (topic === 'yurt:register' || topic === 'yurt:announce') {
623
+ yurtHub.gossip?.handleRemoteAnnounce?.(data) ||
624
+ yurtHub.directory.add?.(data);
625
+ }
626
+
627
+ // YURT room removals
628
+ if (topic === 'yurt:unregister') {
629
+ yurtHub.directory.remove?.(data.entryId || data.bundleId);
630
+ }
631
+
632
+ // YURT relay (message forwarding between rooms on different nodes)
633
+ if (topic === 'yurt:relay') {
634
+ // Deliver to local KATHA channel if we host this bundle
635
+ if (gumbaHub.getBundle(data.bundleId)) {
636
+ // Explicit allowlist — never spread remote peer data directly (proto pollution defense)
637
+ const evt = data.event || {};
638
+ kathaHub.handleEvent({
639
+ channelId: data.bundleId,
640
+ type: evt.type,
641
+ content: evt.content,
642
+ messageId: evt.messageId,
643
+ userId: evt.userId,
644
+ emoji: evt.emoji,
645
+ parentId: evt.parentId,
646
+ threadId: evt.threadId,
647
+ timestamp: evt.timestamp,
648
+ });
649
+ }
650
+ }
651
+
652
+ // GUMBA member changes
653
+ if (topic === 'gumba:member:added') {
654
+ const bundle = gumbaHub.getBundle(data.bundleId);
655
+ if (bundle) {
656
+ try { bundle.addMember(data.dokoId, data.role); } catch { /* already member */ }
657
+ }
658
+ }
659
+ if (topic === 'gumba:member:removed') {
660
+ const bundle = gumbaHub.getBundle(data.bundleId);
661
+ if (bundle) {
662
+ try { bundle.removeMember(data.dokoId); } catch { /* not member */ }
663
+ }
664
+ }
665
+ });
666
+
667
+ log.info('✓ KOMM gossip handlers wired into mesh');
668
+ }