/** * The cloud vault's snapshot manifest: per-table row counts and keyed hashes * of a `cleo.db`, computed the same way on every machine so two machines can * tell whether they hold the same data (T12336, T12950). * * - Only tables whose class syncs across devices are listed * ({@link isPortableTableClass}), minus `portable-secret` (a vault bundle * never carries credentials). Local-only, derived, pending and unclassified * tables differ per machine by design and are left out. * - A table's hash is HMAC-SHA256, keyed by a key derived from the stream's * data key, over the sorted per-row SHA-256 digests. Sorting makes it * independent of physical row order (a VACUUM can renumber rowids), and the * key keeps the plaintext manifest from being a dictionary oracle for the * server: it sees counts, never guessable digests. * - Cells a restore relocates (path and JSON locator columns, as * {@link relocatableColumn} decides) are hashed as if relocated to a * placeholder root, so a relocated store hashes the same as its source. * Historical text, which a restore leaves as written, is hashed as is. * - Cells that never sync hash as NULL ({@link vaultLocalColumns}): credential * columns (an unencrypted bundle clears them) and every column the * classification registry gives its own `local-only`, `portable-secret` or * `strip` class. A restore keeps this machine's values for them * ({@link carryMachineState}), so the two sides must agree without them. * - The rest of a snapshot (other primary databases, plain files) is folded * into pseudo-table entries ({@link vaultDatabaseEntry}, * {@link vaultFilesEntry}) so a change anywhere in the bundle is seen. * * @task T12336 * @epic T12322 */ import type { TableScope } from '@cleocode/contracts'; /** * Version of the manifest computation (which cells hash, and how; which * pseudo-entries exist). 2: non-syncing columns hash as NULL, plus the * `zz_vault_db_*` and `zz_vault_files` entries (T12967, T12969). 3: a row * local by its row-identity spec (no uid, by `localWhenNull` or * `localUnlessFirst`) is neither counted nor hashed, as the change journal * never carries it (T13808). * * It is not the manifest's wire `schemaVersion`: that is * {@link SYNC_SCHEMA_VERSION}, the stream data numbering the vault shares with * the change journal (T13034), and a computation change must not move it (the * server reads a rise there as a schema transition every v3 checkpoint pins). * Each snapshot records its format in a {@link VAULT_FORMAT_KEY} entry * ({@link vaultFormatEntry}), so a reader tells "hashed another way" from * "does not match" ({@link vaultManifestFormat}). Bump it with any change to * what a manifest hashes. */ export declare const VAULT_MANIFEST_FORMAT_VERSION = 3; /** * Manifest key of a snapshot's format record (T13034): which * {@link VAULT_MANIFEST_FORMAT_VERSION} its hashes were computed under. Zero * rows, so the server's count check is unaffected; an annotation, never a * table, so comparisons ignore it. Its hash is keyed: only key holders read * the version. */ export declare const VAULT_FORMAT_KEY = "zz_vault_format"; /** * Manifest key of a snapshot's authored-state digest (T13850): one keyed hash * over every `cleo.db` table's hash, with what memory reads write left out * ({@link READ_SIDE_COLUMNS}, {@link READ_SIDE_TABLES}). Equal on both sides, * a store's table changes since a sync are read telemetry, which a pull may * replace. Zero rows and an annotation, like {@link VAULT_FORMAT_KEY}, so the * server's count check and every table comparison skip it. */ export declare const VAULT_AUTHORED_KEY = "zz_vault_authored"; /** One table's count and keyed hash. */ export interface VaultTableEntry { /** Row count. */ rows: number; /** HMAC-SHA256 hex. */ hash: string; } /** A snapshot manifest, in the wire shape (`Manifest` of the Nexus contract). */ export interface VaultManifest { /** The wire `schemaVersion`: {@link SYNC_SCHEMA_VERSION} on what the vault writes. */ schemaVersion: number; /** Per table. */ tables: Record; } /** Manifest key of a snapshot's plain-file inventory (rows: files, hash: keyed inventory hash). */ export declare const VAULT_FILES_KEY = "zz_vault_files"; /** Prefix of the manifest key of each primary database other than `cleo.db`. */ export declare const VAULT_DB_KEY_PREFIX = "zz_vault_db_"; /** * The manifest key of a non-`cleo.db` primary database, from its path in the * section (`blobs/manifest.db` -> `zz_vault_db_blobs_manifest_db`). * * @param relPath - Path relative to the section root. * @returns A key the wire contract accepts. */ export declare function vaultDatabaseKey(relPath: string): string; /** * Columns of `table` that never sync, so the vault neither compares nor * restores them over this machine's values: credential columns * ({@link CREDENTIAL_COLUMNS}) and whole-column overrides of the * classification registry (`local-only`, `portable-secret`, `strip`). A * JSON-path override (part of a value) does not exclude the column. * * @param scope - Store scope. * @param table - Table name. * @returns Column names (possibly absent from the actual table). */ export declare function vaultLocalColumns(scope: TableScope, table: string): readonly string[]; /** * Whole columns the classification registry gives the `strip` class (a value * stripped from everything that leaves the device, such as a git remote URL, * which can embed a token), by table. A vault snapshot never carries them * (T13007); like every non-syncing column they hash as NULL and a restore * keeps this machine's values. * * @param scope - Store scope. * @returns Table to stripped column names. */ export declare function vaultStripColumns(scope: TableScope): Record; /** Options for {@link buildVaultManifest}. */ export interface BuildVaultManifestOptions { /** Which `cleo.db` this is (decides the table classes). */ scope: TableScope; /** List every table, classified or not (a primary database other than `cleo.db`). */ allTables?: boolean; /** Key the hashes are made with (derive it from the stream data key). */ hashKey: Uint8Array; /** The store's root as written in its rows; relocatable cells are re-rooted from it. `null`: none (a store a restore does not relocate). */ root: string | null; /** Count and hash rows local by row-identity spec too: format 2, to check a snapshot hashed under it (T13808). */ countLocalRows?: boolean; } /** The manifest plus the tables left out, for reports. */ export interface VaultManifestBuild { manifest: VaultManifest; /** Syncing tables whose name the wire contract cannot carry. */ skipped: string[]; /** Per table, the rows local by row-identity spec, left out of the manifest (T13808). */ localRows: Record; } /** * Whether a table belongs in a vault manifest of `scope`. * * @param scope - Store scope. * @param name - Physical table name. * @returns `true` for syncing, non-secret tables. */ export declare function isVaultManifestTable(scope: TableScope, name: string): boolean; /** * Build the vault manifest of one `cleo.db`. * * Opens the file read-only; the caller passes a snapshot (a staged bundle * copy) or the live store, whose single read transaction keeps it consistent. * * @param dbPath - The database file. * @param opts - Scope, hash key and root to normalise. * @returns The manifest and the tables skipped for their name. */ export declare function buildVaultManifest(dbPath: string, opts: BuildVaultManifestOptions): VaultManifestBuild; /** * One manifest entry for a whole primary database other than `cleo.db` * (blobs manifest, attachments index): every table, relocation-aware like * {@link buildVaultManifest}, folded into one count and keyed hash. * * @param dbPath - The database file. * @param opts - Hash key and root to normalise. * @returns Rows over all tables, and the keyed hash over every table's hash. */ export declare function vaultDatabaseEntry(dbPath: string, opts: { hashKey: Uint8Array; root: string | null; }): VaultTableEntry; /** * Keys of the global `config.json` that belong to the install, not to the * user (T13022): never compared, and a restore keeps this machine's value. */ export declare const VAULT_GLOBAL_CONFIG_LOCAL_KEYS: ReadonlyArray; /** Options of {@link vaultFileDigest}. */ export interface VaultFileDigestOptions { /** Which store the file belongs to. */ scope: TableScope; /** The store's root as written in the file (project), or `null`. */ root: string | null; /** The file's SHA-256 when already known (a bundle manifest lists it). */ sha256?: string; } /** * The digest a vault file inventory records for one plain file (T13005, * T13022). Config files are hashed as JSON with sorted keys, so reordering is * not a change: the project JSON files a restore relocates * ({@link RELOCATED_JSON_FILES}: `config.json`, `project-context.json`) with * their paths re-rooted from `root` onto a placeholder, so a relocated copy * hashes like its source; the global `config.json` without its install keys * ({@link VAULT_GLOBAL_CONFIG_LOCAL_KEYS}). Every other file, and a config * file that is not JSON, is its SHA-256. * * @param absPath - The file. * @param relPath - Its path in the section. * @param opts - Scope, root and known SHA-256. * @returns Hex digest. */ export declare function vaultFileDigest(absPath: string, relPath: string, opts: VaultFileDigestOptions): Promise; /** * One manifest entry for a snapshot's plain files: the count, and a keyed * hash over every (path, sha256) pair, sorted by path. * * @param files - Files as the bundle manifest lists them. * @param hashKey - The manifest hash key. * @returns The entry. */ export declare function vaultFilesEntry(files: ReadonlyArray<{ relPath: string; sha256: string; }>, hashKey: Uint8Array): VaultTableEntry; /** One table's comparison. */ export interface VaultTableComparison { table: string; localRows: number | null; cloudRows: number | null; match: boolean; } /** * Manifest key that labels a snapshot as a fork (T13007): pushed with * `--force` over a head its device had not synced, or after taking another * device's live lease. Zero rows, so the server's count check is unaffected; * an annotation, never a table, so comparisons ignore it. Being on the signed * checkpoint, the label outlives the lease. */ export declare const VAULT_FORK_KEY = "zz_vault_fork"; /** * The {@link VAULT_FORK_KEY} entry of a fork pushed over `overCheckpointId`. * * @param hashKey - The manifest hash key. * @param overCheckpointId - The head the fork was pushed over. * @returns The entry (0 rows; the hash binds it to that head). */ export declare function vaultForkEntry(hashKey: Uint8Array, overCheckpointId: string): VaultTableEntry; /** * The {@link VAULT_FORMAT_KEY} entry of a snapshot hashed under `format`. * * @param hashKey - The manifest hash key. * @param format - The manifest format (default: this CLEO's). * @returns The entry (0 rows; the hash names the format). */ export declare function vaultFormatEntry(hashKey: Uint8Array, format?: number): VaultTableEntry; /** * The manifest format a snapshot's hashes were computed under (T13034): the * version its {@link VAULT_FORMAT_KEY} entry names, or `'unknown'` when the * entry names no format this CLEO knows (a different vault format wrote it, * most likely a newer CLEO). Read as the legacy format 2: * * - no entry: the snapshot predates the record, when vaults stamped their * format as the wire `schemaVersion` (1 or 2), so it reads as 1 for * `schemaVersion` 1 and as 2 otherwise; * - an entry holding the empty-table hash: a build from before the record * carried the parent's entry forward as an emptied table (it only skipped * the fork label), over hashes it computed in format 2. Reading it as * unknown would leave the stream unrestorable and unpushable for every * later CLEO. * * @param manifest - A checkpoint's manifest. * @param hashKey - The manifest hash key. * @returns The format, or `'unknown'`. */ export declare function vaultManifestFormat(manifest: Pick, hashKey: Uint8Array): number | 'unknown'; /** * Whether a manifest key is an annotation (the fork label, the format * record) rather than a table: never compared, counted or carried forward. * * @param key - A manifest key. * @returns `true` for {@link VAULT_FORK_KEY}, {@link VAULT_FORMAT_KEY} and {@link VAULT_AUTHORED_KEY}. */ export declare function isVaultAnnotationKey(key: string): boolean; /** * Whether two `cleo.db` manifests hold the same authored state (T13850): both * carry a {@link VAULT_AUTHORED_KEY} digest and the digests agree, so any * table that differs differs only in what memory reads write. A manifest * without the digest (built by an older CLEO) never agrees. * * @param a - One manifest. * @param b - The other. * @returns `true` when the authored digests agree. */ export declare function sameVaultAuthoredState(a: Pick, b: Pick): boolean; /** * Whether a snapshot manifest carries the fork label. * * @param manifest - A checkpoint's manifest. * @returns `true` when {@link VAULT_FORK_KEY} is present. */ export declare function isVaultForkManifest(manifest: Pick): boolean; /** * Compare two manifests table by table (union of both key sets, sorted). A * table one side does not list and the other lists with 0 rows is the same * (an emptied table stays listed on the cloud side); the annotations * ({@link isVaultAnnotationKey}: fork label, format record) are not tables * and are left out. * * @param local - This side. * @param cloud - The other side. * @returns One line per table. */ export declare function compareVaultManifests(local: Pick, cloud: Pick): VaultTableComparison[]; /** * True when two manifests list the same tables with the same counts and hashes. * * @param a - One manifest. * @param b - The other. * @returns Whether they are equal. */ export declare function sameVaultManifest(a: Pick, b: Pick): boolean; /** Rows this machine held that a restore could not keep. */ export interface VaultLostCredentials { table: string; /** Rows whose credential or machine-local values were dropped. */ rows: number; /** How to re-enter them. */ remedy: string; } /** What {@link carryMachineState} carried into the staged snapshot. */ export interface CarriedMachineState { /** * Tables whose rows now come whole from the live store (`local-only`, or a * `portable-secret` table without a stable key); for a table with file-state * keys, every row but those (T13107). */ preserved: string[]; /** Tables (and their columns) whose non-syncing cells were merged from live rows by a stable key. */ carried: Array<{ table: string; columns: string[]; rows: number; }>; /** Tables left as the snapshot had them (absent here, shaped differently, or without a stable key). */ skipped: string[]; /** Live credentials that could not be kept, with their remedy. */ lost: VaultLostCredentials[]; /** Snapshot-only rows whose foreign local paths were cleared or marked remote, per table. */ scrubbed: Array<{ table: string; rows: number; }>; } /** Options of {@link carryMachineState}. */ export interface CarryMachineStateOptions { /** * The root the snapshot was taken at, when the restore relocates it onto * this machine afterwards (a project store): paths under it are kept, since * the relocation rewrites them. `null` (a global store): no path is relocated. */ snapshotRoot: string | null; /** * Local-only tables whose rows come from the SNAPSHOT, not this machine: a * journal checkpoint carries the merge state of the rows it holds (row * meta, field frontiers; journal spec ยง2.10), which describes those rows * and nothing on this machine (T13312). */ snapshotCarried?: ReadonlySet; } /** * Before a vault restore activates a snapshot, carry this machine's own state * into the staged copy, so a pull never replaces it with another device's * (or with the empty cells an unencrypted bundle carries): * * - `local-only` tables: all rows come from the live store (replica binding, * machine state); a table absent here is emptied. The exception is the * file's own schema state (`SCHEMA_STATE_TABLES`: migration journals, * schema-version sentinels), which stays as the snapshot has it: it * describes the staged file, so the migrator that opens the restored store * reads the journal of that file, not this machine's old one or none (T13104). * A table recording facts about the file's own rows (`SNAPSHOT_STATE_TABLES`: * the bare-table key digests a reconcile recorded) is not carried either: * once the bare tables (local-only) are this machine's, each keeps the * record, the snapshot's or else this machine's, that still matches it, and * loses one neither matches (T13324). * A key/value table mixing both (`FILE_STATE_KEYS`: the row-identity * markers, the sync-schema ledger) is carried key by key: the file's keys * from the snapshot, every other key from the live store; a table only the * live store has is created for this machine's keys when its rule says so * (`_sync_meta`: the `sync.*` flags survive a restore; T13107, T13108). * - `portable-secret` tables: live rows are upserted by a stable key (the * snapshot's copies arrive with their secrets cleared). Without a stable * key the live table is kept whole. * - Every other table: its non-syncing columns ({@link vaultLocalColumns}: * credentials, machine-local paths) are copied from the live row with the * same stable key. A table without one is skipped and reported; rows are * never matched by an integer id, which each machine mints for itself. * `strip` columns ({@link vaultStripColumns}) are not copied: they are NULL * on every row, so this machine recomputes them against the restored data. * - Rows only the snapshot has (a project or skill on another machine only, * a task this machine has not seen) never bring that machine's own values: * every non-syncing cell (claims, leases, counters, paths) is cleared (NULL). * A NOT NULL path outside `snapshotRoot` becomes a * {@link VAULT_REMOTE_PATH_PREFIX} placeholder that is not a path and is * never treated as an orphan; another NOT NULL cell takes its declared * default, or stays when it has none. A path under `snapshotRoot` in a * column the relocation rewrites stays (it becomes this machine's path). * * Syncing cells keep the snapshot's values (they are what the manifest * verified; non-syncing cells hash as NULL, so none of this changes the * manifest). A whole table is carried only when it has the same columns in * both stores; a column merge uses the columns both have. A live credential * that has nowhere to go is reported in `lost` with its re-entry remedy. * * @param stagedDbPath - The staged snapshot database (written in place). * @param liveDbPath - This machine's current database (read only), or `null` when it has none * yet (a project restored onto this machine): every row is then a snapshot-only row. * @param scope - Which `cleo.db` this is. * @param opts - The snapshot's root (see {@link CarryMachineStateOptions}). * @returns What was carried, skipped, lost and scrubbed. */ export declare function carryMachineState(stagedDbPath: string, liveDbPath: string | null, scope: TableScope, opts?: CarryMachineStateOptions): CarriedMachineState; /** * The keyed hash of a table with no rows, as {@link buildVaultManifest} computes it. * * @param hashKey - The manifest hash key. * @param table - Table name. * @returns HMAC-SHA256 hex. */ export declare function emptyVaultTableHash(hashKey: Uint8Array, table: string): string; /** * Table a vault snapshot carries its sealed credential cells in (T13530). It * exists only in a staged snapshot: the export writes it after the credential * columns are cleared, and the restore fills the cleared cells from it and * drops it before the file is placed. Unclassified, so no manifest lists it. */ export declare const VAULT_SEALED_CREDENTIALS_TABLE = "_vault_sealed_credentials"; /** One credential cell a vault snapshot carries sealed under the account's key (T13530). */ export interface SealedCredentialCell { /** Table of the row. */ readonly tbl: string; /** Row uid (the seal's AAD binds it). */ readonly uid: string; /** Credential column. */ readonly col: string; /** The sealed value (base64): ciphertext only, never this machine's key material. */ readonly sealed: string; } /** A row holding credential values, read from a snapshot. */ export interface SnapshotCredentialRow { /** Table of the row. */ readonly table: string; /** Row uid. */ readonly uid: string; /** The whole row (natural key columns included). */ readonly row: Readonly>; } /** * Rows of a snapshot with a uid and a non-NULL value in any of `columns`. * Read-only. * * @param snapshotPath - A staged snapshot. * @param columns - Table to credential columns; absent tables and columns are skipped. * @returns The rows, per table in `columns` order. */ export declare function readSnapshotCredentialRows(snapshotPath: string, columns: Readonly>): SnapshotCredentialRow[]; /** * Write sealed credential cells into a staged snapshot's * {@link VAULT_SEALED_CREDENTIALS_TABLE}. Never a live store: the export * calls it on its own VACUUM copy after the credential columns are cleared. * * @param snapshotPath - A staged snapshot. * @param cells - The sealed cells (nothing is written when empty). */ export declare function writeSealedCredentials(snapshotPath: string, cells: readonly SealedCredentialCell[]): void; /** * The sealed credential cells of a staged snapshot, with the row each belongs * to (empty when the snapshot carries none, or its row is gone). Read-only. * * @param stagedDbPath - A staged snapshot. * @returns Each cell with its row. */ export declare function readSealedCredentials(stagedDbPath: string): Array>; }>; /** * Fill a staged snapshot's credential cells that are still NULL after * {@link carryMachineState} (a value this machine holds always wins), then * drop {@link VAULT_SEALED_CREDENTIALS_TABLE} so it never reaches the placed * store. Captures and side effects are suspended: a restored value is not a * change to journal. * * Cells are matched by row uid. A row this machine holds under another uid * (carried by its natural key, such as an account's provider and label) is * not filled from the sealed cell: its local value wins (T13545). * * @param stagedDbPath - The staged snapshot (never a live store). * @param values - Each cell's value, already encrypted under this machine's key. * @returns The cells filled. */ export declare function fillSnapshotCredentials(stagedDbPath: string, values: ReadonlyArray<{ tbl: string; uid: string; col: string; value: string; }>): number; /** * Drop {@link VAULT_SEALED_CREDENTIALS_TABLE} from a store a bundle import * just placed, when it is there (T13545). The vault restore drops it from its * staged copy itself; any other importer of a global vault bundle relies on * this so the sidecar never stays in a live store. A no-op otherwise. * * @param dbPath - The placed `cleo.db`. * @returns Whether a sidecar was dropped. */ export declare function dropSealedCredentialsSidecar(dbPath: string): boolean; //# sourceMappingURL=vault-manifest.d.ts.map