@xnetjs/sync 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +308 -101
  2. package/dist/index.js +344 -28
  3. package/package.json +4 -4
package/dist/index.d.ts CHANGED
@@ -1,98 +1,7 @@
1
- import { DID, ContentId, PolicyEvaluator, AuthDecision } from '@xnetjs/core';
1
+ import { ContentId, DID, PolicyEvaluator, AuthDecision } from '@xnetjs/core';
2
2
  import { UnifiedSignature, SignatureWire, SecurityLevel, EncryptedData, WrappedKey } from '@xnetjs/crypto';
3
3
  import { PQKeyRegistry, HybridKeyBundle } from '@xnetjs/identity';
4
4
 
5
- /**
6
- * Lamport clock utilities for total ordering in distributed systems.
7
- *
8
- * Lamport timestamps provide a simple, single-integer logical clock that:
9
- * - Guarantees total ordering of events (with tie-breaker)
10
- * - Requires no coordination between nodes
11
- * - Is trivial to merge (max + 1)
12
- *
13
- * Combined with author DID as tie-breaker, this gives deterministic
14
- * ordering across all nodes without the complexity of vector clocks.
15
- */
16
-
17
- /**
18
- * A Lamport timestamp with author for deterministic tie-breaking.
19
- *
20
- * Two changes are ordered by:
21
- * 1. Lamport time (lower = earlier)
22
- * 2. Author DID string comparison (deterministic tie-breaker)
23
- */
24
- interface LamportTimestamp {
25
- /** Logical time - increments on each change */
26
- time: number;
27
- /** Author DID - used for deterministic tie-breaking */
28
- author: DID;
29
- }
30
- /**
31
- * A Lamport clock that tracks the current logical time.
32
- * Each author maintains their own clock instance.
33
- */
34
- interface LamportClock {
35
- /** Current logical time */
36
- time: number;
37
- /** The author's DID */
38
- author: DID;
39
- }
40
- /**
41
- * Create a new Lamport clock for an author.
42
- * Starts at time 0; first tick will produce time 1.
43
- */
44
- declare function createLamportClock(author: DID): LamportClock;
45
- /**
46
- * Tick the clock and return a new timestamp.
47
- * This should be called when creating a new change.
48
- *
49
- * @param clock - The clock to tick
50
- * @returns A tuple of [newClock, timestamp]
51
- */
52
- declare function tick(clock: LamportClock): [LamportClock, LamportTimestamp];
53
- /**
54
- * Update the clock after receiving a change from another node.
55
- * Sets our time to max(ourTime, receivedTime) so next tick is greater.
56
- *
57
- * @param clock - Our local clock
58
- * @param receivedTime - The Lamport time from the received change
59
- * @returns Updated clock
60
- */
61
- declare function receive(clock: LamportClock, receivedTime: number): LamportClock;
62
- /**
63
- * Compare two Lamport timestamps for ordering.
64
- *
65
- * @returns
66
- * -1 if a < b (a happened before b)
67
- * 1 if a > b (a happened after b)
68
- * 0 if a === b (same timestamp - should be rare)
69
- */
70
- declare function compareLamportTimestamps(a: LamportTimestamp, b: LamportTimestamp): -1 | 0 | 1;
71
- /**
72
- * Check if timestamp a is strictly before timestamp b.
73
- */
74
- declare function isBefore(a: LamportTimestamp, b: LamportTimestamp): boolean;
75
- /**
76
- * Check if timestamp a is strictly after timestamp b.
77
- */
78
- declare function isAfter(a: LamportTimestamp, b: LamportTimestamp): boolean;
79
- /**
80
- * Serialize a Lamport timestamp to a string for storage/sorting.
81
- * Format: {time-padded-16-digits}-{author}
82
- *
83
- * The padding ensures lexicographic string sorting matches numeric sorting.
84
- */
85
- declare function serializeTimestamp(ts: LamportTimestamp): string;
86
- /**
87
- * Parse a serialized Lamport timestamp.
88
- */
89
- declare function parseTimestamp(serialized: string): LamportTimestamp;
90
- /**
91
- * Get the maximum Lamport time from a list of timestamps.
92
- * Useful for initializing a clock after loading existing changes.
93
- */
94
- declare function maxTime(timestamps: LamportTimestamp[]): number;
95
-
96
5
  /**
97
6
  * Change types for xNet sync primitives
98
7
  *
@@ -149,8 +58,11 @@ interface Change<T = unknown> {
149
58
  signature: Uint8Array;
150
59
  /** Wall clock timestamp (milliseconds) - for display, not ordering */
151
60
  wallTime: number;
152
- /** Lamport timestamp for ordering */
153
- lamport: LamportTimestamp;
61
+ /**
62
+ * Lamport logical time for ordering (a plain integer). The author tiebreak
63
+ * for LWW comes from `authorDID`; this field is just the clock value.
64
+ */
65
+ lamport: number;
154
66
  /**
155
67
  * Groups changes that should be treated as a single atomic operation.
156
68
  * All changes with the same batchId were created in one transaction.
@@ -180,7 +92,7 @@ interface UnsignedChange<T = unknown> {
180
92
  parentHash: ContentId | null;
181
93
  authorDID: DID;
182
94
  wallTime: number;
183
- lamport: LamportTimestamp;
95
+ lamport: number;
184
96
  batchId?: string;
185
97
  batchIndex?: number;
186
98
  batchSize?: number;
@@ -194,7 +106,7 @@ interface CreateChangeOptions<T> {
194
106
  payload: T;
195
107
  parentHash: ContentId | null;
196
108
  authorDID: DID;
197
- lamport: LamportTimestamp;
109
+ lamport: number;
198
110
  wallTime?: number;
199
111
  batchId?: string;
200
112
  batchIndex?: number;
@@ -225,6 +137,22 @@ declare function computeChangeHash<T>(unsigned: UnsignedChange<T>): ContentId;
225
137
  * @returns Signed change with hash and signature
226
138
  */
227
139
  declare function signChange<T>(unsigned: UnsignedChange<T>, signingKey: Uint8Array): Change<T>;
140
+ /**
141
+ * Pluggable async change signer. Lets integrators move Ed25519 signing off
142
+ * the interactive path (WebCrypto, a worker, or a remote signer) while
143
+ * producing byte-identical signatures to {@link signChange}.
144
+ */
145
+ type ChangeSigner = <T>(unsigned: UnsignedChange<T>) => Promise<Change<T>>;
146
+ /**
147
+ * Create a {@link ChangeSigner} backed by WebCrypto Ed25519.
148
+ *
149
+ * Ed25519 is deterministic (RFC 8032), so signatures are byte-identical to
150
+ * the synchronous {@link signChange} path — only the execution moves off
151
+ * the calling thread. Returns null when the runtime has no SubtleCrypto;
152
+ * if WebCrypto rejects Ed25519 at runtime, the signer falls back to the
153
+ * synchronous path transparently.
154
+ */
155
+ declare function createWebCryptoChangeSigner(signingKey: Uint8Array): ChangeSigner | null;
228
156
  /**
229
157
  * Verify a change's signature against a public key.
230
158
  *
@@ -238,6 +166,16 @@ declare function signChange<T>(unsigned: UnsignedChange<T>, signingKey: Uint8Arr
238
166
  * @returns true if the signature is valid
239
167
  */
240
168
  declare function verifyChange<T>(change: Change<T>, publicKey: Uint8Array): boolean;
169
+ /**
170
+ * Recompute the content hash of a signed change from its own fields.
171
+ *
172
+ * Reconstructs the unsigned form (the exact field set that goes into the hash)
173
+ * and runs {@link computeChangeHash}. This is the single source of truth for
174
+ * "which fields are hashed" — {@link verifyChangeHash} is defined in terms of
175
+ * it, and callers that need to *report* a mismatch (not just detect one) can
176
+ * use it to surface the hash this build expects.
177
+ */
178
+ declare function recomputeChangeHash<T>(change: Change<T>): ContentId;
241
179
  /**
242
180
  * Verify that a change's hash is correct (not tampered).
243
181
  * This re-computes the hash from the change data and compares.
@@ -251,6 +189,97 @@ declare function verifyChangeHash<T>(change: Change<T>): boolean;
251
189
  */
252
190
  declare function createChangeId(): string;
253
191
 
