@xnetjs/sync 0.12.0 → 2.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 +88 -7
- package/dist/index.js +73 -8
- package/package.json +4 -4
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
|
-
* -
|
|
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 =
|
|
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
|
|
2991
|
-
*
|
|
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
|
+
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:
|
|
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 !==
|
|
2860
|
+
if (wire.v !== this.version) {
|
|
2829
2861
|
return {
|
|
2830
2862
|
success: false,
|
|
2831
|
-
error: `Expected
|
|
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:
|
|
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 ===
|
|
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.
|
|
3
|
+
"version": "2.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.
|
|
30
|
-
"@xnetjs/crypto": "0.
|
|
31
|
-
"@xnetjs/identity": "0.
|
|
29
|
+
"@xnetjs/core": "2.0.0",
|
|
30
|
+
"@xnetjs/crypto": "2.0.0",
|
|
31
|
+
"@xnetjs/identity": "2.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"fast-check": "^4.8.0",
|