@affectively/aeon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/dist/core/index.cjs +4 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +146 -0
- package/dist/core/index.d.ts +146 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/distributed/index.cjs +1874 -0
- package/dist/distributed/index.cjs.map +1 -0
- package/dist/distributed/index.d.cts +2 -0
- package/dist/distributed/index.d.ts +2 -0
- package/dist/distributed/index.js +1869 -0
- package/dist/distributed/index.js.map +1 -0
- package/dist/index-C_4CMV5c.d.cts +1207 -0
- package/dist/index-C_4CMV5c.d.ts +1207 -0
- package/dist/index.cjs +4671 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +812 -0
- package/dist/index.d.ts +812 -0
- package/dist/index.js +4632 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.cjs +64 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +38 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +57 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/versioning/index.cjs +871 -0
- package/dist/versioning/index.cjs.map +1 -0
- package/dist/versioning/index.d.cts +472 -0
- package/dist/versioning/index.d.ts +472 -0
- package/dist/versioning/index.js +866 -0
- package/dist/versioning/index.js.map +1 -0
- package/package.json +142 -0
|
@@ -0,0 +1,1869 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
// src/distributed/SyncCoordinator.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logger.ts
|
|
6
|
+
var consoleLogger = {
|
|
7
|
+
debug: (...args) => {
|
|
8
|
+
console.debug("[AEON:DEBUG]", ...args);
|
|
9
|
+
},
|
|
10
|
+
info: (...args) => {
|
|
11
|
+
console.info("[AEON:INFO]", ...args);
|
|
12
|
+
},
|
|
13
|
+
warn: (...args) => {
|
|
14
|
+
console.warn("[AEON:WARN]", ...args);
|
|
15
|
+
},
|
|
16
|
+
error: (...args) => {
|
|
17
|
+
console.error("[AEON:ERROR]", ...args);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var currentLogger = consoleLogger;
|
|
21
|
+
function getLogger() {
|
|
22
|
+
return currentLogger;
|
|
23
|
+
}
|
|
24
|
+
var logger = {
|
|
25
|
+
debug: (...args) => getLogger().debug(...args),
|
|
26
|
+
info: (...args) => getLogger().info(...args),
|
|
27
|
+
warn: (...args) => getLogger().warn(...args),
|
|
28
|
+
error: (...args) => getLogger().error(...args)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/distributed/SyncCoordinator.ts
|
|
32
|
+
var SyncCoordinator = class extends EventEmitter {
|
|
33
|
+
nodes = /* @__PURE__ */ new Map();
|
|
34
|
+
sessions = /* @__PURE__ */ new Map();
|
|
35
|
+
syncEvents = [];
|
|
36
|
+
nodeHeartbeats = /* @__PURE__ */ new Map();
|
|
37
|
+
heartbeatInterval = null;
|
|
38
|
+
// Crypto support
|
|
39
|
+
cryptoProvider = null;
|
|
40
|
+
nodesByDID = /* @__PURE__ */ new Map();
|
|
41
|
+
// DID -> nodeId
|
|
42
|
+
constructor() {
|
|
43
|
+
super();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Configure cryptographic provider for authenticated sync
|
|
47
|
+
*/
|
|
48
|
+
configureCrypto(provider) {
|
|
49
|
+
this.cryptoProvider = provider;
|
|
50
|
+
logger.debug("[SyncCoordinator] Crypto configured", {
|
|
51
|
+
initialized: provider.isInitialized()
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if crypto is configured
|
|
56
|
+
*/
|
|
57
|
+
isCryptoEnabled() {
|
|
58
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Register a node with DID-based identity
|
|
62
|
+
*/
|
|
63
|
+
async registerAuthenticatedNode(nodeInfo) {
|
|
64
|
+
const node = {
|
|
65
|
+
...nodeInfo
|
|
66
|
+
};
|
|
67
|
+
this.nodes.set(node.id, node);
|
|
68
|
+
this.nodeHeartbeats.set(node.id, Date.now());
|
|
69
|
+
this.nodesByDID.set(nodeInfo.did, node.id);
|
|
70
|
+
if (this.cryptoProvider) {
|
|
71
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
72
|
+
id: node.id,
|
|
73
|
+
did: nodeInfo.did,
|
|
74
|
+
publicSigningKey: nodeInfo.publicSigningKey,
|
|
75
|
+
publicEncryptionKey: nodeInfo.publicEncryptionKey
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const event = {
|
|
79
|
+
type: "node-joined",
|
|
80
|
+
nodeId: node.id,
|
|
81
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
82
|
+
data: { did: nodeInfo.did, authenticated: true }
|
|
83
|
+
};
|
|
84
|
+
this.syncEvents.push(event);
|
|
85
|
+
this.emit("node-joined", node);
|
|
86
|
+
logger.debug("[SyncCoordinator] Authenticated node registered", {
|
|
87
|
+
nodeId: node.id,
|
|
88
|
+
did: nodeInfo.did,
|
|
89
|
+
version: node.version
|
|
90
|
+
});
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get node by DID
|
|
95
|
+
*/
|
|
96
|
+
getNodeByDID(did) {
|
|
97
|
+
const nodeId = this.nodesByDID.get(did);
|
|
98
|
+
if (!nodeId) return void 0;
|
|
99
|
+
return this.nodes.get(nodeId);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get all authenticated nodes (nodes with DIDs)
|
|
103
|
+
*/
|
|
104
|
+
getAuthenticatedNodes() {
|
|
105
|
+
return Array.from(this.nodes.values()).filter((n) => n.did);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create an authenticated sync session with UCAN-based authorization
|
|
109
|
+
*/
|
|
110
|
+
async createAuthenticatedSyncSession(initiatorDID, participantDIDs, options) {
|
|
111
|
+
const initiatorNodeId = this.nodesByDID.get(initiatorDID);
|
|
112
|
+
if (!initiatorNodeId) {
|
|
113
|
+
throw new Error(`Initiator node with DID ${initiatorDID} not found`);
|
|
114
|
+
}
|
|
115
|
+
const participantIds = [];
|
|
116
|
+
for (const did of participantDIDs) {
|
|
117
|
+
const nodeId = this.nodesByDID.get(did);
|
|
118
|
+
if (nodeId) {
|
|
119
|
+
participantIds.push(nodeId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let sessionToken;
|
|
123
|
+
if (this.cryptoProvider && this.cryptoProvider.isInitialized()) {
|
|
124
|
+
const capabilities = (options?.requiredCapabilities || ["aeon:sync:read", "aeon:sync:write"]).map((cap) => ({ can: cap, with: "*" }));
|
|
125
|
+
if (participantDIDs.length > 0) {
|
|
126
|
+
sessionToken = await this.cryptoProvider.createUCAN(
|
|
127
|
+
participantDIDs[0],
|
|
128
|
+
capabilities,
|
|
129
|
+
{ expirationSeconds: 3600 }
|
|
130
|
+
// 1 hour
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const session = {
|
|
135
|
+
id: `sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
136
|
+
initiatorId: initiatorNodeId,
|
|
137
|
+
participantIds,
|
|
138
|
+
status: "pending",
|
|
139
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
140
|
+
itemsSynced: 0,
|
|
141
|
+
itemsFailed: 0,
|
|
142
|
+
conflictsDetected: 0,
|
|
143
|
+
initiatorDID,
|
|
144
|
+
participantDIDs,
|
|
145
|
+
encryptionMode: options?.encryptionMode || "none",
|
|
146
|
+
requiredCapabilities: options?.requiredCapabilities,
|
|
147
|
+
sessionToken
|
|
148
|
+
};
|
|
149
|
+
this.sessions.set(session.id, session);
|
|
150
|
+
const event = {
|
|
151
|
+
type: "sync-started",
|
|
152
|
+
sessionId: session.id,
|
|
153
|
+
nodeId: initiatorNodeId,
|
|
154
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
155
|
+
data: {
|
|
156
|
+
authenticated: true,
|
|
157
|
+
initiatorDID,
|
|
158
|
+
participantCount: participantDIDs.length,
|
|
159
|
+
encryptionMode: session.encryptionMode
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
this.syncEvents.push(event);
|
|
163
|
+
this.emit("sync-started", session);
|
|
164
|
+
logger.debug("[SyncCoordinator] Authenticated sync session created", {
|
|
165
|
+
sessionId: session.id,
|
|
166
|
+
initiatorDID,
|
|
167
|
+
participants: participantDIDs.length,
|
|
168
|
+
encryptionMode: session.encryptionMode
|
|
169
|
+
});
|
|
170
|
+
return session;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Verify a node's UCAN capabilities for a session
|
|
174
|
+
*/
|
|
175
|
+
async verifyNodeCapabilities(sessionId, nodeDID, token) {
|
|
176
|
+
if (!this.cryptoProvider) {
|
|
177
|
+
return { authorized: true };
|
|
178
|
+
}
|
|
179
|
+
const session = this.sessions.get(sessionId);
|
|
180
|
+
if (!session) {
|
|
181
|
+
return { authorized: false, error: `Session ${sessionId} not found` };
|
|
182
|
+
}
|
|
183
|
+
const result = await this.cryptoProvider.verifyUCAN(token, {
|
|
184
|
+
requiredCapabilities: session.requiredCapabilities?.map((cap) => ({
|
|
185
|
+
can: cap,
|
|
186
|
+
with: "*"
|
|
187
|
+
}))
|
|
188
|
+
});
|
|
189
|
+
if (!result.authorized) {
|
|
190
|
+
logger.warn("[SyncCoordinator] Node capability verification failed", {
|
|
191
|
+
sessionId,
|
|
192
|
+
nodeDID,
|
|
193
|
+
error: result.error
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Register a node in the cluster
|
|
200
|
+
*/
|
|
201
|
+
registerNode(node) {
|
|
202
|
+
this.nodes.set(node.id, node);
|
|
203
|
+
this.nodeHeartbeats.set(node.id, Date.now());
|
|
204
|
+
const event = {
|
|
205
|
+
type: "node-joined",
|
|
206
|
+
nodeId: node.id,
|
|
207
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
208
|
+
};
|
|
209
|
+
this.syncEvents.push(event);
|
|
210
|
+
this.emit("node-joined", node);
|
|
211
|
+
logger.debug("[SyncCoordinator] Node registered", {
|
|
212
|
+
nodeId: node.id,
|
|
213
|
+
address: node.address,
|
|
214
|
+
version: node.version
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Deregister a node from the cluster
|
|
219
|
+
*/
|
|
220
|
+
deregisterNode(nodeId) {
|
|
221
|
+
const node = this.nodes.get(nodeId);
|
|
222
|
+
if (!node) {
|
|
223
|
+
throw new Error(`Node ${nodeId} not found`);
|
|
224
|
+
}
|
|
225
|
+
this.nodes.delete(nodeId);
|
|
226
|
+
this.nodeHeartbeats.delete(nodeId);
|
|
227
|
+
const event = {
|
|
228
|
+
type: "node-left",
|
|
229
|
+
nodeId,
|
|
230
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
231
|
+
};
|
|
232
|
+
this.syncEvents.push(event);
|
|
233
|
+
this.emit("node-left", node);
|
|
234
|
+
logger.debug("[SyncCoordinator] Node deregistered", { nodeId });
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Create a new sync session
|
|
238
|
+
*/
|
|
239
|
+
createSyncSession(initiatorId, participantIds) {
|
|
240
|
+
const node = this.nodes.get(initiatorId);
|
|
241
|
+
if (!node) {
|
|
242
|
+
throw new Error(`Initiator node ${initiatorId} not found`);
|
|
243
|
+
}
|
|
244
|
+
const session = {
|
|
245
|
+
id: `sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
246
|
+
initiatorId,
|
|
247
|
+
participantIds,
|
|
248
|
+
status: "pending",
|
|
249
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
+
itemsSynced: 0,
|
|
251
|
+
itemsFailed: 0,
|
|
252
|
+
conflictsDetected: 0
|
|
253
|
+
};
|
|
254
|
+
this.sessions.set(session.id, session);
|
|
255
|
+
const event = {
|
|
256
|
+
type: "sync-started",
|
|
257
|
+
sessionId: session.id,
|
|
258
|
+
nodeId: initiatorId,
|
|
259
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
260
|
+
};
|
|
261
|
+
this.syncEvents.push(event);
|
|
262
|
+
this.emit("sync-started", session);
|
|
263
|
+
logger.debug("[SyncCoordinator] Sync session created", {
|
|
264
|
+
sessionId: session.id,
|
|
265
|
+
initiator: initiatorId,
|
|
266
|
+
participants: participantIds.length
|
|
267
|
+
});
|
|
268
|
+
return session;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Update sync session
|
|
272
|
+
*/
|
|
273
|
+
updateSyncSession(sessionId, updates) {
|
|
274
|
+
const session = this.sessions.get(sessionId);
|
|
275
|
+
if (!session) {
|
|
276
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
277
|
+
}
|
|
278
|
+
Object.assign(session, updates);
|
|
279
|
+
if (updates.status === "completed" || updates.status === "failed") {
|
|
280
|
+
session.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
281
|
+
const event = {
|
|
282
|
+
type: "sync-completed",
|
|
283
|
+
sessionId,
|
|
284
|
+
nodeId: session.initiatorId,
|
|
285
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
286
|
+
data: { status: updates.status, itemsSynced: session.itemsSynced }
|
|
287
|
+
};
|
|
288
|
+
this.syncEvents.push(event);
|
|
289
|
+
this.emit("sync-completed", session);
|
|
290
|
+
}
|
|
291
|
+
logger.debug("[SyncCoordinator] Sync session updated", {
|
|
292
|
+
sessionId,
|
|
293
|
+
status: session.status,
|
|
294
|
+
itemsSynced: session.itemsSynced
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Record a conflict during sync
|
|
299
|
+
*/
|
|
300
|
+
recordConflict(sessionId, nodeId, conflictData) {
|
|
301
|
+
const session = this.sessions.get(sessionId);
|
|
302
|
+
if (session) {
|
|
303
|
+
session.conflictsDetected++;
|
|
304
|
+
const event = {
|
|
305
|
+
type: "conflict-detected",
|
|
306
|
+
sessionId,
|
|
307
|
+
nodeId,
|
|
308
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
309
|
+
data: conflictData
|
|
310
|
+
};
|
|
311
|
+
this.syncEvents.push(event);
|
|
312
|
+
this.emit("conflict-detected", { session, nodeId, conflictData });
|
|
313
|
+
logger.debug("[SyncCoordinator] Conflict recorded", {
|
|
314
|
+
sessionId,
|
|
315
|
+
nodeId,
|
|
316
|
+
totalConflicts: session.conflictsDetected
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Update node status
|
|
322
|
+
*/
|
|
323
|
+
updateNodeStatus(nodeId, status) {
|
|
324
|
+
const node = this.nodes.get(nodeId);
|
|
325
|
+
if (!node) {
|
|
326
|
+
throw new Error(`Node ${nodeId} not found`);
|
|
327
|
+
}
|
|
328
|
+
node.status = status;
|
|
329
|
+
this.nodeHeartbeats.set(nodeId, Date.now());
|
|
330
|
+
logger.debug("[SyncCoordinator] Node status updated", {
|
|
331
|
+
nodeId,
|
|
332
|
+
status
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Record heartbeat from node
|
|
337
|
+
*/
|
|
338
|
+
recordHeartbeat(nodeId) {
|
|
339
|
+
const node = this.nodes.get(nodeId);
|
|
340
|
+
if (!node) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
node.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
344
|
+
this.nodeHeartbeats.set(nodeId, Date.now());
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Get all nodes
|
|
348
|
+
*/
|
|
349
|
+
getNodes() {
|
|
350
|
+
return Array.from(this.nodes.values());
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get node by ID
|
|
354
|
+
*/
|
|
355
|
+
getNode(nodeId) {
|
|
356
|
+
return this.nodes.get(nodeId);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Get online nodes
|
|
360
|
+
*/
|
|
361
|
+
getOnlineNodes() {
|
|
362
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "online");
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get nodes by capability
|
|
366
|
+
*/
|
|
367
|
+
getNodesByCapability(capability) {
|
|
368
|
+
return Array.from(this.nodes.values()).filter(
|
|
369
|
+
(n) => n.capabilities.includes(capability)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get sync session
|
|
374
|
+
*/
|
|
375
|
+
getSyncSession(sessionId) {
|
|
376
|
+
return this.sessions.get(sessionId);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get all sync sessions
|
|
380
|
+
*/
|
|
381
|
+
getAllSyncSessions() {
|
|
382
|
+
return Array.from(this.sessions.values());
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get active sync sessions
|
|
386
|
+
*/
|
|
387
|
+
getActiveSyncSessions() {
|
|
388
|
+
return Array.from(this.sessions.values()).filter((s) => s.status === "active");
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get sessions for a node
|
|
392
|
+
*/
|
|
393
|
+
getSessionsForNode(nodeId) {
|
|
394
|
+
return Array.from(this.sessions.values()).filter(
|
|
395
|
+
(s) => s.initiatorId === nodeId || s.participantIds.includes(nodeId)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get sync statistics
|
|
400
|
+
*/
|
|
401
|
+
getStatistics() {
|
|
402
|
+
const sessions = Array.from(this.sessions.values());
|
|
403
|
+
const completed = sessions.filter((s) => s.status === "completed").length;
|
|
404
|
+
const failed = sessions.filter((s) => s.status === "failed").length;
|
|
405
|
+
const active = sessions.filter((s) => s.status === "active").length;
|
|
406
|
+
const totalItemsSynced = sessions.reduce((sum, s) => sum + s.itemsSynced, 0);
|
|
407
|
+
const totalConflicts = sessions.reduce((sum, s) => sum + s.conflictsDetected, 0);
|
|
408
|
+
return {
|
|
409
|
+
totalNodes: this.nodes.size,
|
|
410
|
+
onlineNodes: this.getOnlineNodes().length,
|
|
411
|
+
offlineNodes: this.nodes.size - this.getOnlineNodes().length,
|
|
412
|
+
totalSessions: sessions.length,
|
|
413
|
+
activeSessions: active,
|
|
414
|
+
completedSessions: completed,
|
|
415
|
+
failedSessions: failed,
|
|
416
|
+
successRate: sessions.length > 0 ? completed / sessions.length * 100 : 0,
|
|
417
|
+
totalItemsSynced,
|
|
418
|
+
totalConflicts,
|
|
419
|
+
averageConflictsPerSession: sessions.length > 0 ? totalConflicts / sessions.length : 0
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Get sync events
|
|
424
|
+
*/
|
|
425
|
+
getSyncEvents(limit) {
|
|
426
|
+
const events = [...this.syncEvents];
|
|
427
|
+
if (limit) {
|
|
428
|
+
return events.slice(-limit);
|
|
429
|
+
}
|
|
430
|
+
return events;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get sync events for session
|
|
434
|
+
*/
|
|
435
|
+
getSessionEvents(sessionId) {
|
|
436
|
+
return this.syncEvents.filter((e) => e.sessionId === sessionId);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Check node health
|
|
440
|
+
*/
|
|
441
|
+
getNodeHealth() {
|
|
442
|
+
const health = {};
|
|
443
|
+
for (const [nodeId, lastHeartbeat] of this.nodeHeartbeats) {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const downtime = now - lastHeartbeat;
|
|
446
|
+
const isHealthy = downtime < 3e4;
|
|
447
|
+
health[nodeId] = {
|
|
448
|
+
isHealthy,
|
|
449
|
+
downtime
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return health;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Start heartbeat monitoring
|
|
456
|
+
*/
|
|
457
|
+
startHeartbeatMonitoring(interval = 5e3) {
|
|
458
|
+
if (this.heartbeatInterval) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
this.heartbeatInterval = setInterval(() => {
|
|
462
|
+
const health = this.getNodeHealth();
|
|
463
|
+
for (const [nodeId, { isHealthy }] of Object.entries(health)) {
|
|
464
|
+
const node = this.nodes.get(nodeId);
|
|
465
|
+
if (!node) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const newStatus = isHealthy ? "online" : "offline";
|
|
469
|
+
if (node.status !== newStatus) {
|
|
470
|
+
this.updateNodeStatus(nodeId, newStatus);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}, interval);
|
|
474
|
+
logger.debug("[SyncCoordinator] Heartbeat monitoring started", { interval });
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Stop heartbeat monitoring
|
|
478
|
+
*/
|
|
479
|
+
stopHeartbeatMonitoring() {
|
|
480
|
+
if (this.heartbeatInterval) {
|
|
481
|
+
clearInterval(this.heartbeatInterval);
|
|
482
|
+
this.heartbeatInterval = null;
|
|
483
|
+
logger.debug("[SyncCoordinator] Heartbeat monitoring stopped");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Clear all state (for testing)
|
|
488
|
+
*/
|
|
489
|
+
clear() {
|
|
490
|
+
this.nodes.clear();
|
|
491
|
+
this.sessions.clear();
|
|
492
|
+
this.syncEvents = [];
|
|
493
|
+
this.nodeHeartbeats.clear();
|
|
494
|
+
this.nodesByDID.clear();
|
|
495
|
+
this.cryptoProvider = null;
|
|
496
|
+
this.stopHeartbeatMonitoring();
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get the crypto provider (for advanced usage)
|
|
500
|
+
*/
|
|
501
|
+
getCryptoProvider() {
|
|
502
|
+
return this.cryptoProvider;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/distributed/ReplicationManager.ts
|
|
507
|
+
var ReplicationManager = class {
|
|
508
|
+
replicas = /* @__PURE__ */ new Map();
|
|
509
|
+
policies = /* @__PURE__ */ new Map();
|
|
510
|
+
replicationEvents = [];
|
|
511
|
+
syncStatus = /* @__PURE__ */ new Map();
|
|
512
|
+
// Crypto support
|
|
513
|
+
cryptoProvider = null;
|
|
514
|
+
replicasByDID = /* @__PURE__ */ new Map();
|
|
515
|
+
// DID -> replicaId
|
|
516
|
+
/**
|
|
517
|
+
* Configure cryptographic provider for encrypted replication
|
|
518
|
+
*/
|
|
519
|
+
configureCrypto(provider) {
|
|
520
|
+
this.cryptoProvider = provider;
|
|
521
|
+
logger.debug("[ReplicationManager] Crypto configured", {
|
|
522
|
+
initialized: provider.isInitialized()
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check if crypto is configured
|
|
527
|
+
*/
|
|
528
|
+
isCryptoEnabled() {
|
|
529
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Register an authenticated replica with DID
|
|
533
|
+
*/
|
|
534
|
+
async registerAuthenticatedReplica(replica, encrypted = false) {
|
|
535
|
+
const authenticatedReplica = {
|
|
536
|
+
...replica,
|
|
537
|
+
encrypted
|
|
538
|
+
};
|
|
539
|
+
this.replicas.set(replica.id, authenticatedReplica);
|
|
540
|
+
this.replicasByDID.set(replica.did, replica.id);
|
|
541
|
+
if (!this.syncStatus.has(replica.nodeId)) {
|
|
542
|
+
this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
|
|
543
|
+
}
|
|
544
|
+
if (this.cryptoProvider && replica.publicSigningKey) {
|
|
545
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
546
|
+
id: replica.nodeId,
|
|
547
|
+
did: replica.did,
|
|
548
|
+
publicSigningKey: replica.publicSigningKey,
|
|
549
|
+
publicEncryptionKey: replica.publicEncryptionKey
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const event = {
|
|
553
|
+
type: "replica-added",
|
|
554
|
+
replicaId: replica.id,
|
|
555
|
+
nodeId: replica.nodeId,
|
|
556
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
557
|
+
details: { did: replica.did, encrypted, authenticated: true }
|
|
558
|
+
};
|
|
559
|
+
this.replicationEvents.push(event);
|
|
560
|
+
logger.debug("[ReplicationManager] Authenticated replica registered", {
|
|
561
|
+
replicaId: replica.id,
|
|
562
|
+
did: replica.did,
|
|
563
|
+
encrypted
|
|
564
|
+
});
|
|
565
|
+
return authenticatedReplica;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get replica by DID
|
|
569
|
+
*/
|
|
570
|
+
getReplicaByDID(did) {
|
|
571
|
+
const replicaId = this.replicasByDID.get(did);
|
|
572
|
+
if (!replicaId) return void 0;
|
|
573
|
+
return this.replicas.get(replicaId);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get all encrypted replicas
|
|
577
|
+
*/
|
|
578
|
+
getEncryptedReplicas() {
|
|
579
|
+
return Array.from(this.replicas.values()).filter((r) => r.encrypted);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Encrypt data for replication to a specific replica
|
|
583
|
+
*/
|
|
584
|
+
async encryptForReplica(data, targetReplicaDID) {
|
|
585
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
586
|
+
throw new Error("Crypto provider not initialized");
|
|
587
|
+
}
|
|
588
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
589
|
+
const encrypted = await this.cryptoProvider.encrypt(dataBytes, targetReplicaDID);
|
|
590
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
591
|
+
return {
|
|
592
|
+
ct: encrypted.ct,
|
|
593
|
+
iv: encrypted.iv,
|
|
594
|
+
tag: encrypted.tag,
|
|
595
|
+
epk: encrypted.epk,
|
|
596
|
+
senderDID: localDID || void 0,
|
|
597
|
+
targetDID: targetReplicaDID,
|
|
598
|
+
encryptedAt: encrypted.encryptedAt
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Decrypt data received from replication
|
|
603
|
+
*/
|
|
604
|
+
async decryptReplicationData(encrypted) {
|
|
605
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
606
|
+
throw new Error("Crypto provider not initialized");
|
|
607
|
+
}
|
|
608
|
+
const decrypted = await this.cryptoProvider.decrypt(
|
|
609
|
+
{
|
|
610
|
+
alg: "ECIES-P256",
|
|
611
|
+
ct: encrypted.ct,
|
|
612
|
+
iv: encrypted.iv,
|
|
613
|
+
tag: encrypted.tag,
|
|
614
|
+
epk: encrypted.epk
|
|
615
|
+
},
|
|
616
|
+
encrypted.senderDID
|
|
617
|
+
);
|
|
618
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Create an encrypted replication policy
|
|
622
|
+
*/
|
|
623
|
+
createEncryptedPolicy(name, replicationFactor, consistencyLevel, encryptionMode, options) {
|
|
624
|
+
const policy = {
|
|
625
|
+
id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
626
|
+
name,
|
|
627
|
+
replicationFactor,
|
|
628
|
+
consistencyLevel,
|
|
629
|
+
syncInterval: options?.syncInterval || 1e3,
|
|
630
|
+
maxReplicationLag: options?.maxReplicationLag || 1e4,
|
|
631
|
+
encryptionMode,
|
|
632
|
+
requiredCapabilities: options?.requiredCapabilities
|
|
633
|
+
};
|
|
634
|
+
this.policies.set(policy.id, policy);
|
|
635
|
+
logger.debug("[ReplicationManager] Encrypted policy created", {
|
|
636
|
+
policyId: policy.id,
|
|
637
|
+
name,
|
|
638
|
+
replicationFactor,
|
|
639
|
+
encryptionMode
|
|
640
|
+
});
|
|
641
|
+
return policy;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Verify a replica's capabilities via UCAN
|
|
645
|
+
*/
|
|
646
|
+
async verifyReplicaCapabilities(replicaDID, token, policyId) {
|
|
647
|
+
if (!this.cryptoProvider) {
|
|
648
|
+
return { authorized: true };
|
|
649
|
+
}
|
|
650
|
+
const policy = policyId ? this.policies.get(policyId) : void 0;
|
|
651
|
+
const result = await this.cryptoProvider.verifyUCAN(token, {
|
|
652
|
+
requiredCapabilities: policy?.requiredCapabilities?.map((cap) => ({
|
|
653
|
+
can: cap,
|
|
654
|
+
with: "*"
|
|
655
|
+
}))
|
|
656
|
+
});
|
|
657
|
+
if (!result.authorized) {
|
|
658
|
+
logger.warn("[ReplicationManager] Replica capability verification failed", {
|
|
659
|
+
replicaDID,
|
|
660
|
+
error: result.error
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Register a replica
|
|
667
|
+
*/
|
|
668
|
+
registerReplica(replica) {
|
|
669
|
+
this.replicas.set(replica.id, replica);
|
|
670
|
+
if (!this.syncStatus.has(replica.nodeId)) {
|
|
671
|
+
this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
|
|
672
|
+
}
|
|
673
|
+
const event = {
|
|
674
|
+
type: "replica-added",
|
|
675
|
+
replicaId: replica.id,
|
|
676
|
+
nodeId: replica.nodeId,
|
|
677
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
678
|
+
};
|
|
679
|
+
this.replicationEvents.push(event);
|
|
680
|
+
logger.debug("[ReplicationManager] Replica registered", {
|
|
681
|
+
replicaId: replica.id,
|
|
682
|
+
nodeId: replica.nodeId,
|
|
683
|
+
status: replica.status
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Remove a replica
|
|
688
|
+
*/
|
|
689
|
+
removeReplica(replicaId) {
|
|
690
|
+
const replica = this.replicas.get(replicaId);
|
|
691
|
+
if (!replica) {
|
|
692
|
+
throw new Error(`Replica ${replicaId} not found`);
|
|
693
|
+
}
|
|
694
|
+
this.replicas.delete(replicaId);
|
|
695
|
+
const event = {
|
|
696
|
+
type: "replica-removed",
|
|
697
|
+
replicaId,
|
|
698
|
+
nodeId: replica.nodeId,
|
|
699
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
700
|
+
};
|
|
701
|
+
this.replicationEvents.push(event);
|
|
702
|
+
logger.debug("[ReplicationManager] Replica removed", { replicaId });
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Create a replication policy
|
|
706
|
+
*/
|
|
707
|
+
createPolicy(name, replicationFactor, consistencyLevel, syncInterval = 1e3, maxReplicationLag = 1e4) {
|
|
708
|
+
const policy = {
|
|
709
|
+
id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
710
|
+
name,
|
|
711
|
+
replicationFactor,
|
|
712
|
+
consistencyLevel,
|
|
713
|
+
syncInterval,
|
|
714
|
+
maxReplicationLag
|
|
715
|
+
};
|
|
716
|
+
this.policies.set(policy.id, policy);
|
|
717
|
+
logger.debug("[ReplicationManager] Policy created", {
|
|
718
|
+
policyId: policy.id,
|
|
719
|
+
name,
|
|
720
|
+
replicationFactor,
|
|
721
|
+
consistencyLevel
|
|
722
|
+
});
|
|
723
|
+
return policy;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Update replica status
|
|
727
|
+
*/
|
|
728
|
+
updateReplicaStatus(replicaId, status, lagBytes = 0, lagMillis = 0) {
|
|
729
|
+
const replica = this.replicas.get(replicaId);
|
|
730
|
+
if (!replica) {
|
|
731
|
+
throw new Error(`Replica ${replicaId} not found`);
|
|
732
|
+
}
|
|
733
|
+
replica.status = status;
|
|
734
|
+
replica.lagBytes = lagBytes;
|
|
735
|
+
replica.lagMillis = lagMillis;
|
|
736
|
+
replica.lastSyncTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
737
|
+
const event = {
|
|
738
|
+
type: status === "syncing" ? "replica-synced" : "sync-failed",
|
|
739
|
+
replicaId,
|
|
740
|
+
nodeId: replica.nodeId,
|
|
741
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
742
|
+
details: { status, lagBytes, lagMillis }
|
|
743
|
+
};
|
|
744
|
+
this.replicationEvents.push(event);
|
|
745
|
+
const syncStatus = this.syncStatus.get(replica.nodeId);
|
|
746
|
+
if (syncStatus) {
|
|
747
|
+
if (status === "syncing" || status === "secondary") {
|
|
748
|
+
syncStatus.synced++;
|
|
749
|
+
} else if (status === "failed") {
|
|
750
|
+
syncStatus.failed++;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
logger.debug("[ReplicationManager] Replica status updated", {
|
|
754
|
+
replicaId,
|
|
755
|
+
status,
|
|
756
|
+
lagBytes,
|
|
757
|
+
lagMillis
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Get replicas for node
|
|
762
|
+
*/
|
|
763
|
+
getReplicasForNode(nodeId) {
|
|
764
|
+
return Array.from(this.replicas.values()).filter((r) => r.nodeId === nodeId);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get healthy replicas
|
|
768
|
+
*/
|
|
769
|
+
getHealthyReplicas() {
|
|
770
|
+
return Array.from(this.replicas.values()).filter(
|
|
771
|
+
(r) => r.status === "secondary" || r.status === "primary"
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Get syncing replicas
|
|
776
|
+
*/
|
|
777
|
+
getSyncingReplicas() {
|
|
778
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "syncing");
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Get failed replicas
|
|
782
|
+
*/
|
|
783
|
+
getFailedReplicas() {
|
|
784
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "failed");
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Check replication health for policy
|
|
788
|
+
*/
|
|
789
|
+
checkReplicationHealth(policyId) {
|
|
790
|
+
const policy = this.policies.get(policyId);
|
|
791
|
+
if (!policy) {
|
|
792
|
+
throw new Error(`Policy ${policyId} not found`);
|
|
793
|
+
}
|
|
794
|
+
const healthy = this.getHealthyReplicas();
|
|
795
|
+
const maxLag = Math.max(0, ...healthy.map((r) => r.lagMillis));
|
|
796
|
+
return {
|
|
797
|
+
healthy: healthy.length >= policy.replicationFactor && maxLag <= policy.maxReplicationLag,
|
|
798
|
+
replicasInPolicy: policy.replicationFactor,
|
|
799
|
+
healthyReplicas: healthy.length,
|
|
800
|
+
replicationLag: maxLag
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Get consistency level
|
|
805
|
+
*/
|
|
806
|
+
getConsistencyLevel(policyId) {
|
|
807
|
+
const policy = this.policies.get(policyId);
|
|
808
|
+
if (!policy) {
|
|
809
|
+
return "eventual";
|
|
810
|
+
}
|
|
811
|
+
return policy.consistencyLevel;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Get replica
|
|
815
|
+
*/
|
|
816
|
+
getReplica(replicaId) {
|
|
817
|
+
return this.replicas.get(replicaId);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Get all replicas
|
|
821
|
+
*/
|
|
822
|
+
getAllReplicas() {
|
|
823
|
+
return Array.from(this.replicas.values());
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get policy
|
|
827
|
+
*/
|
|
828
|
+
getPolicy(policyId) {
|
|
829
|
+
return this.policies.get(policyId);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Get all policies
|
|
833
|
+
*/
|
|
834
|
+
getAllPolicies() {
|
|
835
|
+
return Array.from(this.policies.values());
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Get replication statistics
|
|
839
|
+
*/
|
|
840
|
+
getStatistics() {
|
|
841
|
+
const healthy = this.getHealthyReplicas().length;
|
|
842
|
+
const syncing = this.getSyncingReplicas().length;
|
|
843
|
+
const failed = this.getFailedReplicas().length;
|
|
844
|
+
const total = this.replicas.size;
|
|
845
|
+
const replicationLags = Array.from(this.replicas.values()).map((r) => r.lagMillis);
|
|
846
|
+
const avgLag = replicationLags.length > 0 ? replicationLags.reduce((a, b) => a + b) / replicationLags.length : 0;
|
|
847
|
+
const maxLag = replicationLags.length > 0 ? Math.max(...replicationLags) : 0;
|
|
848
|
+
return {
|
|
849
|
+
totalReplicas: total,
|
|
850
|
+
healthyReplicas: healthy,
|
|
851
|
+
syncingReplicas: syncing,
|
|
852
|
+
failedReplicas: failed,
|
|
853
|
+
healthiness: total > 0 ? healthy / total * 100 : 0,
|
|
854
|
+
averageReplicationLagMs: avgLag,
|
|
855
|
+
maxReplicationLagMs: maxLag,
|
|
856
|
+
totalPolicies: this.policies.size
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Get replication events
|
|
861
|
+
*/
|
|
862
|
+
getReplicationEvents(limit) {
|
|
863
|
+
const events = [...this.replicationEvents];
|
|
864
|
+
if (limit) {
|
|
865
|
+
return events.slice(-limit);
|
|
866
|
+
}
|
|
867
|
+
return events;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Get sync status for node
|
|
871
|
+
*/
|
|
872
|
+
getSyncStatus(nodeId) {
|
|
873
|
+
return this.syncStatus.get(nodeId) || { synced: 0, failed: 0 };
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Get replication lag distribution
|
|
877
|
+
*/
|
|
878
|
+
getReplicationLagDistribution() {
|
|
879
|
+
const distribution = {
|
|
880
|
+
"0-100ms": 0,
|
|
881
|
+
"100-500ms": 0,
|
|
882
|
+
"500-1000ms": 0,
|
|
883
|
+
"1000+ms": 0
|
|
884
|
+
};
|
|
885
|
+
for (const replica of this.replicas.values()) {
|
|
886
|
+
if (replica.lagMillis <= 100) {
|
|
887
|
+
distribution["0-100ms"]++;
|
|
888
|
+
} else if (replica.lagMillis <= 500) {
|
|
889
|
+
distribution["100-500ms"]++;
|
|
890
|
+
} else if (replica.lagMillis <= 1e3) {
|
|
891
|
+
distribution["500-1000ms"]++;
|
|
892
|
+
} else {
|
|
893
|
+
distribution["1000+ms"]++;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return distribution;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Check if can satisfy consistency level
|
|
900
|
+
*/
|
|
901
|
+
canSatisfyConsistency(policyId, _requiredAcks) {
|
|
902
|
+
const policy = this.policies.get(policyId);
|
|
903
|
+
if (!policy) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
const healthyCount = this.getHealthyReplicas().length;
|
|
907
|
+
switch (policy.consistencyLevel) {
|
|
908
|
+
case "eventual":
|
|
909
|
+
return true;
|
|
910
|
+
// Always achievable
|
|
911
|
+
case "read-after-write":
|
|
912
|
+
return healthyCount >= 1;
|
|
913
|
+
case "strong":
|
|
914
|
+
return healthyCount >= policy.replicationFactor;
|
|
915
|
+
default:
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Clear all state (for testing)
|
|
921
|
+
*/
|
|
922
|
+
clear() {
|
|
923
|
+
this.replicas.clear();
|
|
924
|
+
this.policies.clear();
|
|
925
|
+
this.replicationEvents = [];
|
|
926
|
+
this.syncStatus.clear();
|
|
927
|
+
this.replicasByDID.clear();
|
|
928
|
+
this.cryptoProvider = null;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Get the crypto provider (for advanced usage)
|
|
932
|
+
*/
|
|
933
|
+
getCryptoProvider() {
|
|
934
|
+
return this.cryptoProvider;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// src/distributed/SyncProtocol.ts
|
|
939
|
+
var SyncProtocol = class {
|
|
940
|
+
version = "1.0.0";
|
|
941
|
+
messageQueue = [];
|
|
942
|
+
messageMap = /* @__PURE__ */ new Map();
|
|
943
|
+
handshakes = /* @__PURE__ */ new Map();
|
|
944
|
+
protocolErrors = [];
|
|
945
|
+
messageCounter = 0;
|
|
946
|
+
// Crypto support
|
|
947
|
+
cryptoProvider = null;
|
|
948
|
+
cryptoConfig = null;
|
|
949
|
+
/**
|
|
950
|
+
* Configure cryptographic provider for authenticated/encrypted messages
|
|
951
|
+
*/
|
|
952
|
+
configureCrypto(provider, config) {
|
|
953
|
+
this.cryptoProvider = provider;
|
|
954
|
+
this.cryptoConfig = {
|
|
955
|
+
encryptionMode: config?.encryptionMode ?? "none",
|
|
956
|
+
requireSignatures: config?.requireSignatures ?? false,
|
|
957
|
+
requireCapabilities: config?.requireCapabilities ?? false,
|
|
958
|
+
requiredCapabilities: config?.requiredCapabilities
|
|
959
|
+
};
|
|
960
|
+
logger.debug("[SyncProtocol] Crypto configured", {
|
|
961
|
+
encryptionMode: this.cryptoConfig.encryptionMode,
|
|
962
|
+
requireSignatures: this.cryptoConfig.requireSignatures,
|
|
963
|
+
requireCapabilities: this.cryptoConfig.requireCapabilities
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Check if crypto is configured
|
|
968
|
+
*/
|
|
969
|
+
isCryptoEnabled() {
|
|
970
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Get crypto configuration
|
|
974
|
+
*/
|
|
975
|
+
getCryptoConfig() {
|
|
976
|
+
return this.cryptoConfig ? { ...this.cryptoConfig } : null;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Get protocol version
|
|
980
|
+
*/
|
|
981
|
+
getVersion() {
|
|
982
|
+
return this.version;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Create authenticated handshake message with DID and keys
|
|
986
|
+
*/
|
|
987
|
+
async createAuthenticatedHandshake(capabilities, targetDID) {
|
|
988
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
989
|
+
throw new Error("Crypto provider not initialized");
|
|
990
|
+
}
|
|
991
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
992
|
+
if (!localDID) {
|
|
993
|
+
throw new Error("Local DID not available");
|
|
994
|
+
}
|
|
995
|
+
const publicInfo = await this.cryptoProvider.exportPublicIdentity();
|
|
996
|
+
if (!publicInfo) {
|
|
997
|
+
throw new Error("Cannot export public identity");
|
|
998
|
+
}
|
|
999
|
+
let ucan;
|
|
1000
|
+
if (targetDID && this.cryptoConfig?.requireCapabilities) {
|
|
1001
|
+
const caps = this.cryptoConfig.requiredCapabilities || [
|
|
1002
|
+
{ can: "aeon:sync:read", with: "*" },
|
|
1003
|
+
{ can: "aeon:sync:write", with: "*" }
|
|
1004
|
+
];
|
|
1005
|
+
ucan = await this.cryptoProvider.createUCAN(targetDID, caps);
|
|
1006
|
+
}
|
|
1007
|
+
const handshakePayload = {
|
|
1008
|
+
protocolVersion: this.version,
|
|
1009
|
+
nodeId: localDID,
|
|
1010
|
+
capabilities,
|
|
1011
|
+
state: "initiating",
|
|
1012
|
+
did: localDID,
|
|
1013
|
+
publicSigningKey: publicInfo.publicSigningKey,
|
|
1014
|
+
publicEncryptionKey: publicInfo.publicEncryptionKey,
|
|
1015
|
+
ucan
|
|
1016
|
+
};
|
|
1017
|
+
const message = {
|
|
1018
|
+
type: "handshake",
|
|
1019
|
+
version: this.version,
|
|
1020
|
+
sender: localDID,
|
|
1021
|
+
receiver: targetDID || "",
|
|
1022
|
+
messageId: this.generateMessageId(),
|
|
1023
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1024
|
+
payload: handshakePayload
|
|
1025
|
+
};
|
|
1026
|
+
if (this.cryptoConfig?.requireSignatures) {
|
|
1027
|
+
const signed = await this.cryptoProvider.signData(handshakePayload);
|
|
1028
|
+
message.auth = {
|
|
1029
|
+
senderDID: localDID,
|
|
1030
|
+
receiverDID: targetDID,
|
|
1031
|
+
signature: signed.signature
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
this.messageMap.set(message.messageId, message);
|
|
1035
|
+
this.messageQueue.push(message);
|
|
1036
|
+
logger.debug("[SyncProtocol] Authenticated handshake created", {
|
|
1037
|
+
messageId: message.messageId,
|
|
1038
|
+
did: localDID,
|
|
1039
|
+
capabilities: capabilities.length,
|
|
1040
|
+
hasUCAN: !!ucan
|
|
1041
|
+
});
|
|
1042
|
+
return message;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Verify and process an authenticated handshake
|
|
1046
|
+
*/
|
|
1047
|
+
async verifyAuthenticatedHandshake(message) {
|
|
1048
|
+
if (message.type !== "handshake") {
|
|
1049
|
+
return { valid: false, error: "Message is not a handshake" };
|
|
1050
|
+
}
|
|
1051
|
+
const handshake = message.payload;
|
|
1052
|
+
if (!this.cryptoProvider || !this.cryptoConfig) {
|
|
1053
|
+
this.handshakes.set(message.sender, handshake);
|
|
1054
|
+
return { valid: true, handshake };
|
|
1055
|
+
}
|
|
1056
|
+
if (handshake.did && handshake.publicSigningKey) {
|
|
1057
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
1058
|
+
id: handshake.nodeId,
|
|
1059
|
+
did: handshake.did,
|
|
1060
|
+
publicSigningKey: handshake.publicSigningKey,
|
|
1061
|
+
publicEncryptionKey: handshake.publicEncryptionKey
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (this.cryptoConfig.requireSignatures && message.auth?.signature) {
|
|
1065
|
+
const signed = {
|
|
1066
|
+
payload: handshake,
|
|
1067
|
+
signature: message.auth.signature,
|
|
1068
|
+
signer: message.auth.senderDID || message.sender,
|
|
1069
|
+
algorithm: "ES256",
|
|
1070
|
+
signedAt: Date.now()
|
|
1071
|
+
};
|
|
1072
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
1073
|
+
if (!isValid) {
|
|
1074
|
+
logger.warn("[SyncProtocol] Handshake signature verification failed", {
|
|
1075
|
+
messageId: message.messageId,
|
|
1076
|
+
sender: message.sender
|
|
1077
|
+
});
|
|
1078
|
+
return { valid: false, error: "Invalid signature" };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (this.cryptoConfig.requireCapabilities && handshake.ucan) {
|
|
1082
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1083
|
+
const result = await this.cryptoProvider.verifyUCAN(handshake.ucan, {
|
|
1084
|
+
expectedAudience: localDID || void 0,
|
|
1085
|
+
requiredCapabilities: this.cryptoConfig.requiredCapabilities
|
|
1086
|
+
});
|
|
1087
|
+
if (!result.authorized) {
|
|
1088
|
+
logger.warn("[SyncProtocol] Handshake UCAN verification failed", {
|
|
1089
|
+
messageId: message.messageId,
|
|
1090
|
+
error: result.error
|
|
1091
|
+
});
|
|
1092
|
+
return { valid: false, error: result.error || "Unauthorized" };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
this.handshakes.set(message.sender, handshake);
|
|
1096
|
+
logger.debug("[SyncProtocol] Authenticated handshake verified", {
|
|
1097
|
+
messageId: message.messageId,
|
|
1098
|
+
did: handshake.did
|
|
1099
|
+
});
|
|
1100
|
+
return { valid: true, handshake };
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Sign and optionally encrypt a message payload
|
|
1104
|
+
*/
|
|
1105
|
+
async signMessage(message, payload, encrypt = false) {
|
|
1106
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1107
|
+
throw new Error("Crypto provider not initialized");
|
|
1108
|
+
}
|
|
1109
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1110
|
+
const signed = await this.cryptoProvider.signData(payload);
|
|
1111
|
+
message.auth = {
|
|
1112
|
+
senderDID: localDID || void 0,
|
|
1113
|
+
receiverDID: message.receiver || void 0,
|
|
1114
|
+
signature: signed.signature,
|
|
1115
|
+
encrypted: false
|
|
1116
|
+
};
|
|
1117
|
+
if (encrypt && message.receiver && this.cryptoConfig?.encryptionMode !== "none") {
|
|
1118
|
+
const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
1119
|
+
const encrypted = await this.cryptoProvider.encrypt(payloadBytes, message.receiver);
|
|
1120
|
+
message.payload = encrypted;
|
|
1121
|
+
message.auth.encrypted = true;
|
|
1122
|
+
logger.debug("[SyncProtocol] Message encrypted", {
|
|
1123
|
+
messageId: message.messageId,
|
|
1124
|
+
recipient: message.receiver
|
|
1125
|
+
});
|
|
1126
|
+
} else {
|
|
1127
|
+
message.payload = payload;
|
|
1128
|
+
}
|
|
1129
|
+
return message;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Verify signature and optionally decrypt a message
|
|
1133
|
+
*/
|
|
1134
|
+
async verifyMessage(message) {
|
|
1135
|
+
if (!this.cryptoProvider || !message.auth) {
|
|
1136
|
+
return { valid: true, payload: message.payload };
|
|
1137
|
+
}
|
|
1138
|
+
let payload = message.payload;
|
|
1139
|
+
if (message.auth.encrypted && message.payload) {
|
|
1140
|
+
try {
|
|
1141
|
+
const encrypted = message.payload;
|
|
1142
|
+
const decrypted = await this.cryptoProvider.decrypt(
|
|
1143
|
+
encrypted,
|
|
1144
|
+
message.auth.senderDID
|
|
1145
|
+
);
|
|
1146
|
+
payload = JSON.parse(new TextDecoder().decode(decrypted));
|
|
1147
|
+
logger.debug("[SyncProtocol] Message decrypted", {
|
|
1148
|
+
messageId: message.messageId
|
|
1149
|
+
});
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
return {
|
|
1152
|
+
valid: false,
|
|
1153
|
+
error: `Decryption failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (message.auth.signature && message.auth.senderDID) {
|
|
1158
|
+
const signed = {
|
|
1159
|
+
payload,
|
|
1160
|
+
signature: message.auth.signature,
|
|
1161
|
+
signer: message.auth.senderDID,
|
|
1162
|
+
algorithm: "ES256",
|
|
1163
|
+
signedAt: Date.now()
|
|
1164
|
+
};
|
|
1165
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
1166
|
+
if (!isValid) {
|
|
1167
|
+
return { valid: false, error: "Invalid signature" };
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return { valid: true, payload };
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Create handshake message
|
|
1174
|
+
*/
|
|
1175
|
+
createHandshakeMessage(nodeId, capabilities) {
|
|
1176
|
+
const message = {
|
|
1177
|
+
type: "handshake",
|
|
1178
|
+
version: this.version,
|
|
1179
|
+
sender: nodeId,
|
|
1180
|
+
receiver: "",
|
|
1181
|
+
messageId: this.generateMessageId(),
|
|
1182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1183
|
+
payload: {
|
|
1184
|
+
protocolVersion: this.version,
|
|
1185
|
+
nodeId,
|
|
1186
|
+
capabilities,
|
|
1187
|
+
state: "initiating"
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
this.messageMap.set(message.messageId, message);
|
|
1191
|
+
this.messageQueue.push(message);
|
|
1192
|
+
logger.debug("[SyncProtocol] Handshake message created", {
|
|
1193
|
+
messageId: message.messageId,
|
|
1194
|
+
nodeId,
|
|
1195
|
+
capabilities: capabilities.length
|
|
1196
|
+
});
|
|
1197
|
+
return message;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Create sync request message
|
|
1201
|
+
*/
|
|
1202
|
+
createSyncRequestMessage(sender, receiver, sessionId, fromVersion, toVersion, filter) {
|
|
1203
|
+
const message = {
|
|
1204
|
+
type: "sync-request",
|
|
1205
|
+
version: this.version,
|
|
1206
|
+
sender,
|
|
1207
|
+
receiver,
|
|
1208
|
+
messageId: this.generateMessageId(),
|
|
1209
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1210
|
+
payload: {
|
|
1211
|
+
sessionId,
|
|
1212
|
+
fromVersion,
|
|
1213
|
+
toVersion,
|
|
1214
|
+
filter
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
this.messageMap.set(message.messageId, message);
|
|
1218
|
+
this.messageQueue.push(message);
|
|
1219
|
+
logger.debug("[SyncProtocol] Sync request created", {
|
|
1220
|
+
messageId: message.messageId,
|
|
1221
|
+
sessionId,
|
|
1222
|
+
fromVersion,
|
|
1223
|
+
toVersion
|
|
1224
|
+
});
|
|
1225
|
+
return message;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Create sync response message
|
|
1229
|
+
*/
|
|
1230
|
+
createSyncResponseMessage(sender, receiver, sessionId, fromVersion, toVersion, data, hasMore = false, offset = 0) {
|
|
1231
|
+
const message = {
|
|
1232
|
+
type: "sync-response",
|
|
1233
|
+
version: this.version,
|
|
1234
|
+
sender,
|
|
1235
|
+
receiver,
|
|
1236
|
+
messageId: this.generateMessageId(),
|
|
1237
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1238
|
+
payload: {
|
|
1239
|
+
sessionId,
|
|
1240
|
+
fromVersion,
|
|
1241
|
+
toVersion,
|
|
1242
|
+
data,
|
|
1243
|
+
hasMore,
|
|
1244
|
+
offset
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
this.messageMap.set(message.messageId, message);
|
|
1248
|
+
this.messageQueue.push(message);
|
|
1249
|
+
logger.debug("[SyncProtocol] Sync response created", {
|
|
1250
|
+
messageId: message.messageId,
|
|
1251
|
+
sessionId,
|
|
1252
|
+
itemCount: data.length,
|
|
1253
|
+
hasMore
|
|
1254
|
+
});
|
|
1255
|
+
return message;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Create acknowledgement message
|
|
1259
|
+
*/
|
|
1260
|
+
createAckMessage(sender, receiver, messageId) {
|
|
1261
|
+
const message = {
|
|
1262
|
+
type: "ack",
|
|
1263
|
+
version: this.version,
|
|
1264
|
+
sender,
|
|
1265
|
+
receiver,
|
|
1266
|
+
messageId: this.generateMessageId(),
|
|
1267
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1268
|
+
payload: { acknowledgedMessageId: messageId }
|
|
1269
|
+
};
|
|
1270
|
+
this.messageMap.set(message.messageId, message);
|
|
1271
|
+
this.messageQueue.push(message);
|
|
1272
|
+
return message;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Create error message
|
|
1276
|
+
*/
|
|
1277
|
+
createErrorMessage(sender, receiver, error, relatedMessageId) {
|
|
1278
|
+
const message = {
|
|
1279
|
+
type: "error",
|
|
1280
|
+
version: this.version,
|
|
1281
|
+
sender,
|
|
1282
|
+
receiver,
|
|
1283
|
+
messageId: this.generateMessageId(),
|
|
1284
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
+
payload: {
|
|
1286
|
+
error,
|
|
1287
|
+
relatedMessageId
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
this.messageMap.set(message.messageId, message);
|
|
1291
|
+
this.messageQueue.push(message);
|
|
1292
|
+
this.protocolErrors.push({
|
|
1293
|
+
error,
|
|
1294
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1295
|
+
});
|
|
1296
|
+
logger.error("[SyncProtocol] Error message created", {
|
|
1297
|
+
messageId: message.messageId,
|
|
1298
|
+
errorCode: error.code,
|
|
1299
|
+
recoverable: error.recoverable
|
|
1300
|
+
});
|
|
1301
|
+
return message;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Validate message
|
|
1305
|
+
*/
|
|
1306
|
+
validateMessage(message) {
|
|
1307
|
+
const errors = [];
|
|
1308
|
+
if (!message.type) {
|
|
1309
|
+
errors.push("Message type is required");
|
|
1310
|
+
}
|
|
1311
|
+
if (!message.sender) {
|
|
1312
|
+
errors.push("Sender is required");
|
|
1313
|
+
}
|
|
1314
|
+
if (!message.messageId) {
|
|
1315
|
+
errors.push("Message ID is required");
|
|
1316
|
+
}
|
|
1317
|
+
if (!message.timestamp) {
|
|
1318
|
+
errors.push("Timestamp is required");
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
new Date(message.timestamp);
|
|
1322
|
+
} catch {
|
|
1323
|
+
errors.push("Invalid timestamp format");
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
valid: errors.length === 0,
|
|
1327
|
+
errors
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Serialize message
|
|
1332
|
+
*/
|
|
1333
|
+
serializeMessage(message) {
|
|
1334
|
+
try {
|
|
1335
|
+
return JSON.stringify(message);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
logger.error("[SyncProtocol] Message serialization failed", {
|
|
1338
|
+
messageId: message.messageId,
|
|
1339
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1340
|
+
});
|
|
1341
|
+
throw new Error(`Failed to serialize message: ${error instanceof Error ? error.message : String(error)}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Deserialize message
|
|
1346
|
+
*/
|
|
1347
|
+
deserializeMessage(data) {
|
|
1348
|
+
try {
|
|
1349
|
+
const message = JSON.parse(data);
|
|
1350
|
+
const validation = this.validateMessage(message);
|
|
1351
|
+
if (!validation.valid) {
|
|
1352
|
+
throw new Error(`Invalid message: ${validation.errors.join(", ")}`);
|
|
1353
|
+
}
|
|
1354
|
+
return message;
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
logger.error("[SyncProtocol] Message deserialization failed", {
|
|
1357
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1358
|
+
});
|
|
1359
|
+
throw new Error(`Failed to deserialize message: ${error instanceof Error ? error.message : String(error)}`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Process handshake
|
|
1364
|
+
*/
|
|
1365
|
+
processHandshake(message) {
|
|
1366
|
+
if (message.type !== "handshake") {
|
|
1367
|
+
throw new Error("Message is not a handshake");
|
|
1368
|
+
}
|
|
1369
|
+
const handshake = message.payload;
|
|
1370
|
+
const nodeId = message.sender;
|
|
1371
|
+
this.handshakes.set(nodeId, handshake);
|
|
1372
|
+
logger.debug("[SyncProtocol] Handshake processed", {
|
|
1373
|
+
nodeId,
|
|
1374
|
+
protocolVersion: handshake.protocolVersion,
|
|
1375
|
+
capabilities: handshake.capabilities.length
|
|
1376
|
+
});
|
|
1377
|
+
return handshake;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Get message
|
|
1381
|
+
*/
|
|
1382
|
+
getMessage(messageId) {
|
|
1383
|
+
return this.messageMap.get(messageId);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get all messages
|
|
1387
|
+
*/
|
|
1388
|
+
getAllMessages() {
|
|
1389
|
+
return [...this.messageQueue];
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Get messages by type
|
|
1393
|
+
*/
|
|
1394
|
+
getMessagesByType(type) {
|
|
1395
|
+
return this.messageQueue.filter((m) => m.type === type);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get messages from sender
|
|
1399
|
+
*/
|
|
1400
|
+
getMessagesFromSender(sender) {
|
|
1401
|
+
return this.messageQueue.filter((m) => m.sender === sender);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Get pending messages
|
|
1405
|
+
*/
|
|
1406
|
+
getPendingMessages(receiver) {
|
|
1407
|
+
return this.messageQueue.filter((m) => m.receiver === receiver);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Get handshakes
|
|
1411
|
+
*/
|
|
1412
|
+
getHandshakes() {
|
|
1413
|
+
return new Map(this.handshakes);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Get protocol statistics
|
|
1417
|
+
*/
|
|
1418
|
+
getStatistics() {
|
|
1419
|
+
const messagesByType = {};
|
|
1420
|
+
for (const message of this.messageQueue) {
|
|
1421
|
+
messagesByType[message.type] = (messagesByType[message.type] || 0) + 1;
|
|
1422
|
+
}
|
|
1423
|
+
const errorCount = this.protocolErrors.length;
|
|
1424
|
+
const recoverableErrors = this.protocolErrors.filter((e) => e.error.recoverable).length;
|
|
1425
|
+
return {
|
|
1426
|
+
totalMessages: this.messageQueue.length,
|
|
1427
|
+
messagesByType,
|
|
1428
|
+
totalHandshakes: this.handshakes.size,
|
|
1429
|
+
totalErrors: errorCount,
|
|
1430
|
+
recoverableErrors,
|
|
1431
|
+
unrecoverableErrors: errorCount - recoverableErrors
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Get protocol errors
|
|
1436
|
+
*/
|
|
1437
|
+
getErrors() {
|
|
1438
|
+
return [...this.protocolErrors];
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Generate message ID
|
|
1442
|
+
*/
|
|
1443
|
+
generateMessageId() {
|
|
1444
|
+
this.messageCounter++;
|
|
1445
|
+
return `msg-${Date.now()}-${this.messageCounter}`;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Clear all state (for testing)
|
|
1449
|
+
*/
|
|
1450
|
+
clear() {
|
|
1451
|
+
this.messageQueue = [];
|
|
1452
|
+
this.messageMap.clear();
|
|
1453
|
+
this.handshakes.clear();
|
|
1454
|
+
this.protocolErrors = [];
|
|
1455
|
+
this.messageCounter = 0;
|
|
1456
|
+
this.cryptoProvider = null;
|
|
1457
|
+
this.cryptoConfig = null;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Get the crypto provider (for advanced usage)
|
|
1461
|
+
*/
|
|
1462
|
+
getCryptoProvider() {
|
|
1463
|
+
return this.cryptoProvider;
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/distributed/StateReconciler.ts
|
|
1468
|
+
var StateReconciler = class {
|
|
1469
|
+
stateVersions = /* @__PURE__ */ new Map();
|
|
1470
|
+
reconciliationHistory = [];
|
|
1471
|
+
cryptoProvider = null;
|
|
1472
|
+
requireSignedVersions = false;
|
|
1473
|
+
/**
|
|
1474
|
+
* Configure cryptographic provider for signed state versions
|
|
1475
|
+
*/
|
|
1476
|
+
configureCrypto(provider, requireSigned = false) {
|
|
1477
|
+
this.cryptoProvider = provider;
|
|
1478
|
+
this.requireSignedVersions = requireSigned;
|
|
1479
|
+
logger.debug("[StateReconciler] Crypto configured", {
|
|
1480
|
+
initialized: provider.isInitialized(),
|
|
1481
|
+
requireSigned
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Check if crypto is configured
|
|
1486
|
+
*/
|
|
1487
|
+
isCryptoEnabled() {
|
|
1488
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Record a signed state version with cryptographic verification
|
|
1492
|
+
*/
|
|
1493
|
+
async recordSignedStateVersion(key, version, data) {
|
|
1494
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1495
|
+
throw new Error("Crypto provider not initialized");
|
|
1496
|
+
}
|
|
1497
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1498
|
+
if (!localDID) {
|
|
1499
|
+
throw new Error("Local DID not available");
|
|
1500
|
+
}
|
|
1501
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
1502
|
+
const hashBytes = await this.cryptoProvider.hash(dataBytes);
|
|
1503
|
+
const hash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1504
|
+
const versionData = { version, data, hash };
|
|
1505
|
+
const signed = await this.cryptoProvider.signData(versionData);
|
|
1506
|
+
const stateVersion = {
|
|
1507
|
+
version,
|
|
1508
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1509
|
+
nodeId: localDID,
|
|
1510
|
+
hash,
|
|
1511
|
+
data,
|
|
1512
|
+
signerDID: localDID,
|
|
1513
|
+
signature: signed.signature,
|
|
1514
|
+
signedAt: signed.signedAt
|
|
1515
|
+
};
|
|
1516
|
+
if (!this.stateVersions.has(key)) {
|
|
1517
|
+
this.stateVersions.set(key, []);
|
|
1518
|
+
}
|
|
1519
|
+
this.stateVersions.get(key).push(stateVersion);
|
|
1520
|
+
logger.debug("[StateReconciler] Signed state version recorded", {
|
|
1521
|
+
key,
|
|
1522
|
+
version,
|
|
1523
|
+
signerDID: localDID,
|
|
1524
|
+
hash: hash.slice(0, 16) + "..."
|
|
1525
|
+
});
|
|
1526
|
+
return stateVersion;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Verify a state version's signature
|
|
1530
|
+
*/
|
|
1531
|
+
async verifyStateVersion(version) {
|
|
1532
|
+
if (!version.signature || !version.signerDID) {
|
|
1533
|
+
if (this.requireSignedVersions) {
|
|
1534
|
+
return { valid: false, error: "Signature required but not present" };
|
|
1535
|
+
}
|
|
1536
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(version.data));
|
|
1537
|
+
if (this.cryptoProvider) {
|
|
1538
|
+
const hashBytes = await this.cryptoProvider.hash(dataBytes);
|
|
1539
|
+
const computedHash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1540
|
+
if (computedHash !== version.hash) {
|
|
1541
|
+
return { valid: false, error: "Hash mismatch" };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return { valid: true };
|
|
1545
|
+
}
|
|
1546
|
+
if (!this.cryptoProvider) {
|
|
1547
|
+
return { valid: false, error: "Crypto provider not configured" };
|
|
1548
|
+
}
|
|
1549
|
+
const versionData = {
|
|
1550
|
+
version: version.version,
|
|
1551
|
+
data: version.data,
|
|
1552
|
+
hash: version.hash
|
|
1553
|
+
};
|
|
1554
|
+
const signed = {
|
|
1555
|
+
payload: versionData,
|
|
1556
|
+
signature: version.signature,
|
|
1557
|
+
signer: version.signerDID,
|
|
1558
|
+
algorithm: "ES256",
|
|
1559
|
+
signedAt: version.signedAt || Date.now()
|
|
1560
|
+
};
|
|
1561
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
1562
|
+
if (!isValid) {
|
|
1563
|
+
return { valid: false, error: "Invalid signature" };
|
|
1564
|
+
}
|
|
1565
|
+
return { valid: true };
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Reconcile with verification - only accept verified versions
|
|
1569
|
+
*/
|
|
1570
|
+
async reconcileWithVerification(key, strategy = "last-write-wins") {
|
|
1571
|
+
const versions = this.stateVersions.get(key) || [];
|
|
1572
|
+
const verifiedVersions = [];
|
|
1573
|
+
const verificationErrors = [];
|
|
1574
|
+
for (const version of versions) {
|
|
1575
|
+
const result2 = await this.verifyStateVersion(version);
|
|
1576
|
+
if (result2.valid) {
|
|
1577
|
+
verifiedVersions.push(version);
|
|
1578
|
+
} else {
|
|
1579
|
+
verificationErrors.push(
|
|
1580
|
+
`Version ${version.version} from ${version.nodeId}: ${result2.error}`
|
|
1581
|
+
);
|
|
1582
|
+
logger.warn("[StateReconciler] Version verification failed", {
|
|
1583
|
+
version: version.version,
|
|
1584
|
+
nodeId: version.nodeId,
|
|
1585
|
+
error: result2.error
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (verifiedVersions.length === 0) {
|
|
1590
|
+
return {
|
|
1591
|
+
success: false,
|
|
1592
|
+
mergedState: null,
|
|
1593
|
+
conflictsResolved: 0,
|
|
1594
|
+
strategy,
|
|
1595
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1596
|
+
verificationErrors
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
let result;
|
|
1600
|
+
switch (strategy) {
|
|
1601
|
+
case "last-write-wins":
|
|
1602
|
+
result = this.reconcileLastWriteWins(verifiedVersions);
|
|
1603
|
+
break;
|
|
1604
|
+
case "vector-clock":
|
|
1605
|
+
result = this.reconcileVectorClock(verifiedVersions);
|
|
1606
|
+
break;
|
|
1607
|
+
case "majority-vote":
|
|
1608
|
+
result = this.reconcileMajorityVote(verifiedVersions);
|
|
1609
|
+
break;
|
|
1610
|
+
default:
|
|
1611
|
+
result = this.reconcileLastWriteWins(verifiedVersions);
|
|
1612
|
+
}
|
|
1613
|
+
return { ...result, verificationErrors };
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Record a state version
|
|
1617
|
+
*/
|
|
1618
|
+
recordStateVersion(key, version, timestamp, nodeId, hash, data) {
|
|
1619
|
+
if (!this.stateVersions.has(key)) {
|
|
1620
|
+
this.stateVersions.set(key, []);
|
|
1621
|
+
}
|
|
1622
|
+
const versions = this.stateVersions.get(key);
|
|
1623
|
+
versions.push({
|
|
1624
|
+
version,
|
|
1625
|
+
timestamp,
|
|
1626
|
+
nodeId,
|
|
1627
|
+
hash,
|
|
1628
|
+
data
|
|
1629
|
+
});
|
|
1630
|
+
logger.debug("[StateReconciler] State version recorded", {
|
|
1631
|
+
key,
|
|
1632
|
+
version,
|
|
1633
|
+
nodeId,
|
|
1634
|
+
hash
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Detect conflicts in state versions
|
|
1639
|
+
*/
|
|
1640
|
+
detectConflicts(key) {
|
|
1641
|
+
const versions = this.stateVersions.get(key);
|
|
1642
|
+
if (!versions || versions.length <= 1) {
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
const hashes = new Set(versions.map((v) => v.hash));
|
|
1646
|
+
return hashes.size > 1;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Compare two states and generate diff
|
|
1650
|
+
*/
|
|
1651
|
+
compareStates(state1, state2) {
|
|
1652
|
+
const diff = {
|
|
1653
|
+
added: {},
|
|
1654
|
+
modified: {},
|
|
1655
|
+
removed: [],
|
|
1656
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1657
|
+
};
|
|
1658
|
+
for (const [key, value] of Object.entries(state2)) {
|
|
1659
|
+
if (!(key in state1)) {
|
|
1660
|
+
diff.added[key] = value;
|
|
1661
|
+
} else if (JSON.stringify(state1[key]) !== JSON.stringify(value)) {
|
|
1662
|
+
diff.modified[key] = { old: state1[key], new: value };
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
for (const key of Object.keys(state1)) {
|
|
1666
|
+
if (!(key in state2)) {
|
|
1667
|
+
diff.removed.push(key);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return diff;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Reconcile states using last-write-wins strategy
|
|
1674
|
+
*/
|
|
1675
|
+
reconcileLastWriteWins(versions) {
|
|
1676
|
+
if (versions.length === 0) {
|
|
1677
|
+
throw new Error("No versions to reconcile");
|
|
1678
|
+
}
|
|
1679
|
+
const sorted = [...versions].sort(
|
|
1680
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
1681
|
+
);
|
|
1682
|
+
const latest = sorted[0];
|
|
1683
|
+
const conflictsResolved = versions.length - 1;
|
|
1684
|
+
const result = {
|
|
1685
|
+
success: true,
|
|
1686
|
+
mergedState: latest.data,
|
|
1687
|
+
conflictsResolved,
|
|
1688
|
+
strategy: "last-write-wins",
|
|
1689
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1690
|
+
};
|
|
1691
|
+
this.reconciliationHistory.push(result);
|
|
1692
|
+
logger.debug("[StateReconciler] State reconciled (last-write-wins)", {
|
|
1693
|
+
winnerNode: latest.nodeId,
|
|
1694
|
+
conflictsResolved
|
|
1695
|
+
});
|
|
1696
|
+
return result;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Reconcile states using vector clock strategy
|
|
1700
|
+
*/
|
|
1701
|
+
reconcileVectorClock(versions) {
|
|
1702
|
+
if (versions.length === 0) {
|
|
1703
|
+
throw new Error("No versions to reconcile");
|
|
1704
|
+
}
|
|
1705
|
+
const sorted = [...versions].sort(
|
|
1706
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
1707
|
+
);
|
|
1708
|
+
const latest = sorted[0];
|
|
1709
|
+
let conflictsResolved = 0;
|
|
1710
|
+
for (const v of versions) {
|
|
1711
|
+
const timeDiff = Math.abs(
|
|
1712
|
+
new Date(v.timestamp).getTime() - new Date(latest.timestamp).getTime()
|
|
1713
|
+
);
|
|
1714
|
+
if (timeDiff > 100) {
|
|
1715
|
+
conflictsResolved++;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
const result = {
|
|
1719
|
+
success: true,
|
|
1720
|
+
mergedState: latest.data,
|
|
1721
|
+
conflictsResolved,
|
|
1722
|
+
strategy: "vector-clock",
|
|
1723
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1724
|
+
};
|
|
1725
|
+
this.reconciliationHistory.push(result);
|
|
1726
|
+
logger.debug("[StateReconciler] State reconciled (vector-clock)", {
|
|
1727
|
+
winnerVersion: latest.version,
|
|
1728
|
+
conflictsResolved
|
|
1729
|
+
});
|
|
1730
|
+
return result;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Reconcile states using majority vote strategy
|
|
1734
|
+
*/
|
|
1735
|
+
reconcileMajorityVote(versions) {
|
|
1736
|
+
if (versions.length === 0) {
|
|
1737
|
+
throw new Error("No versions to reconcile");
|
|
1738
|
+
}
|
|
1739
|
+
const hashGroups = /* @__PURE__ */ new Map();
|
|
1740
|
+
for (const version of versions) {
|
|
1741
|
+
if (!hashGroups.has(version.hash)) {
|
|
1742
|
+
hashGroups.set(version.hash, []);
|
|
1743
|
+
}
|
|
1744
|
+
hashGroups.get(version.hash).push(version);
|
|
1745
|
+
}
|
|
1746
|
+
let majorityVersion = null;
|
|
1747
|
+
let maxCount = 0;
|
|
1748
|
+
for (const [, versionGroup] of hashGroups) {
|
|
1749
|
+
if (versionGroup.length > maxCount) {
|
|
1750
|
+
maxCount = versionGroup.length;
|
|
1751
|
+
majorityVersion = versionGroup[0];
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (!majorityVersion) {
|
|
1755
|
+
majorityVersion = versions[0];
|
|
1756
|
+
}
|
|
1757
|
+
const conflictsResolved = versions.length - maxCount;
|
|
1758
|
+
const result = {
|
|
1759
|
+
success: true,
|
|
1760
|
+
mergedState: majorityVersion.data,
|
|
1761
|
+
conflictsResolved,
|
|
1762
|
+
strategy: "majority-vote",
|
|
1763
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1764
|
+
};
|
|
1765
|
+
this.reconciliationHistory.push(result);
|
|
1766
|
+
logger.debug("[StateReconciler] State reconciled (majority-vote)", {
|
|
1767
|
+
majorityCount: maxCount,
|
|
1768
|
+
conflictsResolved
|
|
1769
|
+
});
|
|
1770
|
+
return result;
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Merge multiple states
|
|
1774
|
+
*/
|
|
1775
|
+
mergeStates(states) {
|
|
1776
|
+
if (states.length === 0) {
|
|
1777
|
+
return {};
|
|
1778
|
+
}
|
|
1779
|
+
if (states.length === 1) {
|
|
1780
|
+
return states[0];
|
|
1781
|
+
}
|
|
1782
|
+
const merged = {};
|
|
1783
|
+
for (const state of states) {
|
|
1784
|
+
if (typeof state === "object" && state !== null) {
|
|
1785
|
+
Object.assign(merged, state);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return merged;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Validate state after reconciliation
|
|
1792
|
+
*/
|
|
1793
|
+
validateState(state) {
|
|
1794
|
+
const errors = [];
|
|
1795
|
+
if (state === null) {
|
|
1796
|
+
errors.push("State is null");
|
|
1797
|
+
} else if (state === void 0) {
|
|
1798
|
+
errors.push("State is undefined");
|
|
1799
|
+
} else if (typeof state !== "object") {
|
|
1800
|
+
errors.push("State is not an object");
|
|
1801
|
+
}
|
|
1802
|
+
return {
|
|
1803
|
+
valid: errors.length === 0,
|
|
1804
|
+
errors
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Get state versions for a key
|
|
1809
|
+
*/
|
|
1810
|
+
getStateVersions(key) {
|
|
1811
|
+
return this.stateVersions.get(key) || [];
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Get all state versions
|
|
1815
|
+
*/
|
|
1816
|
+
getAllStateVersions() {
|
|
1817
|
+
const result = {};
|
|
1818
|
+
for (const [key, versions] of this.stateVersions) {
|
|
1819
|
+
result[key] = [...versions];
|
|
1820
|
+
}
|
|
1821
|
+
return result;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Get reconciliation history
|
|
1825
|
+
*/
|
|
1826
|
+
getReconciliationHistory() {
|
|
1827
|
+
return [...this.reconciliationHistory];
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Get reconciliation statistics
|
|
1831
|
+
*/
|
|
1832
|
+
getStatistics() {
|
|
1833
|
+
const resolvedConflicts = this.reconciliationHistory.reduce(
|
|
1834
|
+
(sum, r) => sum + r.conflictsResolved,
|
|
1835
|
+
0
|
|
1836
|
+
);
|
|
1837
|
+
const strategyUsage = {};
|
|
1838
|
+
for (const result of this.reconciliationHistory) {
|
|
1839
|
+
strategyUsage[result.strategy] = (strategyUsage[result.strategy] || 0) + 1;
|
|
1840
|
+
}
|
|
1841
|
+
return {
|
|
1842
|
+
totalReconciliations: this.reconciliationHistory.length,
|
|
1843
|
+
successfulReconciliations: this.reconciliationHistory.filter((r) => r.success).length,
|
|
1844
|
+
totalConflictsResolved: resolvedConflicts,
|
|
1845
|
+
averageConflictsPerReconciliation: this.reconciliationHistory.length > 0 ? resolvedConflicts / this.reconciliationHistory.length : 0,
|
|
1846
|
+
strategyUsage,
|
|
1847
|
+
trackedKeys: this.stateVersions.size
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Clear all state (for testing)
|
|
1852
|
+
*/
|
|
1853
|
+
clear() {
|
|
1854
|
+
this.stateVersions.clear();
|
|
1855
|
+
this.reconciliationHistory = [];
|
|
1856
|
+
this.cryptoProvider = null;
|
|
1857
|
+
this.requireSignedVersions = false;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Get the crypto provider (for advanced usage)
|
|
1861
|
+
*/
|
|
1862
|
+
getCryptoProvider() {
|
|
1863
|
+
return this.cryptoProvider;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
export { ReplicationManager, StateReconciler, SyncCoordinator, SyncProtocol };
|
|
1868
|
+
//# sourceMappingURL=index.js.map
|
|
1869
|
+
//# sourceMappingURL=index.js.map
|