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