@xnetjs/sync 2.4.0 → 3.0.0
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/README.md +5 -0
- package/dist/index.d.ts +173 -1
- package/dist/index.js +132 -12
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Unified sync primitives for xNet -- Change\<T\>, Lamport clocks, hash chains, and a comprehensive Yjs security layer.
|
|
4
4
|
|
|
5
|
+
> **Alpha software.** xNet is released but early: this package is on npm and
|
|
6
|
+
> usable today, but its API can change between releases, sometimes without a
|
|
7
|
+
> migration path. Pin your version. See the
|
|
8
|
+
> [project README](https://github.com/crs48/xNet#readme) for what alpha means here.
|
|
9
|
+
|
|
5
10
|
## Installation
|
|
6
11
|
|
|
7
12
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -172,6 +172,28 @@ declare function createWebCryptoChangeSigner(signingKey: Uint8Array): ChangeSign
|
|
|
172
172
|
* @returns true if the signature is valid
|
|
173
173
|
*/
|
|
174
174
|
declare function verifyChange<T>(change: Change<T>, publicKey: Uint8Array): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Verify a change's signature using the native (WebCrypto) verifier when the
|
|
177
|
+
* runtime has it — ~13x faster than the pure-JS path (exploration 0350/0357).
|
|
178
|
+
*
|
|
179
|
+
* Semantically identical to {@link verifyChange}; use this on bulk paths
|
|
180
|
+
* (hub relay, `.xnetpack` import, NDJSON restore, resync) where per-change
|
|
181
|
+
* verification is the bottleneck. Single interactive writes can keep using
|
|
182
|
+
* the synchronous {@link verifyChange}.
|
|
183
|
+
*/
|
|
184
|
+
declare function verifyChangeFast<T>(change: Change<T>, publicKey: Uint8Array): Promise<boolean>;
|
|
185
|
+
/**
|
|
186
|
+
* Verify many changes at once. Results are positional — `result[i]`
|
|
187
|
+
* corresponds to `entries[i]` — and a failure never short-circuits the rest,
|
|
188
|
+
* so callers can report exactly which change was rejected.
|
|
189
|
+
*
|
|
190
|
+
* This shares one native-support probe and one key import per distinct author
|
|
191
|
+
* across the whole set, which is the common shape for a bulk import.
|
|
192
|
+
*/
|
|
193
|
+
declare function verifyChangesFast<T>(entries: readonly {
|
|
194
|
+
change: Change<T>;
|
|
195
|
+
publicKey: Uint8Array;
|
|
196
|
+
}[]): Promise<boolean[]>;
|
|
175
197
|
/**
|
|
176
198
|
* Recompute the content hash of a signed change from its own fields.
|
|
177
199
|
*
|
|
@@ -195,6 +217,156 @@ declare function verifyChangeHash<T>(change: Change<T>): boolean;
|
|
|
195
217
|
*/
|
|
196
218
|
declare function createChangeId(): string;
|
|
197
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Batch commits: one signature over many changes (exploration 0357 Tier 2).
|
|
222
|
+
*
|
|
223
|
+
* A bulk operation in xNet is N changes, one per node — that is deliberate,
|
|
224
|
+
* and it is what makes per-node parent chains, per-property LWW, and selective
|
|
225
|
+
* sync work. But it means a 10,000-node import pays 10,000 Ed25519 signatures
|
|
226
|
+
* on write and 10,000 verifications on read, plus 88 bytes of base64 signature
|
|
227
|
+
* per change on the wire and on disk.
|
|
228
|
+
*
|
|
229
|
+
* Every mature system in this space solved that the same way: hash every unit,
|
|
230
|
+
* sign only at a batch boundary, and prove membership under the signed root.
|
|
231
|
+
* Hypercore signs one Merkle root per append; Certificate Transparency signs
|
|
232
|
+
* one tree head per interval; AT Protocol signs one commit over an MST no
|
|
233
|
+
* matter how many records changed. Per-message signing (Scuttlebutt, Nostr) is
|
|
234
|
+
* the pattern their successors moved away from.
|
|
235
|
+
*
|
|
236
|
+
* A `BatchCommit` is that boundary. It carries the ordered hashes of the
|
|
237
|
+
* changes it covers plus a BLAKE3 root over them, and is signed ONCE. A
|
|
238
|
+
* verifier then does N cheap hash recomputations (which it must do anyway, to
|
|
239
|
+
* confirm each change matches its own content address) plus ONE signature
|
|
240
|
+
* verification, instead of N signature verifications.
|
|
241
|
+
*
|
|
242
|
+
* ## What this does NOT change
|
|
243
|
+
*
|
|
244
|
+
* - The `Change` record, its canonical bytes, its hash recipe, and the LWW
|
|
245
|
+
* ordering rules are untouched. This is an ADDITIVE record, so the four
|
|
246
|
+
* conformance kernels gain vectors rather than a re-derivation.
|
|
247
|
+
* - Authorization, ledger enforcement, quota, and LWW still run per change. A
|
|
248
|
+
* commit amortizes *authentication*, nothing else.
|
|
249
|
+
*
|
|
250
|
+
* ## Where it is valid
|
|
251
|
+
*
|
|
252
|
+
* Only in lanes where the batch travels as a unit — `.xnetpack` import/export,
|
|
253
|
+
* hub NDJSON restore, initial-sync snapshots, migrations. Interactive single
|
|
254
|
+
* edits keep their per-change signature, because live relay fans a change out
|
|
255
|
+
* to peers who may never receive its siblings, and a change whose validity
|
|
256
|
+
* depends on a sibling would break that.
|
|
257
|
+
*
|
|
258
|
+
* Chain-head signing (ATProto's `prev`, Scuttlebutt's feed) is deliberately
|
|
259
|
+
* NOT used here: xNet's parent-hash chains are per-NODE, so a bulk import of N
|
|
260
|
+
* distinct nodes is N chains of depth 1 and a chain head amortizes nothing.
|
|
261
|
+
* The root must span nodes, hence a list/tree over change hashes.
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Maximum changes one commit may cover.
|
|
266
|
+
*
|
|
267
|
+
* Matches the wire batch cap: a commit is verified by recomputing every member
|
|
268
|
+
* hash, so an unbounded commit would be an unbounded unit of work for the
|
|
269
|
+
* verifier. Larger imports emit several commits.
|
|
270
|
+
*/
|
|
271
|
+
declare const MAX_COMMIT_CHANGES = 1000;
|
|
272
|
+
/** A batch commit before it has been hashed and signed. */
|
|
273
|
+
interface UnsignedBatchCommit {
|
|
274
|
+
id: string;
|
|
275
|
+
type: 'batch-commit';
|
|
276
|
+
protocolVersion: number;
|
|
277
|
+
authorDID: DID;
|
|
278
|
+
/** Room/scope the covered changes belong to, when the lane has one. */
|
|
279
|
+
room?: string;
|
|
280
|
+
/** Ordered hashes of the covered changes. Order is part of the root. */
|
|
281
|
+
changeHashes: ContentId[];
|
|
282
|
+
/** BLAKE3 over the ordered change hashes — see {@link computeBatchRoot}. */
|
|
283
|
+
root: ContentId;
|
|
284
|
+
lamport: number;
|
|
285
|
+
wallTime: number;
|
|
286
|
+
}
|
|
287
|
+
/** A signed batch commit. One signature covers every change it names. */
|
|
288
|
+
interface BatchCommit extends UnsignedBatchCommit {
|
|
289
|
+
hash: ContentId;
|
|
290
|
+
signature: Uint8Array;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Compute the root over an ordered list of change hashes.
|
|
294
|
+
*
|
|
295
|
+
* This is a flat digest, not a Merkle tree: a commit's members always travel
|
|
296
|
+
* together in the lanes where commits are valid, so no member ever needs an
|
|
297
|
+
* O(log n) inclusion proof against the root. If a lane later needs to
|
|
298
|
+
* redistribute covered changes individually, this is the function that becomes
|
|
299
|
+
* a Merkle root (and members gain proofs) — the commit shape does not change.
|
|
300
|
+
*
|
|
301
|
+
* Hashes are joined with a separator that cannot appear in a `cid:blake3:`
|
|
302
|
+
* string, so no concatenation of two hash lists can collide with another.
|
|
303
|
+
*/
|
|
304
|
+
declare function computeBatchRoot(changeHashes: readonly ContentId[]): ContentId;
|
|
305
|
+
/**
|
|
306
|
+
* Hash a commit for signing. Same recipe as a change: recursively key-sorted
|
|
307
|
+
* canonical JSON, BLAKE3, `cid:blake3:` prefix.
|
|
308
|
+
*/
|
|
309
|
+
declare function computeBatchCommitHash(unsigned: UnsignedBatchCommit): ContentId;
|
|
310
|
+
interface CreateBatchCommitOptions {
|
|
311
|
+
id: string;
|
|
312
|
+
authorDID: DID;
|
|
313
|
+
changeHashes: readonly ContentId[];
|
|
314
|
+
lamport: number;
|
|
315
|
+
wallTime: number;
|
|
316
|
+
room?: string;
|
|
317
|
+
protocolVersion?: number;
|
|
318
|
+
}
|
|
319
|
+
/** Build an unsigned commit, computing its root. */
|
|
320
|
+
declare function createUnsignedBatchCommit(options: CreateBatchCommitOptions): UnsignedBatchCommit;
|
|
321
|
+
/** Hash and sign a commit — the ONE signature that covers the whole batch. */
|
|
322
|
+
declare function signBatchCommit(unsigned: UnsignedBatchCommit, signingKey: Uint8Array): BatchCommit;
|
|
323
|
+
/** Recompute a commit's hash from its own fields (the tamper check). */
|
|
324
|
+
declare function recomputeBatchCommitHash(commit: BatchCommit): ContentId;
|
|
325
|
+
/**
|
|
326
|
+
* Verify a commit in isolation: its hash matches its fields, its root matches
|
|
327
|
+
* its change-hash list, and its signature matches its author.
|
|
328
|
+
*
|
|
329
|
+
* This says nothing about whether any particular change belongs to it — use
|
|
330
|
+
* {@link verifyBatch} for that.
|
|
331
|
+
*/
|
|
332
|
+
declare function verifyBatchCommit(commit: BatchCommit, publicKey: Uint8Array): boolean;
|
|
333
|
+
/** Async form of {@link verifyBatchCommit}, using native Ed25519 when present. */
|
|
334
|
+
declare function verifyBatchCommitFast(commit: BatchCommit, publicKey: Uint8Array): Promise<boolean>;
|
|
335
|
+
interface BatchVerificationResult {
|
|
336
|
+
/** True only if the commit is valid AND every supplied change is covered by it. */
|
|
337
|
+
ok: boolean;
|
|
338
|
+
/** Per-change verdicts, positional with the input. */
|
|
339
|
+
members: boolean[];
|
|
340
|
+
reason?: string;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Verify a whole batch: ONE signature check plus N hash recomputations.
|
|
344
|
+
*
|
|
345
|
+
* The membership rules are what keep this from being weaker than per-change
|
|
346
|
+
* signatures:
|
|
347
|
+
*
|
|
348
|
+
* - each change must recompute to its own claimed hash (so payload tampering
|
|
349
|
+
* is caught exactly as it would be for a signed change);
|
|
350
|
+
* - that hash must appear in the commit's list (so a change cannot be smuggled
|
|
351
|
+
* into a batch it was not signed over);
|
|
352
|
+
* - the change's author must equal the commit's author (so a commit cannot
|
|
353
|
+
* launder someone else's authorship — the signer vouches only for their own
|
|
354
|
+
* changes);
|
|
355
|
+
* - the commit's own hash/root/signature must be valid (so the list itself
|
|
356
|
+
* cannot be edited).
|
|
357
|
+
*
|
|
358
|
+
* `recomputeChangeHash` is injected rather than imported to keep this module
|
|
359
|
+
* free of a cycle with `change.ts`.
|
|
360
|
+
*/
|
|
361
|
+
declare function verifyBatch<T>(commit: BatchCommit, changes: readonly Change<T>[], publicKey: Uint8Array, recomputeChangeHash: (change: Change<T>) => ContentId): Promise<BatchVerificationResult>;
|
|
362
|
+
/**
|
|
363
|
+
* Split an ordered change list into commit-sized groups.
|
|
364
|
+
*
|
|
365
|
+
* Order is preserved, so the caller can emit commits and their member changes
|
|
366
|
+
* in causal order.
|
|
367
|
+
*/
|
|
368
|
+
declare function chunkForCommits<T>(changes: readonly T[], size?: number): T[][];
|
|
369
|
+
|
|
198
370
|
/**
|
|
199
371
|
* Lamport clock utilities for total ordering in distributed systems.
|
|
200
372
|
*
|
|
@@ -3488,4 +3660,4 @@ declare function createSecurityPolicy(options?: Partial<SecurityPolicy>): Securi
|
|
|
3488
3660
|
*/
|
|
3489
3661
|
declare function mergeSecurityPolicies(...policies: Partial<SecurityPolicy>[]): SecurityPolicy;
|
|
3490
3662
|
|
|
3491
|
-
export { ALL_FEATURES, type AttestationVerificationResult, type AttestationVerifyResult, type AuthorizedDoc, type AuthorizedRoom, type AuthorizedStateAdapter, AuthorizedSyncManager, type AuthorizedSyncManagerOptions, AuthorizedYjsError, AuthorizedYjsSyncProvider, type AuthorizedYjsSyncProviderOptions, BaseSyncProvider, type BatchFlushCallback, COMPACTION_TIME_THRESHOLD, COMPACTION_UPDATE_THRESHOLD, CURRENT_PROTOCOL_VERSION, type ChainValidationResult, type Change, type ChangeHandler, ChangeHandlerRegistry, type ChangeSerializer, type ChangeSigner, type ClientIdAttestation, type ClientIdAttestationV1, type ClientIdAttestationV2, type ClientIdAttestationWire, type ClientIdMap, ClientIdMapImpl, type ContentKeyProvider, type CreateAttestationOptions, type CreateChangeOptions, type CreateEnvelopeOptions, type CreateYjsChangeOptions, DEFAULT_BATCHER_CONFIG, DEFAULT_RATE_LIMITER_CONFIG, DEFAULT_SECURITY_POLICY, DEFAULT_YJS_SCORING_CONFIG, DEPRECATIONS, DEPRECATION_POLICY, type DeprecationCallback, type DeprecationContext, DeprecationError, type DeprecationNotice, type DeprecationType, type DeprecationWarning, type DeserializeError, type DeserializeOutcome, type DeserializeResult, type EncryptedYjsState, type EnvelopeVerificationResult, type EnvelopeVerifyResult, FEATURES, type FeatureConfig, type FeatureFlag, type FeatureValidationError, type FeatureValidationResult, type FeatureValidationWarning, type Fork, type GrantEventStore, HYBRID_SECURITY_POLICY, type HandlerContext, type HandlerEvent, type IntegrityIssue, type IntegrityIssueType, type IntegrityMonitor, type IntegrityMonitorConfig, type IntegrityMonitorStats, type IntegrityReport, type LamportClock, type LamportTimestamp, MAX_SECURITY_POLICY, MAX_YJS_AWARENESS_UPDATE_SIZE, MAX_YJS_DOC_SIZE, MAX_YJS_STATE_VECTOR_SIZE, MAX_YJS_UPDATES_PER_MINUTE, MAX_YJS_UPDATES_PER_SECOND, MAX_YJS_UPDATE_SIZE, type MergeUpdatesFn, type NegotiatedSession, type NegotiationFailure, type NegotiationResult, type NegotiationWarning, type OperationType, type PeerAction, type PeerCapabilities, type PeerInfo, type PersistedDocState, type PolicyRevisionSimulation, type ProcessResult, type RateLimiterConfig, type ReactIntegrityMonitorOptions, type RecipientKeyResolver, type RegistryStats, type RepairAction, type RepairActionType, type ReplicationNamespaceKind, type ReplicationPlan, type ReplicationPlanDestination, type ReplicationPlanDiagnostic, type ReplicationPlanTraceStep, type ResolvedSyncReplicationPolicy, type SecurityPolicy, type SerializeOptions, type SerializedChange, type SerializerRegistry, type SignedYjsEnvelope, type SignedYjsEnvelopeV1, type SignedYjsEnvelopeV2, type SignedYjsEnvelopeWire, type SyncCompatibilityConfig, type SyncConnectionStatus, type SyncEventListener, type SyncFederationConfig, type SyncFederationHub, type SyncFederationNamespacePolicy, type SyncLifecycleInput, type SyncLifecyclePhase, type SyncLifecycleState, type SyncProvider, type SyncProviderEvents, type SyncProviderOptions, type SyncReplicationConfig, type SyncStatus, type UnsignedChange, type UnsignedYjsChange, V1Serializer, V2Serializer, type ValidationError, type ValidationResult, type ValidationWarning, type VerifyAttestationOptions, type VerifyEnvelopeOptions, type VerifyOptions, VersionNegotiator, type YDocCodec, type YDocLike, YJS_CHANGE_TYPE, YJS_RATE_BURST_ALLOWANCE, YJS_SYNC_CHUNK_SIZE, type YjsAuthDecision, YjsAuthGate, type YjsAuthGateOptions, YjsBatcher, type YjsBatcherConfig, type YjsChange, YjsCheckpointer, type YjsCheckpointerOptions, YjsIntegrityError, type YjsPeerActionEvent, type YjsPeerActionListener, type YjsPeerMetrics, YjsPeerScorer, YjsRateLimiter, type YjsRateLimiterOptions, type YjsScoringConfig, YjsStateIntegrityError, type YjsUpdatePayload, type YjsViolationType, addDependencies, attemptRepair, autoDeserialize, autoSerialize, calculateChunkCount, changeHandlerRegistry, checkAndLogDeprecations, checkDeprecations, chunkUpdate, clearLoggedDeprecations, compareLamportTimestamps, computeChangeHash, configureDeprecationPolicy, createBatchId, createChangeId, createClientIdAttestation, createClientIdAttestationV1, createClientIdAttestationV2, createHandler, createIntegrityMonitor, createLamportClock, createLocalCapabilities, createPersistedDocState, createReactIntegrityMonitor, createSecurityPolicy, createSerializerRegistry, createSyncLifecycleState, createTestContext, createUnsignedChange, createUnsignedYjsChange, createVersionedHandler, createWebCryptoChangeSigner, createYjsChange, decryptYjsState, defaultNegotiator, deriveClientIdFromDid, deriveSyncLifecyclePhase, deserializeClientIdAttestation, deserializeEncryptedYjsState, deserializeYjsEnvelope, detectFork, diffFeatures, encryptYjsState, envelopeSize, estimateBase64DecodedLength, findCommonAncestor, findHeads, findOrphans, findRoots, formatDeprecationReport, formatIntegrityReport, getAllDependencies, getAncestry, getChainDepth, getChainHeads, getChainRoots, getChangeNodeId, getDefaultSerializer, getDeprecation, getDeprecationsByType, getEnabledFeatures, getFeatureConflicts, getFeatureDependencies, getFeatureVersion, getForks, getOptionalFeatures, getRequiredFeatures, getSecurityLevel, getSerializer, hasSignedEnvelope, hashYjsState, inferReplicationNamespaceKind, intersectFeatures, isAfter, isAwarenessUpdateTooLarge, isBase64PayloadTooLarge, isBefore, isCriticalOperation, isDeprecated, isDerivedClientId, isDocumentTooLarge, isEphemeralOperation, isFeatureAvailable, isFeatureEnabled, isLegacyUpdate, isNodeChange, isRemoved, isStateVectorTooLarge, isUpdateTooLarge, isV1Attestation, isV1Envelope, isV2Attestation, isV2Envelope, isYjsChange, loadVerifiedState, logDeprecation, maxTime, mergeSecurityPolicies, normalizeSyncFederationHubs, parseCapabilities, parseTimestamp, planReplicationDestinations, quickIntegrityCheck, reassembleChunks, receive, recomputeChangeHash, registerDeprecation, resolveSyncReplicationPolicy, serializeClientIdAttestation, serializeEncryptedYjsState, serializeTimestamp, serializeYjsEnvelope, serializerRegistry, shouldCompact, signChange, signYjsUpdate, signYjsUpdateBatch, signYjsUpdateV1, signYjsUpdateV2, simulateSyncPolicyRevision, tick, toEncryptedData, topologicalSort, v1Serializer, v2Serializer, validateChain, validateClientIdOwnership, validateFeatureSet, verifyChange, verifyChangeHash, verifyClientIdAttestation, verifyClientIdAttestationV1, verifyClientIdAttestationV2, verifyIntegrity, verifyPersistedDocState, verifySingleChange, verifyYjsEnvelope, verifyYjsEnvelopeQuick, verifyYjsEnvelopeV1, verifyYjsEnvelopeV2, verifyYjsStateIntegrity };
|
|
3663
|
+
export { ALL_FEATURES, type AttestationVerificationResult, type AttestationVerifyResult, type AuthorizedDoc, type AuthorizedRoom, type AuthorizedStateAdapter, AuthorizedSyncManager, type AuthorizedSyncManagerOptions, AuthorizedYjsError, AuthorizedYjsSyncProvider, type AuthorizedYjsSyncProviderOptions, BaseSyncProvider, type BatchCommit, type BatchFlushCallback, type BatchVerificationResult, COMPACTION_TIME_THRESHOLD, COMPACTION_UPDATE_THRESHOLD, CURRENT_PROTOCOL_VERSION, type ChainValidationResult, type Change, type ChangeHandler, ChangeHandlerRegistry, type ChangeSerializer, type ChangeSigner, type ClientIdAttestation, type ClientIdAttestationV1, type ClientIdAttestationV2, type ClientIdAttestationWire, type ClientIdMap, ClientIdMapImpl, type ContentKeyProvider, type CreateAttestationOptions, type CreateBatchCommitOptions, 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_COMMIT_CHANGES, MAX_SECURITY_POLICY, MAX_YJS_AWARENESS_UPDATE_SIZE, MAX_YJS_DOC_SIZE, MAX_YJS_STATE_VECTOR_SIZE, MAX_YJS_UPDATES_PER_MINUTE, MAX_YJS_UPDATES_PER_SECOND, MAX_YJS_UPDATE_SIZE, type MergeUpdatesFn, type NegotiatedSession, type NegotiationFailure, type NegotiationResult, type NegotiationWarning, type OperationType, type PeerAction, type PeerCapabilities, type PeerInfo, type PersistedDocState, type PolicyRevisionSimulation, type ProcessResult, type RateLimiterConfig, type ReactIntegrityMonitorOptions, type RecipientKeyResolver, type RegistryStats, type RepairAction, type RepairActionType, type ReplicationNamespaceKind, type ReplicationPlan, type ReplicationPlanDestination, type ReplicationPlanDiagnostic, type ReplicationPlanTraceStep, type ResolvedSyncReplicationPolicy, type SecurityPolicy, type SerializeOptions, type SerializedChange, type SerializerRegistry, type SignedYjsEnvelope, type SignedYjsEnvelopeV1, type SignedYjsEnvelopeV2, type SignedYjsEnvelopeWire, type SyncCompatibilityConfig, type SyncConnectionStatus, type SyncEventListener, type SyncFederationConfig, type SyncFederationHub, type SyncFederationNamespacePolicy, type SyncLifecycleInput, type SyncLifecyclePhase, type SyncLifecycleState, type SyncProvider, type SyncProviderEvents, type SyncProviderOptions, type SyncReplicationConfig, type SyncStatus, type UnsignedBatchCommit, type UnsignedChange, type UnsignedYjsChange, V1Serializer, V2Serializer, type ValidationError, type ValidationResult, type ValidationWarning, type VerifyAttestationOptions, type VerifyEnvelopeOptions, type VerifyOptions, VersionNegotiator, type YDocCodec, type YDocLike, YJS_CHANGE_TYPE, YJS_RATE_BURST_ALLOWANCE, YJS_SYNC_CHUNK_SIZE, type YjsAuthDecision, YjsAuthGate, type YjsAuthGateOptions, YjsBatcher, type YjsBatcherConfig, type YjsChange, YjsCheckpointer, type YjsCheckpointerOptions, YjsIntegrityError, type YjsPeerActionEvent, type YjsPeerActionListener, type YjsPeerMetrics, YjsPeerScorer, YjsRateLimiter, type YjsRateLimiterOptions, type YjsScoringConfig, YjsStateIntegrityError, type YjsUpdatePayload, type YjsViolationType, addDependencies, attemptRepair, autoDeserialize, autoSerialize, calculateChunkCount, changeHandlerRegistry, checkAndLogDeprecations, checkDeprecations, chunkForCommits, chunkUpdate, clearLoggedDeprecations, compareLamportTimestamps, computeBatchCommitHash, computeBatchRoot, computeChangeHash, configureDeprecationPolicy, createBatchId, createChangeId, createClientIdAttestation, createClientIdAttestationV1, createClientIdAttestationV2, createHandler, createIntegrityMonitor, createLamportClock, createLocalCapabilities, createPersistedDocState, createReactIntegrityMonitor, createSecurityPolicy, createSerializerRegistry, createSyncLifecycleState, createTestContext, createUnsignedBatchCommit, createUnsignedChange, createUnsignedYjsChange, createVersionedHandler, createWebCryptoChangeSigner, createYjsChange, decryptYjsState, defaultNegotiator, deriveClientIdFromDid, deriveSyncLifecyclePhase, deserializeClientIdAttestation, deserializeEncryptedYjsState, deserializeYjsEnvelope, detectFork, diffFeatures, encryptYjsState, envelopeSize, estimateBase64DecodedLength, findCommonAncestor, findHeads, findOrphans, findRoots, formatDeprecationReport, formatIntegrityReport, getAllDependencies, getAncestry, getChainDepth, getChainHeads, getChainRoots, getChangeNodeId, getDefaultSerializer, getDeprecation, getDeprecationsByType, getEnabledFeatures, getFeatureConflicts, getFeatureDependencies, getFeatureVersion, getForks, getOptionalFeatures, getRequiredFeatures, getSecurityLevel, getSerializer, hasSignedEnvelope, hashYjsState, inferReplicationNamespaceKind, intersectFeatures, isAfter, isAwarenessUpdateTooLarge, isBase64PayloadTooLarge, isBefore, isCriticalOperation, isDeprecated, isDerivedClientId, isDocumentTooLarge, isEphemeralOperation, isFeatureAvailable, isFeatureEnabled, isLegacyUpdate, isNodeChange, isRemoved, isStateVectorTooLarge, isUpdateTooLarge, isV1Attestation, isV1Envelope, isV2Attestation, isV2Envelope, isYjsChange, loadVerifiedState, logDeprecation, maxTime, mergeSecurityPolicies, normalizeSyncFederationHubs, parseCapabilities, parseTimestamp, planReplicationDestinations, quickIntegrityCheck, reassembleChunks, receive, recomputeBatchCommitHash, recomputeChangeHash, registerDeprecation, resolveSyncReplicationPolicy, serializeClientIdAttestation, serializeEncryptedYjsState, serializeTimestamp, serializeYjsEnvelope, serializerRegistry, shouldCompact, signBatchCommit, signChange, signYjsUpdate, signYjsUpdateBatch, signYjsUpdateV1, signYjsUpdateV2, simulateSyncPolicyRevision, tick, toEncryptedData, topologicalSort, v1Serializer, v2Serializer, validateChain, validateClientIdOwnership, validateFeatureSet, verifyBatch, verifyBatchCommit, verifyBatchCommitFast, verifyChange, verifyChangeFast, verifyChangeHash, verifyChangesFast, verifyClientIdAttestation, verifyClientIdAttestationV1, verifyClientIdAttestationV2, verifyIntegrity, verifyPersistedDocState, verifySingleChange, verifyYjsEnvelope, verifyYjsEnvelopeQuick, verifyYjsEnvelopeV1, verifyYjsEnvelopeV2, verifyYjsStateIntegrity };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/change.ts
|
|
2
|
-
import { hashHex, sign, verify } from "@xnetjs/crypto";
|
|
2
|
+
import { hashHex, sign, verify, verifyFast, verifyMany } from "@xnetjs/crypto";
|
|
3
3
|
var CURRENT_PROTOCOL_VERSION = 4;
|
|
4
4
|
function createUnsignedChange(options) {
|
|
5
5
|
const unsigned = {
|
|
@@ -112,14 +112,33 @@ function createWebCryptoChangeSigner(signingKey) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
function verifyChange(change, publicKey) {
|
|
115
|
+
warnOnFutureProtocolVersion(change);
|
|
116
|
+
const hashBytes = new TextEncoder().encode(change.hash);
|
|
117
|
+
return verify(hashBytes, change.signature, publicKey);
|
|
118
|
+
}
|
|
119
|
+
function warnOnFutureProtocolVersion(change) {
|
|
115
120
|
const version = change.protocolVersion ?? 0;
|
|
116
121
|
if (version > CURRENT_PROTOCOL_VERSION) {
|
|
117
122
|
console.warn(
|
|
118
123
|
`[xnet/sync] Change ${change.id} uses protocol version ${version}, but current version is ${CURRENT_PROTOCOL_VERSION}. Consider upgrading xNet for full compatibility.`
|
|
119
124
|
);
|
|
120
125
|
}
|
|
126
|
+
}
|
|
127
|
+
async function verifyChangeFast(change, publicKey) {
|
|
128
|
+
warnOnFutureProtocolVersion(change);
|
|
121
129
|
const hashBytes = new TextEncoder().encode(change.hash);
|
|
122
|
-
return
|
|
130
|
+
return verifyFast(hashBytes, change.signature, publicKey);
|
|
131
|
+
}
|
|
132
|
+
async function verifyChangesFast(entries) {
|
|
133
|
+
const encoder = new TextEncoder();
|
|
134
|
+
for (const entry of entries) warnOnFutureProtocolVersion(entry.change);
|
|
135
|
+
return verifyMany(
|
|
136
|
+
entries.map((entry) => ({
|
|
137
|
+
message: encoder.encode(entry.change.hash),
|
|
138
|
+
signature: entry.change.signature,
|
|
139
|
+
publicKey: entry.publicKey
|
|
140
|
+
}))
|
|
141
|
+
);
|
|
123
142
|
}
|
|
124
143
|
function recomputeChangeHash(change) {
|
|
125
144
|
const unsigned = {
|
|
@@ -148,6 +167,95 @@ function createChangeId() {
|
|
|
148
167
|
return crypto.randomUUID();
|
|
149
168
|
}
|
|
150
169
|
|
|
170
|
+
// src/batch-commit.ts
|
|
171
|
+
import { hashHex as hashHex2, sign as sign2, verify as verify2, verifyFast as verifyFast2 } from "@xnetjs/crypto";
|
|
172
|
+
var MAX_COMMIT_CHANGES = 1e3;
|
|
173
|
+
function computeBatchRoot(changeHashes) {
|
|
174
|
+
const canonical = changeHashes.join("\n");
|
|
175
|
+
return `cid:blake3:${hashHex2(new TextEncoder().encode(canonical))}`;
|
|
176
|
+
}
|
|
177
|
+
function computeBatchCommitHash(unsigned) {
|
|
178
|
+
const canonical = JSON.stringify(sortObjectKeys(unsigned));
|
|
179
|
+
return `cid:blake3:${hashHex2(new TextEncoder().encode(canonical))}`;
|
|
180
|
+
}
|
|
181
|
+
function createUnsignedBatchCommit(options) {
|
|
182
|
+
if (options.changeHashes.length === 0) {
|
|
183
|
+
throw new Error("[xnet/sync] a batch commit must cover at least one change");
|
|
184
|
+
}
|
|
185
|
+
if (options.changeHashes.length > MAX_COMMIT_CHANGES) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`[xnet/sync] a batch commit may cover at most ${MAX_COMMIT_CHANGES} changes (got ${options.changeHashes.length}); split the batch`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const changeHashes = [...options.changeHashes];
|
|
191
|
+
const unsigned = {
|
|
192
|
+
id: options.id,
|
|
193
|
+
type: "batch-commit",
|
|
194
|
+
protocolVersion: options.protocolVersion ?? CURRENT_PROTOCOL_VERSION,
|
|
195
|
+
authorDID: options.authorDID,
|
|
196
|
+
changeHashes,
|
|
197
|
+
root: computeBatchRoot(changeHashes),
|
|
198
|
+
lamport: options.lamport,
|
|
199
|
+
wallTime: options.wallTime
|
|
200
|
+
};
|
|
201
|
+
if (options.room !== void 0) unsigned.room = options.room;
|
|
202
|
+
return unsigned;
|
|
203
|
+
}
|
|
204
|
+
function signBatchCommit(unsigned, signingKey) {
|
|
205
|
+
const hash4 = computeBatchCommitHash(unsigned);
|
|
206
|
+
return {
|
|
207
|
+
...unsigned,
|
|
208
|
+
hash: hash4,
|
|
209
|
+
signature: sign2(new TextEncoder().encode(hash4), signingKey)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function recomputeBatchCommitHash(commit) {
|
|
213
|
+
const unsigned = {
|
|
214
|
+
id: commit.id,
|
|
215
|
+
type: commit.type,
|
|
216
|
+
protocolVersion: commit.protocolVersion,
|
|
217
|
+
authorDID: commit.authorDID,
|
|
218
|
+
changeHashes: commit.changeHashes,
|
|
219
|
+
root: commit.root,
|
|
220
|
+
lamport: commit.lamport,
|
|
221
|
+
wallTime: commit.wallTime
|
|
222
|
+
};
|
|
223
|
+
if (commit.room !== void 0) unsigned.room = commit.room;
|
|
224
|
+
return computeBatchCommitHash(unsigned);
|
|
225
|
+
}
|
|
226
|
+
function verifyBatchCommit(commit, publicKey) {
|
|
227
|
+
if (recomputeBatchCommitHash(commit) !== commit.hash) return false;
|
|
228
|
+
if (computeBatchRoot(commit.changeHashes) !== commit.root) return false;
|
|
229
|
+
return verify2(new TextEncoder().encode(commit.hash), commit.signature, publicKey);
|
|
230
|
+
}
|
|
231
|
+
async function verifyBatchCommitFast(commit, publicKey) {
|
|
232
|
+
if (recomputeBatchCommitHash(commit) !== commit.hash) return false;
|
|
233
|
+
if (computeBatchRoot(commit.changeHashes) !== commit.root) return false;
|
|
234
|
+
return verifyFast2(new TextEncoder().encode(commit.hash), commit.signature, publicKey);
|
|
235
|
+
}
|
|
236
|
+
async function verifyBatch(commit, changes, publicKey, recomputeChangeHash2) {
|
|
237
|
+
if (!await verifyBatchCommitFast(commit, publicKey)) {
|
|
238
|
+
return { ok: false, members: changes.map(() => false), reason: "commit is invalid" };
|
|
239
|
+
}
|
|
240
|
+
const covered = new Set(commit.changeHashes);
|
|
241
|
+
const members = changes.map((change) => {
|
|
242
|
+
if (recomputeChangeHash2(change) !== change.hash) return false;
|
|
243
|
+
if (!covered.has(change.hash)) return false;
|
|
244
|
+
if (change.authorDID !== commit.authorDID) return false;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
const failed = members.filter((ok) => !ok).length;
|
|
248
|
+
return failed === 0 ? { ok: true, members } : { ok: false, members, reason: `${failed} change(s) are not covered by this commit` };
|
|
249
|
+
}
|
|
250
|
+
function chunkForCommits(changes, size = MAX_COMMIT_CHANGES) {
|
|
251
|
+
const limit = Math.max(1, Math.min(size, MAX_COMMIT_CHANGES));
|
|
252
|
+
const chunks = [];
|
|
253
|
+
for (let index = 0; index < changes.length; index += limit) {
|
|
254
|
+
chunks.push(changes.slice(index, index + limit));
|
|
255
|
+
}
|
|
256
|
+
return chunks;
|
|
257
|
+
}
|
|
258
|
+
|
|
151
259
|
// src/clock.ts
|
|
152
260
|
function createLamportClock(author) {
|
|
153
261
|
return { time: 0, author };
|
|
@@ -1125,8 +1233,8 @@ function compareText(left, right) {
|
|
|
1125
1233
|
// src/yjs-envelope.ts
|
|
1126
1234
|
import {
|
|
1127
1235
|
hash,
|
|
1128
|
-
sign as
|
|
1129
|
-
verify as
|
|
1236
|
+
sign as sign3,
|
|
1237
|
+
verify as verify3,
|
|
1130
1238
|
hybridSign,
|
|
1131
1239
|
hybridVerify,
|
|
1132
1240
|
encodeSignature,
|
|
@@ -1144,7 +1252,7 @@ function isV1Envelope(envelope) {
|
|
|
1144
1252
|
}
|
|
1145
1253
|
function signYjsUpdateV1(update, authorDID, privateKey, clientId) {
|
|
1146
1254
|
const updateHash = hash(update, "blake3");
|
|
1147
|
-
const signature =
|
|
1255
|
+
const signature = sign3(updateHash, privateKey);
|
|
1148
1256
|
return {
|
|
1149
1257
|
update,
|
|
1150
1258
|
authorDID,
|
|
@@ -1157,7 +1265,7 @@ function verifyYjsEnvelopeV1(envelope) {
|
|
|
1157
1265
|
try {
|
|
1158
1266
|
const publicKey = parseDID(envelope.authorDID);
|
|
1159
1267
|
const updateHash = hash(envelope.update, "blake3");
|
|
1160
|
-
const valid =
|
|
1268
|
+
const valid = verify3(updateHash, envelope.signature, publicKey);
|
|
1161
1269
|
if (!valid) {
|
|
1162
1270
|
return { valid: false, reason: "invalid_signature" };
|
|
1163
1271
|
}
|
|
@@ -1502,9 +1610,9 @@ function reassembleChunks(chunks) {
|
|
|
1502
1610
|
}
|
|
1503
1611
|
|
|
1504
1612
|
// src/yjs-integrity.ts
|
|
1505
|
-
import { hashHex as
|
|
1613
|
+
import { hashHex as hashHex3 } from "@xnetjs/crypto";
|
|
1506
1614
|
function hashYjsState(state) {
|
|
1507
|
-
return
|
|
1615
|
+
return hashHex3(state, "blake3");
|
|
1508
1616
|
}
|
|
1509
1617
|
function verifyYjsStateIntegrity(state, expectedHash) {
|
|
1510
1618
|
return hashYjsState(state) === expectedHash;
|
|
@@ -2113,8 +2221,8 @@ function isDid(value) {
|
|
|
2113
2221
|
// src/clientid-attestation.ts
|
|
2114
2222
|
import {
|
|
2115
2223
|
hash as hash3,
|
|
2116
|
-
sign as
|
|
2117
|
-
verify as
|
|
2224
|
+
sign as sign4,
|
|
2225
|
+
verify as verify4,
|
|
2118
2226
|
hybridSign as hybridSign2,
|
|
2119
2227
|
hybridVerify as hybridVerify2,
|
|
2120
2228
|
encodeSignature as encodeSignature2,
|
|
@@ -2145,7 +2253,7 @@ function createClientIdAttestationV1(clientId, did, privateKey, room, ttlSeconds
|
|
|
2145
2253
|
const expiresAt = Math.floor(Date.now() / 1e3) + ttlSeconds;
|
|
2146
2254
|
const payload = attestationPayloadV1(clientId, did, room, expiresAt);
|
|
2147
2255
|
const payloadHash = hash3(payload, "blake3");
|
|
2148
|
-
const signature =
|
|
2256
|
+
const signature = sign4(payloadHash, privateKey);
|
|
2149
2257
|
return { clientId, did, signature, expiresAt, room };
|
|
2150
2258
|
}
|
|
2151
2259
|
function verifyClientIdAttestationV1(attestation) {
|
|
@@ -2162,7 +2270,7 @@ function verifyClientIdAttestationV1(attestation) {
|
|
|
2162
2270
|
attestation.expiresAt
|
|
2163
2271
|
);
|
|
2164
2272
|
const payloadHash = hash3(payload, "blake3");
|
|
2165
|
-
const valid =
|
|
2273
|
+
const valid = verify4(payloadHash, attestation.signature, publicKey);
|
|
2166
2274
|
if (!valid) {
|
|
2167
2275
|
return { valid: false, reason: "invalid_signature" };
|
|
2168
2276
|
}
|
|
@@ -3923,6 +4031,7 @@ export {
|
|
|
3923
4031
|
DeprecationError,
|
|
3924
4032
|
FEATURES,
|
|
3925
4033
|
HYBRID_SECURITY_POLICY,
|
|
4034
|
+
MAX_COMMIT_CHANGES,
|
|
3926
4035
|
MAX_SECURITY_POLICY,
|
|
3927
4036
|
MAX_YJS_AWARENESS_UPDATE_SIZE,
|
|
3928
4037
|
MAX_YJS_DOC_SIZE,
|
|
@@ -3951,9 +4060,12 @@ export {
|
|
|
3951
4060
|
changeHandlerRegistry,
|
|
3952
4061
|
checkAndLogDeprecations,
|
|
3953
4062
|
checkDeprecations,
|
|
4063
|
+
chunkForCommits,
|
|
3954
4064
|
chunkUpdate,
|
|
3955
4065
|
clearLoggedDeprecations,
|
|
3956
4066
|
compareLamportTimestamps,
|
|
4067
|
+
computeBatchCommitHash,
|
|
4068
|
+
computeBatchRoot,
|
|
3957
4069
|
computeChangeHash,
|
|
3958
4070
|
configureDeprecationPolicy,
|
|
3959
4071
|
createBatchId,
|
|
@@ -3971,6 +4083,7 @@ export {
|
|
|
3971
4083
|
createSerializerRegistry,
|
|
3972
4084
|
createSyncLifecycleState,
|
|
3973
4085
|
createTestContext,
|
|
4086
|
+
createUnsignedBatchCommit,
|
|
3974
4087
|
createUnsignedChange,
|
|
3975
4088
|
createUnsignedYjsChange,
|
|
3976
4089
|
createVersionedHandler,
|
|
@@ -4048,6 +4161,7 @@ export {
|
|
|
4048
4161
|
quickIntegrityCheck,
|
|
4049
4162
|
reassembleChunks,
|
|
4050
4163
|
receive,
|
|
4164
|
+
recomputeBatchCommitHash,
|
|
4051
4165
|
recomputeChangeHash,
|
|
4052
4166
|
registerDeprecation,
|
|
4053
4167
|
resolveSyncReplicationPolicy,
|
|
@@ -4057,6 +4171,7 @@ export {
|
|
|
4057
4171
|
serializeYjsEnvelope,
|
|
4058
4172
|
serializerRegistry,
|
|
4059
4173
|
shouldCompact,
|
|
4174
|
+
signBatchCommit,
|
|
4060
4175
|
signChange,
|
|
4061
4176
|
signYjsUpdate,
|
|
4062
4177
|
signYjsUpdateBatch,
|
|
@@ -4071,8 +4186,13 @@ export {
|
|
|
4071
4186
|
validateChain,
|
|
4072
4187
|
validateClientIdOwnership,
|
|
4073
4188
|
validateFeatureSet,
|
|
4189
|
+
verifyBatch,
|
|
4190
|
+
verifyBatchCommit,
|
|
4191
|
+
verifyBatchCommitFast,
|
|
4074
4192
|
verifyChange,
|
|
4193
|
+
verifyChangeFast,
|
|
4075
4194
|
verifyChangeHash,
|
|
4195
|
+
verifyChangesFast,
|
|
4076
4196
|
verifyClientIdAttestation,
|
|
4077
4197
|
verifyClientIdAttestationV1,
|
|
4078
4198
|
verifyClientIdAttestationV2,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xnetjs/sync",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Unified sync primitives for xNet (Change<T>, vector clocks, hash chains)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"provenance": true
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@xnetjs/
|
|
30
|
-
"@xnetjs/
|
|
31
|
-
"@xnetjs/
|
|
29
|
+
"@xnetjs/core": "3.0.0",
|
|
30
|
+
"@xnetjs/crypto": "3.0.0",
|
|
31
|
+
"@xnetjs/identity": "3.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"fast-check": "^4.8.0",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"test:watch": "vitest",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
|
-
"clean": "rm -rf dist"
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"api:check": "api-extractor run",
|
|
46
|
+
"api:update": "api-extractor run --local"
|
|
45
47
|
}
|
|
46
48
|
}
|