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/katha.js ADDED
@@ -0,0 +1,1012 @@
1
+ /**
2
+ * KATHA - Kommunication And Threading Handler Architecture
3
+ *
4
+ * Chat layer for Yakmesh providing rich messaging features:
5
+ * - Text messages with formatting
6
+ * - Reactions (emoji responses to messages)
7
+ * - Typing indicators (ephemeral presence)
8
+ * - Reply/threading (message relationships)
9
+ * - Read receipts (optional acknowledgment)
10
+ * - Media embeds (images, GIFs as base64)
11
+ *
12
+ * Etymology: कथा (katha) = story, talk, narrative in Sanskrit
13
+ *
14
+ * SECURITY POLICY (2026-02-11):
15
+ * KATHA REQUIRES ANNEX encryption for ALL message transport.
16
+ * - Direct channels: ANNEX session must be established
17
+ * - GUMBA bundles: Bundle encryption via ANNEX
18
+ * - No plaintext messages permitted on wire
19
+ *
20
+ * @module mesh/katha
21
+ * @license MIT
22
+ * @copyright 2026 YAKMESH™ Contributors
23
+ */
24
+
25
+ import { randomBytes } from 'crypto';
26
+ import { sha3_256 } from '@noble/hashes/sha3.js';
27
+ import { bytesToHex } from '@noble/hashes/utils.js';
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+ // CONFIGURATION
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+
33
+ export const KATHA_CONFIG = Object.freeze({
34
+ // Message constraints
35
+ maxMessageLength: 4000, // Max text length (like Discord)
36
+ maxReactionEmojis: 20, // Max unique reactions per message
37
+ maxReactionsPerUser: 1, // One reaction per user per message
38
+ maxMediaSize: 10 * 1024 * 1024, // 10MB max media
39
+
40
+ // Typing indicator
41
+ typingTimeout: 5000, // Typing expires after 5 seconds
42
+ typingThrottle: 3000, // Don't send more than once per 3s
43
+
44
+ // Threading
45
+ maxThreadDepth: 1, // Direct replies only (no nested threads)
46
+ maxThreadReplies: 1000, // Max replies to a single message
47
+
48
+ // Read receipts
49
+ receiptBatchInterval: 1000, // Batch receipts every 1s
50
+ maxReceiptBatch: 50, // Max messages in one receipt batch
51
+
52
+ // Message types
53
+ messageTypes: {
54
+ TEXT: 'katha:text',
55
+ REACTION_ADD: 'katha:reaction:add',
56
+ REACTION_REMOVE: 'katha:reaction:remove',
57
+ TYPING_START: 'katha:typing:start',
58
+ TYPING_STOP: 'katha:typing:stop',
59
+ READ_RECEIPT: 'katha:read',
60
+ EDIT: 'katha:edit',
61
+ DELETE: 'katha:delete',
62
+ MEDIA: 'katha:media',
63
+ },
64
+
65
+ // Media types
66
+ mediaTypes: {
67
+ IMAGE: 'image',
68
+ GIF: 'gif',
69
+ FILE: 'file',
70
+ },
71
+ });
72
+
73
+ // ═══════════════════════════════════════════════════════════════════════════════
74
+ // KATHA MESSAGE - Base chat message
75
+ // ═══════════════════════════════════════════════════════════════════════════════
76
+
77
+ /**
78
+ * KathaMessage - A chat message in the KATHA protocol
79
+ *
80
+ * Immutable once created. Edits create new messages with editOf reference.
81
+ */
82
+ export class KathaMessage {
83
+ /**
84
+ * @param {Object} options
85
+ * @param {string} options.id - Unique message ID
86
+ * @param {string} options.channelId - Channel/room this message belongs to
87
+ * @param {string} options.senderId - Sender's node ID
88
+ * @param {string} options.content - Message text content
89
+ * @param {string} [options.replyTo] - ID of message being replied to
90
+ * @param {number} [options.timestamp] - Unix timestamp
91
+ */
92
+ constructor(options = {}) {
93
+ this.id = options.id || KathaMessage.generateId();
94
+ this.type = KATHA_CONFIG.messageTypes.TEXT;
95
+ this.channelId = options.channelId;
96
+ this.senderId = options.senderId;
97
+ this.content = options.content || '';
98
+ this.replyTo = options.replyTo || null;
99
+ this.timestamp = options.timestamp !== undefined ? options.timestamp : Date.now();
100
+ this.editedAt = options.editedAt || null;
101
+ this.reactions = new Map(); // emoji -> Set of userIds
102
+ }
103
+
104
+ /**
105
+ * Generate a unique message ID
106
+ */
107
+ static generateId() {
108
+ const timestamp = Date.now().toString(36);
109
+ const random = bytesToHex(randomBytes(8));
110
+ return `${timestamp}-${random}`;
111
+ }
112
+
113
+ /**
114
+ * Validate message content
115
+ */
116
+ validate() {
117
+ const errors = [];
118
+
119
+ if (!this.channelId) {
120
+ errors.push('channelId is required');
121
+ }
122
+ if (!this.senderId) {
123
+ errors.push('senderId is required');
124
+ }
125
+ if (typeof this.content !== 'string') {
126
+ errors.push('content must be a string');
127
+ }
128
+ if (this.content.length > KATHA_CONFIG.maxMessageLength) {
129
+ errors.push(`content exceeds max length of ${KATHA_CONFIG.maxMessageLength}`);
130
+ }
131
+
132
+ return {
133
+ valid: errors.length === 0,
134
+ errors,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Add a reaction
140
+ * @returns {boolean} true if reaction was added
141
+ */
142
+ addReaction(emoji, userId) {
143
+ if (!emoji || !userId) return false;
144
+
145
+ // Check limits
146
+ if (this.reactions.size >= KATHA_CONFIG.maxReactionEmojis && !this.reactions.has(emoji)) {
147
+ return false;
148
+ }
149
+
150
+ if (!this.reactions.has(emoji)) {
151
+ this.reactions.set(emoji, new Set());
152
+ }
153
+
154
+ const users = this.reactions.get(emoji);
155
+ if (users.has(userId)) return false;
156
+
157
+ users.add(userId);
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Remove a reaction
163
+ * @returns {boolean} true if reaction was removed
164
+ */
165
+ removeReaction(emoji, userId) {
166
+ if (!this.reactions.has(emoji)) return false;
167
+
168
+ const users = this.reactions.get(emoji);
169
+ const removed = users.delete(userId);
170
+
171
+ // Clean up empty reaction sets
172
+ if (users.size === 0) {
173
+ this.reactions.delete(emoji);
174
+ }
175
+
176
+ return removed;
177
+ }
178
+
179
+ /**
180
+ * Get reaction counts
181
+ */
182
+ getReactionCounts() {
183
+ const counts = {};
184
+ for (const [emoji, users] of this.reactions) {
185
+ counts[emoji] = users.size;
186
+ }
187
+ return counts;
188
+ }
189
+
190
+ /**
191
+ * Check if user reacted with emoji
192
+ */
193
+ hasUserReaction(emoji, userId) {
194
+ return this.reactions.has(emoji) && this.reactions.get(emoji).has(userId);
195
+ }
196
+
197
+ /**
198
+ * Serialize for transmission
199
+ */
200
+ toJSON() {
201
+ // Convert reactions Map to plain object
202
+ const reactions = {};
203
+ for (const [emoji, users] of this.reactions) {
204
+ reactions[emoji] = Array.from(users);
205
+ }
206
+
207
+ return {
208
+ id: this.id,
209
+ type: this.type,
210
+ channelId: this.channelId,
211
+ senderId: this.senderId,
212
+ content: this.content,
213
+ replyTo: this.replyTo,
214
+ timestamp: this.timestamp,
215
+ editedAt: this.editedAt,
216
+ reactions,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Deserialize from transmission
222
+ */
223
+ static fromJSON(json) {
224
+ const msg = new KathaMessage({
225
+ id: json.id,
226
+ channelId: json.channelId,
227
+ senderId: json.senderId,
228
+ content: json.content,
229
+ replyTo: json.replyTo,
230
+ timestamp: json.timestamp,
231
+ editedAt: json.editedAt,
232
+ });
233
+ msg.type = json.type || KATHA_CONFIG.messageTypes.TEXT;
234
+
235
+ // Restore reactions
236
+ if (json.reactions) {
237
+ for (const [emoji, users] of Object.entries(json.reactions)) {
238
+ msg.reactions.set(emoji, new Set(users));
239
+ }
240
+ }
241
+
242
+ return msg;
243
+ }
244
+ }
245
+
246
+ // ═══════════════════════════════════════════════════════════════════════════════
247
+ // KATHA REACTION - Reaction event
248
+ // ═══════════════════════════════════════════════════════════════════════════════
249
+
250
+ /**
251
+ * KathaReaction - A reaction add/remove event
252
+ */
253
+ export class KathaReaction {
254
+ constructor(options = {}) {
255
+ this.type = options.add !== false
256
+ ? KATHA_CONFIG.messageTypes.REACTION_ADD
257
+ : KATHA_CONFIG.messageTypes.REACTION_REMOVE;
258
+ this.messageId = options.messageId;
259
+ this.channelId = options.channelId;
260
+ this.userId = options.userId;
261
+ this.emoji = options.emoji;
262
+ this.timestamp = options.timestamp || Date.now();
263
+ }
264
+
265
+ validate() {
266
+ const errors = [];
267
+ if (!this.messageId) errors.push('messageId is required');
268
+ if (!this.channelId) errors.push('channelId is required');
269
+ if (!this.userId) errors.push('userId is required');
270
+ if (!this.emoji) errors.push('emoji is required');
271
+ // Basic emoji validation (single grapheme cluster or shortcode)
272
+ if (this.emoji && this.emoji.length > 32) errors.push('emoji too long');
273
+
274
+ return { valid: errors.length === 0, errors };
275
+ }
276
+
277
+ toJSON() {
278
+ return {
279
+ type: this.type,
280
+ messageId: this.messageId,
281
+ channelId: this.channelId,
282
+ userId: this.userId,
283
+ emoji: this.emoji,
284
+ timestamp: this.timestamp,
285
+ };
286
+ }
287
+
288
+ static fromJSON(json) {
289
+ return new KathaReaction({
290
+ add: json.type === KATHA_CONFIG.messageTypes.REACTION_ADD,
291
+ messageId: json.messageId,
292
+ channelId: json.channelId,
293
+ userId: json.userId,
294
+ emoji: json.emoji,
295
+ timestamp: json.timestamp,
296
+ });
297
+ }
298
+ }
299
+
300
+ // ═══════════════════════════════════════════════════════════════════════════════
301
+ // KATHA TYPING - Typing indicator
302
+ // ═══════════════════════════════════════════════════════════════════════════════
303
+
304
+ /**
305
+ * KathaTyping - Typing indicator event (ephemeral, not stored)
306
+ */
307
+ export class KathaTyping {
308
+ constructor(options = {}) {
309
+ this.type = options.stop
310
+ ? KATHA_CONFIG.messageTypes.TYPING_STOP
311
+ : KATHA_CONFIG.messageTypes.TYPING_START;
312
+ this.channelId = options.channelId;
313
+ this.userId = options.userId;
314
+ this.timestamp = options.timestamp || Date.now();
315
+ }
316
+
317
+ /**
318
+ * Check if this typing indicator has expired
319
+ */
320
+ isExpired() {
321
+ return Date.now() - this.timestamp > KATHA_CONFIG.typingTimeout;
322
+ }
323
+
324
+ toJSON() {
325
+ return {
326
+ type: this.type,
327
+ channelId: this.channelId,
328
+ userId: this.userId,
329
+ timestamp: this.timestamp,
330
+ };
331
+ }
332
+
333
+ static fromJSON(json) {
334
+ return new KathaTyping({
335
+ stop: json.type === KATHA_CONFIG.messageTypes.TYPING_STOP,
336
+ channelId: json.channelId,
337
+ userId: json.userId,
338
+ timestamp: json.timestamp,
339
+ });
340
+ }
341
+ }
342
+
343
+ // ═══════════════════════════════════════════════════════════════════════════════
344
+ // KATHA READ RECEIPT - Read acknowledgment
345
+ // ═══════════════════════════════════════════════════════════════════════════════
346
+
347
+ /**
348
+ * KathaReadReceipt - Batch read receipt
349
+ */
350
+ export class KathaReadReceipt {
351
+ constructor(options = {}) {
352
+ this.type = KATHA_CONFIG.messageTypes.READ_RECEIPT;
353
+ this.channelId = options.channelId;
354
+ this.userId = options.userId;
355
+ this.messageIds = options.messageIds || [];
356
+ this.lastReadId = options.lastReadId || null; // Alternative: just track last read
357
+ this.timestamp = options.timestamp || Date.now();
358
+ }
359
+
360
+ /**
361
+ * Add a message ID to the receipt batch
362
+ */
363
+ addMessage(messageId) {
364
+ if (this.messageIds.length >= KATHA_CONFIG.maxReceiptBatch) {
365
+ return false;
366
+ }
367
+ if (!this.messageIds.includes(messageId)) {
368
+ this.messageIds.push(messageId);
369
+ }
370
+ return true;
371
+ }
372
+
373
+ toJSON() {
374
+ return {
375
+ type: this.type,
376
+ channelId: this.channelId,
377
+ userId: this.userId,
378
+ messageIds: this.messageIds,
379
+ lastReadId: this.lastReadId,
380
+ timestamp: this.timestamp,
381
+ };
382
+ }
383
+
384
+ static fromJSON(json) {
385
+ return new KathaReadReceipt({
386
+ channelId: json.channelId,
387
+ userId: json.userId,
388
+ messageIds: json.messageIds,
389
+ lastReadId: json.lastReadId,
390
+ timestamp: json.timestamp,
391
+ });
392
+ }
393
+ }
394
+
395
+ // ═══════════════════════════════════════════════════════════════════════════════
396
+ // KATHA MEDIA - Image/GIF/File embed
397
+ // ═══════════════════════════════════════════════════════════════════════════════
398
+
399
+ /**
400
+ * KathaMedia - Media attachment
401
+ */
402
+ export class KathaMedia {
403
+ constructor(options = {}) {
404
+ this.id = options.id || KathaMessage.generateId();
405
+ this.type = KATHA_CONFIG.messageTypes.MEDIA;
406
+ this.mediaType = options.mediaType || KATHA_CONFIG.mediaTypes.IMAGE;
407
+ this.channelId = options.channelId;
408
+ this.senderId = options.senderId;
409
+ this.filename = options.filename || null;
410
+ this.mimeType = options.mimeType || 'application/octet-stream';
411
+ this.size = options.size || 0;
412
+ this.data = options.data || null; // Base64 encoded
413
+ this.hash = options.hash || null; // SHA3-256 of raw data for integrity
414
+ this.caption = options.caption || null;
415
+ this.replyTo = options.replyTo || null;
416
+ this.timestamp = options.timestamp || Date.now();
417
+ }
418
+
419
+ /**
420
+ * Create from a buffer
421
+ */
422
+ static fromBuffer(buffer, options = {}) {
423
+ const base64 = buffer.toString('base64');
424
+ const hash = bytesToHex(sha3_256(buffer));
425
+
426
+ return new KathaMedia({
427
+ ...options,
428
+ data: base64,
429
+ size: buffer.length,
430
+ hash,
431
+ });
432
+ }
433
+
434
+ /**
435
+ * Get the raw buffer
436
+ */
437
+ toBuffer() {
438
+ if (!this.data) return null;
439
+ return Buffer.from(this.data, 'base64');
440
+ }
441
+
442
+ /**
443
+ * Verify data integrity
444
+ */
445
+ verify() {
446
+ if (!this.data || !this.hash) return false;
447
+ const buffer = this.toBuffer();
448
+ const computed = bytesToHex(sha3_256(buffer));
449
+ return computed === this.hash;
450
+ }
451
+
452
+ validate() {
453
+ const errors = [];
454
+ if (!this.channelId) errors.push('channelId is required');
455
+ if (!this.senderId) errors.push('senderId is required');
456
+ if (!this.data) errors.push('data is required');
457
+ if (this.size > KATHA_CONFIG.maxMediaSize) {
458
+ errors.push(`size exceeds max of ${KATHA_CONFIG.maxMediaSize} bytes`);
459
+ }
460
+ if (!Object.values(KATHA_CONFIG.mediaTypes).includes(this.mediaType)) {
461
+ errors.push('invalid mediaType');
462
+ }
463
+
464
+ return { valid: errors.length === 0, errors };
465
+ }
466
+
467
+ toJSON() {
468
+ return {
469
+ id: this.id,
470
+ type: this.type,
471
+ mediaType: this.mediaType,
472
+ channelId: this.channelId,
473
+ senderId: this.senderId,
474
+ filename: this.filename,
475
+ mimeType: this.mimeType,
476
+ size: this.size,
477
+ data: this.data,
478
+ hash: this.hash,
479
+ caption: this.caption,
480
+ replyTo: this.replyTo,
481
+ timestamp: this.timestamp,
482
+ };
483
+ }
484
+
485
+ static fromJSON(json) {
486
+ return new KathaMedia({
487
+ id: json.id,
488
+ mediaType: json.mediaType,
489
+ channelId: json.channelId,
490
+ senderId: json.senderId,
491
+ filename: json.filename,
492
+ mimeType: json.mimeType,
493
+ size: json.size,
494
+ data: json.data,
495
+ hash: json.hash,
496
+ caption: json.caption,
497
+ replyTo: json.replyTo,
498
+ timestamp: json.timestamp,
499
+ });
500
+ }
501
+ }
502
+
503
+ // ═══════════════════════════════════════════════════════════════════════════════
504
+ // KATHA EDIT - Message edit
505
+ // ═══════════════════════════════════════════════════════════════════════════════
506
+
507
+ /**
508
+ * KathaEdit - Message edit event
509
+ */
510
+ export class KathaEdit {
511
+ constructor(options = {}) {
512
+ this.type = KATHA_CONFIG.messageTypes.EDIT;
513
+ this.messageId = options.messageId;
514
+ this.channelId = options.channelId;
515
+ this.userId = options.userId;
516
+ this.newContent = options.newContent;
517
+ this.timestamp = options.timestamp || Date.now();
518
+ }
519
+
520
+ validate() {
521
+ const errors = [];
522
+ if (!this.messageId) errors.push('messageId is required');
523
+ if (!this.channelId) errors.push('channelId is required');
524
+ if (!this.userId) errors.push('userId is required');
525
+ if (typeof this.newContent !== 'string') errors.push('newContent must be a string');
526
+ if (this.newContent && this.newContent.length > KATHA_CONFIG.maxMessageLength) {
527
+ errors.push(`newContent exceeds max length of ${KATHA_CONFIG.maxMessageLength}`);
528
+ }
529
+
530
+ return { valid: errors.length === 0, errors };
531
+ }
532
+
533
+ toJSON() {
534
+ return {
535
+ type: this.type,
536
+ messageId: this.messageId,
537
+ channelId: this.channelId,
538
+ userId: this.userId,
539
+ newContent: this.newContent,
540
+ timestamp: this.timestamp,
541
+ };
542
+ }
543
+
544
+ static fromJSON(json) {
545
+ return new KathaEdit({
546
+ messageId: json.messageId,
547
+ channelId: json.channelId,
548
+ userId: json.userId,
549
+ newContent: json.newContent,
550
+ timestamp: json.timestamp,
551
+ });
552
+ }
553
+ }
554
+
555
+ // ═══════════════════════════════════════════════════════════════════════════════
556
+ // KATHA DELETE - Message deletion
557
+ // ═══════════════════════════════════════════════════════════════════════════════
558
+
559
+ /**
560
+ * KathaDelete - Message deletion event
561
+ */
562
+ export class KathaDelete {
563
+ constructor(options = {}) {
564
+ this.type = KATHA_CONFIG.messageTypes.DELETE;
565
+ this.messageId = options.messageId;
566
+ this.channelId = options.channelId;
567
+ this.userId = options.userId;
568
+ this.timestamp = options.timestamp || Date.now();
569
+ }
570
+
571
+ validate() {
572
+ const errors = [];
573
+ if (!this.messageId) errors.push('messageId is required');
574
+ if (!this.channelId) errors.push('channelId is required');
575
+ if (!this.userId) errors.push('userId is required');
576
+
577
+ return { valid: errors.length === 0, errors };
578
+ }
579
+
580
+ toJSON() {
581
+ return {
582
+ type: this.type,
583
+ messageId: this.messageId,
584
+ channelId: this.channelId,
585
+ userId: this.userId,
586
+ timestamp: this.timestamp,
587
+ };
588
+ }
589
+
590
+ static fromJSON(json) {
591
+ return new KathaDelete({
592
+ messageId: json.messageId,
593
+ channelId: json.channelId,
594
+ userId: json.userId,
595
+ timestamp: json.timestamp,
596
+ });
597
+ }
598
+ }
599
+
600
+ // ═══════════════════════════════════════════════════════════════════════════════
601
+ // KATHA CHANNEL - Channel/room state manager
602
+ // ═══════════════════════════════════════════════════════════════════════════════
603
+
604
+ /**
605
+ * KathaChannel - Manages chat state for a channel
606
+ */
607
+ export class KathaChannel {
608
+ constructor(channelId) {
609
+ this.channelId = channelId;
610
+ this.messages = new Map(); // id -> KathaMessage
611
+ this.threads = new Map(); // parentId -> Set of replyIds
612
+ this.typing = new Map(); // userId -> timestamp
613
+ this.readReceipts = new Map(); // userId -> lastReadId
614
+ this._typingCleanupInterval = null;
615
+ }
616
+
617
+ /**
618
+ * Start typing indicator cleanup
619
+ */
620
+ startTypingCleanup() {
621
+ if (this._typingCleanupInterval) return;
622
+
623
+ this._typingCleanupInterval = setInterval(() => {
624
+ const now = Date.now();
625
+ for (const [userId, timestamp] of this.typing) {
626
+ if (now - timestamp > KATHA_CONFIG.typingTimeout) {
627
+ this.typing.delete(userId);
628
+ }
629
+ }
630
+ }, 1000);
631
+ }
632
+
633
+ /**
634
+ * Stop cleanup interval
635
+ */
636
+ stopTypingCleanup() {
637
+ if (this._typingCleanupInterval) {
638
+ clearInterval(this._typingCleanupInterval);
639
+ this._typingCleanupInterval = null;
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Add a message
645
+ */
646
+ addMessage(message) {
647
+ if (!(message instanceof KathaMessage)) {
648
+ message = KathaMessage.fromJSON(message);
649
+ }
650
+
651
+ this.messages.set(message.id, message);
652
+
653
+ // Track threading
654
+ if (message.replyTo) {
655
+ if (!this.threads.has(message.replyTo)) {
656
+ this.threads.set(message.replyTo, new Set());
657
+ }
658
+ this.threads.get(message.replyTo).add(message.id);
659
+ }
660
+
661
+ return message;
662
+ }
663
+
664
+ /**
665
+ * Get a message
666
+ */
667
+ getMessage(messageId) {
668
+ return this.messages.get(messageId) || null;
669
+ }
670
+
671
+ /**
672
+ * Get thread replies
673
+ */
674
+ getThreadReplies(messageId) {
675
+ const replyIds = this.threads.get(messageId);
676
+ if (!replyIds) return [];
677
+
678
+ return Array.from(replyIds)
679
+ .map(id => this.messages.get(id))
680
+ .filter(Boolean)
681
+ .sort((a, b) => a.timestamp - b.timestamp);
682
+ }
683
+
684
+ /**
685
+ * Apply a reaction event
686
+ */
687
+ applyReaction(reaction) {
688
+ const message = this.messages.get(reaction.messageId);
689
+ if (!message) return false;
690
+
691
+ if (reaction.type === KATHA_CONFIG.messageTypes.REACTION_ADD) {
692
+ return message.addReaction(reaction.emoji, reaction.userId);
693
+ } else {
694
+ return message.removeReaction(reaction.emoji, reaction.userId);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Set typing indicator
700
+ */
701
+ setTyping(userId, isTyping = true) {
702
+ if (isTyping) {
703
+ this.typing.set(userId, Date.now());
704
+ } else {
705
+ this.typing.delete(userId);
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Get users currently typing
711
+ */
712
+ getTypingUsers() {
713
+ const now = Date.now();
714
+ const users = [];
715
+
716
+ for (const [userId, timestamp] of this.typing) {
717
+ if (now - timestamp <= KATHA_CONFIG.typingTimeout) {
718
+ users.push(userId);
719
+ }
720
+ }
721
+
722
+ return users;
723
+ }
724
+
725
+ /**
726
+ * Apply read receipt
727
+ */
728
+ applyReadReceipt(receipt) {
729
+ this.readReceipts.set(receipt.userId, receipt.lastReadId || receipt.messageIds[receipt.messageIds.length - 1]);
730
+ }
731
+
732
+ /**
733
+ * Get read status for a message
734
+ */
735
+ getReadBy(messageId) {
736
+ const readers = [];
737
+
738
+ for (const [userId, lastReadId] of this.readReceipts) {
739
+ const lastRead = this.messages.get(lastReadId);
740
+ const target = this.messages.get(messageId);
741
+
742
+ if (lastRead && target && lastRead.timestamp >= target.timestamp) {
743
+ readers.push(userId);
744
+ }
745
+ }
746
+
747
+ return readers;
748
+ }
749
+
750
+ /**
751
+ * Edit a message
752
+ */
753
+ editMessage(edit) {
754
+ const message = this.messages.get(edit.messageId);
755
+ if (!message) return null;
756
+
757
+ // Only sender can edit
758
+ if (message.senderId !== edit.userId) return null;
759
+
760
+ message.content = edit.newContent;
761
+ message.editedAt = edit.timestamp;
762
+
763
+ return message;
764
+ }
765
+
766
+ /**
767
+ * Delete a message
768
+ */
769
+ deleteMessage(deletion) {
770
+ const message = this.messages.get(deletion.messageId);
771
+ if (!message) return false;
772
+
773
+ // Only sender can delete
774
+ if (message.senderId !== deletion.userId) return false;
775
+
776
+ // Remove from threads
777
+ if (message.replyTo) {
778
+ const thread = this.threads.get(message.replyTo);
779
+ if (thread) thread.delete(message.id);
780
+ }
781
+
782
+ // Don't delete parent if it has replies - just mark content as deleted
783
+ if (this.threads.has(message.id) && this.threads.get(message.id).size > 0) {
784
+ message.content = '[deleted]';
785
+ message.editedAt = deletion.timestamp;
786
+ return true;
787
+ }
788
+
789
+ this.messages.delete(deletion.messageId);
790
+ return true;
791
+ }
792
+
793
+ /**
794
+ * Get messages in time order
795
+ */
796
+ getMessages(options = {}) {
797
+ const { limit = 50, before, after, threadOnly } = options;
798
+
799
+ let msgs = Array.from(this.messages.values());
800
+
801
+ // Filter by thread
802
+ if (threadOnly !== undefined) {
803
+ if (threadOnly) {
804
+ msgs = msgs.filter(m => m.replyTo !== null);
805
+ } else {
806
+ msgs = msgs.filter(m => m.replyTo === null);
807
+ }
808
+ }
809
+
810
+ // Filter by time
811
+ if (before) {
812
+ msgs = msgs.filter(m => m.timestamp < before);
813
+ }
814
+ if (after) {
815
+ msgs = msgs.filter(m => m.timestamp > after);
816
+ }
817
+
818
+ // Sort by timestamp
819
+ msgs.sort((a, b) => a.timestamp - b.timestamp);
820
+
821
+ // Apply limit
822
+ if (limit) {
823
+ msgs = msgs.slice(-limit);
824
+ }
825
+
826
+ return msgs;
827
+ }
828
+
829
+ /**
830
+ * Get channel stats
831
+ */
832
+ getStats() {
833
+ return {
834
+ channelId: this.channelId,
835
+ messageCount: this.messages.size,
836
+ threadCount: this.threads.size,
837
+ typingUsers: this.getTypingUsers().length,
838
+ };
839
+ }
840
+ }
841
+
842
+ // ═══════════════════════════════════════════════════════════════════════════════
843
+ // KATHA HUB - Multi-channel manager
844
+ // ═══════════════════════════════════════════════════════════════════════════════
845
+
846
+ /**
847
+ * KathaHub - Manages multiple chat channels
848
+ */
849
+ export class KathaHub {
850
+ constructor() {
851
+ this.channels = new Map();
852
+ this.eventHandlers = new Map();
853
+ }
854
+
855
+ /**
856
+ * Get or create a channel
857
+ */
858
+ getChannel(channelId) {
859
+ if (!this.channels.has(channelId)) {
860
+ const channel = new KathaChannel(channelId);
861
+ channel.startTypingCleanup();
862
+ this.channels.set(channelId, channel);
863
+ }
864
+ return this.channels.get(channelId);
865
+ }
866
+
867
+ /**
868
+ * Remove a channel
869
+ */
870
+ removeChannel(channelId) {
871
+ const channel = this.channels.get(channelId);
872
+ if (channel) {
873
+ channel.stopTypingCleanup();
874
+ this.channels.delete(channelId);
875
+ }
876
+ }
877
+
878
+ /**
879
+ * Handle incoming KATHA event
880
+ */
881
+ handleEvent(event) {
882
+ const channelId = event.channelId;
883
+ if (!channelId) return null;
884
+
885
+ const channel = this.getChannel(channelId);
886
+ let result = null;
887
+
888
+ switch (event.type) {
889
+ case KATHA_CONFIG.messageTypes.TEXT:
890
+ result = channel.addMessage(KathaMessage.fromJSON(event));
891
+ break;
892
+
893
+ case KATHA_CONFIG.messageTypes.MEDIA:
894
+ result = channel.addMessage(KathaMedia.fromJSON(event));
895
+ break;
896
+
897
+ case KATHA_CONFIG.messageTypes.REACTION_ADD:
898
+ case KATHA_CONFIG.messageTypes.REACTION_REMOVE:
899
+ result = channel.applyReaction(KathaReaction.fromJSON(event));
900
+ break;
901
+
902
+ case KATHA_CONFIG.messageTypes.TYPING_START:
903
+ channel.setTyping(event.userId, true);
904
+ result = true;
905
+ break;
906
+
907
+ case KATHA_CONFIG.messageTypes.TYPING_STOP:
908
+ channel.setTyping(event.userId, false);
909
+ result = true;
910
+ break;
911
+
912
+ case KATHA_CONFIG.messageTypes.READ_RECEIPT:
913
+ channel.applyReadReceipt(KathaReadReceipt.fromJSON(event));
914
+ result = true;
915
+ break;
916
+
917
+ case KATHA_CONFIG.messageTypes.EDIT:
918
+ result = channel.editMessage(KathaEdit.fromJSON(event));
919
+ break;
920
+
921
+ case KATHA_CONFIG.messageTypes.DELETE:
922
+ result = channel.deleteMessage(KathaDelete.fromJSON(event));
923
+ break;
924
+ }
925
+
926
+ // Emit event
927
+ this._emit(event.type, event, result);
928
+
929
+ return result;
930
+ }
931
+
932
+ /**
933
+ * Register event handler
934
+ */
935
+ on(eventType, handler) {
936
+ if (!this.eventHandlers.has(eventType)) {
937
+ this.eventHandlers.set(eventType, []);
938
+ }
939
+ this.eventHandlers.get(eventType).push(handler);
940
+ }
941
+
942
+ /**
943
+ * Remove event handler
944
+ */
945
+ off(eventType, handler) {
946
+ const handlers = this.eventHandlers.get(eventType);
947
+ if (handlers) {
948
+ const idx = handlers.indexOf(handler);
949
+ if (idx >= 0) handlers.splice(idx, 1);
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Emit event to handlers
955
+ */
956
+ _emit(eventType, event, result) {
957
+ const handlers = this.eventHandlers.get(eventType) || [];
958
+ for (const handler of handlers) {
959
+ try {
960
+ handler(event, result);
961
+ } catch (err) {
962
+ console.error('Katha handler error:', err);
963
+ }
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Cleanup all channels
969
+ */
970
+ cleanup() {
971
+ for (const channel of this.channels.values()) {
972
+ channel.stopTypingCleanup();
973
+ }
974
+ this.channels.clear();
975
+ }
976
+
977
+ /**
978
+ * Get hub stats
979
+ */
980
+ getStats() {
981
+ const stats = {
982
+ channelCount: this.channels.size,
983
+ totalMessages: 0,
984
+ channels: {},
985
+ };
986
+
987
+ for (const [id, channel] of this.channels) {
988
+ const channelStats = channel.getStats();
989
+ stats.totalMessages += channelStats.messageCount;
990
+ stats.channels[id] = channelStats;
991
+ }
992
+
993
+ return stats;
994
+ }
995
+ }
996
+
997
+ // ═══════════════════════════════════════════════════════════════════════════════
998
+ // EXPORTS
999
+ // ═══════════════════════════════════════════════════════════════════════════════
1000
+
1001
+ export default {
1002
+ KATHA_CONFIG,
1003
+ KathaMessage,
1004
+ KathaReaction,
1005
+ KathaTyping,
1006
+ KathaReadReceipt,
1007
+ KathaMedia,
1008
+ KathaEdit,
1009
+ KathaDelete,
1010
+ KathaChannel,
1011
+ KathaHub,
1012
+ };