192
+ /**
193
+ * Lamport clock utilities for total ordering in distributed systems.
194
+ *
195
+ * Lamport timestamps provide a simple, single-integer logical clock that:
196
+ * - Guarantees total ordering of events (with tie-breaker)
197
+ * - Requires no coordination between nodes
198
+ * - Is trivial to merge (max + 1)
199
+ *
200
+ * Combined with author DID as tie-breaker, this gives deterministic
201
+ * ordering across all nodes without the complexity of vector clocks.
202
+ */
203
+
204
+ /**
205
+ * A Lamport timestamp with author for deterministic tie-breaking.
206
+ *
207
+ * Two changes are ordered by:
208
+ * 1. Lamport time (lower = earlier)
209
+ * 2. Author DID string comparison (deterministic tie-breaker)
210
+ */
211
+ interface LamportTimestamp {
212
+ /** Logical time - increments on each change */
213
+ time: number;
214
+ /** Author DID - used for deterministic tie-breaking */
215
+ author: DID;
216
+ }
217
+ /**
218
+ * A Lamport clock that tracks the current logical time.
219
+ * Each author maintains their own clock instance.
220
+ */
221
+ interface LamportClock {
222
+ /** Current logical time */
223
+ time: number;
224
+ /** The author's DID */
225
+ author: DID;
226
+ }
227
+ /**
228
+ * Create a new Lamport clock for an author.
229
+ * Starts at time 0; first tick will produce time 1.
230
+ */
231
+ declare function createLamportClock(author: DID): LamportClock;
232
+ /**
233
+ * Tick the clock and return a new timestamp.
234
+ * This should be called when creating a new change.
235
+ *
236
+ * @param clock - The clock to tick
237
+ * @returns A tuple of [newClock, timestamp]
238
+ */
239
+ declare function tick(clock: LamportClock): [LamportClock, LamportTimestamp];
240
+ /**
241
+ * Update the clock after receiving a change from another node.
242
+ * Sets our time to max(ourTime, receivedTime) so next tick is greater.
243
+ *
244
+ * @param clock - Our local clock
245
+ * @param receivedTime - The Lamport time from the received change
246
+ * @returns Updated clock
247
+ */
248
+ declare function receive(clock: LamportClock, receivedTime: number): LamportClock;
249
+ /**
250
+ * Compare two Lamport timestamps for ordering.
251
+ *
252
+ * @returns
253
+ * -1 if a < b (a happened before b)
254
+ * 1 if a > b (a happened after b)
255
+ * 0 if a === b (same timestamp - should be rare)
256
+ */
257
+ declare function compareLamportTimestamps(a: LamportTimestamp, b: LamportTimestamp): -1 | 0 | 1;
258
+ /**
259
+ * Check if timestamp a is strictly before timestamp b.
260
+ */
261
+ declare function isBefore(a: LamportTimestamp, b: LamportTimestamp): boolean;
262
+ /**
263
+ * Check if timestamp a is strictly after timestamp b.
264
+ */
265
+ declare function isAfter(a: LamportTimestamp, b: LamportTimestamp): boolean;
266
+ /**
267
+ * Serialize a Lamport timestamp to a string for storage/sorting.
268
+ * Format: {time-padded-16-digits}-{author}
269
+ *
270
+ * The padding ensures lexicographic string sorting matches numeric sorting.
271
+ */
272
+ declare function serializeTimestamp(ts: LamportTimestamp): string;
273
+ /**
274
+ * Parse a serialized Lamport timestamp.
275
+ */
276
+ declare function parseTimestamp(serialized: string): LamportTimestamp;
277
+ /**
278
+ * Get the maximum Lamport time from a list of timestamps.
279
+ * Useful for initializing a clock after loading existing changes.
280
+ */
281
+ declare function maxTime(timestamps: LamportTimestamp[]): number;
282
+
254
283
  /**
255
284
  * Hash chain utilities for managing linked changes.
256
285
  *
@@ -946,6 +975,145 @@ declare abstract class BaseSyncProvider<T = unknown> implements SyncProvider<T>
946
975
  canUseFeatureWithAll(feature: FeatureFlag): boolean;
947
976
  }
948
977
 
978
+ /**
979
+ * Sync runtime lifecycle helpers.
980
+ */
981
+ type SyncConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
982
+ type SyncLifecyclePhase = 'idle' | 'starting' | 'local-ready' | 'connecting' | 'healthy' | 'degraded' | 'replaying' | 'stopped';
983
+ type SyncLifecycleInput = {
984
+ started: boolean;
985
+ stopped: boolean;
986
+ localReady: boolean;
987
+ everConnected: boolean;
988
+ connectionStatus: SyncConnectionStatus;
989
+ replaying?: boolean;
990
+ };
991
+ type SyncLifecycleState = {
992
+ phase: SyncLifecyclePhase;
993
+ connectionStatus: SyncConnectionStatus;
994
+ replaying: boolean;
995
+ lastTransitionAt: number;
996
+ };
997
+ declare function deriveSyncLifecyclePhase(input: SyncLifecycleInput): SyncLifecyclePhase;
998
+ declare function createSyncLifecycleState(input: SyncLifecycleInput, previous?: SyncLifecycleState): SyncLifecycleState;
999
+
1000
+ /**
1001
+ * Signed-replication policy helpers.
1002
+ */
1003
+ type ReplicationNamespaceKind = 'system' | 'user';
1004
+ type SyncFederationHub = {
1005
+ /**
1006
+ * Stable hub identifier used by policy nodes. Fallback hubs use their URL.
1007
+ */
1008
+ id: string;
1009
+ /** WebSocket URL for this hub. */
1010
+ url: string;
1011
+ /** Lower values are selected first when maxHubs prunes a plan. */
1012
+ priority?: number;
1013
+ /** Namespace kinds this hub accepts. Omit to accept both. */
1014
+ kinds?: readonly ReplicationNamespaceKind[];
1015
+ /** Disabled hubs stay in config for traceability but are not selected. */
1016
+ disabled?: boolean;
1017
+ };
1018
+ type SyncFederationNamespacePolicy = {
1019
+ /** Exact namespace, namespace prefix, or `*`. */
1020
+ namespace: string;
1021
+ /** Optional override when namespace syntax is ambiguous. */
1022
+ kind?: ReplicationNamespaceKind;
1023
+ /** Restrict replication to these hub IDs. */
1024
+ includeHubIds?: readonly string[];
1025
+ /** Remove these hub IDs after includes/defaults are applied. */
1026
+ excludeHubIds?: readonly string[];
1027
+ /** Minimum destination count expected for this namespace. */
1028
+ minHubs?: number;
1029
+ /** Maximum destination count. Pruning is priority/id deterministic. */
1030
+ maxHubs?: number;
1031
+ };
1032
+ type SyncFederationConfig = {
1033
+ /** Federated hub inventory. */
1034
+ hubs?: readonly SyncFederationHub[];
1035
+ /** Namespace-specific destination policies. */
1036
+ namespacePolicies?: readonly SyncFederationNamespacePolicy[];
1037
+ /** Default destinations for `sys/*` namespaces when no policy include list exists. */
1038
+ defaultSystemHubIds?: readonly string[];
1039
+ /** Default destinations for user namespaces when no policy include list exists. */
1040
+ defaultUserHubIds?: readonly string[];
1041
+ };
1042
+ interface SyncCompatibilityConfig {
1043
+ /**
1044
+ * Temporary compatibility mode for legacy peers that still send unsigned
1045
+ * Yjs replication payloads.
1046
+ */
1047
+ allowUnsignedReplication?: boolean;
1048
+ }
1049
+ interface SyncReplicationConfig {
1050
+ /**
1051
+ * Compatibility toggles for older replication paths.
1052
+ */
1053
+ compatibility?: SyncCompatibilityConfig;
1054
+ /**
1055
+ * Multi-hub federation routing policy.
1056
+ */
1057
+ federation?: SyncFederationConfig;
1058
+ }
1059
+ interface ResolvedSyncReplicationPolicy {
1060
+ /**
1061
+ * Whether unsigned replication payloads are accepted.
1062
+ */
1063
+ allowUnsignedReplication: boolean;
1064
+ /**
1065
+ * Whether replication payloads must be signed.
1066
+ */
1067
+ requireSignedReplication: boolean;
1068
+ }
1069
+ type ReplicationPlanDestination = {
1070
+ hubId: string;
1071
+ url: string;
1072
+ priority: number;
1073
+ reason: string;
1074
+ };
1075
+ type ReplicationPlanDiagnostic = {
1076
+ code: 'no_hubs_configured' | 'policy_hub_not_found' | 'minimum_hubs_not_satisfied' | 'hub_kind_mismatch' | 'hub_disabled';
1077
+ message: string;
1078
+ hubId?: string;
1079
+ };
1080
+ type ReplicationPlanTraceStep = {
1081
+ step: string;
1082
+ message: string;
1083
+ hubId?: string;
1084
+ namespace?: string;
1085
+ };
1086
+ type ReplicationPlan = {
1087
+ namespace: string;
1088
+ kind: ReplicationNamespaceKind;
1089
+ policy: SyncFederationNamespacePolicy | null;
1090
+ destinations: ReplicationPlanDestination[];
1091
+ diagnostics: ReplicationPlanDiagnostic[];
1092
+ trace: ReplicationPlanTraceStep[];
1093
+ };
1094
+ type PolicyRevisionSimulation = {
1095
+ before: ReplicationPlan;
1096
+ after: ReplicationPlan;
1097
+ addedHubIds: string[];
1098
+ removedHubIds: string[];
1099
+ retainedHubIds: string[];
1100
+ changed: boolean;
1101
+ };
1102
+ declare function resolveSyncReplicationPolicy(config: SyncReplicationConfig | undefined): ResolvedSyncReplicationPolicy;
1103
+ declare function inferReplicationNamespaceKind(namespace: string): ReplicationNamespaceKind;
1104
+ declare function normalizeSyncFederationHubs(config: SyncReplicationConfig | undefined, fallbackUrls?: string[]): SyncFederationHub[];
1105
+ declare function planReplicationDestinations(input: {
1106
+ namespace: string;
1107
+ config?: SyncReplicationConfig;
1108
+ fallbackHubUrls?: string[];
1109
+ }): ReplicationPlan;
1110
+ declare function simulateSyncPolicyRevision(input: {
1111
+ namespace: string;
1112
+ current?: SyncReplicationConfig;
1113
+ revision?: SyncReplicationConfig;
1114
+ fallbackHubUrls?: string[];
1115
+ }): PolicyRevisionSimulation;
1116
+
949
1117
  /**
950
1118
  * Signed Yjs Envelopes - Per-update signing and verification for Yjs sync messages
951
1119
  *
@@ -1192,6 +1360,10 @@ declare function isLegacyUpdate(msg: unknown): msg is {
1192
1360
  */
