@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.
- package/dist/index.d.ts +308 -101
- package/dist/index.js +344 -28
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,98 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
-
/**
|
|
153
|
-
|
|
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:
|
|
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:
|
|
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
|
|
2035
|
-
lamport:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
120
|
-
if (
|
|
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(
|
|
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(
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
30
|
-
"@xnetjs/crypto": "0.0.
|
|
31
|
-
"@xnetjs/identity": "0.0.
|
|
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",
|