@xnetjs/sync 0.12.0 → 1.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/dist/index.d.ts CHANGED
@@ -14,12 +14,18 @@ import { PQKeyRegistry, HybridKeyBundle } from '@xnetjs/identity';
14
14
  * Current protocol version for Change<T>.
15
15
  *
16
16
  * Version history:
17
- * - 3: Multi-level cryptography with hybrid signatures (Ed25519 + ML-DSA)
17
+ * - 4: Grinding-resistant LWW tiebreak key (exploration 0300). NOTE: the change
18
+ * signature is still **Ed25519-only** — `signChange`/`verifyChange` use the
19
+ * classical `@xnetjs/crypto` `sign`/`verify`. The hybrid/ML-DSA apparatus
20
+ * (`hybrid-signing.ts`) is NOT wired into `Change<T>` yet; wiring it (or the
21
+ * PQ envelope) is tracked in exploration 0307.
22
+ * - 3: Reserved for multi-level cryptography (hybrid Ed25519 + ML-DSA) — defined
23
+ * in the crypto layer but not carried by the change-signing path.
18
24
  * - 2: V2 compact format with abbreviated field names
19
25
  * - 1: Initial versioned protocol (adds protocolVersion field)
20
26
  * - 0/undefined: Legacy unversioned changes (backward compat)
21
27
  */
22
- declare const CURRENT_PROTOCOL_VERSION = 3;
28
+ declare const CURRENT_PROTOCOL_VERSION = 4;
23
29
  /**
24
30
  * A signed change with chain linkage and Lamport ordering.
25
31
  * Generic T allows different payload types for different use cases:
@@ -320,6 +326,13 @@ interface Fork<T = unknown> {
320
326
  * - All hashes are computed correctly (not tampered)
321
327
  * - Parent references exist in the chain (or are null for roots)
322
328
  *
329
+ * SECURITY: this is a **structural** check only — it does NOT verify Ed25519
330
+ * signatures, and a missing parent or a fork is reported (or tolerated) rather
331
+ * than treated as a hard failure. It is not an admission gate for untrusted
332
+ * input: authenticate authorship with `verifyChange` (and apply LWW/idempotency)
333
+ * at the ingest boundary — see `NodeStore.applyRemoteChange` and the hub's
334
+ * `node-relay.ts`, which run the real crypto checks (exploration 0307).
335
+ *
323
336
  * @param changes - The changes to validate
324
337
  * @returns Validation result
325
338
  */
@@ -1923,7 +1936,7 @@ interface AuthorizedYjsSyncProviderOptions<TDoc extends YDocLike = YDocLike> {
1923
1936
  verifyEnvelope?: (envelope: SignedYjsEnvelopeV2) => Promise<EnvelopeVerificationResult>;
1924
1937
  onRejected?: (input: {
1925
1938
  peerId: DID;
1926
- reason: 'rate-exceeded' | 'invalid-signature' | 'unauthorized';
1939
+ reason: 'rate-exceeded' | 'oversized' | 'invalid-signature' | 'unauthorized';
1927
1940
  }) => void;
1928
1941
  }