1193
1361
  /** Maximum size of a single Yjs update (1MB) */
1194
1362
  declare const MAX_YJS_UPDATE_SIZE = 1048576;
1363
+ /** Maximum size of a Yjs state-vector request (64KB) */
1364
+ declare const MAX_YJS_STATE_VECTOR_SIZE = 65536;
1365
+ /** Maximum size of a Yjs awareness update or direct awareness state (64KB) */
1366
+ declare const MAX_YJS_AWARENESS_UPDATE_SIZE = 65536;
1195
1367
  /** Maximum updates per second per connection */
1196
1368
  declare const MAX_YJS_UPDATES_PER_SECOND = 30;
1197
1369
  /** Maximum updates per minute per connection (sustained rate) */
@@ -1287,6 +1459,22 @@ declare class YjsRateLimiter {
1287
1459
  * Check if an update exceeds the size limit.
1288
1460
  */
1289
1461
  declare function isUpdateTooLarge(update: Uint8Array, maxSize?: number): boolean;
1462
+ /**
1463
+ * Estimate decoded byte length for a base64 payload.
1464
+ */
1465
+ declare function estimateBase64DecodedLength(value: string): number;
1466
+ /**
1467
+ * Check if a base64 payload would exceed a decoded size limit.
1468
+ */
1469
+ declare function isBase64PayloadTooLarge(value: string, maxSize: number): boolean;
1470
+ /**
1471
+ * Check if a state-vector request exceeds the size limit.
1472
+ */
1473
+ declare function isStateVectorTooLarge(stateVector: Uint8Array, maxSize?: number): boolean;
1474
+ /**
1475
+ * Check if an awareness update exceeds the size limit.
1476
+ */
1477
+ declare function isAwarenessUpdateTooLarge(update: Uint8Array, maxSize?: number): boolean;
1290
1478
  /**
1291
1479
  * Check if a document state exceeds the size limit.
1292
1480
  */
@@ -1428,6 +1616,17 @@ type YjsViolationType = 'invalidSignature' | 'oversizedUpdate' | 'rateExceeded'
1428
1616
  * Action to take based on peer score.
1429
1617
  */
1430
1618
  type PeerAction = 'allow' | 'warn' | 'throttle' | 'block';
1619
+ /**
1620
+ * Emitted whenever a violation produces a peer action.
1621
+ */
1622
+ interface YjsPeerActionEvent {
1623
+ peerId: string;
1624
+ reason: YjsViolationType;
1625
+ action: PeerAction;
1626
+ score: number;
1627
+ metrics: YjsPeerMetrics;
1628
+ }
1629
+ type YjsPeerActionListener = (event: YjsPeerActionEvent) => void;
1431
1630
  /**
1432
1631
  * Optional telemetry collector interface for sync operations.
1433
1632
  * Compatible with @xnetjs/telemetry TelemetryCollector.
@@ -1504,6 +1703,7 @@ declare class YjsPeerScorer {
1504
1703
  private scores;
1505
1704
  readonly config: YjsScoringConfig;
1506
1705
  private telemetry?;
1706
+ private actionListeners;
1507
1707
  constructor(config?: Partial<YjsScoringConfig>);
1508
1708
  /**
1509
1709
  * Record a violation for a peer.
@@ -1549,10 +1749,17 @@ declare class YjsPeerScorer {
1549
1749
  * Get all metrics (for monitoring endpoint).
1550
1750
  */
1551
1751
  getAllMetrics(): Map<string, YjsPeerMetrics>;
1752
+ /**
1753
+ * Listen for peer actions caused by violations.
1754
+ *
1755
+ * Returns an unsubscribe callback so owning packages can cleanly detach bridges.
1756
+ */
1757
+ onAction(listener: YjsPeerActionListener): () => void;
1552
1758
  /**
1553
1759
  * Remove all state for a disconnected peer.
1554
1760
  */
1555
1761
  remove(peerId: string): void;
1762
+ private emitAction;
1556
1763
  /**
1557
1764
  * Clear all state.
1558
1765
  */
@@ -2031,8 +2238,8 @@ interface CreateYjsChangeOptions {
2031
2238
  privateKey: Uint8Array;
2032
2239
  /** Hash of the previous change in the chain (null for first) */
2033
2240
  parentHash: ContentId | null;
2034
- /** Lamport timestamp for ordering */
2035
- lamport: LamportTimestamp;
2241
+ /** Lamport logical clock value for ordering */
2242
+ lamport: number;
2036
2243
  /** Optional wall time (defaults to Date.now()) */
2037
2244
  wallTime?: number;
2038
2245
  }
@@ -2059,7 +2266,7 @@ declare function createUnsignedYjsChange(options: Omit<CreateYjsChangeOptions, '
2059
2266
  * authorDID: identity.did,
2060
2267
  * privateKey: identity.privateKey,
2061
2268
  * parentHash: lastChangeHash,
2062
- * lamport: { time: 42, did: identity.did }
2269
+ * lamport: 42
2063
2270
  * })
