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