@skillkit/mesh 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/LICENSE +190 -0
- package/README.md +220 -0
- package/dist/index.d.ts +781 -0
- package/dist/index.js +3326 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3326 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/types.ts
|
|
12
|
+
var DEFAULT_PORT, DEFAULT_DISCOVERY_PORT, HEALTH_CHECK_TIMEOUT, DISCOVERY_INTERVAL, MESH_VERSION;
|
|
13
|
+
var init_types = __esm({
|
|
14
|
+
"src/types.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
DEFAULT_PORT = 9876;
|
|
17
|
+
DEFAULT_DISCOVERY_PORT = 9877;
|
|
18
|
+
HEALTH_CHECK_TIMEOUT = 5e3;
|
|
19
|
+
DISCOVERY_INTERVAL = 3e4;
|
|
20
|
+
MESH_VERSION = "1.7.11";
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// src/config/hosts-config.ts
|
|
25
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
26
|
+
import { existsSync } from "fs";
|
|
27
|
+
import { dirname, join } from "path";
|
|
28
|
+
import { homedir, hostname as osHostname } from "os";
|
|
29
|
+
import { randomUUID } from "crypto";
|
|
30
|
+
async function withFileLock(fn) {
|
|
31
|
+
while (fileLock) {
|
|
32
|
+
await fileLock;
|
|
33
|
+
}
|
|
34
|
+
let resolve;
|
|
35
|
+
fileLock = new Promise((r) => {
|
|
36
|
+
resolve = r;
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
return await fn();
|
|
40
|
+
} finally {
|
|
41
|
+
fileLock = null;
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function getHostsFilePath() {
|
|
46
|
+
return HOSTS_FILE_PATH;
|
|
47
|
+
}
|
|
48
|
+
async function loadHostsFile() {
|
|
49
|
+
if (!existsSync(HOSTS_FILE_PATH)) {
|
|
50
|
+
return createDefaultHostsFile();
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const content = await readFile(HOSTS_FILE_PATH, "utf-8");
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
} catch {
|
|
56
|
+
return createDefaultHostsFile();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function saveHostsFile(hostsFile) {
|
|
60
|
+
await mkdir(dirname(HOSTS_FILE_PATH), { recursive: true });
|
|
61
|
+
hostsFile.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
|
+
await writeFile(HOSTS_FILE_PATH, JSON.stringify(hostsFile, null, 2), "utf-8");
|
|
63
|
+
}
|
|
64
|
+
function createDefaultHostsFile() {
|
|
65
|
+
return {
|
|
66
|
+
version: MESH_VERSION,
|
|
67
|
+
localHost: {
|
|
68
|
+
id: randomUUID(),
|
|
69
|
+
name: getDefaultHostName(),
|
|
70
|
+
port: DEFAULT_PORT,
|
|
71
|
+
autoStart: false,
|
|
72
|
+
discoveryEnabled: true
|
|
73
|
+
},
|
|
74
|
+
knownHosts: [],
|
|
75
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function getDefaultHostName() {
|
|
79
|
+
const hostname = osHostname();
|
|
80
|
+
return hostname || `skillkit-host-${randomUUID().slice(0, 8)}`;
|
|
81
|
+
}
|
|
82
|
+
async function getLocalHostConfig() {
|
|
83
|
+
const hostsFile = await loadHostsFile();
|
|
84
|
+
return hostsFile.localHost;
|
|
85
|
+
}
|
|
86
|
+
async function updateLocalHostConfig(updates) {
|
|
87
|
+
return withFileLock(async () => {
|
|
88
|
+
const hostsFile = await loadHostsFile();
|
|
89
|
+
hostsFile.localHost = { ...hostsFile.localHost, ...updates };
|
|
90
|
+
await saveHostsFile(hostsFile);
|
|
91
|
+
return hostsFile.localHost;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function addKnownHost(host) {
|
|
95
|
+
return withFileLock(async () => {
|
|
96
|
+
const hostsFile = await loadHostsFile();
|
|
97
|
+
const existingIndex = hostsFile.knownHosts.findIndex((h) => h.id === host.id);
|
|
98
|
+
if (existingIndex >= 0) {
|
|
99
|
+
hostsFile.knownHosts[existingIndex] = host;
|
|
100
|
+
} else {
|
|
101
|
+
hostsFile.knownHosts.push(host);
|
|
102
|
+
}
|
|
103
|
+
await saveHostsFile(hostsFile);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function removeKnownHost(hostId) {
|
|
107
|
+
return withFileLock(async () => {
|
|
108
|
+
const hostsFile = await loadHostsFile();
|
|
109
|
+
const initialLength = hostsFile.knownHosts.length;
|
|
110
|
+
hostsFile.knownHosts = hostsFile.knownHosts.filter((h) => h.id !== hostId);
|
|
111
|
+
if (hostsFile.knownHosts.length < initialLength) {
|
|
112
|
+
await saveHostsFile(hostsFile);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function getKnownHosts() {
|
|
119
|
+
const hostsFile = await loadHostsFile();
|
|
120
|
+
return hostsFile.knownHosts;
|
|
121
|
+
}
|
|
122
|
+
async function getKnownHost(hostId) {
|
|
123
|
+
const hosts = await getKnownHosts();
|
|
124
|
+
return hosts.find((h) => h.id === hostId);
|
|
125
|
+
}
|
|
126
|
+
async function updateKnownHost(hostId, updates) {
|
|
127
|
+
const hostsFile = await loadHostsFile();
|
|
128
|
+
const index = hostsFile.knownHosts.findIndex((h) => h.id === hostId);
|
|
129
|
+
if (index < 0) return null;
|
|
130
|
+
hostsFile.knownHosts[index] = { ...hostsFile.knownHosts[index], ...updates };
|
|
131
|
+
await saveHostsFile(hostsFile);
|
|
132
|
+
return hostsFile.knownHosts[index];
|
|
133
|
+
}
|
|
134
|
+
async function initializeHostsFile() {
|
|
135
|
+
if (!existsSync(HOSTS_FILE_PATH)) {
|
|
136
|
+
const hostsFile = createDefaultHostsFile();
|
|
137
|
+
await saveHostsFile(hostsFile);
|
|
138
|
+
return hostsFile;
|
|
139
|
+
}
|
|
140
|
+
return loadHostsFile();
|
|
141
|
+
}
|
|
142
|
+
var HOSTS_FILE_PATH, fileLock;
|
|
143
|
+
var init_hosts_config = __esm({
|
|
144
|
+
"src/config/hosts-config.ts"() {
|
|
145
|
+
"use strict";
|
|
146
|
+
init_types();
|
|
147
|
+
HOSTS_FILE_PATH = join(homedir(), ".skillkit", "hosts.json");
|
|
148
|
+
fileLock = null;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// src/config/index.ts
|
|
153
|
+
var config_exports = {};
|
|
154
|
+
__export(config_exports, {
|
|
155
|
+
addKnownHost: () => addKnownHost,
|
|
156
|
+
createDefaultHostsFile: () => createDefaultHostsFile,
|
|
157
|
+
getHostsFilePath: () => getHostsFilePath,
|
|
158
|
+
getKnownHost: () => getKnownHost,
|
|
159
|
+
getKnownHosts: () => getKnownHosts,
|
|
160
|
+
getLocalHostConfig: () => getLocalHostConfig,
|
|
161
|
+
initializeHostsFile: () => initializeHostsFile,
|
|
162
|
+
loadHostsFile: () => loadHostsFile,
|
|
163
|
+
removeKnownHost: () => removeKnownHost,
|
|
164
|
+
saveHostsFile: () => saveHostsFile,
|
|
165
|
+
updateKnownHost: () => updateKnownHost,
|
|
166
|
+
updateLocalHostConfig: () => updateLocalHostConfig
|
|
167
|
+
});
|
|
168
|
+
var init_config = __esm({
|
|
169
|
+
"src/config/index.ts"() {
|
|
170
|
+
"use strict";
|
|
171
|
+
init_hosts_config();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// src/crypto/identity.ts
|
|
176
|
+
import * as ed25519 from "@noble/ed25519";
|
|
177
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
178
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
179
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
180
|
+
import { randomBytes } from "@noble/ciphers/webcrypto";
|
|
181
|
+
var PeerIdentity;
|
|
182
|
+
var init_identity = __esm({
|
|
183
|
+
"src/crypto/identity.ts"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
PeerIdentity = class _PeerIdentity {
|
|
186
|
+
keypair;
|
|
187
|
+
constructor(keypair) {
|
|
188
|
+
this.keypair = keypair;
|
|
189
|
+
}
|
|
190
|
+
static async generate() {
|
|
191
|
+
const privateKey = randomBytes(32);
|
|
192
|
+
const publicKey = await ed25519.getPublicKeyAsync(privateKey);
|
|
193
|
+
const fingerprint = _PeerIdentity.computeFingerprint(publicKey);
|
|
194
|
+
return new _PeerIdentity({
|
|
195
|
+
publicKey,
|
|
196
|
+
privateKey,
|
|
197
|
+
fingerprint
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
static async fromPrivateKey(privateKey) {
|
|
201
|
+
if (privateKey.length !== 32) {
|
|
202
|
+
throw new Error("Private key must be 32 bytes");
|
|
203
|
+
}
|
|
204
|
+
const publicKey = await ed25519.getPublicKeyAsync(privateKey);
|
|
205
|
+
const fingerprint = _PeerIdentity.computeFingerprint(publicKey);
|
|
206
|
+
return new _PeerIdentity({
|
|
207
|
+
publicKey,
|
|
208
|
+
privateKey,
|
|
209
|
+
fingerprint
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
static fromSerialized(data) {
|
|
213
|
+
const publicKey = hexToBytes(data.publicKey);
|
|
214
|
+
const privateKey = hexToBytes(data.privateKey);
|
|
215
|
+
const fingerprint = data.fingerprint;
|
|
216
|
+
const computed = _PeerIdentity.computeFingerprint(publicKey);
|
|
217
|
+
if (computed !== fingerprint) {
|
|
218
|
+
throw new Error("Fingerprint mismatch - corrupted identity");
|
|
219
|
+
}
|
|
220
|
+
return new _PeerIdentity({
|
|
221
|
+
publicKey,
|
|
222
|
+
privateKey,
|
|
223
|
+
fingerprint
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
static computeFingerprint(publicKey) {
|
|
227
|
+
const hash = sha256(publicKey);
|
|
228
|
+
return bytesToHex(hash.slice(0, 8));
|
|
229
|
+
}
|
|
230
|
+
static async verify(signature, message, publicKey) {
|
|
231
|
+
try {
|
|
232
|
+
return await ed25519.verifyAsync(signature, message, publicKey);
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
static async verifyHex(signatureHex, messageHex, publicKeyHex) {
|
|
238
|
+
try {
|
|
239
|
+
const signature = hexToBytes(signatureHex);
|
|
240
|
+
const message = hexToBytes(messageHex);
|
|
241
|
+
const publicKey = hexToBytes(publicKeyHex);
|
|
242
|
+
return await _PeerIdentity.verify(signature, message, publicKey);
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async sign(message) {
|
|
248
|
+
return await ed25519.signAsync(message, this.keypair.privateKey);
|
|
249
|
+
}
|
|
250
|
+
async signString(message) {
|
|
251
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
252
|
+
const signature = await this.sign(messageBytes);
|
|
253
|
+
return bytesToHex(signature);
|
|
254
|
+
}
|
|
255
|
+
async signObject(obj) {
|
|
256
|
+
const message = JSON.stringify(obj);
|
|
257
|
+
return await this.signString(message);
|
|
258
|
+
}
|
|
259
|
+
deriveSharedSecret(peerPublicKey) {
|
|
260
|
+
const x25519PrivateKey = this.keypair.privateKey;
|
|
261
|
+
return x25519.scalarMult(x25519PrivateKey, peerPublicKey);
|
|
262
|
+
}
|
|
263
|
+
deriveSharedSecretHex(peerPublicKeyHex) {
|
|
264
|
+
const peerPublicKey = hexToBytes(peerPublicKeyHex);
|
|
265
|
+
return this.deriveSharedSecret(peerPublicKey);
|
|
266
|
+
}
|
|
267
|
+
serialize() {
|
|
268
|
+
return {
|
|
269
|
+
publicKey: bytesToHex(this.keypair.publicKey),
|
|
270
|
+
privateKey: bytesToHex(this.keypair.privateKey),
|
|
271
|
+
fingerprint: this.keypair.fingerprint
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
get publicKey() {
|
|
275
|
+
return this.keypair.publicKey;
|
|
276
|
+
}
|
|
277
|
+
get publicKeyHex() {
|
|
278
|
+
return bytesToHex(this.keypair.publicKey);
|
|
279
|
+
}
|
|
280
|
+
get privateKey() {
|
|
281
|
+
return this.keypair.privateKey;
|
|
282
|
+
}
|
|
283
|
+
get privateKeyHex() {
|
|
284
|
+
return bytesToHex(this.keypair.privateKey);
|
|
285
|
+
}
|
|
286
|
+
get fingerprint() {
|
|
287
|
+
return this.keypair.fingerprint;
|
|
288
|
+
}
|
|
289
|
+
toJSON() {
|
|
290
|
+
return {
|
|
291
|
+
publicKey: this.publicKeyHex,
|
|
292
|
+
fingerprint: this.fingerprint
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// src/peer/registry.ts
|
|
300
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
301
|
+
import { existsSync as existsSync2 } from "fs";
|
|
302
|
+
import { join as join2 } from "path";
|
|
303
|
+
import { homedir as homedir2 } from "os";
|
|
304
|
+
async function getPeerRegistry() {
|
|
305
|
+
if (!globalRegistry) {
|
|
306
|
+
globalRegistry = new PeerRegistryManager();
|
|
307
|
+
await globalRegistry.initialize();
|
|
308
|
+
}
|
|
309
|
+
return globalRegistry;
|
|
310
|
+
}
|
|
311
|
+
var PEERS_DIR, PeerRegistryManager, globalRegistry;
|
|
312
|
+
var init_registry = __esm({
|
|
313
|
+
"src/peer/registry.ts"() {
|
|
314
|
+
"use strict";
|
|
315
|
+
init_hosts_config();
|
|
316
|
+
PEERS_DIR = join2(homedir2(), ".skillkit", "mesh", "peers");
|
|
317
|
+
PeerRegistryManager = class {
|
|
318
|
+
registry = {
|
|
319
|
+
peers: /* @__PURE__ */ new Map(),
|
|
320
|
+
localPeers: /* @__PURE__ */ new Map()
|
|
321
|
+
};
|
|
322
|
+
async initialize() {
|
|
323
|
+
await mkdir2(PEERS_DIR, { recursive: true });
|
|
324
|
+
await this.loadLocalPeers();
|
|
325
|
+
}
|
|
326
|
+
async registerLocalPeer(registration) {
|
|
327
|
+
const localConfig = await getLocalHostConfig();
|
|
328
|
+
const peer = {
|
|
329
|
+
hostId: localConfig.id,
|
|
330
|
+
agentId: registration.agentId,
|
|
331
|
+
agentName: registration.agentName,
|
|
332
|
+
aliases: registration.aliases,
|
|
333
|
+
capabilities: registration.capabilities,
|
|
334
|
+
status: "online",
|
|
335
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
336
|
+
};
|
|
337
|
+
this.registry.localPeers.set(peer.agentId, peer);
|
|
338
|
+
await this.saveLocalPeers();
|
|
339
|
+
return peer;
|
|
340
|
+
}
|
|
341
|
+
async unregisterLocalPeer(agentId) {
|
|
342
|
+
const deleted = this.registry.localPeers.delete(agentId);
|
|
343
|
+
if (deleted) {
|
|
344
|
+
await this.saveLocalPeers();
|
|
345
|
+
}
|
|
346
|
+
return deleted;
|
|
347
|
+
}
|
|
348
|
+
registerRemotePeer(peer) {
|
|
349
|
+
const key = `${peer.hostId}:${peer.agentId}`;
|
|
350
|
+
this.registry.peers.set(key, peer);
|
|
351
|
+
}
|
|
352
|
+
unregisterRemotePeer(hostId, agentId) {
|
|
353
|
+
const key = `${hostId}:${agentId}`;
|
|
354
|
+
return this.registry.peers.delete(key);
|
|
355
|
+
}
|
|
356
|
+
getPeer(hostId, agentId) {
|
|
357
|
+
const key = `${hostId}:${agentId}`;
|
|
358
|
+
return this.registry.peers.get(key);
|
|
359
|
+
}
|
|
360
|
+
getLocalPeer(agentId) {
|
|
361
|
+
return this.registry.localPeers.get(agentId);
|
|
362
|
+
}
|
|
363
|
+
getAllPeers() {
|
|
364
|
+
return [
|
|
365
|
+
...Array.from(this.registry.localPeers.values()),
|
|
366
|
+
...Array.from(this.registry.peers.values())
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
getLocalPeers() {
|
|
370
|
+
return Array.from(this.registry.localPeers.values());
|
|
371
|
+
}
|
|
372
|
+
getRemotePeers() {
|
|
373
|
+
return Array.from(this.registry.peers.values());
|
|
374
|
+
}
|
|
375
|
+
getPeersByHost(hostId) {
|
|
376
|
+
return this.getAllPeers().filter((p) => p.hostId === hostId);
|
|
377
|
+
}
|
|
378
|
+
findPeerByName(name) {
|
|
379
|
+
const lowerName = name.toLowerCase();
|
|
380
|
+
for (const peer of this.getAllPeers()) {
|
|
381
|
+
if (peer.agentName.toLowerCase() === lowerName) {
|
|
382
|
+
return peer;
|
|
383
|
+
}
|
|
384
|
+
if (peer.aliases.some((a) => a.toLowerCase() === lowerName)) {
|
|
385
|
+
return peer;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
findPeersByCapability(capability) {
|
|
391
|
+
return this.getAllPeers().filter((p) => p.capabilities.includes(capability));
|
|
392
|
+
}
|
|
393
|
+
async resolvePeerAddress(nameOrId) {
|
|
394
|
+
const parts = nameOrId.split("@");
|
|
395
|
+
const peerName = parts[0];
|
|
396
|
+
const hostName = parts[1];
|
|
397
|
+
if (hostName) {
|
|
398
|
+
const hosts2 = await getKnownHosts();
|
|
399
|
+
const host2 = hosts2.find(
|
|
400
|
+
(h) => h.name.toLowerCase() === hostName.toLowerCase() || h.id === hostName
|
|
401
|
+
);
|
|
402
|
+
if (!host2) return null;
|
|
403
|
+
const peer2 = this.getPeersByHost(host2.id).find(
|
|
404
|
+
(p) => p.agentName.toLowerCase() === peerName.toLowerCase() || p.agentId === peerName || p.aliases.some((a) => a.toLowerCase() === peerName.toLowerCase())
|
|
405
|
+
);
|
|
406
|
+
if (!peer2) return null;
|
|
407
|
+
return { host: host2, peer: peer2 };
|
|
408
|
+
}
|
|
409
|
+
const peer = this.findPeerByName(peerName);
|
|
410
|
+
if (!peer) return null;
|
|
411
|
+
const hosts = await getKnownHosts();
|
|
412
|
+
const host = hosts.find((h) => h.id === peer.hostId);
|
|
413
|
+
if (!host) {
|
|
414
|
+
const localConfig = await getLocalHostConfig();
|
|
415
|
+
if (peer.hostId === localConfig.id) {
|
|
416
|
+
return {
|
|
417
|
+
host: {
|
|
418
|
+
id: localConfig.id,
|
|
419
|
+
name: localConfig.name,
|
|
420
|
+
address: "127.0.0.1",
|
|
421
|
+
port: localConfig.port,
|
|
422
|
+
status: "online",
|
|
423
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
424
|
+
},
|
|
425
|
+
peer
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return { host, peer };
|
|
431
|
+
}
|
|
432
|
+
updatePeerStatus(hostId, agentId, status) {
|
|
433
|
+
const key = `${hostId}:${agentId}`;
|
|
434
|
+
const peer = this.registry.peers.get(key);
|
|
435
|
+
if (peer) {
|
|
436
|
+
peer.status = status;
|
|
437
|
+
peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
markHostOffline(hostId) {
|
|
441
|
+
for (const [, peer] of this.registry.peers) {
|
|
442
|
+
if (peer.hostId === hostId) {
|
|
443
|
+
peer.status = "offline";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
markHostOnline(hostId) {
|
|
448
|
+
for (const [, peer] of this.registry.peers) {
|
|
449
|
+
if (peer.hostId === hostId) {
|
|
450
|
+
peer.status = "online";
|
|
451
|
+
peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
clearRemotePeers() {
|
|
456
|
+
this.registry.peers.clear();
|
|
457
|
+
}
|
|
458
|
+
async loadLocalPeers() {
|
|
459
|
+
const filePath = join2(PEERS_DIR, "local-peers.json");
|
|
460
|
+
if (!existsSync2(filePath)) return;
|
|
461
|
+
try {
|
|
462
|
+
const content = await readFile2(filePath, "utf-8");
|
|
463
|
+
const peers = JSON.parse(content);
|
|
464
|
+
for (const peer of peers) {
|
|
465
|
+
this.registry.localPeers.set(peer.agentId, peer);
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async saveLocalPeers() {
|
|
471
|
+
const filePath = join2(PEERS_DIR, "local-peers.json");
|
|
472
|
+
const peers = Array.from(this.registry.localPeers.values());
|
|
473
|
+
await mkdir2(PEERS_DIR, { recursive: true });
|
|
474
|
+
await writeFile2(filePath, JSON.stringify(peers, null, 2), "utf-8");
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
globalRegistry = null;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// src/peer/health.ts
|
|
482
|
+
import got from "got";
|
|
483
|
+
async function checkHostHealth(host, options = {}) {
|
|
484
|
+
const timeout = options.timeout ?? HEALTH_CHECK_TIMEOUT;
|
|
485
|
+
const startTime = Date.now();
|
|
486
|
+
const result = {
|
|
487
|
+
hostId: host.id,
|
|
488
|
+
address: host.address,
|
|
489
|
+
port: host.port,
|
|
490
|
+
status: "unknown",
|
|
491
|
+
latencyMs: 0,
|
|
492
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
493
|
+
};
|
|
494
|
+
try {
|
|
495
|
+
const url = `http://${host.address}:${host.port}/health`;
|
|
496
|
+
const response = await got.get(url, {
|
|
497
|
+
timeout: { request: timeout },
|
|
498
|
+
retry: { limit: 0 },
|
|
499
|
+
throwHttpErrors: false
|
|
500
|
+
});
|
|
501
|
+
result.latencyMs = Date.now() - startTime;
|
|
502
|
+
if (response.statusCode === 200) {
|
|
503
|
+
result.status = "online";
|
|
504
|
+
} else {
|
|
505
|
+
result.status = "offline";
|
|
506
|
+
result.error = `HTTP ${response.statusCode}`;
|
|
507
|
+
}
|
|
508
|
+
} catch (err) {
|
|
509
|
+
result.latencyMs = Date.now() - startTime;
|
|
510
|
+
result.status = "offline";
|
|
511
|
+
result.error = err.code || err.message || "Connection failed";
|
|
512
|
+
}
|
|
513
|
+
if (options.updateStatus !== false) {
|
|
514
|
+
await updateKnownHost(host.id, {
|
|
515
|
+
status: result.status,
|
|
516
|
+
lastSeen: result.status === "online" ? result.checkedAt : host.lastSeen
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
async function checkAllHostsHealth(options = {}) {
|
|
522
|
+
const hosts = await getKnownHosts();
|
|
523
|
+
const results = await Promise.all(hosts.map((host) => checkHostHealth(host, options)));
|
|
524
|
+
return results;
|
|
525
|
+
}
|
|
526
|
+
async function getOnlineHosts() {
|
|
527
|
+
const hosts = await getKnownHosts();
|
|
528
|
+
return hosts.filter((h) => h.status === "online");
|
|
529
|
+
}
|
|
530
|
+
async function getOfflineHosts() {
|
|
531
|
+
const hosts = await getKnownHosts();
|
|
532
|
+
return hosts.filter((h) => h.status === "offline");
|
|
533
|
+
}
|
|
534
|
+
async function waitForHost(host, maxWaitMs = 3e4, intervalMs = 1e3) {
|
|
535
|
+
const deadline = Date.now() + maxWaitMs;
|
|
536
|
+
while (Date.now() < deadline) {
|
|
537
|
+
const result = await checkHostHealth(host, { updateStatus: false });
|
|
538
|
+
if (result.status === "online") {
|
|
539
|
+
await updateKnownHost(host.id, {
|
|
540
|
+
status: "online",
|
|
541
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
542
|
+
});
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
var HealthMonitor;
|
|
550
|
+
var init_health = __esm({
|
|
551
|
+
"src/peer/health.ts"() {
|
|
552
|
+
"use strict";
|
|
553
|
+
init_types();
|
|
554
|
+
init_hosts_config();
|
|
555
|
+
HealthMonitor = class {
|
|
556
|
+
interval = null;
|
|
557
|
+
running = false;
|
|
558
|
+
checking = false;
|
|
559
|
+
onStatusChange;
|
|
560
|
+
constructor(options = {}) {
|
|
561
|
+
this.onStatusChange = options.onStatusChange;
|
|
562
|
+
}
|
|
563
|
+
async start(intervalMs = 3e4) {
|
|
564
|
+
if (this.running) return;
|
|
565
|
+
this.running = true;
|
|
566
|
+
await this.checkAll();
|
|
567
|
+
this.interval = setInterval(() => {
|
|
568
|
+
if (!this.checking) {
|
|
569
|
+
this.checking = true;
|
|
570
|
+
this.checkAll().finally(() => {
|
|
571
|
+
this.checking = false;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}, intervalMs);
|
|
575
|
+
}
|
|
576
|
+
stop() {
|
|
577
|
+
if (!this.running) return;
|
|
578
|
+
if (this.interval) {
|
|
579
|
+
clearInterval(this.interval);
|
|
580
|
+
this.interval = null;
|
|
581
|
+
}
|
|
582
|
+
this.running = false;
|
|
583
|
+
}
|
|
584
|
+
isRunning() {
|
|
585
|
+
return this.running;
|
|
586
|
+
}
|
|
587
|
+
async checkAll() {
|
|
588
|
+
const hosts = await getKnownHosts();
|
|
589
|
+
for (const host of hosts) {
|
|
590
|
+
const oldStatus = host.status;
|
|
591
|
+
const result = await checkHostHealth(host);
|
|
592
|
+
if (this.onStatusChange && oldStatus !== result.status) {
|
|
593
|
+
this.onStatusChange(host, oldStatus, result.status);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// src/peer/index.ts
|
|
602
|
+
var peer_exports = {};
|
|
603
|
+
__export(peer_exports, {
|
|
604
|
+
HealthMonitor: () => HealthMonitor,
|
|
605
|
+
PeerRegistryManager: () => PeerRegistryManager,
|
|
606
|
+
checkAllHostsHealth: () => checkAllHostsHealth,
|
|
607
|
+
checkHostHealth: () => checkHostHealth,
|
|
608
|
+
getOfflineHosts: () => getOfflineHosts,
|
|
609
|
+
getOnlineHosts: () => getOnlineHosts,
|
|
610
|
+
getPeerRegistry: () => getPeerRegistry,
|
|
611
|
+
waitForHost: () => waitForHost
|
|
612
|
+
});
|
|
613
|
+
var init_peer = __esm({
|
|
614
|
+
"src/peer/index.ts"() {
|
|
615
|
+
"use strict";
|
|
616
|
+
init_registry();
|
|
617
|
+
init_health();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// src/crypto/encryption.ts
|
|
622
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
623
|
+
import { randomBytes as randomBytes2 } from "@noble/ciphers/webcrypto";
|
|
624
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes3 } from "@noble/hashes/utils";
|
|
625
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
626
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
627
|
+
import { x25519 as x255192 } from "@noble/curves/ed25519";
|
|
628
|
+
function generateNonce() {
|
|
629
|
+
return bytesToHex2(randomBytes2(24));
|
|
630
|
+
}
|
|
631
|
+
function generateMessageId() {
|
|
632
|
+
return bytesToHex2(randomBytes2(16));
|
|
633
|
+
}
|
|
634
|
+
var MessageEncryption, PublicKeyEncryption;
|
|
635
|
+
var init_encryption = __esm({
|
|
636
|
+
"src/crypto/encryption.ts"() {
|
|
637
|
+
"use strict";
|
|
638
|
+
MessageEncryption = class _MessageEncryption {
|
|
639
|
+
key;
|
|
640
|
+
constructor(sharedSecret) {
|
|
641
|
+
this.key = hkdf(sha2562, sharedSecret, void 0, "skillkit-mesh-v1", 32);
|
|
642
|
+
}
|
|
643
|
+
encrypt(plaintext) {
|
|
644
|
+
const nonce = randomBytes2(24);
|
|
645
|
+
const data = typeof plaintext === "string" ? new TextEncoder().encode(plaintext) : plaintext;
|
|
646
|
+
const cipher = xchacha20poly1305(this.key, nonce);
|
|
647
|
+
const ciphertext = cipher.encrypt(data);
|
|
648
|
+
return {
|
|
649
|
+
nonce: bytesToHex2(nonce),
|
|
650
|
+
ciphertext: bytesToHex2(ciphertext)
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
decrypt(encrypted) {
|
|
654
|
+
const nonce = hexToBytes3(encrypted.nonce);
|
|
655
|
+
const ciphertext = hexToBytes3(encrypted.ciphertext);
|
|
656
|
+
const cipher = xchacha20poly1305(this.key, nonce);
|
|
657
|
+
return cipher.decrypt(ciphertext);
|
|
658
|
+
}
|
|
659
|
+
decryptToString(encrypted) {
|
|
660
|
+
const plaintext = this.decrypt(encrypted);
|
|
661
|
+
return new TextDecoder().decode(plaintext);
|
|
662
|
+
}
|
|
663
|
+
decryptToObject(encrypted) {
|
|
664
|
+
const plaintext = this.decryptToString(encrypted);
|
|
665
|
+
return JSON.parse(plaintext);
|
|
666
|
+
}
|
|
667
|
+
encryptObject(obj) {
|
|
668
|
+
return this.encrypt(JSON.stringify(obj));
|
|
669
|
+
}
|
|
670
|
+
static fromSharedSecret(sharedSecret) {
|
|
671
|
+
return new _MessageEncryption(sharedSecret);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
PublicKeyEncryption = class _PublicKeyEncryption {
|
|
675
|
+
static encrypt(message, recipientPublicKey) {
|
|
676
|
+
const ephemeralPrivateKey = randomBytes2(32);
|
|
677
|
+
const ephemeralPublicKey = x255192.scalarMultBase(ephemeralPrivateKey);
|
|
678
|
+
const sharedSecret = x255192.scalarMult(ephemeralPrivateKey, recipientPublicKey);
|
|
679
|
+
const key = hkdf(sha2562, sharedSecret, void 0, "skillkit-mesh-pk-v1", 32);
|
|
680
|
+
const nonce = randomBytes2(24);
|
|
681
|
+
const cipher = xchacha20poly1305(key, nonce);
|
|
682
|
+
const ciphertext = cipher.encrypt(message);
|
|
683
|
+
return {
|
|
684
|
+
ephemeralPublicKey: bytesToHex2(ephemeralPublicKey),
|
|
685
|
+
nonce: bytesToHex2(nonce),
|
|
686
|
+
ciphertext: bytesToHex2(ciphertext)
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
static encryptString(message, recipientPublicKey) {
|
|
690
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
691
|
+
return _PublicKeyEncryption.encrypt(messageBytes, recipientPublicKey);
|
|
692
|
+
}
|
|
693
|
+
static encryptStringHex(message, recipientPublicKeyHex) {
|
|
694
|
+
const recipientPublicKey = hexToBytes3(recipientPublicKeyHex);
|
|
695
|
+
return _PublicKeyEncryption.encryptString(message, recipientPublicKey);
|
|
696
|
+
}
|
|
697
|
+
static decrypt(encrypted, recipientPrivateKey) {
|
|
698
|
+
const ephemeralPublicKey = hexToBytes3(encrypted.ephemeralPublicKey);
|
|
699
|
+
const nonce = hexToBytes3(encrypted.nonce);
|
|
700
|
+
const ciphertext = hexToBytes3(encrypted.ciphertext);
|
|
701
|
+
const sharedSecret = x255192.scalarMult(recipientPrivateKey, ephemeralPublicKey);
|
|
702
|
+
const key = hkdf(sha2562, sharedSecret, void 0, "skillkit-mesh-pk-v1", 32);
|
|
703
|
+
const cipher = xchacha20poly1305(key, nonce);
|
|
704
|
+
return cipher.decrypt(ciphertext);
|
|
705
|
+
}
|
|
706
|
+
static decryptToString(encrypted, recipientPrivateKey) {
|
|
707
|
+
const plaintext = _PublicKeyEncryption.decrypt(encrypted, recipientPrivateKey);
|
|
708
|
+
return new TextDecoder().decode(plaintext);
|
|
709
|
+
}
|
|
710
|
+
static decryptToObject(encrypted, recipientPrivateKey) {
|
|
711
|
+
const plaintext = _PublicKeyEncryption.decryptToString(
|
|
712
|
+
encrypted,
|
|
713
|
+
recipientPrivateKey
|
|
714
|
+
);
|
|
715
|
+
return JSON.parse(plaintext);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// src/crypto/signatures.ts
|
|
722
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes6 } from "@noble/hashes/utils";
|
|
723
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha256";
|
|
724
|
+
function generateNonce2() {
|
|
725
|
+
const bytes = new Uint8Array(16);
|
|
726
|
+
crypto.getRandomValues(bytes);
|
|
727
|
+
return bytesToHex4(bytes);
|
|
728
|
+
}
|
|
729
|
+
function canonicalize(obj) {
|
|
730
|
+
if (obj === null || obj === void 0) {
|
|
731
|
+
return "null";
|
|
732
|
+
}
|
|
733
|
+
if (typeof obj !== "object") {
|
|
734
|
+
return JSON.stringify(obj);
|
|
735
|
+
}
|
|
736
|
+
if (Array.isArray(obj)) {
|
|
737
|
+
return "[" + obj.map(canonicalize).join(",") + "]";
|
|
738
|
+
}
|
|
739
|
+
const keys = Object.keys(obj).sort();
|
|
740
|
+
const pairs = keys.map((key) => {
|
|
741
|
+
const value = obj[key];
|
|
742
|
+
return JSON.stringify(key) + ":" + canonicalize(value);
|
|
743
|
+
});
|
|
744
|
+
return "{" + pairs.join(",") + "}";
|
|
745
|
+
}
|
|
746
|
+
function hashData(data) {
|
|
747
|
+
const canonical = canonicalize(data);
|
|
748
|
+
return sha2563(new TextEncoder().encode(canonical));
|
|
749
|
+
}
|
|
750
|
+
async function signData(data, identity) {
|
|
751
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
752
|
+
const nonce = generateNonce2();
|
|
753
|
+
const toSign = {
|
|
754
|
+
data,
|
|
755
|
+
timestamp,
|
|
756
|
+
nonce,
|
|
757
|
+
senderFingerprint: identity.fingerprint
|
|
758
|
+
};
|
|
759
|
+
const hash = hashData(toSign);
|
|
760
|
+
const signature = await identity.sign(hash);
|
|
761
|
+
return {
|
|
762
|
+
data,
|
|
763
|
+
signature: bytesToHex4(signature),
|
|
764
|
+
senderFingerprint: identity.fingerprint,
|
|
765
|
+
senderPublicKey: identity.publicKeyHex,
|
|
766
|
+
timestamp,
|
|
767
|
+
nonce
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
async function verifySignedData(signed, trustedPublicKey) {
|
|
771
|
+
try {
|
|
772
|
+
const publicKey = trustedPublicKey || hexToBytes6(signed.senderPublicKey);
|
|
773
|
+
const computedFingerprint = PeerIdentity.computeFingerprint(publicKey);
|
|
774
|
+
if (computedFingerprint !== signed.senderFingerprint) {
|
|
775
|
+
return {
|
|
776
|
+
valid: false,
|
|
777
|
+
error: "Fingerprint mismatch"
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const toSign = {
|
|
781
|
+
data: signed.data,
|
|
782
|
+
timestamp: signed.timestamp,
|
|
783
|
+
nonce: signed.nonce,
|
|
784
|
+
senderFingerprint: signed.senderFingerprint
|
|
785
|
+
};
|
|
786
|
+
const hash = hashData(toSign);
|
|
787
|
+
const signature = hexToBytes6(signed.signature);
|
|
788
|
+
const valid = await PeerIdentity.verify(signature, hash, publicKey);
|
|
789
|
+
if (!valid) {
|
|
790
|
+
return {
|
|
791
|
+
valid: false,
|
|
792
|
+
error: "Invalid signature"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
valid: true,
|
|
797
|
+
fingerprint: signed.senderFingerprint
|
|
798
|
+
};
|
|
799
|
+
} catch (error) {
|
|
800
|
+
return {
|
|
801
|
+
valid: false,
|
|
802
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function isSignedDataExpired(signed, maxAgeMs = 5 * 60 * 1e3) {
|
|
807
|
+
const timestamp = new Date(signed.timestamp).getTime();
|
|
808
|
+
const now = Date.now();
|
|
809
|
+
return now - timestamp > maxAgeMs;
|
|
810
|
+
}
|
|
811
|
+
function extractSignerFingerprint(signed) {
|
|
812
|
+
try {
|
|
813
|
+
const publicKey = hexToBytes6(signed.senderPublicKey);
|
|
814
|
+
const computed = PeerIdentity.computeFingerprint(publicKey);
|
|
815
|
+
if (computed === signed.senderFingerprint) {
|
|
816
|
+
return signed.senderFingerprint;
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
} catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
var init_signatures = __esm({
|
|
824
|
+
"src/crypto/signatures.ts"() {
|
|
825
|
+
"use strict";
|
|
826
|
+
init_identity();
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// src/crypto/storage.ts
|
|
831
|
+
import {
|
|
832
|
+
createCipheriv,
|
|
833
|
+
createDecipheriv,
|
|
834
|
+
randomBytes as randomBytes4,
|
|
835
|
+
scrypt,
|
|
836
|
+
createHash as createHash2
|
|
837
|
+
} from "crypto";
|
|
838
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
839
|
+
import { dirname as dirname2 } from "path";
|
|
840
|
+
import { existsSync as existsSync4 } from "fs";
|
|
841
|
+
function deriveKey(passphrase, salt) {
|
|
842
|
+
return new Promise((resolve, reject) => {
|
|
843
|
+
const options = {
|
|
844
|
+
N: SCRYPT_N,
|
|
845
|
+
r: SCRYPT_R,
|
|
846
|
+
p: SCRYPT_P
|
|
847
|
+
};
|
|
848
|
+
scrypt(passphrase, salt, KEY_LENGTH, options, (err, derivedKey) => {
|
|
849
|
+
if (err) reject(err);
|
|
850
|
+
else resolve(derivedKey);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function generateSalt() {
|
|
855
|
+
return randomBytes4(SALT_LENGTH);
|
|
856
|
+
}
|
|
857
|
+
function generateIV() {
|
|
858
|
+
return randomBytes4(IV_LENGTH);
|
|
859
|
+
}
|
|
860
|
+
function encrypt(data, key, iv) {
|
|
861
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
862
|
+
const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
863
|
+
const authTag = cipher.getAuthTag();
|
|
864
|
+
return { ciphertext, authTag };
|
|
865
|
+
}
|
|
866
|
+
function decrypt(ciphertext, key, iv, authTag) {
|
|
867
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
868
|
+
decipher.setAuthTag(authTag);
|
|
869
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
870
|
+
}
|
|
871
|
+
async function encryptData(data, passphrase) {
|
|
872
|
+
const dataBytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
873
|
+
const salt = generateSalt();
|
|
874
|
+
const iv = generateIV();
|
|
875
|
+
const key = await deriveKey(passphrase, salt);
|
|
876
|
+
const { ciphertext, authTag } = encrypt(dataBytes, key, iv);
|
|
877
|
+
return {
|
|
878
|
+
version: "2.0",
|
|
879
|
+
encrypted: true,
|
|
880
|
+
algorithm: "aes-256-gcm",
|
|
881
|
+
kdf: "scrypt",
|
|
882
|
+
salt: Buffer.from(salt).toString("hex"),
|
|
883
|
+
iv: Buffer.from(iv).toString("hex"),
|
|
884
|
+
ciphertext: ciphertext.toString("hex"),
|
|
885
|
+
authTag: authTag.toString("hex")
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
async function decryptData(encrypted, passphrase) {
|
|
889
|
+
if (encrypted.version !== "2.0" || !encrypted.encrypted) {
|
|
890
|
+
throw new Error("Invalid encrypted file format");
|
|
891
|
+
}
|
|
892
|
+
const salt = Buffer.from(encrypted.salt, "hex");
|
|
893
|
+
const iv = Buffer.from(encrypted.iv, "hex");
|
|
894
|
+
const ciphertext = Buffer.from(encrypted.ciphertext, "hex");
|
|
895
|
+
const authTag = Buffer.from(encrypted.authTag, "hex");
|
|
896
|
+
const key = await deriveKey(passphrase, salt);
|
|
897
|
+
return decrypt(ciphertext, key, iv, authTag);
|
|
898
|
+
}
|
|
899
|
+
async function encryptObject(obj, passphrase) {
|
|
900
|
+
const json = JSON.stringify(obj);
|
|
901
|
+
return encryptData(json, passphrase);
|
|
902
|
+
}
|
|
903
|
+
async function decryptObject(encrypted, passphrase) {
|
|
904
|
+
const decrypted = await decryptData(encrypted, passphrase);
|
|
905
|
+
return JSON.parse(decrypted.toString("utf-8"));
|
|
906
|
+
}
|
|
907
|
+
async function encryptFile(data, passphrase, outputPath) {
|
|
908
|
+
const dir = dirname2(outputPath);
|
|
909
|
+
if (!existsSync4(dir)) {
|
|
910
|
+
await mkdir4(dir, { recursive: true });
|
|
911
|
+
}
|
|
912
|
+
const encrypted = await encryptObject(data, passphrase);
|
|
913
|
+
await writeFile4(outputPath, JSON.stringify(encrypted, null, 2));
|
|
914
|
+
}
|
|
915
|
+
async function decryptFile(inputPath, passphrase) {
|
|
916
|
+
const content = await readFile4(inputPath, "utf-8");
|
|
917
|
+
const encrypted = JSON.parse(content);
|
|
918
|
+
return decryptObject(encrypted, passphrase);
|
|
919
|
+
}
|
|
920
|
+
function isEncryptedFile(data) {
|
|
921
|
+
if (typeof data !== "object" || data === null) return false;
|
|
922
|
+
const obj = data;
|
|
923
|
+
return obj.version === "2.0" && obj.encrypted === true && obj.algorithm === "aes-256-gcm" && obj.kdf === "scrypt" && typeof obj.salt === "string" && typeof obj.iv === "string" && typeof obj.ciphertext === "string" && typeof obj.authTag === "string";
|
|
924
|
+
}
|
|
925
|
+
function hashPassphrase(passphrase) {
|
|
926
|
+
return createHash2("sha256").update(passphrase).digest("hex").slice(0, 16);
|
|
927
|
+
}
|
|
928
|
+
function generateMachineKey() {
|
|
929
|
+
const hostname = process.env.HOSTNAME || "unknown";
|
|
930
|
+
const user = process.env.USER || process.env.USERNAME || "unknown";
|
|
931
|
+
const combined = `skillkit-mesh-${hostname}-${user}`;
|
|
932
|
+
return createHash2("sha256").update(combined).digest("hex");
|
|
933
|
+
}
|
|
934
|
+
var SCRYPT_N, SCRYPT_R, SCRYPT_P, KEY_LENGTH, IV_LENGTH, SALT_LENGTH;
|
|
935
|
+
var init_storage = __esm({
|
|
936
|
+
"src/crypto/storage.ts"() {
|
|
937
|
+
"use strict";
|
|
938
|
+
SCRYPT_N = 2 ** 14;
|
|
939
|
+
SCRYPT_R = 8;
|
|
940
|
+
SCRYPT_P = 1;
|
|
941
|
+
KEY_LENGTH = 32;
|
|
942
|
+
IV_LENGTH = 12;
|
|
943
|
+
SALT_LENGTH = 32;
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// src/crypto/keystore.ts
|
|
948
|
+
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5, access, unlink } from "fs/promises";
|
|
949
|
+
import { join as join4 } from "path";
|
|
950
|
+
import { homedir as homedir4 } from "os";
|
|
951
|
+
import { existsSync as existsSync5 } from "fs";
|
|
952
|
+
function getKeystore(config) {
|
|
953
|
+
if (!globalKeystore) {
|
|
954
|
+
globalKeystore = new SecureKeystore(config);
|
|
955
|
+
}
|
|
956
|
+
return globalKeystore;
|
|
957
|
+
}
|
|
958
|
+
function resetKeystore() {
|
|
959
|
+
globalKeystore = null;
|
|
960
|
+
}
|
|
961
|
+
var DEFAULT_KEYSTORE_PATH, SecureKeystore, globalKeystore;
|
|
962
|
+
var init_keystore = __esm({
|
|
963
|
+
"src/crypto/keystore.ts"() {
|
|
964
|
+
"use strict";
|
|
965
|
+
init_identity();
|
|
966
|
+
init_storage();
|
|
967
|
+
DEFAULT_KEYSTORE_PATH = join4(homedir4(), ".skillkit", "mesh", "identity");
|
|
968
|
+
SecureKeystore = class {
|
|
969
|
+
path;
|
|
970
|
+
passphrase;
|
|
971
|
+
identity = null;
|
|
972
|
+
keystoreData = null;
|
|
973
|
+
constructor(config = {}) {
|
|
974
|
+
this.path = config.path || DEFAULT_KEYSTORE_PATH;
|
|
975
|
+
if (config.encryptionKey) {
|
|
976
|
+
this.passphrase = config.encryptionKey;
|
|
977
|
+
} else if (config.useMachineKey !== false) {
|
|
978
|
+
this.passphrase = generateMachineKey();
|
|
979
|
+
} else {
|
|
980
|
+
throw new Error("Encryption key or machine key required");
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
get keypairPath() {
|
|
984
|
+
return join4(this.path, "keypair.enc");
|
|
985
|
+
}
|
|
986
|
+
get keystoreDataPath() {
|
|
987
|
+
return join4(this.path, "keystore.json");
|
|
988
|
+
}
|
|
989
|
+
async ensureDirectory() {
|
|
990
|
+
if (!existsSync5(this.path)) {
|
|
991
|
+
await mkdir5(this.path, { recursive: true, mode: 448 });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async loadOrCreateIdentity() {
|
|
995
|
+
if (this.identity) {
|
|
996
|
+
return this.identity;
|
|
997
|
+
}
|
|
998
|
+
await this.ensureDirectory();
|
|
999
|
+
try {
|
|
1000
|
+
await access(this.keypairPath);
|
|
1001
|
+
const content = await readFile5(this.keypairPath, "utf-8");
|
|
1002
|
+
const encrypted = JSON.parse(content);
|
|
1003
|
+
if (isEncryptedFile(encrypted)) {
|
|
1004
|
+
const serialized = await decryptObject(
|
|
1005
|
+
encrypted,
|
|
1006
|
+
this.passphrase
|
|
1007
|
+
);
|
|
1008
|
+
this.identity = PeerIdentity.fromSerialized(serialized);
|
|
1009
|
+
} else {
|
|
1010
|
+
this.identity = PeerIdentity.fromSerialized(encrypted);
|
|
1011
|
+
}
|
|
1012
|
+
} catch {
|
|
1013
|
+
this.identity = await PeerIdentity.generate();
|
|
1014
|
+
await this.saveIdentity();
|
|
1015
|
+
}
|
|
1016
|
+
return this.identity;
|
|
1017
|
+
}
|
|
1018
|
+
async saveIdentity() {
|
|
1019
|
+
if (!this.identity) {
|
|
1020
|
+
throw new Error("No identity to save");
|
|
1021
|
+
}
|
|
1022
|
+
await this.ensureDirectory();
|
|
1023
|
+
const serialized = this.identity.serialize();
|
|
1024
|
+
const encrypted = await encryptObject(serialized, this.passphrase);
|
|
1025
|
+
await writeFile5(this.keypairPath, JSON.stringify(encrypted, null, 2), {
|
|
1026
|
+
mode: 384
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
async getIdentity() {
|
|
1030
|
+
return this.identity;
|
|
1031
|
+
}
|
|
1032
|
+
async hasIdentity() {
|
|
1033
|
+
try {
|
|
1034
|
+
await access(this.keypairPath);
|
|
1035
|
+
return true;
|
|
1036
|
+
} catch {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async deleteIdentity() {
|
|
1041
|
+
try {
|
|
1042
|
+
await unlink(this.keypairPath);
|
|
1043
|
+
this.identity = null;
|
|
1044
|
+
} catch {
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async loadKeystoreData() {
|
|
1048
|
+
if (this.keystoreData) {
|
|
1049
|
+
return this.keystoreData;
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
await access(this.keystoreDataPath);
|
|
1053
|
+
const content = await readFile5(this.keystoreDataPath, "utf-8");
|
|
1054
|
+
this.keystoreData = JSON.parse(content);
|
|
1055
|
+
} catch {
|
|
1056
|
+
this.keystoreData = {
|
|
1057
|
+
version: "1.0",
|
|
1058
|
+
trustedPeers: [],
|
|
1059
|
+
revokedFingerprints: []
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
return this.keystoreData;
|
|
1063
|
+
}
|
|
1064
|
+
async saveKeystoreData() {
|
|
1065
|
+
if (!this.keystoreData) return;
|
|
1066
|
+
await this.ensureDirectory();
|
|
1067
|
+
await writeFile5(
|
|
1068
|
+
this.keystoreDataPath,
|
|
1069
|
+
JSON.stringify(this.keystoreData, null, 2),
|
|
1070
|
+
{ mode: 384 }
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
async addTrustedPeer(fingerprint, publicKey, name) {
|
|
1074
|
+
const data = await this.loadKeystoreData();
|
|
1075
|
+
const existing = data.trustedPeers.findIndex(
|
|
1076
|
+
(p) => p.fingerprint === fingerprint
|
|
1077
|
+
);
|
|
1078
|
+
if (existing >= 0) {
|
|
1079
|
+
data.trustedPeers[existing] = {
|
|
1080
|
+
fingerprint,
|
|
1081
|
+
publicKey,
|
|
1082
|
+
name,
|
|
1083
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1084
|
+
};
|
|
1085
|
+
} else {
|
|
1086
|
+
data.trustedPeers.push({
|
|
1087
|
+
fingerprint,
|
|
1088
|
+
publicKey,
|
|
1089
|
+
name,
|
|
1090
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
const revokedIndex = data.revokedFingerprints.indexOf(fingerprint);
|
|
1094
|
+
if (revokedIndex >= 0) {
|
|
1095
|
+
data.revokedFingerprints.splice(revokedIndex, 1);
|
|
1096
|
+
}
|
|
1097
|
+
await this.saveKeystoreData();
|
|
1098
|
+
}
|
|
1099
|
+
async removeTrustedPeer(fingerprint) {
|
|
1100
|
+
const data = await this.loadKeystoreData();
|
|
1101
|
+
data.trustedPeers = data.trustedPeers.filter(
|
|
1102
|
+
(p) => p.fingerprint !== fingerprint
|
|
1103
|
+
);
|
|
1104
|
+
await this.saveKeystoreData();
|
|
1105
|
+
}
|
|
1106
|
+
async revokePeer(fingerprint) {
|
|
1107
|
+
const data = await this.loadKeystoreData();
|
|
1108
|
+
data.trustedPeers = data.trustedPeers.filter(
|
|
1109
|
+
(p) => p.fingerprint !== fingerprint
|
|
1110
|
+
);
|
|
1111
|
+
if (!data.revokedFingerprints.includes(fingerprint)) {
|
|
1112
|
+
data.revokedFingerprints.push(fingerprint);
|
|
1113
|
+
}
|
|
1114
|
+
await this.saveKeystoreData();
|
|
1115
|
+
}
|
|
1116
|
+
async isRevoked(fingerprint) {
|
|
1117
|
+
const data = await this.loadKeystoreData();
|
|
1118
|
+
return data.revokedFingerprints.includes(fingerprint);
|
|
1119
|
+
}
|
|
1120
|
+
async isTrusted(fingerprint) {
|
|
1121
|
+
const data = await this.loadKeystoreData();
|
|
1122
|
+
return data.trustedPeers.some((p) => p.fingerprint === fingerprint);
|
|
1123
|
+
}
|
|
1124
|
+
async getTrustedPeer(fingerprint) {
|
|
1125
|
+
const data = await this.loadKeystoreData();
|
|
1126
|
+
return data.trustedPeers.find((p) => p.fingerprint === fingerprint) || null;
|
|
1127
|
+
}
|
|
1128
|
+
async getTrustedPeers() {
|
|
1129
|
+
const data = await this.loadKeystoreData();
|
|
1130
|
+
return [...data.trustedPeers];
|
|
1131
|
+
}
|
|
1132
|
+
async getRevokedFingerprints() {
|
|
1133
|
+
const data = await this.loadKeystoreData();
|
|
1134
|
+
return [...data.revokedFingerprints];
|
|
1135
|
+
}
|
|
1136
|
+
async clearRevokedPeers() {
|
|
1137
|
+
const data = await this.loadKeystoreData();
|
|
1138
|
+
data.revokedFingerprints = [];
|
|
1139
|
+
await this.saveKeystoreData();
|
|
1140
|
+
}
|
|
1141
|
+
async exportPublicInfo() {
|
|
1142
|
+
const identity = await this.loadOrCreateIdentity();
|
|
1143
|
+
return {
|
|
1144
|
+
fingerprint: identity.fingerprint,
|
|
1145
|
+
publicKey: identity.publicKeyHex
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
globalKeystore = null;
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// src/crypto/index.ts
|
|
1154
|
+
var crypto_exports = {};
|
|
1155
|
+
__export(crypto_exports, {
|
|
1156
|
+
MessageEncryption: () => MessageEncryption,
|
|
1157
|
+
PeerIdentity: () => PeerIdentity,
|
|
1158
|
+
PublicKeyEncryption: () => PublicKeyEncryption,
|
|
1159
|
+
SecureKeystore: () => SecureKeystore,
|
|
1160
|
+
decryptData: () => decryptData,
|
|
1161
|
+
decryptFile: () => decryptFile,
|
|
1162
|
+
decryptObject: () => decryptObject,
|
|
1163
|
+
deriveKey: () => deriveKey,
|
|
1164
|
+
encryptData: () => encryptData,
|
|
1165
|
+
encryptFile: () => encryptFile,
|
|
1166
|
+
encryptObject: () => encryptObject,
|
|
1167
|
+
extractSignerFingerprint: () => extractSignerFingerprint,
|
|
1168
|
+
generateIV: () => generateIV,
|
|
1169
|
+
generateMachineKey: () => generateMachineKey,
|
|
1170
|
+
generateMessageId: () => generateMessageId,
|
|
1171
|
+
generateNonce: () => generateNonce,
|
|
1172
|
+
generateSalt: () => generateSalt,
|
|
1173
|
+
getKeystore: () => getKeystore,
|
|
1174
|
+
hashPassphrase: () => hashPassphrase,
|
|
1175
|
+
isEncryptedFile: () => isEncryptedFile,
|
|
1176
|
+
isSignedDataExpired: () => isSignedDataExpired,
|
|
1177
|
+
resetKeystore: () => resetKeystore,
|
|
1178
|
+
signData: () => signData,
|
|
1179
|
+
verifySignedData: () => verifySignedData
|
|
1180
|
+
});
|
|
1181
|
+
var init_crypto = __esm({
|
|
1182
|
+
"src/crypto/index.ts"() {
|
|
1183
|
+
"use strict";
|
|
1184
|
+
init_identity();
|
|
1185
|
+
init_encryption();
|
|
1186
|
+
init_signatures();
|
|
1187
|
+
init_keystore();
|
|
1188
|
+
init_storage();
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// src/index.ts
|
|
1193
|
+
init_types();
|
|
1194
|
+
init_config();
|
|
1195
|
+
|
|
1196
|
+
// src/discovery/local.ts
|
|
1197
|
+
init_types();
|
|
1198
|
+
init_hosts_config();
|
|
1199
|
+
import { createSocket } from "dgram";
|
|
1200
|
+
import { networkInterfaces } from "os";
|
|
1201
|
+
var MULTICAST_ADDR = "239.255.255.250";
|
|
1202
|
+
var DISCOVERY_INTERVAL_MS = 3e4;
|
|
1203
|
+
var LocalDiscovery = class {
|
|
1204
|
+
socket = null;
|
|
1205
|
+
announceInterval = null;
|
|
1206
|
+
running = false;
|
|
1207
|
+
options;
|
|
1208
|
+
discoveredHosts = /* @__PURE__ */ new Map();
|
|
1209
|
+
constructor(options = {}) {
|
|
1210
|
+
this.options = {
|
|
1211
|
+
port: options.port ?? DEFAULT_DISCOVERY_PORT,
|
|
1212
|
+
interval: options.interval ?? DISCOVERY_INTERVAL_MS,
|
|
1213
|
+
onDiscover: options.onDiscover ?? (() => {
|
|
1214
|
+
})
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
async start() {
|
|
1218
|
+
if (this.running) return;
|
|
1219
|
+
this.socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
1220
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
1221
|
+
this.handleMessage(msg, rinfo);
|
|
1222
|
+
});
|
|
1223
|
+
this.socket.on("error", (err) => {
|
|
1224
|
+
console.error("Discovery socket error:", err);
|
|
1225
|
+
});
|
|
1226
|
+
await new Promise((resolve, reject) => {
|
|
1227
|
+
this.socket.bind(this.options.port, () => {
|
|
1228
|
+
try {
|
|
1229
|
+
this.socket.addMembership(MULTICAST_ADDR);
|
|
1230
|
+
this.socket.setBroadcast(true);
|
|
1231
|
+
this.socket.setMulticastTTL(1);
|
|
1232
|
+
resolve();
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
this.socket?.close();
|
|
1235
|
+
this.socket = null;
|
|
1236
|
+
reject(err);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
this.running = true;
|
|
1241
|
+
await this.announce();
|
|
1242
|
+
this.announceInterval = setInterval(() => {
|
|
1243
|
+
void this.announce().catch((err) => {
|
|
1244
|
+
console.error("Discovery announce error:", err);
|
|
1245
|
+
});
|
|
1246
|
+
}, this.options.interval);
|
|
1247
|
+
}
|
|
1248
|
+
async stop() {
|
|
1249
|
+
if (!this.running) return;
|
|
1250
|
+
if (this.announceInterval) {
|
|
1251
|
+
clearInterval(this.announceInterval);
|
|
1252
|
+
this.announceInterval = null;
|
|
1253
|
+
}
|
|
1254
|
+
if (this.socket) {
|
|
1255
|
+
try {
|
|
1256
|
+
this.socket.dropMembership(MULTICAST_ADDR);
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
this.socket.close();
|
|
1260
|
+
this.socket = null;
|
|
1261
|
+
}
|
|
1262
|
+
this.running = false;
|
|
1263
|
+
}
|
|
1264
|
+
async announce() {
|
|
1265
|
+
if (!this.socket || !this.running) return;
|
|
1266
|
+
const localConfig = await getLocalHostConfig();
|
|
1267
|
+
const localAddress = getLocalIPAddress();
|
|
1268
|
+
const message = {
|
|
1269
|
+
type: "announce",
|
|
1270
|
+
hostId: localConfig.id,
|
|
1271
|
+
hostName: localConfig.name,
|
|
1272
|
+
address: localAddress,
|
|
1273
|
+
port: localConfig.port,
|
|
1274
|
+
tailscaleIP: localConfig.tailscaleIP,
|
|
1275
|
+
version: MESH_VERSION,
|
|
1276
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1277
|
+
};
|
|
1278
|
+
const buffer = Buffer.from(JSON.stringify(message));
|
|
1279
|
+
this.socket.send(buffer, 0, buffer.length, this.options.port, MULTICAST_ADDR);
|
|
1280
|
+
}
|
|
1281
|
+
async query() {
|
|
1282
|
+
if (!this.socket || !this.running) return;
|
|
1283
|
+
const localConfig = await getLocalHostConfig();
|
|
1284
|
+
const localAddress = getLocalIPAddress();
|
|
1285
|
+
const message = {
|
|
1286
|
+
type: "query",
|
|
1287
|
+
hostId: localConfig.id,
|
|
1288
|
+
hostName: localConfig.name,
|
|
1289
|
+
address: localAddress,
|
|
1290
|
+
port: localConfig.port,
|
|
1291
|
+
version: MESH_VERSION,
|
|
1292
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1293
|
+
};
|
|
1294
|
+
const buffer = Buffer.from(JSON.stringify(message));
|
|
1295
|
+
this.socket.send(buffer, 0, buffer.length, this.options.port, MULTICAST_ADDR);
|
|
1296
|
+
}
|
|
1297
|
+
getDiscoveredHosts() {
|
|
1298
|
+
return Array.from(this.discoveredHosts.values());
|
|
1299
|
+
}
|
|
1300
|
+
isRunning() {
|
|
1301
|
+
return this.running;
|
|
1302
|
+
}
|
|
1303
|
+
async handleMessage(msg, rinfo) {
|
|
1304
|
+
try {
|
|
1305
|
+
const message = JSON.parse(msg.toString());
|
|
1306
|
+
if (!message || message.type !== "announce" && message.type !== "query" && message.type !== "response") return;
|
|
1307
|
+
if (!message.hostId || !message.hostName) return;
|
|
1308
|
+
const port = Number(message.port);
|
|
1309
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return;
|
|
1310
|
+
const localConfig = await getLocalHostConfig();
|
|
1311
|
+
if (message.hostId === localConfig.id) return;
|
|
1312
|
+
const host = {
|
|
1313
|
+
id: message.hostId,
|
|
1314
|
+
name: message.hostName,
|
|
1315
|
+
address: rinfo.address,
|
|
1316
|
+
port,
|
|
1317
|
+
tailscaleIP: message.tailscaleIP,
|
|
1318
|
+
status: "online",
|
|
1319
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1320
|
+
version: message.version
|
|
1321
|
+
};
|
|
1322
|
+
this.discoveredHosts.set(host.id, host);
|
|
1323
|
+
await addKnownHost(host);
|
|
1324
|
+
this.options.onDiscover(host);
|
|
1325
|
+
if (message.type === "query") {
|
|
1326
|
+
await this.announce();
|
|
1327
|
+
}
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
function getLocalIPAddress() {
|
|
1333
|
+
const interfaces = networkInterfaces();
|
|
1334
|
+
for (const name of Object.keys(interfaces)) {
|
|
1335
|
+
const iface = interfaces[name];
|
|
1336
|
+
if (!iface) continue;
|
|
1337
|
+
for (const addr of iface) {
|
|
1338
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
1339
|
+
return addr.address;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return "127.0.0.1";
|
|
1344
|
+
}
|
|
1345
|
+
function getAllLocalIPAddresses() {
|
|
1346
|
+
const addresses = [];
|
|
1347
|
+
const interfaces = networkInterfaces();
|
|
1348
|
+
for (const name of Object.keys(interfaces)) {
|
|
1349
|
+
const iface = interfaces[name];
|
|
1350
|
+
if (!iface) continue;
|
|
1351
|
+
for (const addr of iface) {
|
|
1352
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
1353
|
+
addresses.push(addr.address);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return addresses;
|
|
1358
|
+
}
|
|
1359
|
+
async function discoverOnce(timeout = 5e3) {
|
|
1360
|
+
const discovery = new LocalDiscovery();
|
|
1361
|
+
await discovery.start();
|
|
1362
|
+
await discovery.query();
|
|
1363
|
+
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
1364
|
+
const hosts = discovery.getDiscoveredHosts();
|
|
1365
|
+
await discovery.stop();
|
|
1366
|
+
return hosts;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/discovery/secure-local.ts
|
|
1370
|
+
init_types();
|
|
1371
|
+
init_hosts_config();
|
|
1372
|
+
init_identity();
|
|
1373
|
+
import { createSocket as createSocket2 } from "dgram";
|
|
1374
|
+
import { networkInterfaces as networkInterfaces2 } from "os";
|
|
1375
|
+
|
|
1376
|
+
// src/security/config.ts
|
|
1377
|
+
var SECURITY_PRESETS = {
|
|
1378
|
+
development: {
|
|
1379
|
+
discovery: { mode: "open" },
|
|
1380
|
+
transport: { encryption: "none", tls: "none", requireAuth: false },
|
|
1381
|
+
trust: { autoTrustFirst: true, requireManualApproval: false }
|
|
1382
|
+
},
|
|
1383
|
+
signed: {
|
|
1384
|
+
discovery: { mode: "signed" },
|
|
1385
|
+
transport: { encryption: "optional", tls: "none", requireAuth: false },
|
|
1386
|
+
trust: { autoTrustFirst: true, requireManualApproval: false }
|
|
1387
|
+
},
|
|
1388
|
+
secure: {
|
|
1389
|
+
discovery: { mode: "signed" },
|
|
1390
|
+
transport: { encryption: "required", tls: "self-signed", requireAuth: true },
|
|
1391
|
+
trust: { autoTrustFirst: true, requireManualApproval: false }
|
|
1392
|
+
},
|
|
1393
|
+
strict: {
|
|
1394
|
+
discovery: { mode: "trusted-only" },
|
|
1395
|
+
transport: { encryption: "required", tls: "self-signed", requireAuth: true },
|
|
1396
|
+
trust: { autoTrustFirst: false, requireManualApproval: true }
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
var DEFAULT_SECURITY_CONFIG = {
|
|
1400
|
+
...SECURITY_PRESETS.secure
|
|
1401
|
+
};
|
|
1402
|
+
function getSecurityPreset(preset) {
|
|
1403
|
+
return { ...SECURITY_PRESETS[preset] };
|
|
1404
|
+
}
|
|
1405
|
+
function mergeSecurityConfig(base, overrides) {
|
|
1406
|
+
return {
|
|
1407
|
+
identityPath: overrides.identityPath ?? base.identityPath,
|
|
1408
|
+
discovery: {
|
|
1409
|
+
...base.discovery,
|
|
1410
|
+
...overrides.discovery
|
|
1411
|
+
},
|
|
1412
|
+
transport: {
|
|
1413
|
+
...base.transport,
|
|
1414
|
+
...overrides.transport
|
|
1415
|
+
},
|
|
1416
|
+
trust: {
|
|
1417
|
+
...base.trust,
|
|
1418
|
+
...overrides.trust
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function validateSecurityConfig(config) {
|
|
1423
|
+
const errors = [];
|
|
1424
|
+
const validDiscoveryModes = ["open", "signed", "trusted-only"];
|
|
1425
|
+
if (!validDiscoveryModes.includes(config.discovery.mode)) {
|
|
1426
|
+
errors.push(`Invalid discovery mode: ${config.discovery.mode}`);
|
|
1427
|
+
}
|
|
1428
|
+
const validEncryption = ["none", "optional", "required"];
|
|
1429
|
+
if (!validEncryption.includes(config.transport.encryption)) {
|
|
1430
|
+
errors.push(`Invalid transport encryption: ${config.transport.encryption}`);
|
|
1431
|
+
}
|
|
1432
|
+
const validTLS = ["none", "self-signed", "ca-signed"];
|
|
1433
|
+
if (!validTLS.includes(config.transport.tls)) {
|
|
1434
|
+
errors.push(`Invalid TLS mode: ${config.transport.tls}`);
|
|
1435
|
+
}
|
|
1436
|
+
if (config.transport.encryption === "required" && config.transport.tls === "none") {
|
|
1437
|
+
errors.push("Required encryption needs TLS enabled");
|
|
1438
|
+
}
|
|
1439
|
+
if (config.discovery.mode === "trusted-only" && !config.trust.trustedFingerprints?.length) {
|
|
1440
|
+
if (!config.trust.autoTrustFirst) {
|
|
1441
|
+
errors.push("trusted-only discovery requires trustedFingerprints or autoTrustFirst");
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return errors;
|
|
1445
|
+
}
|
|
1446
|
+
function isSecurityEnabled(config) {
|
|
1447
|
+
return config.discovery.mode !== "open" || config.transport.encryption !== "none" || config.transport.requireAuth;
|
|
1448
|
+
}
|
|
1449
|
+
function describeSecurityLevel(config) {
|
|
1450
|
+
if (config.discovery.mode === "open" && config.transport.encryption === "none" && !config.transport.requireAuth) {
|
|
1451
|
+
return "development (no security)";
|
|
1452
|
+
}
|
|
1453
|
+
if (config.discovery.mode === "trusted-only" && config.transport.encryption === "required" && config.transport.requireAuth) {
|
|
1454
|
+
return "strict (maximum security)";
|
|
1455
|
+
}
|
|
1456
|
+
if (config.discovery.mode === "signed" && config.transport.encryption === "required" && config.transport.requireAuth) {
|
|
1457
|
+
return "secure (recommended)";
|
|
1458
|
+
}
|
|
1459
|
+
if (config.discovery.mode === "signed") {
|
|
1460
|
+
return "signed (partial security)";
|
|
1461
|
+
}
|
|
1462
|
+
return "custom";
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/discovery/secure-local.ts
|
|
1466
|
+
import { hexToBytes as hexToBytes2 } from "@noble/hashes/utils";
|
|
1467
|
+
var MULTICAST_ADDR2 = "239.255.255.250";
|
|
1468
|
+
var DISCOVERY_INTERVAL_MS2 = 3e4;
|
|
1469
|
+
var SecureLocalDiscovery = class {
|
|
1470
|
+
socket = null;
|
|
1471
|
+
announceInterval = null;
|
|
1472
|
+
running = false;
|
|
1473
|
+
options;
|
|
1474
|
+
discoveredHosts = /* @__PURE__ */ new Map();
|
|
1475
|
+
identity = null;
|
|
1476
|
+
keystore = null;
|
|
1477
|
+
securityMode;
|
|
1478
|
+
constructor(options = {}) {
|
|
1479
|
+
this.options = {
|
|
1480
|
+
port: options.port ?? DEFAULT_DISCOVERY_PORT,
|
|
1481
|
+
interval: options.interval ?? DISCOVERY_INTERVAL_MS2,
|
|
1482
|
+
onDiscover: options.onDiscover ?? (() => {
|
|
1483
|
+
}),
|
|
1484
|
+
security: options.security ?? DEFAULT_SECURITY_CONFIG
|
|
1485
|
+
};
|
|
1486
|
+
this.identity = options.identity ?? null;
|
|
1487
|
+
this.keystore = options.keystore ?? null;
|
|
1488
|
+
this.securityMode = this.options.security.discovery.mode;
|
|
1489
|
+
}
|
|
1490
|
+
async initialize() {
|
|
1491
|
+
if (!this.identity && this.keystore) {
|
|
1492
|
+
this.identity = await this.keystore.loadOrCreateIdentity();
|
|
1493
|
+
}
|
|
1494
|
+
if (!this.identity && this.securityMode !== "open") {
|
|
1495
|
+
this.identity = await PeerIdentity.generate();
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
async start() {
|
|
1499
|
+
if (this.running) return;
|
|
1500
|
+
await this.initialize();
|
|
1501
|
+
this.socket = createSocket2({ type: "udp4", reuseAddr: true });
|
|
1502
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
1503
|
+
this.handleMessage(msg, rinfo);
|
|
1504
|
+
});
|
|
1505
|
+
this.socket.on("error", (err) => {
|
|
1506
|
+
console.error("Discovery socket error:", err);
|
|
1507
|
+
});
|
|
1508
|
+
await new Promise((resolve, reject) => {
|
|
1509
|
+
this.socket.bind(this.options.port, () => {
|
|
1510
|
+
try {
|
|
1511
|
+
this.socket.addMembership(MULTICAST_ADDR2);
|
|
1512
|
+
this.socket.setBroadcast(true);
|
|
1513
|
+
this.socket.setMulticastTTL(128);
|
|
1514
|
+
resolve();
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
reject(err);
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
this.running = true;
|
|
1521
|
+
await this.announce();
|
|
1522
|
+
this.announceInterval = setInterval(() => {
|
|
1523
|
+
this.announce();
|
|
1524
|
+
}, this.options.interval);
|
|
1525
|
+
}
|
|
1526
|
+
async stop() {
|
|
1527
|
+
if (!this.running) return;
|
|
1528
|
+
if (this.announceInterval) {
|
|
1529
|
+
clearInterval(this.announceInterval);
|
|
1530
|
+
this.announceInterval = null;
|
|
1531
|
+
}
|
|
1532
|
+
if (this.socket) {
|
|
1533
|
+
try {
|
|
1534
|
+
this.socket.dropMembership(MULTICAST_ADDR2);
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
this.socket.close();
|
|
1538
|
+
this.socket = null;
|
|
1539
|
+
}
|
|
1540
|
+
this.running = false;
|
|
1541
|
+
}
|
|
1542
|
+
async announce() {
|
|
1543
|
+
if (!this.socket || !this.running) return;
|
|
1544
|
+
const localConfig = await getLocalHostConfig();
|
|
1545
|
+
const localAddress = getLocalIPAddress2();
|
|
1546
|
+
const baseMessage = {
|
|
1547
|
+
type: "announce",
|
|
1548
|
+
hostId: localConfig.id,
|
|
1549
|
+
hostName: localConfig.name,
|
|
1550
|
+
address: localAddress,
|
|
1551
|
+
port: localConfig.port,
|
|
1552
|
+
tailscaleIP: localConfig.tailscaleIP,
|
|
1553
|
+
version: MESH_VERSION,
|
|
1554
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1555
|
+
};
|
|
1556
|
+
let message = baseMessage;
|
|
1557
|
+
if (this.identity && this.securityMode !== "open") {
|
|
1558
|
+
const signature = await this.identity.signObject(baseMessage);
|
|
1559
|
+
message = {
|
|
1560
|
+
...baseMessage,
|
|
1561
|
+
signature,
|
|
1562
|
+
publicKey: this.identity.publicKeyHex,
|
|
1563
|
+
fingerprint: this.identity.fingerprint
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
const buffer = Buffer.from(JSON.stringify(message));
|
|
1567
|
+
this.socket.send(buffer, 0, buffer.length, this.options.port, MULTICAST_ADDR2);
|
|
1568
|
+
}
|
|
1569
|
+
async query() {
|
|
1570
|
+
if (!this.socket || !this.running) return;
|
|
1571
|
+
const localConfig = await getLocalHostConfig();
|
|
1572
|
+
const localAddress = getLocalIPAddress2();
|
|
1573
|
+
const baseMessage = {
|
|
1574
|
+
type: "query",
|
|
1575
|
+
hostId: localConfig.id,
|
|
1576
|
+
hostName: localConfig.name,
|
|
1577
|
+
address: localAddress,
|
|
1578
|
+
port: localConfig.port,
|
|
1579
|
+
version: MESH_VERSION,
|
|
1580
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1581
|
+
};
|
|
1582
|
+
let message = baseMessage;
|
|
1583
|
+
if (this.identity && this.securityMode !== "open") {
|
|
1584
|
+
const signature = await this.identity.signObject(baseMessage);
|
|
1585
|
+
message = {
|
|
1586
|
+
...baseMessage,
|
|
1587
|
+
signature,
|
|
1588
|
+
publicKey: this.identity.publicKeyHex,
|
|
1589
|
+
fingerprint: this.identity.fingerprint
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
const buffer = Buffer.from(JSON.stringify(message));
|
|
1593
|
+
this.socket.send(buffer, 0, buffer.length, this.options.port, MULTICAST_ADDR2);
|
|
1594
|
+
}
|
|
1595
|
+
getDiscoveredHosts() {
|
|
1596
|
+
return Array.from(this.discoveredHosts.values());
|
|
1597
|
+
}
|
|
1598
|
+
isRunning() {
|
|
1599
|
+
return this.running;
|
|
1600
|
+
}
|
|
1601
|
+
getFingerprint() {
|
|
1602
|
+
return this.identity?.fingerprint ?? null;
|
|
1603
|
+
}
|
|
1604
|
+
async handleMessage(msg, rinfo) {
|
|
1605
|
+
try {
|
|
1606
|
+
const raw = JSON.parse(msg.toString());
|
|
1607
|
+
const localConfig = await getLocalHostConfig();
|
|
1608
|
+
if (raw.hostId === localConfig.id) return;
|
|
1609
|
+
let message;
|
|
1610
|
+
let fingerprint;
|
|
1611
|
+
if (this.isSignedMessage(raw)) {
|
|
1612
|
+
const signedMsg = raw;
|
|
1613
|
+
fingerprint = signedMsg.fingerprint;
|
|
1614
|
+
if (this.securityMode === "signed" || this.securityMode === "trusted-only") {
|
|
1615
|
+
const isValid = await this.verifySignedMessage(signedMsg);
|
|
1616
|
+
if (!isValid) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (this.securityMode === "trusted-only" && this.keystore) {
|
|
1621
|
+
const isTrusted = await this.keystore.isTrusted(fingerprint);
|
|
1622
|
+
const isRevoked = await this.keystore.isRevoked(fingerprint);
|
|
1623
|
+
if (isRevoked) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (!isTrusted) {
|
|
1627
|
+
if (this.options.security.trust.autoTrustFirst) {
|
|
1628
|
+
await this.keystore.addTrustedPeer(
|
|
1629
|
+
fingerprint,
|
|
1630
|
+
signedMsg.publicKey,
|
|
1631
|
+
signedMsg.hostName
|
|
1632
|
+
);
|
|
1633
|
+
} else {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
message = {
|
|
1639
|
+
type: signedMsg.type,
|
|
1640
|
+
hostId: signedMsg.hostId,
|
|
1641
|
+
hostName: signedMsg.hostName,
|
|
1642
|
+
address: signedMsg.address,
|
|
1643
|
+
port: signedMsg.port,
|
|
1644
|
+
tailscaleIP: signedMsg.tailscaleIP,
|
|
1645
|
+
version: signedMsg.version,
|
|
1646
|
+
timestamp: signedMsg.timestamp
|
|
1647
|
+
};
|
|
1648
|
+
} else {
|
|
1649
|
+
if (this.securityMode !== "open") {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
message = raw;
|
|
1653
|
+
}
|
|
1654
|
+
const host = {
|
|
1655
|
+
id: message.hostId,
|
|
1656
|
+
name: message.hostName,
|
|
1657
|
+
address: message.address || rinfo.address,
|
|
1658
|
+
port: message.port,
|
|
1659
|
+
tailscaleIP: message.tailscaleIP,
|
|
1660
|
+
status: "online",
|
|
1661
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1662
|
+
version: message.version,
|
|
1663
|
+
metadata: fingerprint ? { fingerprint } : void 0
|
|
1664
|
+
};
|
|
1665
|
+
this.discoveredHosts.set(host.id, host);
|
|
1666
|
+
await addKnownHost(host);
|
|
1667
|
+
this.options.onDiscover(host, fingerprint);
|
|
1668
|
+
if (message.type === "query") {
|
|
1669
|
+
await this.announce();
|
|
1670
|
+
}
|
|
1671
|
+
} catch {
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
isSignedMessage(msg) {
|
|
1675
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
1676
|
+
const obj = msg;
|
|
1677
|
+
return typeof obj.signature === "string" && typeof obj.publicKey === "string" && typeof obj.fingerprint === "string";
|
|
1678
|
+
}
|
|
1679
|
+
async verifySignedMessage(msg) {
|
|
1680
|
+
try {
|
|
1681
|
+
const publicKey = hexToBytes2(msg.publicKey);
|
|
1682
|
+
const computedFingerprint = PeerIdentity.computeFingerprint(publicKey);
|
|
1683
|
+
if (computedFingerprint !== msg.fingerprint) {
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
const baseMessage = {
|
|
1687
|
+
type: msg.type,
|
|
1688
|
+
hostId: msg.hostId,
|
|
1689
|
+
hostName: msg.hostName,
|
|
1690
|
+
address: msg.address,
|
|
1691
|
+
port: msg.port,
|
|
1692
|
+
tailscaleIP: msg.tailscaleIP,
|
|
1693
|
+
version: msg.version,
|
|
1694
|
+
timestamp: msg.timestamp
|
|
1695
|
+
};
|
|
1696
|
+
const messageBytes = new TextEncoder().encode(JSON.stringify(baseMessage));
|
|
1697
|
+
const signature = hexToBytes2(msg.signature);
|
|
1698
|
+
return await PeerIdentity.verify(signature, messageBytes, publicKey);
|
|
1699
|
+
} catch {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
function getLocalIPAddress2() {
|
|
1705
|
+
const interfaces = networkInterfaces2();
|
|
1706
|
+
for (const name of Object.keys(interfaces)) {
|
|
1707
|
+
const iface = interfaces[name];
|
|
1708
|
+
if (!iface) continue;
|
|
1709
|
+
for (const addr of iface) {
|
|
1710
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
1711
|
+
return addr.address;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return "127.0.0.1";
|
|
1716
|
+
}
|
|
1717
|
+
async function discoverOnceSecure(timeout = 5e3, options = {}) {
|
|
1718
|
+
const discovery = new SecureLocalDiscovery(options);
|
|
1719
|
+
await discovery.start();
|
|
1720
|
+
await discovery.query();
|
|
1721
|
+
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
1722
|
+
const hosts = discovery.getDiscoveredHosts();
|
|
1723
|
+
await discovery.stop();
|
|
1724
|
+
return hosts;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/discovery/tailscale.ts
|
|
1728
|
+
init_types();
|
|
1729
|
+
import { exec } from "child_process";
|
|
1730
|
+
import { promisify } from "util";
|
|
1731
|
+
var execAsync = promisify(exec);
|
|
1732
|
+
async function getTailscaleStatus() {
|
|
1733
|
+
try {
|
|
1734
|
+
const { stdout } = await execAsync("tailscale status --json");
|
|
1735
|
+
const status = JSON.parse(stdout);
|
|
1736
|
+
const self = status.Self ? {
|
|
1737
|
+
id: status.Self.ID,
|
|
1738
|
+
name: status.Self.HostName,
|
|
1739
|
+
tailscaleIP: status.Self.TailscaleIPs?.[0] || "",
|
|
1740
|
+
hostname: status.Self.HostName,
|
|
1741
|
+
online: status.Self.Online ?? true,
|
|
1742
|
+
os: status.Self.OS
|
|
1743
|
+
} : void 0;
|
|
1744
|
+
const peers = [];
|
|
1745
|
+
if (status.Peer) {
|
|
1746
|
+
for (const [id, peer] of Object.entries(status.Peer)) {
|
|
1747
|
+
peers.push({
|
|
1748
|
+
id,
|
|
1749
|
+
name: peer.HostName,
|
|
1750
|
+
tailscaleIP: peer.TailscaleIPs?.[0] || "",
|
|
1751
|
+
hostname: peer.HostName,
|
|
1752
|
+
online: peer.Online ?? false,
|
|
1753
|
+
os: peer.OS,
|
|
1754
|
+
lastSeen: peer.LastSeen
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return {
|
|
1759
|
+
available: true,
|
|
1760
|
+
self,
|
|
1761
|
+
peers,
|
|
1762
|
+
magicDNSSuffix: status.MagicDNSSuffix
|
|
1763
|
+
};
|
|
1764
|
+
} catch {
|
|
1765
|
+
return {
|
|
1766
|
+
available: false,
|
|
1767
|
+
peers: []
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async function isTailscaleAvailable() {
|
|
1772
|
+
try {
|
|
1773
|
+
await execAsync("tailscale version");
|
|
1774
|
+
return true;
|
|
1775
|
+
} catch {
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
async function getTailscaleIP() {
|
|
1780
|
+
const status = await getTailscaleStatus();
|
|
1781
|
+
return status.self?.tailscaleIP ?? null;
|
|
1782
|
+
}
|
|
1783
|
+
async function discoverTailscaleHosts(skillkitPort) {
|
|
1784
|
+
const status = await getTailscaleStatus();
|
|
1785
|
+
if (!status.available) return [];
|
|
1786
|
+
const hosts = [];
|
|
1787
|
+
for (const peer of status.peers) {
|
|
1788
|
+
if (!peer.online) continue;
|
|
1789
|
+
hosts.push({
|
|
1790
|
+
id: `tailscale-${peer.id}`,
|
|
1791
|
+
name: peer.name,
|
|
1792
|
+
address: peer.tailscaleIP,
|
|
1793
|
+
port: skillkitPort,
|
|
1794
|
+
tailscaleIP: peer.tailscaleIP,
|
|
1795
|
+
status: "unknown",
|
|
1796
|
+
lastSeen: peer.lastSeen ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1797
|
+
version: MESH_VERSION,
|
|
1798
|
+
metadata: {
|
|
1799
|
+
discoveredVia: "tailscale",
|
|
1800
|
+
os: peer.os
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return hosts;
|
|
1805
|
+
}
|
|
1806
|
+
async function resolveTailscaleName(hostname) {
|
|
1807
|
+
const status = await getTailscaleStatus();
|
|
1808
|
+
if (!status.available) return null;
|
|
1809
|
+
const normalizedName = hostname.toLowerCase();
|
|
1810
|
+
for (const peer of status.peers) {
|
|
1811
|
+
if (peer.hostname.toLowerCase() === normalizedName || peer.name.toLowerCase() === normalizedName) {
|
|
1812
|
+
return peer.tailscaleIP;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (status.magicDNSSuffix && hostname.endsWith(status.magicDNSSuffix)) {
|
|
1816
|
+
const shortName = hostname.slice(0, -status.magicDNSSuffix.length - 1);
|
|
1817
|
+
for (const peer of status.peers) {
|
|
1818
|
+
if (peer.hostname.toLowerCase() === shortName.toLowerCase()) {
|
|
1819
|
+
return peer.tailscaleIP;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/index.ts
|
|
1827
|
+
init_peer();
|
|
1828
|
+
|
|
1829
|
+
// src/transport/http.ts
|
|
1830
|
+
init_types();
|
|
1831
|
+
import got2 from "got";
|
|
1832
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1833
|
+
var HttpTransport = class {
|
|
1834
|
+
client;
|
|
1835
|
+
options;
|
|
1836
|
+
constructor(host, options = {}) {
|
|
1837
|
+
this.options = {
|
|
1838
|
+
timeout: options.timeout ?? HEALTH_CHECK_TIMEOUT,
|
|
1839
|
+
retries: options.retries ?? 2,
|
|
1840
|
+
retryDelay: options.retryDelay ?? 1e3,
|
|
1841
|
+
baseUrl: options.baseUrl ?? `http://${host.address}:${host.port}`,
|
|
1842
|
+
headers: options.headers ?? {}
|
|
1843
|
+
};
|
|
1844
|
+
this.client = got2.extend({
|
|
1845
|
+
prefixUrl: this.options.baseUrl,
|
|
1846
|
+
timeout: { request: this.options.timeout },
|
|
1847
|
+
retry: {
|
|
1848
|
+
limit: this.options.retries,
|
|
1849
|
+
calculateDelay: () => this.options.retryDelay
|
|
1850
|
+
},
|
|
1851
|
+
headers: {
|
|
1852
|
+
"Content-Type": "application/json",
|
|
1853
|
+
"X-SkillKit-Transport": "http",
|
|
1854
|
+
...this.options.headers
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
async send(path, payload) {
|
|
1859
|
+
const message = {
|
|
1860
|
+
id: randomUUID2(),
|
|
1861
|
+
type: "request",
|
|
1862
|
+
from: "local",
|
|
1863
|
+
to: path,
|
|
1864
|
+
payload,
|
|
1865
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1866
|
+
};
|
|
1867
|
+
const response = await this.client.post(path, {
|
|
1868
|
+
json: message
|
|
1869
|
+
});
|
|
1870
|
+
return JSON.parse(response.body);
|
|
1871
|
+
}
|
|
1872
|
+
async sendMessage(to, type, payload) {
|
|
1873
|
+
return this.send("message", {
|
|
1874
|
+
to,
|
|
1875
|
+
type,
|
|
1876
|
+
payload
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
async registerPeer(registration) {
|
|
1880
|
+
return this.send("peer/register", registration);
|
|
1881
|
+
}
|
|
1882
|
+
async getPeers() {
|
|
1883
|
+
const response = await this.client.get("peers");
|
|
1884
|
+
return JSON.parse(response.body);
|
|
1885
|
+
}
|
|
1886
|
+
async healthCheck() {
|
|
1887
|
+
try {
|
|
1888
|
+
const response = await this.client.get("health");
|
|
1889
|
+
return response.statusCode === 200;
|
|
1890
|
+
} catch {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
async function sendToHost(host, path, payload, options = {}) {
|
|
1896
|
+
const transport = new HttpTransport(host, options);
|
|
1897
|
+
return transport.send(path, payload);
|
|
1898
|
+
}
|
|
1899
|
+
async function broadcastToHosts(hosts, path, payload, options = {}) {
|
|
1900
|
+
const results = /* @__PURE__ */ new Map();
|
|
1901
|
+
await Promise.all(
|
|
1902
|
+
hosts.map(async (host) => {
|
|
1903
|
+
try {
|
|
1904
|
+
const response = await sendToHost(host, path, payload, options);
|
|
1905
|
+
results.set(host.id, response);
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
results.set(host.id, err);
|
|
1908
|
+
}
|
|
1909
|
+
})
|
|
1910
|
+
);
|
|
1911
|
+
return results;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/transport/websocket.ts
|
|
1915
|
+
init_types();
|
|
1916
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
1917
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1918
|
+
var WebSocketTransport = class {
|
|
1919
|
+
socket = null;
|
|
1920
|
+
options;
|
|
1921
|
+
url;
|
|
1922
|
+
reconnectAttempts = 0;
|
|
1923
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
1924
|
+
connected = false;
|
|
1925
|
+
reconnectTimer = null;
|
|
1926
|
+
manualClose = false;
|
|
1927
|
+
constructor(host, options = {}) {
|
|
1928
|
+
this.url = `ws://${host.address}:${host.port}/ws`;
|
|
1929
|
+
this.options = {
|
|
1930
|
+
timeout: options.timeout ?? 5e3,
|
|
1931
|
+
retries: options.retries ?? 3,
|
|
1932
|
+
retryDelay: options.retryDelay ?? 1e3,
|
|
1933
|
+
reconnect: options.reconnect ?? true,
|
|
1934
|
+
reconnectInterval: options.reconnectInterval ?? 5e3,
|
|
1935
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
async connect() {
|
|
1939
|
+
return new Promise((resolve, reject) => {
|
|
1940
|
+
this.socket = new WebSocket(this.url, {
|
|
1941
|
+
handshakeTimeout: this.options.timeout
|
|
1942
|
+
});
|
|
1943
|
+
const timeout = setTimeout(() => {
|
|
1944
|
+
this.socket?.close();
|
|
1945
|
+
reject(new Error("Connection timeout"));
|
|
1946
|
+
}, this.options.timeout);
|
|
1947
|
+
this.socket.on("open", () => {
|
|
1948
|
+
clearTimeout(timeout);
|
|
1949
|
+
this.connected = true;
|
|
1950
|
+
this.reconnectAttempts = 0;
|
|
1951
|
+
resolve();
|
|
1952
|
+
});
|
|
1953
|
+
this.socket.on("message", (data) => {
|
|
1954
|
+
try {
|
|
1955
|
+
const message = JSON.parse(data.toString());
|
|
1956
|
+
this.messageHandlers.forEach((handler) => handler(message, this.socket));
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
this.socket.on("close", () => {
|
|
1961
|
+
this.connected = false;
|
|
1962
|
+
if (this.options.reconnect && !this.manualClose) {
|
|
1963
|
+
this.scheduleReconnect();
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
this.socket.on("error", (err) => {
|
|
1967
|
+
clearTimeout(timeout);
|
|
1968
|
+
if (!this.connected) {
|
|
1969
|
+
reject(err);
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
disconnect() {
|
|
1975
|
+
this.manualClose = true;
|
|
1976
|
+
if (this.reconnectTimer) {
|
|
1977
|
+
clearTimeout(this.reconnectTimer);
|
|
1978
|
+
this.reconnectTimer = null;
|
|
1979
|
+
}
|
|
1980
|
+
if (this.socket) {
|
|
1981
|
+
this.socket.close();
|
|
1982
|
+
this.socket = null;
|
|
1983
|
+
}
|
|
1984
|
+
this.connected = false;
|
|
1985
|
+
}
|
|
1986
|
+
async send(message) {
|
|
1987
|
+
if (!this.socket || !this.connected) {
|
|
1988
|
+
throw new Error("Not connected");
|
|
1989
|
+
}
|
|
1990
|
+
const fullMessage = {
|
|
1991
|
+
...message,
|
|
1992
|
+
id: randomUUID3(),
|
|
1993
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1994
|
+
};
|
|
1995
|
+
return new Promise((resolve, reject) => {
|
|
1996
|
+
this.socket.send(JSON.stringify(fullMessage), (err) => {
|
|
1997
|
+
if (err) reject(err);
|
|
1998
|
+
else resolve();
|
|
1999
|
+
});
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
onMessage(handler) {
|
|
2003
|
+
this.messageHandlers.add(handler);
|
|
2004
|
+
return () => this.messageHandlers.delete(handler);
|
|
2005
|
+
}
|
|
2006
|
+
isConnected() {
|
|
2007
|
+
return this.connected;
|
|
2008
|
+
}
|
|
2009
|
+
scheduleReconnect() {
|
|
2010
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
2014
|
+
this.reconnectAttempts++;
|
|
2015
|
+
try {
|
|
2016
|
+
await this.connect();
|
|
2017
|
+
} catch {
|
|
2018
|
+
this.scheduleReconnect();
|
|
2019
|
+
}
|
|
2020
|
+
}, this.options.reconnectInterval);
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
var WebSocketServer2 = class {
|
|
2024
|
+
server = null;
|
|
2025
|
+
clients = /* @__PURE__ */ new Set();
|
|
2026
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
2027
|
+
port;
|
|
2028
|
+
running = false;
|
|
2029
|
+
constructor(port = DEFAULT_PORT) {
|
|
2030
|
+
this.port = port;
|
|
2031
|
+
}
|
|
2032
|
+
async start() {
|
|
2033
|
+
if (this.running) return;
|
|
2034
|
+
return new Promise((resolve, reject) => {
|
|
2035
|
+
this.server = new WebSocketServer({ port: this.port, path: "/ws" });
|
|
2036
|
+
this.server.on("listening", () => {
|
|
2037
|
+
this.running = true;
|
|
2038
|
+
resolve();
|
|
2039
|
+
});
|
|
2040
|
+
this.server.on("error", (err) => {
|
|
2041
|
+
reject(err);
|
|
2042
|
+
});
|
|
2043
|
+
this.server.on("connection", (socket) => {
|
|
2044
|
+
this.clients.add(socket);
|
|
2045
|
+
socket.on("message", (data) => {
|
|
2046
|
+
try {
|
|
2047
|
+
const message = JSON.parse(data.toString());
|
|
2048
|
+
this.messageHandlers.forEach((handler) => handler(message, socket));
|
|
2049
|
+
} catch {
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
socket.on("close", () => {
|
|
2053
|
+
this.clients.delete(socket);
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
stop() {
|
|
2059
|
+
if (!this.running) return;
|
|
2060
|
+
for (const client of this.clients) {
|
|
2061
|
+
client.close();
|
|
2062
|
+
}
|
|
2063
|
+
this.clients.clear();
|
|
2064
|
+
this.server?.close();
|
|
2065
|
+
this.server = null;
|
|
2066
|
+
this.running = false;
|
|
2067
|
+
}
|
|
2068
|
+
broadcast(message) {
|
|
2069
|
+
const fullMessage = {
|
|
2070
|
+
...message,
|
|
2071
|
+
id: randomUUID3(),
|
|
2072
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2073
|
+
};
|
|
2074
|
+
const data = JSON.stringify(fullMessage);
|
|
2075
|
+
for (const client of this.clients) {
|
|
2076
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2077
|
+
client.send(data);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
sendTo(socket, message) {
|
|
2082
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
2083
|
+
const fullMessage = {
|
|
2084
|
+
...message,
|
|
2085
|
+
id: randomUUID3(),
|
|
2086
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2087
|
+
};
|
|
2088
|
+
socket.send(JSON.stringify(fullMessage));
|
|
2089
|
+
}
|
|
2090
|
+
onMessage(handler) {
|
|
2091
|
+
this.messageHandlers.add(handler);
|
|
2092
|
+
return () => this.messageHandlers.delete(handler);
|
|
2093
|
+
}
|
|
2094
|
+
getClientCount() {
|
|
2095
|
+
return this.clients.size;
|
|
2096
|
+
}
|
|
2097
|
+
isRunning() {
|
|
2098
|
+
return this.running;
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
// src/transport/secure-websocket.ts
|
|
2103
|
+
init_types();
|
|
2104
|
+
init_identity();
|
|
2105
|
+
init_encryption();
|
|
2106
|
+
import WebSocket2, { WebSocketServer as WebSocketServer3 } from "ws";
|
|
2107
|
+
import { createServer as createHttpsServer } from "https";
|
|
2108
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2109
|
+
|
|
2110
|
+
// src/security/auth.ts
|
|
2111
|
+
init_identity();
|
|
2112
|
+
import { SignJWT, jwtVerify, importPKCS8, importSPKI } from "jose";
|
|
2113
|
+
import { randomBytes as randomBytes3, createPrivateKey, createPublicKey } from "crypto";
|
|
2114
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes4 } from "@noble/hashes/utils";
|
|
2115
|
+
var DEFAULT_TOKEN_TTL = 24 * 60 * 60;
|
|
2116
|
+
var CHALLENGE_SIZE = 32;
|
|
2117
|
+
var CHALLENGE_EXPIRY_MS = 30 * 1e3;
|
|
2118
|
+
var AuthManager = class {
|
|
2119
|
+
identity;
|
|
2120
|
+
pendingChallenges = /* @__PURE__ */ new Map();
|
|
2121
|
+
constructor(identity) {
|
|
2122
|
+
this.identity = identity;
|
|
2123
|
+
}
|
|
2124
|
+
async getSigningKey() {
|
|
2125
|
+
const privateKeyObj = createPrivateKey({
|
|
2126
|
+
key: Buffer.concat([
|
|
2127
|
+
Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
2128
|
+
Buffer.from(this.identity.privateKey)
|
|
2129
|
+
]),
|
|
2130
|
+
format: "der",
|
|
2131
|
+
type: "pkcs8"
|
|
2132
|
+
});
|
|
2133
|
+
const pem = privateKeyObj.export({ type: "pkcs8", format: "pem" });
|
|
2134
|
+
return importPKCS8(pem, "EdDSA");
|
|
2135
|
+
}
|
|
2136
|
+
async getVerifyingKey(publicKeyHex) {
|
|
2137
|
+
const publicKeyBytes = hexToBytes4(publicKeyHex);
|
|
2138
|
+
const publicKeyObj = createPublicKey({
|
|
2139
|
+
key: Buffer.concat([
|
|
2140
|
+
Buffer.from("302a300506032b6570032100", "hex"),
|
|
2141
|
+
Buffer.from(publicKeyBytes)
|
|
2142
|
+
]),
|
|
2143
|
+
format: "der",
|
|
2144
|
+
type: "spki"
|
|
2145
|
+
});
|
|
2146
|
+
const pem = publicKeyObj.export({ type: "spki", format: "pem" });
|
|
2147
|
+
return importSPKI(pem, "EdDSA");
|
|
2148
|
+
}
|
|
2149
|
+
async createToken(hostId, capabilities = [], ttlSeconds = DEFAULT_TOKEN_TTL) {
|
|
2150
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2151
|
+
const signingKey = await this.getSigningKey();
|
|
2152
|
+
const token = await new SignJWT({
|
|
2153
|
+
hostId,
|
|
2154
|
+
fingerprint: this.identity.fingerprint,
|
|
2155
|
+
publicKey: this.identity.publicKeyHex,
|
|
2156
|
+
capabilities
|
|
2157
|
+
}).setProtectedHeader({ alg: "EdDSA" }).setIssuedAt(now).setExpirationTime(now + ttlSeconds).setIssuer("skillkit-mesh").sign(signingKey);
|
|
2158
|
+
return token;
|
|
2159
|
+
}
|
|
2160
|
+
async verifyToken(token) {
|
|
2161
|
+
try {
|
|
2162
|
+
const parts = token.split(".");
|
|
2163
|
+
if (parts.length !== 3) {
|
|
2164
|
+
return null;
|
|
2165
|
+
}
|
|
2166
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
2167
|
+
if (!payload.publicKey) {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
const computedFingerprint = PeerIdentity.computeFingerprint(hexToBytes4(payload.publicKey));
|
|
2171
|
+
if (computedFingerprint !== payload.fingerprint) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
const verifyingKey = await this.getVerifyingKey(payload.publicKey);
|
|
2175
|
+
const { payload: verified } = await jwtVerify(token, verifyingKey, {
|
|
2176
|
+
issuer: "skillkit-mesh"
|
|
2177
|
+
});
|
|
2178
|
+
return {
|
|
2179
|
+
hostId: verified.hostId,
|
|
2180
|
+
fingerprint: verified.fingerprint,
|
|
2181
|
+
publicKey: verified.publicKey,
|
|
2182
|
+
capabilities: verified.capabilities || [],
|
|
2183
|
+
iat: verified.iat,
|
|
2184
|
+
exp: verified.exp
|
|
2185
|
+
};
|
|
2186
|
+
} catch {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
createChallenge() {
|
|
2191
|
+
const challengeBytes = randomBytes3(CHALLENGE_SIZE);
|
|
2192
|
+
const challenge = bytesToHex3(challengeBytes);
|
|
2193
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2194
|
+
this.pendingChallenges.set(challenge, {
|
|
2195
|
+
expected: challenge,
|
|
2196
|
+
expires: Date.now() + CHALLENGE_EXPIRY_MS
|
|
2197
|
+
});
|
|
2198
|
+
this.cleanupExpiredChallenges();
|
|
2199
|
+
return {
|
|
2200
|
+
challenge,
|
|
2201
|
+
timestamp
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
async respondToChallenge(challengeRequest) {
|
|
2205
|
+
const message = `${challengeRequest.challenge}:${challengeRequest.timestamp}`;
|
|
2206
|
+
const signature = await this.identity.signString(message);
|
|
2207
|
+
return {
|
|
2208
|
+
challenge: challengeRequest.challenge,
|
|
2209
|
+
signature,
|
|
2210
|
+
publicKey: this.identity.publicKeyHex,
|
|
2211
|
+
fingerprint: this.identity.fingerprint,
|
|
2212
|
+
timestamp: challengeRequest.timestamp
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
async verifyChallengeResponse(response) {
|
|
2216
|
+
const pending = this.pendingChallenges.get(response.challenge);
|
|
2217
|
+
if (!pending) {
|
|
2218
|
+
return {
|
|
2219
|
+
authenticated: false,
|
|
2220
|
+
error: "Unknown or expired challenge"
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
if (Date.now() > pending.expires) {
|
|
2224
|
+
this.pendingChallenges.delete(response.challenge);
|
|
2225
|
+
return {
|
|
2226
|
+
authenticated: false,
|
|
2227
|
+
error: "Challenge expired"
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
const publicKey = hexToBytes4(response.publicKey);
|
|
2232
|
+
const computedFingerprint = PeerIdentity.computeFingerprint(publicKey);
|
|
2233
|
+
if (computedFingerprint !== response.fingerprint) {
|
|
2234
|
+
return {
|
|
2235
|
+
authenticated: false,
|
|
2236
|
+
error: "Fingerprint mismatch"
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
const message = `${response.challenge}:${response.timestamp}`;
|
|
2240
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
2241
|
+
const signature = hexToBytes4(response.signature);
|
|
2242
|
+
const valid = await PeerIdentity.verify(signature, messageBytes, publicKey);
|
|
2243
|
+
if (!valid) {
|
|
2244
|
+
return {
|
|
2245
|
+
authenticated: false,
|
|
2246
|
+
error: "Invalid signature"
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
this.pendingChallenges.delete(response.challenge);
|
|
2250
|
+
return {
|
|
2251
|
+
authenticated: true,
|
|
2252
|
+
fingerprint: response.fingerprint,
|
|
2253
|
+
publicKey: response.publicKey
|
|
2254
|
+
};
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
return {
|
|
2257
|
+
authenticated: false,
|
|
2258
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
cleanupExpiredChallenges() {
|
|
2263
|
+
const now = Date.now();
|
|
2264
|
+
for (const [challenge, data] of this.pendingChallenges) {
|
|
2265
|
+
if (now > data.expires) {
|
|
2266
|
+
this.pendingChallenges.delete(challenge);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async createSignedMessage(payload) {
|
|
2271
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2272
|
+
const toSign = JSON.stringify({ payload, timestamp });
|
|
2273
|
+
const signature = await this.identity.signString(toSign);
|
|
2274
|
+
return {
|
|
2275
|
+
payload,
|
|
2276
|
+
signature,
|
|
2277
|
+
fingerprint: this.identity.fingerprint,
|
|
2278
|
+
timestamp
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
async verifySignedMessage(message) {
|
|
2282
|
+
try {
|
|
2283
|
+
if (!message.publicKey) {
|
|
2284
|
+
return { valid: false, error: "Missing public key" };
|
|
2285
|
+
}
|
|
2286
|
+
const publicKey = hexToBytes4(message.publicKey);
|
|
2287
|
+
const computedFingerprint = PeerIdentity.computeFingerprint(publicKey);
|
|
2288
|
+
if (computedFingerprint !== message.fingerprint) {
|
|
2289
|
+
return { valid: false, error: "Fingerprint mismatch" };
|
|
2290
|
+
}
|
|
2291
|
+
const toSign = JSON.stringify({
|
|
2292
|
+
payload: message.payload,
|
|
2293
|
+
timestamp: message.timestamp
|
|
2294
|
+
});
|
|
2295
|
+
const messageBytes = new TextEncoder().encode(toSign);
|
|
2296
|
+
const signature = hexToBytes4(message.signature);
|
|
2297
|
+
const valid = await PeerIdentity.verify(signature, messageBytes, publicKey);
|
|
2298
|
+
if (!valid) {
|
|
2299
|
+
return { valid: false, error: "Invalid signature" };
|
|
2300
|
+
}
|
|
2301
|
+
return { valid: true, fingerprint: message.fingerprint };
|
|
2302
|
+
} catch (error) {
|
|
2303
|
+
return {
|
|
2304
|
+
valid: false,
|
|
2305
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
get fingerprint() {
|
|
2310
|
+
return this.identity.fingerprint;
|
|
2311
|
+
}
|
|
2312
|
+
get publicKey() {
|
|
2313
|
+
return this.identity.publicKeyHex;
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
function extractBearerToken(authHeader) {
|
|
2317
|
+
if (!authHeader) return null;
|
|
2318
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
2319
|
+
return match ? match[1] : null;
|
|
2320
|
+
}
|
|
2321
|
+
function createBearerHeader(token) {
|
|
2322
|
+
return `Bearer ${token}`;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/security/tls.ts
|
|
2326
|
+
import {
|
|
2327
|
+
generateKeyPairSync,
|
|
2328
|
+
createHash
|
|
2329
|
+
} from "crypto";
|
|
2330
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
2331
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2332
|
+
import { join as join3 } from "path";
|
|
2333
|
+
import { homedir as homedir3 } from "os";
|
|
2334
|
+
var DEFAULT_CERT_PATH = join3(homedir3(), ".skillkit", "mesh", "certs");
|
|
2335
|
+
function generateSelfSignedCertificate(hostId, hostName, validDays = 365) {
|
|
2336
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
2337
|
+
modulusLength: 2048,
|
|
2338
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
2339
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
2340
|
+
});
|
|
2341
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
2342
|
+
const notAfter = /* @__PURE__ */ new Date();
|
|
2343
|
+
notAfter.setDate(notAfter.getDate() + validDays);
|
|
2344
|
+
const serialNumber = createHash("sha256").update(hostId + Date.now().toString()).digest("hex").slice(0, 16);
|
|
2345
|
+
const certPem = createSimpleCert({
|
|
2346
|
+
publicKey,
|
|
2347
|
+
privateKey,
|
|
2348
|
+
subject: `CN=${hostName},O=SkillKit Mesh,OU=${hostId}`,
|
|
2349
|
+
issuer: `CN=${hostName},O=SkillKit Mesh,OU=${hostId}`,
|
|
2350
|
+
serialNumber,
|
|
2351
|
+
notBefore,
|
|
2352
|
+
notAfter,
|
|
2353
|
+
altNames: ["localhost", "127.0.0.1", hostName]
|
|
2354
|
+
});
|
|
2355
|
+
return {
|
|
2356
|
+
cert: certPem,
|
|
2357
|
+
key: privateKey
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
function createSimpleCert(params) {
|
|
2361
|
+
const base64Encode = (str) => Buffer.from(str).toString("base64");
|
|
2362
|
+
const formatDate = (date) => {
|
|
2363
|
+
const y = date.getUTCFullYear().toString().slice(-2);
|
|
2364
|
+
const m = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
2365
|
+
const d = date.getUTCDate().toString().padStart(2, "0");
|
|
2366
|
+
const h = date.getUTCHours().toString().padStart(2, "0");
|
|
2367
|
+
const min = date.getUTCMinutes().toString().padStart(2, "0");
|
|
2368
|
+
const s = date.getUTCSeconds().toString().padStart(2, "0");
|
|
2369
|
+
return `${y}${m}${d}${h}${min}${s}Z`;
|
|
2370
|
+
};
|
|
2371
|
+
const certInfo = {
|
|
2372
|
+
version: 3,
|
|
2373
|
+
serialNumber: params.serialNumber,
|
|
2374
|
+
subject: params.subject,
|
|
2375
|
+
issuer: params.issuer,
|
|
2376
|
+
notBefore: formatDate(params.notBefore),
|
|
2377
|
+
notAfter: formatDate(params.notAfter),
|
|
2378
|
+
publicKey: params.publicKey,
|
|
2379
|
+
altNames: params.altNames
|
|
2380
|
+
};
|
|
2381
|
+
const certData = JSON.stringify(certInfo);
|
|
2382
|
+
const certBase64 = base64Encode(certData);
|
|
2383
|
+
const lines = ["-----BEGIN CERTIFICATE-----"];
|
|
2384
|
+
for (let i = 0; i < certBase64.length; i += 64) {
|
|
2385
|
+
lines.push(certBase64.slice(i, i + 64));
|
|
2386
|
+
}
|
|
2387
|
+
lines.push("-----END CERTIFICATE-----");
|
|
2388
|
+
return lines.join("\n");
|
|
2389
|
+
}
|
|
2390
|
+
var TLSManager = class _TLSManager {
|
|
2391
|
+
certPath;
|
|
2392
|
+
constructor(certPath) {
|
|
2393
|
+
this.certPath = certPath || DEFAULT_CERT_PATH;
|
|
2394
|
+
}
|
|
2395
|
+
async ensureDirectory() {
|
|
2396
|
+
if (!existsSync3(this.certPath)) {
|
|
2397
|
+
await mkdir3(this.certPath, { recursive: true, mode: 448 });
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
async generateCertificate(hostId, hostName = "localhost", validDays = 365) {
|
|
2401
|
+
await this.ensureDirectory();
|
|
2402
|
+
const { cert, key } = generateSelfSignedCertificate(
|
|
2403
|
+
hostId,
|
|
2404
|
+
hostName,
|
|
2405
|
+
validDays
|
|
2406
|
+
);
|
|
2407
|
+
const certFile = join3(this.certPath, `${hostId}.crt`);
|
|
2408
|
+
const keyFile = join3(this.certPath, `${hostId}.key`);
|
|
2409
|
+
await writeFile3(certFile, cert, { mode: 420 });
|
|
2410
|
+
await writeFile3(keyFile, key, { mode: 384 });
|
|
2411
|
+
const fingerprint = createHash("sha256").update(cert).digest("hex");
|
|
2412
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
2413
|
+
const notAfter = /* @__PURE__ */ new Date();
|
|
2414
|
+
notAfter.setDate(notAfter.getDate() + validDays);
|
|
2415
|
+
return {
|
|
2416
|
+
cert,
|
|
2417
|
+
key,
|
|
2418
|
+
fingerprint,
|
|
2419
|
+
notBefore,
|
|
2420
|
+
notAfter,
|
|
2421
|
+
subject: `CN=${hostName},O=SkillKit Mesh,OU=${hostId}`
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
async loadCertificate(hostId) {
|
|
2425
|
+
const certFile = join3(this.certPath, `${hostId}.crt`);
|
|
2426
|
+
const keyFile = join3(this.certPath, `${hostId}.key`);
|
|
2427
|
+
if (!existsSync3(certFile) || !existsSync3(keyFile)) {
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
const cert = await readFile3(certFile, "utf-8");
|
|
2431
|
+
const key = await readFile3(keyFile, "utf-8");
|
|
2432
|
+
const fingerprint = createHash("sha256").update(cert).digest("hex");
|
|
2433
|
+
return {
|
|
2434
|
+
cert,
|
|
2435
|
+
key,
|
|
2436
|
+
fingerprint,
|
|
2437
|
+
notBefore: /* @__PURE__ */ new Date(),
|
|
2438
|
+
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
|
|
2439
|
+
subject: hostId
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
async loadOrCreateCertificate(hostId, hostName = "localhost") {
|
|
2443
|
+
const existing = await this.loadCertificate(hostId);
|
|
2444
|
+
if (existing) {
|
|
2445
|
+
return existing;
|
|
2446
|
+
}
|
|
2447
|
+
return this.generateCertificate(hostId, hostName);
|
|
2448
|
+
}
|
|
2449
|
+
async hasCertificate(hostId) {
|
|
2450
|
+
const certFile = join3(this.certPath, `${hostId}.crt`);
|
|
2451
|
+
return existsSync3(certFile);
|
|
2452
|
+
}
|
|
2453
|
+
getCertificatePath(hostId) {
|
|
2454
|
+
return {
|
|
2455
|
+
certPath: join3(this.certPath, `${hostId}.crt`),
|
|
2456
|
+
keyPath: join3(this.certPath, `${hostId}.key`)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
createServerContext(certInfo, options) {
|
|
2460
|
+
const context = {
|
|
2461
|
+
cert: certInfo.cert,
|
|
2462
|
+
key: certInfo.key,
|
|
2463
|
+
requestCert: options?.requestClientCert ?? false,
|
|
2464
|
+
rejectUnauthorized: false
|
|
2465
|
+
};
|
|
2466
|
+
if (options?.trustedCAs?.length) {
|
|
2467
|
+
context.ca = options.trustedCAs;
|
|
2468
|
+
context.rejectUnauthorized = true;
|
|
2469
|
+
}
|
|
2470
|
+
return context;
|
|
2471
|
+
}
|
|
2472
|
+
createClientContext(certInfo, options) {
|
|
2473
|
+
const context = {
|
|
2474
|
+
rejectUnauthorized: false
|
|
2475
|
+
};
|
|
2476
|
+
if (certInfo) {
|
|
2477
|
+
context.cert = certInfo.cert;
|
|
2478
|
+
context.key = certInfo.key;
|
|
2479
|
+
}
|
|
2480
|
+
if (options?.trustedCAs?.length) {
|
|
2481
|
+
context.ca = options.trustedCAs;
|
|
2482
|
+
}
|
|
2483
|
+
return context;
|
|
2484
|
+
}
|
|
2485
|
+
static computeCertFingerprint(cert) {
|
|
2486
|
+
return createHash("sha256").update(cert).digest("hex");
|
|
2487
|
+
}
|
|
2488
|
+
static verifyCertFingerprint(cert, expectedFingerprint) {
|
|
2489
|
+
const actual = _TLSManager.computeCertFingerprint(cert);
|
|
2490
|
+
return actual.toLowerCase() === expectedFingerprint.toLowerCase();
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
var globalTLSManager = null;
|
|
2494
|
+
function getTLSManager(certPath) {
|
|
2495
|
+
if (!globalTLSManager) {
|
|
2496
|
+
globalTLSManager = new TLSManager(certPath);
|
|
2497
|
+
}
|
|
2498
|
+
return globalTLSManager;
|
|
2499
|
+
}
|
|
2500
|
+
function resetTLSManager() {
|
|
2501
|
+
globalTLSManager = null;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/transport/secure-websocket.ts
|
|
2505
|
+
import { hexToBytes as hexToBytes5 } from "@noble/hashes/utils";
|
|
2506
|
+
var SecureWebSocketTransport = class {
|
|
2507
|
+
socket = null;
|
|
2508
|
+
host;
|
|
2509
|
+
options;
|
|
2510
|
+
identity = null;
|
|
2511
|
+
keystore = null;
|
|
2512
|
+
authManager = null;
|
|
2513
|
+
encryption = null;
|
|
2514
|
+
reconnectAttempts = 0;
|
|
2515
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
2516
|
+
connected = false;
|
|
2517
|
+
authenticated = false;
|
|
2518
|
+
reconnectTimer = null;
|
|
2519
|
+
constructor(host, options = {}) {
|
|
2520
|
+
this.host = host;
|
|
2521
|
+
this.options = {
|
|
2522
|
+
timeout: options.timeout ?? 5e3,
|
|
2523
|
+
reconnect: options.reconnect ?? true,
|
|
2524
|
+
reconnectInterval: options.reconnectInterval ?? 5e3,
|
|
2525
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
2526
|
+
security: options.security ?? DEFAULT_SECURITY_CONFIG
|
|
2527
|
+
};
|
|
2528
|
+
this.identity = options.identity ?? null;
|
|
2529
|
+
this.keystore = options.keystore ?? null;
|
|
2530
|
+
}
|
|
2531
|
+
async initialize() {
|
|
2532
|
+
if (!this.identity && this.keystore) {
|
|
2533
|
+
this.identity = await this.keystore.loadOrCreateIdentity();
|
|
2534
|
+
}
|
|
2535
|
+
if (!this.identity) {
|
|
2536
|
+
this.identity = await PeerIdentity.generate();
|
|
2537
|
+
}
|
|
2538
|
+
this.authManager = new AuthManager(this.identity);
|
|
2539
|
+
}
|
|
2540
|
+
getUrl() {
|
|
2541
|
+
const protocol = this.options.security.transport.tls !== "none" ? "wss" : "ws";
|
|
2542
|
+
return `${protocol}://${this.host.address}:${this.host.port}/ws`;
|
|
2543
|
+
}
|
|
2544
|
+
async connect() {
|
|
2545
|
+
if (!this.identity) {
|
|
2546
|
+
await this.initialize();
|
|
2547
|
+
}
|
|
2548
|
+
return new Promise((resolve, reject) => {
|
|
2549
|
+
const url = this.getUrl();
|
|
2550
|
+
const wsOptions = {
|
|
2551
|
+
handshakeTimeout: this.options.timeout,
|
|
2552
|
+
rejectUnauthorized: false
|
|
2553
|
+
};
|
|
2554
|
+
this.socket = new WebSocket2(url, wsOptions);
|
|
2555
|
+
const timeout = setTimeout(() => {
|
|
2556
|
+
this.socket?.close();
|
|
2557
|
+
reject(new Error("Connection timeout"));
|
|
2558
|
+
}, this.options.timeout);
|
|
2559
|
+
this.socket.on("open", async () => {
|
|
2560
|
+
clearTimeout(timeout);
|
|
2561
|
+
this.connected = true;
|
|
2562
|
+
this.reconnectAttempts = 0;
|
|
2563
|
+
if (this.options.security.transport.requireAuth) {
|
|
2564
|
+
try {
|
|
2565
|
+
await this.performClientHandshake();
|
|
2566
|
+
this.authenticated = true;
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
this.socket?.close();
|
|
2569
|
+
reject(new Error(`Authentication failed: ${err}`));
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
} else {
|
|
2573
|
+
this.authenticated = true;
|
|
2574
|
+
}
|
|
2575
|
+
resolve();
|
|
2576
|
+
});
|
|
2577
|
+
this.socket.on("message", async (data) => {
|
|
2578
|
+
try {
|
|
2579
|
+
const raw = JSON.parse(data.toString());
|
|
2580
|
+
if (raw.type === "auth:challenge") {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
let message;
|
|
2584
|
+
let senderFingerprint;
|
|
2585
|
+
if (this.encryption && raw.ciphertext) {
|
|
2586
|
+
const decrypted = this.encryption.decryptToObject({
|
|
2587
|
+
nonce: raw.nonce,
|
|
2588
|
+
ciphertext: raw.ciphertext
|
|
2589
|
+
});
|
|
2590
|
+
message = decrypted;
|
|
2591
|
+
senderFingerprint = raw.senderFingerprint;
|
|
2592
|
+
} else if (raw.signature) {
|
|
2593
|
+
const secure = raw;
|
|
2594
|
+
senderFingerprint = secure.senderFingerprint;
|
|
2595
|
+
message = {
|
|
2596
|
+
id: secure.id,
|
|
2597
|
+
type: secure.type,
|
|
2598
|
+
from: secure.from,
|
|
2599
|
+
to: secure.to,
|
|
2600
|
+
payload: secure.payload,
|
|
2601
|
+
timestamp: secure.timestamp
|
|
2602
|
+
};
|
|
2603
|
+
} else {
|
|
2604
|
+
message = raw;
|
|
2605
|
+
}
|
|
2606
|
+
this.messageHandlers.forEach(
|
|
2607
|
+
(handler) => handler(message, this.socket, senderFingerprint)
|
|
2608
|
+
);
|
|
2609
|
+
} catch {
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
this.socket.on("close", () => {
|
|
2613
|
+
this.connected = false;
|
|
2614
|
+
this.authenticated = false;
|
|
2615
|
+
if (this.options.reconnect) {
|
|
2616
|
+
this.scheduleReconnect();
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
this.socket.on("error", (err) => {
|
|
2620
|
+
clearTimeout(timeout);
|
|
2621
|
+
if (!this.connected) {
|
|
2622
|
+
reject(err);
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
async performClientHandshake() {
|
|
2628
|
+
return new Promise((resolve, reject) => {
|
|
2629
|
+
const handleChallenge = async (data) => {
|
|
2630
|
+
try {
|
|
2631
|
+
const msg = JSON.parse(data.toString());
|
|
2632
|
+
if (msg.type === "auth:challenge") {
|
|
2633
|
+
const challenge = {
|
|
2634
|
+
challenge: msg.challenge,
|
|
2635
|
+
timestamp: msg.timestamp
|
|
2636
|
+
};
|
|
2637
|
+
const response = await this.authManager.respondToChallenge(challenge);
|
|
2638
|
+
this.socket.send(
|
|
2639
|
+
JSON.stringify({
|
|
2640
|
+
type: "auth:response",
|
|
2641
|
+
...response
|
|
2642
|
+
})
|
|
2643
|
+
);
|
|
2644
|
+
} else if (msg.type === "auth:success") {
|
|
2645
|
+
this.socket.off("message", handleChallenge);
|
|
2646
|
+
if (msg.serverPublicKey) {
|
|
2647
|
+
const serverPubKey = hexToBytes5(msg.serverPublicKey);
|
|
2648
|
+
const sharedSecret = this.identity.deriveSharedSecret(serverPubKey);
|
|
2649
|
+
this.encryption = new MessageEncryption(sharedSecret);
|
|
2650
|
+
}
|
|
2651
|
+
resolve();
|
|
2652
|
+
} else if (msg.type === "auth:failed") {
|
|
2653
|
+
this.socket.off("message", handleChallenge);
|
|
2654
|
+
reject(new Error(msg.error || "Authentication failed"));
|
|
2655
|
+
}
|
|
2656
|
+
} catch (err) {
|
|
2657
|
+
reject(err);
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
this.socket.on("message", handleChallenge);
|
|
2661
|
+
setTimeout(() => {
|
|
2662
|
+
this.socket.off("message", handleChallenge);
|
|
2663
|
+
reject(new Error("Authentication timeout"));
|
|
2664
|
+
}, this.options.timeout);
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
disconnect() {
|
|
2668
|
+
if (this.reconnectTimer) {
|
|
2669
|
+
clearTimeout(this.reconnectTimer);
|
|
2670
|
+
this.reconnectTimer = null;
|
|
2671
|
+
}
|
|
2672
|
+
if (this.socket) {
|
|
2673
|
+
this.socket.close();
|
|
2674
|
+
this.socket = null;
|
|
2675
|
+
}
|
|
2676
|
+
this.connected = false;
|
|
2677
|
+
this.authenticated = false;
|
|
2678
|
+
this.encryption = null;
|
|
2679
|
+
}
|
|
2680
|
+
async send(message) {
|
|
2681
|
+
if (!this.socket || !this.connected) {
|
|
2682
|
+
throw new Error("Not connected");
|
|
2683
|
+
}
|
|
2684
|
+
if (!this.authenticated) {
|
|
2685
|
+
throw new Error("Not authenticated");
|
|
2686
|
+
}
|
|
2687
|
+
const fullMessage = {
|
|
2688
|
+
...message,
|
|
2689
|
+
id: randomUUID4(),
|
|
2690
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2691
|
+
};
|
|
2692
|
+
let dataToSend;
|
|
2693
|
+
if (this.encryption && this.options.security.transport.encryption === "required") {
|
|
2694
|
+
const encrypted = this.encryption.encryptObject(fullMessage);
|
|
2695
|
+
dataToSend = JSON.stringify({
|
|
2696
|
+
id: fullMessage.id,
|
|
2697
|
+
senderFingerprint: this.identity.fingerprint,
|
|
2698
|
+
nonce: encrypted.nonce,
|
|
2699
|
+
ciphertext: encrypted.ciphertext,
|
|
2700
|
+
timestamp: fullMessage.timestamp
|
|
2701
|
+
});
|
|
2702
|
+
} else if (this.identity) {
|
|
2703
|
+
const signature = await this.identity.signObject(fullMessage);
|
|
2704
|
+
const secureMessage = {
|
|
2705
|
+
...fullMessage,
|
|
2706
|
+
signature,
|
|
2707
|
+
senderFingerprint: this.identity.fingerprint,
|
|
2708
|
+
senderPublicKey: this.identity.publicKeyHex,
|
|
2709
|
+
nonce: randomUUID4()
|
|
2710
|
+
};
|
|
2711
|
+
dataToSend = JSON.stringify(secureMessage);
|
|
2712
|
+
} else {
|
|
2713
|
+
dataToSend = JSON.stringify(fullMessage);
|
|
2714
|
+
}
|
|
2715
|
+
return new Promise((resolve, reject) => {
|
|
2716
|
+
this.socket.send(dataToSend, (err) => {
|
|
2717
|
+
if (err) reject(err);
|
|
2718
|
+
else resolve();
|
|
2719
|
+
});
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
onMessage(handler) {
|
|
2723
|
+
this.messageHandlers.add(handler);
|
|
2724
|
+
return () => this.messageHandlers.delete(handler);
|
|
2725
|
+
}
|
|
2726
|
+
isConnected() {
|
|
2727
|
+
return this.connected;
|
|
2728
|
+
}
|
|
2729
|
+
isAuthenticated() {
|
|
2730
|
+
return this.authenticated;
|
|
2731
|
+
}
|
|
2732
|
+
getFingerprint() {
|
|
2733
|
+
return this.identity?.fingerprint ?? null;
|
|
2734
|
+
}
|
|
2735
|
+
scheduleReconnect() {
|
|
2736
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
2740
|
+
this.reconnectAttempts++;
|
|
2741
|
+
try {
|
|
2742
|
+
await this.connect();
|
|
2743
|
+
} catch {
|
|
2744
|
+
this.scheduleReconnect();
|
|
2745
|
+
}
|
|
2746
|
+
}, this.options.reconnectInterval);
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
var SecureWebSocketServer = class {
|
|
2750
|
+
wss = null;
|
|
2751
|
+
httpsServer = null;
|
|
2752
|
+
clients = /* @__PURE__ */ new Map();
|
|
2753
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
2754
|
+
port;
|
|
2755
|
+
running = false;
|
|
2756
|
+
identity = null;
|
|
2757
|
+
keystore = null;
|
|
2758
|
+
authManager = null;
|
|
2759
|
+
tlsManager = null;
|
|
2760
|
+
certInfo = null;
|
|
2761
|
+
security;
|
|
2762
|
+
hostId;
|
|
2763
|
+
constructor(port = DEFAULT_PORT, options = {}) {
|
|
2764
|
+
this.port = port;
|
|
2765
|
+
this.security = options.security ?? DEFAULT_SECURITY_CONFIG;
|
|
2766
|
+
this.identity = options.identity ?? null;
|
|
2767
|
+
this.keystore = options.keystore ?? null;
|
|
2768
|
+
this.hostId = options.hostId ?? randomUUID4();
|
|
2769
|
+
}
|
|
2770
|
+
async initialize() {
|
|
2771
|
+
if (!this.identity && this.keystore) {
|
|
2772
|
+
this.identity = await this.keystore.loadOrCreateIdentity();
|
|
2773
|
+
}
|
|
2774
|
+
if (!this.identity) {
|
|
2775
|
+
this.identity = await PeerIdentity.generate();
|
|
2776
|
+
}
|
|
2777
|
+
this.authManager = new AuthManager(this.identity);
|
|
2778
|
+
if (this.security.transport.tls !== "none") {
|
|
2779
|
+
this.tlsManager = new TLSManager();
|
|
2780
|
+
this.certInfo = await this.tlsManager.loadOrCreateCertificate(
|
|
2781
|
+
this.hostId,
|
|
2782
|
+
"localhost"
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
async start() {
|
|
2787
|
+
if (this.running) return;
|
|
2788
|
+
await this.initialize();
|
|
2789
|
+
return new Promise((resolve, reject) => {
|
|
2790
|
+
if (this.security.transport.tls !== "none" && this.certInfo) {
|
|
2791
|
+
this.httpsServer = createHttpsServer({
|
|
2792
|
+
cert: this.certInfo.cert,
|
|
2793
|
+
key: this.certInfo.key
|
|
2794
|
+
});
|
|
2795
|
+
this.wss = new WebSocketServer3({
|
|
2796
|
+
server: this.httpsServer,
|
|
2797
|
+
path: "/ws"
|
|
2798
|
+
});
|
|
2799
|
+
this.httpsServer.listen(this.port, () => {
|
|
2800
|
+
this.running = true;
|
|
2801
|
+
resolve();
|
|
2802
|
+
});
|
|
2803
|
+
this.httpsServer.on("error", reject);
|
|
2804
|
+
} else {
|
|
2805
|
+
this.wss = new WebSocketServer3({ port: this.port, path: "/ws" });
|
|
2806
|
+
this.wss.on("listening", () => {
|
|
2807
|
+
this.running = true;
|
|
2808
|
+
resolve();
|
|
2809
|
+
});
|
|
2810
|
+
this.wss.on("error", reject);
|
|
2811
|
+
}
|
|
2812
|
+
this.wss.on("connection", (socket) => {
|
|
2813
|
+
this.handleConnection(socket);
|
|
2814
|
+
});
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
async handleConnection(socket) {
|
|
2818
|
+
if (this.security.transport.requireAuth) {
|
|
2819
|
+
try {
|
|
2820
|
+
const client = await this.performServerHandshake(socket);
|
|
2821
|
+
this.clients.set(socket, client);
|
|
2822
|
+
} catch {
|
|
2823
|
+
socket.close();
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
} else {
|
|
2827
|
+
this.clients.set(socket, {
|
|
2828
|
+
socket,
|
|
2829
|
+
fingerprint: "anonymous",
|
|
2830
|
+
publicKey: ""
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
socket.on("message", async (data) => {
|
|
2834
|
+
try {
|
|
2835
|
+
const raw = JSON.parse(data.toString());
|
|
2836
|
+
if (raw.type === "auth:response") {
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
const client = this.clients.get(socket);
|
|
2840
|
+
if (!client) return;
|
|
2841
|
+
let message;
|
|
2842
|
+
let senderFingerprint = client.fingerprint;
|
|
2843
|
+
if (client.encryption && raw.ciphertext) {
|
|
2844
|
+
const decrypted = client.encryption.decryptToObject({
|
|
2845
|
+
nonce: raw.nonce,
|
|
2846
|
+
ciphertext: raw.ciphertext
|
|
2847
|
+
});
|
|
2848
|
+
message = decrypted;
|
|
2849
|
+
} else if (raw.signature) {
|
|
2850
|
+
const secure = raw;
|
|
2851
|
+
senderFingerprint = secure.senderFingerprint;
|
|
2852
|
+
message = {
|
|
2853
|
+
id: secure.id,
|
|
2854
|
+
type: secure.type,
|
|
2855
|
+
from: secure.from,
|
|
2856
|
+
to: secure.to,
|
|
2857
|
+
payload: secure.payload,
|
|
2858
|
+
timestamp: secure.timestamp
|
|
2859
|
+
};
|
|
2860
|
+
} else {
|
|
2861
|
+
message = raw;
|
|
2862
|
+
}
|
|
2863
|
+
this.messageHandlers.forEach(
|
|
2864
|
+
(handler) => handler(message, socket, senderFingerprint)
|
|
2865
|
+
);
|
|
2866
|
+
} catch {
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
socket.on("close", () => {
|
|
2870
|
+
this.clients.delete(socket);
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
async performServerHandshake(socket) {
|
|
2874
|
+
return new Promise((resolve, reject) => {
|
|
2875
|
+
const challenge = this.authManager.createChallenge();
|
|
2876
|
+
socket.send(
|
|
2877
|
+
JSON.stringify({
|
|
2878
|
+
type: "auth:challenge",
|
|
2879
|
+
...challenge
|
|
2880
|
+
})
|
|
2881
|
+
);
|
|
2882
|
+
const timeout = setTimeout(() => {
|
|
2883
|
+
socket.off("message", handleResponse);
|
|
2884
|
+
reject(new Error("Handshake timeout"));
|
|
2885
|
+
}, 1e4);
|
|
2886
|
+
const handleResponse = async (data) => {
|
|
2887
|
+
try {
|
|
2888
|
+
const msg = JSON.parse(data.toString());
|
|
2889
|
+
if (msg.type === "auth:response") {
|
|
2890
|
+
clearTimeout(timeout);
|
|
2891
|
+
socket.off("message", handleResponse);
|
|
2892
|
+
const response = {
|
|
2893
|
+
challenge: msg.challenge,
|
|
2894
|
+
signature: msg.signature,
|
|
2895
|
+
publicKey: msg.publicKey,
|
|
2896
|
+
fingerprint: msg.fingerprint,
|
|
2897
|
+
timestamp: msg.timestamp
|
|
2898
|
+
};
|
|
2899
|
+
const result = await this.authManager.verifyChallengeResponse(response);
|
|
2900
|
+
if (!result.authenticated) {
|
|
2901
|
+
socket.send(
|
|
2902
|
+
JSON.stringify({
|
|
2903
|
+
type: "auth:failed",
|
|
2904
|
+
error: result.error
|
|
2905
|
+
})
|
|
2906
|
+
);
|
|
2907
|
+
reject(new Error(result.error));
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (this.keystore) {
|
|
2911
|
+
const isRevoked = await this.keystore.isRevoked(result.fingerprint);
|
|
2912
|
+
if (isRevoked) {
|
|
2913
|
+
socket.send(
|
|
2914
|
+
JSON.stringify({
|
|
2915
|
+
type: "auth:failed",
|
|
2916
|
+
error: "Peer is revoked"
|
|
2917
|
+
})
|
|
2918
|
+
);
|
|
2919
|
+
reject(new Error("Peer is revoked"));
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
let encryption;
|
|
2924
|
+
if (this.security.transport.encryption === "required") {
|
|
2925
|
+
const clientPubKey = hexToBytes5(response.publicKey);
|
|
2926
|
+
const sharedSecret = this.identity.deriveSharedSecret(clientPubKey);
|
|
2927
|
+
encryption = new MessageEncryption(sharedSecret);
|
|
2928
|
+
}
|
|
2929
|
+
socket.send(
|
|
2930
|
+
JSON.stringify({
|
|
2931
|
+
type: "auth:success",
|
|
2932
|
+
serverFingerprint: this.identity.fingerprint,
|
|
2933
|
+
serverPublicKey: this.identity.publicKeyHex
|
|
2934
|
+
})
|
|
2935
|
+
);
|
|
2936
|
+
resolve({
|
|
2937
|
+
socket,
|
|
2938
|
+
fingerprint: result.fingerprint,
|
|
2939
|
+
publicKey: response.publicKey,
|
|
2940
|
+
encryption
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
} catch (err) {
|
|
2944
|
+
clearTimeout(timeout);
|
|
2945
|
+
reject(err);
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
socket.on("message", handleResponse);
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
stop() {
|
|
2952
|
+
if (!this.running) return;
|
|
2953
|
+
for (const [socket] of this.clients) {
|
|
2954
|
+
socket.close();
|
|
2955
|
+
}
|
|
2956
|
+
this.clients.clear();
|
|
2957
|
+
this.wss?.close();
|
|
2958
|
+
this.httpsServer?.close();
|
|
2959
|
+
this.wss = null;
|
|
2960
|
+
this.httpsServer = null;
|
|
2961
|
+
this.running = false;
|
|
2962
|
+
}
|
|
2963
|
+
async broadcast(message) {
|
|
2964
|
+
const fullMessage = {
|
|
2965
|
+
...message,
|
|
2966
|
+
id: randomUUID4(),
|
|
2967
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2968
|
+
};
|
|
2969
|
+
for (const [socket, client] of this.clients) {
|
|
2970
|
+
if (socket.readyState !== WebSocket2.OPEN) continue;
|
|
2971
|
+
let dataToSend;
|
|
2972
|
+
if (client.encryption) {
|
|
2973
|
+
const encrypted = client.encryption.encryptObject(fullMessage);
|
|
2974
|
+
dataToSend = JSON.stringify({
|
|
2975
|
+
id: fullMessage.id,
|
|
2976
|
+
senderFingerprint: this.identity.fingerprint,
|
|
2977
|
+
nonce: encrypted.nonce,
|
|
2978
|
+
ciphertext: encrypted.ciphertext,
|
|
2979
|
+
timestamp: fullMessage.timestamp
|
|
2980
|
+
});
|
|
2981
|
+
} else if (this.identity) {
|
|
2982
|
+
const signature = await this.identity.signObject(fullMessage);
|
|
2983
|
+
const secureMessage = {
|
|
2984
|
+
...fullMessage,
|
|
2985
|
+
signature,
|
|
2986
|
+
senderFingerprint: this.identity.fingerprint,
|
|
2987
|
+
senderPublicKey: this.identity.publicKeyHex,
|
|
2988
|
+
nonce: randomUUID4()
|
|
2989
|
+
};
|
|
2990
|
+
dataToSend = JSON.stringify(secureMessage);
|
|
2991
|
+
} else {
|
|
2992
|
+
dataToSend = JSON.stringify(fullMessage);
|
|
2993
|
+
}
|
|
2994
|
+
socket.send(dataToSend);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
async sendTo(socket, message) {
|
|
2998
|
+
if (socket.readyState !== WebSocket2.OPEN) return;
|
|
2999
|
+
const client = this.clients.get(socket);
|
|
3000
|
+
if (!client) return;
|
|
3001
|
+
const fullMessage = {
|
|
3002
|
+
...message,
|
|
3003
|
+
id: randomUUID4(),
|
|
3004
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3005
|
+
};
|
|
3006
|
+
let dataToSend;
|
|
3007
|
+
if (client.encryption) {
|
|
3008
|
+
const encrypted = client.encryption.encryptObject(fullMessage);
|
|
3009
|
+
dataToSend = JSON.stringify({
|
|
3010
|
+
id: fullMessage.id,
|
|
3011
|
+
senderFingerprint: this.identity.fingerprint,
|
|
3012
|
+
nonce: encrypted.nonce,
|
|
3013
|
+
ciphertext: encrypted.ciphertext,
|
|
3014
|
+
timestamp: fullMessage.timestamp
|
|
3015
|
+
});
|
|
3016
|
+
} else if (this.identity) {
|
|
3017
|
+
const signature = await this.identity.signObject(fullMessage);
|
|
3018
|
+
const secureMessage = {
|
|
3019
|
+
...fullMessage,
|
|
3020
|
+
signature,
|
|
3021
|
+
senderFingerprint: this.identity.fingerprint,
|
|
3022
|
+
senderPublicKey: this.identity.publicKeyHex,
|
|
3023
|
+
nonce: randomUUID4()
|
|
3024
|
+
};
|
|
3025
|
+
dataToSend = JSON.stringify(secureMessage);
|
|
3026
|
+
} else {
|
|
3027
|
+
dataToSend = JSON.stringify(fullMessage);
|
|
3028
|
+
}
|
|
3029
|
+
socket.send(dataToSend);
|
|
3030
|
+
}
|
|
3031
|
+
onMessage(handler) {
|
|
3032
|
+
this.messageHandlers.add(handler);
|
|
3033
|
+
return () => this.messageHandlers.delete(handler);
|
|
3034
|
+
}
|
|
3035
|
+
getClientCount() {
|
|
3036
|
+
return this.clients.size;
|
|
3037
|
+
}
|
|
3038
|
+
isRunning() {
|
|
3039
|
+
return this.running;
|
|
3040
|
+
}
|
|
3041
|
+
getFingerprint() {
|
|
3042
|
+
return this.identity?.fingerprint ?? null;
|
|
3043
|
+
}
|
|
3044
|
+
getAuthenticatedClients() {
|
|
3045
|
+
return Array.from(this.clients.values()).map((c) => ({
|
|
3046
|
+
fingerprint: c.fingerprint,
|
|
3047
|
+
publicKey: c.publicKey
|
|
3048
|
+
}));
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
|
|
3052
|
+
// src/transport/secure-http.ts
|
|
3053
|
+
init_types();
|
|
3054
|
+
init_identity();
|
|
3055
|
+
import got3 from "got";
|
|
3056
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3057
|
+
var SecureHttpTransport = class {
|
|
3058
|
+
client;
|
|
3059
|
+
options;
|
|
3060
|
+
identity = null;
|
|
3061
|
+
keystore = null;
|
|
3062
|
+
authManager = null;
|
|
3063
|
+
authToken = null;
|
|
3064
|
+
host;
|
|
3065
|
+
security;
|
|
3066
|
+
constructor(host, options = {}) {
|
|
3067
|
+
this.host = host;
|
|
3068
|
+
this.options = {
|
|
3069
|
+
timeout: options.timeout ?? HEALTH_CHECK_TIMEOUT,
|
|
3070
|
+
retries: options.retries ?? 2,
|
|
3071
|
+
retryDelay: options.retryDelay ?? 1e3,
|
|
3072
|
+
headers: options.headers ?? {}
|
|
3073
|
+
};
|
|
3074
|
+
this.identity = options.identity ?? null;
|
|
3075
|
+
this.keystore = options.keystore ?? null;
|
|
3076
|
+
this.authToken = options.authToken ?? null;
|
|
3077
|
+
this.security = options.security ?? DEFAULT_SECURITY_CONFIG;
|
|
3078
|
+
const protocol = this.security.transport.tls !== "none" ? "https" : "http";
|
|
3079
|
+
const baseUrl = `${protocol}://${host.address}:${host.port}`;
|
|
3080
|
+
this.client = got3.extend({
|
|
3081
|
+
prefixUrl: baseUrl,
|
|
3082
|
+
timeout: { request: this.options.timeout },
|
|
3083
|
+
retry: {
|
|
3084
|
+
limit: this.options.retries,
|
|
3085
|
+
calculateDelay: () => this.options.retryDelay
|
|
3086
|
+
},
|
|
3087
|
+
https: {
|
|
3088
|
+
rejectUnauthorized: false
|
|
3089
|
+
},
|
|
3090
|
+
headers: {
|
|
3091
|
+
"Content-Type": "application/json",
|
|
3092
|
+
"X-SkillKit-Transport": "secure-http",
|
|
3093
|
+
...this.options.headers
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
async initialize() {
|
|
3098
|
+
if (!this.identity && this.keystore) {
|
|
3099
|
+
this.identity = await this.keystore.loadOrCreateIdentity();
|
|
3100
|
+
}
|
|
3101
|
+
if (!this.identity) {
|
|
3102
|
+
this.identity = await PeerIdentity.generate();
|
|
3103
|
+
}
|
|
3104
|
+
this.authManager = new AuthManager(this.identity);
|
|
3105
|
+
if (this.security.transport.requireAuth && !this.authToken) {
|
|
3106
|
+
this.authToken = await this.authManager.createToken(this.host.id);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
async getSecureHeaders() {
|
|
3110
|
+
const headers = {};
|
|
3111
|
+
if (this.authToken) {
|
|
3112
|
+
headers["Authorization"] = createBearerHeader(this.authToken);
|
|
3113
|
+
}
|
|
3114
|
+
if (this.identity) {
|
|
3115
|
+
headers["X-SkillKit-Fingerprint"] = this.identity.fingerprint;
|
|
3116
|
+
}
|
|
3117
|
+
return headers;
|
|
3118
|
+
}
|
|
3119
|
+
async send(path, payload) {
|
|
3120
|
+
if (!this.identity) {
|
|
3121
|
+
await this.initialize();
|
|
3122
|
+
}
|
|
3123
|
+
const message = {
|
|
3124
|
+
id: randomUUID5(),
|
|
3125
|
+
type: "request",
|
|
3126
|
+
from: this.identity?.fingerprint ?? "local",
|
|
3127
|
+
to: path,
|
|
3128
|
+
payload,
|
|
3129
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3130
|
+
};
|
|
3131
|
+
let body = message;
|
|
3132
|
+
if (this.identity) {
|
|
3133
|
+
const signature = await this.identity.signObject(message);
|
|
3134
|
+
body = {
|
|
3135
|
+
...message,
|
|
3136
|
+
signature,
|
|
3137
|
+
senderFingerprint: this.identity.fingerprint,
|
|
3138
|
+
senderPublicKey: this.identity.publicKeyHex,
|
|
3139
|
+
nonce: randomUUID5()
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
const secureHeaders = await this.getSecureHeaders();
|
|
3143
|
+
const response = await this.client.post(path, {
|
|
3144
|
+
json: body,
|
|
3145
|
+
headers: secureHeaders
|
|
3146
|
+
});
|
|
3147
|
+
return JSON.parse(response.body);
|
|
3148
|
+
}
|
|
3149
|
+
async sendMessage(to, type, payload) {
|
|
3150
|
+
return this.send("message", {
|
|
3151
|
+
to,
|
|
3152
|
+
type,
|
|
3153
|
+
payload
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
async registerPeer(registration) {
|
|
3157
|
+
return this.send("peer/register", registration);
|
|
3158
|
+
}
|
|
3159
|
+
async getPeers() {
|
|
3160
|
+
if (!this.identity) {
|
|
3161
|
+
await this.initialize();
|
|
3162
|
+
}
|
|
3163
|
+
const secureHeaders = await this.getSecureHeaders();
|
|
3164
|
+
const response = await this.client.get("peers", {
|
|
3165
|
+
headers: secureHeaders
|
|
3166
|
+
});
|
|
3167
|
+
return JSON.parse(response.body);
|
|
3168
|
+
}
|
|
3169
|
+
async healthCheck() {
|
|
3170
|
+
try {
|
|
3171
|
+
const response = await this.client.get("health", {
|
|
3172
|
+
headers: await this.getSecureHeaders()
|
|
3173
|
+
});
|
|
3174
|
+
return response.statusCode === 200;
|
|
3175
|
+
} catch {
|
|
3176
|
+
return false;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
getFingerprint() {
|
|
3180
|
+
return this.identity?.fingerprint ?? null;
|
|
3181
|
+
}
|
|
3182
|
+
setAuthToken(token) {
|
|
3183
|
+
this.authToken = token;
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
async function sendToHostSecure(host, path, payload, options = {}) {
|
|
3187
|
+
const transport = new SecureHttpTransport(host, options);
|
|
3188
|
+
await transport.initialize();
|
|
3189
|
+
return transport.send(path, payload);
|
|
3190
|
+
}
|
|
3191
|
+
async function broadcastToHostsSecure(hosts, path, payload, options = {}) {
|
|
3192
|
+
const results = /* @__PURE__ */ new Map();
|
|
3193
|
+
await Promise.all(
|
|
3194
|
+
hosts.map(async (host) => {
|
|
3195
|
+
try {
|
|
3196
|
+
const response = await sendToHostSecure(host, path, payload, options);
|
|
3197
|
+
results.set(host.id, response);
|
|
3198
|
+
} catch (err) {
|
|
3199
|
+
results.set(host.id, err);
|
|
3200
|
+
}
|
|
3201
|
+
})
|
|
3202
|
+
);
|
|
3203
|
+
return results;
|
|
3204
|
+
}
|
|
3205
|
+
function verifySecureMessage(message) {
|
|
3206
|
+
if (!message.signature || !message.senderPublicKey || !message.senderFingerprint) {
|
|
3207
|
+
return { valid: false, error: "Missing signature fields" };
|
|
3208
|
+
}
|
|
3209
|
+
const computedFingerprint = PeerIdentity.computeFingerprint(
|
|
3210
|
+
Buffer.from(message.senderPublicKey, "hex")
|
|
3211
|
+
);
|
|
3212
|
+
if (computedFingerprint !== message.senderFingerprint) {
|
|
3213
|
+
return { valid: false, error: "Fingerprint mismatch" };
|
|
3214
|
+
}
|
|
3215
|
+
return { valid: true };
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// src/index.ts
|
|
3219
|
+
init_crypto();
|
|
3220
|
+
async function initializeMesh() {
|
|
3221
|
+
const { initializeHostsFile: initializeHostsFile2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3222
|
+
const { getPeerRegistry: getPeerRegistry2 } = await Promise.resolve().then(() => (init_peer(), peer_exports));
|
|
3223
|
+
await initializeHostsFile2();
|
|
3224
|
+
await getPeerRegistry2();
|
|
3225
|
+
}
|
|
3226
|
+
async function initializeSecureMesh(securityConfig) {
|
|
3227
|
+
const { initializeHostsFile: initializeHostsFile2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3228
|
+
const { getPeerRegistry: getPeerRegistry2 } = await Promise.resolve().then(() => (init_peer(), peer_exports));
|
|
3229
|
+
const { SecureKeystore: SecureKeystore2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
3230
|
+
await initializeHostsFile2();
|
|
3231
|
+
await getPeerRegistry2();
|
|
3232
|
+
const keystore = new SecureKeystore2({
|
|
3233
|
+
path: securityConfig?.identityPath
|
|
3234
|
+
});
|
|
3235
|
+
const identity = await keystore.loadOrCreateIdentity();
|
|
3236
|
+
return { identity, keystore };
|
|
3237
|
+
}
|
|
3238
|
+
export {
|
|
3239
|
+
AuthManager,
|
|
3240
|
+
DEFAULT_DISCOVERY_PORT,
|
|
3241
|
+
DEFAULT_PORT,
|
|
3242
|
+
DEFAULT_SECURITY_CONFIG,
|
|
3243
|
+
DISCOVERY_INTERVAL,
|
|
3244
|
+
HEALTH_CHECK_TIMEOUT,
|
|
3245
|
+
HealthMonitor,
|
|
3246
|
+
HttpTransport,
|
|
3247
|
+
LocalDiscovery,
|
|
3248
|
+
MESH_VERSION,
|
|
3249
|
+
MessageEncryption,
|
|
3250
|
+
PeerIdentity,
|
|
3251
|
+
PeerRegistryManager,
|
|
3252
|
+
PublicKeyEncryption,
|
|
3253
|
+
SECURITY_PRESETS,
|
|
3254
|
+
SecureHttpTransport,
|
|
3255
|
+
SecureKeystore,
|
|
3256
|
+
SecureLocalDiscovery,
|
|
3257
|
+
SecureWebSocketServer,
|
|
3258
|
+
SecureWebSocketTransport,
|
|
3259
|
+
TLSManager,
|
|
3260
|
+
WebSocketServer2 as WebSocketServer,
|
|
3261
|
+
WebSocketTransport,
|
|
3262
|
+
addKnownHost,
|
|
3263
|
+
broadcastToHosts,
|
|
3264
|
+
broadcastToHostsSecure,
|
|
3265
|
+
checkAllHostsHealth,
|
|
3266
|
+
checkHostHealth,
|
|
3267
|
+
createBearerHeader,
|
|
3268
|
+
createDefaultHostsFile,
|
|
3269
|
+
decryptData,
|
|
3270
|
+
decryptFile,
|
|
3271
|
+
decryptObject,
|
|
3272
|
+
deriveKey,
|
|
3273
|
+
describeSecurityLevel,
|
|
3274
|
+
discoverOnce,
|
|
3275
|
+
discoverOnceSecure,
|
|
3276
|
+
discoverTailscaleHosts,
|
|
3277
|
+
encryptData,
|
|
3278
|
+
encryptFile,
|
|
3279
|
+
encryptObject,
|
|
3280
|
+
extractBearerToken,
|
|
3281
|
+
extractSignerFingerprint,
|
|
3282
|
+
generateIV,
|
|
3283
|
+
generateMachineKey,
|
|
3284
|
+
generateMessageId,
|
|
3285
|
+
generateNonce,
|
|
3286
|
+
generateSalt,
|
|
3287
|
+
getAllLocalIPAddresses,
|
|
3288
|
+
getHostsFilePath,
|
|
3289
|
+
getKeystore,
|
|
3290
|
+
getKnownHost,
|
|
3291
|
+
getKnownHosts,
|
|
3292
|
+
getLocalHostConfig,
|
|
3293
|
+
getLocalIPAddress,
|
|
3294
|
+
getOfflineHosts,
|
|
3295
|
+
getOnlineHosts,
|
|
3296
|
+
getPeerRegistry,
|
|
3297
|
+
getSecurityPreset,
|
|
3298
|
+
getTLSManager,
|
|
3299
|
+
getTailscaleIP,
|
|
3300
|
+
getTailscaleStatus,
|
|
3301
|
+
hashPassphrase,
|
|
3302
|
+
initializeHostsFile,
|
|
3303
|
+
initializeMesh,
|
|
3304
|
+
initializeSecureMesh,
|
|
3305
|
+
isEncryptedFile,
|
|
3306
|
+
isSecurityEnabled,
|
|
3307
|
+
isSignedDataExpired,
|
|
3308
|
+
isTailscaleAvailable,
|
|
3309
|
+
loadHostsFile,
|
|
3310
|
+
mergeSecurityConfig,
|
|
3311
|
+
removeKnownHost,
|
|
3312
|
+
resetKeystore,
|
|
3313
|
+
resetTLSManager,
|
|
3314
|
+
resolveTailscaleName,
|
|
3315
|
+
saveHostsFile,
|
|
3316
|
+
sendToHost,
|
|
3317
|
+
sendToHostSecure,
|
|
3318
|
+
signData,
|
|
3319
|
+
updateKnownHost,
|
|
3320
|
+
updateLocalHostConfig,
|
|
3321
|
+
validateSecurityConfig,
|
|
3322
|
+
verifySecureMessage,
|
|
3323
|
+
verifySignedData,
|
|
3324
|
+
waitForHost
|
|
3325
|
+
};
|
|
3326
|
+
//# sourceMappingURL=index.js.map
|