import { GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT } from "../namespaces/gitvault.crypto.js"; import type { GitvaultHead, GitvaultRecoveryReceipt, GitvaultVaultGenesis } from "../namespaces/gitvault.types.js"; import type { GitvaultEncounteredRotation, GitvaultEpochDecryptFailure, GitvaultRetainedRefsReconcileResult } from "./gitvault-publication.js"; import { GitvaultKeystore, type GitvaultIdentityFile } from "./gitvault-keystore.js"; import { type GitvaultMemberBundleHint, type GitvaultMemberRecoveryBundle } from "./gitvault-member-bundle.js"; import type { GitvaultMirrorBackend } from "./gitvault-mirror-backend.js"; export interface GitvaultChainEntry { generation: string; head: GitvaultHead; head_sha256: string; } export interface GitvaultChainBreak { generation: string; reason: string; } export interface GitvaultDiscoveryResult { repo_id: string; genesis: GitvaultVaultGenesis; genesis_sha256: string; /** How the genesis was authenticated. `unauthenticated_salvage` is a real, first-class outcome (protocol §5.1 vocabulary) — never silently upgraded to "trusted". */ pin_trust: "receipt" | "unauthenticated_salvage"; /** Every fully chain-verified head, genesis excluded, generation 1..N ascending. */ chain: GitvaultChainEntry[]; /** The newest fully-verified generation — `"0000000000000000"` (genesis) when the chain is empty. */ newest_generation: string; newest_head_sha256: string; /** Non-null when the walk stopped because a head FAILED verification (a broken chain, not merely "nothing further is mirrored"). Recovery falls back to `newest_generation` either way — this field is what distinguishes an honest fallback from a quiet one. */ chain_break: GitvaultChainBreak | null; /** * Every admitted `rotate_epoch` transition this KEYLESS chain walk saw, * oldest first (D193, rev 42 — `checkChainLink`/`assertNoTransition` admit * it structurally without ever opening an envelope; recovery's keyed * decrypt step, below, is the only place a rotation's envelope is opened). */ rotations: GitvaultEncounteredRotation[]; } export interface GitvaultDiscoverOptions { backend: GitvaultMirrorBackend; /** An explicit pin, overriding the keystore's stored receipt. */ recovery_receipt?: GitvaultRecoveryReceipt; /** Consulted only when `recovery_receipt` is omitted, keyed by the genesis's OWN `repo_id` (read from the mirror first). */ keystore?: GitvaultKeystore; } /** * List `head/`, pin-check the genesis, and walk `prev_sha256` from genesis to * the newest generation this mirror both HOLDS and can VERIFY. A signature or * chain-link failure at generation K stops the walk at K-1 (`chain_break` * names why); a generation simply absent from the mirror also stops the walk * there, with `chain_break: null` (nothing further was ever pushed here, or * hasn't been synced yet — not evidence of corruption). * * A PIN MISMATCH is the one hard, no-fallback refusal (`VAULT_CREATION_CONFLICT`): * a substituted vault must never be silently accepted at "whatever chain * verifies", because a substituted vault's own chain verifies perfectly * against ITS OWN (different) creator key. */ export declare function discoverAndVerifyChain(options: GitvaultDiscoverOptions): Promise; export interface GitvaultRequiredObject { key: string; object_id: string; kind: "ref_state" | "retention_roots" | "wal_pack" | "checkpoint_claim_set" | "checkpoint_manifest" | "checkpoint_pack"; } export interface GitvaultAbsenceAdjudication { object_id: string; key: string; /** `intentionally_pruned` when a stored `prune_intent` names this object id — never corruption; `unexplained_absence` otherwise — a real loss, named rather than silently skipped. */ adjudication: "intentionally_pruned" | "unexplained_absence"; prune_intent_object_id: string | null; } /** * The required object set for one generation, resolved KEYLESSLY: * `checkpoint_claim_set` is plaintext-structured (never encrypted, protocol * §4.7) precisely so ITS ordered pack receipts enumerate every checkpoint * pack + manifest id without decryption — closure resolution never needs a * key. Walks back from `head` to the nearest checkpoint-bearing ancestor (or * genesis) collecting each head's own carriers + WAL entries along the way. */ export declare function closureForGeneration(backend: GitvaultMirrorBackend, chain: readonly GitvaultChainEntry[], targetGeneration: string): Promise; /** * Check presence of every required object; for each ABSENT one, check the * mirror's OWN stored `prune_intent` objects (plaintext-structured, keyless) * for a delete-set entry naming it. Never a silent skip: every absence is * adjudicated one way or the other, and the caller decides what to do with an * `unexplained_absence` (typically: fall back one generation, see {@link * recoverWithFallback}). */ export declare function adjudicateAbsences(backend: GitvaultMirrorBackend, required: readonly GitvaultRequiredObject[]): Promise; export interface GitvaultRecoveryReport { repo_id: string; genesis_sha256: string; pin_trust: "receipt" | "unauthenticated_salvage"; /** * The generation recovery actually landed on — may be BELOW * `chain_verified_to_generation` when either absence fallback (keyless or * keyed) OR — Request 4, D193-D203 rev 42 — an epoch this keystore cannot * open (`epoch_decrypt_failure`, non-null) capped it first, whichever is * more restrictive. */ recovered_generation: string; /** * The newest generation the PURELY STRUCTURAL chain walk verified, * independent of any key material — this is `discoverAndVerifyChain`'s * own `newest_generation`, restated here under fsck's own vocabulary so a * caller can see, honestly, how far signature verification reached versus * how far `recovered_generation` actually materialized. */ chain_verified_to_generation: string; /** Non-null iff the chain walk itself broke (see {@link GitvaultDiscoveryResult.chain_break}). */ chain_break: GitvaultChainBreak | null; /** * Non-null iff an admitted `rotate_epoch` transition capped * `recovered_generation` below `chain_verified_to_generation` because this * keystore holds no envelope for the new epoch (or the envelope is absent/ * altered on this mirror) — Request 4's named epoch boundary, never a bare * generic absence. Always `null` on {@link GitvaultVerifyReport} (mode * `keyless_verify` never opens a key). */ epoch_decrypt_failure: GitvaultEpochDecryptFailure | null; /** Every absence encountered while resolving closures, across every generation tried, oldest attempt first. */ absences: GitvaultAbsenceAdjudication[]; /** True iff any absence was `unexplained_absence` — the loud, un-missable flag; never buried in a nested field. */ data_loss_detected: boolean; validity_not_freshness: typeof GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT; keystore_still_required: typeof GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT; } export interface GitvaultVerifyReport extends GitvaultRecoveryReport { mode: "keyless_verify"; /** Object kinds present + confirmed reachable for the recovered generation, counted (never bytes, never plaintext). */ inventory: Record; /** * Member recovery-bundle sidecars found under `member-recovery-bundles/` in * this prefix — UNVERIFIED availability hints (gitvault-recovery-custody): * nothing about them is authenticated by the chain; they only tell a human * holder "a bundle travels with this mirror, so bundle + source recovery * code can recover it with no server." Empty when the prefix carries none. */ member_recovery_bundles: GitvaultMemberBundleHint[]; } /** * Discovery + chain verification + closure/absence adjudication, WITHOUT * touching any key material — `run402 repos fsck --mirror`. Reports the * recoverable generation and an inventory; never decrypts, never * materializes. A genuinely keyless integrity probe (design D5) — useful as a * CI check that never needs a secret. */ export declare function verifyGitvaultMirror(backend: GitvaultMirrorBackend, options?: { recovery_receipt?: GitvaultRecoveryReceipt; keystore?: GitvaultKeystore; }): Promise; export interface GitvaultRecoverResult extends GitvaultRecoveryReport { mode: "recovered"; out_dir: string; refs: Record; head_target: { kind: "symref"; ref: string; } | { kind: "detached"; oid: string; }; /** * Non-null iff this recovery decrypted via the human-member path (a member * recovery bundle opened with the source recovery code) rather than a * keystore — names WHICH bundle sidecar (`bundle_key`, null when supplied * directly), which wrapper opened, and the rp_id the context was built * with. Faithful: a keystore recovery and a bundle recovery produce the * same repository, but the caller can always tell which happened. */ member_recovery: { bundle_key: string | null; wrapper_id: string; rp_id_used: string; ek_fingerprint: string; } | null; /** * clone-installs-retained-refs (D2): the same `refs/r402/retain/` * bookkeeping `restoreObjectsInto` (clone/fetch) and `fsck` install, applied * here so a recovered bare repo's `git fsck` is silent too — a disaster * drill should never see dangling-commit noise for tips this recovery * itself just proved are retained. D3: a bookkeeping failure degrades to a * `warning` on this field; recovery itself never fails on it. */ retained_refs: GitvaultRetainedRefsReconcileResult; /** * `out_dir` is a BARE repository (`git init --bare`) — objects/refs/HEAD * directly in `out_dir`, no working files. This is deliberate and matches * every other on-disk gitvault layout; it is named explicitly here (rather * than left for the reader to discover via `ls`) because a bare directory * with no working tree reads as a failed or empty recovery otherwise * (dogfood item 3). See `next_actions` for how to get working files. */ layout: "bare"; /** `git clone -worktree` — materializes working files from the bare recovery above. */ next_actions: { action: string; command: string; }[]; } export interface GitvaultRecoverOptions { backend: GitvaultMirrorBackend; out_dir: string; recovery_receipt?: GitvaultRecoveryReceipt; keystore?: GitvaultKeystore; /** * gitvault-recovery-custody — the human-member path: a member recovery * bundle (exported wrapper ciphertexts + key identity) opened with * `source_recovery_code` substitutes for the keystore's encryption * identity. When `source_recovery_code` is supplied WITHOUT a bundle, the * mirror's own `member-recovery-bundles/` sidecars are tried; none found * is the truthful `RECOVERY_BUNDLE_MISSING` refusal (a server-side * wrapper row that was never exported is not an offline backup). The * recovery-receipt pin is still required for trusted recovery — key * material never substitutes for the trust anchor. */ member_bundle?: GitvaultMemberRecoveryBundle; source_recovery_code?: string; /** Seal-time ceremony host for the wrapper context; default = bundle's own `rp_id`, then `console.run402.com`. */ rp_id?: string; } /** The two identity fields recovery actually decrypts with — a keystore's `identity.json` or a member bundle opened by the source recovery code both project onto this. */ export type GitvaultRecoveryIdentity = Pick; /** * Materialize the resolved generation into `out_dir` and run §4.7 restorer * acceptance: scratch restore, every covered ref at its EXACT oid, `git fsck` * full connectivity, recompute the three keyed commitments against the * checkpoint manifest when one covers this generation. Acceptance failure is * a typed non-zero exit (throws) — never a partial, silently-accepted repo. */ export declare function recoverGitvaultMirror(options: GitvaultRecoverOptions): Promise; //# sourceMappingURL=gitvault-recover.d.ts.map