yakmesh 1.3.1 → 1.4.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 +20 -0
- package/README.md +6 -0
- package/announcements/discord-v1.3.1.md +55 -0
- package/content/api.js +348 -0
- package/content/index.js +19 -0
- package/content/store.js +683 -0
- package/deploy-packages/README.md +114 -0
- package/deploy-packages/build-packages.ps1 +205 -0
- package/deploy-packages/yakmesh-full/README.md +122 -0
- package/deploy-packages/yakmesh-full/config/Caddyfile +61 -0
- package/deploy-packages/yakmesh-full/config/php.ini +55 -0
- package/deploy-packages/yakmesh-full/config/yakmesh.config.js +100 -0
- package/deploy-packages/yakmesh-full/start.ps1 +193 -0
- package/deploy-packages/yakmesh-full/stop.ps1 +35 -0
- package/deploy-packages/yakmesh-minimal/README.md +69 -0
- package/deploy-packages/yakmesh-minimal/config/Caddyfile +46 -0
- package/deploy-packages/yakmesh-minimal/config/yakmesh.config.js +79 -0
- package/deploy-packages/yakmesh-minimal/start.ps1 +165 -0
- package/deploy-packages/yakmesh-minimal/stop.ps1 +29 -0
- package/discord.md +13 -58
- package/gossip/protocol.js +13 -2
- package/mesh/annex.js +743 -0
- package/mesh/beacon-broadcast.js +4 -5
- package/mesh/echo-ranging.js +3 -4
- package/mesh/phantom-routing.js +3 -4
- package/mesh/pulse-sync.js +3 -4
- package/mesh/temporal-encoder.js +3 -3
- package/package.json +1 -1
- package/server/index.js +64 -3
- package/yakmesh.config.js +8 -0
package/content/store.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAKMESH™ Content Store
|
|
3
|
+
* Content-addressed storage with consensus proofs
|
|
4
|
+
*
|
|
5
|
+
* Provides public content delivery while maintaining mesh security:
|
|
6
|
+
* - Content addressed by hash (trustless verification)
|
|
7
|
+
* - Consensus proofs for light client verification
|
|
8
|
+
* - Edge caching for instant public access
|
|
9
|
+
* - Mesh sync for decentralized replication
|
|
10
|
+
*
|
|
11
|
+
* @module content/store
|
|
12
|
+
* @license MIT
|
|
13
|
+
* @copyright 2026 YAKMESH Contributors
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
17
|
+
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
18
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
19
|
+
import { join, dirname } from 'path';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Content types supported
|
|
23
|
+
*/
|
|
24
|
+
export const ContentType = {
|
|
25
|
+
JSON: 'application/json',
|
|
26
|
+
HTML: 'text/html',
|
|
27
|
+
TEXT: 'text/plain',
|
|
28
|
+
BINARY: 'application/octet-stream',
|
|
29
|
+
JAVASCRIPT: 'application/javascript',
|
|
30
|
+
CSS: 'text/css',
|
|
31
|
+
IMAGE_PNG: 'image/png',
|
|
32
|
+
IMAGE_JPG: 'image/jpeg',
|
|
33
|
+
IMAGE_SVG: 'image/svg+xml',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Content status in the network
|
|
38
|
+
*/
|
|
39
|
+
export const ContentStatus = {
|
|
40
|
+
LOCAL: 'local', // Only on this node
|
|
41
|
+
PENDING: 'pending', // Awaiting consensus
|
|
42
|
+
VERIFIED: 'verified', // Consensus reached
|
|
43
|
+
REJECTED: 'rejected', // Failed consensus
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compute content hash (SHA3-256)
|
|
48
|
+
*/
|
|
49
|
+
export function computeContentHash(content) {
|
|
50
|
+
if (typeof content === 'string') {
|
|
51
|
+
return bytesToHex(sha3_256(utf8ToBytes(content)));
|
|
52
|
+
}
|
|
53
|
+
if (Buffer.isBuffer(content)) {
|
|
54
|
+
return bytesToHex(sha3_256(new Uint8Array(content)));
|
|
55
|
+
}
|
|
56
|
+
if (content instanceof Uint8Array) {
|
|
57
|
+
return bytesToHex(sha3_256(content));
|
|
58
|
+
}
|
|
59
|
+
// Object - serialize deterministically
|
|
60
|
+
return bytesToHex(sha3_256(utf8ToBytes(JSON.stringify(content))));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Content metadata
|
|
65
|
+
*/
|
|
66
|
+
class ContentMetadata {
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
this.hash = options.hash;
|
|
69
|
+
this.contentType = options.contentType || ContentType.BINARY;
|
|
70
|
+
this.size = options.size || 0;
|
|
71
|
+
this.createdAt = options.createdAt || Date.now();
|
|
72
|
+
this.publishedBy = options.publishedBy || null;
|
|
73
|
+
this.status = options.status || ContentStatus.LOCAL;
|
|
74
|
+
this.consensusProof = options.consensusProof || null;
|
|
75
|
+
this.tags = options.tags || [];
|
|
76
|
+
this.name = options.name || null; // Optional human-readable name
|
|
77
|
+
this.ttl = options.ttl || 0; // 0 = permanent
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
toJSON() {
|
|
81
|
+
return {
|
|
82
|
+
hash: this.hash,
|
|
83
|
+
contentType: this.contentType,
|
|
84
|
+
size: this.size,
|
|
85
|
+
createdAt: this.createdAt,
|
|
86
|
+
publishedBy: this.publishedBy,
|
|
87
|
+
status: this.status,
|
|
88
|
+
consensusProof: this.consensusProof,
|
|
89
|
+
tags: this.tags,
|
|
90
|
+
name: this.name,
|
|
91
|
+
ttl: this.ttl,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static fromJSON(json) {
|
|
96
|
+
return new ContentMetadata(json);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Consensus proof for light client verification
|
|
102
|
+
*/
|
|
103
|
+
class ConsensusProof {
|
|
104
|
+
constructor(options = {}) {
|
|
105
|
+
this.contentHash = options.contentHash;
|
|
106
|
+
this.timestamp = options.timestamp || Date.now();
|
|
107
|
+
this.validators = options.validators || []; // Array of { nodeId, signature }
|
|
108
|
+
this.quorum = options.quorum || 0; // Required signatures
|
|
109
|
+
this.networkId = options.networkId || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if proof has quorum
|
|
114
|
+
*/
|
|
115
|
+
hasQuorum() {
|
|
116
|
+
return this.validators.length >= this.quorum;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add validator signature
|
|
121
|
+
*/
|
|
122
|
+
addValidator(nodeId, signature) {
|
|
123
|
+
if (!this.validators.find(v => v.nodeId === nodeId)) {
|
|
124
|
+
this.validators.push({ nodeId, signature, timestamp: Date.now() });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toJSON() {
|
|
129
|
+
return {
|
|
130
|
+
contentHash: this.contentHash,
|
|
131
|
+
timestamp: this.timestamp,
|
|
132
|
+
validators: this.validators,
|
|
133
|
+
quorum: this.quorum,
|
|
134
|
+
networkId: this.networkId,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static fromJSON(json) {
|
|
139
|
+
return new ConsensusProof(json);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* YAKMESH Content Store
|
|
145
|
+
* Content-addressed storage with mesh sync and public delivery
|
|
146
|
+
*/
|
|
147
|
+
export class ContentStore {
|
|
148
|
+
constructor(config = {}) {
|
|
149
|
+
this.config = {
|
|
150
|
+
dataDir: config.dataDir || './data/content',
|
|
151
|
+
maxContentSize: config.maxContentSize || 10 * 1024 * 1024, // 10MB default
|
|
152
|
+
cacheSize: config.cacheSize || 100, // LRU cache entries
|
|
153
|
+
quorumSize: config.quorumSize || 2, // Minimum validators
|
|
154
|
+
...config,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
this.contentDir = join(this.config.dataDir, 'objects');
|
|
158
|
+
this.metaDir = join(this.config.dataDir, 'meta');
|
|
159
|
+
|
|
160
|
+
// In-memory caches
|
|
161
|
+
this.contentCache = new Map(); // hash -> content (LRU)
|
|
162
|
+
this.metaCache = new Map(); // hash -> ContentMetadata
|
|
163
|
+
this.nameIndex = new Map(); // name -> hash (for human-readable lookup)
|
|
164
|
+
|
|
165
|
+
// Mesh integration (set by init)
|
|
166
|
+
this.mesh = null;
|
|
167
|
+
this.identity = null;
|
|
168
|
+
this.oracle = null;
|
|
169
|
+
this.gossip = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Initialize the content store
|
|
174
|
+
*/
|
|
175
|
+
async init(node = null) {
|
|
176
|
+
// Create directories
|
|
177
|
+
mkdirSync(this.contentDir, { recursive: true });
|
|
178
|
+
mkdirSync(this.metaDir, { recursive: true });
|
|
179
|
+
|
|
180
|
+
// Load existing metadata into cache
|
|
181
|
+
this._loadMetadataIndex();
|
|
182
|
+
|
|
183
|
+
// Integrate with node if provided
|
|
184
|
+
if (node) {
|
|
185
|
+
this.mesh = node.mesh;
|
|
186
|
+
this.identity = node.identity;
|
|
187
|
+
this.oracle = node.oracle;
|
|
188
|
+
this.gossip = node.gossip;
|
|
189
|
+
|
|
190
|
+
// Content gossip is handled by the server via mesh.on('rumor')
|
|
191
|
+
// which calls contentStore._handleContentGossip()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(`✓ Content store initialized: ${this.config.dataDir}`);
|
|
195
|
+
console.log(` Objects: ${this.metaCache.size}`);
|
|
196
|
+
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Load metadata index from disk
|
|
202
|
+
*/
|
|
203
|
+
_loadMetadataIndex() {
|
|
204
|
+
if (!existsSync(this.metaDir)) return;
|
|
205
|
+
|
|
206
|
+
const files = readdirSync(this.metaDir);
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
if (!file.endsWith('.json')) continue;
|
|
209
|
+
try {
|
|
210
|
+
const metaPath = join(this.metaDir, file);
|
|
211
|
+
const json = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
212
|
+
const meta = ContentMetadata.fromJSON(json);
|
|
213
|
+
this.metaCache.set(meta.hash, meta);
|
|
214
|
+
if (meta.name) {
|
|
215
|
+
this.nameIndex.set(meta.name, meta.hash);
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.warn(`Failed to load metadata: ${file}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get content path for a hash
|
|
225
|
+
*/
|
|
226
|
+
_getContentPath(hash) {
|
|
227
|
+
// Store in subdirectories for filesystem efficiency (git-style)
|
|
228
|
+
const prefix = hash.slice(0, 2);
|
|
229
|
+
const suffix = hash.slice(2);
|
|
230
|
+
return join(this.contentDir, prefix, suffix);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get metadata path for a hash
|
|
235
|
+
*/
|
|
236
|
+
_getMetaPath(hash) {
|
|
237
|
+
return join(this.metaDir, `${hash}.json`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Store content
|
|
242
|
+
*/
|
|
243
|
+
async store(content, options = {}) {
|
|
244
|
+
// Compute hash
|
|
245
|
+
const hash = computeContentHash(content);
|
|
246
|
+
|
|
247
|
+
// Check size limit
|
|
248
|
+
const size = Buffer.isBuffer(content) ? content.length :
|
|
249
|
+
typeof content === 'string' ? Buffer.byteLength(content) :
|
|
250
|
+
Buffer.byteLength(JSON.stringify(content));
|
|
251
|
+
|
|
252
|
+
if (size > this.config.maxContentSize) {
|
|
253
|
+
throw new Error(`Content exceeds max size: ${size} > ${this.config.maxContentSize}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check if already exists
|
|
257
|
+
if (this.has(hash)) {
|
|
258
|
+
const existing = this.getMeta(hash);
|
|
259
|
+
return { hash, status: 'exists', meta: existing };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Create metadata
|
|
263
|
+
const meta = new ContentMetadata({
|
|
264
|
+
hash,
|
|
265
|
+
contentType: options.contentType || this._detectContentType(content),
|
|
266
|
+
size,
|
|
267
|
+
publishedBy: this.identity?.identity?.nodeId || options.publishedBy || 'unknown',
|
|
268
|
+
status: ContentStatus.LOCAL,
|
|
269
|
+
tags: options.tags || [],
|
|
270
|
+
name: options.name || null,
|
|
271
|
+
ttl: options.ttl || 0,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Write content to disk
|
|
275
|
+
const contentPath = this._getContentPath(hash);
|
|
276
|
+
mkdirSync(dirname(contentPath), { recursive: true });
|
|
277
|
+
|
|
278
|
+
if (Buffer.isBuffer(content)) {
|
|
279
|
+
writeFileSync(contentPath, content);
|
|
280
|
+
} else if (typeof content === 'string') {
|
|
281
|
+
writeFileSync(contentPath, content, 'utf8');
|
|
282
|
+
} else {
|
|
283
|
+
writeFileSync(contentPath, JSON.stringify(content), 'utf8');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Write metadata
|
|
287
|
+
writeFileSync(this._getMetaPath(hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
288
|
+
|
|
289
|
+
// Update caches
|
|
290
|
+
this.metaCache.set(hash, meta);
|
|
291
|
+
if (meta.name) {
|
|
292
|
+
this.nameIndex.set(meta.name, hash);
|
|
293
|
+
}
|
|
294
|
+
this._addToContentCache(hash, content);
|
|
295
|
+
|
|
296
|
+
// Gossip to mesh
|
|
297
|
+
if (this.gossip && options.publish !== false) {
|
|
298
|
+
await this.publish(hash);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { hash, status: 'stored', meta };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Retrieve content by hash
|
|
306
|
+
*/
|
|
307
|
+
get(hash) {
|
|
308
|
+
// Resolve name to hash if needed
|
|
309
|
+
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
310
|
+
hash = this.nameIndex.get(hash) || hash;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check memory cache
|
|
314
|
+
if (this.contentCache.has(hash)) {
|
|
315
|
+
return this.contentCache.get(hash);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check disk
|
|
319
|
+
const contentPath = this._getContentPath(hash);
|
|
320
|
+
if (!existsSync(contentPath)) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Load and cache
|
|
325
|
+
const content = readFileSync(contentPath);
|
|
326
|
+
this._addToContentCache(hash, content);
|
|
327
|
+
|
|
328
|
+
return content;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get content with metadata and proof
|
|
333
|
+
*/
|
|
334
|
+
getWithProof(hash) {
|
|
335
|
+
const content = this.get(hash);
|
|
336
|
+
if (!content) return null;
|
|
337
|
+
|
|
338
|
+
const meta = this.getMeta(hash);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
content,
|
|
342
|
+
hash,
|
|
343
|
+
meta: meta?.toJSON() || null,
|
|
344
|
+
proof: meta?.consensusProof || null,
|
|
345
|
+
verified: meta?.status === ContentStatus.VERIFIED,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get metadata for content
|
|
351
|
+
*/
|
|
352
|
+
getMeta(hash) {
|
|
353
|
+
// Resolve name if needed
|
|
354
|
+
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
355
|
+
hash = this.nameIndex.get(hash) || hash;
|
|
356
|
+
}
|
|
357
|
+
return this.metaCache.get(hash) || null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Check if content exists
|
|
362
|
+
*/
|
|
363
|
+
has(hash) {
|
|
364
|
+
// Resolve name if needed
|
|
365
|
+
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
366
|
+
hash = this.nameIndex.get(hash) || hash;
|
|
367
|
+
}
|
|
368
|
+
return this.metaCache.has(hash) || existsSync(this._getContentPath(hash));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Delete content
|
|
373
|
+
*/
|
|
374
|
+
delete(hash) {
|
|
375
|
+
const meta = this.getMeta(hash);
|
|
376
|
+
|
|
377
|
+
// Remove from disk
|
|
378
|
+
const contentPath = this._getContentPath(hash);
|
|
379
|
+
const metaPath = this._getMetaPath(hash);
|
|
380
|
+
|
|
381
|
+
if (existsSync(contentPath)) unlinkSync(contentPath);
|
|
382
|
+
if (existsSync(metaPath)) unlinkSync(metaPath);
|
|
383
|
+
|
|
384
|
+
// Remove from caches
|
|
385
|
+
this.contentCache.delete(hash);
|
|
386
|
+
this.metaCache.delete(hash);
|
|
387
|
+
if (meta?.name) {
|
|
388
|
+
this.nameIndex.delete(meta.name);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* List all content
|
|
396
|
+
*/
|
|
397
|
+
list(options = {}) {
|
|
398
|
+
const { tag, status, limit = 100, offset = 0 } = options;
|
|
399
|
+
|
|
400
|
+
let items = Array.from(this.metaCache.values());
|
|
401
|
+
|
|
402
|
+
// Filter by tag
|
|
403
|
+
if (tag) {
|
|
404
|
+
items = items.filter(m => m.tags.includes(tag));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Filter by status
|
|
408
|
+
if (status) {
|
|
409
|
+
items = items.filter(m => m.status === status);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Sort by created date (newest first)
|
|
413
|
+
items.sort((a, b) => b.createdAt - a.createdAt);
|
|
414
|
+
|
|
415
|
+
// Paginate
|
|
416
|
+
return items.slice(offset, offset + limit).map(m => m.toJSON());
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Publish content to mesh
|
|
421
|
+
*/
|
|
422
|
+
async publish(hash) {
|
|
423
|
+
const meta = this.getMeta(hash);
|
|
424
|
+
if (!meta) {
|
|
425
|
+
throw new Error(`Content not found: ${hash}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Create announcement message
|
|
429
|
+
const announcement = {
|
|
430
|
+
type: 'content_announce',
|
|
431
|
+
hash,
|
|
432
|
+
meta: {
|
|
433
|
+
contentType: meta.contentType,
|
|
434
|
+
size: meta.size,
|
|
435
|
+
publishedBy: meta.publishedBy,
|
|
436
|
+
tags: meta.tags,
|
|
437
|
+
name: meta.name,
|
|
438
|
+
},
|
|
439
|
+
timestamp: Date.now(),
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// Sign with node identity
|
|
443
|
+
if (this.identity) {
|
|
444
|
+
announcement.signature = this.identity.sign(JSON.stringify(announcement));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Gossip to mesh
|
|
448
|
+
if (this.gossip) {
|
|
449
|
+
console.log(`📡 Gossiping content_announce for ${hash.slice(0, 16)}...`);
|
|
450
|
+
this.gossip.spreadRumor('content', announcement);
|
|
451
|
+
} else {
|
|
452
|
+
console.log(`⚠️ No gossip protocol available for content announce`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update status
|
|
456
|
+
meta.status = ContentStatus.PENDING;
|
|
457
|
+
writeFileSync(this._getMetaPath(hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
458
|
+
|
|
459
|
+
return { published: true, hash };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Request content from mesh
|
|
464
|
+
*/
|
|
465
|
+
async request(hash) {
|
|
466
|
+
if (this.has(hash)) {
|
|
467
|
+
return this.getWithProof(hash);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Broadcast request
|
|
471
|
+
if (this.gossip) {
|
|
472
|
+
this.gossip.spreadRumor('content', {
|
|
473
|
+
type: 'content_request',
|
|
474
|
+
hash,
|
|
475
|
+
requestedBy: this.identity?.identity?.nodeId,
|
|
476
|
+
timestamp: Date.now(),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Wait for response (with timeout)
|
|
481
|
+
return new Promise((resolve, reject) => {
|
|
482
|
+
const timeout = setTimeout(() => {
|
|
483
|
+
reject(new Error(`Content not found: ${hash}`));
|
|
484
|
+
}, 10000);
|
|
485
|
+
|
|
486
|
+
const checkInterval = setInterval(() => {
|
|
487
|
+
if (this.has(hash)) {
|
|
488
|
+
clearTimeout(timeout);
|
|
489
|
+
clearInterval(checkInterval);
|
|
490
|
+
resolve(this.getWithProof(hash));
|
|
491
|
+
}
|
|
492
|
+
}, 500);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Handle content gossip from peers
|
|
498
|
+
*/
|
|
499
|
+
async _handleContentGossip(data, origin) {
|
|
500
|
+
switch (data.type) {
|
|
501
|
+
case 'content_announce':
|
|
502
|
+
// Peer has new content - request it if we don't have it
|
|
503
|
+
if (!this.has(data.hash)) {
|
|
504
|
+
console.log(`📦 New content announced: ${data.hash.slice(0, 16)}... from ${origin.slice(0, 16)}...`);
|
|
505
|
+
// Request full content via gossip
|
|
506
|
+
if (this.gossip) {
|
|
507
|
+
this.gossip.spreadRumor('content', {
|
|
508
|
+
type: 'content_request',
|
|
509
|
+
hash: data.hash,
|
|
510
|
+
requestedBy: this.identity?.identity?.nodeId,
|
|
511
|
+
timestamp: Date.now(),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'content_request':
|
|
518
|
+
// Peer wants content - send if we have it
|
|
519
|
+
if (this.has(data.hash)) {
|
|
520
|
+
const result = this.getWithProof(data.hash);
|
|
521
|
+
if (this.gossip && result) {
|
|
522
|
+
// Ensure content is properly encoded as base64
|
|
523
|
+
let contentBase64;
|
|
524
|
+
if (Buffer.isBuffer(result.content)) {
|
|
525
|
+
contentBase64 = result.content.toString('base64');
|
|
526
|
+
} else if (typeof result.content === 'string') {
|
|
527
|
+
contentBase64 = Buffer.from(result.content, 'utf8').toString('base64');
|
|
528
|
+
} else {
|
|
529
|
+
contentBase64 = Buffer.from(JSON.stringify(result.content), 'utf8').toString('base64');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
this.gossip.spreadRumor('content', {
|
|
533
|
+
type: 'content_response',
|
|
534
|
+
hash: data.hash,
|
|
535
|
+
content: contentBase64,
|
|
536
|
+
meta: result.meta,
|
|
537
|
+
proof: result.proof,
|
|
538
|
+
timestamp: Date.now(),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
case 'content_response':
|
|
545
|
+
// Received content from peer
|
|
546
|
+
if (!this.has(data.hash)) {
|
|
547
|
+
const content = Buffer.from(data.content, 'base64');
|
|
548
|
+
const computedHash = computeContentHash(content);
|
|
549
|
+
|
|
550
|
+
// Verify hash
|
|
551
|
+
if (computedHash !== data.hash) {
|
|
552
|
+
console.warn(`⚠️ Content hash mismatch from ${origin.slice(0, 16)}...`);
|
|
553
|
+
return;
|
|
554
|
+
} // Store it
|
|
555
|
+
await this.store(content, {
|
|
556
|
+
...data.meta,
|
|
557
|
+
publish: false, // Don't re-gossip
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Apply consensus proof if present
|
|
561
|
+
if (data.proof) {
|
|
562
|
+
const meta = this.getMeta(data.hash);
|
|
563
|
+
meta.consensusProof = ConsensusProof.fromJSON(data.proof);
|
|
564
|
+
meta.status = data.proof.hasQuorum?.() ? ContentStatus.VERIFIED : ContentStatus.PENDING;
|
|
565
|
+
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
console.log(`✓ Content received: ${data.hash.slice(0, 16)}...`);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
|
|
572
|
+
case 'content_validate':
|
|
573
|
+
// Peer is requesting validation vote
|
|
574
|
+
if (this.has(data.hash) && this.identity && this.oracle) {
|
|
575
|
+
const content = this.get(data.hash);
|
|
576
|
+
const isValid = this.oracle.validateContent(content, data.contentType);
|
|
577
|
+
|
|
578
|
+
if (isValid) {
|
|
579
|
+
// Sign validation
|
|
580
|
+
const vote = {
|
|
581
|
+
type: 'content_vote',
|
|
582
|
+
hash: data.hash,
|
|
583
|
+
nodeId: this.identity.identity.nodeId,
|
|
584
|
+
vote: 'valid',
|
|
585
|
+
signature: this.identity.sign(data.hash),
|
|
586
|
+
timestamp: Date.now(),
|
|
587
|
+
};
|
|
588
|
+
this.gossip.spreadRumor('content', vote);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
|
|
593
|
+
case 'content_vote':
|
|
594
|
+
// Received validation vote
|
|
595
|
+
const meta = this.getMeta(data.hash);
|
|
596
|
+
if (meta) {
|
|
597
|
+
if (!meta.consensusProof) {
|
|
598
|
+
meta.consensusProof = new ConsensusProof({
|
|
599
|
+
contentHash: data.hash,
|
|
600
|
+
quorum: this.config.quorumSize,
|
|
601
|
+
networkId: this.mesh?.networkId,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
meta.consensusProof.addValidator(data.nodeId, data.signature);
|
|
605
|
+
|
|
606
|
+
if (meta.consensusProof.hasQuorum()) {
|
|
607
|
+
meta.status = ContentStatus.VERIFIED;
|
|
608
|
+
console.log(`✓ Content verified (quorum reached): ${data.hash.slice(0, 16)}...`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Add to LRU content cache
|
|
619
|
+
*/
|
|
620
|
+
_addToContentCache(hash, content) {
|
|
621
|
+
// Simple LRU: remove oldest if at capacity
|
|
622
|
+
if (this.contentCache.size >= this.config.cacheSize) {
|
|
623
|
+
const oldest = this.contentCache.keys().next().value;
|
|
624
|
+
this.contentCache.delete(oldest);
|
|
625
|
+
}
|
|
626
|
+
this.contentCache.set(hash, content);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Detect content type from content
|
|
631
|
+
*/
|
|
632
|
+
_detectContentType(content) {
|
|
633
|
+
if (typeof content === 'object' && !Buffer.isBuffer(content)) {
|
|
634
|
+
return ContentType.JSON;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const str = content.toString().slice(0, 100);
|
|
638
|
+
|
|
639
|
+
if (str.startsWith('<!DOCTYPE') || str.startsWith('<html')) {
|
|
640
|
+
return ContentType.HTML;
|
|
641
|
+
}
|
|
642
|
+
if (str.startsWith('{') || str.startsWith('[')) {
|
|
643
|
+
return ContentType.JSON;
|
|
644
|
+
}
|
|
645
|
+
if (str.includes('function') || str.includes('const ') || str.includes('import ')) {
|
|
646
|
+
return ContentType.JAVASCRIPT;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return ContentType.TEXT;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Get store statistics
|
|
654
|
+
*/
|
|
655
|
+
getStats() {
|
|
656
|
+
let totalSize = 0;
|
|
657
|
+
let verified = 0;
|
|
658
|
+
let pending = 0;
|
|
659
|
+
let local = 0;
|
|
660
|
+
|
|
661
|
+
for (const meta of this.metaCache.values()) {
|
|
662
|
+
totalSize += meta.size;
|
|
663
|
+
switch (meta.status) {
|
|
664
|
+
case ContentStatus.VERIFIED: verified++; break;
|
|
665
|
+
case ContentStatus.PENDING: pending++; break;
|
|
666
|
+
case ContentStatus.LOCAL: local++; break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
totalObjects: this.metaCache.size,
|
|
672
|
+
totalSize,
|
|
673
|
+
verified,
|
|
674
|
+
pending,
|
|
675
|
+
local,
|
|
676
|
+
cacheSize: this.contentCache.size,
|
|
677
|
+
dataDir: this.config.dataDir,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export { ContentMetadata, ConsensusProof };
|
|
683
|
+
export default ContentStore;
|