2064
2271
  * ```
2065
2272
  */
@@ -2334,7 +2541,7 @@ interface SerializerRegistry {
2334
2541
  * authorDID: string,
2335
2542
  * signature: string (base64),
2336
2543
  * wallTime: number,
2337
- * lamport: { time: number, did: string },
2544
+ * lamport: number,
2338
2545
  * batchId?: string,
2339
2546
  * batchIndex?: number,
2340
2547
  * batchSize?: number
@@ -3200,4 +3407,4 @@ declare function createSecurityPolicy(options?: Partial<SecurityPolicy>): Securi
3200
3407
  */
3201
3408
  declare function mergeSecurityPolicies(...policies: Partial<SecurityPolicy>[]): SecurityPolicy;
3202
3409
 
3203
- export { ALL_FEATURES, type AttestationVerificationResult, type AttestationVerifyResult, type AuthorizedDoc, type AuthorizedRoom, type AuthorizedStateAdapter, AuthorizedSyncManager, type AuthorizedSyncManagerOptions, AuthorizedYjsError, AuthorizedYjsSyncProvider, type AuthorizedYjsSyncProviderOptions, BaseSyncProvider, type BatchFlushCallback, COMPACTION_TIME_THRESHOLD, COMPACTION_UPDATE_THRESHOLD, CURRENT_PROTOCOL_VERSION, type ChainValidationResult, type Change, type ChangeHandler, ChangeHandlerRegistry, type ChangeSerializer, type ClientIdAttestation, type ClientIdAttestationV1, type ClientIdAttestationV2, type ClientIdAttestationWire, type ClientIdMap, ClientIdMapImpl, type ContentKeyProvider, type CreateAttestationOptions, type CreateChangeOptions, type CreateEnvelopeOptions, type CreateYjsChangeOptions, DEFAULT_BATCHER_CONFIG, DEFAULT_RATE_LIMITER_CONFIG, DEFAULT_SECURITY_POLICY, DEFAULT_YJS_SCORING_CONFIG, DEPRECATIONS, DEPRECATION_POLICY, type DeprecationCallback, type DeprecationContext, DeprecationError, type DeprecationNotice, type DeprecationType, type DeprecationWarning, type DeserializeError, type DeserializeOutcome, type DeserializeResult, type EncryptedYjsState, type EnvelopeVerificationResult, type EnvelopeVerifyResult, FEATURES, type FeatureConfig, type FeatureFlag, type FeatureValidationError, type FeatureValidationResult, type FeatureValidationWarning, type Fork, type GrantEventStore, HYBRID_SECURITY_POLICY, type HandlerContext, type HandlerEvent, type IntegrityIssue, type IntegrityIssueType, type IntegrityMonitor, type IntegrityMonitorConfig, type IntegrityMonitorStats, type IntegrityReport, type LamportClock, type LamportTimestamp, MAX_SECURITY_POLICY, MAX_YJS_DOC_SIZE, MAX_YJS_UPDATES_PER_MINUTE, MAX_YJS_UPDATES_PER_SECOND, MAX_YJS_UPDATE_SIZE, type MergeUpdatesFn, type NegotiatedSession, type NegotiationFailure, type NegotiationResult, type NegotiationWarning, type OperationType, type PeerAction, type PeerCapabilities, type PeerInfo, type PersistedDocState, type ProcessResult, type RateLimiterConfig, type ReactIntegrityMonitorOptions, type RecipientKeyResolver, type RegistryStats, type RepairAction, type RepairActionType, type SecurityPolicy, type SerializeOptions, type SerializedChange, type SerializerRegistry, type SignedYjsEnvelope, type SignedYjsEnvelopeV1, type SignedYjsEnvelopeV2, type SignedYjsEnvelopeWire, type SyncEventListener, type SyncProvider, type SyncProviderEvents, type SyncProviderOptions, type SyncStatus, type UnsignedChange, type UnsignedYjsChange, V1Serializer, V2Serializer, type ValidationError, type ValidationResult, type ValidationWarning, type VerifyAttestationOptions, type VerifyEnvelopeOptions, type VerifyOptions, VersionNegotiator, type YDocCodec, type YDocLike, YJS_CHANGE_TYPE, YJS_RATE_BURST_ALLOWANCE, YJS_SYNC_CHUNK_SIZE, type YjsAuthDecision, YjsAuthGate, type YjsAuthGateOptions, YjsBatcher, type YjsBatcherConfig, type YjsChange, YjsCheckpointer, type YjsCheckpointerOptions, YjsIntegrityError, type YjsPeerMetrics, YjsPeerScorer, YjsRateLimiter, type YjsScoringConfig, YjsStateIntegrityError, type YjsUpdatePayload, type YjsViolationType, addDependencies, attemptRepair, autoDeserialize, autoSerialize, calculateChunkCount, changeHandlerRegistry, checkAndLogDeprecations, checkDeprecations, chunkUpdate, clearLoggedDeprecations, compareLamportTimestamps, computeChangeHash, configureDeprecationPolicy, createBatchId, createChangeId, createClientIdAttestation, createClientIdAttestationV1, createClientIdAttestationV2, createHandler, createIntegrityMonitor, createLamportClock, createLocalCapabilities, createPersistedDocState, createReactIntegrityMonitor, createSecurityPolicy, createSerializerRegistry, createTestContext, createUnsignedChange, createUnsignedYjsChange, createVersionedHandler, createYjsChange, decryptYjsState, defaultNegotiator, deserializeClientIdAttestation, deserializeEncryptedYjsState, deserializeYjsEnvelope, detectFork, diffFeatures, encryptYjsState, envelopeSize, findCommonAncestor, findHeads, findOrphans, findRoots, formatDeprecationReport, formatIntegrityReport, getAllDependencies, getAncestry, getChainDepth, getChainHeads, getChainRoots, getChangeNodeId, getDefaultSerializer, getDeprecation, getDeprecationsByType, getEnabledFeatures, getFeatureConflicts, getFeatureDependencies, getFeatureVersion, getForks, getOptionalFeatures, getRequiredFeatures, getSecurityLevel, getSerializer, hasSignedEnvelope, hashYjsState, intersectFeatures, isAfter, isBefore, isCriticalOperation, isDeprecated, isDocumentTooLarge, isEphemeralOperation, isFeatureAvailable, isFeatureEnabled, isLegacyUpdate, isNodeChange, isRemoved, isUpdateTooLarge, isV1Attestation, isV1Envelope, isV2Attestation, isV2Envelope, isYjsChange, loadVerifiedState, logDeprecation, maxTime, mergeSecurityPolicies, parseCapabilities, parseTimestamp, quickIntegrityCheck, reassembleChunks, receive, registerDeprecation, serializeClientIdAttestation, serializeEncryptedYjsState, serializeTimestamp, serializeYjsEnvelope, serializerRegistry, shouldCompact, signChange, signYjsUpdate, signYjsUpdateBatch, signYjsUpdateV1, signYjsUpdateV2, tick, toEncryptedData, topologicalSort, v1Serializer, v2Serializer, validateChain, validateClientIdOwnership, validateFeatureSet, verifyChange, verifyChangeHash, verifyClientIdAttestation, verifyClientIdAttestationV1, verifyClientIdAttestationV2, verifyIntegrity, verifyPersistedDocState, verifySingleChange, verifyYjsEnvelope, verifyYjsEnvelopeQuick, verifyYjsEnvelopeV1, verifyYjsEnvelopeV2, verifyYjsStateIntegrity };
3410
+ export { ALL_FEATURES, type AttestationVerificationResult, type AttestationVerifyResult, type AuthorizedDoc, type AuthorizedRoom, type AuthorizedStateAdapter, AuthorizedSyncManager, type AuthorizedSyncManagerOptions, AuthorizedYjsError, AuthorizedYjsSyncProvider, type AuthorizedYjsSyncProviderOptions, BaseSyncProvider, type BatchFlushCallback, COMPACTION_TIME_THRESHOLD, COMPACTION_UPDATE_THRESHOLD, CURRENT_PROTOCOL_VERSION, type ChainValidationResult, type Change, type ChangeHandler, ChangeHandlerRegistry, type ChangeSerializer, type ChangeSigner, type ClientIdAttestation, type ClientIdAttestationV1, type ClientIdAttestationV2, type ClientIdAttestationWire, type ClientIdMap, ClientIdMapImpl, type ContentKeyProvider, type CreateAttestationOptions, type CreateChangeOptions, type CreateEnvelopeOptions, type CreateYjsChangeOptions, DEFAULT_BATCHER_CONFIG, DEFAULT_RATE_LIMITER_CONFIG, DEFAULT_SECURITY_POLICY, DEFAULT_YJS_SCORING_CONFIG, DEPRECATIONS, DEPRECATION_POLICY, type DeprecationCallback, type DeprecationContext, DeprecationError, type DeprecationNotice, type DeprecationType, type DeprecationWarning, type DeserializeError, type DeserializeOutcome, type DeserializeResult, type EncryptedYjsState, type EnvelopeVerificationResult, type EnvelopeVerifyResult, FEATURES, type FeatureConfig, type FeatureFlag, type FeatureValidationError, type FeatureValidationResult, type FeatureValidationWarning, type Fork, type GrantEventStore, HYBRID_SECURITY_POLICY, type HandlerContext, type HandlerEvent, type IntegrityIssue, type IntegrityIssueType, type IntegrityMonitor, type IntegrityMonitorConfig, type IntegrityMonitorStats, type IntegrityReport, type LamportClock, type LamportTimestamp, MAX_SECURITY_POLICY, MAX_YJS_AWARENESS_UPDATE_SIZE, MAX_YJS_DOC_SIZE, MAX_YJS_STATE_VECTOR_SIZE, MAX_YJS_UPDATES_PER_MINUTE, MAX_YJS_UPDATES_PER_SECOND, MAX_YJS_UPDATE_SIZE, type MergeUpdatesFn, type NegotiatedSession, type NegotiationFailure, type NegotiationResult, type NegotiationWarning, type OperationType, type PeerAction, type PeerCapabilities, type PeerInfo, type PersistedDocState, type PolicyRevisionSimulation, type ProcessResult, type RateLimiterConfig, type ReactIntegrityMonitorOptions, type RecipientKeyResolver, type RegistryStats, type RepairAction, type RepairActionType, type ReplicationNamespaceKind, type ReplicationPlan, type ReplicationPlanDestination, type ReplicationPlanDiagnostic, type ReplicationPlanTraceStep, type ResolvedSyncReplicationPolicy, type SecurityPolicy, type SerializeOptions, type SerializedChange, type SerializerRegistry, type SignedYjsEnvelope, type SignedYjsEnvelopeV1, type SignedYjsEnvelopeV2, type SignedYjsEnvelopeWire, type SyncCompatibilityConfig, type SyncConnectionStatus, type SyncEventListener, type SyncFederationConfig, type SyncFederationHub, type SyncFederationNamespacePolicy, type SyncLifecycleInput, type SyncLifecyclePhase, type SyncLifecycleState, type SyncProvider, type SyncProviderEvents, type SyncProviderOptions, type SyncReplicationConfig, type SyncStatus, type UnsignedChange, type UnsignedYjsChange, V1Serializer, V2Serializer, type ValidationError, type ValidationResult, type ValidationWarning, type VerifyAttestationOptions, type VerifyEnvelopeOptions, type VerifyOptions, VersionNegotiator, type YDocCodec, type YDocLike, YJS_CHANGE_TYPE, YJS_RATE_BURST_ALLOWANCE, YJS_SYNC_CHUNK_SIZE, type YjsAuthDecision, YjsAuthGate, type YjsAuthGateOptions, YjsBatcher, type YjsBatcherConfig, type YjsChange, YjsCheckpointer, type YjsCheckpointerOptions, YjsIntegrityError, type YjsPeerActionEvent, type YjsPeerActionListener, type YjsPeerMetrics, YjsPeerScorer, YjsRateLimiter, type YjsRateLimiterOptions, type YjsScoringConfig, YjsStateIntegrityError, type YjsUpdatePayload, type YjsViolationType, addDependencies, attemptRepair, autoDeserialize, autoSerialize, calculateChunkCount, changeHandlerRegistry, checkAndLogDeprecations, checkDeprecations, chunkUpdate, clearLoggedDeprecations, compareLamportTimestamps, computeChangeHash, configureDeprecationPolicy, createBatchId, createChangeId, createClientIdAttestation, createClientIdAttestationV1, createClientIdAttestationV2, createHandler, createIntegrityMonitor, createLamportClock, createLocalCapabilities, createPersistedDocState, createReactIntegrityMonitor, createSecurityPolicy, createSerializerRegistry, createSyncLifecycleState, createTestContext, createUnsignedChange, createUnsignedYjsChange, createVersionedHandler, createWebCryptoChangeSigner, createYjsChange, decryptYjsState, defaultNegotiator, deriveSyncLifecyclePhase, deserializeClientIdAttestation, deserializeEncryptedYjsState, deserializeYjsEnvelope, detectFork, diffFeatures, encryptYjsState, envelopeSize, estimateBase64DecodedLength, findCommonAncestor, findHeads, findOrphans, findRoots, formatDeprecationReport, formatIntegrityReport, getAllDependencies, getAncestry, getChainDepth, getChainHeads, getChainRoots, getChangeNodeId, getDefaultSerializer, getDeprecation, getDeprecationsByType, getEnabledFeatures, getFeatureConflicts, getFeatureDependencies, getFeatureVersion, getForks, getOptionalFeatures, getRequiredFeatures, getSecurityLevel, getSerializer, hasSignedEnvelope, hashYjsState, inferReplicationNamespaceKind, intersectFeatures, isAfter, isAwarenessUpdateTooLarge, isBase64PayloadTooLarge, isBefore, isCriticalOperation, isDeprecated, isDocumentTooLarge, isEphemeralOperation, isFeatureAvailable, isFeatureEnabled, isLegacyUpdate, isNodeChange, isRemoved, isStateVectorTooLarge, isUpdateTooLarge, isV1Attestation, isV1Envelope, isV2Attestation, isV2Envelope, isYjsChange, loadVerifiedState, logDeprecation, maxTime, mergeSecurityPolicies, normalizeSyncFederationHubs, parseCapabilities, parseTimestamp, planReplicationDestinations, quickIntegrityCheck, reassembleChunks, receive, recomputeChangeHash, registerDeprecation, resolveSyncReplicationPolicy, serializeClientIdAttestation, serializeEncryptedYjsState, serializeTimestamp, serializeYjsEnvelope, serializerRegistry, shouldCompact, signChange, signYjsUpdate, signYjsUpdateBatch, signYjsUpdateV1, signYjsUpdateV2, simulateSyncPolicyRevision, tick, toEncryptedData, topologicalSort, v1Serializer, v2Serializer, validateChain, validateClientIdOwnership, validateFeatureSet, verifyChange, verifyChangeHash, verifyClientIdAttestation, verifyClientIdAttestationV1, verifyClientIdAttestationV2, verifyIntegrity, verifyPersistedDocState, verifySingleChange, verifyYjsEnvelope, verifyYjsEnvelopeQuick, verifyYjsEnvelopeV1, verifyYjsEnvelopeV2, verifyYjsStateIntegrity };
package/dist/index.js CHANGED
@@ -62,6 +62,55 @@ function signChange(unsigned, signingKey) {
62
62
  signature
63
63
  };
64
64
  }
65
+ var ED25519_PKCS8_PREFIX = new Uint8Array([
66
+ 48,
67
+ 46,
68
+ 2,
69
+ 1,
70
+ 0,
71
+ 48,
72
+ 5,
73
+ 6,
74
+ 3,
75
+ 43,
76
+ 101,
77
+ 112,
78
+ 4,
79
+ 34,
80
+ 4,
81
+ 32
82
+ ]);
83
+ function createWebCryptoChangeSigner(signingKey) {
84
+ const subtle = globalThis.crypto?.subtle;
85
+ if (!subtle || typeof subtle.importKey !== "function") {
86
+ return null;
87
+ }
88
+ const pkcs8 = new Uint8Array(ED25519_PKCS8_PREFIX.length + 32);
89
+ pkcs8.set(ED25519_PKCS8_PREFIX, 0);
90
+ pkcs8.set(signingKey.subarray(0, 32), ED25519_PKCS8_PREFIX.length);
91
+ let cryptoKeyPromise = null;
92
+ let webCryptoUnavailable = false;
93
+ const importSigningKey = () => {
94
+ cryptoKeyPromise ??= subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
95
+ return cryptoKeyPromise;
96
+ };
97
+ return async (unsigned) => {
98
+ if (webCryptoUnavailable) {
99
+ return signChange(unsigned, signingKey);
100
+ }
101
+ const hash4 = computeChangeHash(unsigned);
102
+ try {
103
+ const key = await importSigningKey();
104
+ const signature = new Uint8Array(
105
+ await subtle.sign("Ed25519", key, new TextEncoder().encode(hash4))
106
+ );
107
+ return { ...unsigned, hash: hash4, signature };
108
+ } catch {
109
+ webCryptoUnavailable = true;
110
+ return signChange(unsigned, signingKey);
111
+ }
112
+ };
113
+ }
65
114
  function verifyChange(change, publicKey) {
66
115
  const version = change.protocolVersion ?? 0;
67
116
  if (version > CURRENT_PROTOCOL_VERSION) {
@@ -72,7 +121,7 @@ function verifyChange(change, publicKey) {
72
121
  const hashBytes = new TextEncoder().encode(change.hash);
73
122
  return verify(hashBytes, change.signature, publicKey);
74
123
  }
75
- function verifyChangeHash(change) {
124
+ function recomputeChangeHash(change) {
76
125
  const unsigned = {
77
126
  id: change.id,
78
127
  type: change.type,
@@ -90,8 +139,10 @@ function verifyChangeHash(change) {
90
139
  unsigned.batchIndex = change.batchIndex;
91
140
  unsigned.batchSize = change.batchSize;
92
141
  }
93
- const computedHash = computeChangeHash(unsigned);
94
- return computedHash === change.hash;
142
+ return computeChangeHash(unsigned);
143
+ }
144
+ function verifyChangeHash(change) {
145
+ return recomputeChangeHash(change) === change.hash;
95
146
  }
96
147
  function createChangeId() {
97
148
  return crypto.randomUUID();
@@ -116,9 +167,8 @@ function receive(clock, receivedTime) {
116
167
  function compareLamportTimestamps(a, b) {
117
168
  if (a.time < b.time) return -1;
118
169
  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;
170
+ if (a.author < b.author) return -1;
171
+ if (a.author > b.author) return 1;
122
172
  return 0;
123
173
  }
124
174
  function isBefore(a, b) {
@@ -150,6 +200,11 @@ function maxTime(timestamps) {
150
200
  }
151
201
 
152
202
  // src/chain.ts
203
+ function compareChangeOrder(a, b) {
204
+ if (a.lamport !== b.lamport) return a.lamport - b.lamport;
205
+ if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
206
+ return a.authorDID < b.authorDID ? -1 : a.authorDID > b.authorDID ? 1 : 0;
207
+ }
153
208
  function validateChain(changes) {
154
209
  if (changes.length === 0) {
155
210
  return { valid: true };
@@ -252,7 +307,7 @@ function getForks(changes) {
252
307
  for (const forkPoint of forkPoints) {
253
308
  const children = changes.filter((c) => c.parentHash === forkPoint);
254
309
  if (children.length >= 2) {
255
- children.sort((a, b) => compareLamportTimestamps(a.lamport, b.lamport));
310
+ children.sort(compareChangeOrder);
256
311
  forks.push({
257
312
  commonAncestor: forkPoint,
258
313
  branch1: [children[0]],
@@ -286,7 +341,7 @@ function topologicalSort(changes) {
286
341
  visited.add(change.hash);
287
342
  sorted.push(change);
288
343
  }
289
- const sortedInput = [...changes].sort((a, b) => compareLamportTimestamps(a.lamport, b.lamport));
344
+ const sortedInput = [...changes].sort(compareChangeOrder);
290
345
  for (const change of sortedInput) {
291
346
  visit(change);
292
347
  }
@@ -848,6 +903,222 @@ var BaseSyncProvider = class {
848
903
  }
849
904
  };
850
905
 
906
+ // src/sync-runtime.ts
907
+ function deriveSyncLifecyclePhase(input) {
908
+ if (input.stopped) {
909
+ return "stopped";
910
+ }
911
+ if (!input.started) {
912
+ return "idle";
913
+ }
914
+ if (!input.localReady) {
915
+ return "starting";
916
+ }
917
+ if (input.connectionStatus === "connected") {
918
+ return input.replaying ? "replaying" : "healthy";
919
+ }
920
+ if (input.connectionStatus === "connecting") {
921
+ return "connecting";
922
+ }
923
+ if (input.connectionStatus === "disconnected") {
924
+ return input.everConnected ? "degraded" : "local-ready";
925
+ }
926
+ return "degraded";
927
+ }
928
+ function createSyncLifecycleState(input, previous) {
929
+ const replaying = input.replaying ?? false;
930
+ const phase = deriveSyncLifecyclePhase({
931
+ ...input,
932
+ replaying
933
+ });
934
+ const changed = !previous || previous.phase !== phase || previous.connectionStatus !== input.connectionStatus || previous.replaying !== replaying;
935
+ return {
936
+ phase,
937
+ connectionStatus: input.connectionStatus,
938
+ replaying,
939
+ lastTransitionAt: changed ? Date.now() : previous.lastTransitionAt
940
+ };
941
+ }
942
+
943
+ // src/replication-policy.ts
944
+ function resolveSyncReplicationPolicy(config) {
945
+ const allowUnsignedReplication = config?.compatibility?.allowUnsignedReplication === true;
946
+ return {
947
+ allowUnsignedReplication,
948
+ requireSignedReplication: !allowUnsignedReplication
949
+ };
950
+ }
951
+ function inferReplicationNamespaceKind(namespace) {
952
+ const normalized = namespace.trim().toLowerCase();
953
+ return normalized.startsWith("sys/") || normalized.includes("/sys/") ? "system" : "user";
954
+ }
955
+ function normalizeSyncFederationHubs(config, fallbackUrls = []) {
956
+ const configured = config?.federation?.hubs ?? [];
957
+ const fallback = fallbackUrls.map((url, index) => ({
958
+ id: url,
959
+ url,
960
+ priority: configured.length + index
961
+ }));
962
+ const seen = /* @__PURE__ */ new Set();
963
+ return [...configured, ...fallback].map((hub, index) => ({
964
+ ...hub,
965
+ id: hub.id.trim(),
966
+ url: hub.url.trim(),
967
+ priority: hub.priority ?? index
968
+ })).filter((hub) => {
969
+ if (!hub.id || !hub.url || seen.has(hub.id)) return false;
970
+ seen.add(hub.id);
971
+ return true;
972
+ });
973
+ }
974
+ function planReplicationDestinations(input) {
975
+ const namespace = input.namespace.trim();
976
+ const hubs = normalizeSyncFederationHubs(input.config, input.fallbackHubUrls);
977
+ const policy = selectNamespacePolicy(input.config?.federation?.namespacePolicies ?? [], namespace);
978
+ const kind = policy?.kind ?? inferReplicationNamespaceKind(namespace);
979
+ const defaultHubIds = kind === "system" ? input.config?.federation?.defaultSystemHubIds : input.config?.federation?.defaultUserHubIds;
980
+ const includeHubIds = policy?.includeHubIds ?? defaultHubIds;
981
+ const excludeHubIds = new Set(policy?.excludeHubIds ?? []);
982
+ const diagnostics = [];
983
+ const trace = [
984
+ {
985
+ step: "classify",
986
+ namespace,
987
+ message: `Classified namespace as ${kind}.`
988
+ }
989
+ ];
990
+ if (hubs.length === 0) {
991
+ diagnostics.push({
992
+ code: "no_hubs_configured",
993
+ message: "No federation hubs were configured."
994
+ });
995
+ }
996
+ const byId = new Map(hubs.map((hub) => [hub.id, hub]));
997
+ if (includeHubIds) {
998
+ for (const hubId of includeHubIds) {
999
+ if (!byId.has(hubId)) {
1000
+ diagnostics.push({
1001
+ code: "policy_hub_not_found",
1002
+ hubId,
1003
+ message: `Policy references unknown hub "${hubId}".`
1004
+ });
1005
+ }
1006
+ }
1007
+ }
1008
+ const candidateHubs = includeHubIds ? includeHubIds.flatMap((hubId) => {
1009
+ const hub = byId.get(hubId);
1010
+ return hub ? [hub] : [];
1011
+ }) : hubs;
1012
+ const destinations = candidateHubs.flatMap((hub) => {
1013
+ if (hub.disabled) {
1014
+ diagnostics.push({
1015
+ code: "hub_disabled",
1016
+ hubId: hub.id,
1017
+ message: `Hub "${hub.id}" is disabled.`
1018
+ });
1019
+ trace.push({
1020
+ step: "reject-hub",
1021
+ hubId: hub.id,
1022
+ namespace,
1023
+ message: "Rejected disabled hub."
1024
+ });
1025
+ return [];
1026
+ }
1027
+ if (excludeHubIds.has(hub.id)) {
1028
+ trace.push({
1029
+ step: "exclude-hub",
1030
+ hubId: hub.id,
1031
+ namespace,
1032
+ message: "Excluded by namespace policy."
1033
+ });
1034
+ return [];
1035
+ }
1036
+ if (hub.kinds && !hub.kinds.includes(kind)) {
1037
+ diagnostics.push({
1038
+ code: "hub_kind_mismatch",
1039
+ hubId: hub.id,
1040
+ message: `Hub "${hub.id}" does not accept ${kind} namespaces.`
1041
+ });
1042
+ trace.push({
1043
+ step: "reject-hub",
1044
+ hubId: hub.id,
1045
+ namespace,
1046
+ message: `Rejected because hub does not accept ${kind} namespaces.`
1047
+ });
1048
+ return [];
1049
+ }
1050
+ return [
1051
+ {
1052
+ hubId: hub.id,
1053
+ url: hub.url,
1054
+ priority: hub.priority ?? 0,
1055
+ reason: policy ? `matched ${policy.namespace}` : "default federation policy"
1056
+ }
1057
+ ];
1058
+ }).sort(compareDestinations);
1059
+ const maxHubs = policy?.maxHubs;
1060
+ const selected = maxHubs && maxHubs >= 0 ? destinations.slice(0, maxHubs) : destinations;
1061
+ const minHubs = policy?.minHubs;
1062
+ if (minHubs && selected.length < minHubs) {
1063
+ diagnostics.push({
1064
+ code: "minimum_hubs_not_satisfied",
1065
+ message: `Policy requires ${minHubs} hub(s), but only ${selected.length} matched.`
1066
+ });
1067
+ }
1068
+ trace.push({
1069
+ step: "select",
1070
+ namespace,
1071
+ message: `Selected ${selected.length} destination hub(s).`
1072
+ });
1073
+ return {
1074
+ namespace,
1075
+ kind,
1076
+ policy: policy ?? null,
1077
+ destinations: selected,
1078
+ diagnostics,
1079
+ trace
1080
+ };
1081
+ }
1082
+ function simulateSyncPolicyRevision(input) {
1083
+ const before = planReplicationDestinations({
1084
+ namespace: input.namespace,
1085
+ config: input.current,
1086
+ fallbackHubUrls: input.fallbackHubUrls
1087
+ });
1088
+ const after = planReplicationDestinations({
1089
+ namespace: input.namespace,
1090
+ config: input.revision,
1091
+ fallbackHubUrls: input.fallbackHubUrls
1092
+ });
1093
+ const beforeIds = new Set(before.destinations.map((destination) => destination.hubId));
1094
+ const afterIds = new Set(after.destinations.map((destination) => destination.hubId));
1095
+ const addedHubIds = after.destinations.map((destination) => destination.hubId).filter((hubId) => !beforeIds.has(hubId));
1096
+ const removedHubIds = before.destinations.map((destination) => destination.hubId).filter((hubId) => !afterIds.has(hubId));
1097
+ const retainedHubIds = after.destinations.map((destination) => destination.hubId).filter((hubId) => beforeIds.has(hubId));
1098
+ return {
1099
+ before,
1100
+ after,
1101
+ addedHubIds,
1102
+ removedHubIds,
1103
+ retainedHubIds,
1104
+ changed: addedHubIds.length > 0 || removedHubIds.length > 0
1105
+ };
1106
+ }
1107
+ function selectNamespacePolicy(policies, namespace) {
1108
+ return policies.filter((policy) => namespaceMatches(policy.namespace, namespace)).sort(
1109
+ (left, right) => right.namespace.length - left.namespace.length || compareText(left.namespace, right.namespace)
1110
+ )[0];
1111
+ }
1112
+ function namespaceMatches(policyNamespace, namespace) {
1113
+ return policyNamespace === "*" || namespace === policyNamespace || namespace.startsWith(policyNamespace);
1114
+ }
1115
+ function compareDestinations(left, right) {
1116
+ return left.priority - right.priority || compareText(left.hubId, right.hubId);
1117
+ }
1118
+ function compareText(left, right) {
1119
+ return left < right ? -1 : left > right ? 1 : 0;
1120
+ }
1121
+
851
1122
  // src/yjs-envelope.ts
852
1123
  import {
853
1124
  hash,
@@ -1056,6 +1327,8 @@ function isLegacyUpdate(msg) {
1056
1327
 
1057
1328
  // src/yjs-limits.ts
1058
1329
  var MAX_YJS_UPDATE_SIZE = 1048576;
1330
+ var MAX_YJS_STATE_VECTOR_SIZE = 65536;
1331
+ var MAX_YJS_AWARENESS_UPDATE_SIZE = 65536;
1059
1332
  var MAX_YJS_UPDATES_PER_SECOND = 30;
1060
1333
  var MAX_YJS_UPDATES_PER_MINUTE = 600;
1061
1334
  var MAX_YJS_DOC_SIZE = 52428800;
@@ -1186,6 +1459,21 @@ var YjsRateLimiter = class {
1186
1459
  function isUpdateTooLarge(update, maxSize = MAX_YJS_UPDATE_SIZE) {
1187
1460
  return update.length > maxSize;
1188
1461
  }
1462
+ function estimateBase64DecodedLength(value) {
1463
+ const trimmed = value.trim();
1464
+ if (trimmed.length === 0) return 0;
1465
+ const padding = trimmed.endsWith("==") ? 2 : trimmed.endsWith("=") ? 1 : 0;
1466
+ return Math.max(0, Math.floor(trimmed.length * 3 / 4) - padding);
1467
+ }
1468
+ function isBase64PayloadTooLarge(value, maxSize) {
1469
+ return estimateBase64DecodedLength(value) > maxSize;
1470
+ }
1471
+ function isStateVectorTooLarge(stateVector, maxSize = MAX_YJS_STATE_VECTOR_SIZE) {
1472
+ return stateVector.length > maxSize;
1473
+ }
1474
+ function isAwarenessUpdateTooLarge(update, maxSize = MAX_YJS_AWARENESS_UPDATE_SIZE) {
1475
+ return update.length > maxSize;
1476
+ }
1189
1477
  function isDocumentTooLarge(state, maxSize = MAX_YJS_DOC_SIZE) {
1190
1478
  return state.length > maxSize;
1191
1479
  }
@@ -1287,6 +1575,7 @@ var YjsPeerScorer = class {
1287
1575
  scores = /* @__PURE__ */ new Map();
1288
1576
  config;
1289
1577
  telemetry;
1578
+ actionListeners = /* @__PURE__ */ new Set();
1290
1579
  constructor(config) {
1291
1580
  this.config = {
1292
1581
  penalties: { ...DEFAULT_YJS_SCORING_CONFIG.penalties, ...config?.penalties },
@@ -1315,6 +1604,7 @@ var YjsPeerScorer = class {
1315
1604
  this.scores.set(peerId, 0);
1316
1605
  this.telemetry?.reportSecurityEvent("sync.yjs.peer_auto_blocked", "critical");
1317
1606
  this.telemetry?.reportUsage("sync.yjs.peer_action.block", 1);
1607
+ this.emitAction({ peerId, reason, action: "block", score: 0, metrics });
1318
1608
  return "block";
1319
1609
  }
1320
1610
  break;
@@ -1347,6 +1637,7 @@ var YjsPeerScorer = class {
1347
1637
  if (action !== "allow") {
1348
1638
  this.telemetry?.reportUsage(`sync.yjs.peer_action.${action}`, 1);
1349
1639
  }
1640
+ this.emitAction({ peerId, reason, action, score: newScore, metrics });
1350
1641
  return action;
1351
1642
  }
1352
1643
  /**
@@ -1415,6 +1706,17 @@ var YjsPeerScorer = class {
1415
1706
  getAllMetrics() {
1416
1707
  return new Map(this.metrics);
1417
1708
  }
1709
+ /**
1710
+ * Listen for peer actions caused by violations.
1711
+ *
1712
+ * Returns an unsubscribe callback so owning packages can cleanly detach bridges.
1713
+ */
1714
+ onAction(listener) {
1715
+ this.actionListeners.add(listener);
1716
+ return () => {
1717
+ this.actionListeners.delete(listener);
1718
+ };
1719
+ }
1418
1720
  /**
1419
1721
  * Remove all state for a disconnected peer.
1420
1722
  */
@@ -1422,6 +1724,14 @@ var YjsPeerScorer = class {
1422
1724
  this.metrics.delete(peerId);
1423
1725
  this.scores.delete(peerId);
1424
1726
  }
1727
+ emitAction(event) {
1728
+ for (const listener of this.actionListeners) {
1729
+ try {
1730
+ listener(event);
1731
+ } catch {
1732
+ }
1733
+ }
1734
+ }
1425
1735
  /**
1426
1736
  * Clear all state.
1427
1737
  */
@@ -2253,7 +2563,7 @@ var V1Serializer = class {
2253
2563
  authorDID: change.authorDID,
2254
2564
  signature: encodeBase64(change.signature),
2255
2565
  wallTime: change.wallTime,
2256
- lamport: { time: change.lamport.time, author: change.lamport.author }
2566
+ lamport: change.lamport
2257
2567
  };
2258
2568
  if (change.protocolVersion !== void 0) {
2259
2569
  wire.protocolVersion = change.protocolVersion;
@@ -2281,7 +2591,7 @@ var V1Serializer = class {
2281
2591
  rawData: data
2282
2592
  };
2283
2593
  }
2284
- if (!wire.lamport || typeof wire.lamport.time !== "number" || !wire.lamport.author) {
2594
+ if (typeof wire.lamport !== "number") {
2285
2595
  return {
2286
2596
  success: false,
2287
2597
  error: "Invalid or missing lamport timestamp",
@@ -2297,10 +2607,7 @@ var V1Serializer = class {
2297
2607
  authorDID: wire.authorDID,
2298
2608
  signature: decodeBase64(wire.signature),
2299
2609
  wallTime: wire.wallTime,
2300
- lamport: {
2301
- time: wire.lamport.time,
2302
- author: wire.lamport.author
2303
- }
2610
+ lamport: wire.lamport
2304
2611
  };
2305
2612
  if (wire.protocolVersion !== void 0) {
2306
2613
  change.protocolVersion = wire.protocolVersion;
@@ -2368,7 +2675,7 @@ var V2Serializer = class {
2368
2675
  a: change.authorDID,
2369
2676
  s: encodeBase642(change.signature),
2370
2677
  w: change.wallTime,
2371
- l: { t: change.lamport.time, a: change.lamport.author }
2678
+ l: change.lamport
2372
2679
  };
2373
2680
  if (change.batchId !== void 0) {
2374
2681
  wire.bi = change.batchId;
@@ -2400,7 +2707,7 @@ var V2Serializer = class {
2400
2707
  rawData: data
2401
2708
  };
2402
2709
  }
2403
- if (!wire.l || typeof wire.l.t !== "number" || !wire.l.a) {
2710
+ if (typeof wire.l !== "number") {
2404
2711
  return {
2405
2712
  success: false,
2406
2713
  error: "Invalid or missing lamport timestamp",
@@ -2417,10 +2724,7 @@ var V2Serializer = class {
2417
2724
  authorDID: wire.a,
2418
2725
  signature: decodeBase642(wire.s),
2419
2726
  wallTime: wire.w,
2420
- lamport: {
2421
- time: wire.l.t,
2422
- author: wire.l.a
2423
- }
2727
+ lamport: wire.l
2424
2728
  };
2425
2729
  if (wire.bi !== void 0) {
2426
2730
  change.batchId = wire.bi;
@@ -2503,7 +2807,7 @@ var V3Serializer = class {
2503
2807
  a: change.authorDID,
2504
2808
  sig,
2505
2809
  w: change.wallTime,
2506
- l: { t: change.lamport.time, a: change.lamport.author }
2810
+ l: change.lamport
2507
2811
  };
2508
2812
  if (change.batchId !== void 0) {
2509
2813
  wire.bi = change.batchId;
@@ -2542,7 +2846,7 @@ var V3Serializer = class {
2542
2846
  rawData: data
2543
2847
  };
2544
2848
  }
2545
- if (!wire.l || typeof wire.l.t !== "number" || !wire.l.a) {
2849
+ if (typeof wire.l !== "number") {
2546
2850
  return {
2547
2851
  success: false,
2548
2852
  error: "Invalid or missing lamport timestamp",
@@ -2561,10 +2865,7 @@ var V3Serializer = class {
2561
2865
  signature,
2562
2866
  // Type cast for compatibility
2563
2867
  wallTime: wire.w,
2564
- lamport: {
2565
- time: wire.l.t,
2566
- author: wire.l.a
2567
- }
2868
+ lamport: wire.l
2568
2869
  };
2569
2870
  if (wire.bi !== void 0) {
2570
2871
  change.batchId = wire.bi;
@@ -2928,11 +3229,11 @@ async function verifyIntegrity(changes, options = {}) {
2928
3229
  hasIssue = true;
2929
3230
  }
2930
3231
  }
2931
- if (change.lamport.time < 0) {
3232
+ if (change.lamport < 0) {
2932
3233
  issues.push({
2933
3234
  changeId: change.id,
2934
3235
  type: "invalid-lamport",
2935
- details: `Invalid Lamport time: ${change.lamport.time}`,
3236
+ details: `Invalid Lamport time: ${change.lamport}`,
2936
3237
  severity: "error"
2937
3238
  });
2938
3239
  hasIssue = true;
@@ -3560,7 +3861,9 @@ export {
3560
3861
  FEATURES,
3561
3862
  HYBRID_SECURITY_POLICY,
3562
3863
  MAX_SECURITY_POLICY,
3864
+ MAX_YJS_AWARENESS_UPDATE_SIZE,
3563
3865
  MAX_YJS_DOC_SIZE,
3866
+ MAX_YJS_STATE_VECTOR_SIZE,
3564
3867
  MAX_YJS_UPDATES_PER_MINUTE,
3565
3868
  MAX_YJS_UPDATES_PER_SECOND,
3566
3869
  MAX_YJS_UPDATE_SIZE,
@@ -3603,13 +3906,16 @@ export {
3603
3906
  createReactIntegrityMonitor,
3604
3907
  createSecurityPolicy,
3605
3908
  createSerializerRegistry,
3909
+ createSyncLifecycleState,
3606
3910
  createTestContext,
3607
3911
  createUnsignedChange,
3608
3912
  createUnsignedYjsChange,
3609
3913
  createVersionedHandler,
3914
+ createWebCryptoChangeSigner,
3610
3915
  createYjsChange,
3611
3916
  decryptYjsState,
3612
3917
  defaultNegotiator,
3918
+ deriveSyncLifecyclePhase,
3613
3919
  deserializeClientIdAttestation,
3614
3920
  deserializeEncryptedYjsState,
3615
3921
  deserializeYjsEnvelope,
@@ -3617,6 +3923,7 @@ export {
3617
3923
  diffFeatures,
3618
3924
  encryptYjsState,
3619
3925
  envelopeSize,
3926
+ estimateBase64DecodedLength,
3620
3927
  findCommonAncestor,
3621
3928
  findHeads,
3622
3929
  findOrphans,
@@ -3643,8 +3950,11 @@ export {
3643
3950
  getSerializer,
3644
3951
  hasSignedEnvelope,
3645
3952
  hashYjsState,
3953
+ inferReplicationNamespaceKind,
3646
3954
  intersectFeatures,
3647
3955
  isAfter,
3956
+ isAwarenessUpdateTooLarge,
3957
+ isBase64PayloadTooLarge,
3648
3958
  isBefore,
3649
3959
  isCriticalOperation,
3650
3960
  isDeprecated,
@@ -3655,6 +3965,7 @@ export {
3655
3965
  isLegacyUpdate,
3656
3966
  isNodeChange,
3657
3967
  isRemoved,
3968
+ isStateVectorTooLarge,
3658
3969
  isUpdateTooLarge,
3659
3970
  isV1Attestation,
3660
3971
  isV1Envelope,
@@ -3665,12 +3976,16 @@ export {
3665
3976
  logDeprecation,
3666
3977
  maxTime,
3667
3978
  mergeSecurityPolicies,
3979
+ normalizeSyncFederationHubs,
3668
3980
  parseCapabilities,
3669
3981
  parseTimestamp,
3982
+ planReplicationDestinations,
3670
3983
  quickIntegrityCheck,
3671
3984
  reassembleChunks,
3672
3985
  receive,
3986
+ recomputeChangeHash,
3673
3987
  registerDeprecation,
3988
+ resolveSyncReplicationPolicy,
3674
3989
  serializeClientIdAttestation,
3675
3990
  serializeEncryptedYjsState,
3676
3991
  serializeTimestamp,
@@ -3682,6 +3997,7 @@ export {
3682
3997
  signYjsUpdateBatch,
3683
3998
  signYjsUpdateV1,
3684
3999
  signYjsUpdateV2,
4000
+ simulateSyncPolicyRevision,
3685
4001
  tick,
3686
4002
  toEncryptedData,
3687
4003
  topologicalSort,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/sync",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Unified sync primitives for xNet (Change<T>, vector clocks, hash chains)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  "provenance": true
27
27
  },
28
28
  "dependencies": {
29
- "@xnetjs/core": "0.0.2",
30
- "@xnetjs/crypto": "0.0.2",
31
- "@xnetjs/identity": "0.0.2"
29
+ "@xnetjs/core": "0.0.3",
30
+ "@xnetjs/crypto": "0.0.3",
31
+ "@xnetjs/identity": "0.0.3"
32
32
  },
33
33
  "devDependencies": {
34
34
  "tsup": "^8.0.0",