@xnetjs/sync 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3706 @@
1
+ // src/change.ts
2
+ import { hashHex, sign, verify } from "@xnetjs/crypto";
3
+ var CURRENT_PROTOCOL_VERSION = 3;
4
+ function createUnsignedChange(options) {
5
+ const unsigned = {
6
+ protocolVersion: CURRENT_PROTOCOL_VERSION,
7
+ id: options.id,
8
+ type: options.type,
9
+ payload: options.payload,
10
+ parentHash: options.parentHash,
11
+ authorDID: options.authorDID,
12
+ wallTime: options.wallTime ?? Date.now(),
13
+ lamport: options.lamport
14
+ };
15
+ if (options.batchId !== void 0) {
16
+ unsigned.batchId = options.batchId;
17
+ unsigned.batchIndex = options.batchIndex;
18
+ unsigned.batchSize = options.batchSize;
19
+ }
20
+ return unsigned;
21
+ }
22
+ function createBatchId() {
23
+ return `batch-${crypto.randomUUID()}`;
24
+ }
25
+ function sortObjectKeys(obj) {
26
+ if (obj === null || typeof obj !== "object") {
27
+ return obj;
28
+ }
29
+ if (Array.isArray(obj)) {
30
+ return obj.map(sortObjectKeys);
31
+ }
32
+ const sorted = {};
33
+ for (const key of Object.keys(obj).sort()) {
34
+ sorted[key] = sortObjectKeys(obj[key]);
35
+ }
36
+ return sorted;
37
+ }
38
+ function computeChangeHash(unsigned) {
39
+ let toHash;
40
+ if (unsigned.protocolVersion === void 0 || unsigned.protocolVersion === 0) {
41
+ const legacy = {};
42
+ for (const [key, value] of Object.entries(unsigned)) {
43
+ if (key !== "protocolVersion") {
44
+ legacy[key] = value;
45
+ }
46
+ }
47
+ toHash = legacy;
48
+ } else {
49
+ toHash = unsigned;
50
+ }
51
+ const canonical = JSON.stringify(sortObjectKeys(toHash));
52
+ const hashBytes = new TextEncoder().encode(canonical);
53
+ return `cid:blake3:${hashHex(hashBytes)}`;
54
+ }
55
+ function signChange(unsigned, signingKey) {
56
+ const hash4 = computeChangeHash(unsigned);
57
+ const hashBytes = new TextEncoder().encode(hash4);
58
+ const signature = sign(hashBytes, signingKey);
59
+ return {
60
+ ...unsigned,
61
+ hash: hash4,
62
+ signature
63
+ };
64
+ }
65
+ function verifyChange(change, publicKey) {
66
+ const version = change.protocolVersion ?? 0;
67
+ if (version > CURRENT_PROTOCOL_VERSION) {
68
+ console.warn(
69
+ `[xnet/sync] Change ${change.id} uses protocol version ${version}, but current version is ${CURRENT_PROTOCOL_VERSION}. Consider upgrading xNet for full compatibility.`
70
+ );
71
+ }
72
+ const hashBytes = new TextEncoder().encode(change.hash);
73
+ return verify(hashBytes, change.signature, publicKey);
74
+ }
75
+ function verifyChangeHash(change) {
76
+ const unsigned = {
77
+ id: change.id,
78
+ type: change.type,
79
+ payload: change.payload,
80
+ parentHash: change.parentHash,
81
+ authorDID: change.authorDID,
82
+ wallTime: change.wallTime,
83
+ lamport: change.lamport
84
+ };
85
+ if (change.protocolVersion !== void 0) {
86
+ unsigned.protocolVersion = change.protocolVersion;
87
+ }
88
+ if (change.batchId !== void 0) {
89
+ unsigned.batchId = change.batchId;
90
+ unsigned.batchIndex = change.batchIndex;
91
+ unsigned.batchSize = change.batchSize;
92
+ }
93
+ const computedHash = computeChangeHash(unsigned);
94
+ return computedHash === change.hash;
95
+ }
96
+ function createChangeId() {
97
+ return crypto.randomUUID();
98
+ }
99
+
100
+ // src/clock.ts
101
+ function createLamportClock(author) {
102
+ return { time: 0, author };
103
+ }
104
+ function tick(clock) {
105
+ const newTime = clock.time + 1;
106
+ const newClock = { ...clock, time: newTime };
107
+ const timestamp = { time: newTime, author: clock.author };
108
+ return [newClock, timestamp];
109
+ }
110
+ function receive(clock, receivedTime) {
111
+ return {
112
+ ...clock,
113
+ time: Math.max(clock.time, receivedTime)
114
+ };
115
+ }
116
+ function compareLamportTimestamps(a, b) {
117
+ if (a.time < b.time) return -1;
118
+ if (a.time > b.time) return 1;
119
+ const authorCmp = a.author.localeCompare(b.author);
120
+ if (authorCmp < 0) return -1;
121
+ if (authorCmp > 0) return 1;
122
+ return 0;
123
+ }
124
+ function isBefore(a, b) {
125
+ return compareLamportTimestamps(a, b) === -1;
126
+ }
127
+ function isAfter(a, b) {
128
+ return compareLamportTimestamps(a, b) === 1;
129
+ }
130
+ function serializeTimestamp(ts) {
131
+ const paddedTime = ts.time.toString().padStart(16, "0");
132
+ return `${paddedTime}-${ts.author}`;
133
+ }
134
+ function parseTimestamp(serialized) {
135
+ const dashIndex = serialized.indexOf("-");
136
+ if (dashIndex === -1) {
137
+ throw new Error(`Invalid serialized timestamp: ${serialized}`);
138
+ }
139
+ const timeStr = serialized.slice(0, dashIndex);
140
+ const author = serialized.slice(dashIndex + 1);
141
+ const time = parseInt(timeStr, 10);
142
+ if (isNaN(time)) {
143
+ throw new Error(`Invalid time in timestamp: ${timeStr}`);
144
+ }
145
+ return { time, author };
146
+ }
147
+ function maxTime(timestamps) {
148
+ if (timestamps.length === 0) return 0;
149
+ return Math.max(...timestamps.map((ts) => ts.time));
150
+ }
151
+
152
+ // src/chain.ts
153
+ function validateChain(changes) {
154
+ if (changes.length === 0) {
155
+ return { valid: true };
156
+ }
157
+ const byHash = /* @__PURE__ */ new Map();
158
+ for (const change of changes) {
159
+ byHash.set(change.hash, change);
160
+ }
161
+ for (const change of changes) {
162
+ if (!verifyChangeHash(change)) {
163
+ return {
164
+ valid: false,
165
+ error: `Change ${change.id} has invalid hash (data may be tampered)`
166
+ };
167
+ }
168
+ if (change.parentHash !== null && !byHash.has(change.parentHash)) {
169
+ }
170
+ }
171
+ const { hasFork, forkPoints } = detectFork(changes);
172
+ if (hasFork) {
173
+ return {
174
+ valid: true,
175
+ // Forks are valid, just need resolution
176
+ forkDetected: true,
177
+ forkPoint: forkPoints[0]
178
+ };
179
+ }
180
+ return { valid: true };
181
+ }
182
+ function detectFork(changes) {
183
+ const childrenByParent = /* @__PURE__ */ new Map();
184
+ for (const change of changes) {
185
+ const children = childrenByParent.get(change.parentHash) || [];
186
+ children.push(change);
187
+ childrenByParent.set(change.parentHash, children);
188
+ }
189
+ const forkPoints = [];
190
+ for (const [parent, children] of childrenByParent) {
191
+ if (children.length > 1 && parent !== null) {
192
+ forkPoints.push(parent);
193
+ }
194
+ }
195
+ return {
196
+ hasFork: forkPoints.length > 0,
197
+ forkPoints
198
+ };
199
+ }
200
+ function getChainHeads(changes) {
201
+ const parentHashes = new Set(
202
+ changes.map((c) => c.parentHash).filter((h) => h !== null)
203
+ );
204
+ return changes.filter((c) => !parentHashes.has(c.hash));
205
+ }
206
+ function getChainRoots(changes) {
207
+ return changes.filter((c) => c.parentHash === null);
208
+ }
209
+ function getAncestry(change, allChanges) {
210
+ const byHash = /* @__PURE__ */ new Map();
211
+ for (const c of allChanges) {
212
+ byHash.set(c.hash, c);
213
+ }
214
+ const ancestry = [];
215
+ let current = change.parentHash;
216
+ while (current !== null) {
217
+ const parent = byHash.get(current);
218
+ if (!parent) break;
219
+ ancestry.unshift(parent);
220
+ current = parent.parentHash;
221
+ }
222
+ return ancestry;
223
+ }
224
+ function findCommonAncestor(a, b, allChanges) {
225
+ const ancestryA = /* @__PURE__ */ new Set([a.hash, ...getAncestry(a, allChanges).map((c) => c.hash)]);
226
+ const byHash = /* @__PURE__ */ new Map();
227
+ for (const c of allChanges) {
228
+ byHash.set(c.hash, c);
229
+ }
230
+ if (ancestryA.has(b.hash)) {
231
+ return b;
232
+ }
233
+ let current = b.parentHash;
234
+ while (current !== null) {
235
+ if (ancestryA.has(current)) {
236
+ return byHash.get(current) || null;
237
+ }
238
+ const parent = byHash.get(current);
239
+ if (!parent) break;
240
+ current = parent.parentHash;
241
+ }
242
+ return null;
243
+ }
244
+ function getForks(changes) {
245
+ const { forkPoints } = detectFork(changes);
246
+ if (forkPoints.length === 0) return [];
247
+ const byHash = /* @__PURE__ */ new Map();
248
+ for (const c of changes) {
249
+ byHash.set(c.hash, c);
250
+ }
251
+ const forks = [];
252
+ for (const forkPoint of forkPoints) {
253
+ const children = changes.filter((c) => c.parentHash === forkPoint);
254
+ if (children.length >= 2) {
255
+ children.sort((a, b) => compareLamportTimestamps(a.lamport, b.lamport));
256
+ forks.push({
257
+ commonAncestor: forkPoint,
258
+ branch1: [children[0]],
259
+ branch2: children.slice(1)
260
+ });
261
+ }
262
+ }
263
+ return forks;
264
+ }
265
+ function topologicalSort(changes) {
266
+ const byHash = /* @__PURE__ */ new Map();
267
+ for (const c of changes) {
268
+ byHash.set(c.hash, c);
269
+ }
270
+ const sorted = [];
271
+ const visited = /* @__PURE__ */ new Set();
272
+ const visiting = /* @__PURE__ */ new Set();
273
+ function visit(change) {
274
+ if (visited.has(change.hash)) return;
275
+ if (visiting.has(change.hash)) {
276
+ throw new Error("Cycle detected in change chain");
277
+ }
278
+ visiting.add(change.hash);
279
+ if (change.parentHash !== null) {
280
+ const parent = byHash.get(change.parentHash);
281
+ if (parent) {
282
+ visit(parent);
283
+ }
284
+ }
285
+ visiting.delete(change.hash);
286
+ visited.add(change.hash);
287
+ sorted.push(change);
288
+ }
289
+ const sortedInput = [...changes].sort((a, b) => compareLamportTimestamps(a.lamport, b.lamport));
290
+ for (const change of sortedInput) {
291
+ visit(change);
292
+ }
293
+ return sorted;
294
+ }
295
+
296
+ // src/features.ts
297
+ var FEATURES = {
298
+ // ─── Core Features (always present since v1) ───────────────────────────────
299
+ /** Basic node change synchronization */
300
+ "node-changes": {
301
+ since: 1,
302
+ required: true,
303
+ description: "Basic node change synchronization"
304
+ },
305
+ /** Yjs CRDT document synchronization */
306
+ "yjs-updates": {
307
+ since: 1,
308
+ required: true,
309
+ description: "Yjs CRDT document synchronization"
310
+ },
311
+ // ─── Protocol v1 Features ──────────────────────────────────────────────────
312
+ /** Cryptographically signed Yjs update envelopes */
313
+ "signed-yjs-envelopes": {
314
+ since: 1,
315
+ required: false,
316
+ description: "Cryptographically signed Yjs update envelopes",
317
+ requires: ["yjs-updates"]
318
+ },
319
+ /** Transaction batching for atomic multi-change operations */
320
+ "batch-changes": {
321
+ since: 1,
322
+ required: false,
323
+ description: "Transaction batching for atomic multi-change operations",
324
+ requires: ["node-changes"]
325
+ },
326
+ /** Lamport clock ordering for conflict resolution */
327
+ "lamport-ordering": {
328
+ since: 1,
329
+ required: false,
330
+ description: "Lamport clock ordering for conflict resolution"
331
+ },
332
+ /** Hash chain integrity verification */
333
+ "hash-chains": {
334
+ since: 1,
335
+ required: false,
336
+ description: "Hash chain integrity verification",
337
+ requires: ["node-changes"]
338
+ },
339
+ // ─── Protocol v2 Features ──────────────────────────────────────────────────
340
+ /** Schema versioning with semver */
341
+ "schema-versioning": {
342
+ since: 2,
343
+ required: false,
344
+ description: "Schema versioning with semver"
345
+ },
346
+ /** Peer capability negotiation on connect */
347
+ "capability-negotiation": {
348
+ since: 2,
349
+ required: false,
350
+ description: "Peer capability negotiation on connect"
351
+ },
352
+ /** Reputation-based peer scoring */
353
+ "peer-scoring": {
354
+ since: 2,
355
+ required: false,
356
+ description: "Reputation-based peer scoring"
357
+ },
358
+ /** Schema lens migrations for version transformations */
359
+ "schema-lenses": {
360
+ since: 2,
361
+ required: false,
362
+ description: "Schema lens migrations for version transformations",
363
+ requires: ["schema-versioning"]
364
+ },
365
+ /** Unknown property preservation (graceful degradation) */
366
+ "unknown-preservation": {
367
+ since: 2,
368
+ required: false,
369
+ description: "Unknown property preservation for forward compatibility"
370
+ },
371
+ // ─── Protocol v3 Features (Future) ─────────────────────────────────────────
372
+ /** Schema inheritance with property overrides */
373
+ "schema-inheritance": {
374
+ since: 3,
375
+ required: false,
376
+ description: "Schema inheritance with property overrides",
377
+ requires: ["schema-versioning"]
378
+ },
379
+ /** Cross-document federated queries */
380
+ "federated-queries": {
381
+ since: 3,
382
+ required: false,
383
+ description: "Cross-document federated queries"
384
+ },
385
+ /** Compressed change payloads */
386
+ "compressed-payloads": {
387
+ since: 3,
388
+ required: false,
389
+ description: "Compressed change payloads for bandwidth efficiency"
390
+ }
391
+ };
392
+ var ALL_FEATURES = Object.keys(FEATURES);
393
+ function getEnabledFeatures(protocolVersion) {
394
+ return ALL_FEATURES.filter((name) => {
395
+ const config = FEATURES[name];
396
+ return config.since <= protocolVersion;
397
+ });
398
+ }
399
+ function isFeatureEnabled(feature, enabledFeatures) {
400
+ return enabledFeatures.includes(feature);
401
+ }
402
+ function getRequiredFeatures(protocolVersion) {
403
+ return getEnabledFeatures(protocolVersion).filter((name) => FEATURES[name].required);
404
+ }
405
+ function getOptionalFeatures(protocolVersion) {
406
+ return getEnabledFeatures(protocolVersion).filter((name) => !FEATURES[name].required);
407
+ }
408
+ function getFeatureVersion(feature) {
409
+ return FEATURES[feature].since;
410
+ }
411
+ function isFeatureAvailable(feature, protocolVersion) {
412
+ return FEATURES[feature].since <= protocolVersion;
413
+ }
414
+ function getFeatureDependencies(feature) {
415
+ return FEATURES[feature].requires ?? [];
416
+ }
417
+ function getFeatureConflicts(feature) {
418
+ return FEATURES[feature].conflicts ?? [];
419
+ }
420
+ function getAllDependencies(feature, visited = /* @__PURE__ */ new Set()) {
421
+ if (visited.has(feature)) {
422
+ return [];
423
+ }
424
+ visited.add(feature);
425
+ const direct = getFeatureDependencies(feature);
426
+ const transitive = [];
427
+ for (const dep of direct) {
428
+ transitive.push(dep);
429
+ transitive.push(...getAllDependencies(dep, visited));
430
+ }
431
+ return [...new Set(transitive)];
432
+ }
433
+ function validateFeatureSet(features, protocolVersion = CURRENT_PROTOCOL_VERSION) {
434
+ const errors = [];
435
+ const warnings = [];
436
+ const featureSet = new Set(features);
437
+ const required = getRequiredFeatures(protocolVersion);
438
+ for (const req of required) {
439
+ if (!featureSet.has(req)) {
440
+ errors.push({
441
+ type: "missing-required",
442
+ feature: req,
443
+ message: `Required feature '${req}' is not in feature set`
444
+ });
445
+ }
446
+ }
447
+ for (const feature of features) {
448
+ if (!isFeatureAvailable(feature, protocolVersion)) {
449
+ errors.push({
450
+ type: "version-mismatch",
451
+ feature,
452
+ message: `Feature '${feature}' requires protocol v${FEATURES[feature].since}, but running v${protocolVersion}`
453
+ });
454
+ }
455
+ const deps = getFeatureDependencies(feature);
456
+ for (const dep of deps) {
457
+ if (!featureSet.has(dep)) {
458
+ errors.push({
459
+ type: "missing-dependency",
460
+ feature,
461
+ relatedFeature: dep,
462
+ message: `Feature '${feature}' requires '${dep}' which is not enabled`
463
+ });
464
+ }
465
+ }
466
+ const conflicts = getFeatureConflicts(feature);
467
+ for (const conflict of conflicts) {
468
+ if (featureSet.has(conflict)) {
469
+ errors.push({
470
+ type: "conflict",
471
+ feature,
472
+ relatedFeature: conflict,
473
+ message: `Feature '${feature}' conflicts with '${conflict}'`
474
+ });
475
+ }
476
+ }
477
+ }
478
+ return {
479
+ valid: errors.length === 0,
480
+ errors,
481
+ warnings
482
+ };
483
+ }
484
+ function intersectFeatures(local, remote) {
485
+ const remoteSet = new Set(remote);
486
+ return local.filter((f) => remoteSet.has(f));
487
+ }
488
+ function diffFeatures(a, b) {
489
+ const bSet = new Set(b);
490
+ return a.filter((f) => !bSet.has(f));
491
+ }
492
+ function addDependencies(features) {
493
+ const result = new Set(features);
494
+ for (const feature of features) {
495
+ const deps = getAllDependencies(feature);
496
+ for (const dep of deps) {
497
+ result.add(dep);
498
+ }
499
+ }
500
+ return [...result];
501
+ }
502
+
503
+ // src/negotiation.ts
504
+ function createLocalCapabilities(peerId, features, options) {
505
+ return {
506
+ peerId,
507
+ protocolVersion: CURRENT_PROTOCOL_VERSION,
508
+ minProtocolVersion: options?.minProtocolVersion ?? 1,
509
+ features: features ?? getEnabledFeatures(CURRENT_PROTOCOL_VERSION),
510
+ packageVersion: options?.packageVersion ?? "0.0.0",
511
+ schemas: options?.schemas
512
+ };
513
+ }
514
+ function parseCapabilities(message) {
515
+ if (!message || typeof message !== "object") {
516
+ return null;
517
+ }
518
+ const msg = message;
519
+ if (typeof msg.peerId !== "string" && typeof msg.did !== "string") {
520
+ return null;
521
+ }
522
+ const peerId = msg.peerId ?? msg.did;
523
+ const protocolVersion = typeof msg.protocolVersion === "number" ? msg.protocolVersion : CURRENT_PROTOCOL_VERSION;
524
+ const minProtocolVersion = typeof msg.minProtocolVersion === "number" ? msg.minProtocolVersion : 1;
525
+ const packageVersion = typeof msg.packageVersion === "string" ? msg.packageVersion : "0.0.0";
526
+ let features = [];
527
+ if (Array.isArray(msg.features)) {
528
+ const knownFeatures = getEnabledFeatures(protocolVersion);
529
+ features = msg.features.filter(
530
+ (f) => knownFeatures.includes(f)
531
+ );
532
+ } else {
533
+ features = getEnabledFeatures(protocolVersion);
534
+ }
535
+ const schemas = Array.isArray(msg.schemas) ? msg.schemas.filter((s) => typeof s === "string") : void 0;
536
+ return {
537
+ peerId,
538
+ protocolVersion,
539
+ minProtocolVersion,
540
+ features,
541
+ packageVersion,
542
+ schemas
543
+ };
544
+ }
545
+ var VersionNegotiator = class {
546
+ /**
547
+ * Negotiate capabilities between local and remote peers.
548
+ *
549
+ * @param local - Local peer's capabilities
550
+ * @param remote - Remote peer's capabilities
551
+ * @returns Negotiated session or failure result
552
+ */
553
+ negotiate(local, remote) {
554
+ const maxVersion = Math.min(local.protocolVersion, remote.protocolVersion);
555
+ const minVersion = Math.max(local.minProtocolVersion, remote.minProtocolVersion);
556
+ if (maxVersion < minVersion) {
557
+ return this.createFailure(local, remote, maxVersion, minVersion);
558
+ }
559
+ const commonFeatures = intersectFeatures(local.features, remote.features);
560
+ const required = getRequiredFeatures(maxVersion);
561
+ const missingRequired = required.filter((f) => !commonFeatures.includes(f));
562
+ if (missingRequired.length > 0) {
563
+ return {
564
+ success: false,
565
+ error: "missing-required-features",
566
+ message: `Missing required features: ${missingRequired.join(", ")}`,
567
+ localVersion: local.protocolVersion,
568
+ remoteVersion: remote.protocolVersion,
569
+ suggestion: "contact-support"
570
+ };
571
+ }
572
+ const warnings = this.generateWarnings(local, remote, commonFeatures, maxVersion);
573
+ return {
574
+ success: true,
575
+ peerId: remote.peerId,
576
+ agreedVersion: maxVersion,
577
+ commonFeatures,
578
+ warnings,
579
+ canUse: (feature) => commonFeatures.includes(feature)
580
+ };
581
+ }
582
+ /**
583
+ * Validate that a set of capabilities is well-formed.
584
+ *
585
+ * @param capabilities - Capabilities to validate
586
+ * @returns Validation result with errors if any
587
+ */
588
+ validateCapabilities(capabilities) {
589
+ const errors = [];
590
+ if (!capabilities.peerId) {
591
+ errors.push("Missing peerId");
592
+ }
593
+ if (capabilities.protocolVersion < 1) {
594
+ errors.push("Protocol version must be at least 1");
595
+ }
596
+ if (capabilities.minProtocolVersion > capabilities.protocolVersion) {
597
+ errors.push("Minimum protocol version cannot exceed protocol version");
598
+ }
599
+ const featureResult = validateFeatureSet(capabilities.features, capabilities.protocolVersion);
600
+ if (!featureResult.valid) {
601
+ for (const error of featureResult.errors) {
602
+ errors.push(error.message);
603
+ }
604
+ }
605
+ return {
606
+ valid: errors.length === 0,
607
+ errors
608
+ };
609
+ }
610
+ /**
611
+ * Create a failure result with appropriate suggestion.
612
+ */
613
+ createFailure(local, remote, _maxVersion, _minVersion) {
614
+ let suggestion;
615
+ if (local.protocolVersion < remote.minProtocolVersion) {
616
+ suggestion = "upgrade-client";
617
+ } else if (remote.protocolVersion < local.minProtocolVersion) {
618
+ suggestion = "upgrade-hub";
619
+ } else {
620
+ suggestion = "upgrade-both";
621
+ }
622
+ return {
623
+ success: false,
624
+ error: "incompatible-versions",
625
+ message: `Version mismatch: local v${local.protocolVersion} (min ${local.minProtocolVersion}), remote v${remote.protocolVersion} (min ${remote.minProtocolVersion})`,
626
+ localVersion: local.protocolVersion,
627
+ remoteVersion: remote.protocolVersion,
628
+ suggestion
629
+ };
630
+ }
631
+ /**
632
+ * Generate warnings about degraded functionality.
633
+ */
634
+ generateWarnings(local, remote, commonFeatures, agreedVersion) {
635
+ const warnings = [];
636
+ if (local.protocolVersion !== remote.protocolVersion) {
637
+ const olderPeer = local.protocolVersion < remote.protocolVersion ? "local" : "remote";
638
+ warnings.push({
639
+ type: "version-mismatch",
640
+ message: `${olderPeer === "local" ? "Local" : "Remote"} peer using older protocol v${Math.min(local.protocolVersion, remote.protocolVersion)} (agreed on v${agreedVersion})`
641
+ });
642
+ }
643
+ const missingFromRemote = diffFeatures(local.features, remote.features);
644
+ if (missingFromRemote.length > 0) {
645
+ warnings.push({
646
+ type: "degraded-features",
647
+ message: `Features unavailable with this peer: ${missingFromRemote.join(", ")}`,
648
+ affectedFeatures: missingFromRemote
649
+ });
650
+ }
651
+ const unknownFromRemote = remote.features.filter(
652
+ (f) => !local.features.includes(f)
653
+ );
654
+ if (unknownFromRemote.length > 0) {
655
+ warnings.push({
656
+ type: "unknown-features",
657
+ message: `Remote advertises unknown features: ${unknownFromRemote.join(", ")}`,
658
+ affectedFeatures: unknownFromRemote
659
+ });
660
+ }
661
+ return warnings;
662
+ }
663
+ };
664
+ var defaultNegotiator = new VersionNegotiator();
665
+
666
+ // src/provider.ts
667
+ var BaseSyncProvider = class {
668
+ _status = "disconnected";
669
+ _peers = /* @__PURE__ */ new Map();
670
+ _listeners = /* @__PURE__ */ new Map();
671
+ /** Local capabilities for negotiation */
672
+ _localCapabilities;
673
+ /** Version negotiator instance */
674
+ _negotiator;
675
+ /** Provider options */
676
+ _options;
677
+ constructor(options) {
678
+ this._options = options;
679
+ this._negotiator = new VersionNegotiator();
680
+ this._localCapabilities = createLocalCapabilities(
681
+ options.localDID ?? `anonymous-${Date.now()}`,
682
+ options.enabledFeatures,
683
+ {
684
+ packageVersion: options.packageVersion,
685
+ minProtocolVersion: options.minProtocolVersion
686
+ }
687
+ );
688
+ }
689
+ get status() {
690
+ return this._status;
691
+ }
692
+ get peers() {
693
+ return Array.from(this._peers.keys());
694
+ }
695
+ get peerInfo() {
696
+ return new Map(this._peers);
697
+ }
698
+ get localCapabilities() {
699
+ return this._localCapabilities;
700
+ }
701
+ /**
702
+ * Check if a feature can be used with a specific peer.
703
+ */
704
+ canUseFeature(peerId, feature) {
705
+ const peer = this._peers.get(peerId);
706
+ if (!peer?.negotiatedSession) {
707
+ return false;
708
+ }
709
+ return peer.negotiatedSession.canUse(feature);
710
+ }
711
+ /**
712
+ * Get the negotiated session for a peer.
713
+ */
714
+ getNegotiatedSession(peerId) {
715
+ return this._peers.get(peerId)?.negotiatedSession;
716
+ }
717
+ async requestChangesFromAll(since) {
718
+ const allChanges = [];
719
+ const seenHashes = /* @__PURE__ */ new Set();
720
+ const promises = this.peers.map(
721
+ (peerId) => this.requestChanges(peerId, since).catch(() => [])
722
+ );
723
+ const results = await Promise.all(promises);
724
+ for (const changes of results) {
725
+ for (const change of changes) {
726
+ if (!seenHashes.has(change.hash)) {
727
+ seenHashes.add(change.hash);
728
+ allChanges.push(change);
729
+ }
730
+ }
731
+ }
732
+ return allChanges;
733
+ }
734
+ on(event, listener) {
735
+ const listeners = this._listeners.get(event) || /* @__PURE__ */ new Set();
736
+ listeners.add(listener);
737
+ this._listeners.set(event, listeners);
738
+ }
739
+ off(event, listener) {
740
+ const listeners = this._listeners.get(event);
741
+ if (listeners) {
742
+ listeners.delete(listener);
743
+ }
744
+ }
745
+ once(event, listener) {
746
+ const onceListener = ((...args) => {
747
+ this.off(event, onceListener);
748
+ listener(...args);
749
+ });
750
+ this.on(event, onceListener);
751
+ }
752
+ emit(event, ...args) {
753
+ const listeners = this._listeners.get(event);
754
+ if (listeners) {
755
+ for (const listener of listeners) {
756
+ try {
757
+ listener(...args);
758
+ } catch (error) {
759
+ console.error(`Error in sync provider event listener for ${event}:`, error);
760
+ }
761
+ }
762
+ }
763
+ }
764
+ setStatus(status) {
765
+ if (this._status !== status) {
766
+ this._status = status;
767
+ this.emit("status-change", status);
768
+ }
769
+ }
770
+ addPeer(id, name) {
771
+ const now = Date.now();
772
+ this._peers.set(id, {
773
+ id,
774
+ name,
775
+ connectedAt: now,
776
+ lastSeen: now
777
+ });
778
+ this.emit("peer-connected", this._peers.get(id));
779
+ }
780
+ removePeer(id) {
781
+ this._peers.delete(id);
782
+ this.emit("peer-disconnected", id);
783
+ }
784
+ updatePeerLastSeen(id) {
785
+ const peer = this._peers.get(id);
786
+ if (peer) {
787
+ peer.lastSeen = Date.now();
788
+ }
789
+ }
790
+ /**
791
+ * Negotiate capabilities with a peer.
792
+ * Called when a peer connects and sends their capabilities.
793
+ *
794
+ * @param peerId - The peer's ID
795
+ * @param remoteCapabilities - The peer's advertised capabilities
796
+ * @returns Negotiation result (success or failure)
797
+ */
798
+ negotiateWithPeer(peerId, remoteCapabilities) {
799
+ const peer = this._peers.get(peerId);
800
+ if (!peer) {
801
+ return {
802
+ success: false,
803
+ error: "invalid-capabilities",
804
+ message: `Peer ${peerId} not found`,
805
+ localVersion: this._localCapabilities.protocolVersion,
806
+ remoteVersion: remoteCapabilities.protocolVersion,
807
+ suggestion: "contact-support"
808
+ };
809
+ }
810
+ peer.capabilities = remoteCapabilities;
811
+ const result = this._negotiator.negotiate(this._localCapabilities, remoteCapabilities);
812
+ if (result.success) {
813
+ peer.negotiatedSession = result;
814
+ this.emit("negotiation-complete", peerId, result);
815
+ if (result.warnings.length > 0) {
816
+ const warningMessages = result.warnings.map((w) => w.message);
817
+ this.emit("capability-degraded", peerId, warningMessages);
818
+ }
819
+ } else {
820
+ this.emit("negotiation-failed", peerId, result.message, result.suggestion);
821
+ if (this._options.strictVersionCheck) {
822
+ this.removePeer(peerId);
823
+ }
824
+ }
825
+ return result;
826
+ }
827
+ /**
828
+ * Get features available with all connected peers.
829
+ * Returns the intersection of all negotiated feature sets.
830
+ */
831
+ getCommonFeatures() {
832
+ const negotiatedPeers = Array.from(this._peers.values()).filter((p) => p.negotiatedSession);
833
+ if (negotiatedPeers.length === 0) {
834
+ return this._localCapabilities.features;
835
+ }
836
+ let common = new Set(negotiatedPeers[0].negotiatedSession.commonFeatures);
837
+ for (let i = 1; i < negotiatedPeers.length; i++) {
838
+ const peerFeatures = new Set(negotiatedPeers[i].negotiatedSession.commonFeatures);
839
+ common = new Set([...common].filter((f) => peerFeatures.has(f)));
840
+ }
841
+ return [...common];
842
+ }
843
+ /**
844
+ * Check if a feature can be used with all connected peers.
845
+ */
846
+ canUseFeatureWithAll(feature) {
847
+ return this.getCommonFeatures().includes(feature);
848
+ }
849
+ };
850
+
851
+ // src/yjs-envelope.ts
852
+ import {
853
+ hash,
854
+ sign as sign2,
855
+ verify as verify2,
856
+ hybridSign,
857
+ hybridVerify,
858
+ encodeSignature,
859
+ decodeSignature,
860
+ toBase64,
861
+ fromBase64,
862
+ DEFAULT_SECURITY_LEVEL
863
+ } from "@xnetjs/crypto";
864
+ import { parseDID } from "@xnetjs/identity";
865
+ function isV2Envelope(envelope) {
866
+ return "v" in envelope && envelope.v === 2;
867
+ }
868
+ function isV1Envelope(envelope) {
869
+ return !("v" in envelope);
870
+ }
871
+ function signYjsUpdateV1(update, authorDID, privateKey, clientId) {
872
+ const updateHash = hash(update, "blake3");
873
+ const signature = sign2(updateHash, privateKey);
874
+ return {
875
+ update,
876
+ authorDID,
877
+ signature,
878
+ timestamp: Date.now(),
879
+ clientId
880
+ };
881
+ }
882
+ function verifyYjsEnvelopeV1(envelope) {
883
+ try {
884
+ const publicKey = parseDID(envelope.authorDID);
885
+ const updateHash = hash(envelope.update, "blake3");
886
+ const valid = verify2(updateHash, envelope.signature, publicKey);
887
+ if (!valid) {
888
+ return { valid: false, reason: "invalid_signature" };
889
+ }
890
+ return { valid: true };
891
+ } catch {
892
+ return { valid: false, reason: "did_resolution_failed" };
893
+ }
894
+ }
895
+ function signYjsUpdateV2(update, docId, clientId, keyBundle, options = {}) {
896
+ const { level = DEFAULT_SECURITY_LEVEL } = options;
897
+ const meta = {
898
+ authorDID: keyBundle.identity.did,
899
+ clientId,
900
+ timestamp: Date.now(),
901
+ docId
902
+ };
903
+ const metaBytes = new TextEncoder().encode(JSON.stringify(meta));
904
+ const combined = new Uint8Array(update.length + metaBytes.length);
905
+ combined.set(update, 0);
906
+ combined.set(metaBytes, update.length);
907
+ const signingHash = hash(combined, "blake3");
908
+ const signature = hybridSign(
909
+ signingHash,
910
+ {
911
+ ed25519: keyBundle.signingKey,
912
+ mlDsa: keyBundle.pqSigningKey
913
+ },
914
+ level
915
+ );
916
+ return {
917
+ v: 2,
918
+ update,
919
+ meta,
920
+ signature
921
+ };
922
+ }
923
+ function signYjsUpdateBatch(updates, docId, clientId, keyBundle, options = {}) {
924
+ return updates.map((update) => signYjsUpdateV2(update, docId, clientId, keyBundle, options));
925
+ }
926
+ async function verifyYjsEnvelopeV2(envelope, options = {}) {
927
+ const { registry, expectedDocId, maxAge, minLevel = 0, policy = "strict" } = options;
928
+ const errors = [];
929
+ if (expectedDocId && envelope.meta.docId !== expectedDocId) {
930
+ errors.push(`Document ID mismatch: expected ${expectedDocId}, got ${envelope.meta.docId}`);
931
+ }
932
+ if (maxAge) {
933
+ const age = Date.now() - envelope.meta.timestamp;
934
+ if (age > maxAge) {
935
+ errors.push(`Envelope too old: ${age}ms > ${maxAge}ms`);
936
+ }
937
+ }
938
+ if (envelope.signature.level < minLevel) {
939
+ errors.push(
940
+ `Security level too low: ${envelope.signature.level} < ${minLevel} (required minimum)`
941
+ );
942
+ }
943
+ let ed25519PublicKey;
944
+ try {
945
+ ed25519PublicKey = parseDID(envelope.meta.authorDID);
946
+ } catch {
947
+ errors.push("Failed to parse author DID");
948
+ return {
949
+ valid: false,
950
+ level: envelope.signature.level,
951
+ errors,
952
+ authorDID: envelope.meta.authorDID,
953
+ clientId: envelope.meta.clientId
954
+ };
955
+ }
956
+ let pqPublicKey;
957
+ if (envelope.signature.level >= 1 && registry) {
958
+ const lookedUp = await registry.lookup(envelope.meta.authorDID);
959
+ pqPublicKey = lookedUp ?? void 0;
960
+ }
961
+ const metaBytes = new TextEncoder().encode(JSON.stringify(envelope.meta));
962
+ const combined = new Uint8Array(envelope.update.length + metaBytes.length);
963
+ combined.set(envelope.update, 0);
964
+ combined.set(metaBytes, envelope.update.length);
965
+ const signingHash = hash(combined, "blake3");
966
+ const result = hybridVerify(
967
+ signingHash,
968
+ envelope.signature,
969
+ { ed25519: ed25519PublicKey, mlDsa: pqPublicKey },
970
+ { minLevel, policy }
971
+ );
972
+ if (!result.valid) {
973
+ if (result.details.ed25519?.error) errors.push(result.details.ed25519.error);
974
+ if (result.details.mlDsa?.error) errors.push(result.details.mlDsa.error);
975
+ if (!result.details.ed25519?.error && !result.details.mlDsa?.error) {
976
+ errors.push("Signature verification failed");
977
+ }
978
+ }
979
+ return {
980
+ valid: errors.length === 0,
981
+ level: envelope.signature.level,
982
+ errors,
983
+ authorDID: envelope.meta.authorDID,
984
+ clientId: envelope.meta.clientId
985
+ };
986
+ }
987
+ async function verifyYjsEnvelopeQuick(envelope, options = {}) {
988
+ const result = await verifyYjsEnvelopeV2(envelope, options);
989
+ return result.valid;
990
+ }
991
+ function serializeYjsEnvelope(envelope) {
992
+ return {
993
+ v: 2,
994
+ u: toBase64(envelope.update),
995
+ m: {
996
+ a: envelope.meta.authorDID,
997
+ c: envelope.meta.clientId,
998
+ t: envelope.meta.timestamp,
999
+ d: envelope.meta.docId
1000
+ },
1001
+ s: encodeSignature(envelope.signature)
1002
+ };
1003
+ }
1004
+ function deserializeYjsEnvelope(wire) {
1005
+ if (wire.v !== 2) {
1006
+ throw new Error(`Unsupported envelope version: ${wire.v}. Expected version 2.`);
1007
+ }
1008
+ return {
1009
+ v: 2,
1010
+ update: fromBase64(wire.u),
1011
+ meta: {
1012
+ authorDID: wire.m.a,
1013
+ clientId: wire.m.c,
1014
+ timestamp: wire.m.t,
1015
+ docId: wire.m.d
1016
+ },
1017
+ signature: decodeSignature(wire.s)
1018
+ };
1019
+ }
1020
+ function envelopeSize(envelope) {
1021
+ let size = envelope.update.length;
1022
+ size += JSON.stringify(envelope.meta).length;
1023
+ if (envelope.signature.ed25519) size += envelope.signature.ed25519.length;
1024
+ if (envelope.signature.mlDsa) size += envelope.signature.mlDsa.length;
1025
+ return size;
1026
+ }
1027
+ function signYjsUpdate(update, authorDIDOrDocId, privateKeyOrClientId, clientIdOrKeyBundle, options) {
1028
+ if (typeof privateKeyOrClientId === "number") {
1029
+ return signYjsUpdateV2(
1030
+ update,
1031
+ authorDIDOrDocId,
1032
+ privateKeyOrClientId,
1033
+ clientIdOrKeyBundle,
1034
+ options
1035
+ );
1036
+ }
1037
+ return signYjsUpdateV1(
1038
+ update,
1039
+ authorDIDOrDocId,
1040
+ privateKeyOrClientId,
1041
+ clientIdOrKeyBundle
1042
+ );
1043
+ }
1044
+ function verifyYjsEnvelope(envelope, options) {
1045
+ if (isV2Envelope(envelope)) {
1046
+ return verifyYjsEnvelopeV2(envelope, options);
1047
+ }
1048
+ return verifyYjsEnvelopeV1(envelope);
1049
+ }
1050
+ function hasSignedEnvelope(msg) {
1051
+ return typeof msg === "object" && msg !== null && "envelope" in msg && typeof msg.envelope === "object" && msg.envelope !== null && "update" in msg.envelope && ("authorDID" in msg.envelope || "meta" in msg.envelope) && ("signature" in msg.envelope || "v" in msg.envelope);
1052
+ }
1053
+ function isLegacyUpdate(msg) {
1054
+ return typeof msg === "object" && msg !== null && "data" in msg && msg.data instanceof Uint8Array && !("envelope" in msg);
1055
+ }
1056
+
1057
+ // src/yjs-limits.ts
1058
+ var MAX_YJS_UPDATE_SIZE = 1048576;
1059
+ var MAX_YJS_UPDATES_PER_SECOND = 30;
1060
+ var MAX_YJS_UPDATES_PER_MINUTE = 600;
1061
+ var MAX_YJS_DOC_SIZE = 52428800;
1062
+ var YJS_SYNC_CHUNK_SIZE = 262144;
1063
+ var YJS_RATE_BURST_ALLOWANCE = 10;
1064
+ var DEFAULT_RATE_LIMITER_CONFIG = {
1065
+ maxPerSecond: MAX_YJS_UPDATES_PER_SECOND,
1066
+ maxPerMinute: MAX_YJS_UPDATES_PER_MINUTE,
1067
+ burstAllowance: YJS_RATE_BURST_ALLOWANCE
1068
+ };
1069
+ var YjsRateLimiter = class {
1070
+ secondWindows = /* @__PURE__ */ new Map();
1071
+ minuteWindows = /* @__PURE__ */ new Map();
1072
+ config;
1073
+ cleanupInterval = null;
1074
+ staleThresholdMs;
1075
+ constructor(options = {}) {
1076
+ const { staleThresholdMs, cleanupIntervalMs, ...config } = options;
1077
+ this.config = { ...DEFAULT_RATE_LIMITER_CONFIG, ...config };
1078
+ this.staleThresholdMs = staleThresholdMs ?? 2 * 60 * 1e3;
1079
+ const interval = cleanupIntervalMs ?? 30 * 1e3;
1080
+ if (interval > 0) {
1081
+ this.startCleanup(interval);
1082
+ }
1083
+ }
1084
+ /** Start periodic cleanup of stale entries. */
1085
+ startCleanup(intervalMs = 30 * 1e3) {
1086
+ this.stopCleanup();
1087
+ this.cleanupInterval = setInterval(() => this.cleanupStale(), intervalMs);
1088
+ if (typeof this.cleanupInterval === "object" && "unref" in this.cleanupInterval) {
1089
+ this.cleanupInterval.unref();
1090
+ }
1091
+ }
1092
+ /** Stop periodic cleanup. */
1093
+ stopCleanup() {
1094
+ if (this.cleanupInterval) {
1095
+ clearInterval(this.cleanupInterval);
1096
+ this.cleanupInterval = null;
1097
+ }
1098
+ }
1099
+ /** Remove entries whose windows have expired. Returns count removed. */
1100
+ cleanupStale() {
1101
+ const now = Date.now();
1102
+ const threshold = now - this.staleThresholdMs;
1103
+ let removed = 0;
1104
+ for (const [peerId, window] of this.secondWindows) {
1105
+ if (window.resetAt < threshold) {
1106
+ this.secondWindows.delete(peerId);
1107
+ removed++;
1108
+ }
1109
+ }
1110
+ for (const [peerId, window] of this.minuteWindows) {
1111
+ if (window.resetAt < threshold) {
1112
+ this.minuteWindows.delete(peerId);
1113
+ removed++;
1114
+ }
1115
+ }
1116
+ return removed;
1117
+ }
1118
+ /** Get number of tracked peers. */
1119
+ get peerCount() {
1120
+ const peers = /* @__PURE__ */ new Set([...this.secondWindows.keys(), ...this.minuteWindows.keys()]);
1121
+ return peers.size;
1122
+ }
1123
+ /**
1124
+ * Check if a peer can send another update.
1125
+ *
1126
+ * @param peerId - Peer identifier
1127
+ * @returns true if allowed, false if rate-limited
1128
+ */
1129
+ allow(peerId) {
1130
+ const now = Date.now();
1131
+ const sec = this.secondWindows.get(peerId);
1132
+ if (!sec || now >= sec.resetAt) {
1133
+ this.secondWindows.set(peerId, { count: 1, resetAt: now + 1e3 });
1134
+ } else {
1135
+ sec.count++;
1136
+ if (sec.count > this.config.maxPerSecond + this.config.burstAllowance) {
1137
+ return false;
1138
+ }
1139
+ }
1140
+ const min = this.minuteWindows.get(peerId);
1141
+ if (!min || now >= min.resetAt) {
1142
+ this.minuteWindows.set(peerId, { count: 1, resetAt: now + 6e4 });
1143
+ } else {
1144
+ min.count++;
1145
+ if (min.count > this.config.maxPerMinute) {
1146
+ return false;
1147
+ }
1148
+ }
1149
+ return true;
1150
+ }
1151
+ /**
1152
+ * Get current rate info for a peer (for debugging/monitoring).
1153
+ */
1154
+ getInfo(peerId) {
1155
+ const sec = this.secondWindows.get(peerId);
1156
+ const min = this.minuteWindows.get(peerId);
1157
+ if (!sec && !min) return void 0;
1158
+ const now = Date.now();
1159
+ return {
1160
+ perSecond: sec && now < sec.resetAt ? sec.count : 0,
1161
+ perMinute: min && now < min.resetAt ? min.count : 0
1162
+ };
1163
+ }
1164
+ /**
1165
+ * Reset state for a disconnected peer.
1166
+ */
1167
+ remove(peerId) {
1168
+ this.secondWindows.delete(peerId);
1169
+ this.minuteWindows.delete(peerId);
1170
+ }
1171
+ /**
1172
+ * Clear all state.
1173
+ */
1174
+ clear() {
1175
+ this.secondWindows.clear();
1176
+ this.minuteWindows.clear();
1177
+ }
1178
+ /**
1179
+ * Stop cleanup and clear all state.
1180
+ */
1181
+ destroy() {
1182
+ this.stopCleanup();
1183
+ this.clear();
1184
+ }
1185
+ };
1186
+ function isUpdateTooLarge(update, maxSize = MAX_YJS_UPDATE_SIZE) {
1187
+ return update.length > maxSize;
1188
+ }
1189
+ function isDocumentTooLarge(state, maxSize = MAX_YJS_DOC_SIZE) {
1190
+ return state.length > maxSize;
1191
+ }
1192
+ function calculateChunkCount(totalSize, chunkSize = YJS_SYNC_CHUNK_SIZE) {
1193
+ return Math.ceil(totalSize / chunkSize);
1194
+ }
1195
+ function chunkUpdate(update, chunkSize = YJS_SYNC_CHUNK_SIZE) {
1196
+ const chunks = [];
1197
+ for (let i = 0; i < update.length; i += chunkSize) {
1198
+ chunks.push(update.slice(i, Math.min(i + chunkSize, update.length)));
1199
+ }
1200
+ return chunks;
1201
+ }
1202
+ function reassembleChunks(chunks) {
1203
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1204
+ const result = new Uint8Array(totalLength);
1205
+ let offset = 0;
1206
+ for (const chunk of chunks) {
1207
+ result.set(chunk, offset);
1208
+ offset += chunk.length;
1209
+ }
1210
+ return result;
1211
+ }
1212
+
1213
+ // src/yjs-integrity.ts
1214
+ import { hashHex as hashHex2 } from "@xnetjs/crypto";
1215
+ function hashYjsState(state) {
1216
+ return hashHex2(state, "blake3");
1217
+ }
1218
+ function verifyYjsStateIntegrity(state, expectedHash) {
1219
+ return hashYjsState(state) === expectedHash;
1220
+ }
1221
+ var YjsIntegrityError = class extends Error {
1222
+ constructor(docId, expectedHash, actualHash) {
1223
+ super(
1224
+ `Yjs state corrupted for doc ${docId}: expected ${expectedHash.slice(0, 16)}..., got ${actualHash.slice(0, 16)}...`
1225
+ );
1226
+ this.docId = docId;
1227
+ this.expectedHash = expectedHash;
1228
+ this.actualHash = actualHash;
1229
+ this.name = "YjsIntegrityError";
1230
+ }
1231
+ };
1232
+ function createPersistedDocState(state, updateCount = 0) {
1233
+ return {
1234
+ state,
1235
+ hash: hashYjsState(state),
1236
+ persistedAt: Date.now(),
1237
+ updateCount
1238
+ };
1239
+ }
1240
+ function verifyPersistedDocState(docId, record) {
1241
+ const actualHash = hashYjsState(record.state);
1242
+ if (actualHash !== record.hash) {
1243
+ throw new YjsIntegrityError(docId, record.hash, actualHash);
1244
+ }
1245
+ }
1246
+ function loadVerifiedState(docId, record) {
1247
+ if (!record.hash) {
1248
+ return record.state;
1249
+ }
1250
+ if (!verifyYjsStateIntegrity(record.state, record.hash)) {
1251
+ throw new YjsIntegrityError(docId, record.hash, hashYjsState(record.state));
1252
+ }
1253
+ return record.state;
1254
+ }
1255
+ var COMPACTION_UPDATE_THRESHOLD = 100;
1256
+ var COMPACTION_TIME_THRESHOLD = 36e5;
1257
+ function shouldCompact(updateCount, persistedAt) {
1258
+ if (updateCount >= COMPACTION_UPDATE_THRESHOLD) {
1259
+ return true;
1260
+ }
1261
+ if (Date.now() - persistedAt > COMPACTION_TIME_THRESHOLD) {
1262
+ return true;
1263
+ }
1264
+ return false;
1265
+ }
1266
+
1267
+ // src/yjs-peer-scoring.ts
1268
+ var DEFAULT_YJS_SCORING_CONFIG = {
1269
+ penalties: {
1270
+ invalidSignature: 30,
1271
+ oversizedUpdate: 10,
1272
+ rateExceeded: 5,
1273
+ unsignedUpdate: 20,
1274
+ unattestedClientId: 15,
1275
+ unauthorizedUpdate: 20
1276
+ },
1277
+ thresholds: {
1278
+ warn: 50,
1279
+ throttle: 30,
1280
+ block: 10
1281
+ },
1282
+ recoveryRate: 1,
1283
+ instantBlockAfter: 3
1284
+ };
1285
+ var YjsPeerScorer = class {
1286
+ metrics = /* @__PURE__ */ new Map();
1287
+ scores = /* @__PURE__ */ new Map();
1288
+ config;
1289
+ telemetry;
1290
+ constructor(config) {
1291
+ this.config = {
1292
+ penalties: { ...DEFAULT_YJS_SCORING_CONFIG.penalties, ...config?.penalties },
1293
+ thresholds: { ...DEFAULT_YJS_SCORING_CONFIG.thresholds, ...config?.thresholds },
1294
+ recoveryRate: config?.recoveryRate ?? DEFAULT_YJS_SCORING_CONFIG.recoveryRate,
1295
+ instantBlockAfter: config?.instantBlockAfter ?? DEFAULT_YJS_SCORING_CONFIG.instantBlockAfter,
1296
+ telemetry: config?.telemetry
1297
+ };
1298
+ this.telemetry = config?.telemetry;
1299
+ }
1300
+ /**
1301
+ * Record a violation for a peer.
1302
+ *
1303
+ * @param peerId - Peer identifier
1304
+ * @param reason - Type of violation
1305
+ * @returns Action to take based on new score
1306
+ */
1307
+ penalize(peerId, reason) {
1308
+ const metrics = this.getOrCreateMetrics(peerId);
1309
+ const penalty = this.config.penalties[reason];
1310
+ switch (reason) {
1311
+ case "invalidSignature":
1312
+ metrics.invalidSignatures++;
1313
+ this.telemetry?.reportSecurityEvent("sync.yjs.invalid_signature", "high");
1314
+ if (metrics.invalidSignatures >= this.config.instantBlockAfter) {
1315
+ this.scores.set(peerId, 0);
1316
+ this.telemetry?.reportSecurityEvent("sync.yjs.peer_auto_blocked", "critical");
1317
+ this.telemetry?.reportUsage("sync.yjs.peer_action.block", 1);
1318
+ return "block";
1319
+ }
1320
+ break;
1321
+ case "oversizedUpdate":
1322
+ metrics.oversizedUpdates++;
1323
+ this.telemetry?.reportSecurityEvent("sync.yjs.oversized_update", "medium");
1324
+ break;
1325
+ case "rateExceeded":
1326
+ metrics.rateExceeded++;
1327
+ this.telemetry?.reportSecurityEvent("sync.yjs.rate_exceeded", "medium");
1328
+ break;
1329
+ case "unsignedUpdate":
1330
+ metrics.unsignedUpdates++;
1331
+ this.telemetry?.reportSecurityEvent("sync.yjs.unsigned_update", "high");
1332
+ break;
1333
+ case "unattestedClientId":
1334
+ metrics.unattestedClientIds++;
1335
+ this.telemetry?.reportSecurityEvent("sync.yjs.unattested_client_id", "medium");
1336
+ break;
1337
+ case "unauthorizedUpdate":
1338
+ metrics.unauthorizedUpdates++;
1339
+ this.telemetry?.reportSecurityEvent("sync.yjs.unauthorized_update", "high");
1340
+ break;
1341
+ }
1342
+ metrics.lastViolation = Date.now();
1343
+ const currentScore = this.scores.get(peerId) ?? 100;
1344
+ const newScore = Math.max(0, currentScore - penalty);
1345
+ this.scores.set(peerId, newScore);
1346
+ const action = this.getAction(newScore);
1347
+ if (action !== "allow") {
1348
+ this.telemetry?.reportUsage(`sync.yjs.peer_action.${action}`, 1);
1349
+ }
1350
+ return action;
1351
+ }
1352
+ /**
1353
+ * Record a valid update (for ratio tracking + score recovery consideration).
1354
+ */
1355
+ recordValid(peerId) {
1356
+ const metrics = this.getOrCreateMetrics(peerId);
1357
+ metrics.validUpdates++;
1358
+ this.telemetry?.reportUsage("sync.yjs.valid_update", 1);
1359
+ }
1360
+ /**
1361
+ * Get current score for a peer.
1362
+ * New peers start at 100.
1363
+ */
1364
+ getScore(peerId) {
1365
+ return this.scores.get(peerId) ?? 100;
1366
+ }
1367
+ /**
1368
+ * Get action for a given score.
1369
+ */
1370
+ getAction(score) {
1371
+ if (score <= this.config.thresholds.block) return "block";
1372
+ if (score <= this.config.thresholds.throttle) return "throttle";
1373
+ if (score <= this.config.thresholds.warn) return "warn";
1374
+ return "allow";
1375
+ }
1376
+ /**
1377
+ * Get current action for a peer.
1378
+ */
1379
+ getPeerAction(peerId) {
1380
+ return this.getAction(this.getScore(peerId));
1381
+ }
1382
+ /**
1383
+ * Recover scores over time (called periodically).
1384
+ * Only recovers if no recent violations.
1385
+ *
1386
+ * @param recoveryWindow - Time in ms that must pass without violations (default: 60s)
1387
+ */
1388
+ tick(recoveryWindow = 6e4) {
1389
+ const now = Date.now();
1390
+ for (const [peerId, score] of this.scores) {
1391
+ if (score >= 100) continue;
1392
+ const metrics = this.metrics.get(peerId);
1393
+ if (!metrics) continue;
1394
+ if (now - metrics.lastViolation > recoveryWindow) {
1395
+ const newScore = Math.min(100, score + this.config.recoveryRate);
1396
+ this.scores.set(peerId, newScore);
1397
+ }
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Get metrics for a peer (for debugging/monitoring).
1402
+ */
1403
+ getMetrics(peerId) {
1404
+ return this.metrics.get(peerId);
1405
+ }
1406
+ /**
1407
+ * Get all peer IDs with metrics.
1408
+ */
1409
+ getAllPeerIds() {
1410
+ return Array.from(this.metrics.keys());
1411
+ }
1412
+ /**
1413
+ * Get all metrics (for monitoring endpoint).
1414
+ */
1415
+ getAllMetrics() {
1416
+ return new Map(this.metrics);
1417
+ }
1418
+ /**
1419
+ * Remove all state for a disconnected peer.
1420
+ */
1421
+ remove(peerId) {
1422
+ this.metrics.delete(peerId);
1423
+ this.scores.delete(peerId);
1424
+ }
1425
+ /**
1426
+ * Clear all state.
1427
+ */
1428
+ clear() {
1429
+ this.metrics.clear();
1430
+ this.scores.clear();
1431
+ }
1432
+ /**
1433
+ * Get violation ratio for a peer.
1434
+ * Returns the ratio of violations to total operations.
1435
+ */
1436
+ getViolationRatio(peerId) {
1437
+ const metrics = this.metrics.get(peerId);
1438
+ if (!metrics) return 0;
1439
+ const violations = metrics.invalidSignatures + metrics.oversizedUpdates + metrics.rateExceeded + metrics.unsignedUpdates + metrics.unattestedClientIds + metrics.unauthorizedUpdates;
1440
+ const total = violations + metrics.validUpdates;
1441
+ if (total === 0) return 0;
1442
+ return violations / total;
1443
+ }
1444
+ getOrCreateMetrics(peerId) {
1445
+ let m = this.metrics.get(peerId);
1446
+ if (!m) {
1447
+ m = {
1448
+ invalidSignatures: 0,
1449
+ oversizedUpdates: 0,
1450
+ rateExceeded: 0,
1451
+ unsignedUpdates: 0,
1452
+ unattestedClientIds: 0,
1453
+ unauthorizedUpdates: 0,
1454
+ validUpdates: 0,
1455
+ firstSeen: Date.now(),
1456
+ lastViolation: 0
1457
+ };
1458
+ this.metrics.set(peerId, m);
1459
+ }
1460
+ return m;
1461
+ }
1462
+ };
1463
+
1464
+ // src/yjs-authorization.ts
1465
+ import {
1466
+ base64ToBytes,
1467
+ bytesToBase64,
1468
+ constantTimeEqual,
1469
+ decrypt,
1470
+ encrypt,
1471
+ hash as hash2
1472
+ } from "@xnetjs/crypto";
1473
+ var YjsStateIntegrityError = class extends Error {
1474
+ constructor(message = "Y.Doc state hash mismatch") {
1475
+ super(message);
1476
+ this.name = "YjsStateIntegrityError";
1477
+ }
1478
+ };
1479
+ function encryptYjsState(state, nodeId, contentKey, options = {}) {
1480
+ const encrypted = encrypt(state, contentKey);
1481
+ return {
1482
+ nodeId,
1483
+ version: 1,
1484
+ encryptedState: encrypted.ciphertext,
1485
+ nonce: encrypted.nonce,
1486
+ stateVector: options.stateVector ?? new Uint8Array(0),
1487
+ stateHash: hash2(state, "blake3"),
1488
+ checkpointedAt: options.checkpointedAt ?? Date.now(),
1489
+ updatesSinceCheckpoint: options.updatesSinceCheckpoint ?? 0
1490
+ };
1491
+ }
1492
+ function decryptYjsState(encrypted, contentKey) {
1493
+ let state;
1494
+ try {
1495
+ state = decrypt(
1496
+ {
1497
+ nonce: encrypted.nonce,
1498
+ ciphertext: encrypted.encryptedState
1499
+ },
1500
+ contentKey
1501
+ );
1502
+ } catch {
1503
+ throw new YjsStateIntegrityError("Y.Doc state failed decryption or integrity check");
1504
+ }
1505
+ const computedHash = hash2(state, "blake3");
1506
+ if (!constantTimeEqual(computedHash, encrypted.stateHash)) {
1507
+ throw new YjsStateIntegrityError();
1508
+ }
1509
+ return state;
1510
+ }
1511
+ function serializeEncryptedYjsState(state) {
1512
+ const payload = {
1513
+ nodeId: state.nodeId,
1514
+ version: 1,
1515
+ encryptedState: bytesToBase64(state.encryptedState),
1516
+ nonce: bytesToBase64(state.nonce),
1517
+ stateVector: bytesToBase64(state.stateVector),
1518
+ stateHash: bytesToBase64(state.stateHash),
1519
+ checkpointedAt: state.checkpointedAt,
1520
+ updatesSinceCheckpoint: state.updatesSinceCheckpoint
1521
+ };
1522
+ return new TextEncoder().encode(JSON.stringify(payload));
1523
+ }
1524
+ function deserializeEncryptedYjsState(bytes) {
1525
+ const decoded = JSON.parse(new TextDecoder().decode(bytes));
1526
+ if (!decoded || decoded.version !== 1 || typeof decoded.nodeId !== "string" || typeof decoded.encryptedState !== "string" || typeof decoded.nonce !== "string" || typeof decoded.stateVector !== "string" || typeof decoded.stateHash !== "string" || typeof decoded.checkpointedAt !== "number" || typeof decoded.updatesSinceCheckpoint !== "number") {
1527
+ throw new Error("Invalid EncryptedYjsState payload");
1528
+ }
1529
+ return {
1530
+ nodeId: decoded.nodeId,
1531
+ version: 1,
1532
+ encryptedState: base64ToBytes(decoded.encryptedState),
1533
+ nonce: base64ToBytes(decoded.nonce),
1534
+ stateVector: base64ToBytes(decoded.stateVector),
1535
+ stateHash: base64ToBytes(decoded.stateHash),
1536
+ checkpointedAt: decoded.checkpointedAt,
1537
+ updatesSinceCheckpoint: decoded.updatesSinceCheckpoint
1538
+ };
1539
+ }
1540
+ var YjsAuthGate = class _YjsAuthGate {
1541
+ constructor(evaluator, nodeId, options = {}) {
1542
+ this.evaluator = evaluator;
1543
+ this.nodeId = nodeId;
1544
+ this.cacheTTL = options.cacheTTL ?? _YjsAuthGate.DEFAULT_CACHE_TTL;
1545
+ this.now = options.now ?? Date.now;
1546
+ }
1547
+ static DEFAULT_CACHE_TTL = 3e4;
1548
+ peerAuthCache = /* @__PURE__ */ new Map();
1549
+ cacheTTL;
1550
+ now;
1551
+ async canApplyUpdate(envelope) {
1552
+ const authorDID = envelope.meta.authorDID;
1553
+ const cached = this.peerAuthCache.get(authorDID);
1554
+ if (cached && cached.expiresAt > this.now()) {
1555
+ return { allowed: cached.allowed, authorDID, cached: true };
1556
+ }
1557
+ const input = {
1558
+ subject: authorDID,
1559
+ action: "write",
1560
+ nodeId: this.nodeId
1561
+ };
1562
+ const decision = await this.evaluator.can(input);
1563
+ this.peerAuthCache.set(authorDID, {
1564
+ allowed: decision.allowed,
1565
+ expiresAt: this.now() + this.cacheTTL
1566
+ });
1567
+ return { allowed: decision.allowed, authorDID, cached: false };
1568
+ }
1569
+ invalidatePeer(did) {
1570
+ this.peerAuthCache.delete(did);
1571
+ }
1572
+ invalidateAll() {
1573
+ this.peerAuthCache.clear();
1574
+ }
1575
+ };
1576
+ var YjsCheckpointer = class {
1577
+ maxUpdates;
1578
+ maxAgeMs;
1579
+ now;
1580
+ constructor(options = {}) {
1581
+ this.maxUpdates = options.maxUpdates ?? 100;
1582
+ this.maxAgeMs = options.maxAgeMs ?? 60 * 60 * 1e3;
1583
+ this.now = options.now ?? Date.now;
1584
+ }
1585
+ shouldCheckpoint(state) {
1586
+ return state.updatesSinceCheckpoint >= this.maxUpdates || this.now() - state.checkpointedAt >= this.maxAgeMs;
1587
+ }
1588
+ checkpoint(input) {
1589
+ return encryptYjsState(input.state, input.nodeId, input.contentKey, {
1590
+ stateVector: input.stateVector,
1591
+ checkpointedAt: this.now(),
1592
+ updatesSinceCheckpoint: 0
1593
+ });
1594
+ }
1595
+ };
1596
+ function toEncryptedData(state) {
1597
+ return {
1598
+ nonce: state.nonce,
1599
+ ciphertext: state.encryptedState
1600
+ };
1601
+ }
1602
+
1603
+ // src/yjs-authorized-sync.ts
1604
+ import { generateContentKey, wrapKeyForRecipient } from "@xnetjs/crypto";
1605
+ var GRANT_SCHEMA_ID = "xnet://xnet.fyi/Grant";
1606
+ var AuthorizedSyncManager = class {
1607
+ options;
1608
+ rooms = /* @__PURE__ */ new Map();
1609
+ constructor(options) {
1610
+ this.options = options;
1611
+ }
1612
+ async acquire(nodeId, mode = "write") {
1613
+ const decision = await this.options.evaluator.can({
1614
+ subject: this.options.authorDID,
1615
+ action: mode === "write" ? "write" : "read",
1616
+ nodeId
1617
+ });
1618
+ if (!decision.allowed) {
1619
+ throw new AuthorizedYjsError("PERMISSION_DENIED", decision);
1620
+ }
1621
+ const contentKey = await this.options.keyProvider.getOrUnwrap(nodeId);
1622
+ const doc = this.options.ydoc.createDoc(nodeId);
1623
+ const encryptedBytes = await this.options.adapter.getDocumentContent(nodeId);
1624
+ if (encryptedBytes) {
1625
+ const encrypted = deserializeEncryptedYjsState(encryptedBytes);
1626
+ const state = decryptYjsState(encrypted, contentKey);
1627
+ this.options.ydoc.applyUpdate(doc, state, "bootstrap");
1628
+ }
1629
+ let room;
1630
+ if (mode === "write") {
1631
+ room = this.joinRoom(nodeId, doc, contentKey);
1632
+ }
1633
+ return {
1634
+ doc,
1635
+ nodeId,
1636
+ mode,
1637
+ contentKey,
1638
+ room,
1639
+ release: () => this.release(nodeId)
1640
+ };
1641
+ }
1642
+ release(nodeId) {
1643
+ const room = this.rooms.get(nodeId);
1644
+ if (!room) {
1645
+ return;
1646
+ }
1647
+ room.unsubscribe?.();
1648
+ this.rooms.delete(nodeId);
1649
+ }
1650
+ joinRoom(nodeId, doc, contentKey) {
1651
+ const existing = this.rooms.get(nodeId);
1652
+ if (existing) {
1653
+ return existing;
1654
+ }
1655
+ const room = {
1656
+ nodeId,
1657
+ doc,
1658
+ contentKey,
1659
+ authGate: new YjsAuthGate(this.options.evaluator, nodeId),
1660
+ authorizedPeers: /* @__PURE__ */ new Set([this.options.authorDID])
1661
+ };
1662
+ room.unsubscribe = this.wireRevocationEvents(room);
1663
+ this.rooms.set(nodeId, room);
1664
+ return room;
1665
+ }
1666
+ wireRevocationEvents(room) {
1667
+ return this.options.store.subscribe((event) => {
1668
+ const grantNode = event.node;
1669
+ if (!grantNode || grantNode.schemaId !== GRANT_SCHEMA_ID) {
1670
+ return;
1671
+ }
1672
+ const resource = grantNode.properties?.resource;
1673
+ if (typeof resource !== "string" || resource !== room.nodeId) {
1674
+ return;
1675
+ }
1676
+ const revokedAt = grantNode.properties?.revokedAt;
1677
+ const grantee = grantNode.properties?.grantee;
1678
+ if (typeof revokedAt !== "number" || revokedAt <= 0 || !isDid(grantee)) {
1679
+ return;
1680
+ }
1681
+ room.authGate.invalidatePeer(grantee);
1682
+ if (!room.authorizedPeers.has(grantee)) {
1683
+ return;
1684
+ }
1685
+ void this.handlePeerRevocation(room, grantee);
1686
+ });
1687
+ }
1688
+ async handlePeerRevocation(room, revokedDid) {
1689
+ room.authorizedPeers.delete(revokedDid);
1690
+ room.doc.emit?.("peer:kicked", [revokedDid]);
1691
+ const newContentKey = generateContentKey();
1692
+ const encrypted = encryptYjsState(
1693
+ this.options.ydoc.encodeStateAsUpdate(room.doc),
1694
+ room.nodeId,
1695
+ newContentKey,
1696
+ {
1697
+ stateVector: this.options.ydoc.encodeStateVector(room.doc)
1698
+ }
1699
+ );
1700
+ await this.options.adapter.setDocumentContent(
1701
+ room.nodeId,
1702
+ serializeEncryptedYjsState(encrypted)
1703
+ );
1704
+ const recipients = [...room.authorizedPeers];
1705
+ let wrappedKeys = {};
1706
+ if (this.options.publicKeyResolver && recipients.length > 0) {
1707
+ const publicKeys = await this.options.publicKeyResolver.resolveBatch(recipients);
1708
+ wrappedKeys = {};
1709
+ for (const [did, pubKey] of publicKeys) {
1710
+ wrappedKeys[did] = wrapKeyForRecipient(newContentKey, pubKey);
1711
+ }
1712
+ }
1713
+ await this.options.onRotateContentKey?.({
1714
+ nodeId: room.nodeId,
1715
+ recipients,
1716
+ wrappedKeys,
1717
+ contentKey: newContentKey
1718
+ });
1719
+ room.contentKey = newContentKey;
1720
+ room.doc.emit?.("key:rotated", [room.nodeId]);
1721
+ }
1722
+ };
1723
+ var AuthorizedYjsSyncProvider = class {
1724
+ nodeId;
1725
+ doc;
1726
+ ydoc;
1727
+ authGate;
1728
+ peerScorer;
1729
+ rateLimiter;
1730
+ verifyEnvelope;
1731
+ onRejected;
1732
+ constructor(options) {
1733
+ this.nodeId = options.nodeId;
1734
+ this.doc = options.doc;
1735
+ this.ydoc = options.ydoc;
1736
+ this.authGate = options.authGate;
1737
+ this.peerScorer = options.peerScorer ?? new YjsPeerScorer();
1738
+ this.rateLimiter = options.rateLimiter;
1739
+ this.verifyEnvelope = options.verifyEnvelope ?? verifyYjsEnvelopeV2;
1740
+ this.onRejected = options.onRejected;
1741
+ }
1742
+ async handleRemoteUpdate(envelope) {
1743
+ const peerId = envelope.meta.authorDID;
1744
+ if (this.rateLimiter && !this.rateLimiter.allow(peerId)) {
1745
+ this.peerScorer.penalize(peerId, "rateExceeded");
1746
+ this.onRejected?.({ peerId, reason: "rate-exceeded" });
1747
+ return false;
1748
+ }
1749
+ const sigResult = await this.verifyEnvelope(envelope);
1750
+ if (!sigResult.valid) {
1751
+ this.peerScorer.penalize(peerId, "invalidSignature");
1752
+ this.onRejected?.({ peerId, reason: "invalid-signature" });
1753
+ return false;
1754
+ }
1755
+ const authResult = await this.authGate.canApplyUpdate(envelope);
1756
+ if (!authResult.allowed) {
1757
+ this.peerScorer.penalize(peerId, "unauthorizedUpdate");
1758
+ this.onRejected?.({ peerId, reason: "unauthorized" });
1759
+ return false;
1760
+ }
1761
+ if (envelope.meta.docId !== this.nodeId) {
1762
+ this.peerScorer.penalize(peerId, "invalidSignature");
1763
+ this.onRejected?.({ peerId, reason: "invalid-signature" });
1764
+ return false;
1765
+ }
1766
+ this.ydoc.applyUpdate(this.doc, envelope.update, "remote");
1767
+ this.peerScorer.recordValid(peerId);
1768
+ return true;
1769
+ }
1770
+ };
1771
+ var AuthorizedYjsError = class extends Error {
1772
+ code;
1773
+ decision;
1774
+ constructor(code, decision) {
1775
+ super(`Yjs authorization denied for '${decision.action}' on '${decision.resource}'`);
1776
+ this.name = "AuthorizedYjsError";
1777
+ this.code = code;
1778
+ this.decision = decision;
1779
+ }
1780
+ };
1781
+ function isDid(value) {
1782
+ return typeof value === "string" && value.startsWith("did:key:");
1783
+ }
1784
+
1785
+ // src/clientid-attestation.ts
1786
+ import {
1787
+ hash as hash3,
1788
+ sign as sign3,
1789
+ verify as verify3,
1790
+ hybridSign as hybridSign2,
1791
+ hybridVerify as hybridVerify2,
1792
+ encodeSignature as encodeSignature2,
1793
+ decodeSignature as decodeSignature2,
1794
+ DEFAULT_SECURITY_LEVEL as DEFAULT_SECURITY_LEVEL2
1795
+ } from "@xnetjs/crypto";
1796
+ import { parseDID as parseDID2 } from "@xnetjs/identity";
1797
+ function isV2Attestation(attestation) {
1798
+ return "v" in attestation && attestation.v === 2;
1799
+ }
1800
+ function isV1Attestation(attestation) {
1801
+ return !("v" in attestation);
1802
+ }
1803
+ function attestationPayloadV1(clientId, did, room, expiresAt) {
1804
+ const text = `clientid-bind:${clientId}:${did}:${room}:${expiresAt}`;
1805
+ return new TextEncoder().encode(text);
1806
+ }
1807
+ function createClientIdAttestationV1(clientId, did, privateKey, room, ttlSeconds = 3600) {
1808
+ const expiresAt = Math.floor(Date.now() / 1e3) + ttlSeconds;
1809
+ const payload = attestationPayloadV1(clientId, did, room, expiresAt);
1810
+ const payloadHash = hash3(payload, "blake3");
1811
+ const signature = sign3(payloadHash, privateKey);
1812
+ return { clientId, did, signature, expiresAt, room };
1813
+ }
1814
+ function verifyClientIdAttestationV1(attestation) {
1815
+ const now = Math.floor(Date.now() / 1e3);
1816
+ if (attestation.expiresAt < now) {
1817
+ return { valid: false, reason: "expired" };
1818
+ }
1819
+ try {
1820
+ const publicKey = parseDID2(attestation.did);
1821
+ const payload = attestationPayloadV1(
1822
+ attestation.clientId,
1823
+ attestation.did,
1824
+ attestation.room,
1825
+ attestation.expiresAt
1826
+ );
1827
+ const payloadHash = hash3(payload, "blake3");
1828
+ const valid = verify3(payloadHash, attestation.signature, publicKey);
1829
+ if (!valid) {
1830
+ return { valid: false, reason: "invalid_signature" };
1831
+ }
1832
+ return { valid: true };
1833
+ } catch {
1834
+ return { valid: false, reason: "did_resolution_failed" };
1835
+ }
1836
+ }
1837
+ function createClientIdAttestationV2(clientId, room, keyBundle, options = {}) {
1838
+ const { expiresInMs, level = DEFAULT_SECURITY_LEVEL2 } = options;
1839
+ const timestamp = Date.now();
1840
+ const expiresAt = expiresInMs ? timestamp + expiresInMs : void 0;
1841
+ const payload = {
1842
+ v: 2,
1843
+ clientId,
1844
+ did: keyBundle.identity.did,
1845
+ timestamp,
1846
+ expiresAt,
1847
+ room
1848
+ };
1849
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
1850
+ const payloadHash = hash3(payloadBytes, "blake3");
1851
+ const signature = hybridSign2(
1852
+ payloadHash,
1853
+ {
1854
+ ed25519: keyBundle.signingKey,
1855
+ mlDsa: keyBundle.pqSigningKey
1856
+ },
1857
+ level
1858
+ );
1859
+ return {
1860
+ v: 2,
1861
+ clientId,
1862
+ did: keyBundle.identity.did,
1863
+ timestamp,
1864
+ expiresAt,
1865
+ room,
1866
+ signature
1867
+ };
1868
+ }
1869
+ async function verifyClientIdAttestationV2(attestation, options = {}) {
1870
+ const { registry, minLevel = 0 } = options;
1871
+ const errors = [];
1872
+ const expired = attestation.expiresAt !== void 0 && Date.now() > attestation.expiresAt;
1873
+ if (expired) {
1874
+ errors.push("Attestation has expired");
1875
+ }
1876
+ if (attestation.signature.level < minLevel) {
1877
+ errors.push(
1878
+ `Security level too low: ${attestation.signature.level} < ${minLevel} (required minimum)`
1879
+ );
1880
+ }
1881
+ let ed25519PublicKey;
1882
+ try {
1883
+ ed25519PublicKey = parseDID2(attestation.did);
1884
+ } catch {
1885
+ errors.push("Failed to parse DID");
1886
+ return {
1887
+ valid: false,
1888
+ expired,
1889
+ level: attestation.signature.level,
1890
+ errors
1891
+ };
1892
+ }
1893
+ let pqPublicKey;
1894
+ if (attestation.signature.level >= 1 && registry) {
1895
+ const lookedUp = await registry.lookup(attestation.did);
1896
+ pqPublicKey = lookedUp ?? void 0;
1897
+ }
1898
+ const payload = {
1899
+ v: 2,
1900
+ clientId: attestation.clientId,
1901
+ did: attestation.did,
1902
+ timestamp: attestation.timestamp,
1903
+ expiresAt: attestation.expiresAt,
1904
+ room: attestation.room
1905
+ };
1906
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
1907
+ const payloadHash = hash3(payloadBytes, "blake3");
1908
+ const result = hybridVerify2(
1909
+ payloadHash,
1910
+ attestation.signature,
1911
+ { ed25519: ed25519PublicKey, mlDsa: pqPublicKey },
1912
+ { minLevel }
1913
+ );
1914
+ if (!result.valid) {
1915
+ if (result.details.ed25519?.error) errors.push(result.details.ed25519.error);
1916
+ if (result.details.mlDsa?.error) errors.push(result.details.mlDsa.error);
1917
+ if (!result.details.ed25519?.error && !result.details.mlDsa?.error) {
1918
+ errors.push("Signature verification failed");
1919
+ }
1920
+ }
1921
+ return {
1922
+ valid: errors.length === 0,
1923
+ expired,
1924
+ level: attestation.signature.level,
1925
+ errors
1926
+ };
1927
+ }
1928
+ function serializeClientIdAttestation(attestation) {
1929
+ return {
1930
+ v: 2,
1931
+ c: attestation.clientId,
1932
+ d: attestation.did,
1933
+ t: attestation.timestamp,
1934
+ e: attestation.expiresAt,
1935
+ r: attestation.room,
1936
+ s: encodeSignature2(attestation.signature)
1937
+ };
1938
+ }
1939
+ function deserializeClientIdAttestation(wire) {
1940
+ if (wire.v !== 2) {
1941
+ throw new Error(`Unsupported attestation version: ${wire.v}. Expected version 2.`);
1942
+ }
1943
+ return {
1944
+ v: 2,
1945
+ clientId: wire.c,
1946
+ did: wire.d,
1947
+ timestamp: wire.t,
1948
+ expiresAt: wire.e,
1949
+ room: wire.r,
1950
+ signature: decodeSignature2(wire.s)
1951
+ };
1952
+ }
1953
+ function createClientIdAttestation(clientId, didOrRoom, privateKeyOrKeyBundle, roomOrOptions, ttlSeconds) {
1954
+ if (typeof privateKeyOrKeyBundle === "object" && "identity" in privateKeyOrKeyBundle && "signingKey" in privateKeyOrKeyBundle) {
1955
+ return createClientIdAttestationV2(
1956
+ clientId,
1957
+ didOrRoom,
1958
+ // This is room in V2
1959
+ privateKeyOrKeyBundle,
1960
+ roomOrOptions
1961
+ );
1962
+ }
1963
+ return createClientIdAttestationV1(
1964
+ clientId,
1965
+ didOrRoom,
1966
+ // This is did in V1
1967
+ privateKeyOrKeyBundle,
1968
+ roomOrOptions,
1969
+ ttlSeconds
1970
+ );
1971
+ }
1972
+ function verifyClientIdAttestation(attestation, options) {
1973
+ if (isV2Attestation(attestation)) {
1974
+ return verifyClientIdAttestationV2(attestation, options);
1975
+ }
1976
+ return verifyClientIdAttestationV1(attestation);
1977
+ }
1978
+ var ClientIdMapImpl = class {
1979
+ byClientId = /* @__PURE__ */ new Map();
1980
+ byDid = /* @__PURE__ */ new Map();
1981
+ getOwner(clientId) {
1982
+ const entry = this.byClientId.get(clientId);
1983
+ if (!entry) return void 0;
1984
+ if (entry.expiresAt < Math.floor(Date.now() / 1e3)) {
1985
+ this.byClientId.delete(clientId);
1986
+ this.byDid.delete(entry.did);
1987
+ return void 0;
1988
+ }
1989
+ return entry.did;
1990
+ }
1991
+ getClientId(did) {
1992
+ const entry = this.byDid.get(did);
1993
+ if (!entry) return void 0;
1994
+ if (entry.expiresAt < Math.floor(Date.now() / 1e3)) {
1995
+ this.byDid.delete(did);
1996
+ this.byClientId.delete(entry.clientId);
1997
+ return void 0;
1998
+ }
1999
+ return entry.clientId;
2000
+ }
2001
+ register(attestation) {
2002
+ const did = isV2Attestation(attestation) ? attestation.did : attestation.did;
2003
+ const expiresAt = isV2Attestation(attestation) ? attestation.expiresAt ? Math.floor(attestation.expiresAt / 1e3) : Math.floor(Date.now() / 1e3) + 3600 : attestation.expiresAt;
2004
+ const prev = this.byDid.get(did);
2005
+ if (prev) {
2006
+ this.byClientId.delete(prev.clientId);
2007
+ }
2008
+ const prevDid = this.byClientId.get(attestation.clientId);
2009
+ if (prevDid) {
2010
+ this.byDid.delete(prevDid.did);
2011
+ }
2012
+ this.byClientId.set(attestation.clientId, { did, expiresAt });
2013
+ this.byDid.set(did, { clientId: attestation.clientId, expiresAt });
2014
+ }
2015
+ cleanup() {
2016
+ const now = Math.floor(Date.now() / 1e3);
2017
+ for (const [clientId, entry] of this.byClientId) {
2018
+ if (entry.expiresAt < now) {
2019
+ this.byClientId.delete(clientId);
2020
+ this.byDid.delete(entry.did);
2021
+ }
2022
+ }
2023
+ }
2024
+ getAll() {
2025
+ const now = Math.floor(Date.now() / 1e3);
2026
+ return Array.from(this.byClientId.entries()).filter(([_, entry]) => entry.expiresAt > now).map(([clientId, entry]) => ({
2027
+ clientId,
2028
+ did: entry.did,
2029
+ expiresAt: entry.expiresAt
2030
+ }));
2031
+ }
2032
+ has(clientId) {
2033
+ return this.getOwner(clientId) !== void 0;
2034
+ }
2035
+ size() {
2036
+ return this.byClientId.size;
2037
+ }
2038
+ /**
2039
+ * Get the count of non-expired entries without mutating state.
2040
+ * This is useful for accurate counts but slower than size().
2041
+ */
2042
+ activeCount() {
2043
+ const now = Date.now();
2044
+ let count = 0;
2045
+ for (const entry of this.byClientId.values()) {
2046
+ if (entry.expiresAt > now) {
2047
+ count++;
2048
+ }
2049
+ }
2050
+ return count;
2051
+ }
2052
+ /**
2053
+ * Clear all bindings.
2054
+ */
2055
+ clear() {
2056
+ this.byClientId.clear();
2057
+ this.byDid.clear();
2058
+ }
2059
+ };
2060
+ function validateClientIdOwnership(clientId, authorDID, map) {
2061
+ const owner = map.getOwner(clientId);
2062
+ if (owner === void 0) {
2063
+ return true;
2064
+ }
2065
+ return owner === authorDID;
2066
+ }
2067
+
2068
+ // src/yjs-change.ts
2069
+ var YJS_CHANGE_TYPE = "yjs-update";
2070
+ function createUnsignedYjsChange(options) {
2071
+ const payload = {
2072
+ nodeId: options.nodeId,
2073
+ update: options.update,
2074
+ clientId: options.clientId,
2075
+ updateCount: options.updateCount
2076
+ };
2077
+ return createUnsignedChange({
2078
+ id: createChangeId(),
2079
+ type: YJS_CHANGE_TYPE,
2080
+ payload,
2081
+ parentHash: options.parentHash,
2082
+ authorDID: options.authorDID,
2083
+ lamport: options.lamport,
2084
+ wallTime: options.wallTime
2085
+ });
2086
+ }
2087
+ function createYjsChange(options) {
2088
+ const unsigned = createUnsignedYjsChange(options);
2089
+ return signChange(unsigned, options.privateKey);
2090
+ }
2091
+ function isYjsChange(change) {
2092
+ return change.type === YJS_CHANGE_TYPE && typeof change.payload === "object" && change.payload !== null && "update" in change.payload && "clientId" in change.payload && "nodeId" in change.payload;
2093
+ }
2094
+ function isNodeChange(change) {
2095
+ return change.type !== YJS_CHANGE_TYPE && typeof change.payload === "object" && change.payload !== null && "nodeId" in change.payload && !("update" in change.payload);
2096
+ }
2097
+ function getChangeNodeId(change) {
2098
+ if (typeof change.payload === "object" && change.payload !== null && "nodeId" in change.payload) {
2099
+ return change.payload.nodeId;
2100
+ }
2101
+ return void 0;
2102
+ }
2103
+
2104
+ // src/yjs-batcher.ts
2105
+ var DEFAULT_BATCHER_CONFIG = {
2106
+ batchWindowMs: 2e3,
2107
+ maxBatchSize: 50,
2108
+ flushOnParagraph: true
2109
+ };
2110
+ function throwMergeNotProvided() {
2111
+ throw new Error(
2112
+ "[YjsBatcher] mergeUpdates function is required. Pass Y.mergeUpdates from the yjs package."
2113
+ );
2114
+ }
2115
+ var YjsBatcher = class {
2116
+ pendingUpdates = [];
2117
+ flushTimer = null;
2118
+ config;
2119
+ onFlush;
2120
+ mergeUpdates;
2121
+ destroyed = false;
2122
+ /**
2123
+ * Create a new YjsBatcher.
2124
+ *
2125
+ * @param onFlush - Callback invoked when a batch is flushed
2126
+ * @param config - Optional configuration overrides
2127
+ * @param mergeUpdates - Function to merge updates (required - use Y.mergeUpdates from yjs)
2128
+ */
2129
+ constructor(onFlush, config, mergeUpdates) {
2130
+ this.onFlush = onFlush;
2131
+ this.config = {
2132
+ ...DEFAULT_BATCHER_CONFIG,
2133
+ ...config
2134
+ };
2135
+ this.mergeUpdates = mergeUpdates ?? throwMergeNotProvided;
2136
+ }
2137
+ /**
2138
+ * Add an update to the current batch.
2139
+ *
2140
+ * @param update - The Yjs update bytes
2141
+ * @param isParagraphBreak - Whether this update is a paragraph break (Enter key)
2142
+ */
2143
+ add(update, isParagraphBreak = false) {
2144
+ if (this.destroyed) {
2145
+ console.warn("[YjsBatcher] Attempted to add update after destroy");
2146
+ return;
2147
+ }
2148
+ this.pendingUpdates.push(update);
2149
+ if (this.pendingUpdates.length >= this.config.maxBatchSize) {
2150
+ this.flush();
2151
+ return;
2152
+ }
2153
+ if (isParagraphBreak && this.config.flushOnParagraph) {
2154
+ this.flush();
2155
+ return;
2156
+ }
2157
+ this.resetTimer();
2158
+ }
2159
+ /**
2160
+ * Force flush the current batch.
2161
+ * Safe to call even if there are no pending updates.
2162
+ */
2163
+ flush() {
2164
+ if (this.flushTimer) {
2165
+ clearTimeout(this.flushTimer);
2166
+ this.flushTimer = null;
2167
+ }
2168
+ if (this.pendingUpdates.length === 0) {
2169
+ return;
2170
+ }
2171
+ const merged = this.mergeUpdates(this.pendingUpdates);
2172
+ const updateCount = this.pendingUpdates.length;
2173
+ this.pendingUpdates = [];
2174
+ try {
2175
+ this.onFlush(merged, updateCount);
2176
+ } catch (err) {
2177
+ console.error("[YjsBatcher] Error in flush callback:", err);
2178
+ }
2179
+ }
2180
+ /**
2181
+ * Check if there are pending updates.
2182
+ */
2183
+ hasPending() {
2184
+ return this.pendingUpdates.length > 0;
2185
+ }
2186
+ /**
2187
+ * Get the number of pending updates.
2188
+ */
2189
+ pendingCount() {
2190
+ return this.pendingUpdates.length;
2191
+ }
2192
+ /**
2193
+ * Destroy the batcher. Flushes any remaining updates.
2194
+ * After destroy, add() calls will be ignored.
2195
+ */
2196
+ destroy() {
2197
+ if (this.destroyed) return;
2198
+ this.destroyed = true;
2199
+ this.flush();
2200
+ }
2201
+ /**
2202
+ * Check if the batcher has been destroyed.
2203
+ */
2204
+ isDestroyed() {
2205
+ return this.destroyed;
2206
+ }
2207
+ /**
2208
+ * Reset or start the flush timer.
2209
+ */
2210
+ resetTimer() {
2211
+ if (this.flushTimer) {
2212
+ clearTimeout(this.flushTimer);
2213
+ }
2214
+ this.flushTimer = setTimeout(() => {
2215
+ this.flushTimer = null;
2216
+ this.flush();
2217
+ }, this.config.batchWindowMs);
2218
+ }
2219
+ };
2220
+
2221
+ // src/serializers/v1.ts
2222
+ function encodeBase64(data) {
2223
+ if (typeof btoa === "function") {
2224
+ let binary = "";
2225
+ for (let i = 0; i < data.length; i++) {
2226
+ binary += String.fromCharCode(data[i]);
2227
+ }
2228
+ return btoa(binary);
2229
+ }
2230
+ return Buffer.from(data).toString("base64");
2231
+ }
2232
+ function decodeBase64(str) {
2233
+ if (typeof atob === "function") {
2234
+ const binary = atob(str);
2235
+ const bytes = new Uint8Array(binary.length);
2236
+ for (let i = 0; i < binary.length; i++) {
2237
+ bytes[i] = binary.charCodeAt(i);
2238
+ }
2239
+ return bytes;
2240
+ }
2241
+ return new Uint8Array(Buffer.from(str, "base64"));
2242
+ }
2243
+ var V1Serializer = class {
2244
+ version = 1;
2245
+ name = "V1 JSON Serializer";
2246
+ serialize(change) {
2247
+ const wire = {
2248
+ id: change.id,
2249
+ type: change.type,
2250
+ payload: change.payload,
2251
+ hash: change.hash,
2252
+ parentHash: change.parentHash,
2253
+ authorDID: change.authorDID,
2254
+ signature: encodeBase64(change.signature),
2255
+ wallTime: change.wallTime,
2256
+ lamport: { time: change.lamport.time, author: change.lamport.author }
2257
+ };
2258
+ if (change.protocolVersion !== void 0) {
2259
+ wire.protocolVersion = change.protocolVersion;
2260
+ }
2261
+ if (change.batchId !== void 0) {
2262
+ wire.batchId = change.batchId;
2263
+ wire.batchIndex = change.batchIndex;
2264
+ wire.batchSize = change.batchSize;
2265
+ }
2266
+ return wire;
2267
+ }
2268
+ deserialize(data) {
2269
+ try {
2270
+ let wire;
2271
+ if (data instanceof Uint8Array) {
2272
+ const json = new TextDecoder().decode(data);
2273
+ wire = JSON.parse(json);
2274
+ } else {
2275
+ wire = data;
2276
+ }
2277
+ if (!wire.id || !wire.type || !wire.hash || !wire.authorDID || !wire.signature) {
2278
+ return {
2279
+ success: false,
2280
+ error: "Missing required fields in V1 change",
2281
+ rawData: data
2282
+ };
2283
+ }
2284
+ if (!wire.lamport || typeof wire.lamport.time !== "number" || !wire.lamport.author) {
2285
+ return {
2286
+ success: false,
2287
+ error: "Invalid or missing lamport timestamp",
2288
+ rawData: data
2289
+ };
2290
+ }
2291
+ const change = {
2292
+ id: wire.id,
2293
+ type: wire.type,
2294
+ payload: wire.payload,
2295
+ hash: wire.hash,
2296
+ parentHash: wire.parentHash,
2297
+ authorDID: wire.authorDID,
2298
+ signature: decodeBase64(wire.signature),
2299
+ wallTime: wire.wallTime,
2300
+ lamport: {
2301
+ time: wire.lamport.time,
2302
+ author: wire.lamport.author
2303
+ }
2304
+ };
2305
+ if (wire.protocolVersion !== void 0) {
2306
+ change.protocolVersion = wire.protocolVersion;
2307
+ }
2308
+ if (wire.batchId !== void 0) {
2309
+ change.batchId = wire.batchId;
2310
+ change.batchIndex = wire.batchIndex;
2311
+ change.batchSize = wire.batchSize;
2312
+ }
2313
+ return { success: true, change };
2314
+ } catch (err) {
2315
+ return {
2316
+ success: false,
2317
+ error: err instanceof Error ? err.message : String(err),
2318
+ rawData: data
2319
+ };
2320
+ }
2321
+ }
2322
+ canDeserialize(data) {
2323
+ if (!data || typeof data !== "object") {
2324
+ return false;
2325
+ }
2326
+ const obj = data;
2327
+ if (obj.protocolVersion !== void 0 && obj.protocolVersion !== 1) {
2328
+ return false;
2329
+ }
2330
+ return typeof obj.id === "string" && typeof obj.type === "string" && typeof obj.hash === "string" && typeof obj.signature === "string";
2331
+ }
2332
+ };
2333
+ var v1Serializer = new V1Serializer();
2334
+
2335
+ // src/serializers/v2.ts
2336
+ function encodeBase642(data) {
2337
+ if (typeof btoa === "function") {
2338
+ let binary = "";
2339
+ for (let i = 0; i < data.length; i++) {
2340
+ binary += String.fromCharCode(data[i]);
2341
+ }
2342
+ return btoa(binary);
2343
+ }
2344
+ return Buffer.from(data).toString("base64");
2345
+ }
2346
+ function decodeBase642(str) {
2347
+ if (typeof atob === "function") {
2348
+ const binary = atob(str);
2349
+ const bytes = new Uint8Array(binary.length);
2350
+ for (let i = 0; i < binary.length; i++) {
2351
+ bytes[i] = binary.charCodeAt(i);
2352
+ }
2353
+ return bytes;
2354
+ }
2355
+ return new Uint8Array(Buffer.from(str, "base64"));
2356
+ }
2357
+ var V2Serializer = class {
2358
+ version = 2;
2359
+ name = "V2 Compact Serializer";
2360
+ serialize(change) {
2361
+ const wire = {
2362
+ v: 2,
2363
+ i: change.id,
2364
+ t: change.type,
2365
+ p: change.payload,
2366
+ h: change.hash,
2367
+ ph: change.parentHash,
2368
+ a: change.authorDID,
2369
+ s: encodeBase642(change.signature),
2370
+ w: change.wallTime,
2371
+ l: { t: change.lamport.time, a: change.lamport.author }
2372
+ };
2373
+ if (change.batchId !== void 0) {
2374
+ wire.bi = change.batchId;
2375
+ wire.bx = change.batchIndex;
2376
+ wire.bs = change.batchSize;
2377
+ }
2378
+ return wire;
2379
+ }
2380
+ deserialize(data) {
2381
+ try {
2382
+ let wire;
2383
+ if (data instanceof Uint8Array) {
2384
+ const json = new TextDecoder().decode(data);
2385
+ wire = JSON.parse(json);
2386
+ } else {
2387
+ wire = data;
2388
+ }
2389
+ if (wire.v !== 2) {
2390
+ return {
2391
+ success: false,
2392
+ error: `Expected v2 format, got v${wire.v}`,
2393
+ rawData: data
2394
+ };
2395
+ }
2396
+ if (!wire.i || !wire.t || !wire.h || !wire.a || !wire.s) {
2397
+ return {
2398
+ success: false,
2399
+ error: "Missing required fields in V2 change",
2400
+ rawData: data
2401
+ };
2402
+ }
2403
+ if (!wire.l || typeof wire.l.t !== "number" || !wire.l.a) {
2404
+ return {
2405
+ success: false,
2406
+ error: "Invalid or missing lamport timestamp",
2407
+ rawData: data
2408
+ };
2409
+ }
2410
+ const change = {
2411
+ protocolVersion: 2,
2412
+ id: wire.i,
2413
+ type: wire.t,
2414
+ payload: wire.p,
2415
+ hash: wire.h,
2416
+ parentHash: wire.ph,
2417
+ authorDID: wire.a,
2418
+ signature: decodeBase642(wire.s),
2419
+ wallTime: wire.w,
2420
+ lamport: {
2421
+ time: wire.l.t,
2422
+ author: wire.l.a
2423
+ }
2424
+ };
2425
+ if (wire.bi !== void 0) {
2426
+ change.batchId = wire.bi;
2427
+ change.batchIndex = wire.bx;
2428
+ change.batchSize = wire.bs;
2429
+ }
2430
+ return { success: true, change };
2431
+ } catch (err) {
2432
+ return {
2433
+ success: false,
2434
+ error: err instanceof Error ? err.message : String(err),
2435
+ rawData: data
2436
+ };
2437
+ }
2438
+ }
2439
+ canDeserialize(data) {
2440
+ if (!data || typeof data !== "object") {
2441
+ return false;
2442
+ }
2443
+ const obj = data;
2444
+ return obj.v === 2 && typeof obj.i === "string" && typeof obj.t === "string";
2445
+ }
2446
+ /**
2447
+ * Add schema version to a payload.
2448
+ * Used when serializing node changes with schema versioning.
2449
+ */
2450
+ static addSchemaVersion(payload, schemaVersion) {
2451
+ return { ...payload, _sv: schemaVersion };
2452
+ }
2453
+ /**
2454
+ * Extract schema version from a payload.
2455
+ */
2456
+ static getSchemaVersion(payload) {
2457
+ if (payload && typeof payload === "object") {
2458
+ const p = payload;
2459
+ return p._sv;
2460
+ }
2461
+ return void 0;
2462
+ }
2463
+ };
2464
+ var v2Serializer = new V2Serializer();
2465
+
2466
+ // src/serializers/v3.ts
2467
+ import {
2468
+ encodeSignature as encodeSignature3,
2469
+ decodeSignature as decodeSignature3
2470
+ } from "@xnetjs/crypto";
2471
+ function isUnifiedSignature(sig) {
2472
+ if (!sig || typeof sig !== "object") return false;
2473
+ const s = sig;
2474
+ if (typeof s.level !== "number") return false;
2475
+ if (s.level < 0 || s.level > 2) return false;
2476
+ return true;
2477
+ }
2478
+ function legacyToUnifiedSignature(sig) {
2479
+ return {
2480
+ level: 0,
2481
+ ed25519: sig
2482
+ };
2483
+ }
2484
+ var V3Serializer = class {
2485
+ version = 3;
2486
+ name = "V3 Multi-Level Crypto Serializer";
2487
+ serialize(change) {
2488
+ let sig;
2489
+ if (isUnifiedSignature(change.signature)) {
2490
+ sig = encodeSignature3(change.signature);
2491
+ } else if (change.signature instanceof Uint8Array) {
2492
+ sig = encodeSignature3(legacyToUnifiedSignature(change.signature));
2493
+ } else {
2494
+ throw new Error("Invalid signature type in change");
2495
+ }
2496
+ const wire = {
2497
+ v: 3,
2498
+ i: change.id,
2499
+ t: change.type,
2500
+ p: change.payload,
2501
+ h: change.hash,
2502
+ ph: change.parentHash,
2503
+ a: change.authorDID,
2504
+ sig,
2505
+ w: change.wallTime,
2506
+ l: { t: change.lamport.time, a: change.lamport.author }
2507
+ };
2508
+ if (change.batchId !== void 0) {
2509
+ wire.bi = change.batchId;
2510
+ wire.bx = change.batchIndex;
2511
+ wire.bs = change.batchSize;
2512
+ }
2513
+ return wire;
2514
+ }
2515
+ deserialize(data) {
2516
+ try {
2517
+ let wire;
2518
+ if (data instanceof Uint8Array) {
2519
+ const json = new TextDecoder().decode(data);
2520
+ wire = JSON.parse(json);
2521
+ } else {
2522
+ wire = data;
2523
+ }
2524
+ if (wire.v !== 3) {
2525
+ return {
2526
+ success: false,
2527
+ error: `Expected v3 format, got v${wire.v}. Clear your database and start fresh.`,
2528
+ rawData: data
2529
+ };
2530
+ }
2531
+ if (!wire.i || !wire.t || !wire.h || !wire.a || !wire.sig) {
2532
+ return {
2533
+ success: false,
2534
+ error: "Missing required fields in V3 change",
2535
+ rawData: data
2536
+ };
2537
+ }
2538
+ if (typeof wire.sig !== "object" || typeof wire.sig.l !== "number") {
2539
+ return {
2540
+ success: false,
2541
+ error: "Invalid signature format in V3 change",
2542
+ rawData: data
2543
+ };
2544
+ }
2545
+ if (!wire.l || typeof wire.l.t !== "number" || !wire.l.a) {
2546
+ return {
2547
+ success: false,
2548
+ error: "Invalid or missing lamport timestamp",
2549
+ rawData: data
2550
+ };
2551
+ }
2552
+ const signature = decodeSignature3(wire.sig);
2553
+ const change = {
2554
+ protocolVersion: 3,
2555
+ id: wire.i,
2556
+ type: wire.t,
2557
+ payload: wire.p,
2558
+ hash: wire.h,
2559
+ parentHash: wire.ph,
2560
+ authorDID: wire.a,
2561
+ signature,
2562
+ // Type cast for compatibility
2563
+ wallTime: wire.w,
2564
+ lamport: {
2565
+ time: wire.l.t,
2566
+ author: wire.l.a
2567
+ }
2568
+ };
2569
+ if (wire.bi !== void 0) {
2570
+ change.batchId = wire.bi;
2571
+ change.batchIndex = wire.bx;
2572
+ change.batchSize = wire.bs;
2573
+ }
2574
+ return { success: true, change };
2575
+ } catch (err) {
2576
+ return {
2577
+ success: false,
2578
+ error: err instanceof Error ? err.message : String(err),
2579
+ rawData: data
2580
+ };
2581
+ }
2582
+ }
2583
+ canDeserialize(data) {
2584
+ if (!data || typeof data !== "object") {
2585
+ return false;
2586
+ }
2587
+ const obj = data;
2588
+ return obj.v === 3 && typeof obj.i === "string" && typeof obj.t === "string" && typeof obj.sig === "object" && obj.sig !== null;
2589
+ }
2590
+ /**
2591
+ * Add schema version to a payload.
2592
+ */
2593
+ static addSchemaVersion(payload, schemaVersion) {
2594
+ return { ...payload, _sv: schemaVersion };
2595
+ }
2596
+ /**
2597
+ * Extract schema version from a payload.
2598
+ */
2599
+ static getSchemaVersion(payload) {
2600
+ if (payload && typeof payload === "object") {
2601
+ const p = payload;
2602
+ return p._sv;
2603
+ }
2604
+ return void 0;
2605
+ }
2606
+ /**
2607
+ * Get the security level of a serialized change.
2608
+ */
2609
+ static getSecurityLevel(wire) {
2610
+ return wire.sig.l;
2611
+ }
2612
+ };
2613
+ var v3Serializer = new V3Serializer();
2614
+
2615
+ // src/serializers/index.ts
2616
+ var DefaultSerializerRegistry = class {
2617
+ serializers = /* @__PURE__ */ new Map();
2618
+ defaultVersion;
2619
+ constructor(defaultVersion = CURRENT_PROTOCOL_VERSION) {
2620
+ this.defaultVersion = defaultVersion;
2621
+ this.register(v1Serializer);
2622
+ this.register(v2Serializer);
2623
+ this.register(v3Serializer);
2624
+ }
2625
+ get(version) {
2626
+ return this.serializers.get(version);
2627
+ }
2628
+ getDefault() {
2629
+ const serializer = this.serializers.get(this.defaultVersion);
2630
+ if (!serializer) {
2631
+ throw new Error(`No serializer registered for default version ${this.defaultVersion}`);
2632
+ }
2633
+ return serializer;
2634
+ }
2635
+ register(serializer) {
2636
+ this.serializers.set(serializer.version, serializer);
2637
+ }
2638
+ getVersions() {
2639
+ return [...this.serializers.keys()].sort((a, b) => a - b);
2640
+ }
2641
+ detect(data) {
2642
+ const versions = this.getVersions().reverse();
2643
+ for (const version of versions) {
2644
+ const serializer = this.serializers.get(version);
2645
+ if (serializer.canDeserialize(data)) {
2646
+ return serializer;
2647
+ }
2648
+ }
2649
+ return void 0;
2650
+ }
2651
+ };
2652
+ var serializerRegistry = new DefaultSerializerRegistry();
2653
+ function getSerializer(version) {
2654
+ return serializerRegistry.get(version);
2655
+ }
2656
+ function getDefaultSerializer() {
2657
+ return serializerRegistry.getDefault();
2658
+ }
2659
+ function autoDeserialize(data) {
2660
+ const serializer = serializerRegistry.detect(data);
2661
+ if (!serializer) {
2662
+ return {
2663
+ success: false,
2664
+ error: "Unable to detect serializer format",
2665
+ rawData: data
2666
+ };
2667
+ }
2668
+ return serializer.deserialize(data);
2669
+ }
2670
+ function autoSerialize(change) {
2671
+ const version = change.protocolVersion ?? 1;
2672
+ const serializer = serializerRegistry.get(version) ?? serializerRegistry.getDefault();
2673
+ return serializer.serialize(change);
2674
+ }
2675
+ function createSerializerRegistry(defaultVersion = CURRENT_PROTOCOL_VERSION, serializers) {
2676
+ const registry = new DefaultSerializerRegistry(defaultVersion);
2677
+ if (serializers) {
2678
+ for (const serializer of serializers) {
2679
+ registry.register(serializer);
2680
+ }
2681
+ }
2682
+ return registry;
2683
+ }
2684
+
2685
+ // src/handlers/index.ts
2686
+ var ChangeHandlerRegistry = class {
2687
+ handlers = /* @__PURE__ */ new Map();
2688
+ unknownTypeListeners = [];
2689
+ invalidChangeListeners = [];
2690
+ /**
2691
+ * Register a handler for a change type and version range.
2692
+ */
2693
+ register(handler) {
2694
+ const existing = this.handlers.get(handler.type) ?? [];
2695
+ existing.push(handler);
2696
+ existing.sort((a, b) => b.maxVersion - a.maxVersion);
2697
+ this.handlers.set(handler.type, existing);
2698
+ }
2699
+ /**
2700
+ * Unregister all handlers for a type.
2701
+ */
2702
+ unregister(type) {
2703
+ return this.handlers.delete(type);
2704
+ }
2705
+ /**
2706
+ * Get the appropriate handler for a change.
2707
+ * Returns null if no handler can process this change.
2708
+ */
2709
+ getHandler(change) {
2710
+ const handlers = this.handlers.get(change.type);
2711
+ if (!handlers || handlers.length === 0) {
2712
+ return null;
2713
+ }
2714
+ const version = change.protocolVersion ?? 0;
2715
+ for (const handler of handlers) {
2716
+ if (version >= handler.minVersion && version <= handler.maxVersion) {
2717
+ if (handler.canHandle(change)) {
2718
+ return handler;
2719
+ }
2720
+ }
2721
+ }
2722
+ for (const handler of handlers) {
2723
+ if (handler.maxVersion === Infinity && handler.minVersion <= version) {
2724
+ if (handler.canHandle(change)) {
2725
+ return handler;
2726
+ }
2727
+ }
2728
+ }
2729
+ return null;
2730
+ }
2731
+ /**
2732
+ * Check if any handler can process this change.
2733
+ */
2734
+ canProcess(change) {
2735
+ return this.getHandler(change) !== null;
2736
+ }
2737
+ /**
2738
+ * Process a change using the appropriate handler.
2739
+ */
2740
+ async process(change, context) {
2741
+ const handler = this.getHandler(change);
2742
+ if (!handler) {
2743
+ await context.storeUnknown(change);
2744
+ context.emit("unknownChangeType", { change });
2745
+ this.notifyUnknownType(change);
2746
+ return;
2747
+ }
2748
+ const validation = handler.validate(change);
2749
+ if (!validation.valid) {
2750
+ context.emit("invalidChange", { change, errors: validation.errors });
2751
+ this.notifyInvalidChange(change, validation.errors);
2752
+ return;
2753
+ }
2754
+ await handler.process(change, context);
2755
+ }
2756
+ /**
2757
+ * Get all registered handler types.
2758
+ */
2759
+ getTypes() {
2760
+ return Array.from(this.handlers.keys());
2761
+ }
2762
+ /**
2763
+ * Get handlers for a specific type.
2764
+ */
2765
+ getHandlersForType(type) {
2766
+ return this.handlers.get(type) ?? [];
2767
+ }
2768
+ /**
2769
+ * Subscribe to unknown change type events.
2770
+ */
2771
+ onUnknownType(listener) {
2772
+ this.unknownTypeListeners.push(listener);
2773
+ return () => {
2774
+ const idx = this.unknownTypeListeners.indexOf(listener);
2775
+ if (idx !== -1) this.unknownTypeListeners.splice(idx, 1);
2776
+ };
2777
+ }
2778
+ /**
2779
+ * Subscribe to invalid change events.
2780
+ */
2781
+ onInvalidChange(listener) {
2782
+ this.invalidChangeListeners.push(listener);
2783
+ return () => {
2784
+ const idx = this.invalidChangeListeners.indexOf(listener);
2785
+ if (idx !== -1) this.invalidChangeListeners.splice(idx, 1);
2786
+ };
2787
+ }
2788
+ /**
2789
+ * Clear all registered handlers.
2790
+ */
2791
+ clear() {
2792
+ this.handlers.clear();
2793
+ }
2794
+ // ─── Private Methods ─────────────────────────────────────────────────────
2795
+ notifyUnknownType(change) {
2796
+ for (const listener of this.unknownTypeListeners) {
2797
+ try {
2798
+ listener(change);
2799
+ } catch (err) {
2800
+ console.error("[ChangeHandlerRegistry] Error in unknownType listener:", err);
2801
+ }
2802
+ }
2803
+ }
2804
+ notifyInvalidChange(change, errors) {
2805
+ for (const listener of this.invalidChangeListeners) {
2806
+ try {
2807
+ listener(change, errors);
2808
+ } catch (err) {
2809
+ console.error("[ChangeHandlerRegistry] Error in invalidChange listener:", err);
2810
+ }
2811
+ }
2812
+ }
2813
+ };
2814
+ var changeHandlerRegistry = new ChangeHandlerRegistry();
2815
+ function createHandler(type, process, validate) {
2816
+ return {
2817
+ type,
2818
+ minVersion: 0,
2819
+ maxVersion: Infinity,
2820
+ canHandle: () => true,
2821
+ process,
2822
+ validate: validate ?? (() => ({ valid: true, errors: [] }))
2823
+ };
2824
+ }
2825
+ function createVersionedHandler(type, minVersion, maxVersion, process, validate) {
2826
+ return {
2827
+ type,
2828
+ minVersion,
2829
+ maxVersion,
2830
+ canHandle: () => true,
2831
+ process,
2832
+ validate: validate ?? (() => ({ valid: true, errors: [] }))
2833
+ };
2834
+ }
2835
+ function createTestContext(overrides) {
2836
+ return {
2837
+ storeUnknown: async () => {
2838
+ },
2839
+ emit: () => {
2840
+ },
2841
+ authorDID: "did:key:test",
2842
+ ...overrides
2843
+ };
2844
+ }
2845
+
2846
+ // src/integrity.ts
2847
+ async function verifyIntegrity(changes, options = {}) {
2848
+ const startTime = Date.now();
2849
+ const issues = [];
2850
+ let valid = 0;
2851
+ const changeById = /* @__PURE__ */ new Map();
2852
+ const changeByHash = /* @__PURE__ */ new Map();
2853
+ const duplicateIds = /* @__PURE__ */ new Set();
2854
+ for (const change of changes) {
2855
+ if (changeById.has(change.id)) {
2856
+ duplicateIds.add(change.id);
2857
+ } else {
2858
+ changeById.set(change.id, change);
2859
+ }
2860
+ changeByHash.set(change.hash, change);
2861
+ }
2862
+ for (const id of duplicateIds) {
2863
+ issues.push({
2864
+ changeId: id,
2865
+ type: "duplicate-id",
2866
+ details: `Duplicate change ID found: ${id}`,
2867
+ severity: "error",
2868
+ repairAction: {
2869
+ type: "remove-duplicate",
2870
+ description: "Remove duplicate changes, keeping the most recent",
2871
+ automatic: false
2872
+ }
2873
+ });
2874
+ }
2875
+ const now = Date.now();
2876
+ const maxFuture = options.maxFutureTimestamp ?? 6e4;
2877
+ for (let i = 0; i < changes.length; i++) {
2878
+ const change = changes[i];
2879
+ if (options.onProgress) {
2880
+ options.onProgress(i + 1, changes.length);
2881
+ }
2882
+ let hasIssue = false;
2883
+ if (duplicateIds.has(change.id)) {
2884
+ continue;
2885
+ }
2886
+ if (!options.skipHashes) {
2887
+ const hashValid = verifyChangeHash(change);
2888
+ if (!hashValid) {
2889
+ issues.push({
2890
+ changeId: change.id,
2891
+ type: "hash-mismatch",
2892
+ details: `Hash mismatch: stored hash does not match computed hash`,
2893
+ severity: "error",
2894
+ repairAction: {
2895
+ type: "recompute-hash",
2896
+ description: "Recompute and update the hash",
2897
+ automatic: true
2898
+ }
2899
+ });
2900
+ hasIssue = true;
2901
+ }
2902
+ }
2903
+ if (!options.skipSignatures && !hasIssue) {
2904
+ if (!change.signature || change.signature.length === 0) {
2905
+ issues.push({
2906
+ changeId: change.id,
2907
+ type: "signature-invalid",
2908
+ details: "Signature is missing or empty",
2909
+ severity: "error"
2910
+ // No automatic repair - would need original signing key
2911
+ });
2912
+ hasIssue = true;
2913
+ }
2914
+ }
2915
+ if (!options.skipChain && change.parentHash !== null) {
2916
+ if (!changeByHash.has(change.parentHash)) {
2917
+ issues.push({
2918
+ changeId: change.id,
2919
+ type: "missing-parent",
2920
+ details: `Parent hash ${change.parentHash} not found`,
2921
+ severity: "warning",
2922
+ repairAction: {
2923
+ type: "request-from-peers",
2924
+ description: "Request the missing parent from connected peers",
2925
+ automatic: true
2926
+ }
2927
+ });
2928
+ hasIssue = true;
2929
+ }
2930
+ }
2931
+ if (change.lamport.time < 0) {
2932
+ issues.push({
2933
+ changeId: change.id,
2934
+ type: "invalid-lamport",
2935
+ details: `Invalid Lamport time: ${change.lamport.time}`,
2936
+ severity: "error"
2937
+ });
2938
+ hasIssue = true;
2939
+ }
2940
+ if (change.wallTime > now + maxFuture) {
2941
+ issues.push({
2942
+ changeId: change.id,
2943
+ type: "future-timestamp",
2944
+ details: `Wall time is in the future: ${new Date(change.wallTime).toISOString()}`,
2945
+ severity: "warning"
2946
+ });
2947
+ hasIssue = true;
2948
+ }
2949
+ if (!hasIssue) {
2950
+ valid++;
2951
+ }
2952
+ }
2953
+ const byType = {
2954
+ "hash-mismatch": 0,
2955
+ "signature-invalid": 0,
2956
+ "chain-broken": 0,
2957
+ "missing-parent": 0,
2958
+ "duplicate-id": 0,
2959
+ "invalid-lamport": 0,
2960
+ "future-timestamp": 0
2961
+ };
2962
+ let errors = 0;
2963
+ let warnings = 0;
2964
+ for (const issue of issues) {
2965
+ byType[issue.type]++;
2966
+ if (issue.severity === "error") {
2967
+ errors++;
2968
+ } else {
2969
+ warnings++;
2970
+ }
2971
+ }
2972
+ const repairable = issues.every(
2973
+ (issue) => issue.repairAction !== void 0 && issue.repairAction.type !== "none"
2974
+ );
2975
+ return {
2976
+ checked: changes.length,
2977
+ valid,
2978
+ issues,
2979
+ repairable,
2980
+ summary: {
2981
+ errors,
2982
+ warnings,
2983
+ byType
2984
+ },
2985
+ durationMs: Date.now() - startTime
2986
+ };
2987
+ }
2988
+ async function quickIntegrityCheck(changes) {
2989
+ return verifyIntegrity(changes, {
2990
+ skipSignatures: true,
2991
+ skipChain: false
2992
+ });
2993
+ }
2994
+ async function verifySingleChange(change, options = {}) {
2995
+ const report = await verifyIntegrity([change], options);
2996
+ return {
2997
+ valid: report.valid === 1,
2998
+ issues: report.issues
2999
+ };
3000
+ }
3001
+ function findOrphans(changes) {
3002
+ const hashSet = new Set(changes.map((c) => c.hash));
3003
+ return changes.filter((c) => c.parentHash !== null && !hashSet.has(c.parentHash));
3004
+ }
3005
+ function findRoots(changes) {
3006
+ return changes.filter((c) => c.parentHash === null);
3007
+ }
3008
+ function findHeads(changes) {
3009
+ const parentHashes = new Set(
3010
+ changes.filter((c) => c.parentHash !== null).map((c) => c.parentHash)
3011
+ );
3012
+ return changes.filter((c) => !parentHashes.has(c.hash));
3013
+ }
3014
+ function getChainDepth(changes) {
3015
+ if (changes.length === 0) return 0;
3016
+ const childMap = /* @__PURE__ */ new Map();
3017
+ for (const change of changes) {
3018
+ const children = childMap.get(change.parentHash) ?? [];
3019
+ children.push(change);
3020
+ childMap.set(change.parentHash, children);
3021
+ }
3022
+ function getDepth(parentHash) {
3023
+ const children = childMap.get(parentHash) ?? [];
3024
+ if (children.length === 0) return 0;
3025
+ return 1 + Math.max(...children.map((c) => getDepth(c.hash)));
3026
+ }
3027
+ return getDepth(null);
3028
+ }
3029
+ async function attemptRepair(changes, issues) {
3030
+ const changeMap = new Map(changes.map((c) => [c.id, { ...c }]));
3031
+ const remainingIssues = [];
3032
+ let repairCount = 0;
3033
+ for (const issue of issues) {
3034
+ if (!issue.repairAction?.automatic) {
3035
+ remainingIssues.push(issue);
3036
+ continue;
3037
+ }
3038
+ const change = changeMap.get(issue.changeId);
3039
+ if (!change) {
3040
+ remainingIssues.push(issue);
3041
+ continue;
3042
+ }
3043
+ switch (issue.repairAction.type) {
3044
+ case "recompute-hash":
3045
+ change.hash = await computeChangeHash(change);
3046
+ repairCount++;
3047
+ break;
3048
+ case "mark-orphan":
3049
+ repairCount++;
3050
+ break;
3051
+ default:
3052
+ remainingIssues.push(issue);
3053
+ }
3054
+ }
3055
+ return {
3056
+ repaired: Array.from(changeMap.values()),
3057
+ remainingIssues,
3058
+ repairCount
3059
+ };
3060
+ }
3061
+ function formatIntegrityReport(report) {
3062
+ const lines = [];
3063
+ lines.push(`Integrity Report`);
3064
+ lines.push(`================`);
3065
+ lines.push(`Checked: ${report.checked} changes`);
3066
+ lines.push(`Valid: ${report.valid} (${Math.round(report.valid / report.checked * 100)}%)`);
3067
+ lines.push(`Duration: ${report.durationMs}ms`);
3068
+ lines.push("");
3069
+ if (report.issues.length === 0) {
3070
+ lines.push("No issues found.");
3071
+ } else {
3072
+ lines.push(
3073
+ `Issues: ${report.issues.length} (${report.summary.errors} errors, ${report.summary.warnings} warnings)`
3074
+ );
3075
+ lines.push("");
3076
+ for (const [type, count] of Object.entries(report.summary.byType)) {
3077
+ if (count > 0) {
3078
+ lines.push(` ${type}: ${count}`);
3079
+ }
3080
+ }
3081
+ lines.push("");
3082
+ lines.push(`Repairable: ${report.repairable ? "Yes" : "No"}`);
3083
+ }
3084
+ return lines.join("\n");
3085
+ }
3086
+
3087
+ // src/deprecation.ts
3088
+ var DEPRECATIONS = [
3089
+ // Protocol deprecations
3090
+ {
3091
+ type: "protocol",
3092
+ subject: "Protocol v0 (unsigned changes)",
3093
+ description: "Protocol v0 changes without signatures are deprecated",
3094
+ deprecatedIn: "0.5.0",
3095
+ removedIn: "1.0.0",
3096
+ alternative: "Protocol v1 with signed changes",
3097
+ migrationGuide: "/docs/migrations/protocol-v0-to-v1",
3098
+ deprecatedDate: "2026-01-15"
3099
+ },
3100
+ {
3101
+ type: "protocol",
3102
+ subject: "Legacy Yjs updates",
3103
+ description: "Unsigned Yjs updates are deprecated",
3104
+ deprecatedIn: "0.5.0",
3105
+ removedIn: "1.0.0",
3106
+ alternative: "Signed Yjs envelopes",
3107
+ migrationGuide: "/docs/migrations/yjs-signed-envelopes"
3108
+ }
3109
+ // Schema deprecations (examples for future use)
3110
+ // {
3111
+ // type: 'schema',
3112
+ // subject: 'xnet://xnet.fyi/Task@1.0.0',
3113
+ // description: 'Task v1.0.0 schema is deprecated',
3114
+ // deprecatedIn: '0.6.0',
3115
+ // alternative: 'xnet://xnet.fyi/Task@2.0.0',
3116
+ // migrationGuide: '/docs/migrations/task-v1-to-v2'
3117
+ // },
3118
+ // Feature deprecations (examples for future use)
3119
+ // {
3120
+ // type: 'feature',
3121
+ // subject: 'legacy-auth',
3122
+ // description: 'Legacy authentication method is deprecated',
3123
+ // deprecatedIn: '0.7.0',
3124
+ // removedIn: '1.0.0',
3125
+ // alternative: 'did-auth',
3126
+ // migrationGuide: '/docs/migrations/legacy-auth-to-did'
3127
+ // }
3128
+ ];
3129
+ function checkDeprecations(context) {
3130
+ const warnings = [];
3131
+ const now = /* @__PURE__ */ new Date();
3132
+ for (const notice of DEPRECATIONS) {
3133
+ let triggered = false;
3134
+ if (notice.type === "protocol") {
3135
+ if (notice.subject.includes("v0") && (context.protocolVersion ?? 0) < 1) {
3136
+ triggered = true;
3137
+ }
3138
+ if (notice.subject.includes("Legacy Yjs") && (context.protocolVersion ?? 0) < 1) {
3139
+ triggered = true;
3140
+ }
3141
+ }
3142
+ if (notice.type === "schema" && context.schemas) {
3143
+ if (context.schemas.includes(notice.subject)) {
3144
+ triggered = true;
3145
+ }
3146
+ }
3147
+ if (notice.type === "feature" && context.features) {
3148
+ if (context.features.includes(notice.subject)) {
3149
+ triggered = true;
3150
+ }
3151
+ }
3152
+ if (triggered) {
3153
+ const warning = createWarning(notice, now);
3154
+ warnings.push(warning);
3155
+ }
3156
+ }
3157
+ return warnings;
3158
+ }
3159
+ function createWarning(notice, now) {
3160
+ let daysUntilRemoval;
3161
+ if (notice.sunsetDate) {
3162
+ const sunset = new Date(notice.sunsetDate);
3163
+ const diff = sunset.getTime() - now.getTime();
3164
+ daysUntilRemoval = Math.ceil(diff / (1e3 * 60 * 60 * 24));
3165
+ }
3166
+ const isPastRemoval = notice.removedIn !== void 0 && daysUntilRemoval !== void 0 && daysUntilRemoval < 0;
3167
+ const message = isPastRemoval ? `REMOVED: ${notice.subject} was removed in v${notice.removedIn}.` : `DEPRECATED: ${notice.subject} is deprecated since v${notice.deprecatedIn}.`;
3168
+ let action = "";
3169
+ if (notice.alternative) {
3170
+ action = `Migrate to ${notice.alternative}.`;
3171
+ }
3172
+ if (notice.removedIn && !isPastRemoval) {
3173
+ action += ` Will be removed in v${notice.removedIn}.`;
3174
+ }
3175
+ if (notice.migrationGuide) {
3176
+ action += ` See ${notice.migrationGuide}`;
3177
+ }
3178
+ return {
3179
+ notice,
3180
+ message,
3181
+ action: action.trim(),
3182
+ severity: isPastRemoval ? "error" : "warning",
3183
+ daysUntilRemoval: daysUntilRemoval !== void 0 && daysUntilRemoval >= 0 ? daysUntilRemoval : void 0
3184
+ };
3185
+ }
3186
+ var DEPRECATION_POLICY = {
3187
+ /** Minimum time between deprecation and removal */
3188
+ minimumDeprecationPeriodDays: 180,
3189
+ /** Whether to log warnings to console */
3190
+ logWarnings: true,
3191
+ /** Whether to throw errors for removed functionality */
3192
+ strictMode: false,
3193
+ /** Console logger for warnings */
3194
+ logger: console.warn
3195
+ };
3196
+ function configureDeprecationPolicy(options) {
3197
+ Object.assign(DEPRECATION_POLICY, options);
3198
+ }
3199
+ var loggedDeprecations = /* @__PURE__ */ new Set();
3200
+ function logDeprecation(warning) {
3201
+ if (!DEPRECATION_POLICY.logWarnings) return;
3202
+ const key = `${warning.notice.type}:${warning.notice.subject}`;
3203
+ if (loggedDeprecations.has(key)) return;
3204
+ loggedDeprecations.add(key);
3205
+ const prefix = warning.severity === "error" ? "[REMOVED]" : "[DEPRECATED]";
3206
+ DEPRECATION_POLICY.logger(`${prefix} ${warning.message} ${warning.action}`);
3207
+ }
3208
+ function checkAndLogDeprecations(context) {
3209
+ const warnings = checkDeprecations(context);
3210
+ for (const warning of warnings) {
3211
+ logDeprecation(warning);
3212
+ if (DEPRECATION_POLICY.strictMode && warning.severity === "error") {
3213
+ throw new DeprecationError(warning);
3214
+ }
3215
+ }
3216
+ return warnings;
3217
+ }
3218
+ function clearLoggedDeprecations() {
3219
+ loggedDeprecations.clear();
3220
+ }
3221
+ function getDeprecationsByType(type) {
3222
+ return DEPRECATIONS.filter((d) => d.type === type);
3223
+ }
3224
+ function getDeprecation(subject) {
3225
+ return DEPRECATIONS.find((d) => d.subject === subject);
3226
+ }
3227
+ function isDeprecated(subject) {
3228
+ return DEPRECATIONS.some((d) => d.subject === subject);
3229
+ }
3230
+ function isRemoved(subject) {
3231
+ const notice = getDeprecation(subject);
3232
+ if (!notice?.removedIn) return false;
3233
+ return true;
3234
+ }
3235
+ function registerDeprecation(notice) {
3236
+ const existing = DEPRECATIONS.findIndex((d) => d.subject === notice.subject);
3237
+ if (existing >= 0) {
3238
+ DEPRECATIONS[existing] = notice;
3239
+ } else {
3240
+ DEPRECATIONS.push(notice);
3241
+ }
3242
+ }
3243
+ var DeprecationError = class extends Error {
3244
+ constructor(warning) {
3245
+ super(`${warning.message} ${warning.action}`);
3246
+ this.warning = warning;
3247
+ this.name = "DeprecationError";
3248
+ }
3249
+ };
3250
+ function formatDeprecationReport(warnings) {
3251
+ if (warnings.length === 0) {
3252
+ return "No deprecation warnings.";
3253
+ }
3254
+ const lines = ["Deprecation Report", "==================", ""];
3255
+ const errors = warnings.filter((w) => w.severity === "error");
3256
+ const warns = warnings.filter((w) => w.severity === "warning");
3257
+ if (errors.length > 0) {
3258
+ lines.push("REMOVED (require immediate action):");
3259
+ for (const w of errors) {
3260
+ lines.push(` - ${w.notice.subject}`);
3261
+ lines.push(` ${w.action}`);
3262
+ }
3263
+ lines.push("");
3264
+ }
3265
+ if (warns.length > 0) {
3266
+ lines.push("DEPRECATED (plan to migrate):");
3267
+ for (const w of warns) {
3268
+ lines.push(` - ${w.notice.subject}`);
3269
+ lines.push(` ${w.action}`);
3270
+ if (w.daysUntilRemoval !== void 0) {
3271
+ lines.push(` ${w.daysUntilRemoval} days until removal`);
3272
+ }
3273
+ }
3274
+ lines.push("");
3275
+ }
3276
+ return lines.join("\n");
3277
+ }
3278
+
3279
+ // src/integrity-monitor.ts
3280
+ var DEFAULT_INTERVAL_MS = 5 * 60 * 1e3;
3281
+ var DEFAULT_MIN_CHANGES = 10;
3282
+ function createIntegrityMonitor(config) {
3283
+ let currentConfig = { ...config };
3284
+ let intervalId = null;
3285
+ let isRunning = false;
3286
+ let isChecking = false;
3287
+ const stats = {
3288
+ checksPerformed: 0,
3289
+ totalIssuesFound: 0,
3290
+ lastCheckAt: null,
3291
+ lastCheckDurationMs: 0,
3292
+ lastReport: null,
3293
+ isRunning: false,
3294
+ isChecking: false
3295
+ };
3296
+ const log = (message) => {
3297
+ if (currentConfig.debug) {
3298
+ console.log(`[IntegrityMonitor] ${message}`);
3299
+ }
3300
+ };
3301
+ const runCheck = async () => {
3302
+ if (isChecking) {
3303
+ log("Check already in progress, skipping");
3304
+ return stats.lastReport ?? createEmptyReport();
3305
+ }
3306
+ isChecking = true;
3307
+ stats.isChecking = true;
3308
+ try {
3309
+ log("Starting integrity check");
3310
+ const startTime = Date.now();
3311
+ const changes = await Promise.resolve(currentConfig.getChanges());
3312
+ const minChanges = currentConfig.minChangesForCheck ?? DEFAULT_MIN_CHANGES;
3313
+ if (changes.length < minChanges) {
3314
+ log(`Skipping check: only ${changes.length} changes (min: ${minChanges})`);
3315
+ return createEmptyReport();
3316
+ }
3317
+ const report = currentConfig.quickCheck ? await quickIntegrityCheck(changes) : await verifyIntegrity(changes, currentConfig.verifyOptions);
3318
+ stats.checksPerformed++;
3319
+ stats.lastCheckAt = /* @__PURE__ */ new Date();
3320
+ stats.lastCheckDurationMs = Date.now() - startTime;
3321
+ stats.lastReport = report;
3322
+ stats.totalIssuesFound += report.issues.length;
3323
+ log(`Check complete: ${report.valid}/${report.checked} valid, ${report.issues.length} issues`);
3324
+ currentConfig.onCheck?.(report);
3325
+ if (report.issues.length > 0) {
3326
+ currentConfig.onIssues?.(report);
3327
+ }
3328
+ return report;
3329
+ } catch (error) {
3330
+ const err = error instanceof Error ? error : new Error(String(error));
3331
+ log(`Check failed: ${err.message}`);
3332
+ currentConfig.onError?.(err);
3333
+ throw err;
3334
+ } finally {
3335
+ isChecking = false;
3336
+ stats.isChecking = false;
3337
+ }
3338
+ };
3339
+ const start = () => {
3340
+ if (isRunning) {
3341
+ log("Already running");
3342
+ return;
3343
+ }
3344
+ const intervalMs = currentConfig.intervalMs ?? DEFAULT_INTERVAL_MS;
3345
+ log(`Starting with interval ${intervalMs}ms`);
3346
+ isRunning = true;
3347
+ stats.isRunning = true;
3348
+ if (currentConfig.checkOnStart) {
3349
+ runCheck().catch(() => {
3350
+ });
3351
+ }
3352
+ intervalId = setInterval(() => {
3353
+ runCheck().catch(() => {
3354
+ });
3355
+ }, intervalMs);
3356
+ };
3357
+ const stop = () => {
3358
+ if (!isRunning) {
3359
+ log("Not running");
3360
+ return;
3361
+ }
3362
+ log("Stopping");
3363
+ if (intervalId !== null) {
3364
+ clearInterval(intervalId);
3365
+ intervalId = null;
3366
+ }
3367
+ isRunning = false;
3368
+ stats.isRunning = false;
3369
+ };
3370
+ const checkNow = async () => {
3371
+ return runCheck();
3372
+ };
3373
+ const getStats = () => {
3374
+ return { ...stats };
3375
+ };
3376
+ const configure = (newConfig) => {
3377
+ const wasRunning = isRunning;
3378
+ if (wasRunning) {
3379
+ stop();
3380
+ }
3381
+ currentConfig = { ...currentConfig, ...newConfig };
3382
+ if (wasRunning) {
3383
+ start();
3384
+ }
3385
+ };
3386
+ return {
3387
+ start,
3388
+ stop,
3389
+ checkNow,
3390
+ getStats,
3391
+ isRunning: () => isRunning,
3392
+ configure
3393
+ };
3394
+ }
3395
+ function createEmptyReport() {
3396
+ return {
3397
+ checked: 0,
3398
+ valid: 0,
3399
+ issues: [],
3400
+ repairable: true,
3401
+ summary: {
3402
+ errors: 0,
3403
+ warnings: 0,
3404
+ byType: {
3405
+ "hash-mismatch": 0,
3406
+ "signature-invalid": 0,
3407
+ "chain-broken": 0,
3408
+ "missing-parent": 0,
3409
+ "duplicate-id": 0,
3410
+ "invalid-lamport": 0,
3411
+ "future-timestamp": 0
3412
+ }
3413
+ },
3414
+ durationMs: 0
3415
+ };
3416
+ }
3417
+ function createReactIntegrityMonitor(options) {
3418
+ const { onStateChange, ...config } = options;
3419
+ const monitor = createIntegrityMonitor({
3420
+ ...config,
3421
+ onCheck: (report) => {
3422
+ config.onCheck?.(report);
3423
+ onStateChange?.(monitor.getStats());
3424
+ },
3425
+ onError: (error) => {
3426
+ config.onError?.(error);
3427
+ onStateChange?.(monitor.getStats());
3428
+ }
3429
+ });
3430
+ const originalStart = monitor.start.bind(monitor);
3431
+ const originalStop = monitor.stop.bind(monitor);
3432
+ monitor.start = () => {
3433
+ originalStart();
3434
+ onStateChange?.(monitor.getStats());
3435
+ };
3436
+ monitor.stop = () => {
3437
+ originalStop();
3438
+ onStateChange?.(monitor.getStats());
3439
+ };
3440
+ return monitor;
3441
+ }
3442
+
3443
+ // src/security-policy.ts
3444
+ import { DEFAULT_SECURITY_LEVEL as DEFAULT_SECURITY_LEVEL3 } from "@xnetjs/crypto";
3445
+ var DEFAULT_SECURITY_POLICY = {
3446
+ default: DEFAULT_SECURITY_LEVEL3,
3447
+ overrides: {
3448
+ // High-frequency, ephemeral operations - fast Ed25519
3449
+ "cursor-update": 0,
3450
+ "presence-update": 0,
3451
+ "typing-indicator": 0,
3452
+ "viewport-update": 0,
3453
+ "awareness-update": 0
3454
+ // Regular operations - use default (currently Level 0)
3455
+ // When we upgrade to Level 1 default, these will automatically use hybrid
3456
+ // 'node-create': 1,
3457
+ // 'node-update': 1,
3458
+ // 'node-delete': 1,
3459
+ // Critical operations - can be upgraded to Level 1/2 when PQ is enabled
3460
+ // 'key-rotation': 2,
3461
+ // 'permission-grant': 1,
3462
+ // 'permission-revoke': 2,
3463
+ // 'identity-recovery': 2,
3464
+ }
3465
+ };
3466
+ var HYBRID_SECURITY_POLICY = {
3467
+ default: 1,
3468
+ overrides: {
3469
+ // High-frequency, ephemeral operations - fast Ed25519
3470
+ "cursor-update": 0,
3471
+ "presence-update": 0,
3472
+ "typing-indicator": 0,
3473
+ "viewport-update": 0,
3474
+ "awareness-update": 0,
3475
+ // Regular operations - hybrid signatures
3476
+ "node-create": 1,
3477
+ "node-update": 1,
3478
+ "node-delete": 1,
3479
+ "yjs-update": 1,
3480
+ "comment-add": 1,
3481
+ // Critical operations - maximum security
3482
+ "key-rotation": 2,
3483
+ "permission-grant": 1,
3484
+ "permission-revoke": 2,
3485
+ "identity-recovery": 2,
3486
+ "share-create": 1
3487
+ }
3488
+ };
3489
+ var MAX_SECURITY_POLICY = {
3490
+ default: 2,
3491
+ overrides: {
3492
+ // High-frequency, ephemeral operations - still fast
3493
+ "cursor-update": 0,
3494
+ "presence-update": 0,
3495
+ "typing-indicator": 0,
3496
+ "viewport-update": 0,
3497
+ "awareness-update": 0
3498
+ }
3499
+ };
3500
+ function getSecurityLevel(operationType, policy = DEFAULT_SECURITY_POLICY) {
3501
+ const override = policy.overrides[operationType];
3502
+ return override !== void 0 ? override : policy.default;
3503
+ }
3504
+ function isEphemeralOperation(operationType) {
3505
+ const ephemeralOps = [
3506
+ "cursor-update",
3507
+ "presence-update",
3508
+ "typing-indicator",
3509
+ "viewport-update",
3510
+ "awareness-update"
3511
+ ];
3512
+ return ephemeralOps.includes(operationType);
3513
+ }
3514
+ function isCriticalOperation(operationType) {
3515
+ const criticalOps = ["key-rotation", "permission-revoke", "identity-recovery"];
3516
+ return criticalOps.includes(operationType);
3517
+ }
3518
+ function createSecurityPolicy(options = {}) {
3519
+ return {
3520
+ default: options.default ?? DEFAULT_SECURITY_LEVEL3,
3521
+ overrides: {
3522
+ ...DEFAULT_SECURITY_POLICY.overrides,
3523
+ ...options.overrides
3524
+ }
3525
+ };
3526
+ }
3527
+ function mergeSecurityPolicies(...policies) {
3528
+ const merged = {
3529
+ default: DEFAULT_SECURITY_LEVEL3,
3530
+ overrides: {}
3531
+ };
3532
+ for (const policy of policies) {
3533
+ if (policy.default !== void 0) {
3534
+ merged.default = policy.default;
3535
+ }
3536
+ if (policy.overrides) {
3537
+ merged.overrides = { ...merged.overrides, ...policy.overrides };
3538
+ }
3539
+ }
3540
+ return merged;
3541
+ }
3542
+ export {
3543
+ ALL_FEATURES,
3544
+ AuthorizedSyncManager,
3545
+ AuthorizedYjsError,
3546
+ AuthorizedYjsSyncProvider,
3547
+ BaseSyncProvider,
3548
+ COMPACTION_TIME_THRESHOLD,
3549
+ COMPACTION_UPDATE_THRESHOLD,
3550
+ CURRENT_PROTOCOL_VERSION,
3551
+ ChangeHandlerRegistry,
3552
+ ClientIdMapImpl,
3553
+ DEFAULT_BATCHER_CONFIG,
3554
+ DEFAULT_RATE_LIMITER_CONFIG,
3555
+ DEFAULT_SECURITY_POLICY,
3556
+ DEFAULT_YJS_SCORING_CONFIG,
3557
+ DEPRECATIONS,
3558
+ DEPRECATION_POLICY,
3559
+ DeprecationError,
3560
+ FEATURES,
3561
+ HYBRID_SECURITY_POLICY,
3562
+ MAX_SECURITY_POLICY,
3563
+ MAX_YJS_DOC_SIZE,
3564
+ MAX_YJS_UPDATES_PER_MINUTE,
3565
+ MAX_YJS_UPDATES_PER_SECOND,
3566
+ MAX_YJS_UPDATE_SIZE,
3567
+ V1Serializer,
3568
+ V2Serializer,
3569
+ VersionNegotiator,
3570
+ YJS_CHANGE_TYPE,
3571
+ YJS_RATE_BURST_ALLOWANCE,
3572
+ YJS_SYNC_CHUNK_SIZE,
3573
+ YjsAuthGate,
3574
+ YjsBatcher,
3575
+ YjsCheckpointer,
3576
+ YjsIntegrityError,
3577
+ YjsPeerScorer,
3578
+ YjsRateLimiter,
3579
+ YjsStateIntegrityError,
3580
+ addDependencies,
3581
+ attemptRepair,
3582
+ autoDeserialize,
3583
+ autoSerialize,
3584
+ calculateChunkCount,
3585
+ changeHandlerRegistry,
3586
+ checkAndLogDeprecations,
3587
+ checkDeprecations,
3588
+ chunkUpdate,
3589
+ clearLoggedDeprecations,
3590
+ compareLamportTimestamps,
3591
+ computeChangeHash,
3592
+ configureDeprecationPolicy,
3593
+ createBatchId,
3594
+ createChangeId,
3595
+ createClientIdAttestation,
3596
+ createClientIdAttestationV1,
3597
+ createClientIdAttestationV2,
3598
+ createHandler,
3599
+ createIntegrityMonitor,
3600
+ createLamportClock,
3601
+ createLocalCapabilities,
3602
+ createPersistedDocState,
3603
+ createReactIntegrityMonitor,
3604
+ createSecurityPolicy,
3605
+ createSerializerRegistry,
3606
+ createTestContext,
3607
+ createUnsignedChange,
3608
+ createUnsignedYjsChange,
3609
+ createVersionedHandler,
3610
+ createYjsChange,
3611
+ decryptYjsState,
3612
+ defaultNegotiator,
3613
+ deserializeClientIdAttestation,
3614
+ deserializeEncryptedYjsState,
3615
+ deserializeYjsEnvelope,
3616
+ detectFork,
3617
+ diffFeatures,
3618
+ encryptYjsState,
3619
+ envelopeSize,
3620
+ findCommonAncestor,
3621
+ findHeads,
3622
+ findOrphans,
3623
+ findRoots,
3624
+ formatDeprecationReport,
3625
+ formatIntegrityReport,
3626
+ getAllDependencies,
3627
+ getAncestry,
3628
+ getChainDepth,
3629
+ getChainHeads,
3630
+ getChainRoots,
3631
+ getChangeNodeId,
3632
+ getDefaultSerializer,
3633
+ getDeprecation,
3634
+ getDeprecationsByType,
3635
+ getEnabledFeatures,
3636
+ getFeatureConflicts,
3637
+ getFeatureDependencies,
3638
+ getFeatureVersion,
3639
+ getForks,
3640
+ getOptionalFeatures,
3641
+ getRequiredFeatures,
3642
+ getSecurityLevel,
3643
+ getSerializer,
3644
+ hasSignedEnvelope,
3645
+ hashYjsState,
3646
+ intersectFeatures,
3647
+ isAfter,
3648
+ isBefore,
3649
+ isCriticalOperation,
3650
+ isDeprecated,
3651
+ isDocumentTooLarge,
3652
+ isEphemeralOperation,
3653
+ isFeatureAvailable,
3654
+ isFeatureEnabled,
3655
+ isLegacyUpdate,
3656
+ isNodeChange,
3657
+ isRemoved,
3658
+ isUpdateTooLarge,
3659
+ isV1Attestation,
3660
+ isV1Envelope,
3661
+ isV2Attestation,
3662
+ isV2Envelope,
3663
+ isYjsChange,
3664
+ loadVerifiedState,
3665
+ logDeprecation,
3666
+ maxTime,
3667
+ mergeSecurityPolicies,
3668
+ parseCapabilities,
3669
+ parseTimestamp,
3670
+ quickIntegrityCheck,
3671
+ reassembleChunks,
3672
+ receive,
3673
+ registerDeprecation,
3674
+ serializeClientIdAttestation,
3675
+ serializeEncryptedYjsState,
3676
+ serializeTimestamp,
3677
+ serializeYjsEnvelope,
3678
+ serializerRegistry,
3679
+ shouldCompact,
3680
+ signChange,
3681
+ signYjsUpdate,
3682
+ signYjsUpdateBatch,
3683
+ signYjsUpdateV1,
3684
+ signYjsUpdateV2,
3685
+ tick,
3686
+ toEncryptedData,
3687
+ topologicalSort,
3688
+ v1Serializer,
3689
+ v2Serializer,
3690
+ validateChain,
3691
+ validateClientIdOwnership,
3692
+ validateFeatureSet,
3693
+ verifyChange,
3694
+ verifyChangeHash,
3695
+ verifyClientIdAttestation,
3696
+ verifyClientIdAttestationV1,
3697
+ verifyClientIdAttestationV2,
3698
+ verifyIntegrity,
3699
+ verifyPersistedDocState,
3700
+ verifySingleChange,
3701
+ verifyYjsEnvelope,
3702
+ verifyYjsEnvelopeQuick,
3703
+ verifyYjsEnvelopeV1,
3704
+ verifyYjsEnvelopeV2,
3705
+ verifyYjsStateIntegrity
3706
+ };