/** * Cloud copy of device vaults — the runner's coordination side * (docs/plans/obsidian-vault-connector.md §6.4, S4). * * When the owner sets a device vault's policy to `storage: mirror`, the server * pushes `runner:vault-mirror {vaultId, enabled, capability, expiresAt}` to the * computer that holds it (again on every reconnect). The capability is a * mirror-scoped Ed25519 token (aud `vault-mirror`, one vault, ≤ 10 minutes): * it lives in this process's memory only and is renewed over the socket * (`runner:vault-mirror-renew`), never written to disk, never logged. * * This class decides WHEN to sync; the Python host (localVault.py → * localVaultMirror.py) does the round itself over HTTPS: * - after an index change (host status with a new indexRev), debounced; * - when a capability arrives (enable, reconnect: uploads the device journal * written while offline); * - every 10 minutes (settings files are not watched); * - after a resume from sleep: a full stat reconciliation first (`changed` * with null), since fs.watch misses changes made while asleep. * One sync runs at a time (all vaults); requests for a vault that is already * queued or running coalesce into one more round. * * Status goes to the server as `runner:vault-mirror-status {vaultId, state, * pending, error?}`; the builder reports each published revision itself. */ import type { VaultHostStatusEvent, VaultHostTransport } from "./vaults.js"; export type MirrorState = "syncing" | "ready" | "partial" | "retry" | "busy" | "error" | "quota" | "auth" | "off"; export interface MirrorStatus { vaultId: string; state: MirrorState; pending?: number; error?: string; publishedSeq?: number; replicaRev?: number; } export interface VaultMirrorOpts { host: VaultHostTransport; /** The vault is registered (and opened) on this runner. */ isRegistered: (vaultId: string) => boolean; /** https base of the mirror routes for one vault (server proxy). */ apiBase: (vaultId: string) => string; /** Socket emit (renew requests, status). */ emit: (event: string, payload: Record) => void; log?: (m: string) => void; now?: () => number; debounceMs?: number; periodicMs?: number; tickMs?: number; /** Start the periodic timer (off in unit tests, which call tick()). */ timers?: boolean; } export declare class VaultMirrorManager { private readonly o; private readonly grants; private readonly due; private readonly lastSync; private readonly lastIndexRev; private readonly failures; private readonly renewAsked; private readonly status; private running; private wake; private ticker; private lastTick; private stopped; constructor(o: VaultMirrorOpts); private now; private get debounceMs(); private get periodicMs(); private get tickMs(); /** Vaults with a live mirror capability on this runner. */ mirrored(): string[]; latest(vaultId: string): MirrorStatus | undefined; /** Statuses to re-send after a reconnect. */ latestStatuses(): MirrorStatus[]; /** runner:vault-mirror */ setGrant(p: { vaultId?: unknown; enabled?: unknown; capability?: unknown; expiresAt?: unknown; }): void; /** Index status from the host: a new revision means files changed. */ onIndexStatus(ev: VaultHostStatusEvent): void; /** The vault was unregistered from this runner. */ forget(vaultId: string): void; /** Ask for a round of `vaultId` in `delayMs` (an earlier request wins). */ schedule(vaultId: string, delayMs: number): void; /** Periodic housekeeping: sleep detection and the 10-minute round. */ tick(): void; /** Resolves once nothing is running or due now (tests, shutdown). */ idle(): Promise; stop(): void; private startTicker; private nextDue; private arm; private pump; private report; private requestRenew; private backoff; private runOne; } /** Server proxy base for one vault's mirror routes. */ export declare function mirrorApiBase(serverHttpBase: string, vaultId: string): string;