yakmesh 1.7.1 β 1.8.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 +55 -1
- package/mesh/sherpa-discovery.js +655 -0
- package/package.json +4 -2
- package/server/index.js +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to YAKMESH will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [1.
|
|
5
|
+
## [1.8.0] - 2026-01-18
|
|
6
|
+
|
|
7
|
+
### ποΈ SHERPA: Decentralized Peer Discovery
|
|
8
|
+
|
|
9
|
+
This release implements SHERPA, a novel peer discovery mechanism that uses the public web as a decentralized DHT.
|
|
10
|
+
|
|
11
|
+
#### New Feature: SHERPA Discovery
|
|
12
|
+
|
|
13
|
+
##### The Innovation: "The Web IS the DHT"
|
|
14
|
+
- Each node exposes `/.well-known/yakmesh/beacon` with its peer list
|
|
15
|
+
- Discovery crawls known endpoints to find new peers
|
|
16
|
+
- No central authority - truly decentralized bootstrap
|
|
17
|
+
- Works with existing CDN infrastructure
|
|
18
|
+
|
|
19
|
+
##### New Module: `mesh/sherpa-discovery.js`
|
|
20
|
+
- `SherpaDiscovery` - Main discovery engine with peer crawling
|
|
21
|
+
- `BeaconMessage` - Signed beacon format for peer advertisement
|
|
22
|
+
- `PeerRegistry` - Scored peer management with decay
|
|
23
|
+
- `createBeaconMiddleware` - Express middleware for beacon endpoint
|
|
24
|
+
|
|
25
|
+
##### New Endpoints
|
|
26
|
+
- `GET /.well-known/yakmesh/beacon` - Advertise this node and known peers
|
|
27
|
+
- `GET /sherpa/status` - Discovery statistics
|
|
28
|
+
- `GET /sherpa/candidates` - Get connection candidates
|
|
29
|
+
|
|
30
|
+
##### Configuration
|
|
31
|
+
```javascript
|
|
32
|
+
// yakmesh.config.js
|
|
33
|
+
export default {
|
|
34
|
+
sherpa: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
selfEndpoint: 'https://mynode.example.com',
|
|
37
|
+
wsEndpoint: 'wss://mynode.example.com:9001',
|
|
38
|
+
seeds: ['https://peer1.example.com', 'https://peer2.example.com'],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
##### Beacon Response Format
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"version": "1.0",
|
|
47
|
+
"nodeId": "abc123...",
|
|
48
|
+
"networkName": "mobius-rabi-junction",
|
|
49
|
+
"timestamp": 1737225600000,
|
|
50
|
+
"capabilities": { "wsPort": 9001, "supportsAnnex": true },
|
|
51
|
+
"peers": [{ "nodeId": "...", "endpoint": "https://..." }],
|
|
52
|
+
"publicKey": "...",
|
|
53
|
+
"signature": "..."
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## [1.7.1] - 2026-01-18
|
|
6
60
|
|
|
7
61
|
### 𦬠NAKPAK & SHERPA: Yak-Themed Protocol Naming
|
|
8
62
|
|
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHERPA - Secure Hidden Endpoint Resolution Path Architecture
|
|
3
|
+
*
|
|
4
|
+
* A novel peer discovery mechanism using public web endpoints as a decentralized DHT.
|
|
5
|
+
* Instead of centralized bootstrap nodes, SHERPA leverages the public-facing portion
|
|
6
|
+
* of yakmesh nodes (Caddy/Abyss web servers) to create a self-organizing peer registry.
|
|
7
|
+
*
|
|
8
|
+
* Key Innovation: "The web IS the DHT"
|
|
9
|
+
* - Each node exposes /.well-known/yakmesh/beacon with its peer list
|
|
10
|
+
* - Discovery crawls known endpoints to find new peers
|
|
11
|
+
* - No central authority - truly decentralized bootstrap
|
|
12
|
+
* - Works with existing CDN infrastructure
|
|
13
|
+
*
|
|
14
|
+
* Etymology: Sherpas guide travelers through hidden mountain paths,
|
|
15
|
+
* just like SHERPA guides nodes to discover each other.
|
|
16
|
+
*
|
|
17
|
+
* @module mesh/sherpa-discovery
|
|
18
|
+
* @license MIT
|
|
19
|
+
* @copyright 2026 YAKMESHβ’ Contributors
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
23
|
+
import { bytesToHex, randomBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
24
|
+
import { EventEmitter } from 'events';
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// SHERPA CONFIGURATION
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
const SHERPA_CONFIG = {
|
|
31
|
+
// Beacon endpoint
|
|
32
|
+
beaconPath: '/.well-known/yakmesh/beacon',
|
|
33
|
+
|
|
34
|
+
// Discovery settings
|
|
35
|
+
maxPeersPerBeacon: 50, // Max peers to advertise in beacon
|
|
36
|
+
maxPeersToReturn: 20, // Max peers to return per request
|
|
37
|
+
maxCrawlDepth: 3, // How many hops to crawl
|
|
38
|
+
crawlTimeout: 5000, // Timeout for each beacon fetch (ms)
|
|
39
|
+
crawlInterval: 300000, // Re-crawl interval (5 minutes)
|
|
40
|
+
|
|
41
|
+
// Peer scoring
|
|
42
|
+
minPeerScore: 0.1, // Minimum score to keep peer
|
|
43
|
+
scoreDecay: 0.95, // Score decay per interval
|
|
44
|
+
successBonus: 0.2, // Score bonus for successful contact
|
|
45
|
+
failurePenalty: 0.3, // Score penalty for failed contact
|
|
46
|
+
|
|
47
|
+
// Security
|
|
48
|
+
maxBeaconSize: 65536, // Max beacon response size (64KB)
|
|
49
|
+
signatureRequired: true, // Require signed beacons
|
|
50
|
+
|
|
51
|
+
// Rate limiting
|
|
52
|
+
maxCrawlsPerMinute: 10, // Prevent crawl storms
|
|
53
|
+
|
|
54
|
+
// Version
|
|
55
|
+
protocolVersion: '1.0',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ============================================================
|
|
59
|
+
// BEACON MESSAGE FORMAT
|
|
60
|
+
// ============================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Beacon message format for /.well-known/yakmesh/beacon
|
|
64
|
+
* This is what nodes advertise to help others discover peers.
|
|
65
|
+
*/
|
|
66
|
+
class BeaconMessage {
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
this.version = SHERPA_CONFIG.protocolVersion;
|
|
69
|
+
this.nodeId = options.nodeId;
|
|
70
|
+
this.networkName = options.networkName;
|
|
71
|
+
this.timestamp = options.timestamp || Date.now();
|
|
72
|
+
this.ttl = options.ttl || 3600; // 1 hour default TTL
|
|
73
|
+
|
|
74
|
+
// Node capabilities
|
|
75
|
+
this.capabilities = {
|
|
76
|
+
wsPort: options.wsPort || null,
|
|
77
|
+
httpPort: options.httpPort || null,
|
|
78
|
+
supportsAnnex: options.supportsAnnex ?? true,
|
|
79
|
+
supportsNakpak: options.supportsNakpak ?? true,
|
|
80
|
+
supportsGossip: options.supportsGossip ?? true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Known peers (other nodes we know about)
|
|
84
|
+
this.peers = options.peers || [];
|
|
85
|
+
|
|
86
|
+
// Cryptographic proof
|
|
87
|
+
this.publicKey = options.publicKey || null;
|
|
88
|
+
this.signature = options.signature || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add a peer to the beacon
|
|
93
|
+
*/
|
|
94
|
+
addPeer(peerInfo) {
|
|
95
|
+
if (this.peers.length >= SHERPA_CONFIG.maxPeersPerBeacon) {
|
|
96
|
+
// Remove lowest-scored peer
|
|
97
|
+
this.peers.sort((a, b) => b.score - a.score);
|
|
98
|
+
this.peers.pop();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.peers.push({
|
|
102
|
+
nodeId: peerInfo.nodeId,
|
|
103
|
+
endpoint: peerInfo.endpoint, // e.g., "https://example.com"
|
|
104
|
+
wsEndpoint: peerInfo.wsEndpoint, // e.g., "wss://example.com:9001"
|
|
105
|
+
lastSeen: peerInfo.lastSeen || Date.now(),
|
|
106
|
+
score: peerInfo.score || 1.0,
|
|
107
|
+
networkName: peerInfo.networkName,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get peers for discovery response (limited subset)
|
|
113
|
+
*/
|
|
114
|
+
getPeersForDiscovery() {
|
|
115
|
+
// Sort by score, return top N
|
|
116
|
+
return [...this.peers]
|
|
117
|
+
.sort((a, b) => b.score - a.score)
|
|
118
|
+
.slice(0, SHERPA_CONFIG.maxPeersToReturn)
|
|
119
|
+
.map(p => ({
|
|
120
|
+
nodeId: p.nodeId,
|
|
121
|
+
endpoint: p.endpoint,
|
|
122
|
+
wsEndpoint: p.wsEndpoint,
|
|
123
|
+
lastSeen: p.lastSeen,
|
|
124
|
+
networkName: p.networkName,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Serialize beacon for HTTP response
|
|
130
|
+
*/
|
|
131
|
+
serialize() {
|
|
132
|
+
return {
|
|
133
|
+
version: this.version,
|
|
134
|
+
nodeId: this.nodeId,
|
|
135
|
+
networkName: this.networkName,
|
|
136
|
+
timestamp: this.timestamp,
|
|
137
|
+
ttl: this.ttl,
|
|
138
|
+
capabilities: this.capabilities,
|
|
139
|
+
peers: this.getPeersForDiscovery(),
|
|
140
|
+
publicKey: this.publicKey,
|
|
141
|
+
signature: this.signature,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create data to sign
|
|
147
|
+
*/
|
|
148
|
+
getSignableData() {
|
|
149
|
+
return JSON.stringify({
|
|
150
|
+
version: this.version,
|
|
151
|
+
nodeId: this.nodeId,
|
|
152
|
+
networkName: this.networkName,
|
|
153
|
+
timestamp: this.timestamp,
|
|
154
|
+
capabilities: this.capabilities,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Deserialize beacon from HTTP response
|
|
160
|
+
*/
|
|
161
|
+
static deserialize(data) {
|
|
162
|
+
const beacon = new BeaconMessage({
|
|
163
|
+
nodeId: data.nodeId,
|
|
164
|
+
networkName: data.networkName,
|
|
165
|
+
timestamp: data.timestamp,
|
|
166
|
+
ttl: data.ttl,
|
|
167
|
+
wsPort: data.capabilities?.wsPort,
|
|
168
|
+
httpPort: data.capabilities?.httpPort,
|
|
169
|
+
supportsAnnex: data.capabilities?.supportsAnnex,
|
|
170
|
+
supportsNakpak: data.capabilities?.supportsNakpak,
|
|
171
|
+
supportsGossip: data.capabilities?.supportsGossip,
|
|
172
|
+
publicKey: data.publicKey,
|
|
173
|
+
signature: data.signature,
|
|
174
|
+
});
|
|
175
|
+
beacon.version = data.version;
|
|
176
|
+
beacon.peers = data.peers || [];
|
|
177
|
+
return beacon;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================
|
|
182
|
+
// PEER REGISTRY
|
|
183
|
+
// ============================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Maintains a registry of known peers with scoring
|
|
187
|
+
*/
|
|
188
|
+
class PeerRegistry {
|
|
189
|
+
constructor(options = {}) {
|
|
190
|
+
this.peers = new Map(); // nodeId -> PeerInfo
|
|
191
|
+
this.networkFilter = options.networkFilter || null; // Only accept peers from this network
|
|
192
|
+
this.maxPeers = options.maxPeers || 1000;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Add or update a peer
|
|
197
|
+
*/
|
|
198
|
+
upsert(peerInfo) {
|
|
199
|
+
const existing = this.peers.get(peerInfo.nodeId);
|
|
200
|
+
|
|
201
|
+
// Filter by network if configured
|
|
202
|
+
if (this.networkFilter && peerInfo.networkName !== this.networkFilter) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (existing) {
|
|
207
|
+
// Update existing peer
|
|
208
|
+
existing.endpoint = peerInfo.endpoint || existing.endpoint;
|
|
209
|
+
existing.wsEndpoint = peerInfo.wsEndpoint || existing.wsEndpoint;
|
|
210
|
+
existing.lastSeen = Math.max(existing.lastSeen, peerInfo.lastSeen || Date.now());
|
|
211
|
+
existing.score = Math.min(1.0, existing.score + SHERPA_CONFIG.successBonus);
|
|
212
|
+
existing.capabilities = peerInfo.capabilities || existing.capabilities;
|
|
213
|
+
} else {
|
|
214
|
+
// Add new peer (evict lowest scored if at capacity)
|
|
215
|
+
if (this.peers.size >= this.maxPeers) {
|
|
216
|
+
this._evictLowest();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.peers.set(peerInfo.nodeId, {
|
|
220
|
+
nodeId: peerInfo.nodeId,
|
|
221
|
+
endpoint: peerInfo.endpoint,
|
|
222
|
+
wsEndpoint: peerInfo.wsEndpoint,
|
|
223
|
+
lastSeen: peerInfo.lastSeen || Date.now(),
|
|
224
|
+
score: peerInfo.score || 1.0,
|
|
225
|
+
networkName: peerInfo.networkName,
|
|
226
|
+
capabilities: peerInfo.capabilities || {},
|
|
227
|
+
discoveredAt: Date.now(),
|
|
228
|
+
failureCount: 0,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Mark a peer as failed (decrease score)
|
|
237
|
+
*/
|
|
238
|
+
markFailed(nodeId) {
|
|
239
|
+
const peer = this.peers.get(nodeId);
|
|
240
|
+
if (peer) {
|
|
241
|
+
peer.score = Math.max(0, peer.score - SHERPA_CONFIG.failurePenalty);
|
|
242
|
+
peer.failureCount++;
|
|
243
|
+
|
|
244
|
+
// Remove if score too low
|
|
245
|
+
if (peer.score < SHERPA_CONFIG.minPeerScore) {
|
|
246
|
+
this.peers.delete(nodeId);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get best peers for connection
|
|
255
|
+
*/
|
|
256
|
+
getBestPeers(count = 10) {
|
|
257
|
+
return [...this.peers.values()]
|
|
258
|
+
.filter(p => p.score >= SHERPA_CONFIG.minPeerScore)
|
|
259
|
+
.sort((a, b) => b.score - a.score)
|
|
260
|
+
.slice(0, count);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get peers for beacon advertisement
|
|
265
|
+
*/
|
|
266
|
+
getForBeacon() {
|
|
267
|
+
return [...this.peers.values()]
|
|
268
|
+
.filter(p => p.score >= SHERPA_CONFIG.minPeerScore && p.endpoint)
|
|
269
|
+
.sort((a, b) => b.score - a.score)
|
|
270
|
+
.slice(0, SHERPA_CONFIG.maxPeersPerBeacon);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Apply score decay to all peers
|
|
275
|
+
*/
|
|
276
|
+
decayScores() {
|
|
277
|
+
for (const peer of this.peers.values()) {
|
|
278
|
+
peer.score *= SHERPA_CONFIG.scoreDecay;
|
|
279
|
+
|
|
280
|
+
if (peer.score < SHERPA_CONFIG.minPeerScore) {
|
|
281
|
+
this.peers.delete(peer.nodeId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Evict lowest-scored peer
|
|
288
|
+
*/
|
|
289
|
+
_evictLowest() {
|
|
290
|
+
let lowest = null;
|
|
291
|
+
let lowestScore = Infinity;
|
|
292
|
+
|
|
293
|
+
for (const [nodeId, peer] of this.peers) {
|
|
294
|
+
if (peer.score < lowestScore) {
|
|
295
|
+
lowestScore = peer.score;
|
|
296
|
+
lowest = nodeId;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (lowest) {
|
|
301
|
+
this.peers.delete(lowest);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
size() {
|
|
306
|
+
return this.peers.size;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
has(nodeId) {
|
|
310
|
+
return this.peers.has(nodeId);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get(nodeId) {
|
|
314
|
+
return this.peers.get(nodeId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ============================================================
|
|
319
|
+
// SHERPA DISCOVERY ENGINE
|
|
320
|
+
// ============================================================
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Main SHERPA discovery engine
|
|
324
|
+
* Crawls beacon endpoints to discover peers in a decentralized manner
|
|
325
|
+
*/
|
|
326
|
+
class SherpaDiscovery extends EventEmitter {
|
|
327
|
+
constructor(options = {}) {
|
|
328
|
+
super();
|
|
329
|
+
|
|
330
|
+
this.nodeId = options.nodeId;
|
|
331
|
+
this.networkName = options.networkName;
|
|
332
|
+
this.publicKey = options.publicKey;
|
|
333
|
+
this.signFn = options.signFn; // Function to sign beacon data
|
|
334
|
+
this.verifyFn = options.verifyFn; // Function to verify signatures
|
|
335
|
+
|
|
336
|
+
// Our own endpoint info
|
|
337
|
+
this.selfEndpoint = options.selfEndpoint || null; // e.g., "https://mynode.com"
|
|
338
|
+
this.wsEndpoint = options.wsEndpoint || null;
|
|
339
|
+
this.capabilities = options.capabilities || {};
|
|
340
|
+
|
|
341
|
+
// Peer registry
|
|
342
|
+
this.registry = new PeerRegistry({
|
|
343
|
+
networkFilter: options.networkFilter || this.networkName,
|
|
344
|
+
maxPeers: options.maxPeers || 1000,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Seed endpoints (initial known beacons to crawl)
|
|
348
|
+
this.seedEndpoints = new Set(options.seedEndpoints || []);
|
|
349
|
+
|
|
350
|
+
// Crawl state
|
|
351
|
+
this.crawlInProgress = false;
|
|
352
|
+
this.lastCrawl = 0;
|
|
353
|
+
this.crawlTimer = null;
|
|
354
|
+
|
|
355
|
+
// Stats
|
|
356
|
+
this.stats = {
|
|
357
|
+
crawlsCompleted: 0,
|
|
358
|
+
beaconsFetched: 0,
|
|
359
|
+
beaconsFailed: 0,
|
|
360
|
+
peersDiscovered: 0,
|
|
361
|
+
peersEvicted: 0,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Start periodic discovery
|
|
367
|
+
*/
|
|
368
|
+
start() {
|
|
369
|
+
if (this.crawlTimer) return;
|
|
370
|
+
|
|
371
|
+
// Initial crawl
|
|
372
|
+
this.crawl();
|
|
373
|
+
|
|
374
|
+
// Periodic crawl
|
|
375
|
+
this.crawlTimer = setInterval(() => {
|
|
376
|
+
this.crawl();
|
|
377
|
+
this.registry.decayScores();
|
|
378
|
+
}, SHERPA_CONFIG.crawlInterval);
|
|
379
|
+
|
|
380
|
+
this.emit('started');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Stop discovery
|
|
385
|
+
*/
|
|
386
|
+
stop() {
|
|
387
|
+
if (this.crawlTimer) {
|
|
388
|
+
clearInterval(this.crawlTimer);
|
|
389
|
+
this.crawlTimer = null;
|
|
390
|
+
}
|
|
391
|
+
this.emit('stopped');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Add a seed endpoint for initial discovery
|
|
396
|
+
*/
|
|
397
|
+
addSeed(endpoint) {
|
|
398
|
+
this.seedEndpoints.add(endpoint);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Generate our beacon response
|
|
403
|
+
*/
|
|
404
|
+
generateBeacon() {
|
|
405
|
+
const beacon = new BeaconMessage({
|
|
406
|
+
nodeId: this.nodeId,
|
|
407
|
+
networkName: this.networkName,
|
|
408
|
+
wsPort: this.capabilities.wsPort,
|
|
409
|
+
httpPort: this.capabilities.httpPort,
|
|
410
|
+
supportsAnnex: this.capabilities.supportsAnnex ?? true,
|
|
411
|
+
supportsNakpak: this.capabilities.supportsNakpak ?? true,
|
|
412
|
+
supportsGossip: this.capabilities.supportsGossip ?? true,
|
|
413
|
+
publicKey: this.publicKey,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Add known peers
|
|
417
|
+
for (const peer of this.registry.getForBeacon()) {
|
|
418
|
+
beacon.addPeer(peer);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Sign beacon
|
|
422
|
+
if (this.signFn && this.publicKey) {
|
|
423
|
+
beacon.signature = this.signFn(beacon.getSignableData());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return beacon.serialize();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Crawl known endpoints to discover peers
|
|
431
|
+
*/
|
|
432
|
+
async crawl() {
|
|
433
|
+
if (this.crawlInProgress) return;
|
|
434
|
+
this.crawlInProgress = true;
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const visited = new Set();
|
|
438
|
+
const toVisit = new Set([
|
|
439
|
+
...this.seedEndpoints,
|
|
440
|
+
...this.registry.getForBeacon().map(p => p.endpoint).filter(Boolean),
|
|
441
|
+
]);
|
|
442
|
+
|
|
443
|
+
let depth = 0;
|
|
444
|
+
|
|
445
|
+
while (toVisit.size > 0 && depth < SHERPA_CONFIG.maxCrawlDepth) {
|
|
446
|
+
const batch = [...toVisit].slice(0, SHERPA_CONFIG.maxCrawlsPerMinute);
|
|
447
|
+
toVisit.clear();
|
|
448
|
+
|
|
449
|
+
const results = await Promise.allSettled(
|
|
450
|
+
batch
|
|
451
|
+
.filter(endpoint => endpoint && !visited.has(endpoint))
|
|
452
|
+
.map(endpoint => this._fetchBeacon(endpoint))
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < results.length; i++) {
|
|
456
|
+
const endpoint = batch[i];
|
|
457
|
+
visited.add(endpoint);
|
|
458
|
+
|
|
459
|
+
if (results[i].status === 'fulfilled') {
|
|
460
|
+
const beacon = results[i].value;
|
|
461
|
+
|
|
462
|
+
// Add the beacon source as a peer
|
|
463
|
+
if (beacon.nodeId && beacon.nodeId !== this.nodeId) {
|
|
464
|
+
this.registry.upsert({
|
|
465
|
+
nodeId: beacon.nodeId,
|
|
466
|
+
endpoint: endpoint,
|
|
467
|
+
wsEndpoint: beacon.capabilities?.wsPort
|
|
468
|
+
? `wss://${new URL(endpoint).hostname}:${beacon.capabilities.wsPort}`
|
|
469
|
+
: null,
|
|
470
|
+
networkName: beacon.networkName,
|
|
471
|
+
capabilities: beacon.capabilities,
|
|
472
|
+
});
|
|
473
|
+
this.stats.peersDiscovered++;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Queue peers for next depth
|
|
477
|
+
for (const peer of beacon.peers || []) {
|
|
478
|
+
if (peer.endpoint && !visited.has(peer.endpoint)) {
|
|
479
|
+
toVisit.add(peer.endpoint);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Also add these peers to our registry
|
|
483
|
+
if (peer.nodeId && peer.nodeId !== this.nodeId) {
|
|
484
|
+
this.registry.upsert({
|
|
485
|
+
nodeId: peer.nodeId,
|
|
486
|
+
endpoint: peer.endpoint,
|
|
487
|
+
wsEndpoint: peer.wsEndpoint,
|
|
488
|
+
networkName: peer.networkName,
|
|
489
|
+
lastSeen: peer.lastSeen,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
this.stats.beaconsFetched++;
|
|
495
|
+
} else {
|
|
496
|
+
this.stats.beaconsFailed++;
|
|
497
|
+
// Mark the peer as failed if we have them
|
|
498
|
+
const peer = [...this.registry.peers.values()]
|
|
499
|
+
.find(p => p.endpoint === endpoint);
|
|
500
|
+
if (peer) {
|
|
501
|
+
this.registry.markFailed(peer.nodeId);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
depth++;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
this.stats.crawlsCompleted++;
|
|
510
|
+
this.lastCrawl = Date.now();
|
|
511
|
+
this.emit('crawl-complete', {
|
|
512
|
+
peersFound: this.registry.size(),
|
|
513
|
+
depth,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
} finally {
|
|
517
|
+
this.crawlInProgress = false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Fetch a beacon from an endpoint
|
|
523
|
+
*/
|
|
524
|
+
async _fetchBeacon(endpoint) {
|
|
525
|
+
const url = new URL(SHERPA_CONFIG.beaconPath, endpoint).toString();
|
|
526
|
+
|
|
527
|
+
const controller = new AbortController();
|
|
528
|
+
const timeout = setTimeout(() => controller.abort(), SHERPA_CONFIG.crawlTimeout);
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const response = await fetch(url, {
|
|
532
|
+
headers: {
|
|
533
|
+
'Accept': 'application/json',
|
|
534
|
+
'User-Agent': `SHERPA/${SHERPA_CONFIG.protocolVersion}`,
|
|
535
|
+
},
|
|
536
|
+
signal: controller.signal,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
if (!response.ok) {
|
|
540
|
+
throw new Error(`HTTP ${response.status}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const text = await response.text();
|
|
544
|
+
if (text.length > SHERPA_CONFIG.maxBeaconSize) {
|
|
545
|
+
throw new Error('Beacon too large');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const data = JSON.parse(text);
|
|
549
|
+
const beacon = BeaconMessage.deserialize(data);
|
|
550
|
+
|
|
551
|
+
// Verify signature if required
|
|
552
|
+
if (SHERPA_CONFIG.signatureRequired && this.verifyFn) {
|
|
553
|
+
if (!beacon.publicKey || !beacon.signature) {
|
|
554
|
+
throw new Error('Missing signature');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const valid = this.verifyFn(
|
|
558
|
+
beacon.getSignableData(),
|
|
559
|
+
beacon.signature,
|
|
560
|
+
beacon.publicKey
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (!valid) {
|
|
564
|
+
throw new Error('Invalid signature');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check timestamp freshness
|
|
569
|
+
const age = Date.now() - beacon.timestamp;
|
|
570
|
+
if (age > beacon.ttl * 1000) {
|
|
571
|
+
throw new Error('Beacon expired');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return beacon;
|
|
575
|
+
|
|
576
|
+
} finally {
|
|
577
|
+
clearTimeout(timeout);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Get connection candidates for mesh networking
|
|
583
|
+
*/
|
|
584
|
+
getConnectionCandidates(count = 5) {
|
|
585
|
+
return this.registry.getBestPeers(count)
|
|
586
|
+
.filter(p => p.wsEndpoint)
|
|
587
|
+
.map(p => ({
|
|
588
|
+
nodeId: p.nodeId,
|
|
589
|
+
wsEndpoint: p.wsEndpoint,
|
|
590
|
+
score: p.score,
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Notify that we successfully connected to a peer
|
|
596
|
+
*/
|
|
597
|
+
markConnected(nodeId) {
|
|
598
|
+
const peer = this.registry.get(nodeId);
|
|
599
|
+
if (peer) {
|
|
600
|
+
peer.score = Math.min(1.0, peer.score + SHERPA_CONFIG.successBonus);
|
|
601
|
+
peer.lastSeen = Date.now();
|
|
602
|
+
peer.failureCount = 0;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Notify that connection to a peer failed
|
|
608
|
+
*/
|
|
609
|
+
markDisconnected(nodeId) {
|
|
610
|
+
this.registry.markFailed(nodeId);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
getStats() {
|
|
614
|
+
return {
|
|
615
|
+
...this.stats,
|
|
616
|
+
registrySize: this.registry.size(),
|
|
617
|
+
seedCount: this.seedEndpoints.size,
|
|
618
|
+
lastCrawl: this.lastCrawl,
|
|
619
|
+
crawlInProgress: this.crawlInProgress,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ============================================================
|
|
625
|
+
// EXPRESS MIDDLEWARE
|
|
626
|
+
// ============================================================
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Express middleware to serve the beacon endpoint
|
|
630
|
+
*/
|
|
631
|
+
function createBeaconMiddleware(sherpa) {
|
|
632
|
+
return (req, res) => {
|
|
633
|
+
try {
|
|
634
|
+
const beacon = sherpa.generateBeacon();
|
|
635
|
+
res.setHeader('Content-Type', 'application/json');
|
|
636
|
+
res.setHeader('Cache-Control', 'public, max-age=60');
|
|
637
|
+
res.setHeader('X-Sherpa-Version', SHERPA_CONFIG.protocolVersion);
|
|
638
|
+
res.json(beacon);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
res.status(500).json({ error: 'Failed to generate beacon' });
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ============================================================
|
|
646
|
+
// EXPORTS
|
|
647
|
+
// ============================================================
|
|
648
|
+
|
|
649
|
+
export {
|
|
650
|
+
SHERPA_CONFIG,
|
|
651
|
+
BeaconMessage,
|
|
652
|
+
PeerRegistry,
|
|
653
|
+
SherpaDiscovery,
|
|
654
|
+
createBeaconMiddleware,
|
|
655
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yakmesh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "YAKMESH: Yielding Atomic Kernel Modular Encryption Secured Hub - Post-quantum secure P2P mesh network for the 2026 threat landscape",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"./mesh/echo-ranging": "./mesh/echo-ranging.js",
|
|
27
27
|
"./mesh/pulse-sync": "./mesh/pulse-sync.js",
|
|
28
28
|
"./mesh/nakpak-routing": "./mesh/nakpak-routing.js",
|
|
29
|
-
"./mesh/beacon-broadcast": "./mesh/beacon-broadcast.js"
|
|
29
|
+
"./mesh/beacon-broadcast": "./mesh/beacon-broadcast.js",
|
|
30
|
+
"./mesh/sherpa-discovery": "./mesh/sherpa-discovery.js"
|
|
30
31
|
},
|
|
31
32
|
"bin": {
|
|
32
33
|
"yakmesh": "cli/index.js"
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"onion-routing",
|
|
68
69
|
"echo-ranging",
|
|
69
70
|
"nakpak-routing",
|
|
71
|
+
"sherpa-discovery",
|
|
70
72
|
"pulse-sync",
|
|
71
73
|
"beacon-broadcast",
|
|
72
74
|
"kyber",
|
package/server/index.js
CHANGED
|
@@ -25,6 +25,9 @@ import { ContentStore, createContentAPI } from '../content/index.js';
|
|
|
25
25
|
// Annex - Autonomous Network Negotiated Encrypted eXchange
|
|
26
26
|
import { Annex } from '../mesh/annex.js';
|
|
27
27
|
|
|
28
|
+
// SHERPA - Secure Hidden Endpoint Resolution Path Architecture
|
|
29
|
+
import { SherpaDiscovery, createBeaconMiddleware } from '../mesh/sherpa-discovery.js';
|
|
30
|
+
|
|
28
31
|
// Oracle system imports
|
|
29
32
|
import {
|
|
30
33
|
getOracle,
|
|
@@ -271,6 +274,31 @@ export class YakmeshNode {
|
|
|
271
274
|
});
|
|
272
275
|
console.log('β Annex channel initialized (encrypted P2P messaging)');
|
|
273
276
|
|
|
277
|
+
// 5c. Initialize SHERPA for decentralized peer discovery
|
|
278
|
+
this.sherpa = new SherpaDiscovery({
|
|
279
|
+
nodeId: this.identity.identity.nodeId,
|
|
280
|
+
networkName: this.genesisNetwork?.networkName,
|
|
281
|
+
publicKey: this.identity.identity.publicKey,
|
|
282
|
+
signFn: (data) => this.identity.sign(data),
|
|
283
|
+
verifyFn: (data, sig, pubKey) => this.identity.verify(data, sig, pubKey),
|
|
284
|
+
selfEndpoint: this.config.sherpa?.selfEndpoint || null,
|
|
285
|
+
wsEndpoint: this.config.sherpa?.wsEndpoint || null,
|
|
286
|
+
capabilities: {
|
|
287
|
+
wsPort: this.config.network.wsPort,
|
|
288
|
+
httpPort: this.config.network.httpPort,
|
|
289
|
+
supportsAnnex: true,
|
|
290
|
+
supportsNakpak: true,
|
|
291
|
+
supportsGossip: true,
|
|
292
|
+
},
|
|
293
|
+
seedEndpoints: this.config.sherpa?.seeds || [],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Start SHERPA if seeds are configured or selfEndpoint is set
|
|
297
|
+
if (this.config.sherpa?.enabled !== false) {
|
|
298
|
+
this.sherpa.start();
|
|
299
|
+
console.log('β SHERPA discovery initialized (decentralized peer discovery)');
|
|
300
|
+
}
|
|
301
|
+
|
|
274
302
|
// 6. Start HTTP server
|
|
275
303
|
await this._startHttpServer();
|
|
276
304
|
|
|
@@ -298,6 +326,9 @@ export class YakmeshNode {
|
|
|
298
326
|
if (this.annex) {
|
|
299
327
|
console.log(` Annex: β Encrypted P2P ready`);
|
|
300
328
|
}
|
|
329
|
+
if (this.sherpa) {
|
|
330
|
+
console.log(` SHERPA: β Beacon at /.well-known/yakmesh/beacon`);
|
|
331
|
+
}
|
|
301
332
|
if (this.adapter) {
|
|
302
333
|
console.log(` Adapter: β Enabled`);
|
|
303
334
|
}
|
|
@@ -606,6 +637,32 @@ export class YakmeshNode {
|
|
|
606
637
|
res.json(this.mesh.getPeers());
|
|
607
638
|
});
|
|
608
639
|
|
|
640
|
+
// =========================================
|
|
641
|
+
// SHERPA: Decentralized Peer Discovery
|
|
642
|
+
// =========================================
|
|
643
|
+
|
|
644
|
+
// Beacon endpoint for SHERPA peer discovery
|
|
645
|
+
// This allows other nodes to discover us and our known peers
|
|
646
|
+
if (this.sherpa) {
|
|
647
|
+
app.get('/.well-known/yakmesh/beacon', createBeaconMiddleware(this.sherpa));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// SHERPA discovery stats
|
|
651
|
+
app.get('/sherpa/status', (req, res) => {
|
|
652
|
+
if (!this.sherpa) {
|
|
653
|
+
return res.status(503).json({ error: 'SHERPA not initialized' });
|
|
654
|
+
}
|
|
655
|
+
res.json(this.sherpa.getStats());
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Get connection candidates from SHERPA
|
|
659
|
+
app.get('/sherpa/candidates', (req, res) => {
|
|
660
|
+
if (!this.sherpa) {
|
|
661
|
+
return res.status(503).json({ error: 'SHERPA not initialized' });
|
|
662
|
+
}
|
|
663
|
+
res.json(this.sherpa.getConnectionCandidates(10));
|
|
664
|
+
});
|
|
665
|
+
|
|
609
666
|
// Replication stats
|
|
610
667
|
app.get('/replication', (req, res) => {
|
|
611
668
|
res.json(this.replication.getStats());
|