@a0n/aeon 5.0.1

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.
Files changed (73) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +199 -0
  3. package/dist/CryptoProvider-SLWjqByk.d.cts +407 -0
  4. package/dist/CryptoProvider-SLWjqByk.d.ts +407 -0
  5. package/dist/compression/index.cjs +1445 -0
  6. package/dist/compression/index.cjs.map +1 -0
  7. package/dist/compression/index.d.cts +451 -0
  8. package/dist/compression/index.d.ts +451 -0
  9. package/dist/compression/index.js +1426 -0
  10. package/dist/compression/index.js.map +1 -0
  11. package/dist/core/index.cjs +4 -0
  12. package/dist/core/index.cjs.map +1 -0
  13. package/dist/core/index.d.cts +212 -0
  14. package/dist/core/index.d.ts +212 -0
  15. package/dist/core/index.js +3 -0
  16. package/dist/core/index.js.map +1 -0
  17. package/dist/crypto/index.cjs +130 -0
  18. package/dist/crypto/index.cjs.map +1 -0
  19. package/dist/crypto/index.d.cts +56 -0
  20. package/dist/crypto/index.d.ts +56 -0
  21. package/dist/crypto/index.js +124 -0
  22. package/dist/crypto/index.js.map +1 -0
  23. package/dist/distributed/index.cjs +2586 -0
  24. package/dist/distributed/index.cjs.map +1 -0
  25. package/dist/distributed/index.d.cts +1005 -0
  26. package/dist/distributed/index.d.ts +1005 -0
  27. package/dist/distributed/index.js +2580 -0
  28. package/dist/distributed/index.js.map +1 -0
  29. package/dist/index.cjs +10953 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +1953 -0
  32. package/dist/index.d.ts +1953 -0
  33. package/dist/index.js +10828 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/offline/index.cjs +419 -0
  36. package/dist/offline/index.cjs.map +1 -0
  37. package/dist/offline/index.d.cts +148 -0
  38. package/dist/offline/index.d.ts +148 -0
  39. package/dist/offline/index.js +415 -0
  40. package/dist/offline/index.js.map +1 -0
  41. package/dist/optimization/index.cjs +800 -0
  42. package/dist/optimization/index.cjs.map +1 -0
  43. package/dist/optimization/index.d.cts +347 -0
  44. package/dist/optimization/index.d.ts +347 -0
  45. package/dist/optimization/index.js +790 -0
  46. package/dist/optimization/index.js.map +1 -0
  47. package/dist/persistence/index.cjs +207 -0
  48. package/dist/persistence/index.cjs.map +1 -0
  49. package/dist/persistence/index.d.cts +95 -0
  50. package/dist/persistence/index.d.ts +95 -0
  51. package/dist/persistence/index.js +204 -0
  52. package/dist/persistence/index.js.map +1 -0
  53. package/dist/presence/index.cjs +489 -0
  54. package/dist/presence/index.cjs.map +1 -0
  55. package/dist/presence/index.d.cts +283 -0
  56. package/dist/presence/index.d.ts +283 -0
  57. package/dist/presence/index.js +485 -0
  58. package/dist/presence/index.js.map +1 -0
  59. package/dist/types-CMxO7QF0.d.cts +33 -0
  60. package/dist/types-CMxO7QF0.d.ts +33 -0
  61. package/dist/utils/index.cjs +64 -0
  62. package/dist/utils/index.cjs.map +1 -0
  63. package/dist/utils/index.d.cts +38 -0
  64. package/dist/utils/index.d.ts +38 -0
  65. package/dist/utils/index.js +57 -0
  66. package/dist/utils/index.js.map +1 -0
  67. package/dist/versioning/index.cjs +1164 -0
  68. package/dist/versioning/index.cjs.map +1 -0
  69. package/dist/versioning/index.d.cts +537 -0
  70. package/dist/versioning/index.d.ts +537 -0
  71. package/dist/versioning/index.js +1159 -0
  72. package/dist/versioning/index.js.map +1 -0
  73. package/package.json +194 -0
