@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 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 verify(hashBytes, change.signature, publicKey);
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 sign2,
1129
- verify as verify2,
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 = sign2(updateHash, privateKey);
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 = verify2(updateHash, envelope.signature, publicKey);
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 hashHex2 } from "@xnetjs/crypto";
1613
+ import { hashHex as hashHex3 } from "@xnetjs/crypto";
1506
1614
  function hashYjsState(state) {
1507
- return hashHex2(state, "blake3");
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 sign3,
2117
- verify as verify3,
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 = sign3(payloadHash, privateKey);
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 = verify3(payloadHash, attestation.signature, publicKey);
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": "2.4.0",
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
- "import": "./dist/index.js",
16
- "types": "./dist/index.d.ts"
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/crypto": "2.4.0",
30
- "@xnetjs/identity": "2.4.0",
31
- "@xnetjs/core": "2.4.0"
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
  }