1929
1942
  declare class AuthorizedYjsSyncProvider<TDoc extends YDocLike = YDocLike> {
@@ -1936,6 +1949,16 @@ declare class AuthorizedYjsSyncProvider<TDoc extends YDocLike = YDocLike> {
1936
1949
  private readonly verifyEnvelope;
1937
1950
  private readonly onRejected?;
1938
1951
  constructor(options: AuthorizedYjsSyncProviderOptions<TDoc>);
1952
+ /**
1953
+ * Admit a remote Yjs update: rate-limit → size cap → signature → authz →
1954
+ * doc-binding, then apply.
1955
+ *
1956
+ * SECURITY (exploration 0307): the peer scorer here is **advisory** — it
1957
+ * accumulates violation penalties but this method does not itself refuse an
1958
+ * already-blocked peer; wire `YjsPeerScorer.getPeerAction`/`onAction` to a
1959
+ * transport that actually drops blocked peers. The size cap below and the
1960
+ * signature/authz/doc-binding checks are the hard gates.
1961
+ */
1939
1962
  handleRemoteUpdate(envelope: SignedYjsEnvelopeV2): Promise<boolean>;
1940
1963
  }
1941
1964
  declare class AuthorizedYjsError extends Error {
@@ -2038,6 +2061,14 @@ interface VerifyAttestationOptions {
2038
2061
  registry?: PQKeyRegistry;
2039
2062
  /** Minimum security level required */
2040
2063
  minLevel?: SecurityLevel;
2064
+ /**
2065
+ * Reject an attestation whose `clientId` is not the value derived from its
2066
+ * DID ({@link deriveClientIdFromDid}) — exploration 0305. Constrains the Yjs
2067
+ * clientID so a client cannot pick a maximal one to win insert races. Default
2068
+ * `false` for backward compatibility with pre-0305 random clientIDs; xNet's
2069
+ * own verification path enables it once clients derive their clientID.
2070
+ */
2071
+ enforceDerivedClientId?: boolean;
2041
2072
  }
2042
2073
  /**
2043
2074
  * Check if an attestation is V2 format.
@@ -2047,6 +2078,23 @@ declare function isV2Attestation(attestation: ClientIdAttestation): attestation
2047
2078
  * Check if an attestation is V1 format.
2048
2079
  */
2049
2080
  declare function isV1Attestation(attestation: ClientIdAttestation): attestation is ClientIdAttestationV1;
2081
+ /**
2082
+ * Derive a Yjs `clientID` deterministically from a DID (exploration 0305).
2083
+ *
2084
+ * Yjs assigns a *random* integer clientID and breaks concurrent same-position
2085
+ * insert ties by *higher clientID*, so a client is free to pick
2086
+ * `clientID = 0x7FFFFFFF` and win every insert race. Deriving the clientID from
2087
+ * the DID removes that free choice: the value is now a random-oracle function of
2088
+ * identity, so a client cannot pick a maximal one without grinding a whole
2089
+ * keypair (and even then it wins no *durable* advantage — see the LWW tiebreak
2090
+ * key, `@xnetjs/core`).
2091
+ *
2092
+ * Returns a positive 31-bit integer (1 … 2^31−1) — comfortably inside Yjs's
2093
+ * clientID range and never zero (Yjs treats 0 as unset in some paths).
2094
+ */
2095
+ declare function deriveClientIdFromDid(did: string): number;
2096
+ /** Whether a clientID is the one derived from `did` (exploration 0305). */
2097
+ declare function isDerivedClientId(clientId: number, did: string): boolean;
2050
2098
  /**
2051
2099
  * Create a signed clientID attestation (V1 format).
2052
2100
  *
@@ -2814,6 +2862,12 @@ declare class ChangeHandlerRegistry {
2814
2862
  declare const changeHandlerRegistry: ChangeHandlerRegistry;
2815
2863
  /**
2816
2864
  * Create a simple handler that accepts all versions.
2865
+ *
2866
+ * SECURITY: the handler pipeline does NOT verify change hashes or signatures,
2867
+ * and the default `validate` accepts everything. Handlers process
2868
+ * already-authenticated changes — callers MUST verify authorship (`verifyChange`
2869
+ * + `verifyChangeHash`) before routing untrusted input through the registry
2870
+ * (exploration 0307).
2817
2871
  */
2818
2872
  declare function createHandler<T>(type: string, process: (change: Change<T>, context: HandlerContext) => Promise<void>, validate?: (change: Change<T>) => ValidationResult): ChangeHandler<T>;
2819
2873
  /**
@@ -2974,6 +3028,14 @@ interface VerifyOptions {
2974
3028
  maxFutureTimestamp?: number;
2975
3029
  /** Progress callback */
2976
3030
  onProgress?: (checked: number, total: number) => void;
3031
+ /**
3032
+ * Resolve an author DID to its Ed25519 public key so signatures can be
3033
+ * verified cryptographically (not merely checked for presence). Defaults to
3034
+ * {@link parseDID} — a `did:key` is self-certifying (the DID *is* the public
3035
+ * key), so no external key lookup is needed. Provide a custom resolver only
3036
+ * for non-`did:key` identities.
3037
+ */
3038
+ resolveKey?: (did: string) => Uint8Array;
2977
3039
  }
2978
3040
  /**
2979
3041
  * Verify the integrity of a set of changes.
@@ -2987,8 +3049,10 @@ interface VerifyOptions {
2987
3049
  */
2988
3050
  declare function verifyIntegrity(changes: Change<unknown>[], options?: VerifyOptions): Promise<IntegrityReport>;
2989
3051
  /**
2990
- * Quick integrity check - only verifies hashes, not signatures.
2991
- * Much faster but less thorough.
3052
+ * Quick integrity check verifies **hashes and chain only**, skipping
3053
+ * signature verification. Faster, but it does NOT authenticate authorship, so
3054
+ * a change with a forged signature and a self-consistent hash passes. Use the
3055
+ * full {@link verifyIntegrity} (default) when authorship matters.
2992
3056
  */
2993
3057
  declare function quickIntegrityCheck(changes: Change<unknown>[]): Promise<IntegrityReport>;
2994
3058
  /**
@@ -3014,11 +3078,28 @@ declare function findHeads(changes: Change<unknown>[]): Change<unknown>[];
3014
3078
  * Get the chain depth (longest path from any root to any head).
3015
3079
  */
3016
3080
  declare function getChainDepth(changes: Change<unknown>[]): number;
3081
+ /**
3082
+ * Options controlling {@link attemptRepair}.
3083
+ */
3084
+ interface RepairOptions {
3085
+ /**
3086
+ * Allow the `recompute-hash` repair, which **overwrites** a change's stored
3087
+ * hash with a freshly computed one. On tampered data this silently reconciles
3088
+ * the tampered payload to a consistent hash (laundering it) rather than
3089
+ * rejecting it, so it is refused unless the caller explicitly asserts the
3090
+ * changes come from a trusted source (e.g. local self-repair, never untrusted
3091
+ * peer input) — exploration 0307. Default: `false`.
3092
+ */
3093
+ trustHashRecompute?: boolean;
3094
+ }
3017
3095
  /**
3018
3096
  * Attempt to repair issues that can be fixed automatically.
3019
3097
  * Returns the repaired changes and any issues that couldn't be fixed.
3098
+ *
3099
+ * SECURITY: `recompute-hash` is gated behind {@link RepairOptions.trustHashRecompute}
3100
+ * — do not enable it for changes received from untrusted peers.
3020
3101
  */
3021
- declare function attemptRepair(changes: Change<unknown>[], issues: IntegrityIssue[]): Promise<{
3102
+ declare function attemptRepair(changes: Change<unknown>[], issues: IntegrityIssue[], options?: RepairOptions): Promise<{
3022
3103
  repaired: Change<unknown>[];
3023
3104
  remainingIssues: IntegrityIssue[];
3024
3105
  repairCount: number;
@@ -3407,4 +3488,4 @@ declare function createSecurityPolicy(options?: Partial<SecurityPolicy>): Securi
3407
3488
  */
3408
3489
  declare function mergeSecurityPolicies(...policies: Partial<SecurityPolicy>[]): SecurityPolicy;
3409
3490
 
3410
- export { ALL_FEATURES, type AttestationVerificationResult, type AttestationVerifyResult, type AuthorizedDoc, type AuthorizedRoom, type AuthorizedStateAdapter, AuthorizedSyncManager, type AuthorizedSyncManagerOptions, AuthorizedYjsError, AuthorizedYjsSyncProvider, type AuthorizedYjsSyncProviderOptions, BaseSyncProvider, type BatchFlushCallback, COMPACTION_TIME_THRESHOLD, COMPACTION_UPDATE_THRESHOLD, CURRENT_PROTOCOL_VERSION, type ChainValidationResult, type Change, type ChangeHandler, ChangeHandlerRegistry, type ChangeSerializer, type ChangeSigner, type ClientIdAttestation, type ClientIdAttestationV1, type ClientIdAttestationV2, type ClientIdAttestationWire, type ClientIdMap, ClientIdMapImpl, type ContentKeyProvider, type CreateAttestationOptions, type CreateChangeOptions, type CreateEnvelopeOptions, type CreateYjsChangeOptions, DEFAULT_BATCHER_CONFIG, DEFAULT_RATE_LIMITER_CONFIG, DEFAULT_SECURITY_POLICY, DEFAULT_YJS_SCORING_CONFIG, DEPRECATIONS, DEPRECATION_POLICY, type DeprecationCallback, type DeprecationContext, DeprecationError, type DeprecationNotice, type DeprecationType, type DeprecationWarning, type DeserializeError, type DeserializeOutcome, type DeserializeResult, type EncryptedYjsState, type EnvelopeVerificationResult, type EnvelopeVerifyResult, FEATURES, type FeatureConfig, type FeatureFlag, type FeatureValidationError, type FeatureValidationResult, type FeatureValidationWarning, type Fork, type GrantEventStore, HYBRID_SECURITY_POLICY, type HandlerContext, type HandlerEvent, type IntegrityIssue, type IntegrityIssueType, type IntegrityMonitor, type IntegrityMonitorConfig, type IntegrityMonitorStats, type IntegrityReport, type LamportClock, type LamportTimestamp, MAX_SECURITY_POLICY, MAX_YJS_AWARENESS_UPDATE_SIZE, MAX_YJS_DOC_SIZE, MAX_YJS_STATE_VECTOR_SIZE, MAX_YJS_UPDATES_PER_MINUTE, MAX_YJS_UPDATES_PER_SECOND, MAX_YJS_UPDATE_SIZE, type MergeUpdatesFn, type NegotiatedSession, type NegotiationFailure, type NegotiationResult, type NegotiationWarning, type OperationType, type PeerAction, type PeerCapabilities, type PeerInfo, type PersistedDocState, type PolicyRevisionSimulation, type ProcessResult, type RateLimiterConfig, type ReactIntegrityMonitorOptions, type RecipientKeyResolver, type RegistryStats, type RepairAction, type RepairActionType, type ReplicationNamespaceKind, type ReplicationPlan, type ReplicationPlanDestination, type ReplicationPlanDiagnostic, type ReplicationPlanTraceStep, type ResolvedSyncReplicationPolicy, type SecurityPolicy, type SerializeOptions, type SerializedChange, type SerializerRegistry, type SignedYjsEnvelope, type SignedYjsEnvelopeV1, type SignedYjsEnvelopeV2, type SignedYjsEnvelopeWire, type SyncCompatibilityConfig, type SyncConnectionStatus, type SyncEventListener, type SyncFederationConfig, type SyncFederationHub, type SyncFederationNamespacePolicy, type SyncLifecycleInput, type SyncLifecyclePhase, type SyncLifecycleState, type SyncProvider, type SyncProviderEvents, type SyncProviderOptions, type SyncReplicationConfig, type SyncStatus, type UnsignedChange, type UnsignedYjsChange, V1Serializer, V2Serializer, type ValidationError, type ValidationResult, type ValidationWarning, type VerifyAttestationOptions, type VerifyEnvelopeOptions, type VerifyOptions, VersionNegotiator, type YDocCodec, type YDocLike, YJS_CHANGE_TYPE, YJS_RATE_BURST_ALLOWANCE, YJS_SYNC_CHUNK_SIZE, type YjsAuthDecision, YjsAuthGate, type YjsAuthGateOptions, YjsBatcher, type YjsBatcherConfig, type YjsChange, YjsCheckpointer, type YjsCheckpointerOptions, YjsIntegrityError, type YjsPeerActionEvent, type YjsPeerActionListener, type YjsPeerMetrics, YjsPeerScorer, YjsRateLimiter, type YjsRateLimiterOptions, type YjsScoringConfig, YjsStateIntegrityError, type YjsUpdatePayload, type YjsViolationType, addDependencies, attemptRepair, autoDeserialize, autoSerialize, calculateChunkCount, changeHandlerRegistry, checkAndLogDeprecations, checkDeprecations, chunkUpdate, clearLoggedDeprecations, compareLamportTimestamps, computeChangeHash, configureDeprecationPolicy, createBatchId, createChangeId, createClientIdAttestation, createClientIdAttestationV1, createClientIdAttestationV2, createHandler, createIntegrityMonitor, createLamportClock, createLocalCapabilities, createPersistedDocState, createReactIntegrityMonitor, createSecurityPolicy, createSerializerRegistry, createSyncLifecycleState, createTestContext, createUnsignedChange, createUnsignedYjsChange, createVersionedHandler, createWebCryptoChangeSigner, createYjsChange, decryptYjsState, defaultNegotiator, deriveSyncLifecyclePhase, deserializeClientIdAttestation, deserializeEncryptedYjsState, deserializeYjsEnvelope, detectFork, diffFeatures, encryptYjsState, envelopeSize, estimateBase64DecodedLength, findCommonAncestor, findHeads, findOrphans, findRoots, formatDeprecationReport, formatIntegrityReport, getAllDependencies, getAncestry, getChainDepth, getChainHeads, getChainRoots, getChangeNodeId, getDefaultSerializer, getDeprecation, getDeprecationsByType, getEnabledFeatures, getFeatureConflicts, getFeatureDependencies, getFeatureVersion, getForks, getOptionalFeatures, getRequiredFeatures, getSecurityLevel, getSerializer, hasSignedEnvelope, hashYjsState, inferReplicationNamespaceKind, intersectFeatures, isAfter, isAwarenessUpdateTooLarge, isBase64PayloadTooLarge, isBefore, isCriticalOperation, isDeprecated, isDocumentTooLarge, isEphemeralOperation, isFeatureAvailable, isFeatureEnabled, isLegacyUpdate, isNodeChange, isRemoved, isStateVectorTooLarge, isUpdateTooLarge, isV1Attestation, isV1Envelope, isV2Attestation, isV2Envelope, isYjsChange, loadVerifiedState, logDeprecation, maxTime, mergeSecurityPolicies, normalizeSyncFederationHubs, parseCapabilities, parseTimestamp, planReplicationDestinations, quickIntegrityCheck, reassembleChunks, receive, recomputeChangeHash, registerDeprecation, resolveSyncReplicationPolicy, serializeClientIdAttestation, serializeEncryptedYjsState, serializeTimestamp, serializeYjsEnvelope, serializerRegistry, shouldCompact, signChange, signYjsUpdate, signYjsUpdateBatch, signYjsUpdateV1, signYjsUpdateV2, simulateSyncPolicyRevision, tick, toEncryptedData, topologicalSort, v1Serializer, v2Serializer, validateChain, validateClientIdOwnership, validateFeatureSet, verifyChange, verifyChangeHash, verifyClientIdAttestation, verifyClientIdAttestationV1, verifyClientIdAttestationV2, verifyIntegrity, verifyPersistedDocState, verifySingleChange, verifyYjsEnvelope, verifyYjsEnvelopeQuick, verifyYjsEnvelopeV1, verifyYjsEnvelopeV2, verifyYjsStateIntegrity };
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 };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/change.ts
2
2
  import { hashHex, sign, verify } from "@xnetjs/crypto";
3
- var CURRENT_PROTOCOL_VERSION = 3;
3
+ var CURRENT_PROTOCOL_VERSION = 4;
4
4
  function createUnsignedChange(options) {
5
5
  const unsigned = {
6
6
  protocolVersion: CURRENT_PROTOCOL_VERSION,
@@ -443,6 +443,9 @@ var FEATURES = {
443
443
  required: false,
444
444
  description: "Compressed change payloads for bandwidth efficiency"
445
445
  }
446
+ // NOTE: the protocol-v4 grinding-resistant LWW tiebreak (exploration 0305) is
447
+ // NOT a negotiated feature flag — it is a convergence rule gated purely on a
448
+ // change's `protocolVersion >= 4` (see `@xnetjs/core`'s compareLwwStamps).
446
449
  };
447
450
  var ALL_FEATURES = Object.keys(FEATURES);
448
451
  function getEnabledFeatures(protocolVersion) {
@@ -2049,6 +2052,16 @@ var AuthorizedYjsSyncProvider = class {
2049
2052
  this.verifyEnvelope = options.verifyEnvelope ?? verifyYjsEnvelopeV2;
2050
2053
  this.onRejected = options.onRejected;
2051
2054
  }
2055
+ /**
2056
+ * Admit a remote Yjs update: rate-limit → size cap → signature → authz →
2057
+ * doc-binding, then apply.
2058
+ *
2059
+ * SECURITY (exploration 0307): the peer scorer here is **advisory** — it
2060
+ * accumulates violation penalties but this method does not itself refuse an
2061
+ * already-blocked peer; wire `YjsPeerScorer.getPeerAction`/`onAction` to a
2062
+ * transport that actually drops blocked peers. The size cap below and the
2063
+ * signature/authz/doc-binding checks are the hard gates.
2064
+ */
2052
2065
  async handleRemoteUpdate(envelope) {
2053
2066
  const peerId = envelope.meta.authorDID;
2054
2067
  if (this.rateLimiter && !this.rateLimiter.allow(peerId)) {
@@ -2056,6 +2069,11 @@ var AuthorizedYjsSyncProvider = class {
2056
2069
  this.onRejected?.({ peerId, reason: "rate-exceeded" });
2057
2070
  return false;
2058
2071
  }
2072
+ if (isUpdateTooLarge(envelope.update)) {
2073
+ this.peerScorer.penalize(peerId, "oversizedUpdate");
2074
+ this.onRejected?.({ peerId, reason: "oversized" });
2075
+ return false;
2076
+ }
2059
2077
  const sigResult = await this.verifyEnvelope(envelope);
2060
2078
  if (!sigResult.valid) {
2061
2079
  this.peerScorer.penalize(peerId, "invalidSignature");
@@ -2110,6 +2128,15 @@ function isV2Attestation(attestation) {
2110
2128
  function isV1Attestation(attestation) {
2111
2129
  return !("v" in attestation);
2112
2130
  }
2131
+ function deriveClientIdFromDid(did) {
2132
+ const digest = hash3(new TextEncoder().encode(`xnet-clientid:${did}`), "blake3");
2133
+ const view = new DataView(digest.buffer, digest.byteOffset, digest.byteLength);
2134
+ const value = view.getUint32(0, false) & 2147483647;
2135
+ return value === 0 ? 1 : value;
2136
+ }
2137
+ function isDerivedClientId(clientId, did) {
2138
+ return clientId === deriveClientIdFromDid(did);
2139
+ }
2113
2140
  function attestationPayloadV1(clientId, did, room, expiresAt) {
2114
2141
  const text = `clientid-bind:${clientId}:${did}:${room}:${expiresAt}`;
2115
2142
  return new TextEncoder().encode(text);
@@ -2177,12 +2204,17 @@ function createClientIdAttestationV2(clientId, room, keyBundle, options = {}) {
2177
2204
  };
2178
2205
  }
2179
2206
  async function verifyClientIdAttestationV2(attestation, options = {}) {
2180
- const { registry, minLevel = 0 } = options;
2207
+ const { registry, minLevel = 0, enforceDerivedClientId = false } = options;
2181
2208
  const errors = [];
2182
2209
  const expired = attestation.expiresAt !== void 0 && Date.now() > attestation.expiresAt;
2183
2210
  if (expired) {
2184
2211
  errors.push("Attestation has expired");
2185
2212
  }
2213
+ if (enforceDerivedClientId && !isDerivedClientId(attestation.clientId, attestation.did)) {
2214
+ errors.push(
2215
+ `clientId ${attestation.clientId} is not derived from the DID (expected ${deriveClientIdFromDid(attestation.did)})`
2216
+ );
2217
+ }
2186
2218
  if (attestation.signature.level < minLevel) {
2187
2219
  errors.push(
2188
2220
  `Security level too low: ${attestation.signature.level} < ${minLevel} (required minimum)`
@@ -2798,7 +2830,7 @@ var V3Serializer = class {
2798
2830
  throw new Error("Invalid signature type in change");
2799
2831
  }
2800
2832
  const wire = {
2801
- v: 3,
2833
+ v: this.version,
2802
2834
  i: change.id,
2803
2835
  t: change.type,
2804
2836
  p: change.payload,
@@ -2825,10 +2857,10 @@ var V3Serializer = class {
2825
2857
  } else {
2826
2858
  wire = data;
2827
2859
  }
2828
- if (wire.v !== 3) {
2860
+ if (wire.v !== this.version) {
2829
2861
  return {
2830
2862
  success: false,
2831
- error: `Expected v3 format, got v${wire.v}. Clear your database and start fresh.`,
2863
+ error: `Expected v${this.version} format, got v${wire.v}. Clear your database and start fresh.`,
2832
2864
  rawData: data
2833
2865
  };
2834
2866
  }
@@ -2855,7 +2887,7 @@ var V3Serializer = class {
2855
2887
  }
2856
2888
  const signature = decodeSignature3(wire.sig);
2857
2889
  const change = {
2858
- protocolVersion: 3,
2890
+ protocolVersion: this.version,
2859
2891
  id: wire.i,
2860
2892
  type: wire.t,
2861
2893
  payload: wire.p,
@@ -2886,7 +2918,7 @@ var V3Serializer = class {
2886
2918
  return false;
2887
2919
  }
2888
2920
  const obj = data;
2889
- return obj.v === 3 && typeof obj.i === "string" && typeof obj.t === "string" && typeof obj.sig === "object" && obj.sig !== null;
2921
+ return obj.v === this.version && typeof obj.i === "string" && typeof obj.t === "string" && typeof obj.sig === "object" && obj.sig !== null;
2890
2922
  }
2891
2923
  /**
2892
2924
  * Add schema version to a payload.
@@ -2913,6 +2945,13 @@ var V3Serializer = class {
2913
2945
  };
2914
2946
  var v3Serializer = new V3Serializer();
2915
2947
 
2948
+ // src/serializers/v4.ts
2949
+ var V4Serializer = class extends V3Serializer {
2950
+ version = 4;
2951
+ name = "V4 Serializer (v3 wire + grinding-resistant LWW tiebreak)";
2952
+ };
2953
+ var v4Serializer = new V4Serializer();
2954
+
2916
2955
  // src/serializers/index.ts
2917
2956
  var DefaultSerializerRegistry = class {
2918
2957
  serializers = /* @__PURE__ */ new Map();
@@ -2922,6 +2961,7 @@ var DefaultSerializerRegistry = class {
2922
2961
  this.register(v1Serializer);
2923
2962
  this.register(v2Serializer);
2924
2963
  this.register(v3Serializer);
2964
+ this.register(v4Serializer);
2925
2965
  }
2926
2966
  get(version) {
2927
2967
  return this.serializers.get(version);
@@ -3145,6 +3185,7 @@ function createTestContext(overrides) {
3145
3185
  }
3146
3186
 
3147
3187
  // src/integrity.ts
3188
+ import { parseDID as parseDID3 } from "@xnetjs/identity";
3148
3189
  async function verifyIntegrity(changes, options = {}) {
3149
3190
  const startTime = Date.now();
3150
3191
  const issues = [];
@@ -3211,6 +3252,24 @@ async function verifyIntegrity(changes, options = {}) {
3211
3252
  // No automatic repair - would need original signing key
3212
3253
  });
3213
3254
  hasIssue = true;
3255
+ } else {
3256
+ const resolveKey = options.resolveKey ?? parseDID3;
3257
+ let signatureValid = false;
3258
+ try {
3259
+ signatureValid = verifyChange(change, resolveKey(change.authorDID));
3260
+ } catch {
3261
+ signatureValid = false;
3262
+ }
3263
+ if (!signatureValid) {
3264
+ issues.push({
3265
+ changeId: change.id,
3266
+ type: "signature-invalid",
3267
+ details: "Signature does not verify against the author DID",
3268
+ severity: "error"
3269
+ // No automatic repair - would need original signing key
3270
+ });
3271
+ hasIssue = true;
3272
+ }
3214
3273
  }
3215
3274
  }
3216
3275
  if (!options.skipChain && change.parentHash !== null) {
@@ -3327,7 +3386,7 @@ function getChainDepth(changes) {
3327
3386
  }
3328
3387
  return getDepth(null);
3329
3388
  }
3330
- async function attemptRepair(changes, issues) {
3389
+ async function attemptRepair(changes, issues, options = {}) {
3331
3390
  const changeMap = new Map(changes.map((c) => [c.id, { ...c }]));
3332
3391
  const remainingIssues = [];
3333
3392
  let repairCount = 0;
@@ -3343,6 +3402,10 @@ async function attemptRepair(changes, issues) {
3343
3402
  }
3344
3403
  switch (issue.repairAction.type) {
3345
3404
  case "recompute-hash":
3405
+ if (!options.trustHashRecompute) {
3406
+ remainingIssues.push(issue);
3407
+ break;
3408
+ }
3346
3409
  change.hash = await computeChangeHash(change);
3347
3410
  repairCount++;
3348
3411
  break;
@@ -3915,6 +3978,7 @@ export {
3915
3978
  createYjsChange,
3916
3979
  decryptYjsState,
3917
3980
  defaultNegotiator,
3981
+ deriveClientIdFromDid,
3918
3982
  deriveSyncLifecyclePhase,
3919
3983
  deserializeClientIdAttestation,
3920
3984
  deserializeEncryptedYjsState,
@@ -3958,6 +4022,7 @@ export {
3958
4022
  isBefore,
3959
4023
  isCriticalOperation,
3960
4024
  isDeprecated,
4025
+ isDerivedClientId,
3961
4026
  isDocumentTooLarge,
3962
4027
  isEphemeralOperation,
3963
4028
  isFeatureAvailable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/sync",
3
- "version": "0.12.0",
3
+ "version": "1.0.0",
4
4
  "description": "Unified sync primitives for xNet (Change<T>, vector clocks, hash chains)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  "provenance": true
27
27
  },
28
28
  "dependencies": {
29
- "@xnetjs/core": "0.12.0",
30
- "@xnetjs/crypto": "0.12.0",
31
- "@xnetjs/identity": "0.12.0"
29
+ "@xnetjs/core": "1.0.0",
30
+ "@xnetjs/crypto": "1.0.0",
31
+ "@xnetjs/identity": "1.0.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "fast-check": "^4.8.0",