/** * gitvault — prune (protocol rev 40 §7.3; task 5.12a). * * The two-phase, ACYCLIC prune the client half owns: * * 1. plan — walk the verified chain, compute the GC ROOT SET (what must * never be deleted) and the pruneable universe, and subtract. * 2. attest — TWO `verifier_receipt`s over the SAME `intent_core_sha256`, * one per CLOSED implementation identity * (`run402-cli` | `r402s-verify`). This SDK is `run402-cli` * and produces exactly ONE of them; the other MUST come from * `r402s-verify`, which by spec shares no implementation code * with the SDK — differential verification is the whole point, * so this module will never synthesize the second receipt. * 3. submit — sign the core, wrap it, sign the wrapper, and POST the * EXACT BYTES. The gateway reads this route through * `express.raw` and verifies the owner signature over the * bytes as sent, so a re-serialized object is a different * object. {@link pruneIntentBytes} is the one serializer. * 4. confirm — poll the intent until its control-plane-signed * `prune_completion` appears, then believe ONLY that: per-object * outcomes are THREE-VALUED (`deleted` | `present_not_attempted` * | `present_after_attempt`) and only `deleted` means the bytes * are gone. `present_after_attempt` is NOT a deletion. * * What this module deliberately does NOT do: * - decide retention eligibility. The ≥90-day schedule is measured against a * control-plane-signed `retention_cutoff` ticket and the server's own * admission times; a client cannot prove either. The plan is a PROPOSAL and * the gateway is the authority — it refuses any candidate that is not * retention-eligible against the bound ticket. * - attest anything it did not check. A receipt this module builds carries * `result: "failed"` (and both booleans as observed) whenever the chain * cannot prove the property, rather than a convenient `true`. */ import { GITVAULT_FORMAT, GITVAULT_SUITE } from "../namespaces/gitvault.crypto.js"; import type { GitvaultCheckpointClaimSet, GitvaultCheckpointManifestReceipt, GitvaultCheckpointPackReceipt, GitvaultCheckpointClaimSetReceipt, GitvaultHead, GitvaultRefStateReceipt, GitvaultRetentionRootsReceipt, GitvaultWalPackReceipt } from "../namespaces/gitvault.types.js"; /** §7.3: at most 10 000 candidates per intent; larger prunes chunk into successive intents. */ export declare const GITVAULT_MAX_PRUNE_CANDIDATES = 10000; /** The CLOSED V0 verifier identity set (`common.json#/$defs/implementation_id`). */ export declare const GITVAULT_VERIFIER_IMPLEMENTATIONS: readonly ["run402-cli", "r402s-verify"]; export type GitvaultVerifierImplementation = (typeof GITVAULT_VERIFIER_IMPLEMENTATIONS)[number]; /** This SDK's identity when it attests. The other half of the pair is never ours to produce. */ export declare const GITVAULT_SDK_VERIFIER_IMPLEMENTATION: GitvaultVerifierImplementation; /** * What this lineage calls itself in a receipt's `implementation_version`. * * The LINEAGE and the PROTOCOL REVISION, not the npm version: a receipt records * which verifier produced it against which frozen wire, and the npm number * moves for reasons that have nothing to do with either. Bump this when the * verification logic or the revision it verifies changes. */ export declare const GITVAULT_SDK_VERIFIER_VERSION = "run402-sdk/r402s-v0-rev40"; /** `common.json#/$defs/receipt_pruneable` — the CLOSED union a delete set may name. */ export type GitvaultPruneableReceipt = GitvaultWalPackReceipt | GitvaultRefStateReceipt | GitvaultRetentionRootsReceipt | GitvaultCheckpointManifestReceipt | GitvaultCheckpointPackReceipt | GitvaultCheckpointClaimSetReceipt; /** One verified chain link plus its resolved checkpoint claim set (null when the head bears no checkpoint). */ export interface GitvaultChainEntry { head: GitvaultHead; head_sha256: string; claim_set: GitvaultCheckpointClaimSet | null; } /** `common.json#/$defs/verifier_receipt_ref` — what the intent carries, not the receipt itself. */ export interface GitvaultVerifierReceiptRef { object_id: string; object_kind: "verifier_receipt"; stored_bytes_sha256: string; size_bytes: string; implementation_id: GitvaultVerifierImplementation; } /** `verifier_receipt.json` — owner-signed, uploaded, then claimed at the intent's fence. */ export interface GitvaultVerifierReceipt { format: typeof GITVAULT_FORMAT; object_kind: "verifier_receipt"; suite: typeof GITVAULT_SUITE; repo_id: string; object_id: string; intent_core_sha256: string; checkpoint_head_sha256: string; cutoff_ticket_sha256: string | null; restored_object_set_hmac: string; retention_evolution_ok: boolean; candidates_outside_roots_ok: boolean; implementation_id: GitvaultVerifierImplementation; implementation_version: string; result: "restored_and_verified" | "failed"; signature: string; } /** `prune_intent_core.json` — owner-signed; the acyclic base BOTH receipts sign. */ export interface GitvaultPruneIntentCore { format: typeof GITVAULT_FORMAT; object_kind: "prune_intent_core"; suite: typeof GITVAULT_SUITE; repo_id: string; object_id: string; gc_epoch: string; maintenance_cycle_id: string | null; maintenance_prune_role: "intermediate" | "final" | null; stage_claim_set_sha256: string | null; batch_index: string | null; batch_count: string | null; authorizing_head_sha256: string; checkpoint_claim_set_sha256: string; gc_root_set_hmac: string; retention_state_hmac: string; delete_set: GitvaultPruneableReceipt[]; nonce: string; signature: string; } /** `prune_intent.json` — the wrapper the gateway publishes create-only under `prune/`. */ export interface GitvaultPruneIntent { format: typeof GITVAULT_FORMAT; object_kind: "prune_intent"; suite: typeof GITVAULT_SUITE; repo_id: string; object_id: string; core: GitvaultPruneIntentCore; intent_core_sha256: string; verifier_receipts: GitvaultVerifierReceiptRef[]; signature: string; } /** One entry of the completion's three-valued vector. */ export interface GitvaultPruneOutcome { object_id: string; result: "deleted" | "present_not_attempted" | "present_after_attempt"; } /** `GET …/prune-intents/:id` — the gateway's secret-free public view (`toPublicIntent`). */ export interface GitvaultPruneIntentRecord { object_id: string; repo_id: string; state: string; gc_epoch: string; intent_sha256: string | null; intent_core_sha256: string | null; candidate_count: number; next_candidate_index: number; maintenance_cycle_id: string | null; maintenance_prune_role: string | null; stage_claim_set_sha256: string | null; batch_index: number | null; batch_count: number | null; completion: { object_id: string | null; sha256: string | null; per_object: GitvaultPruneOutcome[] | null; deleted_count: number; present_after_attempt_count: number; present_not_attempted_count: number; gc_epoch_at_completion: string; cycle_event_seq: string | null; completed_at: string | null; } | null; prepared_at: string | null; intent_put_issued_at: string | null; intent_stored_at: string | null; deleting_started_at: string | null; } /** * A checkpoint's FULL RECOVERY BUNDLE: the claim set, the manifest, every pack, * AND its `covers_through` head's `ref_state` + `retention_roots` carriers. * A bundle missing its carriers is not a recovery path, so the carriers are * part of the protection, not adjacent to it. */ export declare function checkpointRecoveryBundle(entry: GitvaultChainEntry): GitvaultPruneableReceipt[]; /** Every pruneable object the chain has ever named, sorted by `object_id`, deduplicated. */ export declare function collectPruneUniverse(entries: readonly GitvaultChainEntry[]): GitvaultPruneableReceipt[]; /** Pairwise-distinct, sorted by `object_id` — the schema's canonical order, not an incidental one. */ export declare function sortPruneReceipts(receipts: readonly GitvaultPruneableReceipt[]): GitvaultPruneableReceipt[]; export interface GitvaultGcRootSet { /** The protected receipts, sorted — the `gcrootset` commitment's preimage content. */ receipts: GitvaultPruneableReceipt[]; /** The newest checkpoint-bearing entry, or `null` when the chain has none. */ latest_checkpoint: GitvaultChainEntry | null; /** The one before it, or `null`. */ prior_checkpoint: GitvaultChainEntry | null; /** * Why nothing may be pruned yet, or `null` when a prune is structurally * possible. Stated rather than left to be inferred from an empty candidate * list, which would read as "already clean". */ blocked_reason: string | null; } /** * The GC root set: the latest checkpoint's recovery bundle + its WAL suffix + * the immediately-prior checkpoint's recovery bundle + every carrier at or * after the prior checkpoint. * * DELIBERATELY conservative in two places, because under-protecting deletes * history and over-protecting only costs storage: * * - with FEWER THAN TWO checkpoints nothing is prunable at all. The * protection rule exists so a bad latest checkpoint still has a second * recovery path; with a single checkpoint that second path IS the * pre-checkpoint WAL chain, so pruning it would leave exactly one. * - every `ref_state` / `retention_roots` carrier at or after the PRIOR * checkpoint is protected, not just the unexpired-root carriers. Root * expiry is measured against a server-signed ticket this client cannot * evaluate, so it never guesses a carrier is dead. */ export declare function computeGcRootSet(entries: readonly GitvaultChainEntry[]): GitvaultGcRootSet; export interface GitvaultPrunePlan { /** Candidates in canonical order, capped at {@link GITVAULT_MAX_PRUNE_CANDIDATES}. */ candidates: GitvaultPruneableReceipt[]; /** How many pruneable objects were left out by the per-intent cap (chunk into a later intent). */ deferred_count: number; root_set: GitvaultGcRootSet; universe_count: number; } /** universe − GC root set, canonical order, capped. Never mutates its inputs. */ export declare function planPruneCandidates(entries: readonly GitvaultChainEntry[]): GitvaultPrunePlan; export interface GitvaultRetentionEvolutionCheck { ok: boolean; /** Every departure this client could not justify from the chain alone. */ unproven: Array<{ generation: string; ref: string; oid: string; reason: string; }>; } /** * Verify that every retention root which LEFT the map left legally: only at a * checkpoint-bearing generation carrying a cutoff ticket, and only when its * `effective_admitted_at + retentionDays < cutoff_at` strictly. * * `effectiveAdmittedAt` returning `null` is not a failure of the vault — it is * a failure of THIS CLIENT to prove the departure, and the check reports `ok: * false` so the receipt attests `false` rather than a convenient `true`. */ export declare function checkRetentionEvolution(entries: readonly GitvaultChainEntry[], roots: (entry: GitvaultChainEntry) => Array<{ ref: string; oid: string; dropped_at_generation: string; }>, effectiveAdmittedAt: (droppedAtGeneration: string) => string | null, isEligible: (effectiveAdmittedAtIso: string, cutoffAtIso: string) => boolean): GitvaultRetentionEvolutionCheck; export interface GitvaultBuildPruneCoreInput { repo_id: string; gc_epoch: string; authorizing_head_sha256: string; checkpoint_claim_set_sha256: string; gc_root_set_hmac: string; retention_state_hmac: string; delete_set: readonly GitvaultPruneableReceipt[]; /** All five cycle fields are non-null together iff the prune advances a maintenance cycle (§7.2). */ cycle?: { maintenance_cycle_id: string; maintenance_prune_role: "intermediate" | "final"; stage_claim_set_sha256: string; batch_index: string; batch_count: string; } | null; object_id?: string; nonce?: string; } /** * Build + sign `prune_intent_core`. The core is signed on its own AND embedded * in a signed wrapper (the double signature of §7.3) — `intent_core_sha256` is * the stored-bytes hash of the COMPLETE signed core, which is why the core must * be finished before either verifier can attest anything about it. */ export declare function buildPruneIntentCore(input: GitvaultBuildPruneCoreInput, signingSeed: Uint8Array): GitvaultPruneIntentCore; /** The stored-bytes hash of the complete signed core — what BOTH verifier receipts sign. */ export declare function pruneIntentCoreSha256(core: GitvaultPruneIntentCore): string; export interface GitvaultBuildVerifierReceiptInput { repo_id: string; intent_core_sha256: string; checkpoint_head_sha256: string; cutoff_ticket_sha256: string | null; restored_object_set_hmac: string; retention_evolution_ok: boolean; candidates_outside_roots_ok: boolean; implementation_id: GitvaultVerifierImplementation; implementation_version: string; object_id?: string; } /** * Build + sign a `verifier_receipt`. * * `result` is DERIVED, never passed in: a receipt is `restored_and_verified` * exactly when both attestation booleans are true, and `failed` otherwise. A * caller cannot hand this function `failed` booleans and a passing result. */ export declare function buildVerifierReceipt(input: GitvaultBuildVerifierReceiptInput, signingSeed: Uint8Array): GitvaultVerifierReceipt; /** The `verifier_receipt_ref` an intent carries for an uploaded receipt. */ export declare function verifierReceiptRef(receipt: GitvaultVerifierReceipt): GitvaultVerifierReceiptRef; /** * Wrap + sign the intent. * * Every gate the gateway applies to the pair is applied here first, so a * malformed pair never reaches the wire as a wasted round trip: exactly two * refs, one per CLOSED implementation identity, distinct ids AND distinct * hashes, each attesting `restored_and_verified` with both booleans true, and * each signing THIS core. */ export declare function buildPruneIntent(core: GitvaultPruneIntentCore, receipts: readonly GitvaultVerifierReceipt[], signingSeed: Uint8Array): GitvaultPruneIntent; /** * Refuse a supplied core that the chain has moved past — BEFORE the receipts * are uploaded. * * A prune is planned, verified out-of-band by a second implementation, and only * then submitted, so there is a real window in which a push or a compaction * lands underneath it. The gateway would refuse such an intent at its fence, * but by then both receipts are stored objects the fence CLAIMS, so a stale * submission burns evidence for nothing. Same registry codes the gateway uses, * raised one step earlier. */ export declare function assertPruneCoreStillCurrent(core: GitvaultPruneIntentCore, current: { repo_id: string; gc_epoch: string; checkpoint_claim_set_sha256: string; }): void; /** * THE serializer for the submit route. * * `POST …/prune-intents` is parsed with `express.raw`: the service strict-parses * these bytes and verifies the owner signature over them. Anything that * re-serializes the object between here and the socket — a JSON body helper, a * proxy that reformats — changes what was signed. Send exactly this. */ export declare function pruneIntentBytes(intent: GitvaultPruneIntent): Uint8Array; export interface GitvaultPruneConfirmation { /** `null` until the control-plane-signed completion exists. */ outcome: "completed" | "superseded_no_delete" | "superseded_partial_delete" | null; /** Object ids the completion confirms are GONE. Only `deleted` qualifies. */ deleted: string[]; /** Still present — attempted or not. Never reported as removed. */ present: GitvaultPruneOutcome[]; /** Candidates the completion did not adjudicate at all (a defect; reported, never assumed deleted). */ unadjudicated: string[]; } /** * Read the completion's three-valued vector against the intent's canonical * candidate order. * * The one rule this function exists to enforce: **only `deleted` means gone.** * `present_after_attempt` looks like a deletion in the logs and is not one — * treating it as one is how a client reports history removed that is still * there. A candidate with no entry is `unadjudicated`, never inferred. */ export declare function summarizePruneCompletion(candidateIds: readonly string[], record: GitvaultPruneIntentRecord | null): GitvaultPruneConfirmation; /** Run `fn` in a fresh temp directory and always remove it, even on refusal. */ export declare function withScratchDir(prefix: string, fn: (dir: string) => Promise): Promise; //# sourceMappingURL=gitvault-prune.d.ts.map