Files
skills/docs/security-stack/security/vault/src/sync/sync.md
2026-05-29 08:20:56 +01:00

3.7 KiB

vault sync

(no submodules)

Purpose

sync.ts exposes the VaultSync orchestrator that delegates pull / push / sync operations to a VaultSyncLike adapter (production wires this to @suaveplan/vault-sync's SyncEngine). The orchestrator's responsibilities are: (1) project the in-memory item rows into the wire-shaped SyncSnapshot, (2) hydrate the delegate with the latest local snapshot before every operation, (3) translate the delegate's SyncDelegationResult into the public SyncResult envelope, and (4) wrap any delegate failure in a VAULT.SYNC.FAILED VaultError so consumers see a stable error code regardless of the underlying transport.

This keeps the sync engine generic — it has no knowledge of the vault's encryption layer, item schema, or container format. The orchestrator handles all the impedance matching at the boundary.

Features

  • pull() → SyncResult — pull remote changes via the delegate.
  • push() → SyncResult — push local changes via the delegate.
  • sync() → SyncResult — bidirectional sync via the delegate.
  • currentSnapshot() — read-through to the delegate's view.
  • Snapshot is built on-demand from rows() — the orchestrator never caches a stale snapshot.
  • All operations emit a histogram sample and an op-tagged outcome counter; failures map to VAULT.SYNC.FAILED with the underlying error chained as cause.

Usage: Basic Example

import { VaultSync } from "@suaveplan/vault/sync/sync";

const sync = new VaultSync({
    delegate: vaultSyncEngine,
    rows: () => items.allRows(),
    clock: Date.now,
});

const result = await sync.sync();
console.log(`merged ${result.merged.length} items`);

Usage: Advanced Example

import { VaultSync } from "@suaveplan/vault/sync/sync";
import { VaultError, VAULT_CODES } from "@suaveplan/vault/errors";

async function syncWithBackoff(sync: VaultSync, attempts = 3) {
    for (let i = 0; i < attempts; i++) {
        try {
            return await sync.sync();
        } catch (err) {
            if (
                !VaultError.is(err) ||
                err.code !== VAULT_CODES["SYNC.FAILED"] ||
                i === attempts - 1
            ) throw err;
            await new Promise((r) => setTimeout(r, 100 * 2 ** i));
        }
    }
    throw new Error("unreachable");
}

API Reference

  • class VaultSync.
    • constructor(options: SyncOptions).
    • pull(): Promise<SyncResult> — throws SYNC.FAILED.
    • push(): Promise<SyncResult> — throws SYNC.FAILED.
    • sync(): Promise<SyncResult> — throws SYNC.FAILED.
    • currentSnapshot(): SyncSnapshot.
  • interface SyncOptions — { delegate, rows, clock }.

Implementation Notes

  • buildSnapshot projects only the wire-essential fields (id, updatedAt, version, deleted, fieldsJson, concealedFieldsJson, tagsJson). It deliberately does not decrypt concealed fields — sync sees ciphertext only, end-to-end. The remote service stores ciphertext, the merge layer compares it as opaque bytes, and concealed-field plaintext never leaves the vault layer.
  • The orchestrator hydrates the delegate before every operation. This keeps the snapshot correct even if the caller mutates items between successive pull / push calls.
  • VAULT.SYNC.FAILED is the canonical wrapping for any error raised from the delegate. Callers can inspect err.cause for the underlying reason (transport timeout, merge conflict, payload-too-large, etc.).
  • The OTel outcome counter uses a fixed allowlist (success | failed | rejected); the orchestrator emits success and failed. The rejected slot is reserved for the lifecycle reject path (e.g. vault.sync.pull called while locked) which is enforced one layer up in Vault.