@xnetjs/sync 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3203 -0
- package/dist/index.js +3706 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3203 @@
|
|
|
1
|
+
import { DID, ContentId, PolicyEvaluator, AuthDecision } from '@xnetjs/core';
|
|
2
|
+
import { UnifiedSignature, SignatureWire, SecurityLevel, EncryptedData, WrappedKey } from '@xnetjs/crypto';
|
|
3
|
+
import { PQKeyRegistry, HybridKeyBundle } from '@xnetjs/identity';
|
|
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
|
+
/**
|
|
97
|
+
* Change types for xNet sync primitives
|
|
98
|
+
*
|
|
99
|
+
* Change<T> is the universal unit of sync for both Yjs (CRDT) and
|
|
100
|
+
* event-sourced (records) data. It replaces SignedUpdate and RecordOperation
|
|
101
|
+
* with a single, generic type.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Current protocol version for Change<T>.
|
|
106
|
+
*
|
|
107
|
+
* Version history:
|
|
108
|
+
* - 3: Multi-level cryptography with hybrid signatures (Ed25519 + ML-DSA)
|
|
109
|
+
* - 2: V2 compact format with abbreviated field names
|
|
110
|
+
* - 1: Initial versioned protocol (adds protocolVersion field)
|
|
111
|
+
* - 0/undefined: Legacy unversioned changes (backward compat)
|
|
112
|
+
*/
|
|
113
|
+
declare const CURRENT_PROTOCOL_VERSION = 3;
|
|
114
|
+
/**
|
|
115
|
+
* A signed change with chain linkage and Lamport ordering.
|
|
116
|
+
* Generic T allows different payload types for different use cases:
|
|
117
|
+
* - YjsUpdate for rich text documents
|
|
118
|
+
* - RecordPayload for database operations
|
|
119
|
+
*
|
|
120
|
+
* Changes can optionally be part of a transaction batch, which groups
|
|
121
|
+
* related changes that should be applied atomically. This is important for:
|
|
122
|
+
* - Multi-node operations (move task between projects)
|
|
123
|
+
* - Undo/redo grouping
|
|
124
|
+
* - Audit trails ("user did X" as a single action)
|
|
125
|
+
* - Future blockchain integration (batch = transaction)
|
|
126
|
+
*/
|
|
127
|
+
interface Change<T = unknown> {
|
|
128
|
+
/**
|
|
129
|
+
* Protocol version of this change.
|
|
130
|
+
* - undefined: Legacy change (protocol v0, backward compat)
|
|
131
|
+
* - 1+: Versioned change with protocol version
|
|
132
|
+
*
|
|
133
|
+
* Used for version negotiation and migration paths.
|
|
134
|
+
*/
|
|
135
|
+
protocolVersion?: number;
|
|
136
|
+
/** Unique change ID (nanoid) */
|
|
137
|
+
id: string;
|
|
138
|
+
/** Change type (e.g., 'yjs-update', 'create-item', 'update-item') */
|
|
139
|
+
type: string;
|
|
140
|
+
/** The actual change data */
|
|
141
|
+
payload: T;
|
|
142
|
+
/** Content-addressed hash of this change */
|
|
143
|
+
hash: ContentId;
|
|
144
|
+
/** Hash of the previous change in the chain (null for first) */
|
|
145
|
+
parentHash: ContentId | null;
|
|
146
|
+
/** DID of the author */
|
|
147
|
+
authorDID: DID;
|
|
148
|
+
/** Ed25519 signature of the hash */
|
|
149
|
+
signature: Uint8Array;
|
|
150
|
+
/** Wall clock timestamp (milliseconds) - for display, not ordering */
|
|
151
|
+
wallTime: number;
|
|
152
|
+
/** Lamport timestamp for ordering */
|
|
153
|
+
lamport: LamportTimestamp;
|
|
154
|
+
/**
|
|
155
|
+
* Groups changes that should be treated as a single atomic operation.
|
|
156
|
+
* All changes with the same batchId were created in one transaction.
|
|
157
|
+
* For undo/redo, the entire batch should be undone/redone together.
|
|
158
|
+
*/
|
|
159
|
+
batchId?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Position of this change within the batch (0-indexed).
|
|
162
|
+
* Ensures deterministic ordering when replaying.
|
|
163
|
+
*/
|
|
164
|
+
batchIndex?: number;
|
|
165
|
+
/**
|
|
166
|
+
* Total number of changes in the batch.
|
|
167
|
+
* Receivers can wait for all changes before committing.
|
|
168
|
+
*/
|
|
169
|
+
batchSize?: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* An unsigned change (before hash and signature are computed).
|
|
173
|
+
* Used as input to signChange().
|
|
174
|
+
*/
|
|
175
|
+
interface UnsignedChange<T = unknown> {
|
|
176
|
+
protocolVersion?: number;
|
|
177
|
+
id: string;
|
|
178
|
+
type: string;
|
|
179
|
+
payload: T;
|
|
180
|
+
parentHash: ContentId | null;
|
|
181
|
+
authorDID: DID;
|
|
182
|
+
wallTime: number;
|
|
183
|
+
lamport: LamportTimestamp;
|
|
184
|
+
batchId?: string;
|
|
185
|
+
batchIndex?: number;
|
|
186
|
+
batchSize?: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Options for creating a change
|
|
190
|
+
*/
|
|
191
|
+
interface CreateChangeOptions<T> {
|
|
192
|
+
id: string;
|
|
193
|
+
type: string;
|
|
194
|
+
payload: T;
|
|
195
|
+
parentHash: ContentId | null;
|
|
196
|
+
authorDID: DID;
|
|
197
|
+
lamport: LamportTimestamp;
|
|
198
|
+
wallTime?: number;
|
|
199
|
+
batchId?: string;
|
|
200
|
+
batchIndex?: number;
|
|
201
|
+
batchSize?: number;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Create an unsigned change from options
|
|
205
|
+
*/
|
|
206
|
+
declare function createUnsignedChange<T>(options: CreateChangeOptions<T>): UnsignedChange<T>;
|
|
207
|
+
/**
|
|
208
|
+
* Generate a unique batch ID for grouping changes in a transaction.
|
|
209
|
+
*/
|
|
210
|
+
declare function createBatchId(): string;
|
|
211
|
+
/**
|
|
212
|
+
* Compute the hash of an unsigned change.
|
|
213
|
+
* The hash is computed over a canonical JSON representation with sorted keys.
|
|
214
|
+
*
|
|
215
|
+
* Version handling:
|
|
216
|
+
* - protocolVersion 0/undefined (legacy): hash without protocolVersion field
|
|
217
|
+
* - protocolVersion 1+: include protocolVersion in hash computation
|
|
218
|
+
*/
|
|
219
|
+
declare function computeChangeHash<T>(unsigned: UnsignedChange<T>): ContentId;
|
|
220
|
+
/**
|
|
221
|
+
* Sign an unsigned change, producing a fully signed Change<T>.
|
|
222
|
+
*
|
|
223
|
+
* @param unsigned - The change to sign
|
|
224
|
+
* @param signingKey - Ed25519 private key (32 bytes)
|
|
225
|
+
* @returns Signed change with hash and signature
|
|
226
|
+
*/
|
|
227
|
+
declare function signChange<T>(unsigned: UnsignedChange<T>, signingKey: Uint8Array): Change<T>;
|
|
228
|
+
/**
|
|
229
|
+
* Verify a change's signature against a public key.
|
|
230
|
+
*
|
|
231
|
+
* Protocol version handling:
|
|
232
|
+
* - Accepts changes with protocolVersion <= CURRENT_PROTOCOL_VERSION
|
|
233
|
+
* - Logs warning for future versions but still attempts verification
|
|
234
|
+
* - Never rejects based on version alone (graceful degradation)
|
|
235
|
+
*
|
|
236
|
+
* @param change - The change to verify
|
|
237
|
+
* @param publicKey - Ed25519 public key (32 bytes)
|
|
238
|
+
* @returns true if the signature is valid
|
|
239
|
+
*/
|
|
240
|
+
declare function verifyChange<T>(change: Change<T>, publicKey: Uint8Array): boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Verify that a change's hash is correct (not tampered).
|
|
243
|
+
* This re-computes the hash from the change data and compares.
|
|
244
|
+
*
|
|
245
|
+
* Handles both legacy (no protocolVersion) and versioned changes.
|
|
246
|
+
*/
|
|
247
|
+
declare function verifyChangeHash<T>(change: Change<T>): boolean;
|
|
248
|
+
/**
|
|
249
|
+
* Create a unique change ID.
|
|
250
|
+
* Uses crypto.randomUUID for uniqueness.
|
|
251
|
+
*/
|
|
252
|
+
declare function createChangeId(): string;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Hash chain utilities for managing linked changes.
|
|
256
|
+
*
|
|
257
|
+
* Changes form a hash chain where each change references its parent via
|
|
258
|
+
* parentHash. This enables:
|
|
259
|
+
* - Detecting forks (two changes with the same parent)
|
|
260
|
+
* - Validating chain integrity
|
|
261
|
+
* - Finding chain heads (latest changes)
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Result of validating a chain of changes
|
|
266
|
+
*/
|
|
267
|
+
interface ChainValidationResult {
|
|
268
|
+
/** Whether the chain is valid */
|
|
269
|
+
valid: boolean;
|
|
270
|
+
/** Error message if invalid */
|
|
271
|
+
error?: string;
|
|
272
|
+
/** Whether a fork was detected */
|
|
273
|
+
forkDetected?: boolean;
|
|
274
|
+
/** Hash where the fork occurred */
|
|
275
|
+
forkPoint?: ContentId;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Information about a fork in the chain
|
|
279
|
+
*/
|
|
280
|
+
interface Fork<T = unknown> {
|
|
281
|
+
/** The common ancestor hash where the fork occurred */
|
|
282
|
+
commonAncestor: ContentId;
|
|
283
|
+
/** Changes in the first branch (by Lamport timestamp) */
|
|
284
|
+
branch1: Change<T>[];
|
|
285
|
+
/** Changes in the second branch (by Lamport timestamp) */
|
|
286
|
+
branch2: Change<T>[];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Validate that a list of changes forms a valid hash chain.
|
|
290
|
+
* Checks:
|
|
291
|
+
* - All hashes are computed correctly (not tampered)
|
|
292
|
+
* - Parent references exist in the chain (or are null for roots)
|
|
293
|
+
*
|
|
294
|
+
* @param changes - The changes to validate
|
|
295
|
+
* @returns Validation result
|
|
296
|
+
*/
|
|
297
|
+
declare function validateChain<T>(changes: Change<T>[]): ChainValidationResult;
|
|
298
|
+
/**
|
|
299
|
+
* Detect if there are any forks in the chain.
|
|
300
|
+
* A fork occurs when two different changes have the same parent.
|
|
301
|
+
*
|
|
302
|
+
* @param changes - The changes to check
|
|
303
|
+
* @returns Fork detection result
|
|
304
|
+
*/
|
|
305
|
+
declare function detectFork<T>(changes: Change<T>[]): {
|
|
306
|
+
hasFork: boolean;
|
|
307
|
+
forkPoints: ContentId[];
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Get the head(s) of the chain - changes that are not parents of any other change.
|
|
311
|
+
* Multiple heads indicate a fork that needs resolution.
|
|
312
|
+
*
|
|
313
|
+
* @param changes - The changes to analyze
|
|
314
|
+
* @returns Array of head changes
|
|
315
|
+
*/
|
|
316
|
+
declare function getChainHeads<T>(changes: Change<T>[]): Change<T>[];
|
|
317
|
+
/**
|
|
318
|
+
* Get the root(s) of the chain - changes with no parent (parentHash is null).
|
|
319
|
+
*
|
|
320
|
+
* @param changes - The changes to analyze
|
|
321
|
+
* @returns Array of root changes
|
|
322
|
+
*/
|
|
323
|
+
declare function getChainRoots<T>(changes: Change<T>[]): Change<T>[];
|
|
324
|
+
/**
|
|
325
|
+
* Get the full ancestry of a change (all changes leading up to it).
|
|
326
|
+
*
|
|
327
|
+
* @param change - The change to get ancestry for
|
|
328
|
+
* @param allChanges - All available changes
|
|
329
|
+
* @returns Array of ancestor changes, from oldest to newest (excluding the input change)
|
|
330
|
+
*/
|
|
331
|
+
declare function getAncestry<T>(change: Change<T>, allChanges: Change<T>[]): Change<T>[];
|
|
332
|
+
/**
|
|
333
|
+
* Find the common ancestor of two changes.
|
|
334
|
+
* Returns null if they have no common ancestor.
|
|
335
|
+
*
|
|
336
|
+
* @param a - First change
|
|
337
|
+
* @param b - Second change
|
|
338
|
+
* @param allChanges - All available changes
|
|
339
|
+
* @returns The common ancestor change, or null
|
|
340
|
+
*/
|
|
341
|
+
declare function findCommonAncestor<T>(a: Change<T>, b: Change<T>, allChanges: Change<T>[]): Change<T> | null;
|
|
342
|
+
/**
|
|
343
|
+
* Get detailed fork information.
|
|
344
|
+
*
|
|
345
|
+
* @param changes - The changes to analyze
|
|
346
|
+
* @returns Array of Fork objects describing each fork
|
|
347
|
+
*/
|
|
348
|
+
declare function getForks<T>(changes: Change<T>[]): Fork<T>[];
|
|
349
|
+
/**
|
|
350
|
+
* Sort changes in topological order (parents before children).
|
|
351
|
+
* Within the same level, sort by Lamport timestamp.
|
|
352
|
+
*
|
|
353
|
+
* @param changes - The changes to sort
|
|
354
|
+
* @returns Sorted changes array
|
|
355
|
+
*/
|
|
356
|
+
declare function topologicalSort<T>(changes: Change<T>[]): Change<T>[];
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Feature flag system for xNet protocol capabilities.
|
|
360
|
+
*
|
|
361
|
+
* Features are capabilities that can be negotiated between peers.
|
|
362
|
+
* Each feature has a protocol version where it was introduced, and may
|
|
363
|
+
* have dependencies on other features.
|
|
364
|
+
*
|
|
365
|
+
* Usage:
|
|
366
|
+
* ```typescript
|
|
367
|
+
* import { FEATURES, getEnabledFeatures, isFeatureEnabled } from '@xnetjs/sync'
|
|
368
|
+
*
|
|
369
|
+
* // Get features for a protocol version
|
|
370
|
+
* const features = getEnabledFeatures(2) // ['node-changes', 'yjs-updates', ...]
|
|
371
|
+
*
|
|
372
|
+
* // Check if a feature is enabled in a negotiated set
|
|
373
|
+
* if (isFeatureEnabled('schema-versioning', negotiatedFeatures)) {
|
|
374
|
+
* // Use schema versioning
|
|
375
|
+
* }
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
/**
|
|
379
|
+
* All known feature flag names.
|
|
380
|
+
* Defined as a union type to allow forward-references in FeatureConfig.
|
|
381
|
+
*/
|
|
382
|
+
type FeatureFlag = 'node-changes' | 'yjs-updates' | 'signed-yjs-envelopes' | 'batch-changes' | 'lamport-ordering' | 'hash-chains' | 'schema-versioning' | 'capability-negotiation' | 'peer-scoring' | 'schema-lenses' | 'unknown-preservation' | 'schema-inheritance' | 'federated-queries' | 'compressed-payloads';
|
|
383
|
+
/**
|
|
384
|
+
* Feature configuration for a protocol capability.
|
|
385
|
+
*/
|
|
386
|
+
interface FeatureConfig {
|
|
387
|
+
/** Protocol version when this feature was introduced */
|
|
388
|
+
since: number;
|
|
389
|
+
/** Whether this feature is required (cannot be disabled) */
|
|
390
|
+
required: boolean;
|
|
391
|
+
/** Human-readable description */
|
|
392
|
+
description: string;
|
|
393
|
+
/** Features this one depends on (must be enabled if this is enabled) */
|
|
394
|
+
requires?: FeatureFlag[];
|
|
395
|
+
/** Features that conflict with this one (cannot both be enabled) */
|
|
396
|
+
conflicts?: FeatureFlag[];
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Registry of all supported features.
|
|
400
|
+
*
|
|
401
|
+
* Features are organized by when they were introduced:
|
|
402
|
+
* - Protocol v1: Core sync primitives
|
|
403
|
+
* - Protocol v2: Schema versioning and capability negotiation
|
|
404
|
+
* - Protocol v3+: Future extensions
|
|
405
|
+
*/
|
|
406
|
+
declare const FEATURES: Record<FeatureFlag, FeatureConfig>;
|
|
407
|
+
/**
|
|
408
|
+
* Array of all feature flag names.
|
|
409
|
+
*/
|
|
410
|
+
declare const ALL_FEATURES: FeatureFlag[];
|
|
411
|
+
/**
|
|
412
|
+
* Get all features enabled at a given protocol version.
|
|
413
|
+
*
|
|
414
|
+
* @param protocolVersion - The protocol version to check
|
|
415
|
+
* @returns Array of feature flags enabled at this version
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* ```typescript
|
|
419
|
+
* const v1Features = getEnabledFeatures(1)
|
|
420
|
+
* // ['node-changes', 'yjs-updates', 'signed-yjs-envelopes', ...]
|
|
421
|
+
*
|
|
422
|
+
* const v2Features = getEnabledFeatures(2)
|
|
423
|
+
* // [...v1Features, 'schema-versioning', 'capability-negotiation', ...]
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
declare function getEnabledFeatures(protocolVersion: number): FeatureFlag[];
|
|
427
|
+
/**
|
|
428
|
+
* Check if a feature is enabled in a negotiated feature set.
|
|
429
|
+
*
|
|
430
|
+
* @param feature - The feature to check
|
|
431
|
+
* @param enabledFeatures - The set of enabled features (from negotiation)
|
|
432
|
+
* @returns true if the feature is in the enabled set
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```typescript
|
|
436
|
+
* const negotiated = ['node-changes', 'yjs-updates', 'schema-versioning']
|
|
437
|
+
* isFeatureEnabled('schema-versioning', negotiated) // true
|
|
438
|
+
* isFeatureEnabled('federated-queries', negotiated) // false
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
declare function isFeatureEnabled(feature: FeatureFlag, enabledFeatures: readonly FeatureFlag[] | readonly string[]): boolean;
|
|
442
|
+
/**
|
|
443
|
+
* Get required features for a protocol version.
|
|
444
|
+
* These features cannot be disabled during negotiation.
|
|
445
|
+
*
|
|
446
|
+
* @param protocolVersion - The protocol version to check
|
|
447
|
+
* @returns Array of required feature flags
|
|
448
|
+
*/
|
|
449
|
+
declare function getRequiredFeatures(protocolVersion: number): FeatureFlag[];
|
|
450
|
+
/**
|
|
451
|
+
* Get optional features for a protocol version.
|
|
452
|
+
* These features can be enabled or disabled during negotiation.
|
|
453
|
+
*
|
|
454
|
+
* @param protocolVersion - The protocol version to check
|
|
455
|
+
* @returns Array of optional feature flags
|
|
456
|
+
*/
|
|
457
|
+
declare function getOptionalFeatures(protocolVersion: number): FeatureFlag[];
|
|
458
|
+
/**
|
|
459
|
+
* Get the minimum protocol version required for a feature.
|
|
460
|
+
*
|
|
461
|
+
* @param feature - The feature to check
|
|
462
|
+
* @returns The protocol version when this feature was introduced
|
|
463
|
+
*/
|
|
464
|
+
declare function getFeatureVersion(feature: FeatureFlag): number;
|
|
465
|
+
/**
|
|
466
|
+
* Check if a feature is available at a given protocol version.
|
|
467
|
+
*
|
|
468
|
+
* @param feature - The feature to check
|
|
469
|
+
* @param protocolVersion - The protocol version
|
|
470
|
+
* @returns true if the feature is available at this version
|
|
471
|
+
*/
|
|
472
|
+
declare function isFeatureAvailable(feature: FeatureFlag, protocolVersion: number): boolean;
|
|
473
|
+
/**
|
|
474
|
+
* Result of validating a feature set.
|
|
475
|
+
*/
|
|
476
|
+
interface FeatureValidationResult {
|
|
477
|
+
valid: boolean;
|
|
478
|
+
errors: FeatureValidationError[];
|
|
479
|
+
warnings: FeatureValidationWarning[];
|
|
480
|
+
}
|
|
481
|
+
interface FeatureValidationError {
|
|
482
|
+
type: 'missing-dependency' | 'conflict' | 'version-mismatch' | 'missing-required';
|
|
483
|
+
feature: FeatureFlag;
|
|
484
|
+
message: string;
|
|
485
|
+
relatedFeature?: FeatureFlag;
|
|
486
|
+
}
|
|
487
|
+
interface FeatureValidationWarning {
|
|
488
|
+
type: 'deprecated' | 'experimental';
|
|
489
|
+
feature: FeatureFlag;
|
|
490
|
+
message: string;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get the dependencies of a feature (features it requires).
|
|
494
|
+
*
|
|
495
|
+
* @param feature - The feature to check
|
|
496
|
+
* @returns Array of required features, or empty array if none
|
|
497
|
+
*/
|
|
498
|
+
declare function getFeatureDependencies(feature: FeatureFlag): FeatureFlag[];
|
|
499
|
+
/**
|
|
500
|
+
* Get features that conflict with a given feature.
|
|
501
|
+
*
|
|
502
|
+
* @param feature - The feature to check
|
|
503
|
+
* @returns Array of conflicting features, or empty array if none
|
|
504
|
+
*/
|
|
505
|
+
declare function getFeatureConflicts(feature: FeatureFlag): FeatureFlag[];
|
|
506
|
+
/**
|
|
507
|
+
* Get all transitive dependencies of a feature (recursive).
|
|
508
|
+
*
|
|
509
|
+
* @param feature - The feature to check
|
|
510
|
+
* @param visited - Set of already-visited features (for cycle detection)
|
|
511
|
+
* @returns Array of all required features (direct and transitive)
|
|
512
|
+
*/
|
|
513
|
+
declare function getAllDependencies(feature: FeatureFlag, visited?: Set<FeatureFlag>): FeatureFlag[];
|
|
514
|
+
/**
|
|
515
|
+
* Validate a set of features for consistency.
|
|
516
|
+
*
|
|
517
|
+
* Checks for:
|
|
518
|
+
* - Missing required dependencies
|
|
519
|
+
* - Conflicting features
|
|
520
|
+
* - Required features that are missing
|
|
521
|
+
* - Version compatibility issues
|
|
522
|
+
*
|
|
523
|
+
* @param features - Array of features to validate
|
|
524
|
+
* @param protocolVersion - The protocol version context
|
|
525
|
+
* @returns Validation result with errors and warnings
|
|
526
|
+
*/
|
|
527
|
+
declare function validateFeatureSet(features: readonly FeatureFlag[], protocolVersion?: number): FeatureValidationResult;
|
|
528
|
+
/**
|
|
529
|
+
* Compute the intersection of two feature sets.
|
|
530
|
+
* Used during negotiation to find common features.
|
|
531
|
+
*
|
|
532
|
+
* @param local - Local peer's features
|
|
533
|
+
* @param remote - Remote peer's features
|
|
534
|
+
* @returns Features present in both sets
|
|
535
|
+
*/
|
|
536
|
+
declare function intersectFeatures(local: readonly FeatureFlag[], remote: readonly FeatureFlag[] | readonly string[]): FeatureFlag[];
|
|
537
|
+
/**
|
|
538
|
+
* Compute the difference between two feature sets.
|
|
539
|
+
* Returns features in `a` that are not in `b`.
|
|
540
|
+
*
|
|
541
|
+
* @param a - First feature set
|
|
542
|
+
* @param b - Second feature set
|
|
543
|
+
* @returns Features in a but not in b
|
|
544
|
+
*/
|
|
545
|
+
declare function diffFeatures(a: readonly FeatureFlag[], b: readonly FeatureFlag[] | readonly string[]): FeatureFlag[];
|
|
546
|
+
/**
|
|
547
|
+
* Add all dependencies to a feature set (closure).
|
|
548
|
+
* Ensures the set is valid by including all required dependencies.
|
|
549
|
+
*
|
|
550
|
+
* @param features - Initial feature set
|
|
551
|
+
* @returns Feature set with all dependencies included
|
|
552
|
+
*/
|
|
553
|
+
declare function addDependencies(features: readonly FeatureFlag[]): FeatureFlag[];
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Version negotiation protocol for xNet peers.
|
|
557
|
+
*
|
|
558
|
+
* When two peers connect, they exchange capability information and
|
|
559
|
+
* negotiate a common set of features they both support. This allows:
|
|
560
|
+
* - Older clients to work with newer servers (backward compatibility)
|
|
561
|
+
* - Newer clients to work with older servers (graceful degradation)
|
|
562
|
+
* - Feature-specific behavior based on negotiated capabilities
|
|
563
|
+
*
|
|
564
|
+
* Usage:
|
|
565
|
+
* ```typescript
|
|
566
|
+
* import { VersionNegotiator, createLocalCapabilities } from '@xnetjs/sync'
|
|
567
|
+
*
|
|
568
|
+
* const negotiator = new VersionNegotiator()
|
|
569
|
+
* const local = createLocalCapabilities('did:key:z...', ['node-changes', 'yjs-updates'])
|
|
570
|
+
*
|
|
571
|
+
* // On receiving remote capabilities:
|
|
572
|
+
* const session = negotiator.negotiate(local, remoteCapabilities)
|
|
573
|
+
* if (session.success) {
|
|
574
|
+
* if (session.canUse('schema-versioning')) {
|
|
575
|
+
* // Use schema versioning
|
|
576
|
+
* }
|
|
577
|
+
* }
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Capabilities advertised by a peer during connection.
|
|
583
|
+
*/
|
|
584
|
+
interface PeerCapabilities {
|
|
585
|
+
/** Peer's DID */
|
|
586
|
+
peerId: string;
|
|
587
|
+
/** Maximum protocol version supported */
|
|
588
|
+
protocolVersion: number;
|
|
589
|
+
/** Minimum protocol version supported */
|
|
590
|
+
minProtocolVersion: number;
|
|
591
|
+
/** Features enabled on this peer */
|
|
592
|
+
features: FeatureFlag[];
|
|
593
|
+
/** Package version string (e.g., '0.5.0') */
|
|
594
|
+
packageVersion: string;
|
|
595
|
+
/** Schema IRIs this peer understands (optional) */
|
|
596
|
+
schemas?: string[];
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Result of a successful negotiation.
|
|
600
|
+
*/
|
|
601
|
+
interface NegotiatedSession {
|
|
602
|
+
success: true;
|
|
603
|
+
/** Remote peer's ID */
|
|
604
|
+
peerId: string;
|
|
605
|
+
/** Agreed protocol version (highest common version) */
|
|
606
|
+
agreedVersion: number;
|
|
607
|
+
/** Features both peers support */
|
|
608
|
+
commonFeatures: FeatureFlag[];
|
|
609
|
+
/** Warnings about degraded functionality */
|
|
610
|
+
warnings: NegotiationWarning[];
|
|
611
|
+
/**
|
|
612
|
+
* Check if a feature can be used in this session.
|
|
613
|
+
*/
|
|
614
|
+
canUse(feature: FeatureFlag): boolean;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Result of a failed negotiation.
|
|
618
|
+
*/
|
|
619
|
+
interface NegotiationFailure {
|
|
620
|
+
success: false;
|
|
621
|
+
/** Error code */
|
|
622
|
+
error: 'incompatible-versions' | 'missing-required-features' | 'invalid-capabilities';
|
|
623
|
+
/** Human-readable message */
|
|
624
|
+
message: string;
|
|
625
|
+
/** Local protocol version */
|
|
626
|
+
localVersion: number;
|
|
627
|
+
/** Remote protocol version */
|
|
628
|
+
remoteVersion: number;
|
|
629
|
+
/** Suggestion for resolving the issue */
|
|
630
|
+
suggestion: 'upgrade-client' | 'upgrade-hub' | 'upgrade-both' | 'contact-support';
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Warning about potential issues in a negotiated session.
|
|
634
|
+
*/
|
|
635
|
+
interface NegotiationWarning {
|
|
636
|
+
type: 'degraded-features' | 'version-mismatch' | 'unknown-features';
|
|
637
|
+
message: string;
|
|
638
|
+
/** Features affected by this warning */
|
|
639
|
+
affectedFeatures?: FeatureFlag[];
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Result type for negotiation (success or failure).
|
|
643
|
+
*/
|
|
644
|
+
type NegotiationResult = NegotiatedSession | NegotiationFailure;
|
|
645
|
+
/**
|
|
646
|
+
* Create local capabilities for negotiation.
|
|
647
|
+
*
|
|
648
|
+
* @param peerId - Local peer's DID
|
|
649
|
+
* @param features - Features to advertise (defaults to all enabled at current version)
|
|
650
|
+
* @param options - Additional options
|
|
651
|
+
* @returns PeerCapabilities for this peer
|
|
652
|
+
*/
|
|
653
|
+
declare function createLocalCapabilities(peerId: string, features?: FeatureFlag[], options?: {
|
|
654
|
+
packageVersion?: string;
|
|
655
|
+
minProtocolVersion?: number;
|
|
656
|
+
schemas?: string[];
|
|
657
|
+
}): PeerCapabilities;
|
|
658
|
+
/**
|
|
659
|
+
* Parse capabilities from a handshake message.
|
|
660
|
+
* Handles missing or malformed fields gracefully.
|
|
661
|
+
*
|
|
662
|
+
* @param message - Raw handshake message
|
|
663
|
+
* @returns Parsed capabilities or null if invalid
|
|
664
|
+
*/
|
|
665
|
+
declare function parseCapabilities(message: unknown): PeerCapabilities | null;
|
|
666
|
+
/**
|
|
667
|
+
* Handles version negotiation between peers.
|
|
668
|
+
*
|
|
669
|
+
* The negotiation process:
|
|
670
|
+
* 1. Find highest common protocol version
|
|
671
|
+
* 2. Check version ranges overlap
|
|
672
|
+
* 3. Find common feature set
|
|
673
|
+
* 4. Verify required features are present
|
|
674
|
+
* 5. Generate warnings for degraded functionality
|
|
675
|
+
*/
|
|
676
|
+
declare class VersionNegotiator {
|
|
677
|
+
/**
|
|
678
|
+
* Negotiate capabilities between local and remote peers.
|
|
679
|
+
*
|
|
680
|
+
* @param local - Local peer's capabilities
|
|
681
|
+
* @param remote - Remote peer's capabilities
|
|
682
|
+
* @returns Negotiated session or failure result
|
|
683
|
+
*/
|
|
684
|
+
negotiate(local: PeerCapabilities, remote: PeerCapabilities): NegotiationResult;
|
|
685
|
+
/**
|
|
686
|
+
* Validate that a set of capabilities is well-formed.
|
|
687
|
+
*
|
|
688
|
+
* @param capabilities - Capabilities to validate
|
|
689
|
+
* @returns Validation result with errors if any
|
|
690
|
+
*/
|
|
691
|
+
validateCapabilities(capabilities: PeerCapabilities): {
|
|
692
|
+
valid: boolean;
|
|
693
|
+
errors: string[];
|
|
694
|
+
};
|
|
695
|
+
/**
|
|
696
|
+
* Create a failure result with appropriate suggestion.
|
|
697
|
+
*/
|
|
698
|
+
private createFailure;
|
|
699
|
+
/**
|
|
700
|
+
* Generate warnings about degraded functionality.
|
|
701
|
+
*/
|
|
702
|
+
private generateWarnings;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Default negotiator instance for convenience.
|
|
706
|
+
*/
|
|
707
|
+
declare const defaultNegotiator: VersionNegotiator;
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Sync provider interfaces and types.
|
|
711
|
+
*
|
|
712
|
+
* A SyncProvider is responsible for connecting to the sync network,
|
|
713
|
+
* broadcasting changes to peers, and receiving changes from peers.
|
|
714
|
+
* Different implementations can use different transports (WebRTC, WebSocket, etc.)
|
|
715
|
+
*/
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Connection status of a sync provider.
|
|
719
|
+
*/
|
|
720
|
+
type SyncStatus = 'disconnected' | 'connecting' | 'synced' | 'syncing' | 'error';
|
|
721
|
+
/**
|
|
722
|
+
* Information about a connected peer.
|
|
723
|
+
*/
|
|
724
|
+
interface PeerInfo {
|
|
725
|
+
/** Unique peer identifier */
|
|
726
|
+
id: string;
|
|
727
|
+
/** Human-readable name (if available) */
|
|
728
|
+
name?: string;
|
|
729
|
+
/** When the peer connected */
|
|
730
|
+
connectedAt: number;
|
|
731
|
+
/** Last activity timestamp */
|
|
732
|
+
lastSeen: number;
|
|
733
|
+
/** Peer's advertised capabilities (after handshake) */
|
|
734
|
+
capabilities?: PeerCapabilities;
|
|
735
|
+
/** Negotiated session with this peer */
|
|
736
|
+
negotiatedSession?: NegotiatedSession;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Events emitted by sync providers.
|
|
740
|
+
*/
|
|
741
|
+
interface SyncProviderEvents<T = unknown> {
|
|
742
|
+
/** Fired when sync status changes */
|
|
743
|
+
'status-change': (status: SyncStatus) => void;
|
|
744
|
+
/** Fired when a single change is received from a peer */
|
|
745
|
+
'change-received': (change: Change<T>, peerId: string) => void;
|
|
746
|
+
/** Fired when multiple changes are synced */
|
|
747
|
+
'changes-synced': (changes: Change<T>[]) => void;
|
|
748
|
+
/** Fired when a peer connects */
|
|
749
|
+
'peer-connected': (peer: PeerInfo) => void;
|
|
750
|
+
/** Fired when a peer disconnects */
|
|
751
|
+
'peer-disconnected': (peerId: string) => void;
|
|
752
|
+
/** Fired on error */
|
|
753
|
+
error: (error: Error) => void;
|
|
754
|
+
/**
|
|
755
|
+
* Fired when a change with an unknown type is received.
|
|
756
|
+
* The change is still stored in the change log for forward compatibility,
|
|
757
|
+
* but cannot be processed by the current version.
|
|
758
|
+
*/
|
|
759
|
+
'unknown-change-type': (change: Change<unknown>, peerId: string) => void;
|
|
760
|
+
/**
|
|
761
|
+
* Fired when capability negotiation completes with a peer.
|
|
762
|
+
* Includes the negotiated session with common features.
|
|
763
|
+
*/
|
|
764
|
+
'negotiation-complete': (peerId: string, session: NegotiatedSession) => void;
|
|
765
|
+
/**
|
|
766
|
+
* Fired when capability negotiation fails with a peer.
|
|
767
|
+
* The peer will be disconnected after this event.
|
|
768
|
+
*/
|
|
769
|
+
'negotiation-failed': (peerId: string, error: string, suggestion: string) => void;
|
|
770
|
+
/**
|
|
771
|
+
* Fired when operating with degraded features due to peer compatibility.
|
|
772
|
+
* Includes warnings about unavailable features.
|
|
773
|
+
*/
|
|
774
|
+
'capability-degraded': (peerId: string, warnings: string[]) => void;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Event listener type for type-safe event handling.
|
|
778
|
+
*/
|
|
779
|
+
type SyncEventListener<T, E extends keyof SyncProviderEvents<T>> = SyncProviderEvents<T>[E];
|
|
780
|
+
/**
|
|
781
|
+
* Base interface for all sync providers.
|
|
782
|
+
*
|
|
783
|
+
* Implementations include:
|
|
784
|
+
* - y-webrtc provider for Yjs documents
|
|
785
|
+
* - Custom provider for event-sourced records
|
|
786
|
+
* - Hybrid providers that support both
|
|
787
|
+
*/
|
|
788
|
+
interface SyncProvider<T = unknown> {
|
|
789
|
+
/** Current sync status */
|
|
790
|
+
readonly status: SyncStatus;
|
|
791
|
+
/** List of connected peer IDs */
|
|
792
|
+
readonly peers: string[];
|
|
793
|
+
/** Detailed information about connected peers */
|
|
794
|
+
readonly peerInfo: Map<string, PeerInfo>;
|
|
795
|
+
/** Local peer's capabilities */
|
|
796
|
+
readonly localCapabilities: PeerCapabilities;
|
|
797
|
+
/**
|
|
798
|
+
* Check if a feature can be used with a specific peer.
|
|
799
|
+
* Returns false if not negotiated or feature unavailable.
|
|
800
|
+
*/
|
|
801
|
+
canUseFeature(peerId: string, feature: FeatureFlag): boolean;
|
|
802
|
+
/**
|
|
803
|
+
* Get the negotiated session for a peer.
|
|
804
|
+
* Returns undefined if not yet negotiated.
|
|
805
|
+
*/
|
|
806
|
+
getNegotiatedSession(peerId: string): NegotiatedSession | undefined;
|
|
807
|
+
/**
|
|
808
|
+
* Connect to the sync network.
|
|
809
|
+
* This may involve signaling servers, DHT lookups, etc.
|
|
810
|
+
*/
|
|
811
|
+
connect(): Promise<void>;
|
|
812
|
+
/**
|
|
813
|
+
* Disconnect from the sync network.
|
|
814
|
+
* Closes all peer connections gracefully.
|
|
815
|
+
*/
|
|
816
|
+
disconnect(): Promise<void>;
|
|
817
|
+
/**
|
|
818
|
+
* Broadcast a change to all connected peers.
|
|
819
|
+
*
|
|
820
|
+
* @param change - The change to broadcast
|
|
821
|
+
*/
|
|
822
|
+
broadcast(change: Change<T>): Promise<void>;
|
|
823
|
+
/**
|
|
824
|
+
* Request changes from a specific peer.
|
|
825
|
+
* Used for initial sync or catching up.
|
|
826
|
+
*
|
|
827
|
+
* @param peerId - The peer to request from
|
|
828
|
+
* @param since - Optional hash to request changes since
|
|
829
|
+
* @returns Array of changes
|
|
830
|
+
*/
|
|
831
|
+
requestChanges(peerId: string, since?: string): Promise<Change<T>[]>;
|
|
832
|
+
/**
|
|
833
|
+
* Request changes from all connected peers.
|
|
834
|
+
* Useful for catching up after reconnection.
|
|
835
|
+
*
|
|
836
|
+
* @param since - Optional hash to request changes since
|
|
837
|
+
* @returns Array of unique changes (deduplicated)
|
|
838
|
+
*/
|
|
839
|
+
requestChangesFromAll(since?: string): Promise<Change<T>[]>;
|
|
840
|
+
/**
|
|
841
|
+
* Subscribe to an event.
|
|
842
|
+
*
|
|
843
|
+
* @param event - Event name
|
|
844
|
+
* @param listener - Event listener
|
|
845
|
+
*/
|
|
846
|
+
on<E extends keyof SyncProviderEvents<T>>(event: E, listener: SyncProviderEvents<T>[E]): void;
|
|
847
|
+
/**
|
|
848
|
+
* Unsubscribe from an event.
|
|
849
|
+
*
|
|
850
|
+
* @param event - Event name
|
|
851
|
+
* @param listener - Event listener to remove
|
|
852
|
+
*/
|
|
853
|
+
off<E extends keyof SyncProviderEvents<T>>(event: E, listener: SyncProviderEvents<T>[E]): void;
|
|
854
|
+
/**
|
|
855
|
+
* Subscribe to an event for a single occurrence.
|
|
856
|
+
*
|
|
857
|
+
* @param event - Event name
|
|
858
|
+
* @param listener - Event listener
|
|
859
|
+
*/
|
|
860
|
+
once<E extends keyof SyncProviderEvents<T>>(event: E, listener: SyncProviderEvents<T>[E]): void;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Options for creating a sync provider.
|
|
864
|
+
*/
|
|
865
|
+
interface SyncProviderOptions {
|
|
866
|
+
/** Signaling server URL(s) */
|
|
867
|
+
signalingServers?: string[];
|
|
868
|
+
/** Room/topic name for peer discovery */
|
|
869
|
+
room: string;
|
|
870
|
+
/** Connection timeout in milliseconds */
|
|
871
|
+
timeout?: number;
|
|
872
|
+
/** Whether to auto-reconnect on disconnect */
|
|
873
|
+
autoReconnect?: boolean;
|
|
874
|
+
/** Maximum reconnection attempts */
|
|
875
|
+
maxReconnectAttempts?: number;
|
|
876
|
+
/** Reconnection delay in milliseconds */
|
|
877
|
+
reconnectDelay?: number;
|
|
878
|
+
/** Local peer's DID for capability advertisement */
|
|
879
|
+
localDID?: string;
|
|
880
|
+
/** Features to advertise (defaults to all enabled at current protocol version) */
|
|
881
|
+
enabledFeatures?: FeatureFlag[];
|
|
882
|
+
/** Minimum protocol version to accept from peers */
|
|
883
|
+
minProtocolVersion?: number;
|
|
884
|
+
/** Whether to reject peers with incompatible versions (default: false, just warn) */
|
|
885
|
+
strictVersionCheck?: boolean;
|
|
886
|
+
/** Package version string for debugging */
|
|
887
|
+
packageVersion?: string;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Abstract base class for sync providers with common functionality.
|
|
891
|
+
* Implementations can extend this to reduce boilerplate.
|
|
892
|
+
*/
|
|
893
|
+
declare abstract class BaseSyncProvider<T = unknown> implements SyncProvider<T> {
|
|
894
|
+
protected _status: SyncStatus;
|
|
895
|
+
protected _peers: Map<string, PeerInfo>;
|
|
896
|
+
protected _listeners: Map<string, Set<(...args: unknown[]) => unknown>>;
|
|
897
|
+
/** Local capabilities for negotiation */
|
|
898
|
+
protected _localCapabilities: PeerCapabilities;
|
|
899
|
+
/** Version negotiator instance */
|
|
900
|
+
protected _negotiator: VersionNegotiator;
|
|
901
|
+
/** Provider options */
|
|
902
|
+
protected _options: SyncProviderOptions;
|
|
903
|
+
constructor(options: SyncProviderOptions);
|
|
904
|
+
get status(): SyncStatus;
|
|
905
|
+
get peers(): string[];
|
|
906
|
+
get peerInfo(): Map<string, PeerInfo>;
|
|
907
|
+
get localCapabilities(): PeerCapabilities;
|
|
908
|
+
/**
|
|
909
|
+
* Check if a feature can be used with a specific peer.
|
|
910
|
+
*/
|
|
911
|
+
canUseFeature(peerId: string, feature: FeatureFlag): boolean;
|
|
912
|
+
/**
|
|
913
|
+
* Get the negotiated session for a peer.
|
|
914
|
+
*/
|
|
915
|
+
getNegotiatedSession(peerId: string): NegotiatedSession | undefined;
|
|
916
|
+
abstract connect(): Promise<void>;
|
|
917
|
+
abstract disconnect(): Promise<void>;
|
|
918
|
+
abstract broadcast(change: Change<T>): Promise<void>;
|
|
919
|
+
abstract requestChanges(peerId: string, since?: string): Promise<Change<T>[]>;
|
|
920
|
+
requestChangesFromAll(since?: string): Promise<Change<T>[]>;
|
|
921
|
+
on<E extends keyof SyncProviderEvents<T>>(event: E, listener: SyncProviderEvents<T>[E]): void;
|
|
922
|
+
off<E extends keyof SyncProviderEvents<T>>(event: E, listener: SyncProviderEvents<T>[E]): void;
|
|
923
|
+
once<E extends keyof SyncProviderEvents<T>>(event: E, listener: SyncProviderEvents<T>[E]): void;
|
|
924
|
+
protected emit<E extends keyof SyncProviderEvents<T>>(event: E, ...args: Parameters<SyncProviderEvents<T>[E]>): void;
|
|
925
|
+
protected setStatus(status: SyncStatus): void;
|
|
926
|
+
protected addPeer(id: string, name?: string): void;
|
|
927
|
+
protected removePeer(id: string): void;
|
|
928
|
+
protected updatePeerLastSeen(id: string): void;
|
|
929
|
+
/**
|
|
930
|
+
* Negotiate capabilities with a peer.
|
|
931
|
+
* Called when a peer connects and sends their capabilities.
|
|
932
|
+
*
|
|
933
|
+
* @param peerId - The peer's ID
|
|
934
|
+
* @param remoteCapabilities - The peer's advertised capabilities
|
|
935
|
+
* @returns Negotiation result (success or failure)
|
|
936
|
+
*/
|
|
937
|
+
protected negotiateWithPeer(peerId: string, remoteCapabilities: PeerCapabilities): NegotiationResult;
|
|
938
|
+
/**
|
|
939
|
+
* Get features available with all connected peers.
|
|
940
|
+
* Returns the intersection of all negotiated feature sets.
|
|
941
|
+
*/
|
|
942
|
+
getCommonFeatures(): FeatureFlag[];
|
|
943
|
+
/**
|
|
944
|
+
* Check if a feature can be used with all connected peers.
|
|
945
|
+
*/
|
|
946
|
+
canUseFeatureWithAll(feature: FeatureFlag): boolean;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Signed Yjs Envelopes - Per-update signing and verification for Yjs sync messages
|
|
951
|
+
*
|
|
952
|
+
* Every outgoing Yjs update is wrapped in a signed envelope containing the author's DID
|
|
953
|
+
* and a multi-level signature (Ed25519 and/or ML-DSA-65) over the BLAKE3 hash of the
|
|
954
|
+
* update bytes plus metadata.
|
|
955
|
+
*
|
|
956
|
+
* V2 introduces:
|
|
957
|
+
* - Multi-level signature support (Level 0, 1, 2)
|
|
958
|
+
* - Document ID binding
|
|
959
|
+
* - Wire format with compact JSON encoding
|
|
960
|
+
*
|
|
961
|
+
* V1 is retained for backward compatibility.
|
|
962
|
+
*/
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* A signed envelope wrapping a Yjs update (V1 format).
|
|
966
|
+
* @deprecated Use SignedYjsEnvelopeV2 for new code
|
|
967
|
+
*/
|
|
968
|
+
interface SignedYjsEnvelopeV1 {
|
|
969
|
+
/** Raw Yjs update bytes */
|
|
970
|
+
update: Uint8Array;
|
|
971
|
+
/** Author's DID (did:key:...) */
|
|
972
|
+
authorDID: string;
|
|
973
|
+
/** Ed25519 signature over BLAKE3(update) */
|
|
974
|
+
signature: Uint8Array;
|
|
975
|
+
/** Wall clock timestamp (for ordering/debugging) */
|
|
976
|
+
timestamp: number;
|
|
977
|
+
/** Yjs clientID this author uses in this session */
|
|
978
|
+
clientId: number;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* A signed envelope wrapping a Yjs update (V2 format with multi-level signatures).
|
|
982
|
+
*/
|
|
983
|
+
interface SignedYjsEnvelopeV2 {
|
|
984
|
+
/** Wire format version */
|
|
985
|
+
v: 2;
|
|
986
|
+
/** Raw Yjs update bytes */
|
|
987
|
+
update: Uint8Array;
|
|
988
|
+
/** Envelope metadata */
|
|
989
|
+
meta: {
|
|
990
|
+
/** Author's DID (did:key:...) */
|
|
991
|
+
authorDID: DID;
|
|
992
|
+
/** Yjs clientID this author uses in this session */
|
|
993
|
+
clientId: number;
|
|
994
|
+
/** Wall clock timestamp (ms since epoch) */
|
|
995
|
+
timestamp: number;
|
|
996
|
+
/** Document ID this update applies to */
|
|
997
|
+
docId: string;
|
|
998
|
+
};
|
|
999
|
+
/** Multi-level signature over BLAKE3(update + meta) */
|
|
1000
|
+
signature: UnifiedSignature;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Wire format for SignedYjsEnvelopeV2 (compact JSON).
|
|
1004
|
+
*/
|
|
1005
|
+
interface SignedYjsEnvelopeWire {
|
|
1006
|
+
v: 2;
|
|
1007
|
+
/** Base64-encoded update bytes */
|
|
1008
|
+
u: string;
|
|
1009
|
+
/** Metadata */
|
|
1010
|
+
m: {
|
|
1011
|
+
/** Author DID */
|
|
1012
|
+
a: string;
|
|
1013
|
+
/** Client ID */
|
|
1014
|
+
c: number;
|
|
1015
|
+
/** Timestamp */
|
|
1016
|
+
t: number;
|
|
1017
|
+
/** Document ID */
|
|
1018
|
+
d: string;
|
|
1019
|
+
};
|
|
1020
|
+
/** Signature (wire format) */
|
|
1021
|
+
s: SignatureWire;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Union type for all envelope versions.
|
|
1025
|
+
* Use `isV2Envelope()` to check the version.
|
|
1026
|
+
*/
|
|
1027
|
+
type SignedYjsEnvelope = SignedYjsEnvelopeV1 | SignedYjsEnvelopeV2;
|
|
1028
|
+
/**
|
|
1029
|
+
* Result of envelope verification (V1 format).
|
|
1030
|
+
* @deprecated Use EnvelopeVerificationResult for V2
|
|
1031
|
+
*/
|
|
1032
|
+
interface EnvelopeVerifyResult {
|
|
1033
|
+
valid: boolean;
|
|
1034
|
+
reason?: 'invalid_signature' | 'did_resolution_failed' | 'update_too_large';
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Result of envelope verification (V2 format with details).
|
|
1038
|
+
*/
|
|
1039
|
+
interface EnvelopeVerificationResult {
|
|
1040
|
+
valid: boolean;
|
|
1041
|
+
level: SecurityLevel;
|
|
1042
|
+
errors: string[];
|
|
1043
|
+
authorDID: DID;
|
|
1044
|
+
clientId: number;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Options for creating a signed envelope.
|
|
1048
|
+
*/
|
|
1049
|
+
interface CreateEnvelopeOptions {
|
|
1050
|
+
/** Security level (default: 0 for Ed25519-only) */
|
|
1051
|
+
level?: SecurityLevel;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Options for envelope verification.
|
|
1055
|
+
*/
|
|
1056
|
+
interface VerifyEnvelopeOptions {
|
|
1057
|
+
/** PQ key registry for Level 1/2 verification */
|
|
1058
|
+
registry?: PQKeyRegistry;
|
|
1059
|
+
/** Expected document ID (optional, for extra validation) */
|
|
1060
|
+
expectedDocId?: string;
|
|
1061
|
+
/** Maximum age in ms (optional, for freshness check) */
|
|
1062
|
+
maxAge?: number;
|
|
1063
|
+
/** Minimum security level required */
|
|
1064
|
+
minLevel?: SecurityLevel;
|
|
1065
|
+
/** Verification policy */
|
|
1066
|
+
policy?: 'strict' | 'permissive';
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Check if an envelope is V2 format.
|
|
1070
|
+
*/
|
|
1071
|
+
declare function isV2Envelope(envelope: SignedYjsEnvelope): envelope is SignedYjsEnvelopeV2;
|
|
1072
|
+
/**
|
|
1073
|
+
* Check if an envelope is V1 format.
|
|
1074
|
+
*/
|
|
1075
|
+
declare function isV1Envelope(envelope: SignedYjsEnvelope): envelope is SignedYjsEnvelopeV1;
|
|
1076
|
+
/**
|
|
1077
|
+
* Sign a Yjs update using V1 format (Ed25519 only).
|
|
1078
|
+
*
|
|
1079
|
+
* @deprecated Use signYjsUpdateV2() for new code
|
|
1080
|
+
*
|
|
1081
|
+
* @param update - Raw Yjs update bytes
|
|
1082
|
+
* @param authorDID - Author's DID (did:key:...)
|
|
1083
|
+
* @param privateKey - Ed25519 private key (32 bytes)
|
|
1084
|
+
* @param clientId - Yjs clientID for this session
|
|
1085
|
+
* @returns SignedYjsEnvelopeV1 ready for transmission
|
|
1086
|
+
*/
|
|
1087
|
+
declare function signYjsUpdateV1(update: Uint8Array, authorDID: string, privateKey: Uint8Array, clientId: number): SignedYjsEnvelopeV1;
|
|
1088
|
+
/**
|
|
1089
|
+
* Verify a V1 SignedYjsEnvelope.
|
|
1090
|
+
*
|
|
1091
|
+
* @deprecated Use verifyYjsEnvelopeV2() for V2 envelopes
|
|
1092
|
+
*/
|
|
1093
|
+
declare function verifyYjsEnvelopeV1(envelope: SignedYjsEnvelopeV1): EnvelopeVerifyResult;
|
|
1094
|
+
/**
|
|
1095
|
+
* Sign a Yjs update using V2 format with multi-level signatures.
|
|
1096
|
+
*
|
|
1097
|
+
* @param update - Raw Yjs update bytes
|
|
1098
|
+
* @param docId - Document ID this update applies to
|
|
1099
|
+
* @param clientId - Yjs clientID for this session
|
|
1100
|
+
* @param keyBundle - Key bundle for signing
|
|
1101
|
+
* @param options - Signing options
|
|
1102
|
+
* @returns SignedYjsEnvelopeV2 ready for transmission
|
|
1103
|
+
*
|
|
1104
|
+
* @example
|
|
1105
|
+
* ```typescript
|
|
1106
|
+
* const envelope = signYjsUpdateV2(update, 'doc-1', doc.clientID, keyBundle, { level: 1 })
|
|
1107
|
+
* ws.send(encode({ type: 'sync-update', room, envelope: serializeYjsEnvelope(envelope) }))
|
|
1108
|
+
* ```
|
|
1109
|
+
*/
|
|
1110
|
+
declare function signYjsUpdateV2(update: Uint8Array, docId: string, clientId: number, keyBundle: HybridKeyBundle, options?: CreateEnvelopeOptions): SignedYjsEnvelopeV2;
|
|
1111
|
+
/**
|
|
1112
|
+
* Sign multiple updates in a batch using V2 format.
|
|
1113
|
+
*/
|
|
1114
|
+
declare function signYjsUpdateBatch(updates: Uint8Array[], docId: string, clientId: number, keyBundle: HybridKeyBundle, options?: CreateEnvelopeOptions): SignedYjsEnvelopeV2[];
|
|
1115
|
+
/**
|
|
1116
|
+
* Verify a V2 SignedYjsEnvelope with multi-level signature support.
|
|
1117
|
+
*
|
|
1118
|
+
* @param envelope - The envelope to verify
|
|
1119
|
+
* @param options - Verification options
|
|
1120
|
+
* @returns Verification result with detailed errors
|
|
1121
|
+
*/
|
|
1122
|
+
declare function verifyYjsEnvelopeV2(envelope: SignedYjsEnvelopeV2, options?: VerifyEnvelopeOptions): Promise<EnvelopeVerificationResult>;
|
|
1123
|
+
/**
|
|
1124
|
+
* Quick verification returning just boolean.
|
|
1125
|
+
*/
|
|
1126
|
+
declare function verifyYjsEnvelopeQuick(envelope: SignedYjsEnvelopeV2, options?: VerifyEnvelopeOptions): Promise<boolean>;
|
|
1127
|
+
/**
|
|
1128
|
+
* Serialize V2 envelope to wire format.
|
|
1129
|
+
*/
|
|
1130
|
+
declare function serializeYjsEnvelope(envelope: SignedYjsEnvelopeV2): SignedYjsEnvelopeWire;
|
|
1131
|
+
/**
|
|
1132
|
+
* Deserialize V2 envelope from wire format.
|
|
1133
|
+
*/
|
|
1134
|
+
declare function deserializeYjsEnvelope(wire: SignedYjsEnvelopeWire): SignedYjsEnvelopeV2;
|
|
1135
|
+
/**
|
|
1136
|
+
* Calculate envelope size in bytes (approximate).
|
|
1137
|
+
*/
|
|
1138
|
+
declare function envelopeSize(envelope: SignedYjsEnvelopeV2): number;
|
|
1139
|
+
/**
|
|
1140
|
+
* Sign a Yjs update, creating a SignedYjsEnvelope.
|
|
1141
|
+
*
|
|
1142
|
+
* This is the main API for signing updates. It creates a V1 envelope
|
|
1143
|
+
* for backward compatibility when using simple Ed25519 keys, or a V2
|
|
1144
|
+
* envelope when using a HybridKeyBundle.
|
|
1145
|
+
*
|
|
1146
|
+
* @overload V1 signature (legacy)
|
|
1147
|
+
* @param update - Raw Yjs update bytes
|
|
1148
|
+
* @param authorDID - Author's DID (did:key:...)
|
|
1149
|
+
* @param privateKey - Ed25519 private key (32 bytes)
|
|
1150
|
+
* @param clientId - Yjs clientID for this session
|
|
1151
|
+
*/
|
|
1152
|
+
declare function signYjsUpdate(update: Uint8Array, authorDID: string, privateKey: Uint8Array, clientId: number): SignedYjsEnvelopeV1;
|
|
1153
|
+
/**
|
|
1154
|
+
* @overload V2 signature (multi-level)
|
|
1155
|
+
* @param update - Raw Yjs update bytes
|
|
1156
|
+
* @param docId - Document ID this update applies to
|
|
1157
|
+
* @param clientId - Yjs clientID for this session
|
|
1158
|
+
* @param keyBundle - Key bundle for signing
|
|
1159
|
+
* @param options - Signing options
|
|
1160
|
+
*/
|
|
1161
|
+
declare function signYjsUpdate(update: Uint8Array, docId: string, clientId: number, keyBundle: HybridKeyBundle, options?: CreateEnvelopeOptions): SignedYjsEnvelopeV2;
|
|
1162
|
+
/**
|
|
1163
|
+
* Verify a SignedYjsEnvelope (auto-detects version).
|
|
1164
|
+
*
|
|
1165
|
+
* For V1 envelopes, returns a simple valid/reason result.
|
|
1166
|
+
* For V2 envelopes, returns a detailed verification result.
|
|
1167
|
+
*
|
|
1168
|
+
* @param envelope - The envelope to verify
|
|
1169
|
+
* @param options - Verification options (V2 only)
|
|
1170
|
+
*/
|
|
1171
|
+
declare function verifyYjsEnvelope(envelope: SignedYjsEnvelopeV1): EnvelopeVerifyResult;
|
|
1172
|
+
declare function verifyYjsEnvelope(envelope: SignedYjsEnvelopeV2, options?: VerifyEnvelopeOptions): Promise<EnvelopeVerificationResult>;
|
|
1173
|
+
/**
|
|
1174
|
+
* Type guard to check if a message contains a signed envelope.
|
|
1175
|
+
*/
|
|
1176
|
+
declare function hasSignedEnvelope(msg: unknown): msg is {
|
|
1177
|
+
envelope: SignedYjsEnvelope;
|
|
1178
|
+
[key: string]: unknown;
|
|
1179
|
+
};
|
|
1180
|
+
/**
|
|
1181
|
+
* Check if a message is a legacy unsigned sync update.
|
|
1182
|
+
*/
|
|
1183
|
+
declare function isLegacyUpdate(msg: unknown): msg is {
|
|
1184
|
+
data: Uint8Array;
|
|
1185
|
+
[key: string]: unknown;
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Yjs Update Size and Rate Limits
|
|
1190
|
+
*
|
|
1191
|
+
* Constants and utilities for preventing DoS via oversized or high-frequency Yjs updates.
|
|
1192
|
+
*/
|
|
1193
|
+
/** Maximum size of a single Yjs update (1MB) */
|
|
1194
|
+
declare const MAX_YJS_UPDATE_SIZE = 1048576;
|
|
1195
|
+
/** Maximum updates per second per connection */
|
|
1196
|
+
declare const MAX_YJS_UPDATES_PER_SECOND = 30;
|
|
1197
|
+
/** Maximum updates per minute per connection (sustained rate) */
|
|
1198
|
+
declare const MAX_YJS_UPDATES_PER_MINUTE = 600;
|
|
1199
|
+
/** Maximum document size (full state, 50MB) */
|
|
1200
|
+
declare const MAX_YJS_DOC_SIZE = 52428800;
|
|
1201
|
+
/** Chunk size for large initial syncs (256KB) */
|
|
1202
|
+
declare const YJS_SYNC_CHUNK_SIZE = 262144;
|
|
1203
|
+
/** Burst allowance above per-second limit */
|
|
1204
|
+
declare const YJS_RATE_BURST_ALLOWANCE = 10;
|
|
1205
|
+
/**
|
|
1206
|
+
* Rate limiter configuration.
|
|
1207
|
+
*/
|
|
1208
|
+
interface RateLimiterConfig {
|
|
1209
|
+
/** Maximum updates per second */
|
|
1210
|
+
maxPerSecond: number;
|
|
1211
|
+
/** Maximum updates per minute */
|
|
1212
|
+
maxPerMinute: number;
|
|
1213
|
+
/** Burst allowance above maxPerSecond */
|
|
1214
|
+
burstAllowance: number;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Default rate limiter configuration.
|
|
1218
|
+
*/
|
|
1219
|
+
declare const DEFAULT_RATE_LIMITER_CONFIG: RateLimiterConfig;
|
|
1220
|
+
/**
|
|
1221
|
+
* Extended config for rate limiter with cleanup options.
|
|
1222
|
+
*/
|
|
1223
|
+
interface YjsRateLimiterOptions extends Partial<RateLimiterConfig> {
|
|
1224
|
+
/** How long until an entry is considered stale (default: 2 minutes) */
|
|
1225
|
+
staleThresholdMs?: number;
|
|
1226
|
+
/** Cleanup interval in ms (default: 30 seconds). Set to 0 to disable auto-cleanup. */
|
|
1227
|
+
cleanupIntervalMs?: number;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Rate limiter for Yjs updates.
|
|
1231
|
+
*
|
|
1232
|
+
* Tracks per-peer update frequency and enforces both per-second and per-minute limits.
|
|
1233
|
+
*
|
|
1234
|
+
* @example
|
|
1235
|
+
* ```typescript
|
|
1236
|
+
* const limiter = new YjsRateLimiter()
|
|
1237
|
+
*
|
|
1238
|
+
* if (!limiter.allow(peerId)) {
|
|
1239
|
+
* // Reject update, rate limit exceeded
|
|
1240
|
+
* return { ok: false, reason: 'rate_exceeded' }
|
|
1241
|
+
* }
|
|
1242
|
+
* ```
|
|
1243
|
+
*/
|
|
1244
|
+
declare class YjsRateLimiter {
|
|
1245
|
+
private secondWindows;
|
|
1246
|
+
private minuteWindows;
|
|
1247
|
+
private config;
|
|
1248
|
+
private cleanupInterval;
|
|
1249
|
+
private staleThresholdMs;
|
|
1250
|
+
constructor(options?: YjsRateLimiterOptions);
|
|
1251
|
+
/** Start periodic cleanup of stale entries. */
|
|
1252
|
+
startCleanup(intervalMs?: number): void;
|
|
1253
|
+
/** Stop periodic cleanup. */
|
|
1254
|
+
stopCleanup(): void;
|
|
1255
|
+
/** Remove entries whose windows have expired. Returns count removed. */
|
|
1256
|
+
cleanupStale(): number;
|
|
1257
|
+
/** Get number of tracked peers. */
|
|
1258
|
+
get peerCount(): number;
|
|
1259
|
+
/**
|
|
1260
|
+
* Check if a peer can send another update.
|
|
1261
|
+
*
|
|
1262
|
+
* @param peerId - Peer identifier
|
|
1263
|
+
* @returns true if allowed, false if rate-limited
|
|
1264
|
+
*/
|
|
1265
|
+
allow(peerId: string): boolean;
|
|
1266
|
+
/**
|
|
1267
|
+
* Get current rate info for a peer (for debugging/monitoring).
|
|
1268
|
+
*/
|
|
1269
|
+
getInfo(peerId: string): {
|
|
1270
|
+
perSecond: number;
|
|
1271
|
+
perMinute: number;
|
|
1272
|
+
} | undefined;
|
|
1273
|
+
/**
|
|
1274
|
+
* Reset state for a disconnected peer.
|
|
1275
|
+
*/
|
|
1276
|
+
remove(peerId: string): void;
|
|
1277
|
+
/**
|
|
1278
|
+
* Clear all state.
|
|
1279
|
+
*/
|
|
1280
|
+
clear(): void;
|
|
1281
|
+
/**
|
|
1282
|
+
* Stop cleanup and clear all state.
|
|
1283
|
+
*/
|
|
1284
|
+
destroy(): void;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Check if an update exceeds the size limit.
|
|
1288
|
+
*/
|
|
1289
|
+
declare function isUpdateTooLarge(update: Uint8Array, maxSize?: number): boolean;
|
|
1290
|
+
/**
|
|
1291
|
+
* Check if a document state exceeds the size limit.
|
|
1292
|
+
*/
|
|
1293
|
+
declare function isDocumentTooLarge(state: Uint8Array, maxSize?: number): boolean;
|
|
1294
|
+
/**
|
|
1295
|
+
* Calculate number of chunks needed for a large update.
|
|
1296
|
+
*/
|
|
1297
|
+
declare function calculateChunkCount(totalSize: number, chunkSize?: number): number;
|
|
1298
|
+
/**
|
|
1299
|
+
* Split a large update into chunks.
|
|
1300
|
+
*/
|
|
1301
|
+
declare function chunkUpdate(update: Uint8Array, chunkSize?: number): Uint8Array[];
|
|
1302
|
+
/**
|
|
1303
|
+
* Reassemble chunks into a single update.
|
|
1304
|
+
*/
|
|
1305
|
+
declare function reassembleChunks(chunks: Uint8Array[]): Uint8Array;
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Yjs State Integrity - Hash-at-Rest for Yjs document state
|
|
1309
|
+
*
|
|
1310
|
+
* Provides utilities to hash Yjs state on persist and verify on load,
|
|
1311
|
+
* detecting storage-level corruption before it propagates.
|
|
1312
|
+
*/
|
|
1313
|
+
/**
|
|
1314
|
+
* Compute a BLAKE3 hash of Yjs state bytes.
|
|
1315
|
+
*
|
|
1316
|
+
* @param state - Yjs state bytes (from Y.encodeStateAsUpdate)
|
|
1317
|
+
* @returns Hex-encoded BLAKE3 hash
|
|
1318
|
+
*/
|
|
1319
|
+
declare function hashYjsState(state: Uint8Array): string;
|
|
1320
|
+
/**
|
|
1321
|
+
* Verify that Yjs state bytes match an expected hash.
|
|
1322
|
+
*
|
|
1323
|
+
* @param state - Yjs state bytes to verify
|
|
1324
|
+
* @param expectedHash - Expected hex-encoded BLAKE3 hash
|
|
1325
|
+
* @returns true if hashes match, false if corrupted
|
|
1326
|
+
*/
|
|
1327
|
+
declare function verifyYjsStateIntegrity(state: Uint8Array, expectedHash: string): boolean;
|
|
1328
|
+
/**
|
|
1329
|
+
* Error thrown when Yjs state is corrupted (hash mismatch).
|
|
1330
|
+
*/
|
|
1331
|
+
declare class YjsIntegrityError extends Error {
|
|
1332
|
+
readonly docId: string;
|
|
1333
|
+
readonly expectedHash: string;
|
|
1334
|
+
readonly actualHash: string;
|
|
1335
|
+
constructor(docId: string, expectedHash: string, actualHash: string);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Persisted Yjs document state with integrity hash.
|
|
1339
|
+
*/
|
|
1340
|
+
interface PersistedDocState {
|
|
1341
|
+
/** The raw Yjs state (Y.encodeStateAsUpdate) */
|
|
1342
|
+
state: Uint8Array;
|
|
1343
|
+
/** BLAKE3 hex hash of state bytes */
|
|
1344
|
+
hash: string;
|
|
1345
|
+
/** When this was persisted */
|
|
1346
|
+
persistedAt: number;
|
|
1347
|
+
/** Number of updates merged since last snapshot */
|
|
1348
|
+
updateCount: number;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Create a PersistedDocState from Yjs state bytes.
|
|
1352
|
+
*
|
|
1353
|
+
* @param state - Yjs state bytes
|
|
1354
|
+
* @param updateCount - Number of updates in this state (default: 0)
|
|
1355
|
+
* @returns PersistedDocState with computed hash
|
|
1356
|
+
*/
|
|
1357
|
+
declare function createPersistedDocState(state: Uint8Array, updateCount?: number): PersistedDocState;
|
|
1358
|
+
/**
|
|
1359
|
+
* Verify a PersistedDocState's integrity.
|
|
1360
|
+
*
|
|
1361
|
+
* @param docId - Document ID (for error reporting)
|
|
1362
|
+
* @param record - The persisted state record
|
|
1363
|
+
* @throws YjsIntegrityError if hash doesn't match
|
|
1364
|
+
*/
|
|
1365
|
+
declare function verifyPersistedDocState(docId: string, record: PersistedDocState): void;
|
|
1366
|
+
/**
|
|
1367
|
+
* Safely load Yjs state, verifying integrity if hash is present.
|
|
1368
|
+
*
|
|
1369
|
+
* @param docId - Document ID
|
|
1370
|
+
* @param record - The persisted state record (may be legacy without hash)
|
|
1371
|
+
* @returns The state bytes
|
|
1372
|
+
* @throws YjsIntegrityError if hash exists and doesn't match
|
|
1373
|
+
*/
|
|
1374
|
+
declare function loadVerifiedState(docId: string, record: Partial<PersistedDocState> & {
|
|
1375
|
+
state: Uint8Array;
|
|
1376
|
+
}): Uint8Array;
|
|
1377
|
+
/**
|
|
1378
|
+
* Constants for compaction thresholds.
|
|
1379
|
+
*/
|
|
1380
|
+
/** Compact (re-encode and re-hash) after this many incremental updates */
|
|
1381
|
+
declare const COMPACTION_UPDATE_THRESHOLD = 100;
|
|
1382
|
+
/** Compact after this much time (ms) since last compaction */
|
|
1383
|
+
declare const COMPACTION_TIME_THRESHOLD = 3600000;
|
|
1384
|
+
/**
|
|
1385
|
+
* Check if a document should be compacted.
|
|
1386
|
+
*
|
|
1387
|
+
* @param updateCount - Number of updates since last compaction
|
|
1388
|
+
* @param persistedAt - When the state was last persisted
|
|
1389
|
+
* @returns true if compaction is recommended
|
|
1390
|
+
*/
|
|
1391
|
+
declare function shouldCompact(updateCount: number, persistedAt: number): boolean;
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Yjs Peer Scoring - Track Yjs-specific misbehavior and auto-block repeat offenders
|
|
1395
|
+
*
|
|
1396
|
+
* Extends the concept of peer scoring with Yjs-specific metrics.
|
|
1397
|
+
* Peers that repeatedly send invalid signatures, oversized updates, or exceed
|
|
1398
|
+
* rate limits accumulate negative scores and are eventually auto-blocked.
|
|
1399
|
+
*/
|
|
1400
|
+
/**
|
|
1401
|
+
* Yjs-specific peer metrics for tracking violations.
|
|
1402
|
+
*/
|
|
1403
|
+
interface YjsPeerMetrics {
|
|
1404
|
+
/** Invalid Ed25519 signatures on Yjs envelopes */
|
|
1405
|
+
invalidSignatures: number;
|
|
1406
|
+
/** Updates exceeding size limit */
|
|
1407
|
+
oversizedUpdates: number;
|
|
1408
|
+
/** Updates exceeding rate limit */
|
|
1409
|
+
rateExceeded: number;
|
|
1410
|
+
/** Unsigned updates when signing required */
|
|
1411
|
+
unsignedUpdates: number;
|
|
1412
|
+
/** Updates from unattested clientIDs (Tier 3) */
|
|
1413
|
+
unattestedClientIds: number;
|
|
1414
|
+
/** Updates rejected by authorization policy */
|
|
1415
|
+
unauthorizedUpdates: number;
|
|
1416
|
+
/** Total valid updates (for ratio calculations) */
|
|
1417
|
+
validUpdates: number;
|
|
1418
|
+
/** First seen timestamp */
|
|
1419
|
+
firstSeen: number;
|
|
1420
|
+
/** Last violation timestamp */
|
|
1421
|
+
lastViolation: number;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Violation types for peer scoring.
|
|
1425
|
+
*/
|
|
1426
|
+
type YjsViolationType = 'invalidSignature' | 'oversizedUpdate' | 'rateExceeded' | 'unsignedUpdate' | 'unattestedClientId' | 'unauthorizedUpdate';
|
|
1427
|
+
/**
|
|
1428
|
+
* Action to take based on peer score.
|
|
1429
|
+
*/
|
|
1430
|
+
type PeerAction = 'allow' | 'warn' | 'throttle' | 'block';
|
|
1431
|
+
/**
|
|
1432
|
+
* Optional telemetry collector interface for sync operations.
|
|
1433
|
+
* Compatible with @xnetjs/telemetry TelemetryCollector.
|
|
1434
|
+
*/
|
|
1435
|
+
interface SyncTelemetry {
|
|
1436
|
+
reportPerformance(metricName: string, durationMs: number, codeNamespace?: string): void;
|
|
1437
|
+
reportUsage(metricName: string, value: number): void;
|
|
1438
|
+
reportCrash(error: Error, context?: {
|
|
1439
|
+
codeNamespace?: string;
|
|
1440
|
+
}): void;
|
|
1441
|
+
reportSecurityEvent(eventName: string, severity: 'low' | 'medium' | 'high' | 'critical'): void;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Configuration for Yjs peer scoring.
|
|
1445
|
+
*/
|
|
1446
|
+
interface YjsScoringConfig {
|
|
1447
|
+
/** Points deducted per violation type */
|
|
1448
|
+
penalties: {
|
|
1449
|
+
invalidSignature: number;
|
|
1450
|
+
oversizedUpdate: number;
|
|
1451
|
+
rateExceeded: number;
|
|
1452
|
+
unsignedUpdate: number;
|
|
1453
|
+
unattestedClientId: number;
|
|
1454
|
+
unauthorizedUpdate: number;
|
|
1455
|
+
};
|
|
1456
|
+
/** Score thresholds */
|
|
1457
|
+
thresholds: {
|
|
1458
|
+
warn: number;
|
|
1459
|
+
throttle: number;
|
|
1460
|
+
block: number;
|
|
1461
|
+
};
|
|
1462
|
+
/** Score recovery rate (points per tick of good behavior) */
|
|
1463
|
+
recoveryRate: number;
|
|
1464
|
+
/** Immediate block after N invalid signatures */
|
|
1465
|
+
instantBlockAfter: number;
|
|
1466
|
+
/**
|
|
1467
|
+
* Optional telemetry collector for security events.
|
|
1468
|
+
* When provided, YjsPeerScorer will report:
|
|
1469
|
+
* - Security events for violations (invalid signatures, rate limits, etc.)
|
|
1470
|
+
* - Usage metrics for peer actions (block, throttle, warn)
|
|
1471
|
+
* - Performance metrics for score calculations
|
|
1472
|
+
*/
|
|
1473
|
+
telemetry?: SyncTelemetry;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Default scoring configuration.
|
|
1477
|
+
*/
|
|
1478
|
+
declare const DEFAULT_YJS_SCORING_CONFIG: YjsScoringConfig;
|
|
1479
|
+
/**
|
|
1480
|
+
* Peer scorer for Yjs-specific violations.
|
|
1481
|
+
*
|
|
1482
|
+
* Peers start at score 100. Violations deduct points based on severity.
|
|
1483
|
+
* Score thresholds trigger different actions (warn, throttle, block).
|
|
1484
|
+
*
|
|
1485
|
+
* @example
|
|
1486
|
+
* ```typescript
|
|
1487
|
+
* const scorer = new YjsPeerScorer()
|
|
1488
|
+
*
|
|
1489
|
+
* // On violation:
|
|
1490
|
+
* const action = scorer.penalize(peerId, 'invalidSignature')
|
|
1491
|
+
* if (action === 'block') {
|
|
1492
|
+
* ws.close(4403, 'Blocked due to repeated violations')
|
|
1493
|
+
* }
|
|
1494
|
+
*
|
|
1495
|
+
* // On valid update:
|
|
1496
|
+
* scorer.recordValid(peerId)
|
|
1497
|
+
*
|
|
1498
|
+
* // Periodic recovery:
|
|
1499
|
+
* setInterval(() => scorer.tick(), 60_000)
|
|
1500
|
+
* ```
|
|
1501
|
+
*/
|
|
1502
|
+
declare class YjsPeerScorer {
|
|
1503
|
+
private metrics;
|
|
1504
|
+
private scores;
|
|
1505
|
+
readonly config: YjsScoringConfig;
|
|
1506
|
+
private telemetry?;
|
|
1507
|
+
constructor(config?: Partial<YjsScoringConfig>);
|
|
1508
|
+
/**
|
|
1509
|
+
* Record a violation for a peer.
|
|
1510
|
+
*
|
|
1511
|
+
* @param peerId - Peer identifier
|
|
1512
|
+
* @param reason - Type of violation
|
|
1513
|
+
* @returns Action to take based on new score
|
|
1514
|
+
*/
|
|
1515
|
+
penalize(peerId: string, reason: YjsViolationType): PeerAction;
|
|
1516
|
+
/**
|
|
1517
|
+
* Record a valid update (for ratio tracking + score recovery consideration).
|
|
1518
|
+
*/
|
|
1519
|
+
recordValid(peerId: string): void;
|
|
1520
|
+
/**
|
|
1521
|
+
* Get current score for a peer.
|
|
1522
|
+
* New peers start at 100.
|
|
1523
|
+
*/
|
|
1524
|
+
getScore(peerId: string): number;
|
|
1525
|
+
/**
|
|
1526
|
+
* Get action for a given score.
|
|
1527
|
+
*/
|
|
1528
|
+
getAction(score: number): PeerAction;
|
|
1529
|
+
/**
|
|
1530
|
+
* Get current action for a peer.
|
|
1531
|
+
*/
|
|
1532
|
+
getPeerAction(peerId: string): PeerAction;
|
|
1533
|
+
/**
|
|
1534
|
+
* Recover scores over time (called periodically).
|
|
1535
|
+
* Only recovers if no recent violations.
|
|
1536
|
+
*
|
|
1537
|
+
* @param recoveryWindow - Time in ms that must pass without violations (default: 60s)
|
|
1538
|
+
*/
|
|
1539
|
+
tick(recoveryWindow?: number): void;
|
|
1540
|
+
/**
|
|
1541
|
+
* Get metrics for a peer (for debugging/monitoring).
|
|
1542
|
+
*/
|
|
1543
|
+
getMetrics(peerId: string): YjsPeerMetrics | undefined;
|
|
1544
|
+
/**
|
|
1545
|
+
* Get all peer IDs with metrics.
|
|
1546
|
+
*/
|
|
1547
|
+
getAllPeerIds(): string[];
|
|
1548
|
+
/**
|
|
1549
|
+
* Get all metrics (for monitoring endpoint).
|
|
1550
|
+
*/
|
|
1551
|
+
getAllMetrics(): Map<string, YjsPeerMetrics>;
|
|
1552
|
+
/**
|
|
1553
|
+
* Remove all state for a disconnected peer.
|
|
1554
|
+
*/
|
|
1555
|
+
remove(peerId: string): void;
|
|
1556
|
+
/**
|
|
1557
|
+
* Clear all state.
|
|
1558
|
+
*/
|
|
1559
|
+
clear(): void;
|
|
1560
|
+
/**
|
|
1561
|
+
* Get violation ratio for a peer.
|
|
1562
|
+
* Returns the ratio of violations to total operations.
|
|
1563
|
+
*/
|
|
1564
|
+
getViolationRatio(peerId: string): number;
|
|
1565
|
+
private getOrCreateMetrics;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Yjs authorization primitives: encrypted-at-rest state and peer auth gate.
|
|
1570
|
+
*/
|
|
1571
|
+
|
|
1572
|
+
interface EncryptedYjsState {
|
|
1573
|
+
nodeId: string;
|
|
1574
|
+
version: 1;
|
|
1575
|
+
encryptedState: Uint8Array;
|
|
1576
|
+
nonce: Uint8Array;
|
|
1577
|
+
stateVector: Uint8Array;
|
|
1578
|
+
stateHash: Uint8Array;
|
|
1579
|
+
checkpointedAt: number;
|
|
1580
|
+
updatesSinceCheckpoint: number;
|
|
1581
|
+
}
|
|
1582
|
+
declare class YjsStateIntegrityError extends Error {
|
|
1583
|
+
constructor(message?: string);
|
|
1584
|
+
}
|
|
1585
|
+
declare function encryptYjsState(state: Uint8Array, nodeId: string, contentKey: Uint8Array, options?: {
|
|
1586
|
+
stateVector?: Uint8Array;
|
|
1587
|
+
checkpointedAt?: number;
|
|
1588
|
+
updatesSinceCheckpoint?: number;
|
|
1589
|
+
}): EncryptedYjsState;
|
|
1590
|
+
declare function decryptYjsState(encrypted: EncryptedYjsState, contentKey: Uint8Array): Uint8Array;
|
|
1591
|
+
declare function serializeEncryptedYjsState(state: EncryptedYjsState): Uint8Array;
|
|
1592
|
+
declare function deserializeEncryptedYjsState(bytes: Uint8Array): EncryptedYjsState;
|
|
1593
|
+
interface YjsAuthDecision {
|
|
1594
|
+
allowed: boolean;
|
|
1595
|
+
authorDID: DID;
|
|
1596
|
+
cached: boolean;
|
|
1597
|
+
}
|
|
1598
|
+
interface YjsAuthGateOptions {
|
|
1599
|
+
cacheTTL?: number;
|
|
1600
|
+
now?: () => number;
|
|
1601
|
+
}
|
|
1602
|
+
declare class YjsAuthGate {
|
|
1603
|
+
private readonly evaluator;
|
|
1604
|
+
private readonly nodeId;
|
|
1605
|
+
private static readonly DEFAULT_CACHE_TTL;
|
|
1606
|
+
private readonly peerAuthCache;
|
|
1607
|
+
private readonly cacheTTL;
|
|
1608
|
+
private readonly now;
|
|
1609
|
+
constructor(evaluator: PolicyEvaluator, nodeId: string, options?: YjsAuthGateOptions);
|
|
1610
|
+
canApplyUpdate(envelope: Pick<SignedYjsEnvelopeV2, 'meta'>): Promise<YjsAuthDecision>;
|
|
1611
|
+
invalidatePeer(did: DID): void;
|
|
1612
|
+
invalidateAll(): void;
|
|
1613
|
+
}
|
|
1614
|
+
interface YjsCheckpointerOptions {
|
|
1615
|
+
maxUpdates?: number;
|
|
1616
|
+
maxAgeMs?: number;
|
|
1617
|
+
now?: () => number;
|
|
1618
|
+
}
|
|
1619
|
+
declare class YjsCheckpointer {
|
|
1620
|
+
private readonly maxUpdates;
|
|
1621
|
+
private readonly maxAgeMs;
|
|
1622
|
+
private readonly now;
|
|
1623
|
+
constructor(options?: YjsCheckpointerOptions);
|
|
1624
|
+
shouldCheckpoint(state: Pick<EncryptedYjsState, 'updatesSinceCheckpoint' | 'checkpointedAt'>): boolean;
|
|
1625
|
+
checkpoint(input: {
|
|
1626
|
+
state: Uint8Array;
|
|
1627
|
+
nodeId: string;
|
|
1628
|
+
contentKey: Uint8Array;
|
|
1629
|
+
stateVector?: Uint8Array;
|
|
1630
|
+
}): EncryptedYjsState;
|
|
1631
|
+
}
|
|
1632
|
+
declare function toEncryptedData(state: EncryptedYjsState): EncryptedData;
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Yjs authorized sync manager and provider.
|
|
1636
|
+
*/
|
|
1637
|
+
|
|
1638
|
+
type GrantLikeNode = {
|
|
1639
|
+
schemaId?: string;
|
|
1640
|
+
properties?: Record<string, unknown>;
|
|
1641
|
+
};
|
|
1642
|
+
type GrantEvent = {
|
|
1643
|
+
node?: GrantLikeNode | null;
|
|
1644
|
+
};
|
|
1645
|
+
interface YDocLike {
|
|
1646
|
+
emit?: (event: string, args: unknown[]) => void;
|
|
1647
|
+
}
|
|
1648
|
+
interface YDocCodec<TDoc extends YDocLike = YDocLike> {
|
|
1649
|
+
createDoc(nodeId: string): TDoc;
|
|
1650
|
+
applyUpdate(doc: TDoc, update: Uint8Array, origin?: string): void;
|
|
1651
|
+
encodeStateAsUpdate(doc: TDoc): Uint8Array;
|
|
1652
|
+
encodeStateVector(doc: TDoc): Uint8Array;
|
|
1653
|
+
}
|
|
1654
|
+
type AuthorizedRoom<TDoc extends YDocLike = YDocLike> = {
|
|
1655
|
+
nodeId: string;
|
|
1656
|
+
doc: TDoc;
|
|
1657
|
+
contentKey: Uint8Array;
|
|
1658
|
+
authGate: YjsAuthGate;
|
|
1659
|
+
authorizedPeers: Set<DID>;
|
|
1660
|
+
};
|
|
1661
|
+
type AuthorizedDoc<TDoc extends YDocLike = YDocLike> = {
|
|
1662
|
+
doc: TDoc;
|
|
1663
|
+
nodeId: string;
|
|
1664
|
+
mode: 'read' | 'write';
|
|
1665
|
+
contentKey: Uint8Array;
|
|
1666
|
+
room?: AuthorizedRoom<TDoc>;
|
|
1667
|
+
release: () => void;
|
|
1668
|
+
};
|
|
1669
|
+
interface AuthorizedStateAdapter {
|
|
1670
|
+
getDocumentContent(nodeId: string): Promise<Uint8Array | null>;
|
|
1671
|
+
setDocumentContent(nodeId: string, content: Uint8Array): Promise<void>;
|
|
1672
|
+
}
|
|
1673
|
+
interface GrantEventStore {
|
|
1674
|
+
subscribe(listener: (event: GrantEvent) => void): () => void;
|
|
1675
|
+
}
|
|
1676
|
+
interface ContentKeyProvider {
|
|
1677
|
+
getOrUnwrap(nodeId: string): Promise<Uint8Array>;
|
|
1678
|
+
}
|
|
1679
|
+
interface RecipientKeyResolver {
|
|
1680
|
+
resolveBatch(dids: DID[]): Promise<Map<DID, Uint8Array>>;
|
|
1681
|
+
}
|
|
1682
|
+
interface AuthorizedSyncManagerOptions<TDoc extends YDocLike = YDocLike> {
|
|
1683
|
+
authorDID: DID;
|
|
1684
|
+
evaluator: PolicyEvaluator;
|
|
1685
|
+
adapter: AuthorizedStateAdapter;
|
|
1686
|
+
store: GrantEventStore;
|
|
1687
|
+
keyProvider: ContentKeyProvider;
|
|
1688
|
+
ydoc: YDocCodec<TDoc>;
|
|
1689
|
+
publicKeyResolver?: RecipientKeyResolver;
|
|
1690
|
+
onRotateContentKey?: (input: {
|
|
1691
|
+
nodeId: string;
|
|
1692
|
+
recipients: DID[];
|
|
1693
|
+
wrappedKeys: Record<string, WrappedKey>;
|
|
1694
|
+
contentKey: Uint8Array;
|
|
1695
|
+
}) => Promise<void>;
|
|
1696
|
+
}
|
|
1697
|
+
declare class AuthorizedSyncManager<TDoc extends YDocLike = YDocLike> {
|
|
1698
|
+
private readonly options;
|
|
1699
|
+
private readonly rooms;
|
|
1700
|
+
constructor(options: AuthorizedSyncManagerOptions<TDoc>);
|
|
1701
|
+
acquire(nodeId: string, mode?: 'read' | 'write'): Promise<AuthorizedDoc<TDoc>>;
|
|
1702
|
+
release(nodeId: string): void;
|
|
1703
|
+
private joinRoom;
|
|
1704
|
+
private wireRevocationEvents;
|
|
1705
|
+
private handlePeerRevocation;
|
|
1706
|
+
}
|
|
1707
|
+
interface AuthorizedYjsSyncProviderOptions<TDoc extends YDocLike = YDocLike> {
|
|
1708
|
+
nodeId: string;
|
|
1709
|
+
doc: TDoc;
|
|
1710
|
+
ydoc: Pick<YDocCodec<TDoc>, 'applyUpdate'>;
|
|
1711
|
+
authGate: YjsAuthGate;
|
|
1712
|
+
peerScorer?: YjsPeerScorer;
|
|
1713
|
+
rateLimiter?: {
|
|
1714
|
+
allow(peerId: DID): boolean;
|
|
1715
|
+
};
|
|
1716
|
+
verifyEnvelope?: (envelope: SignedYjsEnvelopeV2) => Promise<EnvelopeVerificationResult>;
|
|
1717
|
+
onRejected?: (input: {
|
|
1718
|
+
peerId: DID;
|
|
1719
|
+
reason: 'rate-exceeded' | 'invalid-signature' | 'unauthorized';
|
|
1720
|
+
}) => void;
|
|
1721
|
+
}
|
|
1722
|
+
declare class AuthorizedYjsSyncProvider<TDoc extends YDocLike = YDocLike> {
|
|
1723
|
+
private readonly nodeId;
|
|
1724
|
+
private readonly doc;
|
|
1725
|
+
private readonly ydoc;
|
|
1726
|
+
private readonly authGate;
|
|
1727
|
+
private readonly peerScorer;
|
|
1728
|
+
private readonly rateLimiter?;
|
|
1729
|
+
private readonly verifyEnvelope;
|
|
1730
|
+
private readonly onRejected?;
|
|
1731
|
+
constructor(options: AuthorizedYjsSyncProviderOptions<TDoc>);
|
|
1732
|
+
handleRemoteUpdate(envelope: SignedYjsEnvelopeV2): Promise<boolean>;
|
|
1733
|
+
}
|
|
1734
|
+
declare class AuthorizedYjsError extends Error {
|
|
1735
|
+
readonly code: 'PERMISSION_DENIED';
|
|
1736
|
+
readonly decision: AuthDecision;
|
|
1737
|
+
constructor(code: 'PERMISSION_DENIED', decision: AuthDecision);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* ClientID-to-DID Binding - Cryptographically bind Yjs clientIDs to DIDs
|
|
1742
|
+
*
|
|
1743
|
+
* Yjs assigns random integer clientIDs that have no cryptographic binding to
|
|
1744
|
+
* the author's identity. This module provides signed attestations that bind
|
|
1745
|
+
* a clientID to a DID for verifiable author attribution.
|
|
1746
|
+
*
|
|
1747
|
+
* V2 introduces multi-level signature support (Ed25519 and/or ML-DSA-65).
|
|
1748
|
+
*/
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* A signed attestation binding a Yjs clientID to a DID (V1 format).
|
|
1752
|
+
* @deprecated Use ClientIdAttestationV2 for new code
|
|
1753
|
+
*/
|
|
1754
|
+
interface ClientIdAttestationV1 {
|
|
1755
|
+
/** The Yjs clientID being attested */
|
|
1756
|
+
clientId: number;
|
|
1757
|
+
/** The DID claiming this clientID */
|
|
1758
|
+
did: string;
|
|
1759
|
+
/** Ed25519 signature over BLAKE3(payload) */
|
|
1760
|
+
signature: Uint8Array;
|
|
1761
|
+
/** Session expiry (Unix timestamp seconds) */
|
|
1762
|
+
expiresAt: number;
|
|
1763
|
+
/** Room this attestation applies to */
|
|
1764
|
+
room: string;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* A signed attestation binding a Yjs clientID to a DID (V2 format with multi-level signatures).
|
|
1768
|
+
*/
|
|
1769
|
+
interface ClientIdAttestationV2 {
|
|
1770
|
+
/** Wire format version */
|
|
1771
|
+
v: 2;
|
|
1772
|
+
/** The Yjs clientID being attested */
|
|
1773
|
+
clientId: number;
|
|
1774
|
+
/** The DID claiming this clientID */
|
|
1775
|
+
did: DID;
|
|
1776
|
+
/** When this binding was created (ms since epoch) */
|
|
1777
|
+
timestamp: number;
|
|
1778
|
+
/** Session expiry (Unix timestamp ms) */
|
|
1779
|
+
expiresAt?: number;
|
|
1780
|
+
/** Room this attestation applies to */
|
|
1781
|
+
room: string;
|
|
1782
|
+
/** Multi-level signature */
|
|
1783
|
+
signature: UnifiedSignature;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Wire format for ClientIdAttestationV2.
|
|
1787
|
+
*/
|
|
1788
|
+
interface ClientIdAttestationWire {
|
|
1789
|
+
v: 2;
|
|
1790
|
+
c: number;
|
|
1791
|
+
d: string;
|
|
1792
|
+
t: number;
|
|
1793
|
+
e?: number;
|
|
1794
|
+
r: string;
|
|
1795
|
+
s: SignatureWire;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Union type for all attestation versions.
|
|
1799
|
+
*/
|
|
1800
|
+
type ClientIdAttestation = ClientIdAttestationV1 | ClientIdAttestationV2;
|
|
1801
|
+
/**
|
|
1802
|
+
* Result of attestation verification (V1 format).
|
|
1803
|
+
*/
|
|
1804
|
+
interface AttestationVerifyResult {
|
|
1805
|
+
valid: boolean;
|
|
1806
|
+
reason?: 'expired' | 'invalid_signature' | 'did_resolution_failed';
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Result of attestation verification (V2 format with details).
|
|
1810
|
+
*/
|
|
1811
|
+
interface AttestationVerificationResult {
|
|
1812
|
+
valid: boolean;
|
|
1813
|
+
expired: boolean;
|
|
1814
|
+
level: SecurityLevel;
|
|
1815
|
+
errors: string[];
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Options for creating an attestation.
|
|
1819
|
+
*/
|
|
1820
|
+
interface CreateAttestationOptions {
|
|
1821
|
+
/** Expiration time in ms (optional) */
|
|
1822
|
+
expiresInMs?: number;
|
|
1823
|
+
/** Security level (default: 0 for Ed25519-only) */
|
|
1824
|
+
level?: SecurityLevel;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Options for verifying an attestation.
|
|
1828
|
+
*/
|
|
1829
|
+
interface VerifyAttestationOptions {
|
|
1830
|
+
/** PQ key registry for Level 1/2 verification */
|
|
1831
|
+
registry?: PQKeyRegistry;
|
|
1832
|
+
/** Minimum security level required */
|
|
1833
|
+
minLevel?: SecurityLevel;
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Check if an attestation is V2 format.
|
|
1837
|
+
*/
|
|
1838
|
+
declare function isV2Attestation(attestation: ClientIdAttestation): attestation is ClientIdAttestationV2;
|
|
1839
|
+
/**
|
|
1840
|
+
* Check if an attestation is V1 format.
|
|
1841
|
+
*/
|
|
1842
|
+
declare function isV1Attestation(attestation: ClientIdAttestation): attestation is ClientIdAttestationV1;
|
|
1843
|
+
/**
|
|
1844
|
+
* Create a signed clientID attestation (V1 format).
|
|
1845
|
+
*
|
|
1846
|
+
* @deprecated Use createClientIdAttestationV2() for new code
|
|
1847
|
+
*/
|
|
1848
|
+
declare function createClientIdAttestationV1(clientId: number, did: string, privateKey: Uint8Array, room: string, ttlSeconds?: number): ClientIdAttestationV1;
|
|
1849
|
+
/**
|
|
1850
|
+
* Verify a V1 clientID attestation.
|
|
1851
|
+
*
|
|
1852
|
+
* @deprecated Use verifyClientIdAttestationV2() for V2 attestations
|
|
1853
|
+
*/
|
|
1854
|
+
declare function verifyClientIdAttestationV1(attestation: ClientIdAttestationV1): AttestationVerifyResult;
|
|
1855
|
+
/**
|
|
1856
|
+
* Create a signed clientID attestation (V2 format with multi-level signatures).
|
|
1857
|
+
*
|
|
1858
|
+
* @param clientId - The Yjs clientID to attest
|
|
1859
|
+
* @param room - The room this attestation applies to
|
|
1860
|
+
* @param keyBundle - Key bundle for signing
|
|
1861
|
+
* @param options - Creation options
|
|
1862
|
+
* @returns Signed attestation
|
|
1863
|
+
*
|
|
1864
|
+
* @example
|
|
1865
|
+
* ```typescript
|
|
1866
|
+
* const attestation = createClientIdAttestationV2(doc.clientID, 'xnet-doc-abc', keyBundle)
|
|
1867
|
+
* ws.send({ type: 'clientid-bind', room, attestation: serializeAttestation(attestation) })
|
|
1868
|
+
* ```
|
|
1869
|
+
*/
|
|
1870
|
+
declare function createClientIdAttestationV2(clientId: number, room: string, keyBundle: HybridKeyBundle, options?: CreateAttestationOptions): ClientIdAttestationV2;
|
|
1871
|
+
/**
|
|
1872
|
+
* Verify a V2 clientID attestation with multi-level signature support.
|
|
1873
|
+
*
|
|
1874
|
+
* @param attestation - The attestation to verify
|
|
1875
|
+
* @param options - Verification options
|
|
1876
|
+
* @returns Verification result with detailed errors
|
|
1877
|
+
*/
|
|
1878
|
+
declare function verifyClientIdAttestationV2(attestation: ClientIdAttestationV2, options?: VerifyAttestationOptions): Promise<AttestationVerificationResult>;
|
|
1879
|
+
/**
|
|
1880
|
+
* Serialize V2 attestation to wire format.
|
|
1881
|
+
*/
|
|
1882
|
+
declare function serializeClientIdAttestation(attestation: ClientIdAttestationV2): ClientIdAttestationWire;
|
|
1883
|
+
/**
|
|
1884
|
+
* Deserialize V2 attestation from wire format.
|
|
1885
|
+
*/
|
|
1886
|
+
declare function deserializeClientIdAttestation(wire: ClientIdAttestationWire): ClientIdAttestationV2;
|
|
1887
|
+
/**
|
|
1888
|
+
* Create a signed clientID attestation.
|
|
1889
|
+
*
|
|
1890
|
+
* @overload V1 signature (legacy)
|
|
1891
|
+
*/
|
|
1892
|
+
declare function createClientIdAttestation(clientId: number, did: string, privateKey: Uint8Array, room: string, ttlSeconds?: number): ClientIdAttestationV1;
|
|
1893
|
+
/**
|
|
1894
|
+
* @overload V2 signature (multi-level)
|
|
1895
|
+
*/
|
|
1896
|
+
declare function createClientIdAttestation(clientId: number, room: string, keyBundle: HybridKeyBundle, options?: CreateAttestationOptions): ClientIdAttestationV2;
|
|
1897
|
+
/**
|
|
1898
|
+
* Verify a clientID attestation (auto-detects version).
|
|
1899
|
+
*/
|
|
1900
|
+
declare function verifyClientIdAttestation(attestation: ClientIdAttestationV1): AttestationVerifyResult;
|
|
1901
|
+
declare function verifyClientIdAttestation(attestation: ClientIdAttestationV2, options?: VerifyAttestationOptions): Promise<AttestationVerificationResult>;
|
|
1902
|
+
/**
|
|
1903
|
+
* Interface for a per-room clientID map.
|
|
1904
|
+
*/
|
|
1905
|
+
interface ClientIdMap {
|
|
1906
|
+
/** Look up DID by clientId */
|
|
1907
|
+
getOwner(clientId: number): string | undefined;
|
|
1908
|
+
/** Look up clientId by DID */
|
|
1909
|
+
getClientId(did: string): number | undefined;
|
|
1910
|
+
/** Register a verified attestation */
|
|
1911
|
+
register(attestation: ClientIdAttestation): void;
|
|
1912
|
+
/** Remove expired bindings */
|
|
1913
|
+
cleanup(): void;
|
|
1914
|
+
/** Get all active bindings */
|
|
1915
|
+
getAll(): Array<{
|
|
1916
|
+
clientId: number;
|
|
1917
|
+
did: string;
|
|
1918
|
+
expiresAt: number;
|
|
1919
|
+
}>;
|
|
1920
|
+
/** Check if a clientID is attested */
|
|
1921
|
+
has(clientId: number): boolean;
|
|
1922
|
+
/** Get the number of active bindings */
|
|
1923
|
+
size(): number;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Implementation of ClientIdMap for tracking clientID->DID bindings.
|
|
1927
|
+
*
|
|
1928
|
+
* @example
|
|
1929
|
+
* ```typescript
|
|
1930
|
+
* const map = new ClientIdMapImpl()
|
|
1931
|
+
*
|
|
1932
|
+
* // On receiving attestation:
|
|
1933
|
+
* const result = verifyClientIdAttestation(attestation)
|
|
1934
|
+
* if (result.valid) {
|
|
1935
|
+
* map.register(attestation)
|
|
1936
|
+
* }
|
|
1937
|
+
*
|
|
1938
|
+
* // On receiving update:
|
|
1939
|
+
* const owner = map.getOwner(envelope.clientId)
|
|
1940
|
+
* if (owner && owner !== envelope.authorDID) {
|
|
1941
|
+
* // ClientID claimed by different DID - reject!
|
|
1942
|
+
* }
|
|
1943
|
+
* ```
|
|
1944
|
+
*/
|
|
1945
|
+
declare class ClientIdMapImpl implements ClientIdMap {
|
|
1946
|
+
private byClientId;
|
|
1947
|
+
private byDid;
|
|
1948
|
+
getOwner(clientId: number): string | undefined;
|
|
1949
|
+
getClientId(did: string): number | undefined;
|
|
1950
|
+
register(attestation: ClientIdAttestation): void;
|
|
1951
|
+
cleanup(): void;
|
|
1952
|
+
getAll(): Array<{
|
|
1953
|
+
clientId: number;
|
|
1954
|
+
did: string;
|
|
1955
|
+
expiresAt: number;
|
|
1956
|
+
}>;
|
|
1957
|
+
has(clientId: number): boolean;
|
|
1958
|
+
size(): number;
|
|
1959
|
+
/**
|
|
1960
|
+
* Get the count of non-expired entries without mutating state.
|
|
1961
|
+
* This is useful for accurate counts but slower than size().
|
|
1962
|
+
*/
|
|
1963
|
+
activeCount(): number;
|
|
1964
|
+
/**
|
|
1965
|
+
* Clear all bindings.
|
|
1966
|
+
*/
|
|
1967
|
+
clear(): void;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Check if an update's clientID matches the expected owner.
|
|
1971
|
+
*
|
|
1972
|
+
* @param clientId - The clientID from the update
|
|
1973
|
+
* @param authorDID - The DID from the envelope
|
|
1974
|
+
* @param map - The clientID map
|
|
1975
|
+
* @returns true if valid (matches or no binding exists), false if mismatch
|
|
1976
|
+
*/
|
|
1977
|
+
declare function validateClientIdOwnership(clientId: number, authorDID: string, map: ClientIdMap): boolean;
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* YjsChange - Wrap Yjs updates in the Change<T> envelope for hash chain integration
|
|
1981
|
+
*
|
|
1982
|
+
* This provides the strongest security tier: Yjs updates become entries in the
|
|
1983
|
+
* same per-node hash chain as NodeChanges. This gives rich text the same
|
|
1984
|
+
* guarantees as structured data — signed, hashed, chain-linked, with a full
|
|
1985
|
+
* audit trail of who typed what and when.
|
|
1986
|
+
*
|
|
1987
|
+
* See: docs/plans/plan03_4_1YjsSecurity/08-hash-chain-integration.md
|
|
1988
|
+
*/
|
|
1989
|
+
|
|
1990
|
+
/**
|
|
1991
|
+
* Change type identifier for Yjs changes.
|
|
1992
|
+
*/
|
|
1993
|
+
declare const YJS_CHANGE_TYPE = "yjs-update";
|
|
1994
|
+
/**
|
|
1995
|
+
* Payload for a YjsChange - contains the batched Yjs update.
|
|
1996
|
+
*/
|
|
1997
|
+
interface YjsUpdatePayload {
|
|
1998
|
+
/** The node this update belongs to */
|
|
1999
|
+
nodeId: string;
|
|
2000
|
+
/** Batched Yjs update bytes (one or more merged updates) */
|
|
2001
|
+
update: Uint8Array;
|
|
2002
|
+
/** Yjs clientID for this author (verified via Step 07) */
|
|
2003
|
+
clientId: number;
|
|
2004
|
+
/** Number of individual Yjs updates batched into this change */
|
|
2005
|
+
updateCount: number;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* A Yjs update wrapped in the Change<T> envelope.
|
|
2009
|
+
* Gets: id, hash, parentHash, signature, authorDID, lamport, wallTime
|
|
2010
|
+
*/
|
|
2011
|
+
type YjsChange = Change<YjsUpdatePayload>;
|
|
2012
|
+
/**
|
|
2013
|
+
* Unsigned version of YjsChange (before signing).
|
|
2014
|
+
*/
|
|
2015
|
+
type UnsignedYjsChange = UnsignedChange<YjsUpdatePayload>;
|
|
2016
|
+
/**
|
|
2017
|
+
* Options for creating a YjsChange.
|
|
2018
|
+
*/
|
|
2019
|
+
interface CreateYjsChangeOptions {
|
|
2020
|
+
/** The node this update belongs to */
|
|
2021
|
+
nodeId: string;
|
|
2022
|
+
/** Batched Yjs update bytes */
|
|
2023
|
+
update: Uint8Array;
|
|
2024
|
+
/** Yjs clientID for this author */
|
|
2025
|
+
clientId: number;
|
|
2026
|
+
/** Number of individual updates in this batch */
|
|
2027
|
+
updateCount: number;
|
|
2028
|
+
/** Author's DID */
|
|
2029
|
+
authorDID: string;
|
|
2030
|
+
/** Author's Ed25519 private key for signing */
|
|
2031
|
+
privateKey: Uint8Array;
|
|
2032
|
+
/** Hash of the previous change in the chain (null for first) */
|
|
2033
|
+
parentHash: ContentId | null;
|
|
2034
|
+
/** Lamport timestamp for ordering */
|
|
2035
|
+
lamport: LamportTimestamp;
|
|
2036
|
+
/** Optional wall time (defaults to Date.now()) */
|
|
2037
|
+
wallTime?: number;
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Create an unsigned YjsChange.
|
|
2041
|
+
*
|
|
2042
|
+
* @param options - Change options
|
|
2043
|
+
* @returns Unsigned change ready for signing
|
|
2044
|
+
*/
|
|
2045
|
+
declare function createUnsignedYjsChange(options: Omit<CreateYjsChangeOptions, 'privateKey'>): UnsignedYjsChange;
|
|
2046
|
+
/**
|
|
2047
|
+
* Create a signed YjsChange.
|
|
2048
|
+
*
|
|
2049
|
+
* @param options - Change options including private key
|
|
2050
|
+
* @returns Signed YjsChange with hash and signature
|
|
2051
|
+
*
|
|
2052
|
+
* @example
|
|
2053
|
+
* ```typescript
|
|
2054
|
+
* const change = createYjsChange({
|
|
2055
|
+
* nodeId: 'page-123',
|
|
2056
|
+
* update: Y.encodeStateAsUpdate(doc),
|
|
2057
|
+
* clientId: doc.clientID,
|
|
2058
|
+
* updateCount: 5,
|
|
2059
|
+
* authorDID: identity.did,
|
|
2060
|
+
* privateKey: identity.privateKey,
|
|
2061
|
+
* parentHash: lastChangeHash,
|
|
2062
|
+
* lamport: { time: 42, did: identity.did }
|
|
2063
|
+
* })
|
|
2064
|
+
* ```
|
|
2065
|
+
*/
|
|
2066
|
+
declare function createYjsChange(options: CreateYjsChangeOptions): YjsChange;
|
|
2067
|
+
/**
|
|
2068
|
+
* Type guard to check if a change is a YjsChange.
|
|
2069
|
+
*
|
|
2070
|
+
* @param change - Any change object
|
|
2071
|
+
* @returns true if the change is a YjsChange
|
|
2072
|
+
*/
|
|
2073
|
+
declare function isYjsChange(change: Change<unknown>): change is YjsChange;
|
|
2074
|
+
/**
|
|
2075
|
+
* Type guard to check if a change is a NodeChange (not a YjsChange).
|
|
2076
|
+
* NodeChanges have 'properties' and 'schemaId' in their payload.
|
|
2077
|
+
*/
|
|
2078
|
+
declare function isNodeChange(change: Change<unknown>): boolean;
|
|
2079
|
+
/**
|
|
2080
|
+
* Extract the nodeId from any change (works for both NodeChange and YjsChange).
|
|
2081
|
+
*/
|
|
2082
|
+
declare function getChangeNodeId(change: Change<unknown>): string | undefined;
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* YjsBatcher - Batch Yjs updates for efficient hash chain integration
|
|
2086
|
+
*
|
|
2087
|
+
* Individual keystrokes generate ~5 updates/sec. Wrapping each in a full
|
|
2088
|
+
* Change<T> is wasteful. Instead, batch updates over a configurable window.
|
|
2089
|
+
*
|
|
2090
|
+
* With 2-second batching:
|
|
2091
|
+
* - Updates/sec: 5 → 0.5
|
|
2092
|
+
* - Overhead/sec: 1KB → 100B
|
|
2093
|
+
* - Signature ops/sec: 5 → 0.5
|
|
2094
|
+
*
|
|
2095
|
+
* See: docs/plans/plan03_4_1YjsSecurity/08-hash-chain-integration.md
|
|
2096
|
+
*/
|
|
2097
|
+
/**
|
|
2098
|
+
* Configuration for the YjsBatcher.
|
|
2099
|
+
*/
|
|
2100
|
+
interface YjsBatcherConfig {
|
|
2101
|
+
/** Batch window in milliseconds (default: 2000) */
|
|
2102
|
+
batchWindowMs: number;
|
|
2103
|
+
/** Max updates per batch before flush (default: 50) */
|
|
2104
|
+
maxBatchSize: number;
|
|
2105
|
+
/** Flush on paragraph break / Enter key (default: true) */
|
|
2106
|
+
flushOnParagraph: boolean;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Default configuration for YjsBatcher.
|
|
2110
|
+
*/
|
|
2111
|
+
declare const DEFAULT_BATCHER_CONFIG: YjsBatcherConfig;
|
|
2112
|
+
/**
|
|
2113
|
+
* Callback invoked when a batch is flushed.
|
|
2114
|
+
*
|
|
2115
|
+
* @param mergedUpdate - The merged Yjs update bytes (from Y.mergeUpdates)
|
|
2116
|
+
* @param updateCount - Number of individual updates in this batch
|
|
2117
|
+
*/
|
|
2118
|
+
type BatchFlushCallback = (mergedUpdate: Uint8Array, updateCount: number) => void;
|
|
2119
|
+
/**
|
|
2120
|
+
* Function that merges multiple Yjs updates into one.
|
|
2121
|
+
* This is typically Y.mergeUpdates from the 'yjs' package.
|
|
2122
|
+
*/
|
|
2123
|
+
type MergeUpdatesFn = (updates: Uint8Array[]) => Uint8Array;
|
|
2124
|
+
/**
|
|
2125
|
+
* YjsBatcher collects Yjs updates and flushes them in batches.
|
|
2126
|
+
*
|
|
2127
|
+
* This reduces the overhead of wrapping each update in a signed Change<T>
|
|
2128
|
+
* while still maintaining a full audit trail.
|
|
2129
|
+
*
|
|
2130
|
+
* @example
|
|
2131
|
+
* ```typescript
|
|
2132
|
+
* import * as Y from 'yjs'
|
|
2133
|
+
*
|
|
2134
|
+
* const batcher = new YjsBatcher(
|
|
2135
|
+
* (mergedUpdate, count) => {
|
|
2136
|
+
* // Create a YjsChange from the merged update
|
|
2137
|
+
* const change = createYjsChange({
|
|
2138
|
+
* nodeId,
|
|
2139
|
+
* update: mergedUpdate,
|
|
2140
|
+
* updateCount: count,
|
|
2141
|
+
* ...
|
|
2142
|
+
* })
|
|
2143
|
+
* // Append to hash chain
|
|
2144
|
+
* store.appendChange(change)
|
|
2145
|
+
* },
|
|
2146
|
+
* { batchWindowMs: 2000 },
|
|
2147
|
+
* Y.mergeUpdates // Pass the merge function
|
|
2148
|
+
* )
|
|
2149
|
+
*
|
|
2150
|
+
* // On each local Yjs update:
|
|
2151
|
+
* doc.on('update', (update, origin) => {
|
|
2152
|
+
* if (origin !== 'remote') {
|
|
2153
|
+
* batcher.add(update)
|
|
2154
|
+
* }
|
|
2155
|
+
* })
|
|
2156
|
+
*
|
|
2157
|
+
* // On component unmount:
|
|
2158
|
+
* batcher.destroy()
|
|
2159
|
+
* ```
|
|
2160
|
+
*/
|
|
2161
|
+
declare class YjsBatcher {
|
|
2162
|
+
private pendingUpdates;
|
|
2163
|
+
private flushTimer;
|
|
2164
|
+
private config;
|
|
2165
|
+
private onFlush;
|
|
2166
|
+
private mergeUpdates;
|
|
2167
|
+
private destroyed;
|
|
2168
|
+
/**
|
|
2169
|
+
* Create a new YjsBatcher.
|
|
2170
|
+
*
|
|
2171
|
+
* @param onFlush - Callback invoked when a batch is flushed
|
|
2172
|
+
* @param config - Optional configuration overrides
|
|
2173
|
+
* @param mergeUpdates - Function to merge updates (required - use Y.mergeUpdates from yjs)
|
|
2174
|
+
*/
|
|
2175
|
+
constructor(onFlush: BatchFlushCallback, config?: Partial<YjsBatcherConfig>, mergeUpdates?: MergeUpdatesFn);
|
|
2176
|
+
/**
|
|
2177
|
+
* Add an update to the current batch.
|
|
2178
|
+
*
|
|
2179
|
+
* @param update - The Yjs update bytes
|
|
2180
|
+
* @param isParagraphBreak - Whether this update is a paragraph break (Enter key)
|
|
2181
|
+
*/
|
|
2182
|
+
add(update: Uint8Array, isParagraphBreak?: boolean): void;
|
|
2183
|
+
/**
|
|
2184
|
+
* Force flush the current batch.
|
|
2185
|
+
* Safe to call even if there are no pending updates.
|
|
2186
|
+
*/
|
|
2187
|
+
flush(): void;
|
|
2188
|
+
/**
|
|
2189
|
+
* Check if there are pending updates.
|
|
2190
|
+
*/
|
|
2191
|
+
hasPending(): boolean;
|
|
2192
|
+
/**
|
|
2193
|
+
* Get the number of pending updates.
|
|
2194
|
+
*/
|
|
2195
|
+
pendingCount(): number;
|
|
2196
|
+
/**
|
|
2197
|
+
* Destroy the batcher. Flushes any remaining updates.
|
|
2198
|
+
* After destroy, add() calls will be ignored.
|
|
2199
|
+
*/
|
|
2200
|
+
destroy(): void;
|
|
2201
|
+
/**
|
|
2202
|
+
* Check if the batcher has been destroyed.
|
|
2203
|
+
*/
|
|
2204
|
+
isDestroyed(): boolean;
|
|
2205
|
+
/**
|
|
2206
|
+
* Reset or start the flush timer.
|
|
2207
|
+
*/
|
|
2208
|
+
private resetTimer;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* Serializer types for version-specific change encoding.
|
|
2213
|
+
*
|
|
2214
|
+
* Each protocol version may have a different wire format for changes.
|
|
2215
|
+
* Serializers handle encoding and decoding for their specific version.
|
|
2216
|
+
*/
|
|
2217
|
+
|
|
2218
|
+
/**
|
|
2219
|
+
* Serialized change format.
|
|
2220
|
+
* Can be either binary (Uint8Array) or JSON-compatible object.
|
|
2221
|
+
*/
|
|
2222
|
+
type SerializedChange = Uint8Array | Record<string, unknown>;
|
|
2223
|
+
/**
|
|
2224
|
+
* Result of deserializing a change.
|
|
2225
|
+
*/
|
|
2226
|
+
interface DeserializeResult<T = unknown> {
|
|
2227
|
+
success: true;
|
|
2228
|
+
change: Change<T>;
|
|
2229
|
+
}
|
|
2230
|
+
interface DeserializeError {
|
|
2231
|
+
success: false;
|
|
2232
|
+
error: string;
|
|
2233
|
+
/** Raw data that failed to deserialize */
|
|
2234
|
+
rawData?: unknown;
|
|
2235
|
+
}
|
|
2236
|
+
type DeserializeOutcome<T = unknown> = DeserializeResult<T> | DeserializeError;
|
|
2237
|
+
/**
|
|
2238
|
+
* Interface for version-specific change serializers.
|
|
2239
|
+
*
|
|
2240
|
+
* Each serializer handles:
|
|
2241
|
+
* - Encoding changes for network transmission
|
|
2242
|
+
* - Decoding changes from network
|
|
2243
|
+
* - Validating change format
|
|
2244
|
+
*/
|
|
2245
|
+
interface ChangeSerializer {
|
|
2246
|
+
/**
|
|
2247
|
+
* Protocol version this serializer handles.
|
|
2248
|
+
*/
|
|
2249
|
+
readonly version: number;
|
|
2250
|
+
/**
|
|
2251
|
+
* Human-readable name for this serializer.
|
|
2252
|
+
*/
|
|
2253
|
+
readonly name: string;
|
|
2254
|
+
/**
|
|
2255
|
+
* Serialize a change for transmission.
|
|
2256
|
+
*
|
|
2257
|
+
* @param change - The change to serialize
|
|
2258
|
+
* @returns Serialized data (binary or JSON object)
|
|
2259
|
+
*/
|
|
2260
|
+
serialize<T>(change: Change<T>): SerializedChange;
|
|
2261
|
+
/**
|
|
2262
|
+
* Deserialize a change from received data.
|
|
2263
|
+
*
|
|
2264
|
+
* @param data - Raw data (binary or JSON object)
|
|
2265
|
+
* @returns Deserialized change or error
|
|
2266
|
+
*/
|
|
2267
|
+
deserialize<T = unknown>(data: SerializedChange): DeserializeOutcome<T>;
|
|
2268
|
+
/**
|
|
2269
|
+
* Check if this serializer can handle the given data.
|
|
2270
|
+
* Used for format detection.
|
|
2271
|
+
*
|
|
2272
|
+
* @param data - Raw data to check
|
|
2273
|
+
* @returns true if this serializer can deserialize the data
|
|
2274
|
+
*/
|
|
2275
|
+
canDeserialize(data: unknown): boolean;
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Options for serialization.
|
|
2279
|
+
*/
|
|
2280
|
+
interface SerializeOptions {
|
|
2281
|
+
/**
|
|
2282
|
+
* Whether to use binary encoding (more compact) or JSON (more readable).
|
|
2283
|
+
* Default: true for production, false for debugging.
|
|
2284
|
+
*/
|
|
2285
|
+
binary?: boolean;
|
|
2286
|
+
/**
|
|
2287
|
+
* Whether to include optional fields with default values.
|
|
2288
|
+
* Default: false (omit default values).
|
|
2289
|
+
*/
|
|
2290
|
+
includeDefaults?: boolean;
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Registry for managing multiple serializer versions.
|
|
2294
|
+
*/
|
|
2295
|
+
interface SerializerRegistry {
|
|
2296
|
+
/**
|
|
2297
|
+
* Get serializer for a specific version.
|
|
2298
|
+
*/
|
|
2299
|
+
get(version: number): ChangeSerializer | undefined;
|
|
2300
|
+
/**
|
|
2301
|
+
* Get the default (latest) serializer.
|
|
2302
|
+
*/
|
|
2303
|
+
getDefault(): ChangeSerializer;
|
|
2304
|
+
/**
|
|
2305
|
+
* Register a serializer.
|
|
2306
|
+
*/
|
|
2307
|
+
register(serializer: ChangeSerializer): void;
|
|
2308
|
+
/**
|
|
2309
|
+
* Get all registered versions.
|
|
2310
|
+
*/
|
|
2311
|
+
getVersions(): number[];
|
|
2312
|
+
/**
|
|
2313
|
+
* Auto-detect serializer for incoming data.
|
|
2314
|
+
*/
|
|
2315
|
+
detect(data: unknown): ChangeSerializer | undefined;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
/**
|
|
2319
|
+
* V1 Serializer - Original change format.
|
|
2320
|
+
*
|
|
2321
|
+
* Protocol v1 uses JSON encoding with the following characteristics:
|
|
2322
|
+
* - protocolVersion field may be missing (legacy) or 1
|
|
2323
|
+
* - Signature is base64 encoded
|
|
2324
|
+
* - All fields use full names (no abbreviations)
|
|
2325
|
+
*
|
|
2326
|
+
* Wire format (JSON):
|
|
2327
|
+
* {
|
|
2328
|
+
* protocolVersion?: 1,
|
|
2329
|
+
* id: string,
|
|
2330
|
+
* type: string,
|
|
2331
|
+
* payload: T,
|
|
2332
|
+
* hash: string,
|
|
2333
|
+
* parentHash: string | null,
|
|
2334
|
+
* authorDID: string,
|
|
2335
|
+
* signature: string (base64),
|
|
2336
|
+
* wallTime: number,
|
|
2337
|
+
* lamport: { time: number, did: string },
|
|
2338
|
+
* batchId?: string,
|
|
2339
|
+
* batchIndex?: number,
|
|
2340
|
+
* batchSize?: number
|
|
2341
|
+
* }
|
|
2342
|
+
*/
|
|
2343
|
+
|
|
2344
|
+
/**
|
|
2345
|
+
* V1 Serializer implementation.
|
|
2346
|
+
*/
|
|
2347
|
+
declare class V1Serializer implements ChangeSerializer {
|
|
2348
|
+
readonly version = 1;
|
|
2349
|
+
readonly name = "V1 JSON Serializer";
|
|
2350
|
+
serialize<T>(change: Change<T>): SerializedChange;
|
|
2351
|
+
deserialize<T = unknown>(data: SerializedChange): DeserializeOutcome<T>;
|
|
2352
|
+
canDeserialize(data: unknown): boolean;
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Default V1 serializer instance.
|
|
2356
|
+
*/
|
|
2357
|
+
declare const v1Serializer: V1Serializer;
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* V2 Serializer - Enhanced change format with schema versioning.
|
|
2361
|
+
*
|
|
2362
|
+
* Protocol v2 extends v1 with:
|
|
2363
|
+
* - Required protocolVersion field (always 2)
|
|
2364
|
+
* - Schema version in payload (_sv field)
|
|
2365
|
+
* - Abbreviated field names for compact transmission
|
|
2366
|
+
* - Support for binary payload compression (future)
|
|
2367
|
+
*
|
|
2368
|
+
* Wire format (JSON, abbreviated):
|
|
2369
|
+
* {
|
|
2370
|
+
* v: 2, // protocolVersion
|
|
2371
|
+
* i: string, // id
|
|
2372
|
+
* t: string, // type
|
|
2373
|
+
* p: { ..., _sv?: string }, // payload with optional schema version
|
|
2374
|
+
* h: string, // hash
|
|
2375
|
+
* ph: string | null, // parentHash
|
|
2376
|
+
* a: string, // authorDID
|
|
2377
|
+
* s: string, // signature (base64)
|
|
2378
|
+
* w: number, // wallTime
|
|
2379
|
+
* l: { t: number, a: string }, // lamport { time, author }
|
|
2380
|
+
* bi?: string, // batchId
|
|
2381
|
+
* bx?: number, // batchIndex
|
|
2382
|
+
* bs?: number // batchSize
|
|
2383
|
+
* }
|
|
2384
|
+
*/
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* V2 Serializer implementation.
|
|
2388
|
+
* Uses abbreviated field names for more compact wire format.
|
|
2389
|
+
*/
|
|
2390
|
+
declare class V2Serializer implements ChangeSerializer {
|
|
2391
|
+
readonly version = 2;
|
|
2392
|
+
readonly name = "V2 Compact Serializer";
|
|
2393
|
+
serialize<T>(change: Change<T>): SerializedChange;
|
|
2394
|
+
deserialize<T = unknown>(data: SerializedChange): DeserializeOutcome<T>;
|
|
2395
|
+
canDeserialize(data: unknown): boolean;
|
|
2396
|
+
/**
|
|
2397
|
+
* Add schema version to a payload.
|
|
2398
|
+
* Used when serializing node changes with schema versioning.
|
|
2399
|
+
*/
|
|
2400
|
+
static addSchemaVersion<T extends Record<string, unknown>>(payload: T, schemaVersion: string): T & {
|
|
2401
|
+
_sv: string;
|
|
2402
|
+
};
|
|
2403
|
+
/**
|
|
2404
|
+
* Extract schema version from a payload.
|
|
2405
|
+
*/
|
|
2406
|
+
static getSchemaVersion(payload: unknown): string | undefined;
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Default V2 serializer instance.
|
|
2410
|
+
*/
|
|
2411
|
+
declare const v2Serializer: V2Serializer;
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Serializer registry and exports.
|
|
2415
|
+
*
|
|
2416
|
+
* This module provides:
|
|
2417
|
+
* - Version-specific serializers (V1, V2, V3)
|
|
2418
|
+
* - A registry for managing serializers
|
|
2419
|
+
* - Auto-detection for incoming data
|
|
2420
|
+
* - Helper function for selecting serializer by version
|
|
2421
|
+
*
|
|
2422
|
+
* V3 is the default for new changes (multi-level signature support).
|
|
2423
|
+
*/
|
|
2424
|
+
|
|
2425
|
+
/**
|
|
2426
|
+
* Default serializer registry implementation.
|
|
2427
|
+
*/
|
|
2428
|
+
declare class DefaultSerializerRegistry implements SerializerRegistry {
|
|
2429
|
+
private serializers;
|
|
2430
|
+
private defaultVersion;
|
|
2431
|
+
constructor(defaultVersion?: number);
|
|
2432
|
+
get(version: number): ChangeSerializer | undefined;
|
|
2433
|
+
getDefault(): ChangeSerializer;
|
|
2434
|
+
register(serializer: ChangeSerializer): void;
|
|
2435
|
+
getVersions(): number[];
|
|
2436
|
+
detect(data: unknown): ChangeSerializer | undefined;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Default serializer registry instance.
|
|
2440
|
+
*/
|
|
2441
|
+
declare const serializerRegistry: DefaultSerializerRegistry;
|
|
2442
|
+
/**
|
|
2443
|
+
* Get a serializer for a specific protocol version.
|
|
2444
|
+
*
|
|
2445
|
+
* @param version - Protocol version number
|
|
2446
|
+
* @returns Serializer for that version, or undefined if not found
|
|
2447
|
+
*/
|
|
2448
|
+
declare function getSerializer(version: number): ChangeSerializer | undefined;
|
|
2449
|
+
/**
|
|
2450
|
+
* Get the default serializer (for current protocol version).
|
|
2451
|
+
*/
|
|
2452
|
+
declare function getDefaultSerializer(): ChangeSerializer;
|
|
2453
|
+
/**
|
|
2454
|
+
* Detect and deserialize a change from raw data.
|
|
2455
|
+
* Automatically detects the format and uses appropriate serializer.
|
|
2456
|
+
*
|
|
2457
|
+
* @param data - Raw serialized data
|
|
2458
|
+
* @returns Deserialized change or error
|
|
2459
|
+
*/
|
|
2460
|
+
declare function autoDeserialize<T = unknown>(data: SerializedChange): DeserializeOutcome<T>;
|
|
2461
|
+
/**
|
|
2462
|
+
* Serialize a change using the appropriate serializer for its protocol version.
|
|
2463
|
+
*
|
|
2464
|
+
* @param change - Change to serialize
|
|
2465
|
+
* @returns Serialized data
|
|
2466
|
+
*/
|
|
2467
|
+
declare function autoSerialize<T>(change: Change<T>): SerializedChange;
|
|
2468
|
+
/**
|
|
2469
|
+
* Create a serializer registry with custom serializers.
|
|
2470
|
+
*
|
|
2471
|
+
* @param defaultVersion - Default protocol version to use
|
|
2472
|
+
* @param serializers - Optional additional serializers to register
|
|
2473
|
+
* @returns New serializer registry
|
|
2474
|
+
*/
|
|
2475
|
+
declare function createSerializerRegistry(defaultVersion?: number, serializers?: ChangeSerializer[]): SerializerRegistry;
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Change Handler Registry - Version-specific handlers for processing changes.
|
|
2479
|
+
*
|
|
2480
|
+
* This registry allows registering handlers by change type and version range,
|
|
2481
|
+
* enabling backward-compatible processing of older change formats.
|
|
2482
|
+
*
|
|
2483
|
+
* @example
|
|
2484
|
+
* ```typescript
|
|
2485
|
+
* const registry = new ChangeHandlerRegistry()
|
|
2486
|
+
*
|
|
2487
|
+
* registry.register({
|
|
2488
|
+
* type: 'yjs-update',
|
|
2489
|
+
* minVersion: 1,
|
|
2490
|
+
* maxVersion: Infinity,
|
|
2491
|
+
* canHandle: (change) => change.type === 'yjs-update',
|
|
2492
|
+
* process: async (change, ctx) => { ... },
|
|
2493
|
+
* validate: (change) => ({ valid: true, errors: [] })
|
|
2494
|
+
* })
|
|
2495
|
+
*
|
|
2496
|
+
* await registry.process(incomingChange, context)
|
|
2497
|
+
* ```
|
|
2498
|
+
*/
|
|
2499
|
+
|
|
2500
|
+
/**
|
|
2501
|
+
* Result of validating a change.
|
|
2502
|
+
*/
|
|
2503
|
+
interface ValidationResult {
|
|
2504
|
+
/** Whether the change is valid */
|
|
2505
|
+
valid: boolean;
|
|
2506
|
+
/** Error messages if validation failed */
|
|
2507
|
+
errors: string[];
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Context provided to change handlers during processing.
|
|
2511
|
+
*/
|
|
2512
|
+
interface HandlerContext {
|
|
2513
|
+
/** Store an unknown change for later processing */
|
|
2514
|
+
storeUnknown: (change: Change<unknown>) => Promise<void>;
|
|
2515
|
+
/** Emit an event */
|
|
2516
|
+
emit: (event: string, data: Record<string, unknown>) => void;
|
|
2517
|
+
/** The author DID of the current user */
|
|
2518
|
+
authorDID?: string;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Handler for processing a specific type and version range of changes.
|
|
2522
|
+
*/
|
|
2523
|
+
interface ChangeHandler<T = unknown> {
|
|
2524
|
+
/** Change type this handler processes (e.g., 'yjs-update', 'record-create') */
|
|
2525
|
+
type: string;
|
|
2526
|
+
/** Minimum protocol version this handler supports (inclusive) */
|
|
2527
|
+
minVersion: number;
|
|
2528
|
+
/** Maximum protocol version this handler supports (inclusive, use Infinity for latest) */
|
|
2529
|
+
maxVersion: number;
|
|
2530
|
+
/**
|
|
2531
|
+
* Check if this handler can process the given change.
|
|
2532
|
+
* Called after type and version matching for additional checks.
|
|
2533
|
+
*/
|
|
2534
|
+
canHandle(change: Change<unknown>): boolean;
|
|
2535
|
+
/**
|
|
2536
|
+
* Process the change and apply it to the store/state.
|
|
2537
|
+
*/
|
|
2538
|
+
process(change: Change<T>, context: HandlerContext): Promise<void>;
|
|
2539
|
+
/**
|
|
2540
|
+
* Validate the change structure and content.
|
|
2541
|
+
* Should check that all required fields are present and valid.
|
|
2542
|
+
*/
|
|
2543
|
+
validate(change: Change<T>): ValidationResult;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Registry for version-specific change handlers.
|
|
2547
|
+
*
|
|
2548
|
+
* Handlers are matched by:
|
|
2549
|
+
* 1. Change type (exact match)
|
|
2550
|
+
* 2. Protocol version (within handler's min/max range)
|
|
2551
|
+
* 3. Optional canHandle() for additional checks
|
|
2552
|
+
*
|
|
2553
|
+
* If no exact match is found, falls back to the newest handler that
|
|
2554
|
+
* can process older versions (for backward compatibility).
|
|
2555
|
+
*/
|
|
2556
|
+
declare class ChangeHandlerRegistry {
|
|
2557
|
+
private handlers;
|
|
2558
|
+
private unknownTypeListeners;
|
|
2559
|
+
private invalidChangeListeners;
|
|
2560
|
+
/**
|
|
2561
|
+
* Register a handler for a change type and version range.
|
|
2562
|
+
*/
|
|
2563
|
+
register<T>(handler: ChangeHandler<T>): void;
|
|
2564
|
+
/**
|
|
2565
|
+
* Unregister all handlers for a type.
|
|
2566
|
+
*/
|
|
2567
|
+
unregister(type: string): boolean;
|
|
2568
|
+
/**
|
|
2569
|
+
* Get the appropriate handler for a change.
|
|
2570
|
+
* Returns null if no handler can process this change.
|
|
2571
|
+
*/
|
|
2572
|
+
getHandler(change: Change<unknown>): ChangeHandler<unknown> | null;
|
|
2573
|
+
/**
|
|
2574
|
+
* Check if any handler can process this change.
|
|
2575
|
+
*/
|
|
2576
|
+
canProcess(change: Change<unknown>): boolean;
|
|
2577
|
+
/**
|
|
2578
|
+
* Process a change using the appropriate handler.
|
|
2579
|
+
*/
|
|
2580
|
+
process(change: Change<unknown>, context: HandlerContext): Promise<void>;
|
|
2581
|
+
/**
|
|
2582
|
+
* Get all registered handler types.
|
|
2583
|
+
*/
|
|
2584
|
+
getTypes(): string[];
|
|
2585
|
+
/**
|
|
2586
|
+
* Get handlers for a specific type.
|
|
2587
|
+
*/
|
|
2588
|
+
getHandlersForType(type: string): ChangeHandler<unknown>[];
|
|
2589
|
+
/**
|
|
2590
|
+
* Subscribe to unknown change type events.
|
|
2591
|
+
*/
|
|
2592
|
+
onUnknownType(listener: (change: Change<unknown>) => void): () => void;
|
|
2593
|
+
/**
|
|
2594
|
+
* Subscribe to invalid change events.
|
|
2595
|
+
*/
|
|
2596
|
+
onInvalidChange(listener: (change: Change<unknown>, errors: string[]) => void): () => void;
|
|
2597
|
+
/**
|
|
2598
|
+
* Clear all registered handlers.
|
|
2599
|
+
*/
|
|
2600
|
+
clear(): void;
|
|
2601
|
+
private notifyUnknownType;
|
|
2602
|
+
private notifyInvalidChange;
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Default global change handler registry instance.
|
|
2606
|
+
*/
|
|
2607
|
+
declare const changeHandlerRegistry: ChangeHandlerRegistry;
|
|
2608
|
+
/**
|
|
2609
|
+
* Create a simple handler that accepts all versions.
|
|
2610
|
+
*/
|
|
2611
|
+
declare function createHandler<T>(type: string, process: (change: Change<T>, context: HandlerContext) => Promise<void>, validate?: (change: Change<T>) => ValidationResult): ChangeHandler<T>;
|
|
2612
|
+
/**
|
|
2613
|
+
* Create a handler for a specific version range.
|
|
2614
|
+
*/
|
|
2615
|
+
declare function createVersionedHandler<T>(type: string, minVersion: number, maxVersion: number, process: (change: Change<T>, context: HandlerContext) => Promise<void>, validate?: (change: Change<T>) => ValidationResult): ChangeHandler<T>;
|
|
2616
|
+
/**
|
|
2617
|
+
* Create a mock context for testing handlers.
|
|
2618
|
+
*/
|
|
2619
|
+
declare function createTestContext(overrides?: Partial<HandlerContext>): HandlerContext;
|
|
2620
|
+
/**
|
|
2621
|
+
* A validation error with optional context.
|
|
2622
|
+
*/
|
|
2623
|
+
interface ValidationError {
|
|
2624
|
+
/** Error code */
|
|
2625
|
+
code: string;
|
|
2626
|
+
/** Human-readable message */
|
|
2627
|
+
message: string;
|
|
2628
|
+
/** Property path if applicable */
|
|
2629
|
+
path?: string;
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* A validation warning (non-fatal issue).
|
|
2633
|
+
*/
|
|
2634
|
+
interface ValidationWarning {
|
|
2635
|
+
/** Warning code */
|
|
2636
|
+
code: string;
|
|
2637
|
+
/** Human-readable message */
|
|
2638
|
+
message: string;
|
|
2639
|
+
/** Property path if applicable */
|
|
2640
|
+
path?: string;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Events emitted during change processing.
|
|
2644
|
+
*/
|
|
2645
|
+
type HandlerEvent = {
|
|
2646
|
+
type: 'unknownChangeType';
|
|
2647
|
+
change: Change<unknown>;
|
|
2648
|
+
} | {
|
|
2649
|
+
type: 'invalidChange';
|
|
2650
|
+
change: Change<unknown>;
|
|
2651
|
+
errors: string[];
|
|
2652
|
+
} | {
|
|
2653
|
+
type: 'processed';
|
|
2654
|
+
change: Change<unknown>;
|
|
2655
|
+
duration: number;
|
|
2656
|
+
};
|
|
2657
|
+
/**
|
|
2658
|
+
* Result of processing a change.
|
|
2659
|
+
*/
|
|
2660
|
+
interface ProcessResult {
|
|
2661
|
+
/** Whether processing succeeded */
|
|
2662
|
+
success: boolean;
|
|
2663
|
+
/** Handler that processed the change (if any) */
|
|
2664
|
+
handlerType?: string;
|
|
2665
|
+
/** Processing duration in ms */
|
|
2666
|
+
duration: number;
|
|
2667
|
+
/** Errors if processing failed */
|
|
2668
|
+
errors?: string[];
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Statistics about the registry.
|
|
2672
|
+
*/
|
|
2673
|
+
interface RegistryStats {
|
|
2674
|
+
/** Number of registered handler types */
|
|
2675
|
+
typeCount: number;
|
|
2676
|
+
/** Total number of handlers */
|
|
2677
|
+
handlerCount: number;
|
|
2678
|
+
/** Types with multiple version-specific handlers */
|
|
2679
|
+
versionedTypes: string[];
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
* Data Integrity Verification
|
|
2684
|
+
*
|
|
2685
|
+
* Utilities for verifying the integrity of change data, including:
|
|
2686
|
+
* - Hash verification
|
|
2687
|
+
* - Signature verification
|
|
2688
|
+
* - Chain integrity checks
|
|
2689
|
+
* - Detection of corruption and repair suggestions
|
|
2690
|
+
*
|
|
2691
|
+
* @example
|
|
2692
|
+
* ```typescript
|
|
2693
|
+
* const report = await verifyIntegrity(changes)
|
|
2694
|
+
* if (report.issues.length > 0) {
|
|
2695
|
+
* console.log('Issues found:', report.issues)
|
|
2696
|
+
* if (report.repairable) {
|
|
2697
|
+
* console.log('All issues can be repaired')
|
|
2698
|
+
* }
|
|
2699
|
+
* }
|
|
2700
|
+
* ```
|
|
2701
|
+
*/
|
|
2702
|
+
|
|
2703
|
+
/**
|
|
2704
|
+
* Types of integrity issues that can be detected.
|
|
2705
|
+
*/
|
|
2706
|
+
type IntegrityIssueType = 'hash-mismatch' | 'signature-invalid' | 'chain-broken' | 'missing-parent' | 'duplicate-id' | 'invalid-lamport' | 'future-timestamp';
|
|
2707
|
+
/**
|
|
2708
|
+
* Possible repair actions for integrity issues.
|
|
2709
|
+
*/
|
|
2710
|
+
type RepairActionType = 'recompute-hash' | 'request-from-peers' | 'remove-duplicate' | 'mark-orphan' | 'none';
|
|
2711
|
+
/**
|
|
2712
|
+
* A repair action that can be taken to fix an integrity issue.
|
|
2713
|
+
*/
|
|
2714
|
+
interface RepairAction {
|
|
2715
|
+
type: RepairActionType;
|
|
2716
|
+
description: string;
|
|
2717
|
+
/** Whether this repair can be done automatically */
|
|
2718
|
+
automatic: boolean;
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* An integrity issue found during verification.
|
|
2722
|
+
*/
|
|
2723
|
+
interface IntegrityIssue {
|
|
2724
|
+
/** The change ID that has the issue */
|
|
2725
|
+
changeId: string;
|
|
2726
|
+
/** Type of issue */
|
|
2727
|
+
type: IntegrityIssueType;
|
|
2728
|
+
/** Human-readable description */
|
|
2729
|
+
details: string;
|
|
2730
|
+
/** Severity: error = data may be corrupted, warning = anomaly detected */
|
|
2731
|
+
severity: 'error' | 'warning';
|
|
2732
|
+
/** Suggested repair action (if any) */
|
|
2733
|
+
repairAction?: RepairAction;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Result of an integrity verification.
|
|
2737
|
+
*/
|
|
2738
|
+
interface IntegrityReport {
|
|
2739
|
+
/** Number of changes checked */
|
|
2740
|
+
checked: number;
|
|
2741
|
+
/** Number of changes that passed all checks */
|
|
2742
|
+
valid: number;
|
|
2743
|
+
/** List of issues found */
|
|
2744
|
+
issues: IntegrityIssue[];
|
|
2745
|
+
/** Whether all issues can be repaired */
|
|
2746
|
+
repairable: boolean;
|
|
2747
|
+
/** Summary statistics */
|
|
2748
|
+
summary: {
|
|
2749
|
+
errors: number;
|
|
2750
|
+
warnings: number;
|
|
2751
|
+
byType: Record<IntegrityIssueType, number>;
|
|
2752
|
+
};
|
|
2753
|
+
/** Time taken for verification (ms) */
|
|
2754
|
+
durationMs: number;
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Options for integrity verification.
|
|
2758
|
+
*/
|
|
2759
|
+
interface VerifyOptions {
|
|
2760
|
+
/** Skip signature verification (faster but less thorough) */
|
|
2761
|
+
skipSignatures?: boolean;
|
|
2762
|
+
/** Skip hash verification */
|
|
2763
|
+
skipHashes?: boolean;
|
|
2764
|
+
/** Skip chain verification */
|
|
2765
|
+
skipChain?: boolean;
|
|
2766
|
+
/** Maximum future timestamp allowed (ms from now) */
|
|
2767
|
+
maxFutureTimestamp?: number;
|
|
2768
|
+
/** Progress callback */
|
|
2769
|
+
onProgress?: (checked: number, total: number) => void;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Verify the integrity of a set of changes.
|
|
2773
|
+
*
|
|
2774
|
+
* Checks performed:
|
|
2775
|
+
* 1. Hash verification - computed hash matches stored hash
|
|
2776
|
+
* 2. Signature verification - Ed25519 signature is valid
|
|
2777
|
+
* 3. Chain integrity - parent hashes exist and form valid chains
|
|
2778
|
+
* 4. Duplicate detection - no duplicate change IDs
|
|
2779
|
+
* 5. Timestamp validation - no future timestamps
|
|
2780
|
+
*/
|
|
2781
|
+
declare function verifyIntegrity(changes: Change<unknown>[], options?: VerifyOptions): Promise<IntegrityReport>;
|
|
2782
|
+
/**
|
|
2783
|
+
* Quick integrity check - only verifies hashes, not signatures.
|
|
2784
|
+
* Much faster but less thorough.
|
|
2785
|
+
*/
|
|
2786
|
+
declare function quickIntegrityCheck(changes: Change<unknown>[]): Promise<IntegrityReport>;
|
|
2787
|
+
/**
|
|
2788
|
+
* Verify a single change.
|
|
2789
|
+
*/
|
|
2790
|
+
declare function verifySingleChange(change: Change<unknown>, options?: VerifyOptions): Promise<{
|
|
2791
|
+
valid: boolean;
|
|
2792
|
+
issues: IntegrityIssue[];
|
|
2793
|
+
}>;
|
|
2794
|
+
/**
|
|
2795
|
+
* Find orphaned changes (changes whose parents are missing).
|
|
2796
|
+
*/
|
|
2797
|
+
declare function findOrphans(changes: Change<unknown>[]): Change<unknown>[];
|
|
2798
|
+
/**
|
|
2799
|
+
* Find root changes (changes with no parent).
|
|
2800
|
+
*/
|
|
2801
|
+
declare function findRoots(changes: Change<unknown>[]): Change<unknown>[];
|
|
2802
|
+
/**
|
|
2803
|
+
* Find head changes (changes that are not parents of any other change).
|
|
2804
|
+
*/
|
|
2805
|
+
declare function findHeads(changes: Change<unknown>[]): Change<unknown>[];
|
|
2806
|
+
/**
|
|
2807
|
+
* Get the chain depth (longest path from any root to any head).
|
|
2808
|
+
*/
|
|
2809
|
+
declare function getChainDepth(changes: Change<unknown>[]): number;
|
|
2810
|
+
/**
|
|
2811
|
+
* Attempt to repair issues that can be fixed automatically.
|
|
2812
|
+
* Returns the repaired changes and any issues that couldn't be fixed.
|
|
2813
|
+
*/
|
|
2814
|
+
declare function attemptRepair(changes: Change<unknown>[], issues: IntegrityIssue[]): Promise<{
|
|
2815
|
+
repaired: Change<unknown>[];
|
|
2816
|
+
remainingIssues: IntegrityIssue[];
|
|
2817
|
+
repairCount: number;
|
|
2818
|
+
}>;
|
|
2819
|
+
/**
|
|
2820
|
+
* Generate a human-readable integrity report.
|
|
2821
|
+
*/
|
|
2822
|
+
declare function formatIntegrityReport(report: IntegrityReport): string;
|
|
2823
|
+
|
|
2824
|
+
/**
|
|
2825
|
+
* Deprecation System
|
|
2826
|
+
*
|
|
2827
|
+
* Tracks deprecated protocols, schemas, and features to help developers
|
|
2828
|
+
* migrate away from outdated functionality before it's removed.
|
|
2829
|
+
*
|
|
2830
|
+
* @example
|
|
2831
|
+
* ```typescript
|
|
2832
|
+
* // Check for deprecation warnings
|
|
2833
|
+
* const warnings = checkDeprecations({
|
|
2834
|
+
* protocolVersion: 0,
|
|
2835
|
+
* schemas: ['xnet://xnet.fyi/Task@1.0.0'],
|
|
2836
|
+
* features: ['legacy-auth']
|
|
2837
|
+
* })
|
|
2838
|
+
*
|
|
2839
|
+
* for (const warning of warnings) {
|
|
2840
|
+
* console.warn(warning.message)
|
|
2841
|
+
* }
|
|
2842
|
+
* ```
|
|
2843
|
+
*/
|
|
2844
|
+
|
|
2845
|
+
/**
|
|
2846
|
+
* Schema IRI type (mirrors @xnetjs/data to avoid circular dependency).
|
|
2847
|
+
* Format: xnet://namespace/Name@version
|
|
2848
|
+
*/
|
|
2849
|
+
type SchemaIRI = `xnet://${string}/${string}` | `xnet://${string}/${string}@${string}`;
|
|
2850
|
+
/**
|
|
2851
|
+
* Type of deprecated item.
|
|
2852
|
+
*/
|
|
2853
|
+
type DeprecationType = 'protocol' | 'schema' | 'feature' | 'api';
|
|
2854
|
+
/**
|
|
2855
|
+
* A deprecation notice for a protocol, schema, feature, or API.
|
|
2856
|
+
*/
|
|
2857
|
+
interface DeprecationNotice {
|
|
2858
|
+
/** Type of deprecated item */
|
|
2859
|
+
type: DeprecationType;
|
|
2860
|
+
/** Identifier of the deprecated item */
|
|
2861
|
+
subject: string;
|
|
2862
|
+
/** Human-readable description */
|
|
2863
|
+
description: string;
|
|
2864
|
+
/** Version where this was deprecated */
|
|
2865
|
+
deprecatedIn: string;
|
|
2866
|
+
/** Version where this will be/was removed (if known) */
|
|
2867
|
+
removedIn?: string;
|
|
2868
|
+
/** What to use instead */
|
|
2869
|
+
alternative?: string;
|
|
2870
|
+
/** URL to migration documentation */
|
|
2871
|
+
migrationGuide?: string;
|
|
2872
|
+
/** Date when deprecated */
|
|
2873
|
+
deprecatedDate?: string;
|
|
2874
|
+
/** Date when removed/sunset (if known) */
|
|
2875
|
+
sunsetDate?: string;
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Context for checking deprecations.
|
|
2879
|
+
*/
|
|
2880
|
+
interface DeprecationContext {
|
|
2881
|
+
/** Current protocol version in use */
|
|
2882
|
+
protocolVersion?: number;
|
|
2883
|
+
/** Schema IRIs currently in use */
|
|
2884
|
+
schemas?: (SchemaIRI | string)[];
|
|
2885
|
+
/** Features currently enabled */
|
|
2886
|
+
features?: (FeatureFlag | string)[];
|
|
2887
|
+
/** Package version */
|
|
2888
|
+
packageVersion?: string;
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* A deprecation warning generated from context.
|
|
2892
|
+
*/
|
|
2893
|
+
interface DeprecationWarning {
|
|
2894
|
+
/** The deprecation notice that triggered this warning */
|
|
2895
|
+
notice: DeprecationNotice;
|
|
2896
|
+
/** Human-readable warning message */
|
|
2897
|
+
message: string;
|
|
2898
|
+
/** Recommended action to take */
|
|
2899
|
+
action: string;
|
|
2900
|
+
/** Severity: 'warning' for deprecated, 'error' for removed */
|
|
2901
|
+
severity: 'warning' | 'error';
|
|
2902
|
+
/** Days until removal (if sunsetDate is known) */
|
|
2903
|
+
daysUntilRemoval?: number;
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Callback for deprecation events.
|
|
2907
|
+
*/
|
|
2908
|
+
type DeprecationCallback = (warning: DeprecationWarning) => void;
|
|
2909
|
+
/**
|
|
2910
|
+
* Registry of all known deprecations.
|
|
2911
|
+
*
|
|
2912
|
+
* Add new entries here when deprecating functionality.
|
|
2913
|
+
* This serves as both documentation and runtime checking.
|
|
2914
|
+
*/
|
|
2915
|
+
declare const DEPRECATIONS: DeprecationNotice[];
|
|
2916
|
+
/**
|
|
2917
|
+
* Check for deprecation warnings based on current context.
|
|
2918
|
+
*
|
|
2919
|
+
* @param context - The current usage context to check
|
|
2920
|
+
* @returns Array of deprecation warnings
|
|
2921
|
+
*
|
|
2922
|
+
* @example
|
|
2923
|
+
* ```typescript
|
|
2924
|
+
* const warnings = checkDeprecations({
|
|
2925
|
+
* protocolVersion: 0,
|
|
2926
|
+
* schemas: ['xnet://xnet.fyi/Task@1.0.0']
|
|
2927
|
+
* })
|
|
2928
|
+
*
|
|
2929
|
+
* if (warnings.length > 0) {
|
|
2930
|
+
* console.warn('Deprecation warnings:', warnings)
|
|
2931
|
+
* }
|
|
2932
|
+
* ```
|
|
2933
|
+
*/
|
|
2934
|
+
declare function checkDeprecations(context: DeprecationContext): DeprecationWarning[];
|
|
2935
|
+
/**
|
|
2936
|
+
* Default deprecation policy.
|
|
2937
|
+
*
|
|
2938
|
+
* - Minimum deprecation period: 6 months
|
|
2939
|
+
* - Breaking changes require major version bump
|
|
2940
|
+
* - Warnings logged to console in development
|
|
2941
|
+
* - Errors thrown in strict mode for removed functionality
|
|
2942
|
+
*/
|
|
2943
|
+
declare const DEPRECATION_POLICY: {
|
|
2944
|
+
/** Minimum time between deprecation and removal */
|
|
2945
|
+
minimumDeprecationPeriodDays: number;
|
|
2946
|
+
/** Whether to log warnings to console */
|
|
2947
|
+
logWarnings: boolean;
|
|
2948
|
+
/** Whether to throw errors for removed functionality */
|
|
2949
|
+
strictMode: boolean;
|
|
2950
|
+
/** Console logger for warnings */
|
|
2951
|
+
logger: (message: string) => void;
|
|
2952
|
+
};
|
|
2953
|
+
/**
|
|
2954
|
+
* Configure the deprecation policy.
|
|
2955
|
+
*/
|
|
2956
|
+
declare function configureDeprecationPolicy(options: Partial<typeof DEPRECATION_POLICY>): void;
|
|
2957
|
+
/**
|
|
2958
|
+
* Log a deprecation warning (once per session).
|
|
2959
|
+
*
|
|
2960
|
+
* @param warning - The deprecation warning to log
|
|
2961
|
+
*/
|
|
2962
|
+
declare function logDeprecation(warning: DeprecationWarning): void;
|
|
2963
|
+
/**
|
|
2964
|
+
* Check and log deprecation warnings for a context.
|
|
2965
|
+
*
|
|
2966
|
+
* @param context - The current usage context
|
|
2967
|
+
* @returns Array of warnings (also logs them)
|
|
2968
|
+
*/
|
|
2969
|
+
declare function checkAndLogDeprecations(context: DeprecationContext): DeprecationWarning[];
|
|
2970
|
+
/**
|
|
2971
|
+
* Clear the logged deprecations set (for testing).
|
|
2972
|
+
*/
|
|
2973
|
+
declare function clearLoggedDeprecations(): void;
|
|
2974
|
+
/**
|
|
2975
|
+
* Get all deprecation notices of a specific type.
|
|
2976
|
+
*/
|
|
2977
|
+
declare function getDeprecationsByType(type: DeprecationType): DeprecationNotice[];
|
|
2978
|
+
/**
|
|
2979
|
+
* Get a specific deprecation notice by subject.
|
|
2980
|
+
*/
|
|
2981
|
+
declare function getDeprecation(subject: string): DeprecationNotice | undefined;
|
|
2982
|
+
/**
|
|
2983
|
+
* Check if a specific item is deprecated.
|
|
2984
|
+
*/
|
|
2985
|
+
declare function isDeprecated(subject: string): boolean;
|
|
2986
|
+
/**
|
|
2987
|
+
* Check if a specific item has been removed.
|
|
2988
|
+
*/
|
|
2989
|
+
declare function isRemoved(subject: string): boolean;
|
|
2990
|
+
/**
|
|
2991
|
+
* Register a new deprecation notice.
|
|
2992
|
+
* Useful for applications to add their own deprecations.
|
|
2993
|
+
*/
|
|
2994
|
+
declare function registerDeprecation(notice: DeprecationNotice): void;
|
|
2995
|
+
/**
|
|
2996
|
+
* Error thrown when using removed functionality in strict mode.
|
|
2997
|
+
*/
|
|
2998
|
+
declare class DeprecationError extends Error {
|
|
2999
|
+
readonly warning: DeprecationWarning;
|
|
3000
|
+
constructor(warning: DeprecationWarning);
|
|
3001
|
+
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Format deprecation warnings as a human-readable report.
|
|
3004
|
+
*/
|
|
3005
|
+
declare function formatDeprecationReport(warnings: DeprecationWarning[]): string;
|
|
3006
|
+
|
|
3007
|
+
/**
|
|
3008
|
+
* Periodic Integrity Monitor
|
|
3009
|
+
*
|
|
3010
|
+
* A background service that periodically checks data integrity and
|
|
3011
|
+
* emits events when issues are detected.
|
|
3012
|
+
*
|
|
3013
|
+
* @example
|
|
3014
|
+
* ```typescript
|
|
3015
|
+
* const monitor = createIntegrityMonitor({
|
|
3016
|
+
* getChanges: () => storage.getAllChanges(),
|
|
3017
|
+
* intervalMs: 60000, // Check every minute
|
|
3018
|
+
* onIssues: (report) => {
|
|
3019
|
+
* console.warn('Integrity issues:', report.issues)
|
|
3020
|
+
* }
|
|
3021
|
+
* })
|
|
3022
|
+
*
|
|
3023
|
+
* monitor.start()
|
|
3024
|
+
* // ... later
|
|
3025
|
+
* monitor.stop()
|
|
3026
|
+
* ```
|
|
3027
|
+
*/
|
|
3028
|
+
|
|
3029
|
+
/**
|
|
3030
|
+
* Configuration for the integrity monitor.
|
|
3031
|
+
*/
|
|
3032
|
+
interface IntegrityMonitorConfig {
|
|
3033
|
+
/** Function to retrieve all changes for verification */
|
|
3034
|
+
getChanges: () => Promise<Change<unknown>[]> | Change<unknown>[];
|
|
3035
|
+
/** Interval between checks in milliseconds (default: 5 minutes) */
|
|
3036
|
+
intervalMs?: number;
|
|
3037
|
+
/** Use quick check (skip signatures) for periodic checks */
|
|
3038
|
+
quickCheck?: boolean;
|
|
3039
|
+
/** Additional verification options */
|
|
3040
|
+
verifyOptions?: VerifyOptions;
|
|
3041
|
+
/** Called when issues are detected */
|
|
3042
|
+
onIssues?: (report: IntegrityReport) => void;
|
|
3043
|
+
/** Called after each check (including clean ones) */
|
|
3044
|
+
onCheck?: (report: IntegrityReport) => void;
|
|
3045
|
+
/** Called when an error occurs during check */
|
|
3046
|
+
onError?: (error: Error) => void;
|
|
3047
|
+
/** Whether to run a check immediately on start */
|
|
3048
|
+
checkOnStart?: boolean;
|
|
3049
|
+
/** Minimum number of changes before running checks */
|
|
3050
|
+
minChangesForCheck?: number;
|
|
3051
|
+
/** Enable debug logging */
|
|
3052
|
+
debug?: boolean;
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Statistics from the integrity monitor.
|
|
3056
|
+
*/
|
|
3057
|
+
interface IntegrityMonitorStats {
|
|
3058
|
+
/** Number of checks performed */
|
|
3059
|
+
checksPerformed: number;
|
|
3060
|
+
/** Number of issues found across all checks */
|
|
3061
|
+
totalIssuesFound: number;
|
|
3062
|
+
/** Time of last check */
|
|
3063
|
+
lastCheckAt: Date | null;
|
|
3064
|
+
/** Duration of last check in ms */
|
|
3065
|
+
lastCheckDurationMs: number;
|
|
3066
|
+
/** Result of last check */
|
|
3067
|
+
lastReport: IntegrityReport | null;
|
|
3068
|
+
/** Whether the monitor is currently running */
|
|
3069
|
+
isRunning: boolean;
|
|
3070
|
+
/** Whether a check is currently in progress */
|
|
3071
|
+
isChecking: boolean;
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* The integrity monitor instance.
|
|
3075
|
+
*/
|
|
3076
|
+
interface IntegrityMonitor {
|
|
3077
|
+
/** Start periodic monitoring */
|
|
3078
|
+
start(): void;
|
|
3079
|
+
/** Stop periodic monitoring */
|
|
3080
|
+
stop(): void;
|
|
3081
|
+
/** Run a check immediately (independent of the periodic schedule) */
|
|
3082
|
+
checkNow(): Promise<IntegrityReport>;
|
|
3083
|
+
/** Get current statistics */
|
|
3084
|
+
getStats(): IntegrityMonitorStats;
|
|
3085
|
+
/** Check if the monitor is running */
|
|
3086
|
+
isRunning(): boolean;
|
|
3087
|
+
/** Update configuration */
|
|
3088
|
+
configure(config: Partial<IntegrityMonitorConfig>): void;
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Create an integrity monitor instance.
|
|
3092
|
+
*/
|
|
3093
|
+
declare function createIntegrityMonitor(config: IntegrityMonitorConfig): IntegrityMonitor;
|
|
3094
|
+
/**
|
|
3095
|
+
* Options for creating a React-friendly integrity monitor.
|
|
3096
|
+
*/
|
|
3097
|
+
interface ReactIntegrityMonitorOptions extends IntegrityMonitorConfig {
|
|
3098
|
+
/** Emit state changes for React to observe */
|
|
3099
|
+
onStateChange?: (stats: IntegrityMonitorStats) => void;
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Create an integrity monitor with React-friendly state updates.
|
|
3103
|
+
* This wraps the monitor to emit state changes that can be observed
|
|
3104
|
+
* by React hooks.
|
|
3105
|
+
*/
|
|
3106
|
+
declare function createReactIntegrityMonitor(options: ReactIntegrityMonitorOptions): IntegrityMonitor;
|
|
3107
|
+
|
|
3108
|
+
/**
|
|
3109
|
+
* Security policy for operation-based security level selection.
|
|
3110
|
+
*
|
|
3111
|
+
* Different operations may warrant different security levels:
|
|
3112
|
+
* - High-frequency, low-value ops (cursors) → Level 0 (fast)
|
|
3113
|
+
* - Regular data ops → Level 1 (hybrid)
|
|
3114
|
+
* - Critical ops (key rotation) → Level 2 (maximum)
|
|
3115
|
+
*/
|
|
3116
|
+
|
|
3117
|
+
/**
|
|
3118
|
+
* Security policy configuration.
|
|
3119
|
+
*/
|
|
3120
|
+
interface SecurityPolicy {
|
|
3121
|
+
/** Default level for operations not explicitly configured */
|
|
3122
|
+
default: SecurityLevel;
|
|
3123
|
+
/** Per-operation type overrides */
|
|
3124
|
+
overrides: Record<string, SecurityLevel>;
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Common operation types for security level selection.
|
|
3128
|
+
*/
|
|
3129
|
+
type OperationType = 'cursor-update' | 'presence-update' | 'typing-indicator' | 'viewport-update' | 'awareness-update' | 'node-create' | 'node-update' | 'node-delete' | 'yjs-update' | 'comment-add' | 'key-rotation' | 'permission-grant' | 'permission-revoke' | 'identity-recovery' | 'share-create';
|
|
3130
|
+
/**
|
|
3131
|
+
* Default security policy.
|
|
3132
|
+
*
|
|
3133
|
+
* Uses Level 0 (Ed25519-only) by default for performance.
|
|
3134
|
+
* High-frequency ephemeral operations use Level 0.
|
|
3135
|
+
* Regular operations inherit the default.
|
|
3136
|
+
* Critical operations can be configured to use Level 1 or 2.
|
|
3137
|
+
*/
|
|
3138
|
+
declare const DEFAULT_SECURITY_POLICY: SecurityPolicy;
|
|
3139
|
+
/**
|
|
3140
|
+
* Hybrid security policy for when PQ protection is desired.
|
|
3141
|
+
*
|
|
3142
|
+
* Uses Level 1 (hybrid) for regular operations.
|
|
3143
|
+
* Still uses Level 0 for high-frequency ephemeral ops.
|
|
3144
|
+
* Critical ops use Level 2 (PQ-only).
|
|
3145
|
+
*/
|
|
3146
|
+
declare const HYBRID_SECURITY_POLICY: SecurityPolicy;
|
|
3147
|
+
/**
|
|
3148
|
+
* Maximum security policy for high-security environments.
|
|
3149
|
+
*
|
|
3150
|
+
* Uses Level 2 (PQ-only) for everything except ephemeral ops.
|
|
3151
|
+
*/
|
|
3152
|
+
declare const MAX_SECURITY_POLICY: SecurityPolicy;
|
|
3153
|
+
/**
|
|
3154
|
+
* Get the security level for an operation type.
|
|
3155
|
+
*
|
|
3156
|
+
* @param operationType - The type of operation
|
|
3157
|
+
* @param policy - Security policy to use (default: DEFAULT_SECURITY_POLICY)
|
|
3158
|
+
* @returns The appropriate security level
|
|
3159
|
+
*
|
|
3160
|
+
* @example
|
|
3161
|
+
* ```typescript
|
|
3162
|
+
* // Get level for a cursor update (fast)
|
|
3163
|
+
* const level = getSecurityLevel('cursor-update') // 0
|
|
3164
|
+
*
|
|
3165
|
+
* // Get level for a node creation
|
|
3166
|
+
* const level = getSecurityLevel('node-create') // uses policy default
|
|
3167
|
+
*
|
|
3168
|
+
* // Use hybrid policy for PQ protection
|
|
3169
|
+
* const level = getSecurityLevel('node-create', HYBRID_SECURITY_POLICY) // 1
|
|
3170
|
+
* ```
|
|
3171
|
+
*/
|
|
3172
|
+
declare function getSecurityLevel(operationType: string, policy?: SecurityPolicy): SecurityLevel;
|
|
3173
|
+
/**
|
|
3174
|
+
* Check if an operation type is ephemeral (high-frequency, low-value).
|
|
3175
|
+
*
|
|
3176
|
+
* Ephemeral operations typically use Level 0 for performance.
|
|
3177
|
+
*/
|
|
3178
|
+
declare function isEphemeralOperation(operationType: string): boolean;
|
|
3179
|
+
/**
|
|
3180
|
+
* Check if an operation type is critical (requires high security).
|
|
3181
|
+
*/
|
|
3182
|
+
declare function isCriticalOperation(operationType: string): boolean;
|
|
3183
|
+
/**
|
|
3184
|
+
* Create a custom security policy.
|
|
3185
|
+
*
|
|
3186
|
+
* @example
|
|
3187
|
+
* ```typescript
|
|
3188
|
+
* const myPolicy = createSecurityPolicy({
|
|
3189
|
+
* default: 1,
|
|
3190
|
+
* overrides: {
|
|
3191
|
+
* 'cursor-update': 0,
|
|
3192
|
+
* 'key-rotation': 2
|
|
3193
|
+
* }
|
|
3194
|
+
* })
|
|
3195
|
+
* ```
|
|
3196
|
+
*/
|
|
3197
|
+
declare function createSecurityPolicy(options?: Partial<SecurityPolicy>): SecurityPolicy;
|
|
3198
|
+
/**
|
|
3199
|
+
* Merge policies, with later policies taking precedence.
|
|
3200
|
+
*/
|
|
3201
|
+
declare function mergeSecurityPolicies(...policies: Partial<SecurityPolicy>[]): SecurityPolicy;
|
|
3202
|
+
|
|
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 };
|