@@ -0,0 +1,2580 @@
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 _SyncCoordinator extends EventEmitter {
33
+ static MAX_SYNC_EVENTS = 1e4;
34
+ nodes = /* @__PURE__ */ new Map();
35
+ sessions = /* @__PURE__ */ new Map();
36
+ syncEvents = [];
37
+ nodeHeartbeats = /* @__PURE__ */ new Map();
38
+ heartbeatInterval = null;
39
+ // Crypto support
40
+ cryptoProvider = null;
41
+ nodesByDID = /* @__PURE__ */ new Map();
42
+ // DID -> nodeId
43
+ constructor() {
44
+ super();
45
+ }
46
+ addSyncEvent(event) {
47
+ this.syncEvents.push(event);
48
+ if (this.syncEvents.length > _SyncCoordinator.MAX_SYNC_EVENTS) {
49
+ this.syncEvents = this.syncEvents.slice(-_SyncCoordinator.MAX_SYNC_EVENTS);
50
+ }
51
+ }
52
+ /**
53
+ * Configure cryptographic provider for authenticated sync
54
+ */
55
+ configureCrypto(provider) {
56
+ this.cryptoProvider = provider;
57
+ logger.debug("[SyncCoordinator] Crypto configured", {
58
+ initialized: provider.isInitialized()
59
+ });
60
+ }
61
+ /**
62
+ * Check if crypto is configured
63
+ */
64
+ isCryptoEnabled() {
65
+ return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
66
+ }
67
+ /**
68
+ * Register a node with DID-based identity
69
+ */
70
+ async registerAuthenticatedNode(nodeInfo) {
71
+ const node = {
72
+ ...nodeInfo
73
+ };
74
+ this.nodes.set(node.id, node);
75
+ this.nodeHeartbeats.set(node.id, Date.now());
76
+ this.nodesByDID.set(nodeInfo.did, node.id);
77
+ if (this.cryptoProvider) {
78
+ await this.cryptoProvider.registerRemoteNode({
79
+ id: node.id,
80
+ did: nodeInfo.did,
81
+ publicSigningKey: nodeInfo.publicSigningKey,
82
+ publicEncryptionKey: nodeInfo.publicEncryptionKey
83
+ });
84
+ }
85
+ const event = {
86
+ type: "node-joined",
87
+ nodeId: node.id,
88
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
89
+ data: { did: nodeInfo.did, authenticated: true }
90
+ };
91
+ this.addSyncEvent(event);
92
+ this.emit("node-joined", node);
93
+ logger.debug("[SyncCoordinator] Authenticated node registered", {
94
+ nodeId: node.id,
95
+ did: nodeInfo.did,
96
+ version: node.version
97
+ });
98
+ return node;
99
+ }
100
+ /**
101
+ * Get node by DID
102
+ */
103
+ getNodeByDID(did) {
104
+ const nodeId = this.nodesByDID.get(did);
105
+ if (!nodeId) return void 0;
106
+ return this.nodes.get(nodeId);
107
+ }
108
+ /**
109
+ * Get all authenticated nodes (nodes with DIDs)
110
+ */
111
+ getAuthenticatedNodes() {
112
+ return Array.from(this.nodes.values()).filter((n) => n.did);
113
+ }
114
+ /**
115
+ * Create an authenticated sync session with UCAN-based authorization
116
+ */
117
+ async createAuthenticatedSyncSession(initiatorDID, participantDIDs, options) {
118
+ const initiatorNodeId = this.nodesByDID.get(initiatorDID);
119
+ if (!initiatorNodeId) {
120
+ throw new Error(`Initiator node with DID ${initiatorDID} not found`);
121
+ }
122
+ const participantIds = [];
123
+ for (const did of participantDIDs) {
124
+ const nodeId = this.nodesByDID.get(did);
125
+ if (nodeId) {
126
+ participantIds.push(nodeId);
127
+ }
128
+ }
129
+ let sessionToken;
130
+ if (this.cryptoProvider && this.cryptoProvider.isInitialized()) {
131
+ const capabilities = (options?.requiredCapabilities || ["aeon:sync:read", "aeon:sync:write"]).map((cap) => ({ can: cap, with: "*" }));
132
+ if (participantDIDs.length > 0) {
133
+ sessionToken = await this.cryptoProvider.createUCAN(
134
+ participantDIDs[0],
135
+ capabilities,
136
+ { expirationSeconds: 3600 }
137
+ // 1 hour
138
+ );
139
+ }
140
+ }
141
+ const session = {
142
+ id: `sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
143
+ initiatorId: initiatorNodeId,
144
+ participantIds,
145
+ status: "pending",
146
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
147
+ itemsSynced: 0,
148
+ itemsFailed: 0,
149
+ conflictsDetected: 0,
150
+ initiatorDID,
151
+ participantDIDs,
152
+ encryptionMode: options?.encryptionMode || "none",
153
+ requiredCapabilities: options?.requiredCapabilities,
154
+ sessionToken
155
+ };
156
+ this.sessions.set(session.id, session);
157
+ const event = {
158
+ type: "sync-started",
159
+ sessionId: session.id,
160
+ nodeId: initiatorNodeId,
161
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
162
+ data: {
163
+ authenticated: true,
164
+ initiatorDID,
165
+ participantCount: participantDIDs.length,
166
+ encryptionMode: session.encryptionMode
167
+ }
168
+ };
169
+ this.addSyncEvent(event);
170
+ this.emit("sync-started", session);
171
+ logger.debug("[SyncCoordinator] Authenticated sync session created", {
172
+ sessionId: session.id,
173
+ initiatorDID,
174
+ participants: participantDIDs.length,
175
+ encryptionMode: session.encryptionMode
176
+ });
177
+ return session;
178
+ }
179
+ /**
180
+ * Verify a node's UCAN capabilities for a session
181
+ */
182
+ async verifyNodeCapabilities(sessionId, nodeDID, token) {
183
+ if (!this.cryptoProvider) {
184
+ return { authorized: true };
185
+ }
186
+ const session = this.sessions.get(sessionId);
187
+ if (!session) {
188
+ return { authorized: false, error: `Session ${sessionId} not found` };
189
+ }
190
+ const result = await this.cryptoProvider.verifyUCAN(token, {
191
+ requiredCapabilities: session.requiredCapabilities?.map((cap) => ({
192
+ can: cap,
193
+ with: "*"
194
+ }))
195
+ });
196
+ if (!result.authorized) {
197
+ logger.warn("[SyncCoordinator] Node capability verification failed", {
198
+ sessionId,
199
+ nodeDID,
200
+ error: result.error
201
+ });
202
+ }
203
+ return result;
204
+ }
205
+ /**
206
+ * Register a node in the cluster
207
+ */
208
+ registerNode(node) {
209
+ this.nodes.set(node.id, node);
210
+ this.nodeHeartbeats.set(node.id, Date.now());
211
+ const event = {
212
+ type: "node-joined",
213
+ nodeId: node.id,
214
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
215
+ };
216
+ this.addSyncEvent(event);
217
+ this.emit("node-joined", node);
218
+ logger.debug("[SyncCoordinator] Node registered", {
219
+ nodeId: node.id,
220
+ address: node.address,
221
+ version: node.version
222
+ });
223
+ }
224
+ /**
225
+ * Deregister a node from the cluster
226
+ */
227
+ deregisterNode(nodeId) {
228
+ const node = this.nodes.get(nodeId);
229
+ if (!node) {
230
+ throw new Error(`Node ${nodeId} not found`);
231
+ }
232
+ this.nodes.delete(nodeId);
233
+ this.nodeHeartbeats.delete(nodeId);
234
+ const event = {
235
+ type: "node-left",
236
+ nodeId,
237
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
238
+ };
239
+ this.addSyncEvent(event);
240
+ this.emit("node-left", node);
241
+ logger.debug("[SyncCoordinator] Node deregistered", { nodeId });
242
+ }
243
+ /**
244
+ * Create a new sync session
245
+ */
246
+ createSyncSession(initiatorId, participantIds) {
247
+ const node = this.nodes.get(initiatorId);
248
+ if (!node) {
249
+ throw new Error(`Initiator node ${initiatorId} not found`);
250
+ }
251
+ const session = {
252
+ id: `sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
253
+ initiatorId,
254
+ participantIds,
255
+ status: "pending",
256
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
257
+ itemsSynced: 0,
258
+ itemsFailed: 0,
259
+ conflictsDetected: 0
260
+ };
261
+ this.sessions.set(session.id, session);
262
+ const event = {
263
+ type: "sync-started",
264
+ sessionId: session.id,
265
+ nodeId: initiatorId,
266
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
267
+ };
268
+ this.addSyncEvent(event);
269
+ this.emit("sync-started", session);
270
+ logger.debug("[SyncCoordinator] Sync session created", {
271
+ sessionId: session.id,
272
+ initiator: initiatorId,
273
+ participants: participantIds.length
274
+ });
275
+ return session;
276
+ }
277
+ /**
278
+ * Update sync session
279
+ */
280
+ updateSyncSession(sessionId, updates) {
281
+ const session = this.sessions.get(sessionId);
282
+ if (!session) {
283
+ throw new Error(`Session ${sessionId} not found`);
284
+ }
285
+ Object.assign(session, updates);
286
+ if (updates.status === "completed" || updates.status === "failed") {
287
+ session.endTime = (/* @__PURE__ */ new Date()).toISOString();
288
+ const event = {
289
+ type: "sync-completed",
290
+ sessionId,
291
+ nodeId: session.initiatorId,
292
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
293
+ data: { status: updates.status, itemsSynced: session.itemsSynced }
294
+ };
295
+ this.addSyncEvent(event);
296
+ this.emit("sync-completed", session);
297
+ }
298
+ logger.debug("[SyncCoordinator] Sync session updated", {
299
+ sessionId,
300
+ status: session.status,
301
+ itemsSynced: session.itemsSynced
302
+ });
303
+ }
304
+ /**
305
+ * Record a conflict during sync
306
+ */
307
+ recordConflict(sessionId, nodeId, conflictData) {
308
+ const session = this.sessions.get(sessionId);
309
+ if (session) {
310
+ session.conflictsDetected++;
311
+ const event = {
312
+ type: "conflict-detected",
313
+ sessionId,
314
+ nodeId,
315
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
316
+ data: conflictData
317
+ };
318
+ this.addSyncEvent(event);
319
+ this.emit("conflict-detected", { session, nodeId, conflictData });
320
+ logger.debug("[SyncCoordinator] Conflict recorded", {
321
+ sessionId,
322
+ nodeId,
323
+ totalConflicts: session.conflictsDetected
324
+ });
325
+ }
326
+ }
327
+ /**
328
+ * Update node status
329
+ */
330
+ updateNodeStatus(nodeId, status) {
331
+ const node = this.nodes.get(nodeId);
332
+ if (!node) {
333
+ throw new Error(`Node ${nodeId} not found`);
334
+ }
335
+ node.status = status;
336
+ this.nodeHeartbeats.set(nodeId, Date.now());
337
+ logger.debug("[SyncCoordinator] Node status updated", {
338
+ nodeId,
339
+ status
340
+ });
341
+ }
342
+ /**
343
+ * Record heartbeat from node
344
+ */
345
+ recordHeartbeat(nodeId) {
346
+ const node = this.nodes.get(nodeId);
347
+ if (!node) {
348
+ return;
349
+ }
350
+ node.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
351
+ this.nodeHeartbeats.set(nodeId, Date.now());
352
+ }
353
+ /**
354
+ * Get all nodes
355
+ */
356
+ getNodes() {
357
+ return Array.from(this.nodes.values());
358
+ }
359
+ /**
360
+ * Get node by ID
361
+ */
362
+ getNode(nodeId) {
363
+ return this.nodes.get(nodeId);
364
+ }
365
+ /**
366
+ * Get online nodes
367
+ */
368
+ getOnlineNodes() {
369
+ return Array.from(this.nodes.values()).filter((n) => n.status === "online");
370
+ }
371
+ /**
372
+ * Get nodes by capability
373
+ */
374
+ getNodesByCapability(capability) {
375
+ return Array.from(this.nodes.values()).filter(
376
+ (n) => n.capabilities.includes(capability)
377
+ );
378
+ }
379
+ /**
380
+ * Get sync session
381
+ */
382
+ getSyncSession(sessionId) {
383
+ return this.sessions.get(sessionId);
384
+ }
385
+ /**
386
+ * Get all sync sessions
387
+ */
388
+ getAllSyncSessions() {
389
+ return Array.from(this.sessions.values());
390
+ }
391
+ /**
392
+ * Get active sync sessions
393
+ */
394
+ getActiveSyncSessions() {
395
+ return Array.from(this.sessions.values()).filter(
396
+ (s) => s.status === "active"
397
+ );
398
+ }
399
+ /**
400
+ * Get sessions for a node
401
+ */
402
+ getSessionsForNode(nodeId) {
403
+ return Array.from(this.sessions.values()).filter(
404
+ (s) => s.initiatorId === nodeId || s.participantIds.includes(nodeId)
405
+ );
406
+ }
407
+ /**
408
+ * Get sync statistics
409
+ */
410
+ getStatistics() {
411
+ const sessions = Array.from(this.sessions.values());
412
+ const completed = sessions.filter((s) => s.status === "completed").length;
413
+ const failed = sessions.filter((s) => s.status === "failed").length;
414
+ const active = sessions.filter((s) => s.status === "active").length;
415
+ const totalItemsSynced = sessions.reduce(
416
+ (sum, s) => sum + s.itemsSynced,
417
+ 0
418
+ );
419
+ const totalConflicts = sessions.reduce(
420
+ (sum, s) => sum + s.conflictsDetected,
421
+ 0
422
+ );
423
+ return {
424
+ totalNodes: this.nodes.size,
425
+ onlineNodes: this.getOnlineNodes().length,
426
+ offlineNodes: this.nodes.size - this.getOnlineNodes().length,
427
+ totalSessions: sessions.length,
428
+ activeSessions: active,
429
+ completedSessions: completed,
430
+ failedSessions: failed,
431
+ successRate: sessions.length > 0 ? completed / sessions.length * 100 : 0,
432
+ totalItemsSynced,
433
+ totalConflicts,
434
+ averageConflictsPerSession: sessions.length > 0 ? totalConflicts / sessions.length : 0
435
+ };
436
+ }
437
+ /**
438
+ * Get sync events
439
+ */
440
+ getSyncEvents(limit) {
441
+ const events = [...this.syncEvents];
442
+ if (limit) {
443
+ return events.slice(-limit);
444
+ }
445
+ return events;
446
+ }
447
+ /**
448
+ * Get sync events for session
449
+ */
450
+ getSessionEvents(sessionId) {
451
+ return this.syncEvents.filter((e) => e.sessionId === sessionId);
452
+ }
453
+ /**
454
+ * Check node health
455
+ */
456
+ getNodeHealth() {
457
+ const health = {};
458
+ for (const [nodeId, lastHeartbeat] of this.nodeHeartbeats) {
459
+ const now = Date.now();
460
+ const downtime = now - lastHeartbeat;
461
+ const isHealthy = downtime < 3e4;
462
+ health[nodeId] = {
463
+ isHealthy,
464
+ downtime
465
+ };
466
+ }
467
+ return health;
468
+ }
469
+ /**
470
+ * Start heartbeat monitoring
471
+ */
472
+ startHeartbeatMonitoring(interval = 5e3) {
473
+ if (this.heartbeatInterval) {
474
+ return;
475
+ }
476
+ this.heartbeatInterval = setInterval(() => {
477
+ const health = this.getNodeHealth();
478
+ for (const [nodeId, { isHealthy }] of Object.entries(health)) {
479
+ const node = this.nodes.get(nodeId);
480
+ if (!node) {
481
+ continue;
482
+ }
483
+ const newStatus = isHealthy ? "online" : "offline";
484
+ if (node.status !== newStatus) {
485
+ this.updateNodeStatus(nodeId, newStatus);
486
+ }
487
+ }
488
+ }, interval);
489
+ logger.debug("[SyncCoordinator] Heartbeat monitoring started", {
490
+ interval
491
+ });
492
+ }
493
+ /**
494
+ * Stop heartbeat monitoring
495
+ */
496
+ stopHeartbeatMonitoring() {
497
+ if (this.heartbeatInterval) {
498
+ clearInterval(this.heartbeatInterval);
499
+ this.heartbeatInterval = null;
500
+ logger.debug("[SyncCoordinator] Heartbeat monitoring stopped");
501
+ }
502
+ }
503
+ /**
504
+ * Clear all state (for testing)
505
+ */
506
+ clear() {
507
+ this.nodes.clear();
508
+ this.sessions.clear();
509
+ this.syncEvents = [];
510
+ this.nodeHeartbeats.clear();
511
+ this.nodesByDID.clear();
512
+ this.cryptoProvider = null;
513
+ this.stopHeartbeatMonitoring();
514
+ }
515
+ /**
516
+ * Get the crypto provider (for advanced usage)
517
+ */
518
+ getCryptoProvider() {
519
+ return this.cryptoProvider;
520
+ }
521
+ };
522
+
523
+ // src/distributed/ReplicationManager.ts
524
+ var ReplicationManager = class _ReplicationManager {
525
+ static DEFAULT_PERSIST_KEY = "aeon:replication-state:v1";
526
+ replicas = /* @__PURE__ */ new Map();
527
+ policies = /* @__PURE__ */ new Map();
528
+ replicationEvents = [];
529
+ syncStatus = /* @__PURE__ */ new Map();
530
+ // Crypto support
531
+ cryptoProvider = null;
532
+ replicasByDID = /* @__PURE__ */ new Map();
533
+ // DID -> replicaId
534
+ persistence = null;
535
+ persistTimer = null;
536
+ persistInFlight = false;
537
+ persistPending = false;
538
+ constructor(options) {
539
+ if (options?.persistence) {
540
+ this.persistence = {
541
+ ...options.persistence,
542
+ key: options.persistence.key ?? _ReplicationManager.DEFAULT_PERSIST_KEY,
543
+ autoPersist: options.persistence.autoPersist ?? true,
544
+ autoLoad: options.persistence.autoLoad ?? false,
545
+ persistDebounceMs: options.persistence.persistDebounceMs ?? 25
546
+ };
547
+ }
548
+ if (this.persistence?.autoLoad) {
549
+ void this.loadFromPersistence().catch((error) => {
550
+ logger.error("[ReplicationManager] Failed to load persistence", {
551
+ key: this.persistence?.key,
552
+ error: error instanceof Error ? error.message : String(error)
553
+ });
554
+ });
555
+ }
556
+ }
557
+ /**
558
+ * Configure cryptographic provider for encrypted replication
559
+ */
560
+ configureCrypto(provider) {
561
+ this.cryptoProvider = provider;
562
+ logger.debug("[ReplicationManager] Crypto configured", {
563
+ initialized: provider.isInitialized()
564
+ });
565
+ }
566
+ /**
567
+ * Check if crypto is configured
568
+ */
569
+ isCryptoEnabled() {
570
+ return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
571
+ }
572
+ /**
573
+ * Register an authenticated replica with DID
574
+ */
575
+ async registerAuthenticatedReplica(replica, encrypted = false) {
576
+ const authenticatedReplica = {
577
+ ...replica,
578
+ encrypted
579
+ };
580
+ this.replicas.set(replica.id, authenticatedReplica);
581
+ this.replicasByDID.set(replica.did, replica.id);
582
+ if (!this.syncStatus.has(replica.nodeId)) {
583
+ this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
584
+ }
585
+ if (this.cryptoProvider && replica.publicSigningKey) {
586
+ await this.cryptoProvider.registerRemoteNode({
587
+ id: replica.nodeId,
588
+ did: replica.did,
589
+ publicSigningKey: replica.publicSigningKey,
590
+ publicEncryptionKey: replica.publicEncryptionKey
591
+ });
592
+ }
593
+ const event = {
594
+ type: "replica-added",
595
+ replicaId: replica.id,
596
+ nodeId: replica.nodeId,
597
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
598
+ details: { did: replica.did, encrypted, authenticated: true }
599
+ };
600
+ this.replicationEvents.push(event);
601
+ this.schedulePersist();
602
+ logger.debug("[ReplicationManager] Authenticated replica registered", {
603
+ replicaId: replica.id,
604
+ did: replica.did,
605
+ encrypted
606
+ });
607
+ return authenticatedReplica;
608
+ }
609
+ /**
610
+ * Get replica by DID
611
+ */
612
+ getReplicaByDID(did) {
613
+ const replicaId = this.replicasByDID.get(did);
614
+ if (!replicaId) return void 0;
615
+ return this.replicas.get(replicaId);
616
+ }
617
+ /**
618
+ * Get all encrypted replicas
619
+ */
620
+ getEncryptedReplicas() {
621
+ return Array.from(this.replicas.values()).filter((r) => r.encrypted);
622
+ }
623
+ /**
624
+ * Encrypt data for replication to a specific replica
625
+ */
626
+ async encryptForReplica(data, targetReplicaDID) {
627
+ if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
628
+ throw new Error("Crypto provider not initialized");
629
+ }
630
+ const dataBytes = new TextEncoder().encode(JSON.stringify(data));
631
+ const encrypted = await this.cryptoProvider.encrypt(
632
+ dataBytes,
633
+ targetReplicaDID
634
+ );
635
+ const localDID = this.cryptoProvider.getLocalDID();
636
+ return {
637
+ ct: encrypted.ct,
638
+ iv: encrypted.iv,
639
+ tag: encrypted.tag,
640
+ epk: encrypted.epk,
641
+ senderDID: localDID || void 0,
642
+ targetDID: targetReplicaDID,
643
+ encryptedAt: encrypted.encryptedAt
644
+ };
645
+ }
646
+ /**
647
+ * Decrypt data received from replication
648
+ */
649
+ async decryptReplicationData(encrypted) {
650
+ if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
651
+ throw new Error("Crypto provider not initialized");
652
+ }
653
+ const decrypted = await this.cryptoProvider.decrypt(
654
+ {
655
+ alg: "ECIES-P256",
656
+ ct: encrypted.ct,
657
+ iv: encrypted.iv,
658
+ tag: encrypted.tag,
659
+ epk: encrypted.epk
660
+ },
661
+ encrypted.senderDID
662
+ );
663
+ return JSON.parse(new TextDecoder().decode(decrypted));
664
+ }
665
+ /**
666
+ * Create an encrypted replication policy
667
+ */
668
+ createEncryptedPolicy(name, replicationFactor, consistencyLevel, encryptionMode, options) {
669
+ const policy = {
670
+ id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
671
+ name,
672
+ replicationFactor,
673
+ consistencyLevel,
674
+ syncInterval: options?.syncInterval || 1e3,
675
+ maxReplicationLag: options?.maxReplicationLag || 1e4,
676
+ encryptionMode,
677
+ requiredCapabilities: options?.requiredCapabilities
678
+ };
679
+ this.policies.set(policy.id, policy);
680
+ logger.debug("[ReplicationManager] Encrypted policy created", {
681
+ policyId: policy.id,
682
+ name,
683
+ replicationFactor,
684
+ encryptionMode
685
+ });
686
+ return policy;
687
+ }
688
+ /**
689
+ * Verify a replica's capabilities via UCAN
690
+ */
691
+ async verifyReplicaCapabilities(replicaDID, token, policyId) {
692
+ if (!this.cryptoProvider) {
693
+ return { authorized: true };
694
+ }
695
+ const policy = policyId ? this.policies.get(policyId) : void 0;
696
+ const result = await this.cryptoProvider.verifyUCAN(token, {
697
+ requiredCapabilities: policy?.requiredCapabilities?.map((cap) => ({
698
+ can: cap,
699
+ with: "*"
700
+ }))
701
+ });
702
+ if (!result.authorized) {
703
+ logger.warn(
704
+ "[ReplicationManager] Replica capability verification failed",
705
+ {
706
+ replicaDID,
707
+ error: result.error
708
+ }
709
+ );
710
+ }
711
+ return result;
712
+ }
713
+ /**
714
+ * Register a replica
715
+ */
716
+ registerReplica(replica) {
717
+ this.replicas.set(replica.id, replica);
718
+ if (!this.syncStatus.has(replica.nodeId)) {
719
+ this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
720
+ }
721
+ const event = {
722
+ type: "replica-added",
723
+ replicaId: replica.id,
724
+ nodeId: replica.nodeId,
725
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
726
+ };
727
+ this.replicationEvents.push(event);
728
+ this.schedulePersist();
729
+ logger.debug("[ReplicationManager] Replica registered", {
730
+ replicaId: replica.id,
731
+ nodeId: replica.nodeId,
732
+ status: replica.status
733
+ });
734
+ }
735
+ /**
736
+ * Remove a replica
737
+ */
738
+ removeReplica(replicaId) {
739
+ const replica = this.replicas.get(replicaId);
740
+ if (!replica) {
741
+ throw new Error(`Replica ${replicaId} not found`);
742
+ }
743
+ this.replicas.delete(replicaId);
744
+ const event = {
745
+ type: "replica-removed",
746
+ replicaId,
747
+ nodeId: replica.nodeId,
748
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
749
+ };
750
+ this.replicationEvents.push(event);
751
+ this.schedulePersist();
752
+ logger.debug("[ReplicationManager] Replica removed", { replicaId });
753
+ }
754
+ /**
755
+ * Create a replication policy
756
+ */
757
+ createPolicy(name, replicationFactor, consistencyLevel, syncInterval = 1e3, maxReplicationLag = 1e4) {
758
+ const policy = {
759
+ id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
760
+ name,
761
+ replicationFactor,
762
+ consistencyLevel,
763
+ syncInterval,
764
+ maxReplicationLag
765
+ };
766
+ this.policies.set(policy.id, policy);
767
+ this.schedulePersist();
768
+ logger.debug("[ReplicationManager] Policy created", {
769
+ policyId: policy.id,
770
+ name,
771
+ replicationFactor,
772
+ consistencyLevel
773
+ });
774
+ return policy;
775
+ }
776
+ /**
777
+ * Update replica status
778
+ */
779
+ updateReplicaStatus(replicaId, status, lagBytes = 0, lagMillis = 0) {
780
+ const replica = this.replicas.get(replicaId);
781
+ if (!replica) {
782
+ throw new Error(`Replica ${replicaId} not found`);
783
+ }
784
+ replica.status = status;
785
+ replica.lagBytes = lagBytes;
786
+ replica.lagMillis = lagMillis;
787
+ replica.lastSyncTime = (/* @__PURE__ */ new Date()).toISOString();
788
+ const event = {
789
+ type: status === "syncing" ? "replica-synced" : "sync-failed",
790
+ replicaId,
791
+ nodeId: replica.nodeId,
792
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
793
+ details: { status, lagBytes, lagMillis }
794
+ };
795
+ this.replicationEvents.push(event);
796
+ const syncStatus = this.syncStatus.get(replica.nodeId);
797
+ if (syncStatus) {
798
+ if (status === "syncing" || status === "secondary") {
799
+ syncStatus.synced++;
800
+ } else if (status === "failed") {
801
+ syncStatus.failed++;
802
+ }
803
+ }
804
+ logger.debug("[ReplicationManager] Replica status updated", {
805
+ replicaId,
806
+ status,
807
+ lagBytes,
808
+ lagMillis
809
+ });
810
+ this.schedulePersist();
811
+ }
812
+ /**
813
+ * Get replicas for node
814
+ */
815
+ getReplicasForNode(nodeId) {
816
+ return Array.from(this.replicas.values()).filter(
817
+ (r) => r.nodeId === nodeId
818
+ );
819
+ }
820
+ /**
821
+ * Get healthy replicas
822
+ */
823
+ getHealthyReplicas() {
824
+ return Array.from(this.replicas.values()).filter(
825
+ (r) => r.status === "secondary" || r.status === "primary"
826
+ );
827
+ }
828
+ /**
829
+ * Get syncing replicas
830
+ */
831
+ getSyncingReplicas() {
832
+ return Array.from(this.replicas.values()).filter(
833
+ (r) => r.status === "syncing"
834
+ );
835
+ }
836
+ /**
837
+ * Get failed replicas
838
+ */
839
+ getFailedReplicas() {
840
+ return Array.from(this.replicas.values()).filter(
841
+ (r) => r.status === "failed"
842
+ );
843
+ }
844
+ /**
845
+ * Check replication health for policy
846
+ */
847
+ checkReplicationHealth(policyId) {
848
+ const policy = this.policies.get(policyId);
849
+ if (!policy) {
850
+ throw new Error(`Policy ${policyId} not found`);
851
+ }
852
+ const healthy = this.getHealthyReplicas();
853
+ const maxLag = Math.max(0, ...healthy.map((r) => r.lagMillis));
854
+ return {
855
+ healthy: healthy.length >= policy.replicationFactor && maxLag <= policy.maxReplicationLag,
856
+ replicasInPolicy: policy.replicationFactor,
857
+ healthyReplicas: healthy.length,
858
+ replicationLag: maxLag
859
+ };
860
+ }
861
+ /**
862
+ * Get consistency level
863
+ */
864
+ getConsistencyLevel(policyId) {
865
+ const policy = this.policies.get(policyId);
866
+ if (!policy) {
867
+ return "eventual";
868
+ }
869
+ return policy.consistencyLevel;
870
+ }
871
+ /**
872
+ * Get replica
873
+ */
874
+ getReplica(replicaId) {
875
+ return this.replicas.get(replicaId);
876
+ }
877
+ /**
878
+ * Get all replicas
879
+ */
880
+ getAllReplicas() {
881
+ return Array.from(this.replicas.values());
882
+ }
883
+ /**
884
+ * Get policy
885
+ */
886
+ getPolicy(policyId) {
887
+ return this.policies.get(policyId);
888
+ }
889
+ /**
890
+ * Get all policies
891
+ */
892
+ getAllPolicies() {
893
+ return Array.from(this.policies.values());
894
+ }
895
+ /**
896
+ * Get replication statistics
897
+ */
898
+ getStatistics() {
899
+ const healthy = this.getHealthyReplicas().length;
900
+ const syncing = this.getSyncingReplicas().length;
901
+ const failed = this.getFailedReplicas().length;
902
+ const total = this.replicas.size;
903
+ const replicationLags = Array.from(this.replicas.values()).map(
904
+ (r) => r.lagMillis
905
+ );
906
+ const avgLag = replicationLags.length > 0 ? replicationLags.reduce((a, b) => a + b) / replicationLags.length : 0;
907
+ const maxLag = replicationLags.length > 0 ? Math.max(...replicationLags) : 0;
908
+ return {
909
+ totalReplicas: total,
910
+ healthyReplicas: healthy,
911
+ syncingReplicas: syncing,
912
+ failedReplicas: failed,
913
+ healthiness: total > 0 ? healthy / total * 100 : 0,
914
+ averageReplicationLagMs: avgLag,
915
+ maxReplicationLagMs: maxLag,
916
+ totalPolicies: this.policies.size
917
+ };
918
+ }
919
+ /**
920
+ * Get replication events
921
+ */
922
+ getReplicationEvents(limit) {
923
+ const events = [...this.replicationEvents];
924
+ if (limit) {
925
+ return events.slice(-limit);
926
+ }
927
+ return events;
928
+ }
929
+ /**
930
+ * Get sync status for node
931
+ */
932
+ getSyncStatus(nodeId) {
933
+ return this.syncStatus.get(nodeId) || { synced: 0, failed: 0 };
934
+ }
935
+ /**
936
+ * Get replication lag distribution
937
+ */
938
+ getReplicationLagDistribution() {
939
+ const distribution = {
940
+ "0-100ms": 0,
941
+ "100-500ms": 0,
942
+ "500-1000ms": 0,
943
+ "1000+ms": 0
944
+ };
945
+ for (const replica of this.replicas.values()) {
946
+ if (replica.lagMillis <= 100) {
947
+ distribution["0-100ms"]++;
948
+ } else if (replica.lagMillis <= 500) {
949
+ distribution["100-500ms"]++;
950
+ } else if (replica.lagMillis <= 1e3) {
951
+ distribution["500-1000ms"]++;
952
+ } else {
953
+ distribution["1000+ms"]++;
954
+ }
955
+ }
956
+ return distribution;
957
+ }
958
+ /**
959
+ * Check if can satisfy consistency level
960
+ */
961
+ canSatisfyConsistency(policyId, _requiredAcks) {
962
+ const policy = this.policies.get(policyId);
963
+ if (!policy) {
964
+ return false;
965
+ }
966
+ const healthyCount = this.getHealthyReplicas().length;
967
+ switch (policy.consistencyLevel) {
968
+ case "eventual":
969
+ return true;
970
+ // Always achievable
971
+ case "read-after-write":
972
+ return healthyCount >= 1;
973
+ case "strong":
974
+ return healthyCount >= policy.replicationFactor;
975
+ default:
976
+ return false;
977
+ }
978
+ }
979
+ /**
980
+ * Persist current replication state snapshot.
981
+ */
982
+ async saveToPersistence() {
983
+ if (!this.persistence) {
984
+ return;
985
+ }
986
+ const data = {
987
+ replicas: this.getAllReplicas(),
988
+ policies: this.getAllPolicies(),
989
+ syncStatus: Array.from(this.syncStatus.entries()).map(
990
+ ([nodeId, state]) => ({
991
+ nodeId,
992
+ synced: state.synced,
993
+ failed: state.failed
994
+ })
995
+ )
996
+ };
997
+ const envelope = {
998
+ version: 1,
999
+ updatedAt: Date.now(),
1000
+ data
1001
+ };
1002
+ const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
1003
+ await this.persistence.adapter.setItem(
1004
+ this.persistence.key,
1005
+ serialize(envelope)
1006
+ );
1007
+ }
1008
+ /**
1009
+ * Load replication snapshot from persistence.
1010
+ */
1011
+ async loadFromPersistence() {
1012
+ if (!this.persistence) {
1013
+ return { replicas: 0, policies: 0, syncStatus: 0 };
1014
+ }
1015
+ const raw = await this.persistence.adapter.getItem(this.persistence.key);
1016
+ if (!raw) {
1017
+ return { replicas: 0, policies: 0, syncStatus: 0 };
1018
+ }
1019
+ const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
1020
+ const envelope = deserialize(raw);
1021
+ if (envelope.version !== 1 || !envelope.data) {
1022
+ throw new Error("Invalid replication persistence payload");
1023
+ }
1024
+ if (!Array.isArray(envelope.data.replicas) || !Array.isArray(envelope.data.policies) || !Array.isArray(envelope.data.syncStatus)) {
1025
+ throw new Error("Invalid replication persistence structure");
1026
+ }
1027
+ this.replicas.clear();
1028
+ this.policies.clear();
1029
+ this.syncStatus.clear();
1030
+ this.replicasByDID.clear();
1031
+ let importedReplicas = 0;
1032
+ for (const replica of envelope.data.replicas) {
1033
+ if (this.isValidReplica(replica)) {
1034
+ this.replicas.set(replica.id, replica);
1035
+ if (replica.did) {
1036
+ this.replicasByDID.set(replica.did, replica.id);
1037
+ }
1038
+ importedReplicas++;
1039
+ }
1040
+ }
1041
+ let importedPolicies = 0;
1042
+ for (const policy of envelope.data.policies) {
1043
+ if (this.isValidPolicy(policy)) {
1044
+ this.policies.set(policy.id, policy);
1045
+ importedPolicies++;
1046
+ }
1047
+ }
1048
+ let importedSyncStatus = 0;
1049
+ for (const status of envelope.data.syncStatus) {
1050
+ if (typeof status.nodeId === "string" && typeof status.synced === "number" && typeof status.failed === "number") {
1051
+ this.syncStatus.set(status.nodeId, {
1052
+ synced: status.synced,
1053
+ failed: status.failed
1054
+ });
1055
+ importedSyncStatus++;
1056
+ }
1057
+ }
1058
+ logger.debug("[ReplicationManager] Loaded from persistence", {
1059
+ key: this.persistence.key,
1060
+ replicas: importedReplicas,
1061
+ policies: importedPolicies,
1062
+ syncStatus: importedSyncStatus
1063
+ });
1064
+ return {
1065
+ replicas: importedReplicas,
1066
+ policies: importedPolicies,
1067
+ syncStatus: importedSyncStatus
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Remove persisted replication snapshot.
1072
+ */
1073
+ async clearPersistence() {
1074
+ if (!this.persistence) {
1075
+ return;
1076
+ }
1077
+ await this.persistence.adapter.removeItem(this.persistence.key);
1078
+ }
1079
+ schedulePersist() {
1080
+ if (!this.persistence || this.persistence.autoPersist === false) {
1081
+ return;
1082
+ }
1083
+ if (this.persistTimer) {
1084
+ clearTimeout(this.persistTimer);
1085
+ }
1086
+ this.persistTimer = setTimeout(() => {
1087
+ void this.persistSafely();
1088
+ }, this.persistence.persistDebounceMs ?? 25);
1089
+ }
1090
+ async persistSafely() {
1091
+ if (!this.persistence) {
1092
+ return;
1093
+ }
1094
+ if (this.persistInFlight) {
1095
+ this.persistPending = true;
1096
+ return;
1097
+ }
1098
+ this.persistInFlight = true;
1099
+ try {
1100
+ await this.saveToPersistence();
1101
+ } catch (error) {
1102
+ logger.error("[ReplicationManager] Persistence write failed", {
1103
+ key: this.persistence.key,
1104
+ error: error instanceof Error ? error.message : String(error)
1105
+ });
1106
+ } finally {
1107
+ this.persistInFlight = false;
1108
+ const shouldRunAgain = this.persistPending;
1109
+ this.persistPending = false;
1110
+ if (shouldRunAgain) {
1111
+ void this.persistSafely();
1112
+ }
1113
+ }
1114
+ }
1115
+ isValidReplica(value) {
1116
+ if (typeof value !== "object" || value === null) {
1117
+ return false;
1118
+ }
1119
+ const candidate = value;
1120
+ const validStatus = candidate.status === "primary" || candidate.status === "secondary" || candidate.status === "syncing" || candidate.status === "failed";
1121
+ return typeof candidate.id === "string" && typeof candidate.nodeId === "string" && validStatus && typeof candidate.lastSyncTime === "string" && typeof candidate.lagBytes === "number" && typeof candidate.lagMillis === "number";
1122
+ }
1123
+ isValidPolicy(value) {
1124
+ if (typeof value !== "object" || value === null) {
1125
+ return false;
1126
+ }
1127
+ const candidate = value;
1128
+ const validConsistency = candidate.consistencyLevel === "eventual" || candidate.consistencyLevel === "read-after-write" || candidate.consistencyLevel === "strong";
1129
+ return typeof candidate.id === "string" && typeof candidate.name === "string" && typeof candidate.replicationFactor === "number" && validConsistency && typeof candidate.syncInterval === "number" && typeof candidate.maxReplicationLag === "number";
1130
+ }
1131
+ /**
1132
+ * Clear all state (for testing)
1133
+ */
1134
+ clear() {
1135
+ this.replicas.clear();
1136
+ this.policies.clear();
1137
+ this.replicationEvents = [];
1138
+ this.syncStatus.clear();
1139
+ this.replicasByDID.clear();
1140
+ this.cryptoProvider = null;
1141
+ this.schedulePersist();
1142
+ }
1143
+ /**
1144
+ * Get the crypto provider (for advanced usage)
1145
+ */
1146
+ getCryptoProvider() {
1147
+ return this.cryptoProvider;
1148
+ }
1149
+ };
1150
+
1151
+ // src/distributed/SyncProtocol.ts
1152
+ var SyncProtocol = class _SyncProtocol {
1153
+ static DEFAULT_PERSIST_KEY = "aeon:sync-protocol:v1";
1154
+ version = "1.0.0";
1155
+ messageQueue = [];
1156
+ messageMap = /* @__PURE__ */ new Map();
1157
+ handshakes = /* @__PURE__ */ new Map();
1158
+ protocolErrors = [];
1159
+ messageCounter = 0;
1160
+ // Crypto support
1161
+ cryptoProvider = null;
1162
+ cryptoConfig = null;
1163
+ persistence = null;
1164
+ persistTimer = null;
1165
+ persistInFlight = false;
1166
+ persistPending = false;
1167
+ constructor(options) {
1168
+ if (options?.persistence) {
1169
+ this.persistence = {
1170
+ ...options.persistence,
1171
+ key: options.persistence.key ?? _SyncProtocol.DEFAULT_PERSIST_KEY,
1172
+ autoPersist: options.persistence.autoPersist ?? true,
1173
+ autoLoad: options.persistence.autoLoad ?? false,
1174
+ persistDebounceMs: options.persistence.persistDebounceMs ?? 25
1175
+ };
1176
+ }
1177
+ if (this.persistence?.autoLoad) {
1178
+ void this.loadFromPersistence().catch((error) => {
1179
+ logger.error("[SyncProtocol] Failed to load persistence", {
1180
+ key: this.persistence?.key,
1181
+ error: error instanceof Error ? error.message : String(error)
1182
+ });
1183
+ });
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Configure cryptographic provider for authenticated/encrypted messages
1188
+ */
1189
+ configureCrypto(provider, config) {
1190
+ this.cryptoProvider = provider;
1191
+ this.cryptoConfig = {
1192
+ encryptionMode: config?.encryptionMode ?? "none",
1193
+ requireSignatures: config?.requireSignatures ?? false,
1194
+ requireCapabilities: config?.requireCapabilities ?? false,
1195
+ requiredCapabilities: config?.requiredCapabilities
1196
+ };
1197
+ logger.debug("[SyncProtocol] Crypto configured", {
1198
+ encryptionMode: this.cryptoConfig.encryptionMode,
1199
+ requireSignatures: this.cryptoConfig.requireSignatures,
1200
+ requireCapabilities: this.cryptoConfig.requireCapabilities
1201
+ });
1202
+ }
1203
+ /**
1204
+ * Check if crypto is configured
1205
+ */
1206
+ isCryptoEnabled() {
1207
+ return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
1208
+ }
1209
+ /**
1210
+ * Get crypto configuration
1211
+ */
1212
+ getCryptoConfig() {
1213
+ return this.cryptoConfig ? { ...this.cryptoConfig } : null;
1214
+ }
1215
+ /**
1216
+ * Get protocol version
1217
+ */
1218
+ getVersion() {
1219
+ return this.version;
1220
+ }
1221
+ /**
1222
+ * Create authenticated handshake message with DID and keys
1223
+ */
1224
+ async createAuthenticatedHandshake(capabilities, targetDID) {
1225
+ if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
1226
+ throw new Error("Crypto provider not initialized");
1227
+ }
1228
+ const localDID = this.cryptoProvider.getLocalDID();
1229
+ if (!localDID) {
1230
+ throw new Error("Local DID not available");
1231
+ }
1232
+ const publicInfo = await this.cryptoProvider.exportPublicIdentity();
1233
+ if (!publicInfo) {
1234
+ throw new Error("Cannot export public identity");
1235
+ }
1236
+ let ucan;
1237
+ if (targetDID && this.cryptoConfig?.requireCapabilities) {
1238
+ const caps = this.cryptoConfig.requiredCapabilities || [
1239
+ { can: "aeon:sync:read", with: "*" },
1240
+ { can: "aeon:sync:write", with: "*" }
1241
+ ];
1242
+ ucan = await this.cryptoProvider.createUCAN(targetDID, caps);
1243
+ }
1244
+ const handshakePayload = {
1245
+ protocolVersion: this.version,
1246
+ nodeId: localDID,
1247
+ capabilities,
1248
+ state: "initiating",
1249
+ did: localDID,
1250
+ publicSigningKey: publicInfo.publicSigningKey,
1251
+ publicEncryptionKey: publicInfo.publicEncryptionKey,
1252
+ ucan
1253
+ };
1254
+ const message = {
1255
+ type: "handshake",
1256
+ version: this.version,
1257
+ sender: localDID,
1258
+ receiver: targetDID || "",
1259
+ messageId: this.generateMessageId(),
1260
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1261
+ payload: handshakePayload
1262
+ };
1263
+ if (this.cryptoConfig?.requireSignatures) {
1264
+ const signed = await this.cryptoProvider.signData(handshakePayload);
1265
+ message.auth = {
1266
+ senderDID: localDID,
1267
+ receiverDID: targetDID,
1268
+ signature: signed.signature
1269
+ };
1270
+ }
1271
+ this.messageMap.set(message.messageId, message);
1272
+ this.messageQueue.push(message);
1273
+ this.schedulePersist();
1274
+ logger.debug("[SyncProtocol] Authenticated handshake created", {
1275
+ messageId: message.messageId,
1276
+ did: localDID,
1277
+ capabilities: capabilities.length,
1278
+ hasUCAN: !!ucan
1279
+ });
1280
+ return message;
1281
+ }
1282
+ /**
1283
+ * Verify and process an authenticated handshake
1284
+ */
1285
+ async verifyAuthenticatedHandshake(message) {
1286
+ if (message.type !== "handshake") {
1287
+ return { valid: false, error: "Message is not a handshake" };
1288
+ }
1289
+ const handshake = message.payload;
1290
+ if (!this.cryptoProvider || !this.cryptoConfig) {
1291
+ this.handshakes.set(message.sender, handshake);
1292
+ this.schedulePersist();
1293
+ return { valid: true, handshake };
1294
+ }
1295
+ if (handshake.did && handshake.publicSigningKey) {
1296
+ await this.cryptoProvider.registerRemoteNode({
1297
+ id: handshake.nodeId,
1298
+ did: handshake.did,
1299
+ publicSigningKey: handshake.publicSigningKey,
1300
+ publicEncryptionKey: handshake.publicEncryptionKey
1301
+ });
1302
+ }
1303
+ if (this.cryptoConfig.requireSignatures && message.auth?.signature) {
1304
+ const signed = {
1305
+ payload: handshake,
1306
+ signature: message.auth.signature,
1307
+ signer: message.auth.senderDID || message.sender,
1308
+ algorithm: "ES256",
1309
+ signedAt: Date.now()
1310
+ };
1311
+ const isValid = await this.cryptoProvider.verifySignedData(signed);
1312
+ if (!isValid) {
1313
+ logger.warn("[SyncProtocol] Handshake signature verification failed", {
1314
+ messageId: message.messageId,
1315
+ sender: message.sender
1316
+ });
1317
+ return { valid: false, error: "Invalid signature" };
1318
+ }
1319
+ }
1320
+ if (this.cryptoConfig.requireCapabilities && handshake.ucan) {
1321
+ const localDID = this.cryptoProvider.getLocalDID();
1322
+ const result = await this.cryptoProvider.verifyUCAN(handshake.ucan, {
1323
+ expectedAudience: localDID || void 0,
1324
+ requiredCapabilities: this.cryptoConfig.requiredCapabilities
1325
+ });
1326
+ if (!result.authorized) {
1327
+ logger.warn("[SyncProtocol] Handshake UCAN verification failed", {
1328
+ messageId: message.messageId,
1329
+ error: result.error
1330
+ });
1331
+ return { valid: false, error: result.error || "Unauthorized" };
1332
+ }
1333
+ }
1334
+ this.handshakes.set(message.sender, handshake);
1335
+ this.schedulePersist();
1336
+ logger.debug("[SyncProtocol] Authenticated handshake verified", {
1337
+ messageId: message.messageId,
1338
+ did: handshake.did
1339
+ });
1340
+ return { valid: true, handshake };
1341
+ }
1342
+ /**
1343
+ * Sign and optionally encrypt a message payload
1344
+ */
1345
+ async signMessage(message, payload, encrypt = false) {
1346
+ if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
1347
+ throw new Error("Crypto provider not initialized");
1348
+ }
1349
+ const localDID = this.cryptoProvider.getLocalDID();
1350
+ const signed = await this.cryptoProvider.signData(payload);
1351
+ message.auth = {
1352
+ senderDID: localDID || void 0,
1353
+ receiverDID: message.receiver || void 0,
1354
+ signature: signed.signature,
1355
+ encrypted: false
1356
+ };
1357
+ if (encrypt && message.receiver && this.cryptoConfig?.encryptionMode !== "none") {
1358
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
1359
+ const encrypted = await this.cryptoProvider.encrypt(
1360
+ payloadBytes,
1361
+ message.receiver
1362
+ );
1363
+ message.payload = encrypted;
1364
+ message.auth.encrypted = true;
1365
+ logger.debug("[SyncProtocol] Message encrypted", {
1366
+ messageId: message.messageId,
1367
+ recipient: message.receiver
1368
+ });
1369
+ } else {
1370
+ message.payload = payload;
1371
+ }
1372
+ return message;
1373
+ }
1374
+ /**
1375
+ * Verify signature and optionally decrypt a message
1376
+ */
1377
+ async verifyMessage(message) {
1378
+ if (!this.cryptoProvider || !message.auth) {
1379
+ return { valid: true, payload: message.payload };
1380
+ }
1381
+ let payload = message.payload;
1382
+ if (message.auth.encrypted && message.payload) {
1383
+ try {
1384
+ const encrypted = message.payload;
1385
+ const decrypted = await this.cryptoProvider.decrypt(
1386
+ encrypted,
1387
+ message.auth.senderDID
1388
+ );
1389
+ payload = JSON.parse(new TextDecoder().decode(decrypted));
1390
+ logger.debug("[SyncProtocol] Message decrypted", {
1391
+ messageId: message.messageId
1392
+ });
1393
+ } catch (error) {
1394
+ return {
1395
+ valid: false,
1396
+ error: `Decryption failed: ${error instanceof Error ? error.message : String(error)}`
1397
+ };
1398
+ }
1399
+ }
1400
+ if (message.auth.signature && message.auth.senderDID) {
1401
+ const signed = {
1402
+ payload,
1403
+ signature: message.auth.signature,
1404
+ signer: message.auth.senderDID,
1405
+ algorithm: "ES256",
1406
+ signedAt: Date.now()
1407
+ };
1408
+ const isValid = await this.cryptoProvider.verifySignedData(signed);
1409
+ if (!isValid) {
1410
+ return { valid: false, error: "Invalid signature" };
1411
+ }
1412
+ }
1413
+ return { valid: true, payload };
1414
+ }
1415
+ /**
1416
+ * Create handshake message
1417
+ */
1418
+ createHandshakeMessage(nodeId, capabilities) {
1419
+ const message = {
1420
+ type: "handshake",
1421
+ version: this.version,
1422
+ sender: nodeId,
1423
+ receiver: "",
1424
+ messageId: this.generateMessageId(),
1425
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1426
+ payload: {
1427
+ protocolVersion: this.version,
1428
+ nodeId,
1429
+ capabilities,
1430
+ state: "initiating"
1431
+ }
1432
+ };
1433
+ this.messageMap.set(message.messageId, message);
1434
+ this.messageQueue.push(message);
1435
+ this.schedulePersist();
1436
+ logger.debug("[SyncProtocol] Handshake message created", {
1437
+ messageId: message.messageId,
1438
+ nodeId,
1439
+ capabilities: capabilities.length
1440
+ });
1441
+ return message;
1442
+ }
1443
+ /**
1444
+ * Create sync request message
1445
+ */
1446
+ createSyncRequestMessage(sender, receiver, sessionId, fromVersion, toVersion, filter) {
1447
+ const message = {
1448
+ type: "sync-request",
1449
+ version: this.version,
1450
+ sender,
1451
+ receiver,
1452
+ messageId: this.generateMessageId(),
1453
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1454
+ payload: {
1455
+ sessionId,
1456
+ fromVersion,
1457
+ toVersion,
1458
+ filter
1459
+ }
1460
+ };
1461
+ this.messageMap.set(message.messageId, message);
1462
+ this.messageQueue.push(message);
1463
+ this.schedulePersist();
1464
+ logger.debug("[SyncProtocol] Sync request created", {
1465
+ messageId: message.messageId,
1466
+ sessionId,
1467
+ fromVersion,
1468
+ toVersion
1469
+ });
1470
+ return message;
1471
+ }
1472
+ /**
1473
+ * Create sync response message
1474
+ */
1475
+ createSyncResponseMessage(sender, receiver, sessionId, fromVersion, toVersion, data, hasMore = false, offset = 0) {
1476
+ const message = {
1477
+ type: "sync-response",
1478
+ version: this.version,
1479
+ sender,
1480
+ receiver,
1481
+ messageId: this.generateMessageId(),
1482
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1483
+ payload: {
1484
+ sessionId,
1485
+ fromVersion,
1486
+ toVersion,
1487
+ data,
1488
+ hasMore,
1489
+ offset
1490
+ }
1491
+ };
1492
+ this.messageMap.set(message.messageId, message);
1493
+ this.messageQueue.push(message);
1494
+ this.schedulePersist();
1495
+ logger.debug("[SyncProtocol] Sync response created", {
1496
+ messageId: message.messageId,
1497
+ sessionId,
1498
+ itemCount: data.length,
1499
+ hasMore
1500
+ });
1501
+ return message;
1502
+ }
1503
+ /**
1504
+ * Create acknowledgement message
1505
+ */
1506
+ createAckMessage(sender, receiver, messageId) {
1507
+ const message = {
1508
+ type: "ack",
1509
+ version: this.version,
1510
+ sender,
1511
+ receiver,
1512
+ messageId: this.generateMessageId(),
1513
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1514
+ payload: { acknowledgedMessageId: messageId }
1515
+ };
1516
+ this.messageMap.set(message.messageId, message);
1517
+ this.messageQueue.push(message);
1518
+ this.schedulePersist();
1519
+ return message;
1520
+ }
1521
+ /**
1522
+ * Create error message
1523
+ */
1524
+ createErrorMessage(sender, receiver, error, relatedMessageId) {
1525
+ const message = {
1526
+ type: "error",
1527
+ version: this.version,
1528
+ sender,
1529
+ receiver,
1530
+ messageId: this.generateMessageId(),
1531
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1532
+ payload: {
1533
+ error,
1534
+ relatedMessageId
1535
+ }
1536
+ };
1537
+ this.messageMap.set(message.messageId, message);
1538
+ this.messageQueue.push(message);
1539
+ this.protocolErrors.push({
1540
+ error,
1541
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1542
+ });
1543
+ this.schedulePersist();
1544
+ logger.error("[SyncProtocol] Error message created", {
1545
+ messageId: message.messageId,
1546
+ errorCode: error.code,
1547
+ recoverable: error.recoverable
1548
+ });
1549
+ return message;
1550
+ }
1551
+ /**
1552
+ * Validate message
1553
+ */
1554
+ validateMessage(message) {
1555
+ const errors = [];
1556
+ if (!message.type) {
1557
+ errors.push("Message type is required");
1558
+ }
1559
+ if (!message.sender) {
1560
+ errors.push("Sender is required");
1561
+ }
1562
+ if (!message.messageId) {
1563
+ errors.push("Message ID is required");
1564
+ }
1565
+ if (!message.timestamp) {
1566
+ errors.push("Timestamp is required");
1567
+ }
1568
+ const timestampValue = new Date(message.timestamp);
1569
+ if (Number.isNaN(timestampValue.getTime())) {
1570
+ errors.push("Invalid timestamp format");
1571
+ }
1572
+ return {
1573
+ valid: errors.length === 0,
1574
+ errors
1575
+ };
1576
+ }
1577
+ /**
1578
+ * Serialize message
1579
+ */
1580
+ serializeMessage(message) {
1581
+ try {
1582
+ return JSON.stringify(message);
1583
+ } catch (error) {
1584
+ logger.error("[SyncProtocol] Message serialization failed", {
1585
+ messageId: message.messageId,
1586
+ error: error instanceof Error ? error.message : String(error)
1587
+ });
1588
+ throw new Error(
1589
+ `Failed to serialize message: ${error instanceof Error ? error.message : String(error)}`
1590
+ );
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Deserialize message
1595
+ */
1596
+ deserializeMessage(data) {
1597
+ try {
1598
+ const message = JSON.parse(data);
1599
+ const validation = this.validateMessage(message);
1600
+ if (!validation.valid) {
1601
+ throw new Error(`Invalid message: ${validation.errors.join(", ")}`);
1602
+ }
1603
+ return message;
1604
+ } catch (error) {
1605
+ logger.error("[SyncProtocol] Message deserialization failed", {
1606
+ error: error instanceof Error ? error.message : String(error)
1607
+ });
1608
+ throw new Error(
1609
+ `Failed to deserialize message: ${error instanceof Error ? error.message : String(error)}`
1610
+ );
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Process handshake
1615
+ */
1616
+ processHandshake(message) {
1617
+ if (message.type !== "handshake") {
1618
+ throw new Error("Message is not a handshake");
1619
+ }
1620
+ const handshake = message.payload;
1621
+ const nodeId = message.sender;
1622
+ this.handshakes.set(nodeId, handshake);
1623
+ this.schedulePersist();
1624
+ logger.debug("[SyncProtocol] Handshake processed", {
1625
+ nodeId,
1626
+ protocolVersion: handshake.protocolVersion,
1627
+ capabilities: handshake.capabilities.length
1628
+ });
1629
+ return handshake;
1630
+ }
1631
+ /**
1632
+ * Get message
1633
+ */
1634
+ getMessage(messageId) {
1635
+ return this.messageMap.get(messageId);
1636
+ }
1637
+ /**
1638
+ * Get all messages
1639
+ */
1640
+ getAllMessages() {
1641
+ return [...this.messageQueue];
1642
+ }
1643
+ /**
1644
+ * Get messages by type
1645
+ */
1646
+ getMessagesByType(type) {
1647
+ return this.messageQueue.filter((m) => m.type === type);
1648
+ }
1649
+ /**
1650
+ * Get messages from sender
1651
+ */
1652
+ getMessagesFromSender(sender) {
1653
+ return this.messageQueue.filter((m) => m.sender === sender);
1654
+ }
1655
+ /**
1656
+ * Get pending messages
1657
+ */
1658
+ getPendingMessages(receiver) {
1659
+ return this.messageQueue.filter((m) => m.receiver === receiver);
1660
+ }
1661
+ /**
1662
+ * Get handshakes
1663
+ */
1664
+ getHandshakes() {
1665
+ return new Map(this.handshakes);
1666
+ }
1667
+ /**
1668
+ * Get protocol statistics
1669
+ */
1670
+ getStatistics() {
1671
+ const messagesByType = {};
1672
+ for (const message of this.messageQueue) {
1673
+ messagesByType[message.type] = (messagesByType[message.type] || 0) + 1;
1674
+ }
1675
+ const errorCount = this.protocolErrors.length;
1676
+ const recoverableErrors = this.protocolErrors.filter(
1677
+ (e) => e.error.recoverable
1678
+ ).length;
1679
+ return {
1680
+ totalMessages: this.messageQueue.length,
1681
+ messagesByType,
1682
+ totalHandshakes: this.handshakes.size,
1683
+ totalErrors: errorCount,
1684
+ recoverableErrors,
1685
+ unrecoverableErrors: errorCount - recoverableErrors
1686
+ };
1687
+ }
1688
+ /**
1689
+ * Get protocol errors
1690
+ */
1691
+ getErrors() {
1692
+ return [...this.protocolErrors];
1693
+ }
1694
+ /**
1695
+ * Persist protocol state for reconnect/replay.
1696
+ */
1697
+ async saveToPersistence() {
1698
+ if (!this.persistence) {
1699
+ return;
1700
+ }
1701
+ const data = {
1702
+ protocolVersion: this.version,
1703
+ messageCounter: this.messageCounter,
1704
+ messageQueue: this.getAllMessages(),
1705
+ handshakes: Array.from(this.handshakes.entries()).map(
1706
+ ([nodeId, handshake]) => ({
1707
+ nodeId,
1708
+ handshake
1709
+ })
1710
+ ),
1711
+ protocolErrors: this.getErrors()
1712
+ };
1713
+ const envelope = {
1714
+ version: 1,
1715
+ updatedAt: Date.now(),
1716
+ data
1717
+ };
1718
+ const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
1719
+ await this.persistence.adapter.setItem(
1720
+ this.persistence.key,
1721
+ serialize(envelope)
1722
+ );
1723
+ }
1724
+ /**
1725
+ * Load protocol state from persistence.
1726
+ */
1727
+ async loadFromPersistence() {
1728
+ if (!this.persistence) {
1729
+ return { messages: 0, handshakes: 0, errors: 0 };
1730
+ }
1731
+ const raw = await this.persistence.adapter.getItem(this.persistence.key);
1732
+ if (!raw) {
1733
+ return { messages: 0, handshakes: 0, errors: 0 };
1734
+ }
1735
+ const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
1736
+ const envelope = deserialize(raw);
1737
+ if (envelope.version !== 1 || !envelope.data) {
1738
+ throw new Error("Invalid sync protocol persistence payload");
1739
+ }
1740
+ if (!Array.isArray(envelope.data.messageQueue) || !Array.isArray(envelope.data.handshakes) || !Array.isArray(envelope.data.protocolErrors)) {
1741
+ throw new Error("Invalid sync protocol persistence structure");
1742
+ }
1743
+ const nextMessages = [];
1744
+ for (const message of envelope.data.messageQueue) {
1745
+ const validation = this.validateMessage(message);
1746
+ if (!validation.valid) {
1747
+ throw new Error(
1748
+ `Invalid persisted message ${message?.messageId ?? "unknown"}: ${validation.errors.join(", ")}`
1749
+ );
1750
+ }
1751
+ nextMessages.push(message);
1752
+ }
1753
+ const nextHandshakes = /* @__PURE__ */ new Map();
1754
+ for (const entry of envelope.data.handshakes) {
1755
+ if (typeof entry.nodeId !== "string" || !this.isValidHandshake(entry.handshake)) {
1756
+ throw new Error("Invalid persisted handshake payload");
1757
+ }
1758
+ nextHandshakes.set(entry.nodeId, entry.handshake);
1759
+ }
1760
+ const nextErrors = [];
1761
+ for (const entry of envelope.data.protocolErrors) {
1762
+ if (!this.isValidProtocolErrorEntry(entry)) {
1763
+ throw new Error("Invalid persisted protocol error payload");
1764
+ }
1765
+ nextErrors.push(entry);
1766
+ }
1767
+ this.messageQueue = nextMessages;
1768
+ this.messageMap = new Map(nextMessages.map((m) => [m.messageId, m]));
1769
+ this.handshakes = nextHandshakes;
1770
+ this.protocolErrors = nextErrors;
1771
+ this.messageCounter = Math.max(
1772
+ envelope.data.messageCounter || 0,
1773
+ this.messageQueue.length
1774
+ );
1775
+ logger.debug("[SyncProtocol] Loaded from persistence", {
1776
+ key: this.persistence.key,
1777
+ messages: this.messageQueue.length,
1778
+ handshakes: this.handshakes.size,
1779
+ errors: this.protocolErrors.length
1780
+ });
1781
+ return {
1782
+ messages: this.messageQueue.length,
1783
+ handshakes: this.handshakes.size,
1784
+ errors: this.protocolErrors.length
1785
+ };
1786
+ }
1787
+ /**
1788
+ * Clear persisted protocol checkpoint.
1789
+ */
1790
+ async clearPersistence() {
1791
+ if (!this.persistence) {
1792
+ return;
1793
+ }
1794
+ await this.persistence.adapter.removeItem(this.persistence.key);
1795
+ }
1796
+ schedulePersist() {
1797
+ if (!this.persistence || this.persistence.autoPersist === false) {
1798
+ return;
1799
+ }
1800
+ if (this.persistTimer) {
1801
+ clearTimeout(this.persistTimer);
1802
+ }
1803
+ this.persistTimer = setTimeout(() => {
1804
+ void this.persistSafely();
1805
+ }, this.persistence.persistDebounceMs ?? 25);
1806
+ }
1807
+ async persistSafely() {
1808
+ if (!this.persistence) {
1809
+ return;
1810
+ }
1811
+ if (this.persistInFlight) {
1812
+ this.persistPending = true;
1813
+ return;
1814
+ }
1815
+ this.persistInFlight = true;
1816
+ try {
1817
+ await this.saveToPersistence();
1818
+ } catch (error) {
1819
+ logger.error("[SyncProtocol] Persistence write failed", {
1820
+ key: this.persistence.key,
1821
+ error: error instanceof Error ? error.message : String(error)
1822
+ });
1823
+ } finally {
1824
+ this.persistInFlight = false;
1825
+ const shouldRunAgain = this.persistPending;
1826
+ this.persistPending = false;
1827
+ if (shouldRunAgain) {
1828
+ void this.persistSafely();
1829
+ }
1830
+ }
1831
+ }
1832
+ isValidHandshake(value) {
1833
+ if (typeof value !== "object" || value === null) {
1834
+ return false;
1835
+ }
1836
+ const handshake = value;
1837
+ const validState = handshake.state === "initiating" || handshake.state === "responding" || handshake.state === "completed";
1838
+ return typeof handshake.protocolVersion === "string" && typeof handshake.nodeId === "string" && Array.isArray(handshake.capabilities) && handshake.capabilities.every((cap) => typeof cap === "string") && validState;
1839
+ }
1840
+ isValidProtocolErrorEntry(entry) {
1841
+ if (typeof entry !== "object" || entry === null) {
1842
+ return false;
1843
+ }
1844
+ const candidate = entry;
1845
+ return typeof candidate.timestamp === "string" && typeof candidate.error?.code === "string" && typeof candidate.error.message === "string" && typeof candidate.error.recoverable === "boolean";
1846
+ }
1847
+ /**
1848
+ * Generate message ID
1849
+ */
1850
+ generateMessageId() {
1851
+ this.messageCounter++;
1852
+ return `msg-${Date.now()}-${this.messageCounter}`;
1853
+ }
1854
+ /**
1855
+ * Clear all state (for testing)
1856
+ */
1857
+ clear() {
1858
+ this.messageQueue = [];
1859
+ this.messageMap.clear();
1860
+ this.handshakes.clear();
1861
+ this.protocolErrors = [];
1862
+ this.messageCounter = 0;
1863
+ this.cryptoProvider = null;
1864
+ this.cryptoConfig = null;
1865
+ this.schedulePersist();
1866
+ }
1867
+ /**
1868
+ * Get the crypto provider (for advanced usage)
1869
+ */
1870
+ getCryptoProvider() {
1871
+ return this.cryptoProvider;
1872
+ }
1873
+ };
1874
+
1875
+ // src/distributed/StateReconciler.ts
1876
+ var StateReconciler = class {
1877
+ stateVersions = /* @__PURE__ */ new Map();
1878
+ reconciliationHistory = [];
1879
+ cryptoProvider = null;
1880
+ requireSignedVersions = false;
1881
+ /**
1882
+ * Configure cryptographic provider for signed state versions
1883
+ */
1884
+ configureCrypto(provider, requireSigned = false) {
1885
+ this.cryptoProvider = provider;
1886
+ this.requireSignedVersions = requireSigned;
1887
+ logger.debug("[StateReconciler] Crypto configured", {
1888
+ initialized: provider.isInitialized(),
1889
+ requireSigned
1890
+ });
1891
+ }
1892
+ /**
1893
+ * Check if crypto is configured
1894
+ */
1895
+ isCryptoEnabled() {
1896
+ return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
1897
+ }
1898
+ /**
1899
+ * Record a signed state version with cryptographic verification
1900
+ */
1901
+ async recordSignedStateVersion(key, version, data) {
1902
+ if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
1903
+ throw new Error("Crypto provider not initialized");
1904
+ }
1905
+ const localDID = this.cryptoProvider.getLocalDID();
1906
+ if (!localDID) {
1907
+ throw new Error("Local DID not available");
1908
+ }
1909
+ const dataBytes = new TextEncoder().encode(JSON.stringify(data));
1910
+ const hashBytes = await this.cryptoProvider.hash(dataBytes);
1911
+ const hash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1912
+ const versionData = { version, data, hash };
1913
+ const signed = await this.cryptoProvider.signData(versionData);
1914
+ const stateVersion = {
1915
+ version,
1916
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1917
+ nodeId: localDID,
1918
+ hash,
1919
+ data,
1920
+ signerDID: localDID,
1921
+ signature: signed.signature,
1922
+ signedAt: signed.signedAt
1923
+ };
1924
+ if (!this.stateVersions.has(key)) {
1925
+ this.stateVersions.set(key, []);
1926
+ }
1927
+ this.stateVersions.get(key).push(stateVersion);
1928
+ logger.debug("[StateReconciler] Signed state version recorded", {
1929
+ key,
1930
+ version,
1931
+ signerDID: localDID,
1932
+ hash: hash.slice(0, 16) + "..."
1933
+ });
1934
+ return stateVersion;
1935
+ }
1936
+ /**
1937
+ * Verify a state version's signature
1938
+ */
1939
+ async verifyStateVersion(version) {
1940
+ if (!version.signature || !version.signerDID) {
1941
+ if (this.requireSignedVersions) {
1942
+ return { valid: false, error: "Signature required but not present" };
1943
+ }
1944
+ const dataBytes = new TextEncoder().encode(JSON.stringify(version.data));
1945
+ if (this.cryptoProvider) {
1946
+ const hashBytes = await this.cryptoProvider.hash(dataBytes);
1947
+ const computedHash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1948
+ if (computedHash !== version.hash) {
1949
+ return { valid: false, error: "Hash mismatch" };
1950
+ }
1951
+ }
1952
+ return { valid: true };
1953
+ }
1954
+ if (!this.cryptoProvider) {
1955
+ return { valid: false, error: "Crypto provider not configured" };
1956
+ }
1957
+ const versionData = {
1958
+ version: version.version,
1959
+ data: version.data,
1960
+ hash: version.hash
1961
+ };
1962
+ const signed = {
1963
+ payload: versionData,
1964
+ signature: version.signature,
1965
+ signer: version.signerDID,
1966
+ algorithm: "ES256",
1967
+ signedAt: version.signedAt || Date.now()
1968
+ };
1969
+ const isValid = await this.cryptoProvider.verifySignedData(signed);
1970
+ if (!isValid) {
1971
+ return { valid: false, error: "Invalid signature" };
1972
+ }
1973
+ return { valid: true };
1974
+ }
1975
+ /**
1976
+ * Reconcile with verification - only accept verified versions
1977
+ */
1978
+ async reconcileWithVerification(key, strategy = "last-write-wins") {
1979
+ const versions = this.stateVersions.get(key) || [];
1980
+ const verifiedVersions = [];
1981
+ const verificationErrors = [];
1982
+ for (const version of versions) {
1983
+ const result2 = await this.verifyStateVersion(version);
1984
+ if (result2.valid) {
1985
+ verifiedVersions.push(version);
1986
+ } else {
1987
+ verificationErrors.push(
1988
+ `Version ${version.version} from ${version.nodeId}: ${result2.error}`
1989
+ );
1990
+ logger.warn("[StateReconciler] Version verification failed", {
1991
+ version: version.version,
1992
+ nodeId: version.nodeId,
1993
+ error: result2.error
1994
+ });
1995
+ }
1996
+ }
1997
+ if (verifiedVersions.length === 0) {
1998
+ return {
1999
+ success: false,
2000
+ mergedState: null,
2001
+ conflictsResolved: 0,
2002
+ strategy,
2003
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2004
+ verificationErrors
2005
+ };
2006
+ }
2007
+ let result;
2008
+ switch (strategy) {
2009
+ case "last-write-wins":
2010
+ result = this.reconcileLastWriteWins(verifiedVersions);
2011
+ break;
2012
+ case "vector-clock":
2013
+ result = this.reconcileVectorClock(verifiedVersions);
2014
+ break;
2015
+ case "majority-vote":
2016
+ result = this.reconcileMajorityVote(verifiedVersions);
2017
+ break;
2018
+ default:
2019
+ result = this.reconcileLastWriteWins(verifiedVersions);
2020
+ }
2021
+ return { ...result, verificationErrors };
2022
+ }
2023
+ /**
2024
+ * Record a state version
2025
+ */
2026
+ recordStateVersion(key, version, timestamp, nodeId, hash, data) {
2027
+ if (!this.stateVersions.has(key)) {
2028
+ this.stateVersions.set(key, []);
2029
+ }
2030
+ const versions = this.stateVersions.get(key);
2031
+ versions.push({
2032
+ version,
2033
+ timestamp,
2034
+ nodeId,
2035
+ hash,
2036
+ data
2037
+ });
2038
+ logger.debug("[StateReconciler] State version recorded", {
2039
+ key,
2040
+ version,
2041
+ nodeId,
2042
+ hash
2043
+ });
2044
+ }
2045
+ /**
2046
+ * Detect conflicts in state versions
2047
+ */
2048
+ detectConflicts(key) {
2049
+ const versions = this.stateVersions.get(key);
2050
+ if (!versions || versions.length <= 1) {
2051
+ return false;
2052
+ }
2053
+ const hashes = new Set(versions.map((v) => v.hash));
2054
+ return hashes.size > 1;
2055
+ }
2056
+ /**
2057
+ * Compare two states and generate diff
2058
+ */
2059
+ compareStates(state1, state2) {
2060
+ const diff = {
2061
+ added: {},
2062
+ modified: {},
2063
+ removed: [],
2064
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2065
+ };
2066
+ for (const [key, value] of Object.entries(state2)) {
2067
+ if (!(key in state1)) {
2068
+ diff.added[key] = value;
2069
+ } else if (JSON.stringify(state1[key]) !== JSON.stringify(value)) {
2070
+ diff.modified[key] = { old: state1[key], new: value };
2071
+ }
2072
+ }
2073
+ for (const key of Object.keys(state1)) {
2074
+ if (!(key in state2)) {
2075
+ diff.removed.push(key);
2076
+ }
2077
+ }
2078
+ return diff;
2079
+ }
2080
+ /**
2081
+ * Reconcile states using last-write-wins strategy
2082
+ */
2083
+ reconcileLastWriteWins(versions) {
2084
+ if (versions.length === 0) {
2085
+ throw new Error("No versions to reconcile");
2086
+ }
2087
+ const sorted = [...versions].sort(
2088
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
2089
+ );
2090
+ const latest = sorted[0];
2091
+ const conflictsResolved = versions.length - 1;
2092
+ const result = {
2093
+ success: true,
2094
+ mergedState: latest.data,
2095
+ conflictsResolved,
2096
+ strategy: "last-write-wins",
2097
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2098
+ };
2099
+ this.reconciliationHistory.push(result);
2100
+ logger.debug("[StateReconciler] State reconciled (last-write-wins)", {
2101
+ winnerNode: latest.nodeId,
2102
+ conflictsResolved
2103
+ });
2104
+ return result;
2105
+ }
2106
+ /**
2107
+ * Reconcile states using vector clock strategy
2108
+ */
2109
+ reconcileVectorClock(versions) {
2110
+ if (versions.length === 0) {
2111
+ throw new Error("No versions to reconcile");
2112
+ }
2113
+ const sorted = [...versions].sort(
2114
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
2115
+ );
2116
+ const latest = sorted[0];
2117
+ let conflictsResolved = 0;
2118
+ for (const v of versions) {
2119
+ const timeDiff = Math.abs(
2120
+ new Date(v.timestamp).getTime() - new Date(latest.timestamp).getTime()
2121
+ );
2122
+ if (timeDiff > 100) {
2123
+ conflictsResolved++;
2124
+ }
2125
+ }
2126
+ const result = {
2127
+ success: true,
2128
+ mergedState: latest.data,
2129
+ conflictsResolved,
2130
+ strategy: "vector-clock",
2131
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2132
+ };
2133
+ this.reconciliationHistory.push(result);
2134
+ logger.debug("[StateReconciler] State reconciled (vector-clock)", {
2135
+ winnerVersion: latest.version,
2136
+ conflictsResolved
2137
+ });
2138
+ return result;
2139
+ }
2140
+ /**
2141
+ * Reconcile states using majority vote strategy
2142
+ */
2143
+ reconcileMajorityVote(versions) {
2144
+ if (versions.length === 0) {
2145
+ throw new Error("No versions to reconcile");
2146
+ }
2147
+ const hashGroups = /* @__PURE__ */ new Map();
2148
+ for (const version of versions) {
2149
+ if (!hashGroups.has(version.hash)) {
2150
+ hashGroups.set(version.hash, []);
2151
+ }
2152
+ hashGroups.get(version.hash).push(version);
2153
+ }
2154
+ let majorityVersion = null;
2155
+ let maxCount = 0;
2156
+ for (const [, versionGroup] of hashGroups) {
2157
+ if (versionGroup.length > maxCount) {
2158
+ maxCount = versionGroup.length;
2159
+ majorityVersion = versionGroup[0];
2160
+ }
2161
+ }
2162
+ if (!majorityVersion) {
2163
+ majorityVersion = versions[0];
2164
+ }
2165
+ const conflictsResolved = versions.length - maxCount;
2166
+ const result = {
2167
+ success: true,
2168
+ mergedState: majorityVersion.data,
2169
+ conflictsResolved,
2170
+ strategy: "majority-vote",
2171
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2172
+ };
2173
+ this.reconciliationHistory.push(result);
2174
+ logger.debug("[StateReconciler] State reconciled (majority-vote)", {
2175
+ majorityCount: maxCount,
2176
+ conflictsResolved
2177
+ });
2178
+ return result;
2179
+ }
2180
+ /**
2181
+ * Merge multiple states
2182
+ */
2183
+ mergeStates(states) {
2184
+ if (states.length === 0) {
2185
+ return {};
2186
+ }
2187
+ if (states.length === 1) {
2188
+ return states[0];
2189
+ }
2190
+ const merged = {};
2191
+ for (const state of states) {
2192
+ if (typeof state === "object" && state !== null) {
2193
+ Object.assign(merged, state);
2194
+ }
2195
+ }
2196
+ return merged;
2197
+ }
2198
+ /**
2199
+ * Validate state after reconciliation
2200
+ */
2201
+ validateState(state) {
2202
+ const errors = [];
2203
+ if (state === null) {
2204
+ errors.push("State is null");
2205
+ } else if (state === void 0) {
2206
+ errors.push("State is undefined");
2207
+ } else if (typeof state !== "object") {
2208
+ errors.push("State is not an object");
2209
+ }
2210
+ return {
2211
+ valid: errors.length === 0,
2212
+ errors
2213
+ };
2214
+ }
2215
+ /**
2216
+ * Get state versions for a key
2217
+ */
2218
+ getStateVersions(key) {
2219
+ return this.stateVersions.get(key) || [];
2220
+ }
2221
+ /**
2222
+ * Get all state versions
2223
+ */
2224
+ getAllStateVersions() {
2225
+ const result = {};
2226
+ for (const [key, versions] of this.stateVersions) {
2227
+ result[key] = [...versions];
2228
+ }
2229
+ return result;
2230
+ }
2231
+ /**
2232
+ * Get reconciliation history
2233
+ */
2234
+ getReconciliationHistory() {
2235
+ return [...this.reconciliationHistory];
2236
+ }
2237
+ /**
2238
+ * Get reconciliation statistics
2239
+ */
2240
+ getStatistics() {
2241
+ const resolvedConflicts = this.reconciliationHistory.reduce(
2242
+ (sum, r) => sum + r.conflictsResolved,
2243
+ 0
2244
+ );
2245
+ const strategyUsage = {};
2246
+ for (const result of this.reconciliationHistory) {
2247
+ strategyUsage[result.strategy] = (strategyUsage[result.strategy] || 0) + 1;
2248
+ }
2249
+ return {
2250
+ totalReconciliations: this.reconciliationHistory.length,
2251
+ successfulReconciliations: this.reconciliationHistory.filter(
2252
+ (r) => r.success
2253
+ ).length,
2254
+ totalConflictsResolved: resolvedConflicts,
2255
+ averageConflictsPerReconciliation: this.reconciliationHistory.length > 0 ? resolvedConflicts / this.reconciliationHistory.length : 0,
2256
+ strategyUsage,
2257
+ trackedKeys: this.stateVersions.size
2258
+ };
2259
+ }
2260
+ /**
2261
+ * Clear all state (for testing)
2262
+ */
2263
+ clear() {
2264
+ this.stateVersions.clear();
2265
+ this.reconciliationHistory = [];
2266
+ this.cryptoProvider = null;
2267
+ this.requireSignedVersions = false;
2268
+ }
2269
+ /**
2270
+ * Get the crypto provider (for advanced usage)
2271
+ */
2272
+ getCryptoProvider() {
2273
+ return this.cryptoProvider;
2274
+ }
2275
+ };
2276
+
2277
+ // src/distributed/RecoveryLedger.ts
2278
+ function createShardKey(shardRole, shardIndex) {
2279
+ return `${shardRole}:${shardIndex}`;
2280
+ }
2281
+ function sortStrings(values) {
2282
+ return [...values].sort();
2283
+ }
2284
+ function clampObservedAt(observedAt) {
2285
+ return Number.isFinite(observedAt) ? observedAt : Date.now();
2286
+ }
2287
+ var RecoveryLedger = class _RecoveryLedger {
2288
+ objectId;
2289
+ dataShardCount;
2290
+ parityShardCount;
2291
+ recoveryThreshold;
2292
+ requestIds = /* @__PURE__ */ new Set();
2293
+ shards = /* @__PURE__ */ new Map();
2294
+ paths = /* @__PURE__ */ new Map();
2295
+ constructor(config) {
2296
+ this.objectId = config.objectId;
2297
+ this.dataShardCount = config.dataShardCount;
2298
+ this.parityShardCount = config.parityShardCount ?? 0;
2299
+ this.recoveryThreshold = config.recoveryThreshold ?? config.dataShardCount;
2300
+ if (this.dataShardCount <= 0) {
2301
+ throw new Error("dataShardCount must be greater than 0");
2302
+ }
2303
+ const maximumShardUnits = this.dataShardCount + this.parityShardCount;
2304
+ if (this.recoveryThreshold <= 0 || this.recoveryThreshold > maximumShardUnits) {
2305
+ throw new Error("recoveryThreshold must be between 1 and the total number of shard units");
2306
+ }
2307
+ if ("shards" in config) {
2308
+ this.merge(config);
2309
+ }
2310
+ }
2311
+ static fromSnapshot(snapshot) {
2312
+ return new _RecoveryLedger(snapshot);
2313
+ }
2314
+ getObjectId() {
2315
+ return this.objectId;
2316
+ }
2317
+ registerRequest(requestId) {
2318
+ if (requestId) {
2319
+ this.requestIds.add(requestId);
2320
+ }
2321
+ return this;
2322
+ }
2323
+ recordShardObservation(input) {
2324
+ this.assertShardIndex(input.shardRole, input.shardIndex);
2325
+ const shardKey = createShardKey(input.shardRole, input.shardIndex);
2326
+ const observedAt = clampObservedAt(input.observedAt);
2327
+ let shard = this.shards.get(shardKey);
2328
+ if (!shard) {
2329
+ shard = {
2330
+ shardRole: input.shardRole,
2331
+ shardIndex: input.shardIndex,
2332
+ digests: /* @__PURE__ */ new Set(),
2333
+ requestIds: /* @__PURE__ */ new Set(),
2334
+ observedBy: /* @__PURE__ */ new Set(),
2335
+ sources: /* @__PURE__ */ new Set(),
2336
+ firstObservedAt: observedAt,
2337
+ lastObservedAt: observedAt
2338
+ };
2339
+ this.shards.set(shardKey, shard);
2340
+ }
2341
+ shard.digests.add(input.digest);
2342
+ if (input.requestId) {
2343
+ shard.requestIds.add(input.requestId);
2344
+ this.requestIds.add(input.requestId);
2345
+ }
2346
+ if (input.observedBy) {
2347
+ shard.observedBy.add(input.observedBy);
2348
+ }
2349
+ if (input.source) {
2350
+ shard.sources.add(input.source);
2351
+ }
2352
+ shard.firstObservedAt = Math.min(shard.firstObservedAt, observedAt);
2353
+ shard.lastObservedAt = Math.max(shard.lastObservedAt, observedAt);
2354
+ return this;
2355
+ }
2356
+ recordPathObservation(input) {
2357
+ const observedAt = clampObservedAt(input.observedAt);
2358
+ let path = this.paths.get(input.pathId);
2359
+ if (!path) {
2360
+ path = {
2361
+ pathId: input.pathId,
2362
+ status: input.status,
2363
+ requestIds: /* @__PURE__ */ new Set(),
2364
+ observedBy: /* @__PURE__ */ new Set(),
2365
+ reasons: /* @__PURE__ */ new Set(),
2366
+ firstObservedAt: observedAt,
2367
+ lastObservedAt: observedAt
2368
+ };
2369
+ this.paths.set(input.pathId, path);
2370
+ }
2371
+ if (path.status !== input.status) {
2372
+ path.status = path.status === "succeeded" || input.status === "succeeded" ? "succeeded" : "failed";
2373
+ }
2374
+ if (input.requestId) {
2375
+ path.requestIds.add(input.requestId);
2376
+ this.requestIds.add(input.requestId);
2377
+ }
2378
+ if (input.observedBy) {
2379
+ path.observedBy.add(input.observedBy);
2380
+ }
2381
+ if (input.reason) {
2382
+ path.reasons.add(input.reason);
2383
+ }
2384
+ path.firstObservedAt = Math.min(path.firstObservedAt, observedAt);
2385
+ path.lastObservedAt = Math.max(path.lastObservedAt, observedAt);
2386
+ return this;
2387
+ }
2388
+ merge(other) {
2389
+ const snapshot = other instanceof _RecoveryLedger ? other.snapshot() : other;
2390
+ this.assertCompatibleSnapshot(snapshot);
2391
+ for (const requestId of snapshot.requestIds) {
2392
+ this.requestIds.add(requestId);
2393
+ }
2394
+ for (const shard of snapshot.shards) {
2395
+ this.mergeShard(shard);
2396
+ }
2397
+ for (const path of snapshot.paths) {
2398
+ this.mergePath(path);
2399
+ }
2400
+ return this;
2401
+ }
2402
+ getStatus() {
2403
+ const availableDataShards = this.getAvailableShardIndices("data");
2404
+ const availableParityShards = this.getAvailableShardIndices("parity");
2405
+ const missingDataShards = this.getMissingDataShardIndices();
2406
+ const conflictingShards = this.getConflicts();
2407
+ const uniqueShardUnits = availableDataShards.length + availableParityShards.length;
2408
+ const directDataComplete = availableDataShards.length === this.dataShardCount;
2409
+ const canReconstruct = conflictingShards.length === 0 && (directDataComplete || uniqueShardUnits >= this.recoveryThreshold);
2410
+ const needsParityDecode = canReconstruct && !directDataComplete;
2411
+ return {
2412
+ objectId: this.objectId,
2413
+ requestIds: sortStrings(this.requestIds),
2414
+ dataShardCount: this.dataShardCount,
2415
+ parityShardCount: this.parityShardCount,
2416
+ recoveryThreshold: this.recoveryThreshold,
2417
+ availableDataShards,
2418
+ availableParityShards,
2419
+ missingDataShards,
2420
+ uniqueShardUnits,
2421
+ directDataComplete,
2422
+ canReconstruct,
2423
+ needsParityDecode,
2424
+ conflictingShards,
2425
+ failedPaths: this.getPathsByStatus("failed"),
2426
+ succeededPaths: this.getPathsByStatus("succeeded")
2427
+ };
2428
+ }
2429
+ snapshot() {
2430
+ const shards = [...this.shards.values()].sort((left, right) => {
2431
+ if (left.shardRole !== right.shardRole) {
2432
+ return left.shardRole.localeCompare(right.shardRole);
2433
+ }
2434
+ return left.shardIndex - right.shardIndex;
2435
+ }).map((shard) => ({
2436
+ shardRole: shard.shardRole,
2437
+ shardIndex: shard.shardIndex,
2438
+ digests: sortStrings(shard.digests),
2439
+ requestIds: sortStrings(shard.requestIds),
2440
+ observedBy: sortStrings(shard.observedBy),
2441
+ sources: sortStrings(shard.sources),
2442
+ firstObservedAt: shard.firstObservedAt,
2443
+ lastObservedAt: shard.lastObservedAt
2444
+ }));
2445
+ const paths = [...this.paths.values()].sort((left, right) => left.pathId.localeCompare(right.pathId)).map((path) => ({
2446
+ pathId: path.pathId,
2447
+ status: path.status,
2448
+ requestIds: sortStrings(path.requestIds),
2449
+ observedBy: sortStrings(path.observedBy),
2450
+ reasons: sortStrings(path.reasons),
2451
+ firstObservedAt: path.firstObservedAt,
2452
+ lastObservedAt: path.lastObservedAt
2453
+ }));
2454
+ return {
2455
+ objectId: this.objectId,
2456
+ dataShardCount: this.dataShardCount,
2457
+ parityShardCount: this.parityShardCount,
2458
+ recoveryThreshold: this.recoveryThreshold,
2459
+ requestIds: sortStrings(this.requestIds),
2460
+ shards,
2461
+ paths
2462
+ };
2463
+ }
2464
+ mergeShard(shard) {
2465
+ this.assertShardIndex(shard.shardRole, shard.shardIndex);
2466
+ const key = createShardKey(shard.shardRole, shard.shardIndex);
2467
+ const existing = this.shards.get(key);
2468
+ if (!existing) {
2469
+ this.shards.set(key, {
2470
+ shardRole: shard.shardRole,
2471
+ shardIndex: shard.shardIndex,
2472
+ digests: new Set(shard.digests),
2473
+ requestIds: new Set(shard.requestIds),
2474
+ observedBy: new Set(shard.observedBy),
2475
+ sources: new Set(shard.sources),
2476
+ firstObservedAt: shard.firstObservedAt,
2477
+ lastObservedAt: shard.lastObservedAt
2478
+ });
2479
+ return;
2480
+ }
2481
+ for (const digest of shard.digests) {
2482
+ existing.digests.add(digest);
2483
+ }
2484
+ for (const requestId of shard.requestIds) {
2485
+ existing.requestIds.add(requestId);
2486
+ this.requestIds.add(requestId);
2487
+ }
2488
+ for (const observer of shard.observedBy) {
2489
+ existing.observedBy.add(observer);
2490
+ }
2491
+ for (const source of shard.sources) {
2492
+ existing.sources.add(source);
2493
+ }
2494
+ existing.firstObservedAt = Math.min(existing.firstObservedAt, shard.firstObservedAt);
2495
+ existing.lastObservedAt = Math.max(existing.lastObservedAt, shard.lastObservedAt);
2496
+ }
2497
+ mergePath(path) {
2498
+ const existing = this.paths.get(path.pathId);
2499
+ if (!existing) {
2500
+ this.paths.set(path.pathId, {
2501
+ pathId: path.pathId,
2502
+ status: path.status,
2503
+ requestIds: new Set(path.requestIds),
2504
+ observedBy: new Set(path.observedBy),
2505
+ reasons: new Set(path.reasons),
2506
+ firstObservedAt: path.firstObservedAt,
2507
+ lastObservedAt: path.lastObservedAt
2508
+ });
2509
+ for (const requestId of path.requestIds) {
2510
+ this.requestIds.add(requestId);
2511
+ }
2512
+ return;
2513
+ }
2514
+ existing.status = existing.status === "succeeded" || path.status === "succeeded" ? "succeeded" : "failed";
2515
+ for (const requestId of path.requestIds) {
2516
+ existing.requestIds.add(requestId);
2517
+ this.requestIds.add(requestId);
2518
+ }
2519
+ for (const observer of path.observedBy) {
2520
+ existing.observedBy.add(observer);
2521
+ }
2522
+ for (const reason of path.reasons) {
2523
+ existing.reasons.add(reason);
2524
+ }
2525
+ existing.firstObservedAt = Math.min(existing.firstObservedAt, path.firstObservedAt);
2526
+ existing.lastObservedAt = Math.max(existing.lastObservedAt, path.lastObservedAt);
2527
+ }
2528
+ getAvailableShardIndices(role) {
2529
+ return [...this.shards.values()].filter((shard) => shard.shardRole === role).map((shard) => shard.shardIndex).sort((left, right) => left - right);
2530
+ }
2531
+ getMissingDataShardIndices() {
2532
+ const available = new Set(this.getAvailableShardIndices("data"));
2533
+ const missing = [];
2534
+ for (let shardIndex = 0; shardIndex < this.dataShardCount; shardIndex++) {
2535
+ if (!available.has(shardIndex)) {
2536
+ missing.push(shardIndex);
2537
+ }
2538
+ }
2539
+ return missing;
2540
+ }
2541
+ getPathsByStatus(status) {
2542
+ return [...this.paths.values()].filter((path) => path.status === status).map((path) => path.pathId).sort();
2543
+ }
2544
+ getConflicts() {
2545
+ return [...this.shards.values()].filter((shard) => shard.digests.size > 1).map((shard) => ({
2546
+ shardRole: shard.shardRole,
2547
+ shardIndex: shard.shardIndex,
2548
+ digests: sortStrings(shard.digests)
2549
+ })).sort((left, right) => {
2550
+ if (left.shardRole !== right.shardRole) {
2551
+ return left.shardRole.localeCompare(right.shardRole);
2552
+ }
2553
+ return left.shardIndex - right.shardIndex;
2554
+ });
2555
+ }
2556
+ assertCompatibleSnapshot(snapshot) {
2557
+ if (snapshot.objectId !== this.objectId) {
2558
+ throw new Error(`RecoveryLedger object mismatch: expected ${this.objectId}, received ${snapshot.objectId}`);
2559
+ }
2560
+ if (snapshot.dataShardCount !== this.dataShardCount) {
2561
+ throw new Error("RecoveryLedger dataShardCount mismatch");
2562
+ }
2563
+ if (snapshot.parityShardCount !== this.parityShardCount) {
2564
+ throw new Error("RecoveryLedger parityShardCount mismatch");
2565
+ }
2566
+ if (snapshot.recoveryThreshold !== this.recoveryThreshold) {
2567
+ throw new Error("RecoveryLedger recoveryThreshold mismatch");
2568
+ }
2569
+ }
2570
+ assertShardIndex(role, shardIndex) {
2571
+ const upperBound = role === "data" ? this.dataShardCount : this.parityShardCount;
2572
+ if (!Number.isInteger(shardIndex) || shardIndex < 0 || shardIndex >= upperBound) {
2573
+ throw new Error(`Invalid ${role} shard index ${shardIndex}`);
2574
+ }
2575
+ }
2576
+ };
2577
+
2578
+ export { RecoveryLedger, ReplicationManager, StateReconciler, SyncCoordinator, SyncProtocol };
2579
+ //# sourceMappingURL=index.js.map
2580
+ //# sourceMappingURL=index.js.map