sync-limits.ts

3.4 KB · raw

1// Sync trust + resource bounds — audit fixes HV-1 (timestamp clamp) and2// MV-3 (receive-path size bounds). Shared between the background sync3// stack (vault.ts, sync-vault-adapter.ts, sync-coordinator.ts)4// and the transport (shared/sync-transport.ts), so it lives in shared/.56/**7 * HV-1: maximum tolerated forward clock skew on any timestamp received from8 * a sync peer. Matches the SYNC_HELLO skew warning window's intent — a peer9 * whose clock is slightly ahead still merges cleanly, but a deliberately10 * far-future `modifiedAt`/`deletedAt` can no longer permanently pin a11 * malicious value against honest local edits.12 */13export const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;1415/**16 * Clamp a peer-supplied timestamp to `now + MAX_CLOCK_SKEW_MS`. Applied to17 * every remote `modifiedAt` and `deletedAt` BEFORE it is compared against18 * any local timestamp (HV-1). A clamped far-future timestamp loses to any19 * local edit made more than MAX_CLOCK_SKEW_MS from now, and the monotonic20 * local entry clock (vault.ts monotonicEntryTimestamp) can always advance21 * past it.22 */23export function clampSyncTimestamp(ts: number, now: number = Date.now()): number {24  return Math.min(ts, now + MAX_CLOCK_SKEW_MS);25}2627/**28 * HOLE 2 coverage audit — every peer-supplied timestamp in the sync protocol,29 * and what stops it influencing ordering. Re-check this list when adding a30 * timestamp to any wire type.31 *32 *   entry.modifiedAt      clamped (vault.ts applyMergedRemoteEntries)33 *   entry.deletedAt       clamped (vault.ts applyMergedRemoteEntries)34 *   tombstone ts          clamped twice — sync-vault-adapter.ts boundary and35 *                         again in vault.ts, deliberately36 *   manifest.issuedAt     NOT clamped, and must not be: it is checked as a37 *                         two-sided freshness window (|now - issuedAt| >38 *                         DEVICE_MANIFEST_FRESHNESS_MS rejects the manifest39 *                         outright). Rejecting is strictly stronger than40 *                         clamping, and it never participates in ordering.41 *   entry.pairedAt        never compared — stored and displayed only. A peer42 *                         cannot reorder anything with it, and the manifest43 *                         upsert keeps `existing.pairedAt` when we already44 *                         know the device.45 *   revocation.revokedAt  clamped on merge for display sanity. Revocation is46 *                         terminal, so it is not an ordering input by47 *                         construction — that is the actual protection.48 *   SYNC_HELLO.deviceTime warning only; never merged or compared for ordering.49 */5051/**52 * MV-3: hard resource bounds on the sync receive path. A paired/vouched53 * peer (or anything that passes the Noise KK mutual static-key pin) must not be54 * able to OOM the service worker with one frame or one giant manifest.55 */56export const SYNC_LIMITS = {57  /**58   * Max bytes per WebSocket/Noise frame. Cloudflare Workers closes messages59   * above 32 MiB; 24 MiB leaves transport overhead and platform margin.60   */61  MAX_FRAME_BYTES: 24 * 1024 * 1024, // 24 MiB62  /** Max entries accepted in one SYNC_ENTRIES / manifest payload. */63  MAX_ENTRY_COUNT: 100_000,64  /** Max plaintext size of one decrypted entry envelope. */65  MAX_ENTRY_PLAINTEXT_BYTES: 512 * 1024, // 512 KiB66  /** Max tombstones accepted in one SYNC_TOMBSTONES payload. */67  MAX_TOMBSTONE_COUNT: 100_000,68} as const;