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,1718 @@
1
+ /**
2
+ * DARSHAN - Decentralized Archive Remote Streaming and Hosting Access Network
3
+ *
4
+ * A novel content streaming protocol where:
5
+ * - Content STAYS at the host node
6
+ * - Viewers SEE content, they don't COPY it
7
+ * - Bytes stream on-demand through E2E encrypted mesh tunnels
8
+ * - Host maintains complete sovereignty over their content
9
+ *
10
+ * ╔═══════════════════════════════════════════════════════════════════════════════╗
11
+ * ║ "Content stays on the altar. Pilgrims come to see." ║
12
+ * ║ ║
13
+ * ║ दर्शन (darshan) = the act of viewing, sacred sight ║
14
+ * ║ In Hindu tradition: the blessing received by beholding a deity ║
15
+ * ║ In Yakmesh: the privilege of viewing content from its source ║
16
+ * ╚═══════════════════════════════════════════════════════════════════════════════╝
17
+ *
18
+ * PARADIGM SHIFT:
19
+ * - Old: Creator → Upload → Central Server → Download × N (bandwidth waste)
20
+ * - New: Creator's Node → Mesh Tunnel → Viewer's Node (view, not copy)
21
+ *
22
+ * NOVEL PROPERTIES:
23
+ * 1. View ≠ Copy - Content decrypts DURING streaming, no local cache by default
24
+ * 2. Bandwidth Sovereignty - Host controls quality, priority, throttling
25
+ * 3. Proof of Viewing - Cryptographic attestation of view events
26
+ * 4. Lazy Byte Streaming - Only transfer what's being consumed
27
+ * 5. OS Integration - Virtual mounts via FUSE-like interfaces
28
+ *
29
+ * Part of the Himalayan Protocol Family:
30
+ * - ANNEX: E2E encrypted channels (used for content transport)
31
+ * - GUMBA: Access control (used for view permissions)
32
+ * - DARSHAN: Content streaming (this module)
33
+ *
34
+ * @module mesh/darshan
35
+ * @license MIT
36
+ * @copyright 2026 YAKMESH™ Contributors
37
+ */
38
+
39
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
40
+ import { sha3_256 as _nobleSha3 } from '@noble/hashes/sha3.js';
41
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
42
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
43
+ // ACCEL: Hardware-accelerated crypto
44
+ import { sha3_256, mlDsa65Sign, mlDsa65Verify } from '../utils/accel.js';
45
+ import { createLogger } from '../utils/logger.js';
46
+ import EventEmitter from 'events';
47
+
48
+ const log = createLogger('mesh:darshan');
49
+
50
+ // ═══════════════════════════════════════════════════════════════════════════════
51
+ // CONFIGURATION
52
+ // ═══════════════════════════════════════════════════════════════════════════════
53
+
54
+ export const DARSHAN_CONFIG = Object.freeze({
55
+ // Protocol
56
+ version: 1,
57
+ scheme: 'darshan',
58
+
59
+ // Streaming
60
+ defaultChunkSize: 64 * 1024, // 64KB chunks
61
+ maxChunkSize: 1024 * 1024, // 1MB max chunk
62
+ minChunkSize: 1024, // 1KB min chunk
63
+ prefetchChunks: 3, // Prefetch ahead
64
+ maxConcurrentStreams: 10, // Per host limit
65
+ streamTimeout: 30000, // 30s stream timeout
66
+
67
+ // Content
68
+ maxContentSize: 10 * 1024 * 1024 * 1024, // 10GB max
69
+ maxPathLength: 512,
70
+ maxMetadataSize: 64 * 1024, // 64KB metadata
71
+
72
+ // Viewing
73
+ viewSessionExpiry: 24 * 60 * 60 * 1000, // 24 hours
74
+ attestationInterval: 60000, // Attest every 60s during viewing
75
+
76
+ // Quality presets
77
+ qualityPresets: {
78
+ ORIGINAL: 'original',
79
+ HIGH: 'high', // 1080p or best below
80
+ MEDIUM: 'medium', // 720p
81
+ LOW: 'low', // 480p
82
+ THUMBNAIL: 'thumb', // Preview only
83
+ },
84
+
85
+ // Content types
86
+ contentTypes: {
87
+ VIDEO: 'video',
88
+ AUDIO: 'audio',
89
+ IMAGE: 'image',
90
+ DOCUMENT: 'document',
91
+ STREAM: 'stream', // Live stream
92
+ ANY: 'any',
93
+ },
94
+
95
+ // Permissions
96
+ permissions: {
97
+ VIEW: 'view', // Can view only
98
+ DOWNLOAD: 'download', // Can download (explicit opt-in)
99
+ SHARE: 'share', // Can share access tokens
100
+ CACHE: 'cache', // Can cache for offline (time-limited)
101
+ },
102
+
103
+ // Message types
104
+ messageTypes: {
105
+ // Discovery
106
+ CONTENT_LIST: 'darshan:list',
107
+ CONTENT_INFO: 'darshan:info',
108
+
109
+ // Streaming
110
+ STREAM_REQUEST: 'darshan:stream:request',
111
+ STREAM_RESPONSE: 'darshan:stream:response',
112
+ STREAM_CHUNK: 'darshan:stream:chunk',
113
+ STREAM_END: 'darshan:stream:end',
114
+ STREAM_ERROR: 'darshan:stream:error',
115
+
116
+ // Control
117
+ SEEK: 'darshan:seek',
118
+ PAUSE: 'darshan:pause',
119
+ RESUME: 'darshan:resume',
120
+ QUALITY_CHANGE: 'darshan:quality',
121
+
122
+ // Attestation
123
+ VIEW_START: 'darshan:view:start',
124
+ VIEW_HEARTBEAT: 'darshan:view:heartbeat',
125
+ VIEW_END: 'darshan:view:end',
126
+
127
+ // Mount
128
+ MOUNT_REQUEST: 'darshan:mount:request',
129
+ MOUNT_RESPONSE: 'darshan:mount:response',
130
+ UNMOUNT: 'darshan:unmount',
131
+ },
132
+ });
133
+
134
+ // ═══════════════════════════════════════════════════════════════════════════════
135
+ // DARSHAN CONTENT - Content metadata
136
+ // ═══════════════════════════════════════════════════════════════════════════════
137
+
138
+ /**
139
+ * DarshanContent - Metadata for streamable content
140
+ */
141
+ export class DarshanContent {
142
+ constructor(options = {}) {
143
+ // Identity
144
+ this.contentId = options.contentId || DarshanContent.generateId();
145
+ this.hostNodeId = options.hostNodeId;
146
+ this.path = options.path; // Local path on host (private)
147
+
148
+ // Metadata
149
+ this.name = options.name || '';
150
+ this.description = options.description || '';
151
+ this.contentType = options.contentType || DARSHAN_CONFIG.contentTypes.ANY;
152
+ this.mimeType = options.mimeType || 'application/octet-stream';
153
+ this.size = options.size || 0;
154
+ this.duration = options.duration || null; // For video/audio
155
+ this.dimensions = options.dimensions || null; // { width, height }
156
+
157
+ // Hash for integrity
158
+ this.hash = options.hash || null; // SHA3-256 of full content
159
+ this.chunkHashes = options.chunkHashes || []; // Per-chunk verification
160
+
161
+ // Access control
162
+ this.permissions = options.permissions || [DARSHAN_CONFIG.permissions.VIEW];
163
+ this.accessList = options.accessList || null; // GUMBA bundle ID or null for public
164
+
165
+ // Quality options (for video/audio)
166
+ this.availableQualities = options.availableQualities || [DARSHAN_CONFIG.qualityPresets.ORIGINAL];
167
+ this.defaultQuality = options.defaultQuality || DARSHAN_CONFIG.qualityPresets.ORIGINAL;
168
+
169
+ // Timestamps
170
+ this.createdAt = options.createdAt || Date.now();
171
+ this.updatedAt = options.updatedAt || Date.now();
172
+
173
+ // Stats
174
+ this.viewCount = options.viewCount || 0;
175
+ this.totalBytesServed = options.totalBytesServed || 0;
176
+ }
177
+
178
+ static generateId() {
179
+ return 'd-' + bytesToHex(randomBytes(16));
180
+ }
181
+
182
+ /**
183
+ * Get public metadata (safe to share)
184
+ */
185
+ getPublicMetadata() {
186
+ return {
187
+ contentId: this.contentId,
188
+ hostNodeId: this.hostNodeId,
189
+ name: this.name,
190
+ description: this.description,
191
+ contentType: this.contentType,
192
+ mimeType: this.mimeType,
193
+ size: this.size,
194
+ duration: this.duration,
195
+ dimensions: this.dimensions,
196
+ hash: this.hash,
197
+ permissions: this.permissions,
198
+ availableQualities: this.availableQualities,
199
+ defaultQuality: this.defaultQuality,
200
+ createdAt: this.createdAt,
201
+ viewCount: this.viewCount,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Validate content metadata
207
+ */
208
+ validate() {
209
+ const errors = [];
210
+
211
+ if (!this.hostNodeId) errors.push('hostNodeId required');
212
+ if (!this.path) errors.push('path required');
213
+ if (this.path && this.path.length > DARSHAN_CONFIG.maxPathLength) {
214
+ errors.push('path too long');
215
+ }
216
+ // Defense-in-depth: reject path traversal at validation layer too.
217
+ // Note: absolute paths are allowed here because the HOST node legitimately
218
+ // uses them to register content from its own filesystem. The API boundary
219
+ // (darshan-api.js) rejects absolute paths from external requests.
220
+ if (this.path) {
221
+ const normalized = String(this.path).replace(/\\/g, '/');
222
+ if (normalized.includes('..')) {
223
+ errors.push('path traversal not allowed');
224
+ }
225
+ }
226
+ if (this.size > DARSHAN_CONFIG.maxContentSize) {
227
+ errors.push('content exceeds max size');
228
+ }
229
+ if (!Object.values(DARSHAN_CONFIG.contentTypes).includes(this.contentType)) {
230
+ errors.push('invalid contentType');
231
+ }
232
+
233
+ return { valid: errors.length === 0, errors };
234
+ }
235
+
236
+ toJSON() {
237
+ return {
238
+ contentId: this.contentId,
239
+ hostNodeId: this.hostNodeId,
240
+ path: this.path,
241
+ name: this.name,
242
+ description: this.description,
243
+ contentType: this.contentType,
244
+ mimeType: this.mimeType,
245
+ size: this.size,
246
+ duration: this.duration,
247
+ dimensions: this.dimensions,
248
+ hash: this.hash,
249
+ chunkHashes: this.chunkHashes,
250
+ permissions: this.permissions,
251
+ accessList: this.accessList,
252
+ availableQualities: this.availableQualities,
253
+ defaultQuality: this.defaultQuality,
254
+ createdAt: this.createdAt,
255
+ updatedAt: this.updatedAt,
256
+ viewCount: this.viewCount,
257
+ totalBytesServed: this.totalBytesServed,
258
+ };
259
+ }
260
+
261
+ static fromJSON(json) {
262
+ return new DarshanContent(json);
263
+ }
264
+ }
265
+
266
+ // ═══════════════════════════════════════════════════════════════════════════════
267
+ // DARSHAN STREAM - Byte-range streaming
268
+ // ═══════════════════════════════════════════════════════════════════════════════
269
+
270
+ /**
271
+ * DarshanStream - Manages streaming of content bytes
272
+ *
273
+ * "Like water from a mountain spring -
274
+ * you drink what you need, the source remains."
275
+ */
276
+ export class DarshanStream extends EventEmitter {
277
+ constructor(options = {}) {
278
+ super();
279
+
280
+ this.streamId = options.streamId || DarshanStream.generateId();
281
+ this.contentId = options.contentId;
282
+ this.viewerId = options.viewerId;
283
+ this.hostId = options.hostId;
284
+
285
+ // Stream state
286
+ this.state = 'idle'; // idle, requesting, streaming, paused, ended, error
287
+ this.position = 0;
288
+ this.bytesReceived = 0;
289
+ this.bytesTotal = options.size || 0;
290
+
291
+ // Configuration
292
+ this.chunkSize = options.chunkSize || DARSHAN_CONFIG.defaultChunkSize;
293
+ this.quality = options.quality || DARSHAN_CONFIG.qualityPresets.ORIGINAL;
294
+ this.prefetch = options.prefetch !== false;
295
+
296
+ // Buffering
297
+ this.chunks = new Map(); // offset -> { data, verified }
298
+ this.pendingRequests = new Set();
299
+
300
+ // Callbacks
301
+ this.onChunk = options.onChunk || (() => {});
302
+ this.onProgress = options.onProgress || (() => {});
303
+ this.onError = options.onError || (() => {});
304
+ this.onEnd = options.onEnd || (() => {});
305
+
306
+ // Timers
307
+ this._timeout = null;
308
+
309
+ // Stats
310
+ this.startTime = null;
311
+ this.endTime = null;
312
+ }
313
+
314
+ static generateId() {
315
+ return 'stream-' + bytesToHex(randomBytes(8));
316
+ }
317
+
318
+ /**
319
+ * Calculate chunk boundaries for a byte range
320
+ */
321
+ getChunkRange(start, end) {
322
+ const chunkStart = Math.floor(start / this.chunkSize);
323
+ const chunkEnd = Math.floor(end / this.chunkSize);
324
+ return { chunkStart, chunkEnd };
325
+ }
326
+
327
+ /**
328
+ * Request a specific byte range
329
+ */
330
+ requestRange(start, end) {
331
+ const { chunkStart, chunkEnd } = this.getChunkRange(start, end);
332
+ const needed = [];
333
+
334
+ for (let i = chunkStart; i <= chunkEnd; i++) {
335
+ const offset = i * this.chunkSize;
336
+ if (!this.chunks.has(offset) && !this.pendingRequests.has(offset)) {
337
+ needed.push(offset);
338
+ this.pendingRequests.add(offset);
339
+ }
340
+ }
341
+
342
+ return needed;
343
+ }
344
+
345
+ /**
346
+ * Store a received chunk
347
+ */
348
+ receiveChunk(offset, data, hash = null) {
349
+ // Verify hash if provided
350
+ if (hash) {
351
+ const computed = bytesToHex(sha3_256(data));
352
+ if (computed !== hash) {
353
+ this.emit('error', { type: 'integrity', offset, expected: hash, got: computed });
354
+ return false;
355
+ }
356
+ }
357
+
358
+ this.chunks.set(offset, { data, verified: !!hash });
359
+ this.pendingRequests.delete(offset);
360
+ this.bytesReceived += data.length;
361
+
362
+ this.onProgress({
363
+ bytesReceived: this.bytesReceived,
364
+ bytesTotal: this.bytesTotal,
365
+ percent: (this.bytesReceived / this.bytesTotal) * 100,
366
+ });
367
+
368
+ this.emit('chunk', { offset, size: data.length });
369
+ return true;
370
+ }
371
+
372
+ /**
373
+ * Get bytes for a range (from buffer)
374
+ */
375
+ getBytes(start, length) {
376
+ const result = Buffer.alloc(length);
377
+ let filled = 0;
378
+
379
+ const { chunkStart, chunkEnd } = this.getChunkRange(start, start + length - 1);
380
+
381
+ for (let i = chunkStart; i <= chunkEnd; i++) {
382
+ const offset = i * this.chunkSize;
383
+ const chunk = this.chunks.get(offset);
384
+
385
+ if (!chunk) {
386
+ return null; // Gap in buffer
387
+ }
388
+
389
+ const chunkData = chunk.data;
390
+ const srcStart = Math.max(0, start - offset);
391
+ const srcEnd = Math.min(chunkData.length, start + length - offset);
392
+ const dstStart = offset + srcStart - start;
393
+
394
+ chunkData.copy(result, dstStart, srcStart, srcEnd);
395
+ filled += srcEnd - srcStart;
396
+ }
397
+
398
+ return filled === length ? result : null;
399
+ }
400
+
401
+ /**
402
+ * Seek to position
403
+ */
404
+ seek(position) {
405
+ if (position < 0 || position >= this.bytesTotal) {
406
+ return false;
407
+ }
408
+
409
+ this.position = position;
410
+ this.emit('seek', { position });
411
+ return true;
412
+ }
413
+
414
+ /**
415
+ * Pause streaming
416
+ */
417
+ pause() {
418
+ if (this.state === 'streaming') {
419
+ this.state = 'paused';
420
+ this.emit('pause');
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Resume streaming
426
+ */
427
+ resume() {
428
+ if (this.state === 'paused') {
429
+ this.state = 'streaming';
430
+ this.emit('resume');
431
+ }
432
+ }
433
+
434
+ /**
435
+ * End the stream
436
+ */
437
+ end() {
438
+ this.state = 'ended';
439
+ this.endTime = Date.now();
440
+ this._clearTimeout();
441
+ this.onEnd();
442
+ this.emit('end', {
443
+ duration: this.endTime - this.startTime,
444
+ bytesReceived: this.bytesReceived,
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Clean up resources
450
+ */
451
+ destroy() {
452
+ this._clearTimeout();
453
+ this.chunks.clear();
454
+ this.pendingRequests.clear();
455
+ this.removeAllListeners();
456
+ }
457
+
458
+ _clearTimeout() {
459
+ if (this._timeout) {
460
+ clearTimeout(this._timeout);
461
+ this._timeout = null;
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Get stream stats
467
+ */
468
+ getStats() {
469
+ const now = Date.now();
470
+ const duration = (this.endTime || now) - (this.startTime || now);
471
+
472
+ return {
473
+ streamId: this.streamId,
474
+ state: this.state,
475
+ position: this.position,
476
+ bytesReceived: this.bytesReceived,
477
+ bytesTotal: this.bytesTotal,
478
+ bufferedChunks: this.chunks.size,
479
+ pendingRequests: this.pendingRequests.size,
480
+ duration,
481
+ throughput: duration > 0 ? this.bytesReceived / (duration / 1000) : 0,
482
+ };
483
+ }
484
+
485
+ toJSON() {
486
+ return this.getStats();
487
+ }
488
+ }
489
+
490
+ // ═══════════════════════════════════════════════════════════════════════════════
491
+ // DARSHAN ATTESTATION - Proof of viewing
492
+ // ═══════════════════════════════════════════════════════════════════════════════
493
+
494
+ /**
495
+ * DarshanAttestation - Cryptographic proof of content viewing
496
+ *
497
+ * "The sacred witness records all who came to see."
498
+ */
499
+ export class DarshanAttestation {
500
+ constructor(options = {}) {
501
+ this.attestationId = options.attestationId || DarshanAttestation.generateId();
502
+ this.contentId = options.contentId;
503
+ this.viewerId = options.viewerId;
504
+ this.hostId = options.hostId;
505
+
506
+ // View session
507
+ this.sessionId = options.sessionId;
508
+ this.startedAt = options.startedAt || Date.now();
509
+ this.endedAt = options.endedAt || null;
510
+ this.duration = options.duration || 0;
511
+
512
+ // Progress
513
+ this.bytesViewed = options.bytesViewed || 0;
514
+ this.percentViewed = options.percentViewed || 0;
515
+ this.seekCount = options.seekCount || 0;
516
+
517
+ // Quality info
518
+ this.quality = options.quality || DARSHAN_CONFIG.qualityPresets.ORIGINAL;
519
+
520
+ // Signatures
521
+ this.viewerSignature = options.viewerSignature || null;
522
+ this.hostSignature = options.hostSignature || null;
523
+ }
524
+
525
+ static generateId() {
526
+ return 'attest-' + bytesToHex(randomBytes(8));
527
+ }
528
+
529
+ /**
530
+ * Get the attestation payload for signing
531
+ */
532
+ getSignablePayload() {
533
+ return JSON.stringify({
534
+ attestationId: this.attestationId,
535
+ contentId: this.contentId,
536
+ viewerId: this.viewerId,
537
+ hostId: this.hostId,
538
+ sessionId: this.sessionId,
539
+ startedAt: this.startedAt,
540
+ endedAt: this.endedAt,
541
+ bytesViewed: this.bytesViewed,
542
+ percentViewed: this.percentViewed,
543
+ quality: this.quality,
544
+ });
545
+ }
546
+
547
+ /**
548
+ * Sign as viewer
549
+ */
550
+ signAsViewer(secretKey) {
551
+ const payload = utf8ToBytes(this.getSignablePayload());
552
+ const signature = mlDsa65Sign(payload, secretKey);
553
+ this.viewerSignature = bytesToHex(signature);
554
+ return this;
555
+ }
556
+
557
+ /**
558
+ * Sign as host (counter-signature)
559
+ */
560
+ signAsHost(secretKey) {
561
+ // Include viewer signature in host signing
562
+ const payload = utf8ToBytes(this.getSignablePayload() + (this.viewerSignature || ''));
563
+ const signature = mlDsa65Sign(payload, secretKey);
564
+ this.hostSignature = bytesToHex(signature);
565
+ return this;
566
+ }
567
+
568
+ /**
569
+ * Verify viewer signature
570
+ */
571
+ verifyViewer(publicKey) {
572
+ if (!this.viewerSignature) return false;
573
+
574
+ try {
575
+ const payload = utf8ToBytes(this.getSignablePayload());
576
+ const signatureBytes = hexToBytes(this.viewerSignature);
577
+ const publicKeyBytes = typeof publicKey === 'string' ? hexToBytes(publicKey) : publicKey;
578
+ return mlDsa65Verify(signatureBytes, payload, publicKeyBytes);
579
+ } catch (err) {
580
+ return false;
581
+ }
582
+ }
583
+
584
+ /**
585
+ * Verify host signature
586
+ */
587
+ verifyHost(publicKey) {
588
+ if (!this.hostSignature) return false;
589
+
590
+ try {
591
+ const payload = utf8ToBytes(this.getSignablePayload() + (this.viewerSignature || ''));
592
+ const signatureBytes = hexToBytes(this.hostSignature);
593
+ const publicKeyBytes = typeof publicKey === 'string' ? hexToBytes(publicKey) : publicKey;
594
+ return mlDsa65Verify(signatureBytes, payload, publicKeyBytes);
595
+ } catch (err) {
596
+ return false;
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Check if attestation is complete (both signatures)
602
+ */
603
+ isComplete() {
604
+ return !!(this.viewerSignature && this.hostSignature && this.endedAt);
605
+ }
606
+
607
+ /**
608
+ * Update during viewing session
609
+ */
610
+ update(stats) {
611
+ this.bytesViewed = stats.bytesViewed || this.bytesViewed;
612
+ this.percentViewed = stats.percentViewed || this.percentViewed;
613
+ this.seekCount = stats.seekCount || this.seekCount;
614
+ this.duration = Date.now() - this.startedAt;
615
+ }
616
+
617
+ /**
618
+ * Finalize the attestation
619
+ */
620
+ finalize() {
621
+ this.endedAt = Date.now();
622
+ this.duration = this.endedAt - this.startedAt;
623
+ }
624
+
625
+ toJSON() {
626
+ return {
627
+ attestationId: this.attestationId,
628
+ contentId: this.contentId,
629
+ viewerId: this.viewerId,
630
+ hostId: this.hostId,
631
+ sessionId: this.sessionId,
632
+ startedAt: this.startedAt,
633
+ endedAt: this.endedAt,
634
+ duration: this.duration,
635
+ bytesViewed: this.bytesViewed,
636
+ percentViewed: this.percentViewed,
637
+ seekCount: this.seekCount,
638
+ quality: this.quality,
639
+ viewerSignature: this.viewerSignature,
640
+ hostSignature: this.hostSignature,
641
+ };
642
+ }
643
+
644
+ static fromJSON(json) {
645
+ return new DarshanAttestation(json);
646
+ }
647
+ }
648
+
649
+ // ═══════════════════════════════════════════════════════════════════════════════
650
+ // DARSHAN REQUEST - Stream request message
651
+ // ═══════════════════════════════════════════════════════════════════════════════
652
+
653
+ /**
654
+ * DarshanRequest - Request to stream content
655
+ */
656
+ export class DarshanRequest {
657
+ constructor(options = {}) {
658
+ this.requestId = options.requestId || DarshanRequest.generateId();
659
+ this.type = options.type || DARSHAN_CONFIG.messageTypes.STREAM_REQUEST;
660
+ this.contentId = options.contentId;
661
+ this.viewerId = options.viewerId;
662
+ this.timestamp = options.timestamp || Date.now();
663
+
664
+ // Request parameters
665
+ this.startByte = options.startByte || 0;
666
+ this.endByte = options.endByte || null; // null = to end
667
+ this.quality = options.quality || DARSHAN_CONFIG.qualityPresets.ORIGINAL;
668
+ this.chunkSize = options.chunkSize || DARSHAN_CONFIG.defaultChunkSize;
669
+
670
+ // Access proof (GUMBA)
671
+ this.accessProof = options.accessProof || null;
672
+ }
673
+
674
+ static generateId() {
675
+ return 'req-' + bytesToHex(randomBytes(8));
676
+ }
677
+
678
+ /**
679
+ * Create a stream request
680
+ */
681
+ static streamRequest(options) {
682
+ return new DarshanRequest({
683
+ type: DARSHAN_CONFIG.messageTypes.STREAM_REQUEST,
684
+ ...options,
685
+ });
686
+ }
687
+
688
+ /**
689
+ * Create a seek request
690
+ */
691
+ static seekRequest(options) {
692
+ return new DarshanRequest({
693
+ type: DARSHAN_CONFIG.messageTypes.SEEK,
694
+ ...options,
695
+ });
696
+ }
697
+
698
+ validate() {
699
+ const errors = [];
700
+ if (!this.contentId) errors.push('contentId required');
701
+ if (!this.viewerId) errors.push('viewerId required');
702
+ if (this.startByte < 0) errors.push('startByte must be >= 0');
703
+ if (this.endByte !== null && this.endByte < this.startByte) {
704
+ errors.push('endByte must be >= startByte');
705
+ }
706
+ return { valid: errors.length === 0, errors };
707
+ }
708
+
709
+ toJSON() {
710
+ return {
711
+ requestId: this.requestId,
712
+ type: this.type,
713
+ contentId: this.contentId,
714
+ viewerId: this.viewerId,
715
+ timestamp: this.timestamp,
716
+ startByte: this.startByte,
717
+ endByte: this.endByte,
718
+ quality: this.quality,
719
+ chunkSize: this.chunkSize,
720
+ accessProof: this.accessProof,
721
+ };
722
+ }
723
+
724
+ static fromJSON(json) {
725
+ return new DarshanRequest(json);
726
+ }
727
+ }
728
+
729
+ // ═══════════════════════════════════════════════════════════════════════════════
730
+ // DARSHAN CHUNK - Streamed content chunk
731
+ // ═══════════════════════════════════════════════════════════════════════════════
732
+
733
+ /**
734
+ * DarshanChunk - A chunk of streamed content
735
+ */
736
+ export class DarshanChunk {
737
+ constructor(options = {}) {
738
+ this.streamId = options.streamId;
739
+ this.contentId = options.contentId;
740
+ this.offset = options.offset || 0;
741
+ this.data = options.data || null; // Buffer
742
+ this.hash = options.hash || null; // SHA3-256 of data
743
+ this.timestamp = options.timestamp || Date.now();
744
+
745
+ // Position info
746
+ this.isFirst = options.isFirst || false;
747
+ this.isLast = options.isLast || false;
748
+ this.totalSize = options.totalSize || 0;
749
+ }
750
+
751
+ /**
752
+ * Create from buffer with auto-hash
753
+ */
754
+ static fromBuffer(buffer, options = {}) {
755
+ const hash = bytesToHex(sha3_256(buffer));
756
+ return new DarshanChunk({
757
+ ...options,
758
+ data: buffer,
759
+ hash,
760
+ });
761
+ }
762
+
763
+ /**
764
+ * Verify chunk integrity
765
+ */
766
+ verify() {
767
+ if (!this.data || !this.hash) return false;
768
+ const computed = bytesToHex(sha3_256(this.data));
769
+ return computed === this.hash;
770
+ }
771
+
772
+ toJSON() {
773
+ return {
774
+ streamId: this.streamId,
775
+ contentId: this.contentId,
776
+ offset: this.offset,
777
+ data: this.data ? this.data.toString('base64') : null,
778
+ hash: this.hash,
779
+ timestamp: this.timestamp,
780
+ isFirst: this.isFirst,
781
+ isLast: this.isLast,
782
+ totalSize: this.totalSize,
783
+ };
784
+ }
785
+
786
+ static fromJSON(json) {
787
+ return new DarshanChunk({
788
+ ...json,
789
+ data: json.data ? Buffer.from(json.data, 'base64') : null,
790
+ });
791
+ }
792
+ }
793
+
794
+ // ═══════════════════════════════════════════════════════════════════════════════
795
+ // DARSHAN GATEWAY - Host-side content server
796
+ // ═══════════════════════════════════════════════════════════════════════════════
797
+
798
+ /**
799
+ * DarshanGateway - Serves content to viewers
800
+ *
801
+ * "The temple guardian who opens doors to the worthy."
802
+ *
803
+ * The gateway:
804
+ * - Registers local content for sharing
805
+ * - Validates access proofs (GUMBA integration)
806
+ * - Streams bytes on demand
807
+ * - Tracks view attestations
808
+ * - Controls bandwidth and quality
809
+ */
810
+ export class DarshanGateway extends EventEmitter {
811
+ constructor(identity, options = {}) {
812
+ super();
813
+
814
+ this.identity = identity;
815
+ this.nodeId = identity.identity?.nodeId || identity.nodeId;
816
+ this.options = options;
817
+
818
+ // Content registry
819
+ this.contents = new Map(); // contentId -> DarshanContent
820
+ this.contentByPath = new Map(); // path -> contentId
821
+
822
+ // Active streams
823
+ this.streams = new Map(); // streamId -> stream info
824
+ this.streamsByViewer = new Map(); // viewerId -> Set<streamId>
825
+
826
+ // Attestations
827
+ this.attestations = new Map(); // attestationId -> DarshanAttestation
828
+ this.attestationsBySession = new Map(); // sessionId -> attestationId
829
+
830
+ // Content exclusions (moderation)
831
+ // Excluded content exists but is hidden from listContent()
832
+ this.exclusions = new Map(); // contentId -> { excludedBy, reason, timestamp }
833
+
834
+ // Access control (GUMBA integration)
835
+ this.accessController = options.accessController || null;
836
+
837
+ // Bandwidth control
838
+ this.maxBandwidth = options.maxBandwidth || Infinity;
839
+ this.currentBandwidth = 0;
840
+ this.bandwidthHistory = [];
841
+
842
+ // File reader (injected for testability)
843
+ this.fileReader = options.fileReader || null;
844
+
845
+ // Stats
846
+ this.stats = {
847
+ contentRegistered: 0,
848
+ streamsCreated: 0,
849
+ bytesServed: 0,
850
+ attestationsCreated: 0,
851
+ };
852
+ }
853
+
854
+ /**
855
+ * Register content for sharing
856
+ */
857
+ async registerContent(options) {
858
+ const content = new DarshanContent({
859
+ hostNodeId: this.nodeId,
860
+ ...options,
861
+ });
862
+
863
+ const validation = content.validate();
864
+ if (!validation.valid) {
865
+ return { success: false, errors: validation.errors };
866
+ }
867
+
868
+ // Compute content hash if file reader available
869
+ if (this.fileReader && !content.hash) {
870
+ try {
871
+ const data = await this.fileReader.read(content.path);
872
+ content.hash = bytesToHex(sha3_256(data));
873
+ content.size = data.length;
874
+
875
+ // Compute chunk hashes
876
+ const chunks = Math.ceil(data.length / DARSHAN_CONFIG.defaultChunkSize);
877
+ content.chunkHashes = [];
878
+ for (let i = 0; i < chunks; i++) {
879
+ const start = i * DARSHAN_CONFIG.defaultChunkSize;
880
+ const end = Math.min(start + DARSHAN_CONFIG.defaultChunkSize, data.length);
881
+ const chunkData = data.slice(start, end);
882
+ content.chunkHashes.push(bytesToHex(sha3_256(chunkData)));
883
+ }
884
+ } catch (err) {
885
+ return { success: false, errors: ['Failed to read content: ' + err.message] };
886
+ }
887
+ }
888
+
889
+ this.contents.set(content.contentId, content);
890
+ this.contentByPath.set(content.path, content.contentId);
891
+ this.stats.contentRegistered++;
892
+
893
+ this.emit('content:registered', content);
894
+ log.info('Content registered', { contentId: content.contentId, name: content.name });
895
+
896
+ return { success: true, content };
897
+ }
898
+
899
+ /**
900
+ * Unregister content
901
+ */
902
+ unregisterContent(contentId) {
903
+ const content = this.contents.get(contentId);
904
+ if (!content) return false;
905
+
906
+ this.contentByPath.delete(content.path);
907
+ this.contents.delete(contentId);
908
+
909
+ this.emit('content:unregistered', content);
910
+ return true;
911
+ }
912
+
913
+ /**
914
+ * Get content by ID
915
+ */
916
+ getContent(contentId) {
917
+ return this.contents.get(contentId) || null;
918
+ }
919
+
920
+ /**
921
+ * List all registered content (respects exclusions)
922
+ * @param {Object} options - List options
923
+ * @param {boolean} options.includeExcluded - Include excluded content (for admins)
924
+ */
925
+ listContent(options = {}) {
926
+ const { includeExcluded = false } = options;
927
+ return Array.from(this.contents.values())
928
+ .filter(c => includeExcluded || !this.exclusions.has(c.contentId))
929
+ .map(c => ({
930
+ ...c.getPublicMetadata(),
931
+ excluded: this.exclusions.has(c.contentId),
932
+ }));
933
+ }
934
+
935
+ /**
936
+ * Exclude content from public listing (moderation)
937
+ * Content still exists and can be streamed directly if contentId is known.
938
+ * This is the "polite" moderation model: hide, don't delete.
939
+ *
940
+ * @param {string} contentId - Content to exclude
941
+ * @param {Object} options - Exclusion options
942
+ * @param {string} options.excludedBy - ID of admin/mod who excluded
943
+ * @param {string} options.reason - Reason for exclusion
944
+ */
945
+ excludeContent(contentId, options = {}) {
946
+ if (!this.contents.has(contentId)) {
947
+ return { success: false, error: 'CONTENT_NOT_FOUND' };
948
+ }
949
+
950
+ const exclusion = {
951
+ contentId,
952
+ excludedBy: options.excludedBy || 'system',
953
+ reason: options.reason || 'unspecified',
954
+ timestamp: Date.now(),
955
+ };
956
+
957
+ this.exclusions.set(contentId, exclusion);
958
+ this.emit('content:excluded', exclusion);
959
+ log.info('Content excluded', { contentId, reason: exclusion.reason });
960
+
961
+ return { success: true, exclusion };
962
+ }
963
+
964
+ /**
965
+ * Remove exclusion (reinstate content in listings)
966
+ */
967
+ reinstateContent(contentId) {
968
+ if (!this.exclusions.has(contentId)) {
969
+ return { success: false, error: 'NOT_EXCLUDED' };
970
+ }
971
+
972
+ this.exclusions.delete(contentId);
973
+ this.emit('content:reinstated', { contentId });
974
+ return { success: true };
975
+ }
976
+
977
+ /**
978
+ * Check if content is excluded
979
+ */
980
+ isExcluded(contentId) {
981
+ return this.exclusions.has(contentId);
982
+ }
983
+
984
+ /**
985
+ * Get exclusion info
986
+ */
987
+ getExclusion(contentId) {
988
+ return this.exclusions.get(contentId) || null;
989
+ }
990
+
991
+ /**
992
+ * List all exclusions
993
+ */
994
+ listExclusions() {
995
+ return Array.from(this.exclusions.values());
996
+ }
997
+
998
+ /**
999
+ * Handle stream request
1000
+ */
1001
+ async handleStreamRequest(request, sendChunk) {
1002
+ const content = this.contents.get(request.contentId);
1003
+ if (!content) {
1004
+ return { success: false, error: 'CONTENT_NOT_FOUND' };
1005
+ }
1006
+
1007
+ // Check access if access controller configured
1008
+ if (this.accessController && content.accessList) {
1009
+ const accessResult = await this.accessController.verifyAccess(
1010
+ request.accessProof,
1011
+ () => {} // Public key lookup
1012
+ );
1013
+ if (!accessResult.granted) {
1014
+ this.emit('access:denied', { request, reason: accessResult.reason });
1015
+ return { success: false, error: 'ACCESS_DENIED', reason: accessResult.reason };
1016
+ }
1017
+ }
1018
+
1019
+ // Check concurrent stream limit
1020
+ const viewerStreams = this.streamsByViewer.get(request.viewerId) || new Set();
1021
+ if (viewerStreams.size >= DARSHAN_CONFIG.maxConcurrentStreams) {
1022
+ return { success: false, error: 'TOO_MANY_STREAMS' };
1023
+ }
1024
+
1025
+ // Create stream
1026
+ const streamId = DarshanStream.generateId();
1027
+ const stream = {
1028
+ streamId,
1029
+ contentId: request.contentId,
1030
+ viewerId: request.viewerId,
1031
+ startByte: request.startByte,
1032
+ endByte: request.endByte || content.size - 1,
1033
+ currentByte: request.startByte,
1034
+ quality: request.quality,
1035
+ chunkSize: Math.min(request.chunkSize, DARSHAN_CONFIG.maxChunkSize),
1036
+ createdAt: Date.now(),
1037
+ bytesServed: 0,
1038
+ state: 'streaming',
1039
+ };
1040
+
1041
+ this.streams.set(streamId, stream);
1042
+ viewerStreams.add(streamId);
1043
+ this.streamsByViewer.set(request.viewerId, viewerStreams);
1044
+ this.stats.streamsCreated++;
1045
+
1046
+ // Create attestation
1047
+ const attestation = new DarshanAttestation({
1048
+ contentId: content.contentId,
1049
+ viewerId: request.viewerId,
1050
+ hostId: this.nodeId,
1051
+ sessionId: streamId,
1052
+ });
1053
+ this.attestations.set(attestation.attestationId, attestation);
1054
+ this.attestationsBySession.set(streamId, attestation.attestationId);
1055
+ this.stats.attestationsCreated++;
1056
+
1057
+ this.emit('stream:started', { streamId, viewerId: request.viewerId, content });
1058
+
1059
+ // Start streaming (async generator pattern)
1060
+ this._streamContent(stream, content, sendChunk).catch(err => {
1061
+ log.error('Stream error', { streamId, error: err.message });
1062
+ this.emit('stream:error', { streamId, error: err });
1063
+ });
1064
+
1065
+ return { success: true, streamId, attestationId: attestation.attestationId };
1066
+ }
1067
+
1068
+ /**
1069
+ * Stream content chunks
1070
+ */
1071
+ async _streamContent(stream, content, sendChunk) {
1072
+ if (!this.fileReader) {
1073
+ throw new Error('No file reader configured');
1074
+ }
1075
+
1076
+ const data = await this.fileReader.read(content.path);
1077
+
1078
+ while (stream.currentByte <= stream.endByte && stream.state === 'streaming') {
1079
+ const chunkStart = stream.currentByte;
1080
+ const chunkEnd = Math.min(chunkStart + stream.chunkSize - 1, stream.endByte);
1081
+ const chunkData = data.slice(chunkStart, chunkEnd + 1);
1082
+
1083
+ const chunk = DarshanChunk.fromBuffer(Buffer.from(chunkData), {
1084
+ streamId: stream.streamId,
1085
+ contentId: stream.contentId,
1086
+ offset: chunkStart,
1087
+ isFirst: chunkStart === stream.startByte,
1088
+ isLast: chunkEnd >= stream.endByte,
1089
+ totalSize: content.size,
1090
+ });
1091
+
1092
+ await sendChunk(chunk);
1093
+
1094
+ stream.currentByte = chunkEnd + 1;
1095
+ stream.bytesServed += chunkData.length;
1096
+ this.stats.bytesServed += chunkData.length;
1097
+ content.totalBytesServed += chunkData.length;
1098
+
1099
+ this.emit('chunk:sent', { streamId: stream.streamId, offset: chunkStart, size: chunkData.length });
1100
+ }
1101
+
1102
+ // Update attestation
1103
+ const attestationId = this.attestationsBySession.get(stream.streamId);
1104
+ if (attestationId) {
1105
+ const attestation = this.attestations.get(attestationId);
1106
+ if (attestation) {
1107
+ attestation.update({
1108
+ bytesViewed: stream.bytesServed,
1109
+ percentViewed: (stream.bytesServed / content.size) * 100,
1110
+ });
1111
+ attestation.finalize();
1112
+ attestation.signAsHost(this.identity.identity.secretKey);
1113
+ }
1114
+ }
1115
+
1116
+ stream.state = 'ended';
1117
+ content.viewCount++;
1118
+
1119
+ this.emit('stream:ended', { streamId: stream.streamId, bytesServed: stream.bytesServed });
1120
+
1121
+ // Cleanup
1122
+ const viewerStreams = this.streamsByViewer.get(stream.viewerId);
1123
+ if (viewerStreams) {
1124
+ viewerStreams.delete(stream.streamId);
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Pause a stream
1130
+ */
1131
+ pauseStream(streamId) {
1132
+ const stream = this.streams.get(streamId);
1133
+ if (stream && stream.state === 'streaming') {
1134
+ stream.state = 'paused';
1135
+ this.emit('stream:paused', { streamId });
1136
+ return true;
1137
+ }
1138
+ return false;
1139
+ }
1140
+
1141
+ /**
1142
+ * Resume a stream
1143
+ */
1144
+ resumeStream(streamId) {
1145
+ const stream = this.streams.get(streamId);
1146
+ if (stream && stream.state === 'paused') {
1147
+ stream.state = 'streaming';
1148
+ this.emit('stream:resumed', { streamId });
1149
+ return true;
1150
+ }
1151
+ return false;
1152
+ }
1153
+
1154
+ /**
1155
+ * End a stream
1156
+ */
1157
+ endStream(streamId) {
1158
+ const stream = this.streams.get(streamId);
1159
+ if (stream) {
1160
+ stream.state = 'ended';
1161
+ this.emit('stream:ended', { streamId, bytesServed: stream.bytesServed });
1162
+ return true;
1163
+ }
1164
+ return false;
1165
+ }
1166
+
1167
+ /**
1168
+ * Get attestation
1169
+ */
1170
+ getAttestation(attestationId) {
1171
+ return this.attestations.get(attestationId) || null;
1172
+ }
1173
+
1174
+ /**
1175
+ * Get gateway stats
1176
+ */
1177
+ getStats() {
1178
+ return {
1179
+ ...this.stats,
1180
+ activeStreams: this.streams.size,
1181
+ registeredContent: this.contents.size,
1182
+ excludedContent: this.exclusions.size,
1183
+ activeViewers: this.streamsByViewer.size,
1184
+ };
1185
+ }
1186
+ }
1187
+
1188
+ // ═══════════════════════════════════════════════════════════════════════════════
1189
+ // DARSHAN VIEWER - Client-side content viewer
1190
+ // ═══════════════════════════════════════════════════════════════════════════════
1191
+
1192
+ /**
1193
+ * DarshanViewer - Views content from remote hosts
1194
+ *
1195
+ * "The pilgrim who receives the blessing of sight."
1196
+ */
1197
+ export class DarshanViewer extends EventEmitter {
1198
+ constructor(identity, options = {}) {
1199
+ super();
1200
+
1201
+ this.identity = identity;
1202
+ this.nodeId = identity.identity?.nodeId || identity.nodeId;
1203
+ this.options = options;
1204
+
1205
+ // Active streams
1206
+ this.streams = new Map(); // streamId -> DarshanStream
1207
+ this.streamsByContent = new Map(); // contentId -> streamId
1208
+
1209
+ // Content cache (metadata only, not data by default)
1210
+ this.knownContent = new Map(); // contentId -> DarshanContent metadata
1211
+
1212
+ // Attestations we've received
1213
+ this.attestations = new Map(); // attestationId -> DarshanAttestation
1214
+
1215
+ // Access proofs provider (GUMBA integration)
1216
+ this.proofProvider = options.proofProvider || null;
1217
+
1218
+ // Transport layer (send messages to hosts)
1219
+ this.sendMessage = options.sendMessage || (() => {});
1220
+
1221
+ // Stats
1222
+ this.stats = {
1223
+ streamsOpened: 0,
1224
+ bytesReceived: 0,
1225
+ contentViewed: 0,
1226
+ };
1227
+ }
1228
+
1229
+ /**
1230
+ * Request content info from a host
1231
+ */
1232
+ async requestContentInfo(hostId, contentId) {
1233
+ return new Promise((resolve, reject) => {
1234
+ const requestId = 'info-' + bytesToHex(randomBytes(4));
1235
+
1236
+ const timeout = setTimeout(() => {
1237
+ reject(new Error('Content info request timeout'));
1238
+ }, DARSHAN_CONFIG.streamTimeout);
1239
+
1240
+ const handler = (response) => {
1241
+ if (response.requestId === requestId) {
1242
+ clearTimeout(timeout);
1243
+ this.removeListener('content:info:response', handler);
1244
+
1245
+ if (response.error) {
1246
+ reject(new Error(response.error));
1247
+ } else {
1248
+ const content = DarshanContent.fromJSON(response.content);
1249
+ this.knownContent.set(content.contentId, content);
1250
+ resolve(content);
1251
+ }
1252
+ }
1253
+ };
1254
+
1255
+ this.on('content:info:response', handler);
1256
+
1257
+ this.sendMessage(hostId, {
1258
+ type: DARSHAN_CONFIG.messageTypes.CONTENT_INFO,
1259
+ requestId,
1260
+ contentId,
1261
+ viewerId: this.nodeId,
1262
+ });
1263
+ });
1264
+ }
1265
+
1266
+ /**
1267
+ * Start streaming content
1268
+ */
1269
+ async startStream(hostId, contentId, options = {}) {
1270
+ // Get content info if not cached
1271
+ let content = this.knownContent.get(contentId);
1272
+ if (!content && options.contentInfo) {
1273
+ content = DarshanContent.fromJSON(options.contentInfo);
1274
+ this.knownContent.set(contentId, content);
1275
+ }
1276
+
1277
+ // Get access proof if needed
1278
+ let accessProof = null;
1279
+ if (this.proofProvider && content?.accessList) {
1280
+ accessProof = await this.proofProvider.getProof(content.accessList, this.nodeId);
1281
+ }
1282
+
1283
+ // Create stream
1284
+ const stream = new DarshanStream({
1285
+ contentId,
1286
+ viewerId: this.nodeId,
1287
+ hostId,
1288
+ size: content?.size || 0,
1289
+ chunkSize: options.chunkSize || DARSHAN_CONFIG.defaultChunkSize,
1290
+ quality: options.quality || DARSHAN_CONFIG.qualityPresets.ORIGINAL,
1291
+ onChunk: options.onChunk,
1292
+ onProgress: options.onProgress,
1293
+ onError: options.onError,
1294
+ onEnd: options.onEnd,
1295
+ });
1296
+
1297
+ stream.state = 'requesting';
1298
+ stream.startTime = Date.now();
1299
+
1300
+ this.streams.set(stream.streamId, stream);
1301
+ this.streamsByContent.set(contentId, stream.streamId);
1302
+ this.stats.streamsOpened++;
1303
+
1304
+ // Send stream request
1305
+ const request = DarshanRequest.streamRequest({
1306
+ contentId,
1307
+ viewerId: this.nodeId,
1308
+ startByte: options.startByte || 0,
1309
+ endByte: options.endByte || null,
1310
+ quality: stream.quality,
1311
+ chunkSize: stream.chunkSize,
1312
+ accessProof,
1313
+ });
1314
+
1315
+ this.sendMessage(hostId, {
1316
+ type: DARSHAN_CONFIG.messageTypes.STREAM_REQUEST,
1317
+ request: request.toJSON(),
1318
+ streamId: stream.streamId,
1319
+ });
1320
+
1321
+ this.emit('stream:requested', { streamId: stream.streamId, contentId, hostId });
1322
+
1323
+ return stream;
1324
+ }
1325
+
1326
+ /**
1327
+ * Handle stream response from host
1328
+ */
1329
+ handleStreamResponse(response) {
1330
+ const stream = this.streams.get(response.streamId);
1331
+ if (!stream) {
1332
+ log.warn('Stream response for unknown stream', { streamId: response.streamId });
1333
+ return;
1334
+ }
1335
+
1336
+ if (response.success) {
1337
+ stream.state = 'streaming';
1338
+ stream.attestationId = response.attestationId;
1339
+ this.emit('stream:started', { streamId: stream.streamId });
1340
+ } else {
1341
+ stream.state = 'error';
1342
+ stream.onError(response.error);
1343
+ this.emit('stream:error', { streamId: stream.streamId, error: response.error });
1344
+ }
1345
+ }
1346
+
1347
+ /**
1348
+ * Handle incoming chunk
1349
+ */
1350
+ handleChunk(chunkData) {
1351
+ const chunk = DarshanChunk.fromJSON(chunkData);
1352
+ const stream = this.streams.get(chunk.streamId);
1353
+
1354
+ if (!stream) {
1355
+ log.warn('Chunk for unknown stream', { streamId: chunk.streamId });
1356
+ return;
1357
+ }
1358
+
1359
+ // Verify chunk integrity
1360
+ if (!chunk.verify()) {
1361
+ stream.onError({ type: 'integrity', offset: chunk.offset });
1362
+ this.emit('chunk:invalid', { streamId: chunk.streamId, offset: chunk.offset });
1363
+ return;
1364
+ }
1365
+
1366
+ // Store chunk
1367
+ stream.receiveChunk(chunk.offset, chunk.data, chunk.hash);
1368
+ stream.onChunk(chunk);
1369
+
1370
+ this.stats.bytesReceived += chunk.data.length;
1371
+
1372
+ // Check if stream complete
1373
+ if (chunk.isLast) {
1374
+ stream.state = 'ended';
1375
+ stream.endTime = Date.now();
1376
+ stream.onEnd();
1377
+ this.stats.contentViewed++;
1378
+ this.emit('stream:complete', { streamId: chunk.streamId });
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * Seek within a stream
1384
+ */
1385
+ seek(streamId, position) {
1386
+ const stream = this.streams.get(streamId);
1387
+ if (!stream) return false;
1388
+
1389
+ stream.seek(position);
1390
+
1391
+ this.sendMessage(stream.hostId, {
1392
+ type: DARSHAN_CONFIG.messageTypes.SEEK,
1393
+ streamId,
1394
+ position,
1395
+ viewerId: this.nodeId,
1396
+ });
1397
+
1398
+ return true;
1399
+ }
1400
+
1401
+ /**
1402
+ * Pause a stream
1403
+ */
1404
+ pause(streamId) {
1405
+ const stream = this.streams.get(streamId);
1406
+ if (!stream) return false;
1407
+
1408
+ stream.pause();
1409
+
1410
+ this.sendMessage(stream.hostId, {
1411
+ type: DARSHAN_CONFIG.messageTypes.PAUSE,
1412
+ streamId,
1413
+ viewerId: this.nodeId,
1414
+ });
1415
+
1416
+ return true;
1417
+ }
1418
+
1419
+ /**
1420
+ * Resume a stream
1421
+ */
1422
+ resume(streamId) {
1423
+ const stream = this.streams.get(streamId);
1424
+ if (!stream) return false;
1425
+
1426
+ stream.resume();
1427
+
1428
+ this.sendMessage(stream.hostId, {
1429
+ type: DARSHAN_CONFIG.messageTypes.RESUME,
1430
+ streamId,
1431
+ viewerId: this.nodeId,
1432
+ });
1433
+
1434
+ return true;
1435
+ }
1436
+
1437
+ /**
1438
+ * End a stream
1439
+ */
1440
+ endStream(streamId) {
1441
+ const stream = this.streams.get(streamId);
1442
+ if (!stream) return false;
1443
+
1444
+ stream.end();
1445
+ this.streams.delete(streamId);
1446
+ this.streamsByContent.delete(stream.contentId);
1447
+
1448
+ this.sendMessage(stream.hostId, {
1449
+ type: DARSHAN_CONFIG.messageTypes.STREAM_END,
1450
+ streamId,
1451
+ viewerId: this.nodeId,
1452
+ });
1453
+
1454
+ return true;
1455
+ }
1456
+
1457
+ /**
1458
+ * Sign attestation as viewer
1459
+ */
1460
+ signAttestation(attestation) {
1461
+ attestation.signAsViewer(this.identity.identity.secretKey);
1462
+ this.attestations.set(attestation.attestationId, attestation);
1463
+ return attestation;
1464
+ }
1465
+
1466
+ /**
1467
+ * Get stream for content
1468
+ */
1469
+ getStreamForContent(contentId) {
1470
+ const streamId = this.streamsByContent.get(contentId);
1471
+ return streamId ? this.streams.get(streamId) : null;
1472
+ }
1473
+
1474
+ /**
1475
+ * Get viewer stats
1476
+ */
1477
+ getStats() {
1478
+ return {
1479
+ ...this.stats,
1480
+ activeStreams: this.streams.size,
1481
+ knownContent: this.knownContent.size,
1482
+ };
1483
+ }
1484
+ }
1485
+
1486
+ // ═══════════════════════════════════════════════════════════════════════════════
1487
+ // DARSHAN MOUNT - Virtual filesystem mount
1488
+ // ═══════════════════════════════════════════════════════════════════════════════
1489
+
1490
+ /**
1491
+ * DarshanMount - Virtual mount point for remote content
1492
+ *
1493
+ * Maps remote content to local virtual paths.
1494
+ * Actual FUSE integration would be platform-specific.
1495
+ * This provides the abstraction layer.
1496
+ *
1497
+ * "A window into the temple, not a door."
1498
+ */
1499
+ export class DarshanMount extends EventEmitter {
1500
+ constructor(options = {}) {
1501
+ super();
1502
+
1503
+ this.mountPoint = options.mountPoint || '/yak';
1504
+ this.viewer = options.viewer; // DarshanViewer instance
1505
+
1506
+ // Virtual directory structure
1507
+ this.virtualFs = new Map(); // path -> { type, contentId, hostId, content }
1508
+
1509
+ // Active file handles
1510
+ this.handles = new Map(); // handleId -> { path, stream, position }
1511
+ this.nextHandleId = 1;
1512
+
1513
+ // Mount state
1514
+ this.mounted = false;
1515
+
1516
+ // Cache policy
1517
+ this.cachePolicy = options.cachePolicy || 'none'; // none, session, persistent
1518
+ }
1519
+
1520
+ /**
1521
+ * Mount the virtual filesystem
1522
+ */
1523
+ async mount() {
1524
+ if (this.mounted) {
1525
+ throw new Error('Already mounted');
1526
+ }
1527
+
1528
+ this.mounted = true;
1529
+ this.emit('mount', { mountPoint: this.mountPoint });
1530
+ log.info('DARSHAN mounted', { mountPoint: this.mountPoint });
1531
+
1532
+ return true;
1533
+ }
1534
+
1535
+ /**
1536
+ * Unmount the virtual filesystem
1537
+ */
1538
+ async unmount() {
1539
+ if (!this.mounted) return false;
1540
+
1541
+ // Close all open handles
1542
+ for (const [handleId, handle] of this.handles) {
1543
+ if (handle.stream) {
1544
+ this.viewer.endStream(handle.stream.streamId);
1545
+ }
1546
+ }
1547
+ this.handles.clear();
1548
+
1549
+ this.mounted = false;
1550
+ this.emit('unmount', { mountPoint: this.mountPoint });
1551
+
1552
+ return true;
1553
+ }
1554
+
1555
+ /**
1556
+ * Add content to virtual filesystem
1557
+ */
1558
+ addContent(hostId, content, virtualPath = null) {
1559
+ const path = virtualPath || `${this.mountPoint}/${hostId}/${content.name}`;
1560
+
1561
+ this.virtualFs.set(path, {
1562
+ type: 'file',
1563
+ contentId: content.contentId,
1564
+ hostId,
1565
+ content,
1566
+ size: content.size,
1567
+ mtime: content.updatedAt,
1568
+ });
1569
+
1570
+ // Ensure parent directories exist
1571
+ const parts = path.split('/').filter(Boolean);
1572
+ let currentPath = '';
1573
+ for (let i = 0; i < parts.length - 1; i++) {
1574
+ currentPath += '/' + parts[i];
1575
+ if (!this.virtualFs.has(currentPath)) {
1576
+ this.virtualFs.set(currentPath, { type: 'directory', children: new Set() });
1577
+ }
1578
+ const dir = this.virtualFs.get(currentPath);
1579
+ if (dir.type === 'directory') {
1580
+ dir.children.add(parts[i + 1]);
1581
+ }
1582
+ }
1583
+
1584
+ this.emit('content:added', { path, contentId: content.contentId });
1585
+
1586
+ return path;
1587
+ }
1588
+
1589
+ /**
1590
+ * Remove content from virtual filesystem
1591
+ */
1592
+ removeContent(path) {
1593
+ const entry = this.virtualFs.get(path);
1594
+ if (!entry) return false;
1595
+
1596
+ this.virtualFs.delete(path);
1597
+ this.emit('content:removed', { path });
1598
+
1599
+ return true;
1600
+ }
1601
+
1602
+ /**
1603
+ * List directory contents
1604
+ */
1605
+ readdir(path) {
1606
+ const entry = this.virtualFs.get(path);
1607
+ if (!entry || entry.type !== 'directory') {
1608
+ return null;
1609
+ }
1610
+ return Array.from(entry.children);
1611
+ }
1612
+
1613
+ /**
1614
+ * Get file/directory stats
1615
+ */
1616
+ stat(path) {
1617
+ const entry = this.virtualFs.get(path);
1618
+ if (!entry) return null;
1619
+
1620
+ return {
1621
+ type: entry.type,
1622
+ size: entry.size || 0,
1623
+ mtime: entry.mtime || Date.now(),
1624
+ contentId: entry.contentId,
1625
+ };
1626
+ }
1627
+
1628
+ /**
1629
+ * Open a file for reading
1630
+ */
1631
+ async open(path) {
1632
+ const entry = this.virtualFs.get(path);
1633
+ if (!entry || entry.type !== 'file') {
1634
+ throw new Error('File not found: ' + path);
1635
+ }
1636
+
1637
+ // Start stream
1638
+ const stream = await this.viewer.startStream(
1639
+ entry.hostId,
1640
+ entry.contentId,
1641
+ { contentInfo: entry.content }
1642
+ );
1643
+
1644
+ const handleId = this.nextHandleId++;
1645
+ this.handles.set(handleId, {
1646
+ path,
1647
+ stream,
1648
+ position: 0,
1649
+ });
1650
+
1651
+ this.emit('file:opened', { handleId, path });
1652
+
1653
+ return handleId;
1654
+ }
1655
+
1656
+ /**
1657
+ * Read from an open file
1658
+ */
1659
+ async read(handleId, length) {
1660
+ const handle = this.handles.get(handleId);
1661
+ if (!handle) {
1662
+ throw new Error('Invalid handle: ' + handleId);
1663
+ }
1664
+
1665
+ // Request specific range
1666
+ handle.stream.requestRange(handle.position, handle.position + length - 1);
1667
+
1668
+ // Wait for data (simplified - real impl would use proper async)
1669
+ const data = handle.stream.getBytes(handle.position, length);
1670
+
1671
+ if (data) {
1672
+ handle.position += data.length;
1673
+ return data;
1674
+ }
1675
+
1676
+ return null; // Data not yet available
1677
+ }
1678
+
1679
+ /**
1680
+ * Seek within an open file
1681
+ */
1682
+ seek(handleId, position) {
1683
+ const handle = this.handles.get(handleId);
1684
+ if (!handle) return false;
1685
+
1686
+ handle.position = position;
1687
+ handle.stream.seek(position);
1688
+
1689
+ return true;
1690
+ }
1691
+
1692
+ /**
1693
+ * Close an open file
1694
+ */
1695
+ close(handleId) {
1696
+ const handle = this.handles.get(handleId);
1697
+ if (!handle) return false;
1698
+
1699
+ this.viewer.endStream(handle.stream.streamId);
1700
+ this.handles.delete(handleId);
1701
+
1702
+ this.emit('file:closed', { handleId, path: handle.path });
1703
+
1704
+ return true;
1705
+ }
1706
+
1707
+ /**
1708
+ * Get mount stats
1709
+ */
1710
+ getStats() {
1711
+ return {
1712
+ mountPoint: this.mountPoint,
1713
+ mounted: this.mounted,
1714
+ virtualFiles: this.virtualFs.size,
1715
+ openHandles: this.handles.size,
1716
+ };
1717
+ }
1718
+ }