yakmesh 1.3.1 β 1.3.2
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 +670 -0
- package/discord.md +9 -4
- package/package.json +1 -1
- package/server/index.js +40 -3
- package/yakmesh.config.js +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to YAKMESH will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.2] - 2026-01-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Public Content Delivery API** - Content-addressed storage for decentralized website hosting
|
|
9
|
+
- `GET /content` - List available content with stats
|
|
10
|
+
- `GET /content/:hash` - Fetch content by hash with optional proof
|
|
11
|
+
- `POST /content` - Publish content with consensus verification
|
|
12
|
+
- Content gossip via mesh for cross-node synchronization
|
|
13
|
+
- Consensus proof system for verified content
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Gossip protocol method calls (use `spreadRumor()` instead of `broadcast()`)
|
|
17
|
+
- Direct messaging via mesh instead of non-existent gossip.sendTo()
|
|
18
|
+
|
|
19
|
+
### Community
|
|
20
|
+
- Added social links: Discord, Telegram, X (Twitter)
|
|
21
|
+
- Created Discord announcement template
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
5
25
|
## [1.3.1] - 2026-01-16
|
|
6
26
|
|
|
7
27
|
### Security
|
package/README.md
CHANGED
|
@@ -179,6 +179,12 @@ See [TRADEMARK.md](TRADEMARK.md) for trademark usage policy.
|
|
|
179
179
|
<br><br>
|
|
180
180
|
<strong><a href="https://yakmesh.dev">yakmesh.dev</a></strong>
|
|
181
181
|
<br><br>
|
|
182
|
+
<p>
|
|
183
|
+
<a href="https://discord.gg/E62tAE2wGh">π¬ Discord</a> β’
|
|
184
|
+
<a href="https://t.me/yakmesh">π± Telegram</a> β’
|
|
185
|
+
<a href="https://x.com/yakmesh">π Twitter</a>
|
|
186
|
+
</p>
|
|
187
|
+
<br>
|
|
182
188
|
<sub>Β© 2026 YAKMESHβ’ Project. Sturdy & Secure.</sub>
|
|
183
189
|
<br>
|
|
184
190
|
<sub>YAKMESHβ’ is a trademark of PeerQuanta, application pending (Serial No. 99594620).</sub>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 𦬠YAKMESH v1.3.1 β Public Content Delivery + Mesh Peering Confirmed!
|
|
2
|
+
|
|
3
|
+
Hey everyone! Big update today:
|
|
4
|
+
|
|
5
|
+
## β
What's New
|
|
6
|
+
|
|
7
|
+
### π Public Content Delivery API
|
|
8
|
+
We've added a complete **content-addressed storage system** with public delivery:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
GET /content/:hash β Fetch any content by its hash
|
|
12
|
+
GET /content/:hash/proof β Get consensus proof for verification
|
|
13
|
+
POST /content/publish β Store and gossip content to mesh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Key features:**
|
|
17
|
+
- Content addressed by SHA3-256 hash (trustless verification)
|
|
18
|
+
- Consensus proofs for light client verification
|
|
19
|
+
- LRU caching for instant edge delivery
|
|
20
|
+
- Automatic mesh sync via gossip protocol
|
|
21
|
+
|
|
22
|
+
### π First Successful LAN Mesh Peering
|
|
23
|
+
Tested and confirmed: **two Yakmesh nodes successfully peered** with matching network fingerprints. The Code Proof Protocol verified both were running identical codebases before allowing the connection.
|
|
24
|
+
|
|
25
|
+
**Connection is as simple as:**
|
|
26
|
+
```powershell
|
|
27
|
+
POST http://localhost:3000/connect
|
|
28
|
+
{ "address": "ws://192.168.1.178:9001" }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### π± New Social Channels
|
|
32
|
+
We're now on:
|
|
33
|
+
- π¬ **Discord**: https://discord.gg/E62tAE2wGh
|
|
34
|
+
- π± **Telegram**: https://t.me/yakmesh
|
|
35
|
+
- π **Twitter**: https://x.com/yakmesh
|
|
36
|
+
|
|
37
|
+
## π¦ Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install yakmesh@1.3.1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## π Links
|
|
44
|
+
- π Website: https://yakmesh.dev
|
|
45
|
+
- π GitHub: https://github.com/yakmesh/yakmesh
|
|
46
|
+
- π¦ npm: https://npmjs.com/package/yakmesh
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
**What's next?**
|
|
51
|
+
- Multi-node cluster testing
|
|
52
|
+
- Production deployment
|
|
53
|
+
- Website/webapp hosting demos
|
|
54
|
+
|
|
55
|
+
Questions? Drop them here! π¦¬
|
package/content/api.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAKMESHβ’ Public Content API
|
|
3
|
+
* HTTP endpoints for public content delivery
|
|
4
|
+
*
|
|
5
|
+
* Public (no auth required):
|
|
6
|
+
* - GET /content/:hash - Fetch content by hash
|
|
7
|
+
* - GET /content/:hash/meta - Fetch metadata only
|
|
8
|
+
* - GET /content/:hash/proof - Fetch consensus proof
|
|
9
|
+
* - GET /content/list - List available content
|
|
10
|
+
*
|
|
11
|
+
* Authenticated (rate limited):
|
|
12
|
+
* - POST /content/publish - Publish new content
|
|
13
|
+
* - DELETE /content/:hash - Remove content (owner only)
|
|
14
|
+
*
|
|
15
|
+
* @module content/api
|
|
16
|
+
* @license MIT
|
|
17
|
+
* @copyright 2026 YAKMESH Contributors
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Router } from 'express';
|
|
21
|
+
import { ContentStore, ContentType, ContentStatus, computeContentHash } from './store.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create content API router
|
|
25
|
+
*/
|
|
26
|
+
export function createContentAPI(contentStore, options = {}) {
|
|
27
|
+
const router = Router();
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
writeLimiter,
|
|
31
|
+
readLimiter,
|
|
32
|
+
validateString,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
// =========================================
|
|
36
|
+
// PUBLIC READ ENDPOINTS (No Auth)
|
|
37
|
+
// =========================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* GET /content/:hash
|
|
41
|
+
* Fetch content by hash with optional proof
|
|
42
|
+
*
|
|
43
|
+
* Query params:
|
|
44
|
+
* - proof=1 : Include consensus proof in response headers
|
|
45
|
+
* - download=1 : Force download (Content-Disposition)
|
|
46
|
+
*/
|
|
47
|
+
router.get('/:hash', readLimiter, (req, res) => {
|
|
48
|
+
const { hash } = req.params;
|
|
49
|
+
const includeProof = req.query.proof === '1';
|
|
50
|
+
const download = req.query.download === '1';
|
|
51
|
+
|
|
52
|
+
// Get content with metadata
|
|
53
|
+
const result = contentStore.getWithProof(hash);
|
|
54
|
+
|
|
55
|
+
if (!result) {
|
|
56
|
+
return res.status(404).json({
|
|
57
|
+
error: 'Content not found',
|
|
58
|
+
hash,
|
|
59
|
+
hint: 'Content may not have synced yet. Try again later.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set content type
|
|
64
|
+
res.setHeader('Content-Type', result.meta?.contentType || 'application/octet-stream');
|
|
65
|
+
res.setHeader('Content-Length', result.meta?.size || result.content.length);
|
|
66
|
+
res.setHeader('X-Content-Hash', result.hash);
|
|
67
|
+
res.setHeader('X-Content-Status', result.meta?.status || 'unknown');
|
|
68
|
+
|
|
69
|
+
// Cache headers (immutable content = cache forever)
|
|
70
|
+
if (result.verified) {
|
|
71
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
72
|
+
} else {
|
|
73
|
+
res.setHeader('Cache-Control', 'public, max-age=60');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Include proof in headers if requested
|
|
77
|
+
if (includeProof && result.proof) {
|
|
78
|
+
res.setHeader('X-Consensus-Proof', JSON.stringify(result.proof));
|
|
79
|
+
res.setHeader('X-Verified', result.verified ? 'true' : 'false');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Download disposition
|
|
83
|
+
if (download && result.meta?.name) {
|
|
84
|
+
res.setHeader('Content-Disposition', `attachment; filename="${result.meta.name}"`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Send content
|
|
88
|
+
res.send(result.content);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* GET /content/:hash/meta
|
|
93
|
+
* Fetch metadata only (no content body)
|
|
94
|
+
*/
|
|
95
|
+
router.get('/:hash/meta', readLimiter, (req, res) => {
|
|
96
|
+
const { hash } = req.params;
|
|
97
|
+
const meta = contentStore.getMeta(hash);
|
|
98
|
+
|
|
99
|
+
if (!meta) {
|
|
100
|
+
return res.status(404).json({ error: 'Content not found', hash });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
res.json(meta.toJSON ? meta.toJSON() : meta);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* GET /content/:hash/proof
|
|
108
|
+
* Fetch consensus proof for light client verification
|
|
109
|
+
*/
|
|
110
|
+
router.get('/:hash/proof', readLimiter, (req, res) => {
|
|
111
|
+
const { hash } = req.params;
|
|
112
|
+
const meta = contentStore.getMeta(hash);
|
|
113
|
+
|
|
114
|
+
if (!meta) {
|
|
115
|
+
return res.status(404).json({ error: 'Content not found', hash });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!meta.consensusProof) {
|
|
119
|
+
return res.status(404).json({
|
|
120
|
+
error: 'No consensus proof yet',
|
|
121
|
+
hash,
|
|
122
|
+
status: meta.status,
|
|
123
|
+
hint: 'Content may still be pending consensus.',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
res.json({
|
|
128
|
+
hash,
|
|
129
|
+
verified: meta.status === ContentStatus.VERIFIED,
|
|
130
|
+
proof: meta.consensusProof.toJSON ? meta.consensusProof.toJSON() : meta.consensusProof,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* GET /content/list
|
|
136
|
+
* List available content
|
|
137
|
+
*
|
|
138
|
+
* Query params:
|
|
139
|
+
* - tag=<tag> : Filter by tag
|
|
140
|
+
* - status=<status> : Filter by status (local, pending, verified)
|
|
141
|
+
* - limit=<n> : Max results (default 100)
|
|
142
|
+
* - offset=<n> : Pagination offset
|
|
143
|
+
*/
|
|
144
|
+
router.get('/', readLimiter, (req, res) => {
|
|
145
|
+
const { tag, status, limit = 100, offset = 0 } = req.query;
|
|
146
|
+
|
|
147
|
+
const items = contentStore.list({
|
|
148
|
+
tag,
|
|
149
|
+
status,
|
|
150
|
+
limit: Math.min(parseInt(limit) || 100, 1000),
|
|
151
|
+
offset: parseInt(offset) || 0,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
res.json({
|
|
155
|
+
items,
|
|
156
|
+
count: items.length,
|
|
157
|
+
stats: contentStore.getStats(),
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* HEAD /content/:hash
|
|
163
|
+
* Check if content exists (useful for CDN/cache validation)
|
|
164
|
+
*/
|
|
165
|
+
router.head('/:hash', readLimiter, (req, res) => {
|
|
166
|
+
const { hash } = req.params;
|
|
167
|
+
|
|
168
|
+
if (contentStore.has(hash)) {
|
|
169
|
+
const meta = contentStore.getMeta(hash);
|
|
170
|
+
res.setHeader('Content-Type', meta?.contentType || 'application/octet-stream');
|
|
171
|
+
res.setHeader('Content-Length', meta?.size || 0);
|
|
172
|
+
res.setHeader('X-Content-Hash', hash);
|
|
173
|
+
res.setHeader('X-Content-Status', meta?.status || 'unknown');
|
|
174
|
+
res.status(200).end();
|
|
175
|
+
} else {
|
|
176
|
+
res.status(404).end();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// =========================================
|
|
181
|
+
// AUTHENTICATED WRITE ENDPOINTS
|
|
182
|
+
// =========================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* POST /content/publish
|
|
186
|
+
* Publish new content to the mesh
|
|
187
|
+
*
|
|
188
|
+
* Body (JSON):
|
|
189
|
+
* {
|
|
190
|
+
* content: <string|object>,
|
|
191
|
+
* contentType?: <mime-type>,
|
|
192
|
+
* name?: <human-readable-name>,
|
|
193
|
+
* tags?: [<tag>, ...],
|
|
194
|
+
* ttl?: <seconds>
|
|
195
|
+
* }
|
|
196
|
+
*
|
|
197
|
+
* Body (multipart/form-data):
|
|
198
|
+
* - file: uploaded file
|
|
199
|
+
* - name: optional name
|
|
200
|
+
* - tags: comma-separated tags
|
|
201
|
+
*/
|
|
202
|
+
router.post('/publish', writeLimiter, async (req, res) => {
|
|
203
|
+
try {
|
|
204
|
+
let content;
|
|
205
|
+
let options = {};
|
|
206
|
+
|
|
207
|
+
// Handle JSON body
|
|
208
|
+
if (req.is('application/json')) {
|
|
209
|
+
if (!req.body.content) {
|
|
210
|
+
return res.status(400).json({ error: 'Content required' });
|
|
211
|
+
}
|
|
212
|
+
content = req.body.content;
|
|
213
|
+
options = {
|
|
214
|
+
contentType: req.body.contentType,
|
|
215
|
+
name: req.body.name,
|
|
216
|
+
tags: req.body.tags || [],
|
|
217
|
+
ttl: req.body.ttl || 0,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Handle raw body
|
|
221
|
+
else if (req.body && Buffer.isBuffer(req.body)) {
|
|
222
|
+
content = req.body;
|
|
223
|
+
options = {
|
|
224
|
+
contentType: req.headers['content-type'] || 'application/octet-stream',
|
|
225
|
+
name: req.headers['x-content-name'],
|
|
226
|
+
tags: req.headers['x-content-tags']?.split(',') || [],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Handle form data (basic - for full multipart use multer)
|
|
230
|
+
else if (req.body?.content) {
|
|
231
|
+
content = req.body.content;
|
|
232
|
+
options = {
|
|
233
|
+
contentType: req.body.contentType,
|
|
234
|
+
name: req.body.name,
|
|
235
|
+
tags: typeof req.body.tags === 'string' ? req.body.tags.split(',') : req.body.tags,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
return res.status(400).json({ error: 'Content required in body' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Store and publish
|
|
243
|
+
const result = await contentStore.store(content, options);
|
|
244
|
+
|
|
245
|
+
res.status(201).json({
|
|
246
|
+
success: true,
|
|
247
|
+
hash: result.hash,
|
|
248
|
+
status: result.status,
|
|
249
|
+
meta: result.meta?.toJSON ? result.meta.toJSON() : result.meta,
|
|
250
|
+
url: `/content/${result.hash}`,
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
res.status(500).json({ error: error.message });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* POST /content/request
|
|
259
|
+
* Request content from mesh (if not locally available)
|
|
260
|
+
*/
|
|
261
|
+
router.post('/request', writeLimiter, async (req, res) => {
|
|
262
|
+
const { hash } = req.body;
|
|
263
|
+
|
|
264
|
+
if (!hash) {
|
|
265
|
+
return res.status(400).json({ error: 'Hash required' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Check local first
|
|
270
|
+
if (contentStore.has(hash)) {
|
|
271
|
+
return res.json({
|
|
272
|
+
found: true,
|
|
273
|
+
local: true,
|
|
274
|
+
hash,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Request from mesh
|
|
279
|
+
const result = await contentStore.request(hash);
|
|
280
|
+
|
|
281
|
+
res.json({
|
|
282
|
+
found: true,
|
|
283
|
+
local: false,
|
|
284
|
+
hash: result.hash,
|
|
285
|
+
meta: result.meta,
|
|
286
|
+
});
|
|
287
|
+
} catch (error) {
|
|
288
|
+
res.status(404).json({
|
|
289
|
+
found: false,
|
|
290
|
+
hash,
|
|
291
|
+
error: error.message,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* DELETE /content/:hash
|
|
298
|
+
* Remove content (local only - cannot remove from mesh)
|
|
299
|
+
*/
|
|
300
|
+
router.delete('/:hash', writeLimiter, (req, res) => {
|
|
301
|
+
const { hash } = req.params;
|
|
302
|
+
|
|
303
|
+
if (!contentStore.has(hash)) {
|
|
304
|
+
return res.status(404).json({ error: 'Content not found', hash });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Note: This only removes locally - content may still exist on other nodes
|
|
308
|
+
contentStore.delete(hash);
|
|
309
|
+
|
|
310
|
+
res.json({
|
|
311
|
+
deleted: true,
|
|
312
|
+
hash,
|
|
313
|
+
note: 'Content removed locally. Other mesh nodes may still have copies.',
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* GET /content/stats
|
|
319
|
+
* Get content store statistics
|
|
320
|
+
*/
|
|
321
|
+
router.get('/stats', readLimiter, (req, res) => {
|
|
322
|
+
res.json(contentStore.getStats());
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* POST /content/verify
|
|
327
|
+
* Compute hash for content without storing
|
|
328
|
+
* Useful for clients to verify content integrity
|
|
329
|
+
*/
|
|
330
|
+
router.post('/verify', readLimiter, (req, res) => {
|
|
331
|
+
const content = req.body.content || req.body;
|
|
332
|
+
|
|
333
|
+
if (!content) {
|
|
334
|
+
return res.status(400).json({ error: 'Content required' });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const hash = computeContentHash(content);
|
|
338
|
+
|
|
339
|
+
res.json({
|
|
340
|
+
hash,
|
|
341
|
+
exists: contentStore.has(hash),
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return router;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export default createContentAPI;
|
package/content/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAKMESHβ’ Content Module
|
|
3
|
+
* Content-addressed storage with public delivery
|
|
4
|
+
*
|
|
5
|
+
* @module content
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @copyright 2026 YAKMESH Contributors
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
ContentStore,
|
|
12
|
+
ContentType,
|
|
13
|
+
ContentStatus,
|
|
14
|
+
ContentMetadata,
|
|
15
|
+
ConsensusProof,
|
|
16
|
+
computeContentHash,
|
|
17
|
+
} from './store.js';
|
|
18
|
+
|
|
19
|
+
export { createContentAPI } from './api.js';
|
package/content/store.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
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
|
+
this.gossip.spreadRumor('content', announcement);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Update status
|
|
453
|
+
meta.status = ContentStatus.PENDING;
|
|
454
|
+
writeFileSync(this._getMetaPath(hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
455
|
+
|
|
456
|
+
return { published: true, hash };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Request content from mesh
|
|
461
|
+
*/
|
|
462
|
+
async request(hash) {
|
|
463
|
+
if (this.has(hash)) {
|
|
464
|
+
return this.getWithProof(hash);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Broadcast request
|
|
468
|
+
if (this.gossip) {
|
|
469
|
+
this.gossip.spreadRumor('content', {
|
|
470
|
+
type: 'content_request',
|
|
471
|
+
hash,
|
|
472
|
+
requestedBy: this.identity?.identity?.nodeId,
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Wait for response (with timeout)
|
|
478
|
+
return new Promise((resolve, reject) => {
|
|
479
|
+
const timeout = setTimeout(() => {
|
|
480
|
+
reject(new Error(`Content not found: ${hash}`));
|
|
481
|
+
}, 10000);
|
|
482
|
+
|
|
483
|
+
const checkInterval = setInterval(() => {
|
|
484
|
+
if (this.has(hash)) {
|
|
485
|
+
clearTimeout(timeout);
|
|
486
|
+
clearInterval(checkInterval);
|
|
487
|
+
resolve(this.getWithProof(hash));
|
|
488
|
+
}
|
|
489
|
+
}, 500);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Handle content gossip from peers
|
|
495
|
+
*/
|
|
496
|
+
async _handleContentGossip(data, origin) {
|
|
497
|
+
switch (data.type) {
|
|
498
|
+
case 'content_announce':
|
|
499
|
+
// Peer has new content - request it if we don't have it
|
|
500
|
+
if (!this.has(data.hash)) {
|
|
501
|
+
console.log(`π¦ New content announced: ${data.hash.slice(0, 16)}... from ${origin.slice(0, 16)}...`);
|
|
502
|
+
// Request full content via mesh
|
|
503
|
+
if (this.mesh) {
|
|
504
|
+
this.mesh.sendTo(origin, {
|
|
505
|
+
type: 'content_request',
|
|
506
|
+
hash: data.hash,
|
|
507
|
+
requestedBy: this.identity?.identity?.nodeId,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case 'content_request':
|
|
514
|
+
// Peer wants content - send if we have it
|
|
515
|
+
if (this.has(data.hash)) {
|
|
516
|
+
const result = this.getWithProof(data.hash);
|
|
517
|
+
if (this.mesh && result) {
|
|
518
|
+
this.mesh.sendTo(origin, {
|
|
519
|
+
type: 'content_response',
|
|
520
|
+
hash: data.hash,
|
|
521
|
+
content: result.content.toString('base64'),
|
|
522
|
+
meta: result.meta,
|
|
523
|
+
proof: result.proof,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'content_response':
|
|
530
|
+
// Received content from peer
|
|
531
|
+
if (!this.has(data.hash)) {
|
|
532
|
+
const content = Buffer.from(data.content, 'base64');
|
|
533
|
+
const computedHash = computeContentHash(content);
|
|
534
|
+
|
|
535
|
+
// Verify hash
|
|
536
|
+
if (computedHash !== data.hash) {
|
|
537
|
+
console.warn(`β οΈ Content hash mismatch from ${origin.slice(0, 16)}...`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Store it
|
|
542
|
+
await this.store(content, {
|
|
543
|
+
...data.meta,
|
|
544
|
+
publish: false, // Don't re-gossip
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Apply consensus proof if present
|
|
548
|
+
if (data.proof) {
|
|
549
|
+
const meta = this.getMeta(data.hash);
|
|
550
|
+
meta.consensusProof = ConsensusProof.fromJSON(data.proof);
|
|
551
|
+
meta.status = data.proof.hasQuorum?.() ? ContentStatus.VERIFIED : ContentStatus.PENDING;
|
|
552
|
+
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(`β Content received: ${data.hash.slice(0, 16)}...`);
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
|
|
559
|
+
case 'content_validate':
|
|
560
|
+
// Peer is requesting validation vote
|
|
561
|
+
if (this.has(data.hash) && this.identity && this.oracle) {
|
|
562
|
+
const content = this.get(data.hash);
|
|
563
|
+
const isValid = this.oracle.validateContent(content, data.contentType);
|
|
564
|
+
|
|
565
|
+
if (isValid) {
|
|
566
|
+
// Sign validation
|
|
567
|
+
const vote = {
|
|
568
|
+
type: 'content_vote',
|
|
569
|
+
hash: data.hash,
|
|
570
|
+
nodeId: this.identity.identity.nodeId,
|
|
571
|
+
vote: 'valid',
|
|
572
|
+
signature: this.identity.sign(data.hash),
|
|
573
|
+
timestamp: Date.now(),
|
|
574
|
+
};
|
|
575
|
+
this.gossip.spreadRumor('content', vote);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
case 'content_vote':
|
|
581
|
+
// Received validation vote
|
|
582
|
+
const meta = this.getMeta(data.hash);
|
|
583
|
+
if (meta) {
|
|
584
|
+
if (!meta.consensusProof) {
|
|
585
|
+
meta.consensusProof = new ConsensusProof({
|
|
586
|
+
contentHash: data.hash,
|
|
587
|
+
quorum: this.config.quorumSize,
|
|
588
|
+
networkId: this.mesh?.networkId,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
meta.consensusProof.addValidator(data.nodeId, data.signature);
|
|
592
|
+
|
|
593
|
+
if (meta.consensusProof.hasQuorum()) {
|
|
594
|
+
meta.status = ContentStatus.VERIFIED;
|
|
595
|
+
console.log(`β Content verified (quorum reached): ${data.hash.slice(0, 16)}...`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
599
|
+
}
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Add to LRU content cache
|
|
606
|
+
*/
|
|
607
|
+
_addToContentCache(hash, content) {
|
|
608
|
+
// Simple LRU: remove oldest if at capacity
|
|
609
|
+
if (this.contentCache.size >= this.config.cacheSize) {
|
|
610
|
+
const oldest = this.contentCache.keys().next().value;
|
|
611
|
+
this.contentCache.delete(oldest);
|
|
612
|
+
}
|
|
613
|
+
this.contentCache.set(hash, content);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Detect content type from content
|
|
618
|
+
*/
|
|
619
|
+
_detectContentType(content) {
|
|
620
|
+
if (typeof content === 'object' && !Buffer.isBuffer(content)) {
|
|
621
|
+
return ContentType.JSON;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const str = content.toString().slice(0, 100);
|
|
625
|
+
|
|
626
|
+
if (str.startsWith('<!DOCTYPE') || str.startsWith('<html')) {
|
|
627
|
+
return ContentType.HTML;
|
|
628
|
+
}
|
|
629
|
+
if (str.startsWith('{') || str.startsWith('[')) {
|
|
630
|
+
return ContentType.JSON;
|
|
631
|
+
}
|
|
632
|
+
if (str.includes('function') || str.includes('const ') || str.includes('import ')) {
|
|
633
|
+
return ContentType.JAVASCRIPT;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return ContentType.TEXT;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get store statistics
|
|
641
|
+
*/
|
|
642
|
+
getStats() {
|
|
643
|
+
let totalSize = 0;
|
|
644
|
+
let verified = 0;
|
|
645
|
+
let pending = 0;
|
|
646
|
+
let local = 0;
|
|
647
|
+
|
|
648
|
+
for (const meta of this.metaCache.values()) {
|
|
649
|
+
totalSize += meta.size;
|
|
650
|
+
switch (meta.status) {
|
|
651
|
+
case ContentStatus.VERIFIED: verified++; break;
|
|
652
|
+
case ContentStatus.PENDING: pending++; break;
|
|
653
|
+
case ContentStatus.LOCAL: local++; break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
totalObjects: this.metaCache.size,
|
|
659
|
+
totalSize,
|
|
660
|
+
verified,
|
|
661
|
+
pending,
|
|
662
|
+
local,
|
|
663
|
+
cacheSize: this.contentCache.size,
|
|
664
|
+
dataDir: this.config.dataDir,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export { ContentMetadata, ConsensusProof };
|
|
670
|
+
export default ContentStore;
|
package/discord.md
CHANGED
|
@@ -52,12 +52,14 @@ const { slices } = encoder.encode('Hello mesh!');
|
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
## π¦ Current Version: `1.2
|
|
55
|
+
## π¦ Current Version: `1.3.2`
|
|
56
56
|
|
|
57
|
-
β
TME (Temporal Matrix Encoding)
|
|
57
|
+
β
TMEβ’ (Temporal Matrix Encoding)
|
|
58
58
|
β
ML-DSA-65 Post-Quantum Signatures
|
|
59
|
-
β
|
|
60
|
-
β
|
|
59
|
+
β
Code Proof Protocol (codebase verification)
|
|
60
|
+
β
Public Content Delivery API (v1.3.2 - gossip integration)
|
|
61
|
+
β
ECHOβ’, PULSEβ’, PHANTOMβ’, BEACONβ’ protocols
|
|
62
|
+
β
68+ tests passing
|
|
61
63
|
|
|
62
64
|
---
|
|
63
65
|
|
|
@@ -65,6 +67,9 @@ const { slices } = encoder.encode('Hello mesh!');
|
|
|
65
67
|
π Website: https://yakmesh.dev
|
|
66
68
|
π¦ npm: https://npmjs.com/package/yakmesh
|
|
67
69
|
π GitHub: https://github.com/yakmesh/yakmesh
|
|
70
|
+
π¬ Discord: https://discord.gg/E62tAE2wGh
|
|
71
|
+
π± Telegram: https://t.me/yakmesh
|
|
72
|
+
π Twitter: https://x.com/yakmesh
|
|
68
73
|
π Whitepaper: `docs/WHITEPAPER.md`
|
|
69
74
|
|
|
70
75
|
**USPTO Serial No. 99594620**
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -19,6 +19,9 @@ import { MeshNetwork } from '../mesh/network.js';
|
|
|
19
19
|
import { ReplicationEngine } from '../database/replication.js';
|
|
20
20
|
import { GossipProtocol } from '../gossip/protocol.js';
|
|
21
21
|
|
|
22
|
+
// Content store for public delivery
|
|
23
|
+
import { ContentStore, createContentAPI } from '../content/index.js';
|
|
24
|
+
|
|
22
25
|
// Oracle system imports
|
|
23
26
|
import {
|
|
24
27
|
getOracle,
|
|
@@ -115,6 +118,9 @@ export class YakmeshNode {
|
|
|
115
118
|
this.codeProof = null;
|
|
116
119
|
this.consensus = null;
|
|
117
120
|
|
|
121
|
+
// Content store for public delivery
|
|
122
|
+
this.contentStore = null;
|
|
123
|
+
|
|
118
124
|
// Time source detector
|
|
119
125
|
this.timeSource = null;
|
|
120
126
|
|
|
@@ -206,12 +212,26 @@ export class YakmeshNode {
|
|
|
206
212
|
if (topic === 'network_handshake') {
|
|
207
213
|
this._handleNetworkHandshake(data, origin);
|
|
208
214
|
}
|
|
215
|
+
|
|
216
|
+
// Handle content gossip (for public content delivery)
|
|
217
|
+
if (topic === 'content') {
|
|
218
|
+
if (this.contentStore) {
|
|
219
|
+
this.contentStore._handleContentGossip(data, origin);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
209
222
|
});
|
|
210
223
|
|
|
211
|
-
// 5.
|
|
224
|
+
// 5. Initialize content store for public delivery
|
|
225
|
+
this.contentStore = new ContentStore({
|
|
226
|
+
dataDir: this.config.database?.contentPath || './data/content',
|
|
227
|
+
quorumSize: 2,
|
|
228
|
+
});
|
|
229
|
+
await this.contentStore.init(this);
|
|
230
|
+
|
|
231
|
+
// 6. Start HTTP server
|
|
212
232
|
await this._startHttpServer();
|
|
213
233
|
|
|
214
|
-
//
|
|
234
|
+
// 7. Connect to bootstrap nodes
|
|
215
235
|
await this._connectToBootstrap();
|
|
216
236
|
|
|
217
237
|
// 7. Initialize PeerQuanta integration (if enabled)
|
|
@@ -222,13 +242,18 @@ export class YakmeshNode {
|
|
|
222
242
|
console.log('\nβ Yakmesh Node is running!\n');
|
|
223
243
|
console.log(` Node ID: ${this.identity.identity.nodeId}`);
|
|
224
244
|
console.log(` HTTP: http://localhost:${this.config.network.httpPort}`);
|
|
245
|
+
console.log(` Content: http://localhost:${this.config.network.httpPort}/content`);
|
|
225
246
|
console.log(` Dashboard: http://localhost:${this.config.network.httpPort}/dashboard`);
|
|
226
247
|
console.log(` WebSocket: ws://localhost:${this.config.network.wsPort}`);
|
|
227
248
|
console.log(` Algorithm: ML-DSA-65 (Post-Quantum)`);
|
|
228
249
|
console.log(` Oracle: β ${this.oracle.selfHash.slice(0, 16)}...`);
|
|
229
250
|
console.log(` Network: ${this.genesisNetwork.networkName} (${this.genesisNetwork.networkId})`);
|
|
251
|
+
if (this.contentStore) {
|
|
252
|
+
const stats = this.contentStore.getStats();
|
|
253
|
+
console.log(` Content: ${stats.totalObjects} objects (${stats.verified} verified)`);
|
|
254
|
+
}
|
|
230
255
|
if (this.adapter) {
|
|
231
|
-
console.log(` Adapter:
|
|
256
|
+
console.log(` Adapter: β Enabled`);
|
|
232
257
|
}
|
|
233
258
|
console.log('');
|
|
234
259
|
|
|
@@ -491,6 +516,18 @@ export class YakmeshNode {
|
|
|
491
516
|
return obj && typeof obj === 'object' && !Array.isArray(obj);
|
|
492
517
|
};
|
|
493
518
|
|
|
519
|
+
// =========================================
|
|
520
|
+
// PUBLIC CONTENT API (No Auth for reads)
|
|
521
|
+
// =========================================
|
|
522
|
+
|
|
523
|
+
// Mount content API at /content
|
|
524
|
+
const contentAPI = createContentAPI(this.contentStore, {
|
|
525
|
+
writeLimiter,
|
|
526
|
+
readLimiter: generalLimiter,
|
|
527
|
+
validateString,
|
|
528
|
+
});
|
|
529
|
+
app.use('/content', contentAPI);
|
|
530
|
+
|
|
494
531
|
// Serve dashboard
|
|
495
532
|
app.get('/dashboard', (req, res) => {
|
|
496
533
|
res.sendFile('dashboard/index.html', { root: import.meta.dirname + '/..' });
|