yakmesh 2.8.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +637 -0
- package/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +334 -74
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +28 -13
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side ANNEX - Post-Quantum Encrypted Sessions
|
|
3
|
+
*
|
|
4
|
+
* Handles ANNEX key exchange and message encryption/decryption for
|
|
5
|
+
* browser clients connecting via WebSocket.
|
|
6
|
+
*
|
|
7
|
+
* Protocol (server perspective):
|
|
8
|
+
* 1. Receive client's ML-KEM-768 public key
|
|
9
|
+
* 2. Encapsulate shared secret, send ciphertext to client
|
|
10
|
+
* 3. Both derive AES-256-GCM key from shared secret
|
|
11
|
+
* 4. All subsequent messages encrypted/decrypted via session
|
|
12
|
+
*
|
|
13
|
+
* @module server/crypto/annex
|
|
14
|
+
* @license MIT
|
|
15
|
+
* @copyright 2026 YAKMESH Contributors
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
19
|
+
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
20
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
21
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
22
|
+
|
|
23
|
+
// Configuration (must match client)
|
|
24
|
+
const ANNEX_CONFIG = {
|
|
25
|
+
nonceSize: 12,
|
|
26
|
+
keyDerivationSalt: 'YAKMESH-ANNEX-VANI-2026',
|
|
27
|
+
sessionTimeout: 3600000, // 1 hour session lifetime
|
|
28
|
+
maxMessagesPerKey: 50000, // Force re-handshake after N messages
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ServerAnnexSession - Server-side encrypted session with a browser client
|
|
33
|
+
*/
|
|
34
|
+
export class ServerAnnexSession {
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.sessionId = options.sessionId;
|
|
37
|
+
this.clientId = options.clientId || 'client';
|
|
38
|
+
this.serverId = options.serverId || 'server';
|
|
39
|
+
|
|
40
|
+
// Encryption key (derived from shared secret)
|
|
41
|
+
this.encryptionKey = null;
|
|
42
|
+
|
|
43
|
+
// Sequence counters for replay protection
|
|
44
|
+
this.sendSequence = 0;
|
|
45
|
+
this.recvSequence = 0;
|
|
46
|
+
this.messageCount = 0;
|
|
47
|
+
|
|
48
|
+
// State
|
|
49
|
+
this.established = false;
|
|
50
|
+
this.createdAt = Date.now();
|
|
51
|
+
this.lastActivity = Date.now();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if session has exceeded its lifetime or message limit
|
|
56
|
+
*/
|
|
57
|
+
isExpired() {
|
|
58
|
+
if (!this.established) return false;
|
|
59
|
+
const age = Date.now() - this.createdAt;
|
|
60
|
+
return age > ANNEX_CONFIG.sessionTimeout ||
|
|
61
|
+
this.messageCount >= ANNEX_CONFIG.maxMessagesPerKey;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if session is approaching expiry (80% of limits)
|
|
66
|
+
* Used to trigger proactive rekey before hard expiry
|
|
67
|
+
*/
|
|
68
|
+
isNearingExpiry() {
|
|
69
|
+
if (!this.established) return false;
|
|
70
|
+
const age = Date.now() - this.createdAt;
|
|
71
|
+
return age > ANNEX_CONFIG.sessionTimeout * 0.8 ||
|
|
72
|
+
this.messageCount >= ANNEX_CONFIG.maxMessagesPerKey * 0.8;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Rekey the session with a new client public key.
|
|
77
|
+
* Preserves the connection but rotates the encryption key.
|
|
78
|
+
* Returns new ciphertext to send back to client.
|
|
79
|
+
*/
|
|
80
|
+
async rekey(publicKeyHex) {
|
|
81
|
+
const oldKey = this.encryptionKey;
|
|
82
|
+
|
|
83
|
+
// Generate new shared secret from client's new public key
|
|
84
|
+
const publicKey = hexToBytes(publicKeyHex);
|
|
85
|
+
const { sharedSecret, cipherText } = ml_kem768.encapsulate(publicKey);
|
|
86
|
+
|
|
87
|
+
// Derive fresh AES-256 key
|
|
88
|
+
this.encryptionKey = this._deriveKey(sharedSecret);
|
|
89
|
+
|
|
90
|
+
// Zero old key and shared secret
|
|
91
|
+
if (oldKey?.fill) oldKey.fill(0);
|
|
92
|
+
if (sharedSecret?.fill) sharedSecret.fill(0);
|
|
93
|
+
|
|
94
|
+
// Reset counters but preserve session identity
|
|
95
|
+
this.sendSequence = 0;
|
|
96
|
+
this.recvSequence = 0;
|
|
97
|
+
this.messageCount = 0;
|
|
98
|
+
this.createdAt = Date.now();
|
|
99
|
+
this.lastActivity = Date.now();
|
|
100
|
+
|
|
101
|
+
console.log(`[ServerAnnex] Session ${this.sessionId} rekeyed successfully`);
|
|
102
|
+
|
|
103
|
+
return bytesToHex(cipherText);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle client's public key - encapsulate shared secret
|
|
108
|
+
* Returns ciphertext to send back to client
|
|
109
|
+
*/
|
|
110
|
+
async handlePublicKey(publicKeyHex) {
|
|
111
|
+
const publicKey = hexToBytes(publicKeyHex);
|
|
112
|
+
|
|
113
|
+
// Server encapsulates - generates random shared secret + ciphertext
|
|
114
|
+
const { sharedSecret, cipherText } = ml_kem768.encapsulate(publicKey);
|
|
115
|
+
|
|
116
|
+
// Derive AES-256 key from shared secret
|
|
117
|
+
this.encryptionKey = this._deriveKey(sharedSecret);
|
|
118
|
+
|
|
119
|
+
// Zero shared secret immediately — only the derived key is needed from here
|
|
120
|
+
// sharedSecret is a Uint8Array from ml-kem768
|
|
121
|
+
if (sharedSecret?.fill) sharedSecret.fill(0);
|
|
122
|
+
|
|
123
|
+
this.established = true;
|
|
124
|
+
|
|
125
|
+
console.log(`[ServerAnnex] Session ${this.sessionId} established with ${this.clientId}`);
|
|
126
|
+
|
|
127
|
+
return bytesToHex(cipherText);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Derive AES-256 key from shared secret using SHA3-256
|
|
132
|
+
*/
|
|
133
|
+
_deriveKey(sharedSecret) {
|
|
134
|
+
// Hash: sharedSecret + salt + sessionId + clientId + serverId
|
|
135
|
+
const keyMaterial = Buffer.concat([
|
|
136
|
+
Buffer.from(sharedSecret),
|
|
137
|
+
Buffer.from(ANNEX_CONFIG.keyDerivationSalt),
|
|
138
|
+
Buffer.from(this.sessionId),
|
|
139
|
+
Buffer.from(this.clientId), // Client is "local" from their perspective
|
|
140
|
+
Buffer.from(this.serverId), // Server is "remote" from their perspective
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
// Use SHA3-256 to derive 32-byte AES key
|
|
144
|
+
const key = Buffer.from(sha3_256(keyMaterial));
|
|
145
|
+
|
|
146
|
+
return key;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Encrypt data for transmission to client
|
|
151
|
+
* Returns { nonce, ciphertext, authTag, sequence } as hex strings
|
|
152
|
+
*/
|
|
153
|
+
encrypt(plaintext) {
|
|
154
|
+
if (!this.established || !this.encryptionKey) {
|
|
155
|
+
throw new Error('Session not established');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nonce = randomBytes(ANNEX_CONFIG.nonceSize);
|
|
159
|
+
|
|
160
|
+
// Encode plaintext
|
|
161
|
+
const data = typeof plaintext === 'string'
|
|
162
|
+
? Buffer.from(plaintext)
|
|
163
|
+
: Buffer.from(JSON.stringify(plaintext));
|
|
164
|
+
|
|
165
|
+
// AAD for GCM authentication
|
|
166
|
+
const aadString = `${this.sessionId}:${this.sendSequence}`;
|
|
167
|
+
|
|
168
|
+
// Create cipher with AAD
|
|
169
|
+
const cipher = createCipheriv('aes-256-gcm', this.encryptionKey, nonce);
|
|
170
|
+
cipher.setAAD(Buffer.from(aadString));
|
|
171
|
+
|
|
172
|
+
// Encrypt
|
|
173
|
+
const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
174
|
+
const authTag = cipher.getAuthTag();
|
|
175
|
+
|
|
176
|
+
const sequence = this.sendSequence;
|
|
177
|
+
this.sendSequence++;
|
|
178
|
+
this.messageCount++;
|
|
179
|
+
this.lastActivity = Date.now();
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
nonce: bytesToHex(nonce),
|
|
183
|
+
ciphertext: bytesToHex(ciphertext),
|
|
184
|
+
authTag: bytesToHex(authTag),
|
|
185
|
+
sequence,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Decrypt received data from client
|
|
191
|
+
* Returns plaintext string (JSON)
|
|
192
|
+
*/
|
|
193
|
+
decrypt(encryptedData) {
|
|
194
|
+
if (!this.established || !this.encryptionKey) {
|
|
195
|
+
throw new Error('Session not established');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { nonce, ciphertext, authTag, sequence } = encryptedData;
|
|
199
|
+
|
|
200
|
+
// Replay protection — reject any sequence not strictly greater than last received
|
|
201
|
+
if (typeof sequence !== 'number' || sequence <= this.recvSequence) {
|
|
202
|
+
throw new Error(`Replay detected: sequence ${sequence} <= ${this.recvSequence}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const nonceBuffer = Buffer.from(hexToBytes(nonce));
|
|
206
|
+
const ciphertextBuffer = Buffer.from(hexToBytes(ciphertext));
|
|
207
|
+
const authTagBuffer = Buffer.from(hexToBytes(authTag));
|
|
208
|
+
|
|
209
|
+
// Create decipher with AAD
|
|
210
|
+
const decipher = createDecipheriv('aes-256-gcm', this.encryptionKey, nonceBuffer);
|
|
211
|
+
decipher.setAAD(Buffer.from(`${this.sessionId}:${sequence}`));
|
|
212
|
+
decipher.setAuthTag(authTagBuffer);
|
|
213
|
+
|
|
214
|
+
// Decrypt
|
|
215
|
+
const plaintext = Buffer.concat([decipher.update(ciphertextBuffer), decipher.final()]);
|
|
216
|
+
|
|
217
|
+
this.recvSequence = sequence;
|
|
218
|
+
this.messageCount++;
|
|
219
|
+
this.lastActivity = Date.now();
|
|
220
|
+
|
|
221
|
+
return plaintext.toString('utf8');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Destroy session and zero keys
|
|
226
|
+
*/
|
|
227
|
+
destroy() {
|
|
228
|
+
if (this.encryptionKey) {
|
|
229
|
+
this.encryptionKey.fill(0);
|
|
230
|
+
this.encryptionKey = null;
|
|
231
|
+
}
|
|
232
|
+
this.established = false;
|
|
233
|
+
console.log(`[ServerAnnex] Session ${this.sessionId} destroyed`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handshake message types (must match client)
|
|
238
|
+
export const ANNEX_HANDSHAKE_TYPE = {
|
|
239
|
+
PUBLIC_KEY: 'annex:public_key',
|
|
240
|
+
ENCAPSULATED: 'annex:encapsulated',
|
|
241
|
+
ENCRYPTED: 'annex:encrypted',
|
|
242
|
+
ERROR: 'annex:error',
|
|
243
|
+
REKEY: 'annex:rekey', // Server -> Client: session needs rekeying
|
|
244
|
+
REKEY_ACK: 'annex:rekey_ack', // Client -> Server: new public key for rekey
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export default ServerAnnexSession;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DARSHAN Content Streaming API
|
|
3
|
+
*
|
|
4
|
+
* "Content stays on the altar. Pilgrims come to see."
|
|
5
|
+
*
|
|
6
|
+
* Exposes the DARSHAN protocol as HTTP endpoints for:
|
|
7
|
+
* - Content registration and listing
|
|
8
|
+
* - Stream requests and chunk delivery
|
|
9
|
+
* - View attestation and proof-of-viewing
|
|
10
|
+
* - Bandwidth control and quality selection
|
|
11
|
+
*
|
|
12
|
+
* DARSHAN is the streaming backbone for yakapp-studio content sharing.
|
|
13
|
+
* Creators keep sovereignty — no copies leave unless explicitly permitted.
|
|
14
|
+
*
|
|
15
|
+
* @module server/darshan-api
|
|
16
|
+
* @license MIT
|
|
17
|
+
* @copyright 2026 YAKMESH™ Contributors
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Router } from 'express';
|
|
21
|
+
import { createLogger } from '../utils/logger.js';
|
|
22
|
+
|
|
23
|
+
const log = createLogger('server:darshan');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create the DARSHAN API router.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {Object} params.darshanGateway - DarshanGateway instance (content host)
|
|
30
|
+
* @param {Object} params.gossip - GossipProtocol instance
|
|
31
|
+
* @param {Object} params.identity - NodeIdentity instance
|
|
32
|
+
* @param {Function} params.writeLimiter - Express rate limiter for writes
|
|
33
|
+
* @param {Function} params.requirePeerAuth - Peer auth middleware
|
|
34
|
+
* @returns {Router} Express router mounted at /darshan
|
|
35
|
+
*/
|
|
36
|
+
export function createDarshanAPI({
|
|
37
|
+
darshanGateway,
|
|
38
|
+
gossip,
|
|
39
|
+
identity,
|
|
40
|
+
writeLimiter,
|
|
41
|
+
requirePeerAuth,
|
|
42
|
+
}) {
|
|
43
|
+
const router = Router();
|
|
44
|
+
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// STATUS
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
router.get('/status', (req, res) => {
|
|
50
|
+
res.json({
|
|
51
|
+
darshan: 'operational',
|
|
52
|
+
stats: darshanGateway.stats,
|
|
53
|
+
contentCount: darshanGateway.contents.size,
|
|
54
|
+
activeStreams: darshanGateway.streams.size,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
59
|
+
// CONTENT REGISTRY
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* GET /darshan/content — List available content (public metadata only)
|
|
64
|
+
*/
|
|
65
|
+
router.get('/content', (req, res) => {
|
|
66
|
+
const type = req.query.type || null;
|
|
67
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
68
|
+
|
|
69
|
+
let contents = [];
|
|
70
|
+
for (const content of darshanGateway.contents.values()) {
|
|
71
|
+
// Check exclusions
|
|
72
|
+
if (darshanGateway.exclusions.has(content.contentId)) continue;
|
|
73
|
+
|
|
74
|
+
if (type && content.contentType !== type) continue;
|
|
75
|
+
|
|
76
|
+
contents.push(content.getPublicMetadata());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
contents = contents.slice(0, limit);
|
|
80
|
+
|
|
81
|
+
res.json({
|
|
82
|
+
contents,
|
|
83
|
+
total: contents.length,
|
|
84
|
+
nodeId: identity.identity.nodeId,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* GET /darshan/content/:contentId — Get content metadata
|
|
90
|
+
*/
|
|
91
|
+
router.get('/content/:contentId', (req, res) => {
|
|
92
|
+
const { contentId } = req.params;
|
|
93
|
+
const content = darshanGateway.contents.get(contentId);
|
|
94
|
+
|
|
95
|
+
if (!content || darshanGateway.exclusions.has(contentId)) {
|
|
96
|
+
return res.status(404).json({ error: 'Content not found' });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
res.json(content.getPublicMetadata());
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* POST /darshan/content — Register content for sharing
|
|
104
|
+
*/
|
|
105
|
+
router.post('/content', writeLimiter, requirePeerAuth, (req, res) => {
|
|
106
|
+
const { path, name, description, contentType, mimeType, permissions, accessList } = req.body;
|
|
107
|
+
|
|
108
|
+
if (!path) {
|
|
109
|
+
return res.status(400).json({ error: 'path required' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Path traversal defense — reject .., absolute paths, drive letters, and null bytes.
|
|
113
|
+
// DARSHAN content paths must be relative within the host's content root.
|
|
114
|
+
// IMPORTANT: URL-decode BEFORE checking, to catch %2e%2e (%2e = .) bypass.
|
|
115
|
+
let normalizedPath;
|
|
116
|
+
try {
|
|
117
|
+
normalizedPath = decodeURIComponent(String(path)).replace(/\\/g, '/');
|
|
118
|
+
} catch {
|
|
119
|
+
return res.status(400).json({ error: 'Invalid path: malformed encoding' });
|
|
120
|
+
}
|
|
121
|
+
if (normalizedPath.includes('\0') || // Null byte injection
|
|
122
|
+
normalizedPath.includes('..') || // Traversal
|
|
123
|
+
normalizedPath.startsWith('/') || // Absolute path
|
|
124
|
+
/^[a-zA-Z]:/.test(normalizedPath)) { // Windows drive letter
|
|
125
|
+
return res.status(400).json({ error: 'Invalid path: traversal or absolute paths not allowed' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
darshanGateway.registerContent({
|
|
129
|
+
path,
|
|
130
|
+
name,
|
|
131
|
+
description,
|
|
132
|
+
contentType,
|
|
133
|
+
mimeType,
|
|
134
|
+
permissions,
|
|
135
|
+
accessList,
|
|
136
|
+
}).then(result => {
|
|
137
|
+
if (result.success !== false) {
|
|
138
|
+
// Announce to mesh
|
|
139
|
+
gossip.spreadRumor('darshan:content:available', {
|
|
140
|
+
contentId: result.contentId || result.content?.contentId,
|
|
141
|
+
hostNodeId: identity.identity.nodeId,
|
|
142
|
+
metadata: result.content?.getPublicMetadata?.() || result,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
res.json(result);
|
|
146
|
+
}).catch(error => {
|
|
147
|
+
res.status(500).json({ error: error.message });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* DELETE /darshan/content/:contentId — Unregister content
|
|
153
|
+
*/
|
|
154
|
+
router.delete('/content/:contentId', writeLimiter, requirePeerAuth, (req, res) => {
|
|
155
|
+
const { contentId } = req.params;
|
|
156
|
+
|
|
157
|
+
const content = darshanGateway.contents.get(contentId);
|
|
158
|
+
if (!content) {
|
|
159
|
+
return res.status(404).json({ error: 'Content not found' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
darshanGateway.contents.delete(contentId);
|
|
163
|
+
if (content.path) {
|
|
164
|
+
darshanGateway.contentByPath.delete(content.path);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Announce removal to mesh
|
|
168
|
+
gossip.spreadRumor('darshan:content:removed', {
|
|
169
|
+
contentId,
|
|
170
|
+
hostNodeId: identity.identity.nodeId,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
res.json({ success: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
+
// STREAMING
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* POST /darshan/stream — Request a stream
|
|
182
|
+
*/
|
|
183
|
+
router.post('/stream', writeLimiter, requirePeerAuth, (req, res) => {
|
|
184
|
+
const { contentId, quality, viewerNodeId } = req.body;
|
|
185
|
+
|
|
186
|
+
if (!contentId) {
|
|
187
|
+
return res.status(400).json({ error: 'contentId required' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const content = darshanGateway.contents.get(contentId);
|
|
191
|
+
if (!content || darshanGateway.exclusions.has(contentId)) {
|
|
192
|
+
return res.status(404).json({ error: 'Content not found' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check access if GUMBA-controlled
|
|
196
|
+
if (content.accessList) {
|
|
197
|
+
// Access verification would go through GUMBA — simplified for now
|
|
198
|
+
const viewer = viewerNodeId || req.authenticatedPeer;
|
|
199
|
+
if (!viewer) {
|
|
200
|
+
return res.status(403).json({ error: 'GUMBA access proof required' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const stream = darshanGateway.createStream?.(contentId, {
|
|
206
|
+
quality,
|
|
207
|
+
viewerNodeId: viewerNodeId || req.authenticatedPeer || 'anonymous',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (stream) {
|
|
211
|
+
res.json({
|
|
212
|
+
success: true,
|
|
213
|
+
streamId: stream.streamId || stream.id,
|
|
214
|
+
contentId,
|
|
215
|
+
totalChunks: Math.ceil(content.size / content.chunkSize || 65536),
|
|
216
|
+
chunkHashes: content.chunkHashes,
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
// Fallback: return content info for direct streaming
|
|
220
|
+
content.viewCount++;
|
|
221
|
+
darshanGateway.stats.streamsCreated++;
|
|
222
|
+
res.json({
|
|
223
|
+
success: true,
|
|
224
|
+
contentId,
|
|
225
|
+
size: content.size,
|
|
226
|
+
mimeType: content.mimeType,
|
|
227
|
+
hash: content.hash,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
res.status(500).json({ error: error.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* GET /darshan/stream/:streamId/chunk/:index — Get a chunk
|
|
237
|
+
*/
|
|
238
|
+
router.get('/stream/:streamId/chunk/:index', (req, res) => {
|
|
239
|
+
const { streamId, index } = req.params;
|
|
240
|
+
const chunkIndex = parseInt(index);
|
|
241
|
+
|
|
242
|
+
const stream = darshanGateway.streams.get(streamId);
|
|
243
|
+
if (!stream) {
|
|
244
|
+
return res.status(404).json({ error: 'Stream not found or expired' });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const chunk = stream.getChunk?.(chunkIndex);
|
|
249
|
+
if (chunk) {
|
|
250
|
+
res.set('Content-Type', 'application/octet-stream');
|
|
251
|
+
res.set('X-Chunk-Index', chunkIndex.toString());
|
|
252
|
+
res.set('X-Chunk-Hash', chunk.hash || '');
|
|
253
|
+
res.send(chunk.data || chunk);
|
|
254
|
+
} else {
|
|
255
|
+
res.status(404).json({ error: 'Chunk not available' });
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
res.status(500).json({ error: error.message });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
263
|
+
// VIEW ATTESTATION
|
|
264
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* POST /darshan/attest — Submit a view attestation (proof of viewing)
|
|
268
|
+
*/
|
|
269
|
+
router.post('/attest', requirePeerAuth, (req, res) => {
|
|
270
|
+
const { contentId, viewerNodeId, streamId, duration, bytesConsumed } = req.body;
|
|
271
|
+
|
|
272
|
+
if (!contentId || !viewerNodeId) {
|
|
273
|
+
return res.status(400).json({ error: 'contentId and viewerNodeId required' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const attestation = darshanGateway.createAttestation?.({
|
|
278
|
+
contentId,
|
|
279
|
+
viewerNodeId,
|
|
280
|
+
streamId,
|
|
281
|
+
duration,
|
|
282
|
+
bytesConsumed,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (attestation) {
|
|
286
|
+
res.json({
|
|
287
|
+
success: true,
|
|
288
|
+
attestationId: attestation.attestationId || attestation.id,
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
// Track basic stats even without full attestation module
|
|
292
|
+
const content = darshanGateway.contents.get(contentId);
|
|
293
|
+
if (content) {
|
|
294
|
+
content.viewCount++;
|
|
295
|
+
content.totalBytesServed += bytesConsumed || 0;
|
|
296
|
+
}
|
|
297
|
+
darshanGateway.stats.attestationsCreated++;
|
|
298
|
+
res.json({ success: true, tracked: true });
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
res.status(500).json({ error: error.message });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* GET /darshan/attestations/:contentId — Get view attestations for content
|
|
307
|
+
*/
|
|
308
|
+
router.get('/attestations/:contentId', (req, res) => {
|
|
309
|
+
const { contentId } = req.params;
|
|
310
|
+
|
|
311
|
+
const attestations = [];
|
|
312
|
+
for (const att of darshanGateway.attestations.values()) {
|
|
313
|
+
if (att.contentId === contentId) {
|
|
314
|
+
attestations.push(att.toJSON ? att.toJSON() : att);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
res.json({ attestations, total: attestations.length });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
log.info('✓ DARSHAN API initialized (content streaming + attestation)');
|
|
322
|
+
|
|
323
|
+
return router;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Wire DARSHAN gossip handlers into the mesh rumor stream.
|
|
328
|
+
*/
|
|
329
|
+
export function wireDarshanGossip(mesh, darshanGateway) {
|
|
330
|
+
mesh.on('rumor', (topic, data, origin) => {
|
|
331
|
+
// Content availability announcements from other nodes
|
|
332
|
+
if (topic === 'darshan:content:available') {
|
|
333
|
+
darshanGateway.emit?.('remote:content:available', data);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Content removal announcements
|
|
337
|
+
if (topic === 'darshan:content:removed') {
|
|
338
|
+
darshanGateway.emit?.('remote:content:removed', data);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
log.info('✓ DARSHAN gossip handlers wired into mesh');
|
|
343
|
+
}
|