yakmesh 2.8.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +637 -0
- package/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +334 -74
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +28 -13
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
|
@@ -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
|
+
}
|