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,247 @@
1
+ /**
2
+ * Server-side ANNEX - Post-Quantum Encrypted Sessions
3
+ *
4
+ * Handles ANNEX key exchange and message encryption/decryption for
5
+ * browser clients connecting via WebSocket.
6
+ *
7
+ * Protocol (server perspective):
8
+ * 1. Receive client's ML-KEM-768 public key
9
+ * 2. Encapsulate shared secret, send ciphertext to client
10
+ * 3. Both derive AES-256-GCM key from shared secret
11
+ * 4. All subsequent messages encrypted/decrypted via session
12
+ *
13
+ * @module server/crypto/annex
14
+ * @license MIT
15
+ * @copyright 2026 YAKMESH Contributors
16
+ */
17
+
18
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
19
+ import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
20
+ import { sha3_256 } from '@noble/hashes/sha3.js';
21
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
22
+
23
+ // Configuration (must match client)
24
+ const ANNEX_CONFIG = {
25
+ nonceSize: 12,
26
+ keyDerivationSalt: 'YAKMESH-ANNEX-VANI-2026',
27
+ sessionTimeout: 3600000, // 1 hour session lifetime
28
+ maxMessagesPerKey: 50000, // Force re-handshake after N messages
29
+ };
30
+
31
+ /**
32
+ * ServerAnnexSession - Server-side encrypted session with a browser client
33
+ */
34
+ export class ServerAnnexSession {
35
+ constructor(options = {}) {
36
+ this.sessionId = options.sessionId;
37
+ this.clientId = options.clientId || 'client';
38
+ this.serverId = options.serverId || 'server';
39
+
40
+ // Encryption key (derived from shared secret)
41
+ this.encryptionKey = null;
42
+
43
+ // Sequence counters for replay protection
44
+ this.sendSequence = 0;
45
+ this.recvSequence = 0;
46
+ this.messageCount = 0;
47
+
48
+ // State
49
+ this.established = false;
50
+ this.createdAt = Date.now();
51
+ this.lastActivity = Date.now();
52
+ }
53
+
54
+ /**
55
+ * Check if session has exceeded its lifetime or message limit
56
+ */
57
+ isExpired() {
58
+ if (!this.established) return false;
59
+ const age = Date.now() - this.createdAt;
60
+ return age > ANNEX_CONFIG.sessionTimeout ||
61
+ this.messageCount >= ANNEX_CONFIG.maxMessagesPerKey;
62
+ }
63
+
64
+ /**
65
+ * Check if session is approaching expiry (80% of limits)
66
+ * Used to trigger proactive rekey before hard expiry
67
+ */
68
+ isNearingExpiry() {
69
+ if (!this.established) return false;
70
+ const age = Date.now() - this.createdAt;
71
+ return age > ANNEX_CONFIG.sessionTimeout * 0.8 ||
72
+ this.messageCount >= ANNEX_CONFIG.maxMessagesPerKey * 0.8;
73
+ }
74
+
75
+ /**
76
+ * Rekey the session with a new client public key.
77
+ * Preserves the connection but rotates the encryption key.
78
+ * Returns new ciphertext to send back to client.
79
+ */
80
+ async rekey(publicKeyHex) {
81
+ const oldKey = this.encryptionKey;
82
+
83
+ // Generate new shared secret from client's new public key
84
+ const publicKey = hexToBytes(publicKeyHex);
85
+ const { sharedSecret, cipherText } = ml_kem768.encapsulate(publicKey);
86
+
87
+ // Derive fresh AES-256 key
88
+ this.encryptionKey = this._deriveKey(sharedSecret);
89
+
90
+ // Zero old key and shared secret
91
+ if (oldKey?.fill) oldKey.fill(0);
92
+ if (sharedSecret?.fill) sharedSecret.fill(0);
93
+
94
+ // Reset counters but preserve session identity
95
+ this.sendSequence = 0;
96
+ this.recvSequence = 0;
97
+ this.messageCount = 0;
98
+ this.createdAt = Date.now();
99
+ this.lastActivity = Date.now();
100
+
101
+ console.log(`[ServerAnnex] Session ${this.sessionId} rekeyed successfully`);
102
+
103
+ return bytesToHex(cipherText);
104
+ }
105
+
106
+ /**
107
+ * Handle client's public key - encapsulate shared secret
108
+ * Returns ciphertext to send back to client
109
+ */
110
+ async handlePublicKey(publicKeyHex) {
111
+ const publicKey = hexToBytes(publicKeyHex);
112
+
113
+ // Server encapsulates - generates random shared secret + ciphertext
114
+ const { sharedSecret, cipherText } = ml_kem768.encapsulate(publicKey);
115
+
116
+ // Derive AES-256 key from shared secret
117
+ this.encryptionKey = this._deriveKey(sharedSecret);
118
+
119
+ // Zero shared secret immediately — only the derived key is needed from here
120
+ // sharedSecret is a Uint8Array from ml-kem768
121
+ if (sharedSecret?.fill) sharedSecret.fill(0);
122
+
123
+ this.established = true;
124
+
125
+ console.log(`[ServerAnnex] Session ${this.sessionId} established with ${this.clientId}`);
126
+
127
+ return bytesToHex(cipherText);
128
+ }
129
+
130
+ /**
131
+ * Derive AES-256 key from shared secret using SHA3-256
132
+ */
133
+ _deriveKey(sharedSecret) {
134
+ // Hash: sharedSecret + salt + sessionId + clientId + serverId
135
+ const keyMaterial = Buffer.concat([
136
+ Buffer.from(sharedSecret),
137
+ Buffer.from(ANNEX_CONFIG.keyDerivationSalt),
138
+ Buffer.from(this.sessionId),
139
+ Buffer.from(this.clientId), // Client is "local" from their perspective
140
+ Buffer.from(this.serverId), // Server is "remote" from their perspective
141
+ ]);
142
+
143
+ // Use SHA3-256 to derive 32-byte AES key
144
+ const key = Buffer.from(sha3_256(keyMaterial));
145
+
146
+ return key;
147
+ }
148
+
149
+ /**
150
+ * Encrypt data for transmission to client
151
+ * Returns { nonce, ciphertext, authTag, sequence } as hex strings
152
+ */
153
+ encrypt(plaintext) {
154
+ if (!this.established || !this.encryptionKey) {
155
+ throw new Error('Session not established');
156
+ }
157
+
158
+ const nonce = randomBytes(ANNEX_CONFIG.nonceSize);
159
+
160
+ // Encode plaintext
161
+ const data = typeof plaintext === 'string'
162
+ ? Buffer.from(plaintext)
163
+ : Buffer.from(JSON.stringify(plaintext));
164
+
165
+ // AAD for GCM authentication
166
+ const aadString = `${this.sessionId}:${this.sendSequence}`;
167
+
168
+ // Create cipher with AAD
169
+ const cipher = createCipheriv('aes-256-gcm', this.encryptionKey, nonce);
170
+ cipher.setAAD(Buffer.from(aadString));
171
+
172
+ // Encrypt
173
+ const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
174
+ const authTag = cipher.getAuthTag();
175
+
176
+ const sequence = this.sendSequence;
177
+ this.sendSequence++;
178
+ this.messageCount++;
179
+ this.lastActivity = Date.now();
180
+
181
+ return {
182
+ nonce: bytesToHex(nonce),
183
+ ciphertext: bytesToHex(ciphertext),
184
+ authTag: bytesToHex(authTag),
185
+ sequence,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Decrypt received data from client
191
+ * Returns plaintext string (JSON)
192
+ */
193
+ decrypt(encryptedData) {
194
+ if (!this.established || !this.encryptionKey) {
195
+ throw new Error('Session not established');
196
+ }
197
+
198
+ const { nonce, ciphertext, authTag, sequence } = encryptedData;
199
+
200
+ // Replay protection — reject any sequence not strictly greater than last received
201
+ if (typeof sequence !== 'number' || sequence <= this.recvSequence) {
202
+ throw new Error(`Replay detected: sequence ${sequence} <= ${this.recvSequence}`);
203
+ }
204
+
205
+ const nonceBuffer = Buffer.from(hexToBytes(nonce));
206
+ const ciphertextBuffer = Buffer.from(hexToBytes(ciphertext));
207
+ const authTagBuffer = Buffer.from(hexToBytes(authTag));
208
+
209
+ // Create decipher with AAD
210
+ const decipher = createDecipheriv('aes-256-gcm', this.encryptionKey, nonceBuffer);
211
+ decipher.setAAD(Buffer.from(`${this.sessionId}:${sequence}`));
212
+ decipher.setAuthTag(authTagBuffer);
213
+
214
+ // Decrypt
215
+ const plaintext = Buffer.concat([decipher.update(ciphertextBuffer), decipher.final()]);
216
+
217
+ this.recvSequence = sequence;
218
+ this.messageCount++;
219
+ this.lastActivity = Date.now();
220
+
221
+ return plaintext.toString('utf8');
222
+ }
223
+
224
+ /**
225
+ * Destroy session and zero keys
226
+ */
227
+ destroy() {
228
+ if (this.encryptionKey) {
229
+ this.encryptionKey.fill(0);
230
+ this.encryptionKey = null;
231
+ }
232
+ this.established = false;
233
+ console.log(`[ServerAnnex] Session ${this.sessionId} destroyed`);
234
+ }
235
+ }
236
+
237
+ // Handshake message types (must match client)
238
+ export const ANNEX_HANDSHAKE_TYPE = {
239
+ PUBLIC_KEY: 'annex:public_key',
240
+ ENCAPSULATED: 'annex:encapsulated',
241
+ ENCRYPTED: 'annex:encrypted',
242
+ ERROR: 'annex:error',
243
+ REKEY: 'annex:rekey', // Server -> Client: session needs rekeying
244
+ REKEY_ACK: 'annex:rekey_ack', // Client -> Server: new public key for rekey
245
+ };
246
+
247
+ export default ServerAnnexSession;
@@ -0,0 +1,343 @@
1
+ /**
2
+ * DARSHAN Content Streaming API
3
+ *
4
+ * "Content stays on the altar. Pilgrims come to see."
5
+ *
6
+ * Exposes the DARSHAN protocol as HTTP endpoints for:
7
+ * - Content registration and listing
8
+ * - Stream requests and chunk delivery
9
+ * - View attestation and proof-of-viewing
10
+ * - Bandwidth control and quality selection
11
+ *
12
+ * DARSHAN is the streaming backbone for yakapp-studio content sharing.
13
+ * Creators keep sovereignty — no copies leave unless explicitly permitted.
14
+ *
15
+ * @module server/darshan-api
16
+ * @license MIT
17
+ * @copyright 2026 YAKMESH™ Contributors
18
+ */
19
+
20
+ import { Router } from 'express';
21
+ import { createLogger } from '../utils/logger.js';
22
+
23
+ const log = createLogger('server:darshan');
24
+
25
+ /**
26
+ * Create the DARSHAN API router.
27
+ *
28
+ * @param {Object} params
29
+ * @param {Object} params.darshanGateway - DarshanGateway instance (content host)
30
+ * @param {Object} params.gossip - GossipProtocol instance
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 /darshan
35
+ */
36
+ export function createDarshanAPI({
37
+ darshanGateway,
38
+ gossip,
39
+ identity,
40
+ writeLimiter,
41
+ requirePeerAuth,
42
+ }) {
43
+ const router = Router();
44
+
45
+ // ═══════════════════════════════════════════════════════════════════════════
46
+ // STATUS
47
+ // ═══════════════════════════════════════════════════════════════════════════
48
+
49
+ router.get('/status', (req, res) => {
50
+ res.json({
51
+ darshan: 'operational',
52
+ stats: darshanGateway.stats,
53
+ contentCount: darshanGateway.contents.size,
54
+ activeStreams: darshanGateway.streams.size,
55
+ });
56
+ });
57
+
58
+ // ═══════════════════════════════════════════════════════════════════════════
59
+ // CONTENT REGISTRY
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+
62
+ /**
63
+ * GET /darshan/content — List available content (public metadata only)
64
+ */
65
+ router.get('/content', (req, res) => {
66
+ const type = req.query.type || null;
67
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
68
+
69
+ let contents = [];
70
+ for (const content of darshanGateway.contents.values()) {
71
+ // Check exclusions
72
+ if (darshanGateway.exclusions.has(content.contentId)) continue;
73
+
74
+ if (type && content.contentType !== type) continue;
75
+
76
+ contents.push(content.getPublicMetadata());
77
+ }
78
+
79
+ contents = contents.slice(0, limit);
80
+
81
+ res.json({
82
+ contents,
83
+ total: contents.length,
84
+ nodeId: identity.identity.nodeId,
85
+ });
86
+ });
87
+
88
+ /**
89
+ * GET /darshan/content/:contentId — Get content metadata
90
+ */
91
+ router.get('/content/:contentId', (req, res) => {
92
+ const { contentId } = req.params;
93
+ const content = darshanGateway.contents.get(contentId);
94
+
95
+ if (!content || darshanGateway.exclusions.has(contentId)) {
96
+ return res.status(404).json({ error: 'Content not found' });
97
+ }
98
+
99
+ res.json(content.getPublicMetadata());
100
+ });
101
+
102
+ /**
103
+ * POST /darshan/content — Register content for sharing
104
+ */
105
+ router.post('/content', writeLimiter, requirePeerAuth, (req, res) => {
106
+ const { path, name, description, contentType, mimeType, permissions, accessList } = req.body;
107
+
108
+ if (!path) {
109
+ return res.status(400).json({ error: 'path required' });
110
+ }
111
+
112
+ // Path traversal defense — reject .., absolute paths, drive letters, and null bytes.
113
+ // DARSHAN content paths must be relative within the host's content root.
114
+ // IMPORTANT: URL-decode BEFORE checking, to catch %2e%2e (%2e = .) bypass.
115
+ let normalizedPath;
116
+ try {
117
+ normalizedPath = decodeURIComponent(String(path)).replace(/\\/g, '/');
118
+ } catch {
119
+ return res.status(400).json({ error: 'Invalid path: malformed encoding' });
120
+ }
121
+ if (normalizedPath.includes('\0') || // Null byte injection
122
+ normalizedPath.includes('..') || // Traversal
123
+ normalizedPath.startsWith('/') || // Absolute path
124
+ /^[a-zA-Z]:/.test(normalizedPath)) { // Windows drive letter
125
+ return res.status(400).json({ error: 'Invalid path: traversal or absolute paths not allowed' });
126
+ }
127
+
128
+ darshanGateway.registerContent({
129
+ path,
130
+ name,
131
+ description,
132
+ contentType,
133
+ mimeType,
134
+ permissions,
135
+ accessList,
136
+ }).then(result => {
137
+ if (result.success !== false) {
138
+ // Announce to mesh
139
+ gossip.spreadRumor('darshan:content:available', {
140
+ contentId: result.contentId || result.content?.contentId,
141
+ hostNodeId: identity.identity.nodeId,
142
+ metadata: result.content?.getPublicMetadata?.() || result,
143
+ });
144
+ }
145
+ res.json(result);
146
+ }).catch(error => {
147
+ res.status(500).json({ error: error.message });
148
+ });
149
+ });
150
+
151
+ /**
152
+ * DELETE /darshan/content/:contentId — Unregister content
153
+ */
154
+ router.delete('/content/:contentId', writeLimiter, requirePeerAuth, (req, res) => {
155
+ const { contentId } = req.params;
156
+
157
+ const content = darshanGateway.contents.get(contentId);
158
+ if (!content) {
159
+ return res.status(404).json({ error: 'Content not found' });
160
+ }
161
+
162
+ darshanGateway.contents.delete(contentId);
163
+ if (content.path) {
164
+ darshanGateway.contentByPath.delete(content.path);
165
+ }
166
+
167
+ // Announce removal to mesh
168
+ gossip.spreadRumor('darshan:content:removed', {
169
+ contentId,
170
+ hostNodeId: identity.identity.nodeId,
171
+ });
172
+
173
+ res.json({ success: true });
174
+ });
175
+
176
+ // ═══════════════════════════════════════════════════════════════════════════
177
+ // STREAMING
178
+ // ═══════════════════════════════════════════════════════════════════════════
179
+
180
+ /**
181
+ * POST /darshan/stream — Request a stream
182
+ */
183
+ router.post('/stream', writeLimiter, requirePeerAuth, (req, res) => {
184
+ const { contentId, quality, viewerNodeId } = req.body;
185
+
186
+ if (!contentId) {
187
+ return res.status(400).json({ error: 'contentId required' });
188
+ }
189
+
190
+ const content = darshanGateway.contents.get(contentId);
191
+ if (!content || darshanGateway.exclusions.has(contentId)) {
192
+ return res.status(404).json({ error: 'Content not found' });
193
+ }
194
+
195
+ // Check access if GUMBA-controlled
196
+ if (content.accessList) {
197
+ // Access verification would go through GUMBA — simplified for now
198
+ const viewer = viewerNodeId || req.authenticatedPeer;
199
+ if (!viewer) {
200
+ return res.status(403).json({ error: 'GUMBA access proof required' });
201
+ }
202
+ }
203
+
204
+ try {
205
+ const stream = darshanGateway.createStream?.(contentId, {
206
+ quality,
207
+ viewerNodeId: viewerNodeId || req.authenticatedPeer || 'anonymous',
208
+ });
209
+
210
+ if (stream) {
211
+ res.json({
212
+ success: true,
213
+ streamId: stream.streamId || stream.id,
214
+ contentId,
215
+ totalChunks: Math.ceil(content.size / content.chunkSize || 65536),
216
+ chunkHashes: content.chunkHashes,
217
+ });
218
+ } else {
219
+ // Fallback: return content info for direct streaming
220
+ content.viewCount++;
221
+ darshanGateway.stats.streamsCreated++;
222
+ res.json({
223
+ success: true,
224
+ contentId,
225
+ size: content.size,
226
+ mimeType: content.mimeType,
227
+ hash: content.hash,
228
+ });
229
+ }
230
+ } catch (error) {
231
+ res.status(500).json({ error: error.message });
232
+ }
233
+ });
234
+
235
+ /**
236
+ * GET /darshan/stream/:streamId/chunk/:index — Get a chunk
237
+ */
238
+ router.get('/stream/:streamId/chunk/:index', (req, res) => {
239
+ const { streamId, index } = req.params;
240
+ const chunkIndex = parseInt(index);
241
+
242
+ const stream = darshanGateway.streams.get(streamId);
243
+ if (!stream) {
244
+ return res.status(404).json({ error: 'Stream not found or expired' });
245
+ }
246
+
247
+ try {
248
+ const chunk = stream.getChunk?.(chunkIndex);
249
+ if (chunk) {
250
+ res.set('Content-Type', 'application/octet-stream');
251
+ res.set('X-Chunk-Index', chunkIndex.toString());
252
+ res.set('X-Chunk-Hash', chunk.hash || '');
253
+ res.send(chunk.data || chunk);
254
+ } else {
255
+ res.status(404).json({ error: 'Chunk not available' });
256
+ }
257
+ } catch (error) {
258
+ res.status(500).json({ error: error.message });
259
+ }
260
+ });
261
+
262
+ // ═══════════════════════════════════════════════════════════════════════════
263
+ // VIEW ATTESTATION
264
+ // ═══════════════════════════════════════════════════════════════════════════
265
+
266
+ /**
267
+ * POST /darshan/attest — Submit a view attestation (proof of viewing)
268
+ */
269
+ router.post('/attest', requirePeerAuth, (req, res) => {
270
+ const { contentId, viewerNodeId, streamId, duration, bytesConsumed } = req.body;
271
+
272
+ if (!contentId || !viewerNodeId) {
273
+ return res.status(400).json({ error: 'contentId and viewerNodeId required' });
274
+ }
275
+
276
+ try {
277
+ const attestation = darshanGateway.createAttestation?.({
278
+ contentId,
279
+ viewerNodeId,
280
+ streamId,
281
+ duration,
282
+ bytesConsumed,
283
+ });
284
+
285
+ if (attestation) {
286
+ res.json({
287
+ success: true,
288
+ attestationId: attestation.attestationId || attestation.id,
289
+ });
290
+ } else {
291
+ // Track basic stats even without full attestation module
292
+ const content = darshanGateway.contents.get(contentId);
293
+ if (content) {
294
+ content.viewCount++;
295
+ content.totalBytesServed += bytesConsumed || 0;
296
+ }
297
+ darshanGateway.stats.attestationsCreated++;
298
+ res.json({ success: true, tracked: true });
299
+ }
300
+ } catch (error) {
301
+ res.status(500).json({ error: error.message });
302
+ }
303
+ });
304
+
305
+ /**
306
+ * GET /darshan/attestations/:contentId — Get view attestations for content
307
+ */
308
+ router.get('/attestations/:contentId', (req, res) => {
309
+ const { contentId } = req.params;
310
+
311
+ const attestations = [];
312
+ for (const att of darshanGateway.attestations.values()) {
313
+ if (att.contentId === contentId) {
314
+ attestations.push(att.toJSON ? att.toJSON() : att);
315
+ }
316
+ }
317
+
318
+ res.json({ attestations, total: attestations.length });
319
+ });
320
+
321
+ log.info('✓ DARSHAN API initialized (content streaming + attestation)');
322
+
323
+ return router;
324
+ }
325
+
326
+ /**
327
+ * Wire DARSHAN gossip handlers into the mesh rumor stream.
328
+ */
329
+ export function wireDarshanGossip(mesh, darshanGateway) {
330
+ mesh.on('rumor', (topic, data, origin) => {
331
+ // Content availability announcements from other nodes
332
+ if (topic === 'darshan:content:available') {
333
+ darshanGateway.emit?.('remote:content:available', data);
334
+ }
335
+
336
+ // Content removal announcements
337
+ if (topic === 'darshan:content:removed') {
338
+ darshanGateway.emit?.('remote:content:removed', data);
339
+ }
340
+ });
341
+
342
+ log.info('✓ DARSHAN gossip handlers wired into mesh');
343
+ }