/** * gitvault — publication (protocol rev 41 §6 + §4.4–4.7 + §5A client side; * task 5.4). * * What lives here: * - the PURE evaluators: ref transactions (§6.1 — force-with-lease only, * fast-forward for non-force branch updates, immutable tags, deletes need * expected-old, pairwise-distinct refs refused BEFORE evaluation, the * §6.5 cardinality bounds), retention-root evolution (§4.5 — map keyed * `(ref, oid)`, renewal, expiry ONLY at checkpoints against the pre-signing * cutoff ticket, strict `effective + 90d < cutoff_at`), the heads-listing * page contract (§6.3 / D186 — anchor echo, cursor echo, has_more/ * next_cursor coupling, strictly-above-anchor, gapless across pages, * truthful total), chain linkage (`CHAIN_BROKEN`), and the transition * fail-closed rule (`UPGRADE_REQUIRED`); * - {@link GitvaultTransport} — the control-plane + bucket operations the * vault needs (extends task 5.3's creation transport) — and * {@link createGitvaultHttpTransport}, the `fetch`-backed implementation * over the SDK kernel (upload sessions → presigned PUT with * `If-None-Match: *` → finalize); * - {@link GitvaultVault} — discover/verify to newest with the RESUMABLE * verification budget, materialize (decrypt ref_state + retention_roots → * the materialized pin), push (pack_set or a checkpoint-bearing head when * the delta exceeds the 64-receipt budget, upload with receipt-compare, * admission with 409 re-apply-and-retry, head read-back before any pin * advances), checkpoint build + acceptance self-check, and repair (the * mandatory fresh checkpoint + the exact repair-root algebra). * * Dual pins (§6.4) live in the keystore repo file: `head_pin` is * `highest_authenticated`, `materialized_pin` is `highest_materialized` — the * ONLY push base. A regression below the authenticated pin is * `GENERATION_REGRESSION`; an authenticated-but-undecryptable head is * `CHAIN_UNUSABLE` (read-only at the materialized pin); an admitted non-null * transition is `UPGRADE_REQUIRED` (read-only past it, no publish). */ import type { Client } from "../kernel.js"; import { type GitvaultMirrorBackend } from "./gitvault-mirror-backend.js"; import type { GitvaultMirrorCredential, GitvaultMirrorDestination } from "./gitvault-mirror-config.js"; import { type GitvaultCheckpointStaleness } from "../namespaces/gitvault.js"; import type { GitvaultActivationToken, GitvaultCaptureBinding, GitvaultCaptureReceipt, GitvaultCheckpointBlock, GitvaultCheckpointClaimSet, GitvaultCheckpointManifest, GitvaultDigestLabel, GitvaultHead, GitvaultHeadTarget, GitvaultHeadsListingPage, GitvaultHeadsListingRequest, GitvaultOpenReceipt, GitvaultRecipientConfirmationReceipt, GitvaultRefState, GitvaultRefTransaction, GitvaultRefUpdate, GitvaultRepairDescriptor, GitvaultRetentionCutoff, GitvaultRetentionCutoffReceipt, GitvaultRetentionRoot, GitvaultRetentionRoots, GitvaultRetentionRootsReceipt, GitvaultRotateEpochPayload, GitvaultRotationAttemptDescriptor, GitvaultRotationReason, GitvaultVaultGenesis } from "../namespaces/gitvault.types.js"; import type { GitvaultCreationTransport } from "./gitvault-creation-journal.js"; import { GitvaultKeystore, type GitvaultRepoFile } from "./gitvault-keystore.js"; import type { GitvaultPruneIntentRecord } from "./gitvault-prune.js"; export declare const GITVAULT_MAX_CANONICAL_REFS = 10000; export declare const GITVAULT_MAX_REF_UPDATES_PER_TRANSACTION = 1000; export declare const GITVAULT_MAX_RETENTION_ROOT_ENTRIES = 50000; export declare const GITVAULT_MAX_REPAIR_ADDED_ROOTS = 10000; export declare const GITVAULT_MAX_WAL_RECEIPTS_PER_HEAD = 64; export declare const GITVAULT_MAX_CHECKPOINT_PACKS = 4096; export declare const GITVAULT_MAX_CHECKPOINT_TOTAL_STORED_BYTES = 858993459200n; export declare const GITVAULT_MULTI_OBJECT_PACK_TARGET_BYTES = 201326592; export declare const GITVAULT_MAX_REF_STATE_OBJECT_BYTES = 33554432; export declare const GITVAULT_MAX_HEADS_PER_LISTING_PAGE = 1000; export declare const GITVAULT_VERIFICATION_BUDGET_HEADS = 100000; export declare const GITVAULT_RETENTION_MIN_DAYS = 90; /** Default admission-conflict retries before the push gives up (each retry re-verifies + re-applies to the winner). */ export declare const GITVAULT_PUSH_CONFLICT_RETRIES = 5; /** * `run402@/` — the audit-provenance string named on * `self_open_attestation.reader_entrypoint` (D209) and * `recipient_open_receipt.reader_entrypoint` (D210): "names the client * implementation + entry point that produced the evidence." Never an * authorization input; no wire grammar is promised for it (protocol-v0.md * §4.14, `rotate_epoch_payload.json`'s own field `$comment`). */ export declare function gitvaultReaderEntrypoint(entrypoint: string): string; export declare function generationToBigInt(generation: string): bigint; export declare function bigIntToGeneration(value: bigint): string; export declare function nextGeneration(generation: string): string; export type GitvaultRefMap = Record; /** A tip that left the canonical map in this transaction — it enters `retention_roots` in the same generation. */ export interface GitvaultDroppedTip { ref: string; oid: string; reason: "deleted" | "force_displaced"; } export interface GitvaultRefTransactionEvaluation { refs: GitvaultRefMap; dropped: GitvaultDroppedTip[]; } export interface GitvaultEvaluateRefTransactionOptions { /** Ancestry oracle: true iff `ancestor` is reachable from `descendant`. */ isAncestor: (ancestor: string, descendant: string) => Promise | boolean; /** `refuse` (default — user pushes): any `refs/run402/*` update is refused; `allow`: the protocol's own deploy-ref move. */ protocol_refs?: "refuse" | "allow"; } /** One failing update of a refused transaction (never a silent revert). */ export interface GitvaultRefUpdateFailure { ref: string; reason: "expected_old_mismatch" | "non_fast_forward" | "tag_immutable" | "delete_requires_expected_old" | "noop"; expected_old_oid: string | null; current_oid: string | null; } /** * Evaluate a §6.1 transaction against the materialized map. Refusals in * order: pairwise-distinct refs (before evaluation), grammar, the update cap, * then per-update semantics collected into ONE `REF_EXPECTED_OLD_MISMATCH` * (every failing update listed), then the resulting-state cardinality. */ export declare function evaluateRefTransaction(current: GitvaultRefMap, transaction: GitvaultRefTransaction, options: GitvaultEvaluateRefTransactionOptions): Promise; /** §6.5 bound: ≤ 10 000 canonical refs and the serialized map ≤ 32 MiB. */ export declare function assertRefMapCardinality(refs: GitvaultRefMap): void; /** The §4.4 deploy-ref move: force-with-lease `refs/run402/deploys/latest` → `oid` (creation when absent). */ export declare function deployRefTransaction(current: GitvaultRefMap, oid: string): GitvaultRefTransaction; /** `effective_admitted_at = max(prepared_at, storage creation time of the winning admission record)` (§4.10). */ export declare function effectiveAdmittedAt(preparedAt: string, recordStorageCreatedAt: string): string; /** A root may be removed iff `effective_admitted_at + 90 days < cutoff_at` (STRICT; §4.5a). */ export declare function isRootEligibleForRemoval(effectiveAdmittedAtIso: string, cutoffAtIso: string, retentionDays?: number): boolean; export interface GitvaultEvolveRootsOptions { /** The generation being built (`g+1`) — stamped as `dropped_at_generation` on new/renewed keys. */ generation: string; /** Tips dropped or force-displaced by this generation's transaction. */ dropped: Array<{ ref: string; oid: string; }>; /** * Present ONLY when this generation carries a checkpoint bound to a cutoff * ticket: expiry is evaluated against `cutoff_at` using the resolver for each * root's drop generation. Removal is PERMISSIVE — a resolver returning * `null` keeps the root. */ checkpoint_cutoff?: { cutoff_at: string; effectiveAdmittedAt: (droppedAtGeneration: string) => string | null; }; } export declare function compareRoots(a: GitvaultRetentionRoot, b: GitvaultRetentionRoot): number; /** roots(g+1) = roots(g) ∪ dropped (RENEWING an existing `(ref, oid)` key) ∖ {expired, only at a checkpoint with a ticket}. */ export declare function evolveRetentionRoots(previous: GitvaultRetentionRoot[], options: GitvaultEvolveRootsOptions): GitvaultRetentionRoot[]; /** * The client-local, protocol-owned namespace (design D4). Distinct from the * VAULT-side `refs/run402/*` (e.g. `GITVAULT_DEPLOY_REF`) that rides the wire * as part of `ref_state` — `refs/r402/*` never rides the wire at all; it is * written directly into the local `.git` by the materializer and reconciled * on every later fetch/fsck. A push naming any ref under it is refused by the * remote helper before a transaction is ever built (see * `git-remote-run402.mjs`'s `partitionProtectedRefPushes`) — this constant is * the ONE place the namespace string is spelled, shared by both sides. */ export declare const GITVAULT_R402_REF_NAMESPACE = "refs/r402/"; /** Where a retained (branch-unreachable) deploy-capture tip gets its local ref (design D1/D2). */ export declare const GITVAULT_RETAIN_REF_PREFIX = "refs/r402/retain/"; /** * D1's ref-identity choice, recorded here because the design doc's own * assumption did not hold: `GitvaultRetentionRoot` carries no per-capture * stable id (only `{ref, oid, dropped_at_generation}`) — the capture id that * DOES exist (`GitvaultHead.capture_binding.capture_id`) lives on the head * that INTRODUCED a tip onto a canonical ref, not on the retention-root entry * recording its later displacement, and correlating the two would require * walking the chain further back than materialization already reads (D2 * forbids new reads here). The commit oid itself is already in hand, is * content-addressed (so it is exactly as stable as a capture id — neither * ever changes for the same history), and needs no correlation at all — the * ref name IS the tip's own identity. */ export declare function gitvaultRetainedRefName(oid: string): string; export interface GitvaultRetainedRefsReconcileResult { /** `refs/r402/retain/` refs created or moved this call. */ written: string[]; /** `refs/r402/retain/*` refs removed this call — a root the vault no longer retains. */ deleted: string[]; /** How many distinct retained, branch-unreachable, locally-present tips this call found. */ retained_count: number; /** * Non-null on ANY bookkeeping failure (D3) — permissions, an exotic * filesystem, a git invocation error. The caller (fetch/fsck) turns this * into exactly one stderr note and otherwise proceeds unchanged: a clone, * fetch, or fsck NEVER fails because this could not be written. */ warning: string | null; } /** * D2: install/remove local `refs/r402/retain/` refs so every retained * (branch-unreachable) tip the vault's materialized retention roots name is * locally referenced — the git-ecosystem `refs/pull/*` precedent, so a fresh * `git fsck` is silent and `git for-each-ref refs/r402/` names what is * retained and why (D6). * * Skips a root tip already reachable from a canonical ref (`state.refs`) or * the HEAD target when detached — no redundant refs (D2). Reconciliation is * namespace-scoped: only `refs/r402/retain/*` is ever read, written, or * deleted; nothing else is touched, even when other bookkeeping under * `refs/r402/*` exists. * * D3 — warn, never fail: driven from a SINGLE try/catch around the whole * operation (list existing → compute the desired set → one atomic * `update-ref --stdin` transaction for every create/update/delete). Any * failure anywhere in that sequence returns a `warning` string and touches * nothing further; it never throws, so a clone/fetch/fsck calling this can * never fail on it. Called only when `repoDir` is an actual git repository — * `repos fsck` addresses a vault by `repo_id`/`project_id` alone as often as * by a local checkout, and "no local repo here" is a normal, silent no-op, * never a warning. */ /** * Name prefix of the scratch directory {@link GitvaultVault.buildPacks} * allocates INSIDE the repository's git common dir. Dotted and clearly named * so it can never be mistaken for `objects/`, `refs/`, or a pack directory. */ export declare const GITVAULT_PACK_SCRATCH_PREFIX = ".run402-gitvault-packs-"; /** * Allocate the pack-objects scratch directory on the OBJECT STORE's own * filesystem: `/.run402-gitvault-packs-XXXXXX`, never under * `os.tmpdir()`. * * `git pack-objects ` writes its temporary pack under * `/objects/pack/` and then `rename(2)`s it onto * `-.pack`. When `/tmp` is a different filesystem (tmpfs in a * cloud container) that rename fails INSIDE git with EXDEV ("unable to rename * temporary file … Invalid cross-device link") and no JS-side fallback can * catch it — so the base path has to share the object store's filesystem. * Git ignores unknown siblings of `objects/` and `refs/` (fsck, gc, and * object enumeration never walk them), so a leftover from a crash is inert; * the caller still removes it on every path. */ export declare function allocatePackScratchDir(repoDir: string): Promise; export declare function reconcileRetainedTipRefs(repoDir: string, state: { refs: GitvaultRefMap; roots: readonly GitvaultRetentionRoot[]; head_target: GitvaultHeadTarget; }): Promise; export interface GitvaultListingProgress { /** The anchor (constant across the sequence). */ after_generation: string; /** The highest generation delivered so far (== anchor before page 1). */ last_generation: string; /** Generations delivered so far (for the truthful-total check). */ delivered: number; } /** Validate a listing request before it is sent (the request schema, D186). */ export declare function validateHeadsListingRequest(request: GitvaultHeadsListingRequest): void; /** * Validate one page against the request and the sequence so far. Returns the * advanced progress. Refusals: anchor not echoed / wrong vault / coupling * violation / retired member → `GITVAULT_LISTING_PAGE_INVALID`; an entry at or * below the anchor → `GENERATION_REGRESSION`; a gap within or across pages → * `CHAIN_BROKEN`; an untruthful final `total` → `CHAIN_BROKEN`. */ export declare function verifyHeadsListingPage(page: GitvaultHeadsListingPage, request: GitvaultHeadsListingRequest, progress: GitvaultListingProgress, expectedRepoId?: string): GitvaultListingProgress; /** * The continuation request for `page`, or `null` when the sequence is complete. * The anchor stays CONSTANT and `next_cursor` is echoed UNCHANGED — the cursor * is stored and echoed, never parsed or edited (a client that re-anchors or * edits a byte earns `INVALID_CURSOR` from the platform, D186). */ export declare function nextListingRequest(request: GitvaultHeadsListingRequest, page: GitvaultHeadsListingPage): GitvaultHeadsListingRequest | null; /** * §6.4: the vault's newest generation may never fall BELOW the authenticated * pin. A listing (or a storage read) that says otherwise is a rollback, not a * quiet vault — `GENERATION_REGRESSION`, no publish. */ export declare function checkGenerationRegression(listedNewestGeneration: string, pinnedGeneration: string): void; /** The parameters a maintenance open requests, and that the C1 record must echo. */ export interface GitvaultOpenBindingRecord { base_head_sha256: string; prior_checkpoint_claim_set_sha256: string | null; r2_cap_size_bytes: string; } /** `SHA-256("r402s/v0/open-binding" ‖ lp(client_open_id) ‖ lp(base_head) ‖ lp_opt(prior) ‖ lp(cap))`. */ export declare function openBindingDigest(clientOpenId: string, record: GitvaultOpenBindingRecord): string; /** * Recompute the binding from the record's OWN fields and compare it bytewise * with the signed issuance digest. The error registry has no dedicated code for * the fence inequality (D145), so the client surfaces * `GITVAULT_OPEN_BINDING_MISMATCH`; a same-`client_open_id` retry that carries a * DIFFERENT binding is the registry's `CLIENT_OPEN_ID_CONFLICT`. */ export declare function checkOpenBinding(clientOpenId: string, record: GitvaultOpenBindingRecord, issuanceOpenBindingSha256: string, options?: { retry?: boolean; }): void; export interface GitvaultChainLinkInput { head: GitvaultHead; stored_bytes: Uint8Array; /** The listing's hash for this generation. */ listed_sha256: string; expected_generation: string; prev_sha256: string; repo_id: string; /** The registered writer public key (V0: the genesis creator key). */ writer_public_key: Uint8Array | string; writer_key_id: string; /** * The predecessor's own `epoch` (D194, rev 42) — `GITVAULT_GENESIS_EPOCH` * for generation 1's predecessor (genesis). Drives the D193 epoch- * continuity check below; pure and keyless (no envelope is ever opened * here — only the head's own `epoch` FIELD is checked). */ prev_epoch: string; } /** Verify one link: bytes hash to the listing, strict parse, generation, prev linkage, epoch pin, repo, writer signature. */ export declare function checkChainLink(input: GitvaultChainLinkInput): void; /** * The transition fail-closed rule: a V0 client that encounters an ADMITTED * transition kind it cannot validate stops advancing — read-only at the * materialized pin, no publish past it, `UPGRADE_REQUIRED`. Unknown kinds * are a parse reject. * * `rotate_epoch` is EXEMPT from this rule as of rev 42 (D193: "epoch * rotation is ACTIVATED") — `checkChainLink`'s own D194 epoch-continuity * check already validates its structural admissibility, and * {@link parseRotateEpochPayload} / the caller's own envelope-open step * (`GitvaultVault.verifyToNewest`) handle it fully. `add_writer_key` is * EXEMPT as of rev 47 (gitvault-multi-writer) the same way — {@link * parseAddWriterKeyPayload} + `validateAddWriterKeyPayload` * (`gitvault-writer-state.js`) handle it fully, INSIDE `verifyToNewest`'s * own loop, BEFORE this function ever runs on that head (so a transition * that fails writer validation never reaches here at all — this function * only ever sees ones that already passed). The other two kinds * (`add_envelope`, `transfer_binding`) remain genuinely unactivated and stay * fail-closed exactly as before. */ export declare function assertNoTransition(head: GitvaultHead): void; /** One object to upload — identity fixed BEFORE the PUT; the receipt is compared against it. */ export interface GitvaultUploadObject { /** * Storage path relative to the vault root (§3). CLIENT-LOCAL addressing only: * the control plane derives the bucket key from `object_kind` + the ledger * identity and REFUSES a manifest entry carrying an unexpected member, so * this never rides the wire (5.6c). */ path: string; object_kind: string; /** `null` for path-addressed kinds (envelopes) — those ride `epoch` + `recipient_fingerprint`. */ object_id: string | null; bytes: Uint8Array; /** SHA-256 of `bytes` (ciphertext hash for frames, stored-bytes hash for plaintext kinds). */ sha256: string; size_bytes: string; /** `wal_pack` only — §4.1: the ONLY receipt kind carrying `base_generation`. */ base_generation?: string; } export interface GitvaultUploadReceipt { path: string; object_id: string | null; sha256: string; size_bytes: string; } /** * The control plane addresses stored objects by LEDGER IDENTITY, never by * bucket path: heads and admission records have their own generation-addressed * routes, and every uploadable kind is named by `object_kind` + `object_id` * (or, for envelopes, `epoch` + `recipient_fingerprint`). This resolver maps * the SDK's internal `gitvaultPaths` strings onto that identity so the rest of * the vault can keep addressing objects the way §3 describes them. */ export type GitvaultWireRef = { kind: "head"; generation: string; } | { kind: "admission"; generation: string; } | { kind: "object"; read: GitvaultObjectReadRequest; }; /** One entry of a `POST …/object-reads` batch. */ export interface GitvaultObjectReadRequest { object_kind: string; object_id?: string; epoch?: string; recipient_fingerprint?: string; /** D195, rev 42 — present only for a rotation-attempt `key_envelope` read. */ rotation_id?: string; /** D197, rev 42 — present only for a `recipient_pin_manifest` read. */ pin_manifest_version?: string; } /** `null` for a path with no wire identity (e.g. a locally-held cutoff ticket). */ export declare function gitvaultWireRefForPath(path: string): GitvaultWireRef | null; /** The manifest entry for one upload — closed-key, exactly what the control plane validates. */ export declare function gitvaultManifestEntry(object: GitvaultUploadObject): GitvaultObjectReadRequest & { sha256: string; size_bytes: string; base_generation?: string; }; /** Mirrors the gateway's `GITVAULT_INLINE_UPLOAD_MAX_OBJECT_BYTES` (`services/gitvault/upload-sessions.ts`). */ export declare const GITVAULT_INLINE_UPLOAD_MAX_OBJECT_BYTES = 262144; /** Mirrors the gateway's `GITVAULT_INLINE_UPLOAD_MAX_REQUEST_BYTES`. */ export declare const GITVAULT_INLINE_UPLOAD_MAX_REQUEST_BYTES = 1048576; /** * The client-side mirror of the gateway's `isInlineUploadRequest` + per-object/ * per-request cap check: every object must fit under the PER-OBJECT cap AND * the batch's total under the PER-REQUEST cap, or the whole batch takes the * presigned session+PUT+finalize shape — no per-object mixing, matching the * server's `VALIDATION_FAILED` refusal on a mixed request. An empty batch is * never "inline" (nothing to send either way; `upload()`'s own early return * already short-circuits before this is consulted, and `putObject` always * wraps exactly one object so it never hits this branch). * * Takes the narrowest shape that satisfies every call site (`GitvaultUploadObject[]` * for `uploadObjects`, a single `{bytes}`-shaped array for `putObject`) so * neither caller needs to fabricate unrelated fields just to ask the * question. */ export declare function gitvaultInlineUploadEligible(objects: readonly { bytes: Uint8Array; }[]): boolean; /** * The stable key both sides agree on, used to pair receipts back to * requests — MIRRORS the gateway's `keyEnvelopeLedgerId`/`pinManifestLedgerId` * (services/gitvault/epoch-rotation.ts) exactly; drift here breaks receipt * pairing at upload finalize for a rotation-attempt envelope or a pin * manifest. */ export declare function gitvaultLedgerId(read: GitvaultObjectReadRequest): string; export interface GitvaultAdmitHeadRequest { repo_id: string; generation: string; stored_bytes: Uint8Array; stored_bytes_sha256: string; } export type GitvaultAdmitHeadResult = { outcome: "admitted"; admission_record_sha256: string; capture_receipt: GitvaultCaptureReceipt | null; } | { outcome: "conflict"; winner: { generation: string; stored_bytes_sha256: string; }; }; export interface GitvaultRetentionCutoffIssued { ticket: GitvaultRetentionCutoff; receipt: GitvaultRetentionCutoffReceipt; } /** * Everything the vault needs from the control plane + bucket. Extends the * creation transport so one implementation serves 5.3–5.6. All methods are * idempotent from the state machines' point of view. */ /** The `resource_binding` an upload session is charged against (§7.2 / §9.3). */ export type GitvaultResourceBinding = { kind: "ordinary_push"; } | { kind: "maintenance_cycle"; maintenance_lease_id: string; } | { kind: "repair_attempt"; repair_attempt_id: string; }; /** `POST …/maintenance-leases` — the owner's compact/prune reservation (§7.2). */ export interface GitvaultMaintenanceLeaseRequest { repo_id: string; base_head_sha256: string; current_checkpoint_hash?: string | null; r1_size_bytes: string; r2_cap_size_bytes: string; p_before_c1_size_bytes?: string; p_before_c2_size_bytes?: string; } export interface GitvaultMaintenanceLease { maintenance_lease_id: string; repo_id: string; base_head_sha256: string; current_checkpoint_hash: string | null; reservation_size_bytes: string; maintenance_headroom_bytes: string; /** Returned ONCE — the liveness instrument (heartbeat / release). Never logged, never cached. */ holder_token: string; expires_at: string | null; hard_deadline_at: string | null; } /** `POST …/compaction-grant`'s result (gitvault-checkpoint-cadence design D3). */ export interface GitvaultCompactionGrant { /** * The vault's server-measured `source_bytes` at grant time, capped — never * client-declared. NOTE the byte fields arrive as STRINGS on the wire (the * gateway serializes Postgres BIGINTs as strings, verified live) — consumers * MUST `Number(...)` before arithmetic; `Number.isFinite` on the raw value * is false and silently discards the grant. */ granted_bytes: number | string; expires_at: string; /** The org's pooled storage already in use, at grant time. */ pool_used_bytes: number | string; /** The org's plain tier storage limit (unraised). */ pool_limit_bytes: number | string; /** `pool_limit_bytes` + this grant's `granted_bytes` — the limit the preflight arithmetic should use while this grant is active. */ effective_pool_limit_bytes: number | string; } export interface GitvaultTransport extends GitvaultCreationTransport { /** * Read N independent carrier objects (ref_state, retention_roots, WAL/ * checkpoint packs — anything `object-reads`-addressed, never a * generation-addressed head/admission) in ONE presigned batch (gitvault- * client-round-trips design D2): one `object-reads` POST naming every * path, then the resulting GETs issued with bounded concurrency. Order in * the result array matches `paths`; a missing object is `null` at its * index, the same "absent" reading {@link GitvaultCreationTransport.getObject} * gives for one object. Callers that need exactly one object still use * {@link GitvaultCreationTransport.getObject} — this is for the plural * case only, so a single-object caller never pays a batch's overhead. * * `expected` (gitvault-small-object-inline design D3), when supplied, is * INDEX-ALIGNED with `paths`: `expected[i]`, if present, is the sha256 hex * the caller will itself check `paths[i]`'s bytes against. An * `object-reads`-backed implementation MAY use it to verify a * gateway-supplied `inline` reply before trusting it, falling back to * that slot's ordinary fetch on a mismatch — client-internal plumbing, * never a new verification obligation (every real caller already * hash-checks its bytes before use) and never required: an absent array, * or an absent element within it, is byte-identical to before that * change. */ getObjects(request: { repo_id: string; paths: string[]; expected?: Array; }): Promise>; /** * OPTIONAL per-object settlement over the SAME batch shape as * {@link getObjects} (gitvault-pipelined-restore D2): one presign POST, * the same bounded-concurrency GETs — counted ops identical — but the * result is one promise PER path, each settling when its own object's * bytes land, so a consumer can decrypt/verify/apply object i while later * objects are still downloading. Order and absence semantics match * `getObjects` (index-aligned; `null` for absent), including `expected` * (gitvault-small-object-inline design D3 — see {@link getObjects}'s doc * comment). Every returned promise is pre-marked handled, so an abandoned * tail after a mid-batch failure never surfaces as an unhandled * rejection. A transport without this method degrades to the * `getObjects` barrier — pipelining is a wall-clock property, never a * correctness dependency. */ getObjectsSettled?(request: { repo_id: string; paths: string[]; expected?: Array; }): Promise>>; /** * Read the EXACT stored bytes of many generation-addressed heads in ONE * POST (`…/head-reads`, gitvault-batched-head-reads). * * Heads are the one hot read that cannot ride {@link getObjects}: that * batch is carrier-only by wire design and fails closed on a * generation-addressed path, so a cold chain walk otherwise pays ~G/6 * sequenced waves of full round trips for bytes that are ~1.2 KB each. * * `generations` must be STRICTLY ASCENDING; the route is all-or-nothing, so * the result is either one `Uint8Array` per requested generation in request * order, or `null` meaning UNSUPPORTED — an older gateway, a refusal, a * network fault, anything. `null` is never "absent bytes": it is the * caller's signal to fall back to per-generation reads, which produce the * per-item nulls and the real failure envelopes. Bytes are raw and * UNTRUSTED exactly as a per-generation read's are — this batch changes * transport, never trust. */ getHeads(request: { repo_id: string; generations: string[]; }): Promise; listHeads(request: GitvaultHeadsListingRequest & { repo_id: string; }): Promise; /** * Session → create-only presigned PUTs (`If-None-Match: *`) → finalize; * receipts in request order. * * gitvault-byo-primary-bucket task 3.2 — `byo`, when present, marks this * vault as `storage_profile: "byo"` and names where the CLIENT itself * writes payload-kind objects directly (never through run402's own * bucket): the inline-upload fast path is skipped entirely (a BYO vault * refuses inline bytes-in-body — the transport shape itself would route * payload bytes through run402), every session object with `put: null` * is written by THIS caller straight to `byo.destination` with * `byo.credential` (resolved at use time, never transmitted), and * finalize carries the resulting per-object attestations. Omitted (the * default) is byte-identical to today. */ uploadObjects(request: { repo_id: string; objects: GitvaultUploadObject[]; resource_binding?: GitvaultResourceBinding; byo?: { destination: GitvaultMirrorDestination; credential?: GitvaultMirrorCredential; }; }): Promise; admitHead(request: GitvaultAdmitHeadRequest): Promise; requestRetentionCutoff(request: { repo_id: string; base_head_sha256: string; }): Promise; exchangeActivationToken(request: { repo_id: string; operation_id: string; capture_receipt: GitvaultCaptureReceipt; }): Promise; submitOverrideCompletion(request: { repo_id: string; operation_id: string; capture_receipt: GitvaultCaptureReceipt; }): Promise<{ cleared: boolean; }>; /** The vault record — policy, allocation generation, storage + maintenance state (§9.2). */ getVaultRecord(request: { repo_id: string; }): Promise; /** * `GET …/state` (gitvault-composite-state-read design D1) — the pin-current * fast path: the vault record, the newest generation, its head's exact * stored bytes, and both carriers (`ref_state`/`retention_roots`) resolved * to raw bytes here (inline decoded, or fetched from the presigned URL — * both arms are indistinguishable to the caller after this returns, and * NEITHER is verified by this call). `head`/`carriers` are `null` together * for a freshly allocated vault with no admitted generation yet (mirrors * `newest_generation: null`); a carrier can independently be `null` when * its stored bytes are absent (the same "absent" reading {@link * GitvaultCreationTransport.getObject} gives for one object — the caller * treats it identically, never a route-level distinction). * * {@link GitvaultVault} verifies every field here EXACTLY as it does * walking the paginated listing — chain link from the caller's own pin, * carrier hashes against the head's own embedded receipts, and the writer * signature. This route bundles bytes; it never becomes the verifier. A * caller more than one generation behind its own pin ignores `head`/ * `carriers` and falls back to {@link listHeads} — that decision lives in * the vault, never here. * * `since` (gitvault-delta-fetch): the caller's materialized generation. * A gateway that recognizes it MAY answer with a bounded `delta` * (intermediate heads + their WAL packs, inline under caps) — and MAY * ignore it entirely (older gateway, disqualified span); absence of * `delta` is never an error. Delta bytes are UNTRUSTED exactly like every * other stored byte: consumers hash-check before use, and a failed check * is a plain miss that the ordinary reads absorb. * * `restore` (gitvault-restore-recipe design D2), ORTHOGONAL to `since` — * declares restore intent. A gateway that recognizes it AND can locate a * checkpoint boundary within its own bound MAY answer with `restore_plan` * (the heads from that boundary to newest, the boundary checkpoint's * claim set + manifest, and every pack the span references) — and MAY * ignore it entirely (older gateway, no locatable boundary, a transition * inside the span); absence of `restore_plan` is never an error. Plan * bytes are UNTRUSTED exactly like `delta`'s: {@link GitvaultVault} * re-derives the full backward chain-link, claim-set signature, * cross-equality, per-pack receipt-hash, and AEAD-open obligations before * using anything here, and falls back to the ordinary backward walk on * any failure. */ getState(request: { repo_id: string; since?: string; restore?: boolean; }): Promise; /** Resolve a project's vault without local state (the cold-restart entry point). */ findVaultByProject(request: { project_id: string; }): Promise; /** * Resolve a vault by its address-form `org-slug/name` (repo-first-onramp * task 4.3, design D6) — `GET /gitvault/v1/vaults?repo=/`. * `RESOURCE_NOT_FOUND` for no such org OR no such name within it (the two * collapse deliberately, so slug-namespace probing learns nothing extra); * `SLUG_RELEASED` (with `successor_slug`/`released_at`/`cooldown_until` on * the error) while the slug is in its post-rename cooldown — never * auto-followed. */ findVaultByRepo(request: { org_slug: string; repo_name: string; }): Promise; acquireMaintenanceLease(request: GitvaultMaintenanceLeaseRequest): Promise; heartbeatMaintenanceLease(request: { repo_id: string; maintenance_lease_id: string; holder_token: string; }): Promise<{ maintenance_lease_id: string; expires_at: string | null; }>; releaseMaintenanceLease(request: { repo_id: string; maintenance_lease_id: string; holder_token: string; }): Promise<{ maintenance_lease_id: string; status: string; }>; /** * `POST …/compaction-grant` (gitvault-checkpoint-cadence design D3) — a * short-lived, TTL'd, at-most-one-per-project headroom grant that raises * the org's EFFECTIVE pooled storage limit by at most this vault's * server-measured `source_bytes`, so compaction's own transient ~2x * overshoot (the new checkpoint coexisting with the not-yet-pruned * history) can land without the routine manual override * (`--force-headroom`). Rejects `GITVAULT_COMPACTION_GRANT_ACTIVE` (409) * when this project already holds an active grant — the caller reads * that as "another compaction is already in flight for this vault" and * skips its own cycle rather than racing it. An older gateway 404s/ * `ROUTE_NOT_FOUND`s; the caller falls back to compacting without a * grant, exactly as before this route existed. */ openCompactionGrant(request: { repo_id: string; }): Promise; /** * `DELETE …/compaction-grant` — idempotent; `{closed: false}` when * nothing was active (already closed, already expired, or never * opened). Always safe to call best-effort in a `finally`: closing an * absent grant is a no-op, never an error. */ closeCompactionGrant(request: { repo_id: string; }): Promise<{ closed: boolean; }>; /** * `POST …/prune-intents` — the intent's EXACT BYTES (§7.3). * * The route is parsed with `express.raw` and the owner signature is verified * over the bytes as sent, so the transport MUST NOT re-serialize: it puts * `intent_bytes` on the wire verbatim under `Content-Type: application/json`. * An implementation that accepts a parsed object here and stringifies it is * signing one thing and sending another. */ submitPruneIntent(request: { repo_id: string; intent_bytes: Uint8Array; }): Promise; /** `GET …/prune-intents/:id` — the intent's state and, once signed, its completion. */ getPruneIntent(request: { repo_id: string; prune_intent_object_id: string; }): Promise; /** * The org's directory of envelope-capable principals (gitvault-human- * envelopes design D7, `GET /orgs/v1/:org_id/encryption-keys`) — every * active human member who has published an encryption key. Read by * {@link GitvaultVault.reconcileEnvelopeRecipients} to diff against a * vault's current recipient set. */ listOrgEncryptionKeys(request: { org_id: string; }): Promise; /** * The `ek_` fingerprints already covering this vault, at any epoch * (`GET /gitvault/v1/vaults/:vault_id/envelope-recipients`, task 2.2) — * fingerprints only, never envelope bytes; the recipient-only rule on * envelope BYTES elsewhere is unaffected. */ listEnvelopeRecipients(request: { repo_id: string; }): Promise; /** * `POST …/rotation-attempts` (D195) — the FIRST write of any rotation * attempt, BEFORE any `key_envelope` upload for it. `descriptor` is the * COMPLETE, signed `rotation_attempt_descriptor`; the gateway re-derives * `rotation_id` from the stored bytes and returns it alongside the * (possibly-idempotent-replayed) descriptor. */ createRotationAttempt(request: { repo_id: string; descriptor: GitvaultRotationAttemptDescriptor; }): Promise<{ rotation_id: string; descriptor: GitvaultRotationAttemptDescriptor; deduplicated: boolean; }>; /** `POST …/recipients/:principal_id/confirm` (D197) — first-seen pin confirmation; owner + step-up. */ confirmRecipient(request: { repo_id: string; principal_id: string; new_fingerprint: string; }): Promise; /** `POST …/recipients/:principal_id/repin` (D197) — re-pin ceremony; owner + step-up. */ repinRecipient(request: { repo_id: string; principal_id: string; old_ek_fingerprint: string; new_fingerprint: string; }): Promise; /** `POST …/recipients/:principal_id/key-revocation` (D199) — declares `reason:"recipient_key_revoked"` admissible; owner + step-up. Returns the D194 counters this rotation must be fenced against — the ONE reason value with a client-visible counter read. */ declareRecipientKeyRevoked(request: { repo_id: string; principal_id: string; }): Promise<{ recipient_state_version: string; recipient_revocation_version: string; }>; /** `POST …/epoch-secret-exposure` (D199) — declares `reason:"epoch_secret_exposed"` admissible, VAULT-scoped; owner + step-up. */ declareEpochSecretExposed(request: { repo_id: string; }): Promise<{ epoch_secret_exposure_version: string; }>; /** `POST …/writer-authority/declare-unavailable` (D202) — an explicit, audited fact that the writer signing key is gone; owner + step-up. */ declareWriterAuthorityUnavailable(request: { repo_id: string; }): Promise<{ declared_at: string; declared_by: string | null; }>; /** * `POST …/recipients/:principal_id/proof-of-open` (D210, rev 44, §9.2) — * submit fsck's OWN `chain_verified_to_generation` / * `decryptable_to_generation` evidence VERBATIM, plus the `ek_fingerprint` * of the envelope this identity actually opened. Self-match only: the * gateway requires `principal_id` to equal the AUTHENTICATED caller, * never overridable — a mismatch is the ordinary 403 * `GITVAULT_ACCESS_DENIED`, not a `proof-of-open`-specific error. * Idempotent on `(repo_id, principal_id, ek_fingerprint, * decryptable_to_generation)`: `deduplicated: true` means the gateway * returned the tuple's EXISTING receipt (HTTP `200`) rather than minting * a fresh one (HTTP `201`) — both are the tuple's one committed winner, * never a partial/pretend re-insert. */ submitOpenProof(request: { repo_id: string; principal_id: string; ek_fingerprint: string; chain_verified_to_generation: string; decryptable_to_generation: string; reader_entrypoint: string; }): Promise<{ receipt: GitvaultOpenReceipt; deduplicated: boolean; }>; } /** * One row of the org's envelope-capable-principal directory * ({@link GitvaultTransport.listOrgEncryptionKeys}). * * The gateway route (`GET /orgs/v1/:org_id/encryption-keys`, `routes/org.ts` * in run402-private) returns `public_key` on every row — the raw key * material is what makes the directory usable for wrapping at all. * The field stays OPTIONAL in this wire type the * same way `desired[]` does on {@link GitvaultEnvelopeRecipientsResponse}: * a response is network data, and an older/rolling-deploy gateway that * omits the field must degrade to a per-entry `skipped` report * (`missing_public_key`) from {@link GitvaultVault.reconcileEnvelopeRecipients}, * never a hardcoded assumption or a thrown error. */ export interface GitvaultOrgEncryptionKeyEntry { principal_id: string; display_name: string | null; ek_fingerprint: string; suite: string; created_at: string; /** Raw base64url X25519 public key. Present on every current-gateway row; tolerated as absent (per-entry `missing_public_key` skip) for wire robustness only. */ public_key?: string; /** * gitvault-multi-writer (rev 47) D9 — the SAME row's vault-WRITER signing * half (`routes/org.ts` in run402-private, deployed alongside `public_key` * D9). Raw base64url Ed25519 public key; `null` when this principal has * never published a signing half (a pre-rev47 or encryption-only * enrollment), absent entirely on an older gateway that predates D9. */ signing_pubkey?: string | null; signing_fingerprint?: string | null; signing_possession_verified_at?: string | null; [key: string]: unknown; } /** `GET /orgs/v1/:org_id/encryption-keys` — {@link GitvaultTransport.listOrgEncryptionKeys}'s result. */ export interface GitvaultOrgEncryptionKeyDirectory { org_id: string; keys: GitvaultOrgEncryptionKeyEntry[]; } /** * One row of the vault's org-level, membership-driven DESIRED-recipient * state. `status` is the server's own honest accounting of what membership * currently wants, NOT a claim about whether access was actually revoked: * `"pending_removal"` means membership removed this principal but gitvault * protocol v0 has no epoch-rotation mechanism yet to un-wrap their existing * `key_envelope`, so `covered: true` on a `pending_removal` row is a REAL * continuing-access fact, not stale data. */ export interface GitvaultDesiredRecipientEntry { principal_id: string; display_name: string | null; status: "active" | "pending_removal"; /** `null` when this principal is desired but has not published an encryption key yet. */ ek_fingerprint: string | null; public_key: string | null; suite: string | null; /** `true` when `ek_fingerprint` currently has a `key_envelope` on this vault. */ covered: boolean; } /** `GET /gitvault/v1/vaults/:vault_id/envelope-recipients` — {@link GitvaultTransport.listEnvelopeRecipients}'s result. */ export interface GitvaultEnvelopeRecipientsResponse { vault_id: string; recipient_fingerprints: string[]; /** * The org's desired-recipient state, cross-referenced against this * vault's coverage — see {@link GitvaultDesiredRecipientEntry}. OPTIONAL: * absent (not empty) on an older gateway that predates this field — * callers MUST distinguish "field absent" (older gateway, state genuinely * unknown) from "field present and empty" (org has no desired recipients) * rather than treating both as "no data." */ desired?: GitvaultDesiredRecipientEntry[]; /** The desired-recipient substrate's own monotonic version, for cheap client-side diffing. Present iff `desired` is present. */ desired_state_version?: number; /** * D194's two org-level rotation counters, read (never locked) alongside * `desired` so a writer can drive the writer-capable * `reason:"member_removed"` rotation from the same read it partitions H * from ({@link GitvaultVault.rotateEpochForMemberRemoval}). OPTIONAL: * absent on a gateway that predates them. */ recipient_state_version?: string; recipient_revocation_version?: string; } /** `GET /gitvault/v1/vaults/:vault_id` — the shape `reads.ts:getVaultRecord` returns. */ export interface GitvaultVaultRecord { repo_id: string; project_id: string; org_id: string; gitvault_policy: "required" | "grandfathered" | null; gitvault_policy_version: string; gitvault_policy_changed_at: string | null; allocation_generation: string; allocation_sha256: string | null; newest_generation: string | null; genesis_admitted_at: string | null; latest_effective_admitted_at: string | null; admitted_generations: string; gc_epoch: string; repair_version: string; repair_fence_state: string; /** * gitvault-byo-primary-bucket task 3.1/3.5 (protocol-v0.md rev 46 §9.2, * D220). Absent-or-`"managed"` is BYTE-IDENTICAL to every vault allocated * before this fold. `byo_destination` is the destination's ADDRESS ONLY * (never credential material) and is non-null iff `storage_profile === * "byo"`. Chosen at allocation only in v1 — no route flips it on an * existing vault. */ storage_profile?: "managed" | "byo"; byo_destination?: string | null; storage: { source_bytes: string; open_session_reserved_bytes: string; objects: Record; }; maintenance: { lease: { maintenance_lease_id: string; base_head_sha256: string; reservation_size_bytes: string; expires_at: string | null; hard_deadline_at: string | null; } | null; open_cycle: { maintenance_cycle_id: string; state: string; last_cycle_progress_at: string | null; } | null; pending_repair_attempt_id: string | null; }; /** * gitvault-multi-writer (rev 47) task 3.7 — the writer chain state's read * surface, mirrored client-side from the gateway's `VaultRecord` (see * `services/gitvault/reads.ts` in run402-private for the authoritative doc * comment). `writer_set` is the on-chain-recognized set exactly as the * chain itself would verify (regardless of any gateway-only block — that's * `ineligible_members`). `pending_writers` is the reverse direction: * active org members at role developer+ with a published, * possession-verified signing key who are NOT yet in `writer_set.writers` * — candidates for the "writer"-door `add_writer_key` (task 5.7's * `reconcile()`). `ineligible_members` is a chain-recognized active writer * the gateway has flagged for removal, still active on-chain pending the * next `rotate_epoch{writer_set_update}`. */ writer_set?: { version: string; sha256: string | null; writers: GitvaultVaultWriterSetEntry[]; }; pending_writers?: GitvaultVaultPendingWriter[]; ineligible_members?: GitvaultVaultIneligibleMember[]; /** `gitvault_vaults.read_only_terminal_at IS NOT NULL` — the D228 forced sole-writer-removal terminal state: no further writer-authenticated push is admissible. Absent (never `undefined`-checked as `true`) on an older gateway — treat as `false`. */ read_only_terminal?: boolean; /** * gitvault-multi-writer (rev 47) D6/D227, task 5.9 — decimal-string * uint64 counter, bumped by the gateway every time a membership/role/key * change gateway-blocks one or more writer keys. A client building a * `writer_set_update` freezes THIS value into `rotation_attempt_ * descriptor.writer_revocation_version` at its own admission fence — * there is no writer-side "declare" round-trip (unlike the encryption * side's `declareRecipientKeyRevoked`), so this read is the only source. * Absent on an older gateway that predates this field. */ writer_revocation_version?: string; warnings: { kind: string; message: string; }[]; created_at: string | null; /** * The control plane's SIGNED allocation record for the vault (present on * gateways that wrap it into the vault read; `null`/absent otherwise). * gitvault-agent-envelopes D4: a cold open compares genesis's creator * fingerprints against these — platform-attested consistency, never * independent authentication (the platform serves both sides). */ allocation?: { creator_signing_fingerprint: string; creator_encryption_fingerprint: string; status?: string; service_key_id?: string; [key: string]: unknown; } | null; } /** One chain-recognized active writer on {@link GitvaultVaultRecord.writer_set} — the on-chain shape only; `gateway_blocked_at` (gateway-only) rides {@link GitvaultVaultIneligibleMember} instead. */ export interface GitvaultVaultWriterSetEntry { writer_key_id: string; principal_id: string; authorization_kind: "writer" | "handoff"; admitted_generation: string; admitted_head_sha256: string; } /** An org member eligible to become a writer (active membership at role developer+, a published possession-verified signing key) who is NOT yet in {@link GitvaultVaultRecord.writer_set}'s `writers` — a candidate for the "writer"-door `add_writer_key` ({@link GitvaultVault.reconcileWriterAdmissions}). */ export interface GitvaultVaultPendingWriter { principal_id: string; writer_key_id: string; } /** A chain-recognized active writer the gateway has flagged for removal — still active on-chain, pending the next `rotate_epoch{writer_set_update}`. */ export interface GitvaultVaultIneligibleMember { principal_id: string; writer_key_id: string; gateway_blocked_at: string | null; reason: "membership_revoked" | "role_below_developer" | "encryption_key_revoked" | "gateway_blocked_pending_removal"; } /** * {@link GitvaultTransport.getState}'s result — the SAME bytes the per-object * routes serve, resolved to raw `Uint8Array` here (never verified here; see * that method's own doc comment). `head`/`carriers` are `null` together for * a freshly allocated vault (mirrors `newest_generation: null`); a `carriers` * field can independently be `null` when that carrier's stored bytes are * absent from the backing store. */ export interface GitvaultVaultState { vault: GitvaultVaultRecord; newest_generation: string | null; head: { stored_bytes: Uint8Array; stored_bytes_sha256: string; } | null; carriers: { ref_state: Uint8Array | null; retention_roots: Uint8Array | null; } | null; /** Present only when the gateway answered a `since` request with a qualifying span (gitvault-delta-fetch); absent otherwise, including on every older gateway. */ delta?: GitvaultVaultStateDelta | null; /** Present only when the caller declared `restore: true` AND the gateway located a qualifying checkpoint boundary (gitvault-restore-recipe); absent otherwise, including on every older gateway. */ restore_plan?: GitvaultVaultRestorePlan | null; } /** * The state read's restore recipe (gitvault-restore-recipe design D1-D5): * the heads from the newest checkpoint boundary through the caller's own * newest generation (ascending, boundary-first — ONE entry when the * boundary IS the newest head), that boundary's checkpoint claim set + * manifest (`null` exactly when `boundary_generation` is the genesis * sentinel — no checkpoint exists in the whole bounded chain), and every * checkpoint + WAL pack the span references. Heads and checkpoint bytes are * pure base64url decodes (the gateway sends them inline ALWAYS — design * D4); `packs[].bytes` is ALREADY resolved here exactly as `object-reads` * resolves its own entries (db8d745c's `resolveObjectReadTarget` — a * self-consistency-gated `inline` decode, falling back to the entry's own * `url`/`edge_url` on a lying, absent, or over-cap `inline`), `null` when * neither arm produced bytes (an absent object, a failed GET). UNTRUSTED * THROUGHOUT: nothing here is verified by this decode — {@link * GitvaultVault} re-derives the full chain-link/signature/cross-equality/ * receipt-hash/AEAD-open obligations before consuming anything, matching * one for one by `(object_kind, object_id)`, and falls back to the ordinary * backward walk byte-identically on any failure. */ export interface GitvaultVaultRestorePlan { boundary_generation: string; heads: Array<{ generation: string; stored_bytes: Uint8Array; stored_bytes_sha256: string; }>; checkpoint: { claim_set: { object_id: string; stored_bytes: Uint8Array; }; manifest: { object_id: string; stored_bytes: Uint8Array; }; } | null; packs: Array<{ object_kind: string; object_id: string; bytes: Uint8Array | null; }>; } /** * The state read's bounded delta (gitvault-delta-fetch): the span's heads in * chain order plus their WAL packs' INLINE bytes. Over-cap packs arrive as * presigned references on the wire and are DROPPED here (v1 consumes inline * only — an uncovered pack simply rides the ordinary batched fetch, so the * reference arm costs nothing to ignore). Untrusted throughout: every * consumer hash-checks before use. */ export interface GitvaultVaultStateDelta { heads: Array<{ generation: string; stored_bytes: Uint8Array; stored_bytes_sha256: string; }>; packs: Array<{ object_id: string; bytes: Uint8Array; }>; } export interface GitvaultHttpTransportOptions { /** Wire shape: every vault-scoped route is `/gitvault/v1/vaults/:vault_id/...`; `vault_id` is the `repo_id` unless a mapping is supplied (D185). */ vaultIdFor?: (repoId: string) => string; /** * gitvault-byo-primary-bucket: where THIS machine reads a BYO vault's * payload from. The gateway holds no payload copy of a `storage_profile: * "byo"` vault, so its `object-reads` entries and `GET …/state` carriers * for the payload kinds carry `byo_key` (the object's relative key under * the vault's `byo_destination`) instead of a presign; the transport then * asks this resolver for the vault's locally configured destination * backend (`byo/.json` in the keystore — destination + credential * NAME, never material) and reads the key from it. `null` means this * machine has no such config, and the read fails * `GITVAULT_BYO_NOT_CONFIGURED` by name — never a silent "absent". Absent * option: same as `null`. Managed vaults never consult it. */ byoBackend?: (repoId: string) => GitvaultMirrorBackend | null; /** * gitvault-object-host-predial (design D1, task 1.2): called with the * ORIGIN(s) (`scheme://host`) of an object-store URL this transport just * completed a round trip against — the presigned `url`'s origin, and the * `edge_url`'s origin too when the target carried one, regardless of * which one actually served the bytes (both are worth a future predial; * see `gitvault-prewarm.ts`). Fired ONLY after a completed fetch (any * status, including 404 — a real "absent" response still proves the * origin is reachable); NEVER fired for an `inline`-satisfied read * (nothing was dialed) or a fetch that threw. This is the ONE place the * transport observes origins — the caller (who holds the keystore) is * expected to persist them via `GitvaultKeystore.recordObjectStoreOrigins`; * the transport itself has no keystore and does no persistence. Called * synchronously and never awaited — a throwing callback must never * surface into the read it rode along with, so callers wrap their own * persistence in their own try/catch (this transport does not). */ onObjectStoreOriginObserved?: (repoId: string, origins: string[]) => void; } /** * Independent reads/PUTs within one gitvault step run at this concurrency * (design D2) — "browser-era origin etiquette", well within what S3/the * gateway tolerate, and it keeps in-flight memory bounded by ~6×frame size. * Chain-ordered steps (the head-chain walk, admission, readback) never call * this — they stay strictly sequential. */ export declare const GITVAULT_TRANSPORT_CONCURRENCY = 6; /** * Run `fn` over `items` with at most `limit` in flight at once, preserving * result order regardless of completion order. A plain worker-pool: `limit` * workers each pull the next unclaimed index until the queue is empty. */ export declare function mapBounded(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise): Promise; /** * The `fetch`-backed transport over the SDK kernel. Presigned PUTs carry * `If-None-Match: *` (create-only — the bucket policy demands it) and the * FULL_OBJECT SHA-256 checksum header. */ export declare function createGitvaultHttpTransport(client: Client, options?: GitvaultHttpTransportOptions): GitvaultTransport; export declare const gitvaultPaths: { readonly head: (generation: string) => string; readonly admission: (generation: string) => string; readonly wal: (id: string) => string; readonly refState: (id: string) => string; readonly retentionRoots: (id: string) => string; readonly checkpointManifest: (id: string) => string; readonly checkpointPack: (id: string) => string; readonly claimSet: (id: string) => string; readonly cutoffTicket: (id: string) => string; /** `verifier-receipts/.json` — plaintext-structured, uploaded before a prune intent may reference it (§7.3). */ readonly verifierReceipt: (id: string) => string; /** * `envelopes//` — mirrors * `gitvault-creation-journal.ts`'s private `envelopePath` (the genesis * creator's envelope); this is the same addressing for every OTHER * recipient's `key_envelope`. `rotationId` present (D195, rev 42) widens * this to `envelopes///` — a * rotation-attempt envelope's own path (protocol §1: `rotation_id` * ABSENT, never explicit null, in the genesis/ADD-workaround case). */ readonly envelope: (epoch: string, recipientFingerprint: string, rotationId?: string | null) => string; /** `recipient-pins/.json` (D197, rev 42) — version-addressed, plaintext-structured, writer-signed. */ readonly pinManifest: (pinManifestVersion: string) => string; }; export interface GitvaultRestoreMarker { generation: string; head_sha256: string; } /** Read this target directory's `restored_through` marker, or `null` when nothing was ever restored into it. */ export declare function readGitvaultRestoreMarker(targetRepoDir: string): Promise; /** Default `auto_gc_generations` — see the change proposal's rationale (≈2-3s over the fresh-checkpoint floor at this backlog, a busy repo compacts roughly once per few dozen pushes). */ export declare const GITVAULT_AUTO_GC_GENERATIONS_DEFAULT = 32; /** * Read this checkout's auto-gc threshold, or the default when unset or * unparseable. Never throws — a corrupt local config value degrades to the * default rather than blocking a push's own auto-gc check. */ export declare function readGitvaultAutoGcThreshold(targetRepoDir: string): Promise; /** Set this checkout's auto-gc threshold. `0` disables auto-gc entirely. */ export declare function writeGitvaultAutoGcThreshold(targetRepoDir: string, generations: number): Promise; /** * {@link GitvaultVault.ensureRepoState}'s report when a cold keystore was * restored from its own envelope (gitvault-agent-envelopes D4). `trust` is * `receipt` only when a creator-held recovery receipt pinned genesis; * `platform_attested` means the control plane's signed allocation matched — * consistency the platform itself vouches for, so `independently_verified` * is `false` there and this label must never be read as end-to-end * authentication. `continuity` is `pinned` when this keystore had already * seen this genesis (a later open), `first_seen` on the first. */ export interface GitvaultColdOpenResult { repo_id: string; org_id: string; provenance: "restored_from_envelope"; trust: "receipt" | "platform_attested" | "unauthenticated_salvage"; continuity: "first_seen" | "pinned"; independently_verified: boolean; epoch: string; recipient_fingerprint: string; } export interface GitvaultVaultOptions { keystore: GitvaultKeystore; transport: GitvaultTransport; repo_id: string; /** The local git repository (objects for pack building / ancestry); optional for read-only use. */ repo_dir?: string; now?: () => Date; /** Verification budget per call (resumable — the verified prefix persists). */ verification_budget?: number; conflict_retries?: number; /** The signing service key resolved through the registry — when supplied, control-plane-signed messages (cutoff tickets) are signature-checked. */ service_public_key?: Uint8Array | string; /** * Consult round 2 §5: an ordinary open REFUSES a cold restore whose genesis * the control plane cannot attest (no signed allocation on the vault * record) — a data-path that cannot forge the allocation could otherwise * simply omit it and hand the client a fabricated vault. Only the explicit * recovery flow sets this, and the result still says * `unauthenticated_salvage`. */ allow_unauthenticated_salvage?: boolean; } /** How a checkpoint-bearing head binds a `retention_cutoff` ticket and expires roots (§4.5a / §4.5). */ export interface GitvaultCutoffOptions { /** * `effective_admitted_at` for a root's drop generation, or `null` when this * client cannot resolve it — a `null` RETAINS the root (expiry is permissive). * `effective_admitted_at = max(prepared_at, the admission record's storage * creation time)`; deriving it from `prepared_at` alone shortens the lane. */ effectiveAdmittedAt?: (droppedAtGeneration: string) => string | null; } /** One admitted `rotate_epoch` transition {@link GitvaultVault.verifyToNewest} walked over, KEYLESS (structural only — no envelope opened yet). */ export interface GitvaultEncounteredRotation { /** The rotate_epoch head's own generation. */ generation: string; /** == `payload.new_epoch` == the head's own `epoch`. */ epoch: string; payload: GitvaultRotateEpochPayload; /** * gitvault-multi-writer rev 47 — the writer who actually signed THIS * rotation head (already resolved + signature-verified by the forward * chain walk at the point this was collected — never the vault's fixed * genesis creator in a multi-writer vault). Optional: the BACKWARD * catch-up walk in `verifyToNewest` (decrypt-lag recovery) does not yet * resolve this for a rotation it discovers walking `prev_sha256` * backward from `lastHead` — a documented, narrower remaining gap * distinct from the forward walk's own full fix; `openEpochRotationForRecipient` * falls back to the vault's genesis-creator key when absent, byte-identical * to this field's pre-rev-47 non-existence. */ signing_pubkey?: string; } /** Named detail for a reader's own `GITVAULT_EPOCH_NOT_OPENABLE` / decrypt stop point (Part C: `repos fsck`'s honest `chain_verified_to` vs `decryptable_to` split). */ export interface GitvaultEpochDecryptFailure { generation: string; epoch: string; rotation_id: string | null; code: string; message: string; } /** * The result of an OPT-IN decrypt-validation pass over the walked chain * (`verifyToNewest({decryptValidate: true})`) — never present on an ordinary * keyless chain verify. `decryptable_to_generation` is the newest generation * whose own `ref_state`/`retention_roots` this call actually decrypted * (opening every `rotate_epoch` envelope needed along the way); it EQUALS * the outer state's `generation` on success and can fall short of it in * `strict: false` (tolerant) mode, in which case `failure` names exactly * where and why (never a bare, undifferentiated AEAD failure). */ export interface GitvaultDecryptValidationResult { decryptable_to_generation: string; /** The newest successfully-decrypted `ref_state`/`retention_roots`, or `null` if none this call reached (generation zero, or the very first head failed). */ ref_state: GitvaultRefState | null; retention_roots: GitvaultRetentionRoots | null; /** Every locally-known epoch key at the end of this call, hex-encoded — seeded from the keystore's persisted `epoch_keys` plus every rotation this call itself opened. */ epoch_keys_hex: Record; failure: GitvaultEpochDecryptFailure | null; } export interface GitvaultVerifiedState { generation: string; head_sha256: string; /** `null` at generation zero. */ head: GitvaultHead | null; genesis: GitvaultVaultGenesis; /** Every admitted `rotate_epoch` transition walked THIS call, oldest first — keyless (Part A's structural half; D202's join predicate needs only this). */ rotations: GitvaultEncounteredRotation[]; /** Present iff `decryptValidate` was requested. */ decrypt: GitvaultDecryptValidationResult | null; } export interface GitvaultMaterializedState extends GitvaultVerifiedState { ref_state: GitvaultRefState | null; retention_roots: GitvaultRetentionRoots | null; refs: GitvaultRefMap; roots: GitvaultRetentionRoot[]; head_target: GitvaultHeadTarget; /** Every locally-known epoch key after this call, hex-encoded, keyed by epoch — `restoreObjectsInto` needs every epoch spanned by the covered generations, not just the newest. */ epoch_keys_hex: Record; } /** One directory entry {@link GitvaultVault.reconcileEnvelopeRecipients} wrapped a fresh `key_envelope` for. */ export interface GitvaultReconcileEnvelopeRecipientsWrapped { principal_id: string; ek_fingerprint: string; } /** Why {@link GitvaultVault.reconcileEnvelopeRecipients} did NOT wrap a directory entry it otherwise would have. */ export type GitvaultReconcileEnvelopeRecipientsSkipReason = "missing_public_key" | "invalid_public_key" | "pinned_key_mismatch"; export interface GitvaultReconcileEnvelopeRecipientsSkipped { principal_id: string; ek_fingerprint: string; reason: GitvaultReconcileEnvelopeRecipientsSkipReason; /** `pinned_key_mismatch`: `{pinned_fingerprint, directory_fingerprint}`. `invalid_public_key` (derivation mismatch): `{derived_fingerprint}`. Absent for `missing_public_key` and a bad-encoding `invalid_public_key`. */ details?: Record; } /** {@link GitvaultVault.reconcileEnvelopeRecipients}'s full per-recipient breakdown. */ export interface GitvaultReconcileEnvelopeRecipientsResult { repo_id: string; org_id: string; /** The epoch every wrap in this call used (V0: always {@link GITVAULT_GENESIS_EPOCH}). */ epoch: string; /** Directory entries this call itself wrapped a NEW `key_envelope` for. */ wrapped: GitvaultReconcileEnvelopeRecipientsWrapped[]; /** `ek_` fingerprints already covering the vault before (or, for a raced wrap, as of) this call — no action taken. */ already_covered: string[]; /** Directory entries this call could not (or, for `pinned_key_mismatch`, would not) wrap. */ skipped: GitvaultReconcileEnvelopeRecipientsSkipped[]; } /** One `pending_writers[]` entry {@link GitvaultVault.reconcileWriterAdmissions} admitted via a fresh `add_writer_key{"writer"}` head. */ export interface GitvaultReconcileWriterAdmissionsAdmitted { principal_id: string; writer_key_id: string; generation: string; } /** Why {@link GitvaultVault.reconcileWriterAdmissions} did NOT admit a `pending_writers[]` entry it otherwise would have. */ export type GitvaultReconcileWriterAdmissionsSkipReason = "missing_signing_pubkey" | "invalid_signing_pubkey"; export interface GitvaultReconcileWriterAdmissionsSkipped { principal_id: string; writer_key_id: string; reason: GitvaultReconcileWriterAdmissionsSkipReason; } /** * {@link GitvaultVault.reconcileWriterAdmissions}'s full per-candidate * breakdown (task 5.7). Structurally parallel to * {@link GitvaultReconcileEnvelopeRecipientsResult} (same repo/wrapped-or- * admitted/already-covered/skipped shape), but this reconcile submits a REAL * chain head per admission (one `add_writer_key{"writer"}` transition per * candidate — the protocol allows only one added writer per head) rather * than an out-of-band object upload, so it is never fully parallel: each * admission materializes fresh against the PRIOR admission's own updated * writer set. */ export interface GitvaultReconcileWriterAdmissionsResult { repo_id: string; org_id: string; /** * `false` exactly when THIS session's own key is not (or is no longer) an * active writer — the "writer" door's authorization is the carrying * head's own signer, so an ineligible session cannot admit anyone, and * this call returns immediately with every other field empty. Distinct * from a genuinely empty `pending_writers[]` (still `eligible: true`, * there was simply nothing to do) — otherwise both cases produce the * SAME all-empty shape and a caller could not tell "nothing needed * doing" from "I have no authority to do anything here." */ eligible: boolean; /** `pending_writers[]` entries this call admitted this call, each via its own head. */ admitted: GitvaultReconcileWriterAdmissionsAdmitted[]; /** `writer_key_id`s already active before (or, for a raced admission, as of) this call — no action taken. */ already_covered: string[]; /** `pending_writers[]` entries this call could not admit. */ skipped: GitvaultReconcileWriterAdmissionsSkipped[]; } export interface GitvaultPushOptions { transaction: GitvaultRefTransaction; /** New `HEAD` target for the published ref_state; carried forward when omitted. */ head_target?: GitvaultHeadTarget; protocol_refs?: "refuse" | "allow"; /** Force the checkpoint-bearing form (purpose `ordinary_push`) regardless of delta size. */ checkpoint?: boolean; /** Bind a fresh `retention_cutoff` ticket so expired roots may leave the map (checkpoint-bearing heads only). */ cutoff?: GitvaultCutoffOptions; /** Built lazily at head-sign time — the deploy lane may still be computing the plan digest. */ capture_binding?: GitvaultCaptureBinding | (() => Promise | GitvaultCaptureBinding | null); /** * A caller-supplied base (gitvault-client-round-trips design D1) — used * VERBATIM for the first attempt instead of calling {@link GitvaultVault.materialize} * again. Only meaningful for a base the caller JUST materialized from this * same vault instance (e.g. one `list → push` protocol exchange sharing * one snapshot for both `expected_old` derivation and the push itself) — * supplying a stale base is always SAFE (admission is CAS on generation; * a stale base only makes a conflict more likely, never an incorrect * admission), just not the round-trip win. A conflict retry re-materializes * from storage exactly as when no base is supplied at all. */ base?: GitvaultMaterializedState; } export interface GitvaultPublishResult { generation: string; head_sha256: string; head: GitvaultHead; admission_record_sha256: string; capture_receipt: GitvaultCaptureReceipt | null; /** `wal` = direct WAL receipts; `checkpoint` = the delta shipped as a checkpoint set. */ form: "wal" | "checkpoint"; conflicts_retried: number; refs: GitvaultRefMap; /** * gitvault-clone-scaling (P3): generations-since-checkpoint against the * coverage this checkout has locally learned (`{0, false}` when unknown * — see the keystore field's doc). Advisory data only; consumers echo, * never gate. */ checkpoint_staleness: GitvaultCheckpointStaleness; } /** {@link GitvaultVault.rotateEpoch}'s result (D193-D203, rev 42). */ export interface GitvaultRotationResult { outcome: "admitted"; generation: string; head_sha256: string; new_epoch: string; rotation_id: string; reason: GitvaultRotationReason; included: { principal_id: string; ek_fingerprint: string; }[]; excluded_keyless_principal_ids: string[]; excluded_unconfirmed_principal_ids: string[]; admission_record_sha256: string; capture_receipt: GitvaultCaptureReceipt | null; /** * `"passed"` — this principal is itself an included recipient and its own * new-epoch envelope opened (via {@link import("../namespaces/gitvault.crypto.js").openEpochRotationForRecipient}, * the real reader entry point) to reproduce the sealed `K_e` + * `epoch_key_commitment` — the SAME round-trip that fed * `payload.self_open_attestation` (D209), run BEFORE this head was ever * submitted; a failure THROWS rather than returning here — there is no * `"failed"` value, and the head is never even built when it happens. * `"not_a_recipient"` — this principal (the vault's writer) is not itself * in `included` (e.g. it was excluded, or holds no local encryption key) * — there is nothing for this machine to self-check, and * `payload.self_open_attestation.outcome` is `"writer_not_recipient"`. * This is NOT a confidentiality gap: the writer sampled `kE` itself (it * never needs a `key_envelope` to learn its own secret) and * `keystore.recordEpochRotation` below advances the LOCAL pointer * unconditionally once admission succeeds, regardless of `self_check` — * an agent/CI writer that is deliberately never a directory envelope * recipient (design D1 of `services/gitvault/desired-recipients.ts`: * "agents hold their own vault keys in their CLI keystore") keeps * read/write access to its own vault through every rotation it itself * drives, with or without an envelope. */ self_check: "passed" | "not_a_recipient"; /** * Present iff `options.pending_confirmations` was non-empty and its * receipted entries were folded into THIS SAME head's `pin_manifest` * field (D197 conservation; schema-legal per §4.3 — `transition` and * `pin_manifest` are independent optional fields on one `head`). `null` * when no fold was requested. * * **D196 boundary, load-bearing:** folding does NOT make these principals * `included` in THIS rotation's envelope set — "for a non-genesis * rotation, `confirmed(h)` reads the NEAREST PREDECESSOR admitted pin * manifest — a manifest update riding the SAME head never self-authorizes * its own recipients" (protocol-v0.md D196). They land in * `excluded_unconfirmed_principal_ids` for THIS rotation exactly as they * would without folding. What folding buys: the manifest becomes DURABLY * ADMITTED in the same atomic submission that is itself EXEMPT from * `EPOCH_ROTATION_REQUIRED` (a `rotate_epoch` admission is the gate's own * escape valve — it must be, or the flag it clears could never clear) — * closing the standalone `publishPinManifestUpdate` deadlock where the * manifest-only push is ITSELF an ordinary admission and therefore * ITSELF gated while migration/revocation/exposure is outstanding. The * newly-published principals become eligible starting from the NEXT * rotation (or the next ordinary push, once the flag clears). */ pin_manifest_published: { pin_manifest_version: string; stored_bytes_sha256: string; principal_ids: string[]; } | null; /** * gitvault-multi-writer (task 5.9, D6/D227) — writer keys this SAME head * removed via a folded `writer_set_update`, each with the reason it was * removed for. Empty (never omitted) when nothing needed removing — the * common case, matching `excluded_keyless_principal_ids`' own * always-present-possibly-empty shape rather than `pin_manifest_published`'s * `| null`. */ writers_removed: { writer_key_id: string; principal_id: string; reason: "member_removed" | "writer_key_revoked" | "epoch_secret_exposed"; }[]; } /** * What {@link GitvaultVault.planPush} reports — a REAL * local computation, not an estimate: every number here comes from actually * building the packs (or checkpoint set) and actually sealing/encrypting * them, exactly as a real `push` would. `would_admit_generation` is what this * push would claim AT THE OBSERVED BASE — a concurrent publisher can still * win the race before a real push runs (see the method doc for why that is * not a defect: `git push --dry-run` carries the identical caveat). */ export interface GitvaultPushPlan { /** The base this plan was computed against — `materialize()`'s generation at call time. */ base_generation: string; /** The generation a real push would claim, computed the same way a real push computes it (`nextGeneration(base_generation)`). */ would_admit_generation: string; /** `would_admit_generation` as a plain decimal string — the hex generation is a wire format, not a human one. */ would_admit_generation_decimal: string; /** `wal` = direct WAL receipts; `checkpoint` = the delta would ship as a checkpoint set — the SAME threshold real `push` uses. */ form: "wal" | "checkpoint"; /** The ref map this push would publish (after evaluating the transaction against the base). */ refs: GitvaultRefMap; head_target: GitvaultHeadTarget; /** Every object that would be uploaded — `ref_state`, `retention_roots`, and the WAL/checkpoint pack(s) — with their REAL sealed (encrypted) sizes. */ objects: Array<{ object_kind: GitvaultUploadObject["object_kind"]; size_bytes: string; }>; object_count: number; /** Sum of `objects[].size_bytes` — the REAL ciphertext byte count a real push would upload. */ encrypted_bytes: string; /** Sum of the plaintext pack bytes BEFORE sealing — what "objects that would publish" weigh on the wire before encryption overhead. */ raw_bytes: string; } /** * What {@link GitvaultVault.verifyStoredCheckpoint} observed. Every boolean is * a FINDING, not a promise: a `false` here is what makes a truthful negative * `verifier_receipt` possible. */ export interface GitvaultStoredCheckpointAttestation { checkpoint_head_sha256: string; checkpoint_generation: string; claim_set_sha256: string; /** `null` in the no-removal checkpoint form — a prune needs one, so that is a refusal upstream. */ cutoff_ticket_sha256: string | null; cutoff_at: string | null; covered_tips: string[]; /** Covered tips that did NOT resolve from the restored set. Non-empty ⇒ the checkpoint does not verify. */ missing_tips: string[]; restored_object_set_hmac: string; object_set_matches: boolean; ref_state_matches: boolean; retention_roots_matches: boolean; /** The `rootset` commitment over this generation's roots carrier — the intent core's `retention_state_hmac`. */ retention_state_hmac: string; } export interface GitvaultBuiltCheckpoint { manifest: GitvaultCheckpointManifest; claim_set: GitvaultCheckpointClaimSet; claim_set_receipt: GitvaultCheckpointBlock["claim_set"]; objects: GitvaultUploadObject[]; /** Plaintext packs in order (for the acceptance self-check). */ packs: Uint8Array[]; covered_tips: string[]; } /** A transport-agnostic view of git ops the publication needs (the local repository). */ export declare class GitvaultVault { #private; readonly keystore: GitvaultKeystore; readonly transport: GitvaultTransport; readonly repoId: string; readonly repoDir: string | null; private readonly now; private readonly budget; /** * gitvault-clone-scaling (bench P2): the CURRENT walk window's prefetched * bytes — head bytes (bounded-concurrent direct reads) and carrier frames * (one batched getObjects), keyed by storage path — filled by * `verifyToNewest`'s per-page prefetch and `restoreObjectsInto`'s * backward-window prefetch, consulted by {@link readCachedHeadBytes} / * {@link openMaterializeCarriers} before they pay a network read. Transient (REPLACED each page, so memory is * bounded by one listing page) and UNTRUSTED: every consumer sha-checks * an entry against the exact value it would check network bytes against * (the listing's `stored_bytes_sha256`, a receipt's `ciphertext_sha256`), * the same discipline as the keystore object cache — a wrong or stale * entry is a MISS, never a verification bypass. Deliberately NOT the * keystore cache: that cache's eviction window is a handful of newest * generations by design, so routing a whole page through it would evict * the very bytes the ordered walk is about to read. */ private walkPrefetch; /** * WAL pack bytes carried by the state read's delta (gitvault-delta-fetch), * keyed by object_id — the walkPrefetch discipline exactly: UNTRUSTED * until a consumer's own hash check passes, a mismatch is a plain miss, * and the buffer is transient (stashed by {@link tryStateFastPath}, * consumed and cleared by the next restore). */ private stateDeltaPacks; /** * The state read's restore plan (gitvault-restore-recipe design D1-D5), * stashed RAW — never verified at stash time, unlike `stateDeltaPacks` * (whose heads self-check before entering the shared keystore cache): a * plan's heads/checkpoint/packs are only ever verified by {@link * restoreObjectsInto}'s own full obligation set, and a partially-checked * plan sitting here would be a foot-gun for a future caller that forgot * the difference. Consumed and cleared exactly once, by the next * `restoreObjectsInto` call (either its own materialize, or a dedicated * plan-only read it issues when the incremental walk it started aborts). */ private stateRestorePlan; private readonly retries; private readonly servicePublicKey; private readonly allowUnauthenticatedSalvage; private genesisCache; /** * gitvault-byo-primary-bucket task 3.2 — this vault's resolved BYO write * target, cached after the first resolution (storage_profile/ * byo_destination are immutable-at-allocation in v1, so a single * read-once-per-instance is safe). `undefined` = not yet resolved; * `null` = this machine has no LOCAL BYO write config for this repo * (either an ordinary managed vault, or a BYO vault this machine has not * been configured to write — see {@link resolveByoWriteTarget}). */ private byoResolution; constructor(options: GitvaultVaultOptions); static open(options: GitvaultVaultOptions): GitvaultVault; repoFile(): GitvaultRepoFile; /** * gitvault-agent-envelopes D4 — the COLD OPEN. A keystore that holds an * identity but no repo file for this vault (a member joining from a fresh * machine, or a creator whose repo file was lost) restores `K_repo` from * its OWN `key_envelope` instead of dying `GITVAULT_REPO_STATE_MISSING`: * * 1. genesis (the writer-key source) is fetched and signature-verified; * 2. its creator fingerprints are compared against the control plane's * SIGNED allocation record — `platform_attested`, never `receipt` * (the platform serves both sides of that comparison; a substituted * genesis needs a substituted allocation, which the org's owners can * see — TOFU + audit, human-envelopes D4's tier); * 3. the envelope-recipients read says whether THIS fingerprint is * covered — if not, `GITVAULT_ENVELOPE_PENDING` names the key-holders * who can fulfil and the exact next actions (never a terminal error: * the desired state already records this member; any key-holder's * next gitvault operation wraps); * 4. the base envelope is fetched, opened, and the repo file written * `restored_from_envelope` with the genesis hash PINNED (a later open * seeing a different genesis for this repo_id refuses * `VAULT_CREATION_CONFLICT`). * * Rotation epochs are NOT opened here — `verifyToNewest` walks them and * opens each rotation-scoped envelope this identity is included in, exactly * as it does for every other reader (`openEpochRotationForRecipient`). * * Returns `null` when the repo file already existed (nothing restored). */ /** * gitvault-multi-writer (rev 47) — the vault's CURRENT admitted writer set * for a cold open, computed from the chain itself: every admitted head is * hash-checked against the listing, signature-verified under the writer * the chain admits at that point, and its `add_writer_key` / * `writer_set_update` transitions applied in order — the same rules the * full verifying walk enforces, minus decryption (a cold open holds no * K_repo yet). Fails closed on any defect: a non-genesis wrapper is only * trusted when the chain vouches for it. */ private resolveAdmittedWritersForColdOpen; ensureRepoState(): Promise; /** {@link open}, but a missing repo file triggers {@link ensureRepoState} first. */ static openOrRestore(options: GitvaultVaultOptions): Promise<{ vault: GitvaultVault; restored: GitvaultColdOpenResult | null; }>; private kRepo; private epoch; /** * gitvault-clone-scaling (P3): staleness of the newest checkpoint coverage * this checkout has locally learned, measured at `newestGeneration`. Reads * the keystore AFTER the caller's own persist (a checkpoint-form push has * already recorded its fresh coverage by the time its result is built), so * a compacting push reports itself current. Pure + never-throwing by way of * the helper; unknown coverage reads as `{0, advised: false}` — silent. */ private checkpointStalenessNow; /** * Fetch many generation-addressed heads' bytes ahead of the ordered walk, * keyed by head path. * * BATCH-FIRST (gitvault-batched-head-reads task 4.2): one * `POST …/head-reads` carries a whole page's bytes. Heads deliberately do * NOT ride `getObjects` — the `object-reads` presign batch is CARRIER-ONLY * by wire design (see `getObjectsBytes`'s fail-closed doc comment; the live * probe that caught this recorded `getObjects paths=67 FAILED 1ms` followed * by 25 serial singles), which is exactly why the batch route had to be its * own thing. * * FALLBACK (the shipped gitvault-clone-scaling P2 shape): on ANY * unsupported answer — an older gateway, a refusal, a fault — head bytes * parallelize as the SAME direct GETs the ordered walk itself would issue, * just early and overlapped at {@link GITVAULT_TRANSPORT_CONCURRENCY}. The * transport remembers a route-absent verdict, so the probe is paid once per * client, not once per window. * * Either way a per-head failure simply leaves that slot EMPTY — the walk's * own read owns that failure and its envelope — and results are raw and * UNTRUSTED: callers sha-check before use, per `walkPrefetch`'s contract. * That is what keeps this a transport change and never a trust change. */ private prefetchHeadsConcurrent; private git; /** Fetch + pin-check the genesis (the writer key source). */ genesis(): Promise<{ genesis: GitvaultVaultGenesis; sha256: string; }>; /** * gitvault-composite-state-read design D1 — the pin-current fast path * `verifyToNewest` tries FIRST: one `GET …/state` in place of BOTH the * live "server still holds the pin" read {@link readHead} would otherwise * perform AND, when eligible, the `listHeads` walk that would follow it. * * `null` means ineligible — the caller falls straight through to the * UNCHANGED `readHead` + `listHeads` flow, so a `null` here never weakens * verification, it only declines the shortcut: * - the vault is genuinely more than one generation ahead of `pin` * (the listing-walk shape this change does not touch, per design D1: * "a client whose pin is >1 behind newest_generation falls back to * the existing paginated listing + per-head walk"); * - OR (one-generation-ahead only) the D194 epoch-continuity check the * ONE new head needs `pin`'s own `.epoch` for, and this call declines * to fetch `pin`'s own head bytes over the network — the entire point * of the shortcut — so it needs a LOCAL source for that epoch: either * `pin` is genesis (a fixed, known epoch), or `pin`'s own head bytes * are already cache-warm (from an earlier call, or from `admit()`'s * own post-push cache write). A cold cache here is not a correctness * problem, only a missed optimization. * * On a non-`null` return, `entries` is exactly what ONE real `listHeads` * page's `heads[]` would have been for this pin (0 items when the pin is * already current, 1 when it is exactly one generation behind — chain * link, gaplessness, and signature all still verified by the UNCHANGED * per-entry loop body {@link verifyToNewest} feeds them through), and * `pinnedHead` is `lastHead`'s INITIAL value: the real, verified head at * `pin.generation` when nothing new needs walking (so it is also the * FINAL value — the loop never runs), or a throwaway placeholder when one * new head is coming (the loop overwrites `lastHead` before anything else * ever reads it again — see `verifyToNewest`'s own `prevEpoch` line, * which is the ONLY thing that reads `lastHead`'s pre-loop value). * * Every byte this method reads from `getState` is cache-WARMED (head + * both carriers, keyed exactly as {@link readCachedHeadBytes}/ * {@link openCarrier} already key their own writes) but NEVER trusted * here — every reader downstream re-verifies a cache hit against the hash * it would check network bytes against before using it (this file's own * established cache discipline; see `GitvaultKeystore`'s class doc * comment). A wrong or absent byte this method wrote is therefore just a * cache MISS on the next read, never a verification bypass. */ private tryStateFastPath; /** * Absorb a state read's delta (gitvault-delta-fetch): heads blind-warm the * SAME keystore head cache every walk already re-verifies on read (the * established cache discipline — a wrong byte is a miss, never a bypass), * gated only on each entry's self-consistency; packs stash into the * transient {@link stateDeltaPacks} buffer for the next restore, which * hash-checks each against its carrying head's receipt before use. */ private consumeStateDelta; /** * Warm the D3 carrier cache from a `GET …/state` response's two carriers, * keyed by the SAME `(object_id, ciphertext_sha256)` the carrying head's * own receipts name — a BLIND write (see {@link tryStateFastPath}'s doc * comment: every reader re-verifies a cache hit before trusting it, so * this is safe by construction). Skips a `null` carrier (absent stored * bytes) entirely rather than caching an absence — the existing * `openCarrier`/`decodeCarrierFrame` machinery already has its own * "frame absent" handling (`CHAIN_UNUSABLE`) via a genuine cache miss. */ private warmStateCarrierCache; /** * List from the authenticated pin and verify every link upward. Persists * the verified prefix after each page, so a `VERIFICATION_BUDGET_EXCEEDED` * continues rather than restarts. Returns the newest verified state. * * `options.persist` (default `true`, repo-surface-consolidation task 3.3 — * `repos fsck --no-write`'s audit mode): when `false`, the chain is walked * and verified exactly the same way, but every `keystore.updateRepo(...)` * write below is skipped — no local trust pin advances. A * `VERIFICATION_BUDGET_EXCEEDED` pause in this mode persists nothing, so a * retry restarts from the ORIGINAL pin rather than resuming — the honest * consequence of asking for a no-write audit and then walking off the end * of one call's budget. * * The chain walk itself (`checkChainLink`, `assertNoTransition`, collecting * `rotations[]`) is ALWAYS keyless — an admitted `rotate_epoch` transition * never stops it (D193, rev 42), so `generation`/`head` here are the * genuinely chain-verified newest, independent of whether this principal * can decrypt anything past a rotation it cannot open. * * `options.decryptValidate` (default `false`, Part C — `repos fsck`'s * decrypt-validation pass): additionally opens every `rotate_epoch` * envelope needed and decrypts each walked generation's OWN * `ref_state`/`retention_roots` as it goes — the "main object" restoration * needs per generation — persisting newly-opened epoch keys via * `keystore.recordEpochRotation` exactly like an ordinary rotation * producer/consumer would, and counting each decrypt attempt as an EXTRA * unit against the same `this.budget` (decryption is the expensive step). * `options.strict` (default `true`) throws immediately, fail-closed, on the * first `GITVAULT_EPOCH_NOT_OPENABLE` / AEAD failure it hits — the ordinary * `materialize()` read path's behavior. `strict: false` (fsck's tolerant * mode) instead records `decrypt.failure` and stops attempting further * decrypts while the pure chain walk keeps going — this is exactly what * makes `chain_verified_to` (this call's `generation`) and `decryptable_to` * (`decrypt.decryptable_to_generation`) able to differ honestly. */ verifyToNewest(options?: { persist?: boolean; decryptValidate?: boolean; strict?: boolean; deltaSince?: string; restore?: boolean; }): Promise; /** * Read one NEWLY-LISTED head's raw bytes, trying the local cache first * (design D3), re-verified against `expectedSha256` — the SAME check * network bytes get. Safe to cache-serve ONLY because a caller here * always supplies a hash a FRESH `listHeads` call just reported as * current — the cache never substitutes for that freshness check, it just * avoids re-downloading bytes a live listing already vouched for. A cache * miss or a hash mismatch (never trusted, always falls through) fetches * from the network and, on a match, refreshes the cache entry. Returns * whatever the network returned on a final miss too (including `null`) — * callers keep their own existing absent/mismatch handling unchanged. * * Deliberately NOT used by {@link readHead}: that call verifies the * PINNED generation is STILL held by the server, with no fresh listing * involved — its entire purpose is detecting server-side loss/rollback, * which a cache read can never observe. That call always goes live. */ private readCachedHeadBytes; /** * Confirm the server STILL holds the pinned generation, unchanged — * ALWAYS a live network read (see {@link readCachedHeadBytes}'s doc * comment for why this specific check is not cacheable: it exists to * detect server-side loss, which a cache can never observe). A * successful read still WARMS the cache afterward — later reads of this * SAME generation via the chain walk or restore benefit from it; only * THIS call's own read is exempt. */ private readHead; /** * Decrypt + verify one carrier's already-fetched ciphertext frame; any * failure is `CHAIN_UNUSABLE`. Split out of {@link openCarrier} so a * caller that fetched the frame itself (a cache hit, or one leg of a * batched read) can reuse the same decode + identity/signature checks. * * `keyOverride` supplies the exact `(epoch, k_repo)` this carrier was * sealed under — REQUIRED for any generation that is not necessarily * under `this.epoch()`/`this.kRepo()` (this principal's CURRENT * pointer), which is exactly the case across an epoch rotation; omitted * call sites (checkpoint/prune paths untouched by this fold) keep the * prior CURRENT-pointer behavior unchanged. */ private decodeCarrierFrame; /** * Fetch (network) + decrypt one carrier object by its receipt; any * failure is `CHAIN_UNUSABLE`. Design D3: `ref_state`/`retention_roots` * ciphertext is cached beside the keystore's per-repo state, re-verified * against `receipt.ciphertext_sha256` on every use — a hit skips the * network read entirely. `checkpoint_manifest` is never cached (outside * D3's table). The cache write derives its generation tag from the * DECODED object's own `generation` field, so no caller needs to thread * one through by hand. */ private openCarrier; /** * `materialize`'s ref_state + retention_roots read, batched (design D2): * cache-check both first, then ONE `getObjects` call (one presigned batch * + concurrent GETs) for whichever missed, instead of two independent * presign-then-GET round trips. Falls through to zero network calls when * both are cache-warm. `keyOverride` is the epoch/k_repo the CARRYING * HEAD sealed both carriers under (D194) — both always share one head, so * one override serves both, unlike the WAL/checkpoint pack loops in * {@link restoreObjectsInto} which span many heads and many epochs. */ private openMaterializeCarriers; /** * Verify to newest, then decrypt + apply its carriers — advancing the * materialized pin. `options.persist` (default `true`) is forwarded to * {@link verifyToNewest} and gates this method's OWN `materialized_pin` * write the same way — `repos fsck --no-write` computes and returns the * real ref map and generation without moving either local pin. * * Runs `verifyToNewest({..., decryptValidate: true, strict: true})` * internally (Part A: an ordinary read across an admitted `rotate_epoch` * transition now opens the rotation's own envelope and decrypts under the * NEW epoch, chaining through multiple sequential rotations; a keystore * with no envelope for a new epoch fails CLOSED with * `GITVAULT_EPOCH_NOT_OPENABLE`, never a bare `GITVAULT_AEAD_AUTH_FAILURE`) * — `strict: true` means this call throws exactly where the OLD * (pre-fix) `materialize()` silently produced a wrong `k_obj` instead. */ materialize(options?: { persist?: boolean; deltaSince?: string; restore?: boolean; }): Promise; /** * Wrap the vault's CURRENT epoch key to every org member the directory * lists but the vault does not yet have a `key_envelope` for. * * **This is task 1.1's residual WORKAROUND, not the design D5 ideal.** D5 * describes a recipient-set change as an epoch rotation — "history epochs * stay wrapped as they were; a new member reads from their first covered * epoch forward" — which needs a protocol revision: V0 pins `epoch` to * the single constant `GITVAULT_GENESIS_EPOCH` on every head, so there * is no "forward" to speak of. What this method actually does, legally, without any * protocol change: `key_envelope` objects are never head-referenced (not * even genesis's own envelope is), so uploading an ADDITIONAL one at * `envelopes//` for a missing * recipient is accepted by the existing generic create-only upload route * as-is. The honest consequence: a newly-wrapped member gets the SAME * single epoch every existing member already has, which in V0 means the * vault's ENTIRE history — not "from here forward." True forward-only * semantics wait on task 1's protocol revision; this method does not * pretend otherwise. * * **TOFU pinning (design D4 point 3).** The first time this repo wraps a * given `principal_id`, its CURRENT `ek_fingerprint` is pinned in the * keystore repo file (`envelope_recipient_pins`). On a later call, a * directory entry whose fingerprint no longer matches its pin is a * REFUSAL for that recipient ONLY — reported under `skipped` with reason * `pinned_key_mismatch` and both fingerprints in `details`, never wrapped * under the new key, and never a thrown error that would abort the whole * call (other recipients still get processed). Whether that mismatch is a * legitimate key rotation or a substitution is a product/human decision * this SDK does not make unattended. * * **The gateway directory route carries `public_key` on every row** * (`GET /orgs/v1/:org_id/encryption-keys` — see the * doc comment on {@link GitvaultOrgEncryptionKeyEntry}), so against a * current gateway entries actually wrap. A directory entry that arrives * WITHOUT the field (an older/rolling-deploy gateway) is still tolerated * per-entry — reported under `skipped` with reason `missing_public_key`, * never a thrown error that would abort the other recipients. * * Best-effort by design at the call site, not here: this method itself * either completes (returning a full per-recipient breakdown) or throws * (e.g. `GITVAULT_READ_ONLY` when this principal holds no signing key). * Callers that want "never block on this" (the deploy hook) wrap the call * themselves — see `Gitvault.push`'s `#tryReconcileEnvelopeRecipients`. */ reconcileEnvelopeRecipients(): Promise; /** * `keyOverride` (D194, rev 42): a `rotate_epoch` head's OWN `ref_state`/ * `retention_roots` must be sealed under the NEWLY-sampled `K_e` at the * NEW epoch, never under `this.kRepo()`/`this.epoch()` (the about-to-be- * superseded current key) — every other call site keeps calling `seal` * with no override, unaffected. */ private seal; /** * The owner signing seed, or `GITVAULT_READ_ONLY`. * * Public so the prune lane signs its intent core, wrapper, and verifier * receipt through the SAME refusal path every other signed object uses — a * second "get the seed" helper is a second place for a read-only principal to * slip through. The vault is already open by the time this is reachable, so * `ensureIdentity` never MINTS here (it would refuse at `repoFile()` first). */ signer(): Uint8Array; private writerKeyId; /** * gitvault-multi-writer (task 5.8) — the push pre-check: before signing an * ORDINARY head (any operation that expects this session to already be an * active writer), refuse EARLY and LOCALLY when this session's own key is * not (or is no longer) an active writer, rather than let real * crypto/upload work run only to be refused by the gateway with a less * specific message once the head finally reaches it. Reads the FRESHLY- * pinned `writer_set_pin`; every call site below calls this immediately * after its own `materialize()`/`verifyToNewest()` (same generation the * caller is about to build against), so this performs no verification of * its own — it only reads what the caller already froze. * * `removedMidRace` (design D8/D10) distinguishes the two shapes this * refusal takes: `false` (the default) is the ORDINARY pre-check — this * session was never (or is not currently) an admitted writer, thrown as * `GITVAULT_WRITER_NOT_ADMITTED`. `true` is the CAS-LOSER path * specifically: a retry, after re-materializing from the winner, that * discovers THIS session's own key was removed by whatever won the race — * "stop if removed" in D8's loser-rule sequence (fetch winner → verify → * apply writer transition → stop if removed → rebase → rebuild → bounded * backoff) — thrown as the more specific, client-local * `GITVAULT_WRITER_REMOVED` (D10) instead: a real prior attempt just lost * to a removal, not a caller who was never eligible. * * Deliberately NOT called for a "handoff"-door `add_writer_key` head * ({@link submitWriterActivationHead}): there the signer is BY DESIGN not * yet a writer — becoming one is what that exact head does. */ private assertCallerIsWriter; /** The owner's full signing keypair, or `GITVAULT_READ_ONLY` — same refusal path as {@link signer}, which returns only the seed; {@link sealKeyEnvelope} needs both halves. */ private signingKeypair; private buildRefState; private buildRetentionRoots; /** Plaintext, independently non-thin packs covering `reachable(tips) ∖ reachable(base)`, split at the multi-object target. */ buildPacks(tips: string[], base: string[]): Promise; /** Sorted unique object ids reachable from `tips` (the `"objectset"` content). */ objectSet(tips: string[]): Promise; /** * The same `"objectset"` content computed in an ARBITRARY repository. * * The prune lane's restore-and-verify pass runs against a scratch clone-back, * not the working tree, and must recompute the digest there with the same * canonicalization the manifest was built with — hence one implementation, * parameterized by directory, rather than a second rev-list at the call site. */ objectSetIn(dir: string, tips: string[]): Promise; /** * Decrypt one generation's `retention_roots` carrier by its head receipt. * * `materialize()` opens only the NEWEST carrier; the prune lane must compare * consecutive generations to see which roots LEFT the map, so it needs any * generation's. Same `openCarrier` path, same `CHAIN_UNUSABLE` semantics — a * carrier that cannot be opened is never silently treated as empty. */ openRetentionRootsAt(receipt: GitvaultRetentionRootsReceipt): Promise; private digest; /** * The §1 keyed commitment under one of the five `K_digest` labels. * * Public because the prune lane needs `gcrootset` (over the GC root set's * sorted receipts) and `rootset` (over the retention-roots carrier) and must * compute them with the SAME key derivation the checkpoint manifest uses — * two derivations for one commitment is how a verifier and a publisher stop * agreeing. Keyed by design (§7.3): a server-comparable plaintext digest * would be a confirmation oracle. */ keyedDigest(label: GitvaultDigestLabel, content: unknown): string; /** Strip the single top-level signature — the commitment preimage shape carriers use. */ digestPreimage(o: T): Omit; private withoutSignature; /** Coverage tips (§4.7): canonical refs ∪ unexpired roots ∪ the HEAD target (detached commit; an unborn symref contributes nothing). */ static coverageTips(refs: GitvaultRefMap, roots: GitvaultRetentionRoot[], headTarget: GitvaultHeadTarget): string[]; /** * Build a checkpoint set (§4.7): manifest + packs + the owner-signed claim * set, with the acceptance self-check (restore into an empty scratch, every * covered tip resolves, full connectivity, all three keyed commitments * recomputed). Coverage above the V0 maximum → `CHECKPOINT_SET_LIMIT_EXCEEDED`. */ buildCheckpoint(input: { generation: string; ref_state: GitvaultRefState; retention_roots: GitvaultRetentionRoots; }): Promise; /** §4.7 acceptance: restore from the set ALONE into an empty scratch; every covered ref resolves; fsck connectivity; recompute the three commitments. */ acceptCheckpoint(built: GitvaultBuiltCheckpoint, refState: GitvaultRefState, roots: GitvaultRetentionRoots): Promise; /** * The §4.7 acceptance run against a checkpoint ALREADY IN STORAGE — the * restore-and-verify pass a `verifier_receipt` attests (§7.3). * * `acceptCheckpoint` above proves a checkpoint the client just BUILT; this * proves one the client is about to make a claim about, from the stored bytes * alone. It reports the observed facts rather than throwing on a mismatch, * because "the checkpoint does not verify" is exactly the finding a receipt * must be able to carry as `false` — turning it into an exception would make * an honest negative attestation impossible to produce. */ verifyStoredCheckpoint(head: GitvaultHead, headSha256: string): Promise; /** * The whole verified chain, newest-first walk returned oldest-first, each * head paired with its checkpoint claim set (`null` when it bears none). * * The prune lane needs EVERY generation, not just the newest: a candidate is * an object some head once named and no surviving head still needs, and that * is only computable over the whole chain. Reuses {@link chainFrom}, so the * bytes are re-read and hash-checked against the verified chain rather than * trusted from a listing. */ chainEntries(): Promise>; /** * gitvault-byo-primary-bucket task 3.2 — resolve (once, cached) this * machine's local BYO write config for THIS vault. `null` when none is * configured locally — either because this is an ordinary managed vault * (the common case; deliberately never confirmed with a network call, so * a managed vault's push pays zero extra round trips), or because this * machine has not been configured to write a BYO vault it nonetheless * belongs to (surfaced downstream as `GITVAULT_BYO_BUCKET_WRITE_REFUSED` * when the session actually names a `put: null` object with no target). */ private resolveByoWriteTarget; private uploadAll; /** * Build + upload + sign + admit ONE generation over `base`. Shared by the ref * transaction path (`push`) and the checkpoint-only path (`publishCheckpoint`); the * caller owns the conflict loop because only it knows how to re-derive the * next state from the winner. */ private publishGeneration; /** * The complete push: verify → materialize → evaluate → pack → upload → head → admit (409: re-apply to the winner, retry) → read back → advance pins. * * `options.base` (design D1), when supplied, is used VERBATIM for the * first attempt instead of a fresh {@link materialize} call — a conflict * retry always re-materializes from storage, exactly as when no base is * supplied. */ push(options: GitvaultPushOptions): Promise; /** * A REAL preview of what {@link push} would publish * — runs the SAME local pipeline `push` runs (materialize → evaluate → * evolve retention roots → build refState/retentionRoots → build packs or a * checkpoint set → seal/encrypt) and stops BEFORE the two network * mutations `push` performs (`uploadAll`, `admit`). One shot, no conflict * retry: there is nothing to retry against, since no generation is ever * admitted. `would_admit_generation` is therefore the generation this push * WOULD claim over the CURRENTLY OBSERVED base — a concurrent publisher can * still take it first before a real push runs, exactly as `git push * --dry-run` never promises a fast-forward will still hold by the time a * real push executes. * * Never retries and never allocates: an unallocated vault has no `repo_id` * (hence no encryption key) to preview a push against at all — callers * resolve the vault READ-ONLY first (see `Gitvault.planPush` in * `../namespaces/gitvault.js`, which reports `allocation_needed: true` in * that case instead of calling this method). */ planPush(options: GitvaultPushOptions): Promise; /** * Publish an `ordinary_push` checkpoint-bearing head that changes NO ref (the * canonical map and HEAD target are carried forward). With a cutoff the head * binds a fresh `retention_cutoff` ticket and roots past their ≥90-day lane * may leave the map; without one it is the no-removal form (§4.5a) and every * root is carried. * * Root expiry is PERMISSIVE: a root whose `effective_admitted_at` this client * cannot resolve is RETAINED. That is deliberate — `effective_admitted_at = * max(prepared_at, the admission record's storage creation time)`, and a client * reading only object bytes cannot see the second term. Resolving it from * `prepared_at` alone would shorten the lane, which the protocol's own * delayed-PUT vector calls out as the naive implementation. * * This is NOT `run402 gitvault compact`: the §7.2 maintenance CYCLE (purpose * `maintenance_cycle`, C1/C2 roles, stage claim sets, prune intents, `R2_cap` * accounting) is a separate protocol under compact authority, and gets its own * method when it ships. */ publishCheckpoint(options?: { cutoff?: GitvaultCutoffOptions | false; }): Promise; /** Request a `retention_cutoff` ticket and check it binds THIS base head (and the service key, when one is pinned). */ private issueRetentionCutoff; /** * `overrides.epoch` (D194, rev 42 fix): defaults to `this.epoch()` — the * LOCALLY KNOWN current epoch — rather than the fixed genesis constant. * This is load-bearing, not cosmetic: once ANY rotation has landed, every * ORDINARY (non-`rotate_epoch`) head this principal signs must still * claim the vault's CURRENT epoch (protocol §4.3's chain-link rule — * "every head's epoch equals its predecessor's UNLESS this head admits a * `rotate_epoch` transition"); hard-coding the generation-1 constant here * would make EVERY ordinary push after a vault's first rotation refuse * `CHAIN_BROKEN` forever. `rotateEpoch` passes `overrides.epoch = new_epoch` * explicitly for the ONE head that legitimately claims a DIFFERENT epoch * than `this.epoch()` currently reads (the local pointer only advances * AFTER a successful admit, via `keystore.recordEpochRotation`). */ private signHead; /** Admit a signed head; on success read it back from storage and compare BEFORE any pin advances. */ private admit; /** * Walk the chain BACKWARD from the current tip to find the nearest * admitted `recipient_pin_manifest` receipt (D197: "a fresh client... reads * the LATEST admitted manifest, walking `prev_sha256` back to the nearest * head carrying one"). Heads are plaintext-structured/signed (never * encrypted) so this needs no key material — only signature verification * against the creator's own pubkey. Falls back to `vault_genesis.pin_manifest` * (D198 N-recipient genesis — this SDK does not BUILD one, but reads one * correctly for interop), then to the zero-value sentinel (no manifest has * ever been admitted — every principal starts `excluded_unconfirmed`). * * Cost is O(distance to the nearest pin-manifest-bearing head) — for a * vault that has never published one, that is every generation back to * genesis. There is no index that avoids this in protocol v0 (the same * cost class `verifyToNewest`'s own chain walk already has); a vault with * a long, pin-manifest-free history pays it once per rotation. */ private loadEffectivePinManifest; /** * Local-cache short-circuit (see {@link GitvaultRepoFile.known_pin_manifest}'s * own doc comment for why this is safe): a manifest THIS keystore itself * just built, signed, and admitted is resolved from the on-disk cache * instead of a network `object-reads` round trip — same return shape, * skipped only on an exact `(pin_manifest_version, stored_bytes_sha256)` * match against `receipt`. A miss (cache absent, or naming a DIFFERENT * manifest — e.g. one another principal/machine published) falls through * to the unchanged network path below. * * History: this cache originally also routed around a gateway gap — * `POST …/object-reads` rejected every `recipient_pin_manifest` read * (its null-`idScalar` validation was hardcoded to `key_envelope`'s * `{epoch, recipient_fingerprint}` shape, never generalized when D197 * shipped the second path-addressed kind), which 400'd the network * fallback below. That gateway bug is fixed: the network path works for * any keystore, including * §4.11's fresh-client "SEEDS its local pin file from it" onboarding. * The cache stays purely as the round-trip saver described above. */ private readPinManifestObject; /** * Build the successor `recipient_pin_manifest` object from the currently- * effective PREDECESSOR manifest plus a batch of receipted updates (D197 * full-map conservation: `next_map = prior_map + receipt-authorized * additions/replacements`). Shared by {@link publishPinManifestUpdate} (a * single-entry ordinary-push publish) and {@link rotateEpoch}'s * `pending_confirmations` fold (a multi-entry publish riding the SAME * head as the rotation, D196). Validates EVERY update's receipt against * `prior` before building — a receipt issued against a stale predecessor * fails closed here (`VALIDATION_FAILED`), never silently overwritten, * matching the gateway's own admission-time field-by-field check. * * Does NOT admit anything — the caller uploads `.upload` and attaches the * returned `{object_kind, pin_manifest_version, stored_bytes_sha256, * size_bytes}` receipt shape to whichever head it is publishing on. */ private buildPinManifestUpdate; /** * Build a `recipient_pin_manifest` update (D197 full-map conservation) and * publish it via an ORDINARY head (`gitvault.writer`-sufficient — the * OWNER-GATED half of the ceremony already happened at `/confirm`/`/repin`, * which is what produced `receipt`; PUBLISHING the resulting entry is * ordinary-writer authority, same split as envelope-wrap authority * always had). Carries the SAME refs/roots forward unchanged, at the * CURRENT epoch (this is not a rotation — no new epoch, no new K_e). * * **This is an ORDINARY admission (`transition: null`) and is therefore * itself refused `EPOCH_ROTATION_REQUIRED` while a migration/revocation/ * exposure condition is outstanding on this vault (D193) — the exact * deadlock the incident behind {@link GitvaultVault.rotateEpoch}'s * `pending_confirmations` parameter closes.** When this vault is in that * state, fold the receipt into `rotateEpoch({..., pending_confirmations: * [{principal_id, ek_fingerprint, receipt}]})` instead of calling this * method directly — that submission carries a `rotate_epoch` transition, * which IS the gate's own escape valve. */ publishPinManifestUpdate(input: { principal_id: string; ek_fingerprint: string; confirmed_by: "operator_confirmation"; receipt: GitvaultRecipientConfirmationReceipt; }): Promise; /** * Drive one epoch rotation to a committed head (D193-D203). This is the * producer's obligations in full: sample a FRESH `K_e` independent of * every prior epoch key this principal has locally held; compute the H * bijection from the live desired-recipient state + the effective pin * manifest; seal one `key_envelope` per included recipient from the SAME * `K_e`; submit the create-only `rotation_attempt_descriptor` BEFORE any * envelope upload; upload the envelopes; submit the `rotate_epoch` head; * on a CAS conflict, retry from a fresh `materialize()` (the SAME * conflict-retry shape `push()` uses); after commit, verify this * principal's OWN envelope (when it is itself a recipient) opens to * exactly the committed `K_e` and reproduces `epoch_key_commitment` * (D200's narrowed per-recipient self-check — never a global proof); * advance the local keystore's current epoch/key pointer AND retain the * prior key in `epoch_keys`. * * **`recipient_state_version`/`recipient_revocation_version` are REQUIRED * inputs, not discovered here.** The gateway exposes NO general read route * for `internal.gitvault_recipient_state_counters` (D194) — the ONLY * client-visible read of these two org-scoped counters today is the * response of `POST …/recipients/:principal_id/key-revocation` * ({@link GitvaultTransport.declareRecipientKeyRevoked}), which is why * {@link rotateEpochForKeyRevocation} (below) is the one fully * self-contained entry point. For `member_removed` / `elective_rekey` / * `epoch_secret_exposed`, a caller that does not already know the current * counter pair cannot discover it from any shipped gateway route — this * is a confirmed gap in the live gateway (verified against * `packages/gateway/src/services/gitvault/reads.ts:getVaultRecord` and * every `routes/gitvault*.ts` handler, not inferred), not a client * limitation this SDK can work around. Passing a stale/guessed pair fails * safely: the admission fence's D194 frozen-counter comparison refuses * `RECIPIENT_SET_MISMATCH` rather than silently canonizing against wrong * evidence. * * **`options.pending_confirmations` (the manifest-publish deadlock fix).** * `publishPinManifestUpdate` is an ORDINARY admission (`transition: * null`) and is therefore itself refused `EPOCH_ROTATION_REQUIRED` while * this vault has an urgent/migration condition outstanding — the exact * state a `rotateEpoch` call is being made to clear. On an * `epoch_secret_exposed` rekey `/confirm` mints a receipt server-side, * but the ordinary push that would publish it never admits. Pass the * pending receipted updates * here instead — they ride the SAME head as this rotation's `transition`, * which IS `EPOCH_ROTATION_REQUIRED`'s own escape valve, so the publish * is no longer blocked. **This does NOT include these principals in * THIS rotation's `envelopes[]`** — protocol-v0.md D196 is explicit: "a * manifest update riding the SAME head never self-authorizes its own * recipients"; `confirmed(h)` for THIS rotation still reads only the * PREDECESSOR manifest, unchanged. They land in * `excluded_unconfirmed_principal_ids` here (same as without folding) and * become eligible starting at the NEXT rotation, once this manifest is * the admitted predecessor. See {@link GitvaultRotationResult.pin_manifest_published}. * * **Honest residual — this does not rescue a vault with ZERO ever- * confirmed principals.** If `included` would be empty even with the * fold (no predecessor-confirmed principal exists at all — e.g. a * grandfathered pre-rev-42 vault whose bare genesis never published a * pin manifest and which was never bootstrapped before its * `migration_rotation_required` flag was set), the gateway refuses * `EPOCH_ROTATION_WOULD_LEAVE_VAULT_UNCOVERED` regardless of what rides * along on `pin_manifest` — D196's same-head exclusion makes THIS * impossible to route around from the client. That is a genuine, * currently-open protocol gap (not something this parameter can paper * over) and needs an operator-side decision, not a client workaround. */ rotateEpoch(options: { reason: GitvaultRotationReason; recipient_state_version: string; recipient_revocation_version: string; client_idempotency_key?: string; ikm_e?: Uint8Array; pending_confirmations?: { principal_id: string; ek_fingerprint: string; receipt: GitvaultRecipientConfirmationReceipt; }[]; /** * gitvault-multi-writer (task 5.9, D7/D228) — an outstanding * gateway-blocked writer set (`ineligible_members`, checked fresh on * every call — see the `writer_set_update` fold-in below) is ALWAYS * folded into this rotation automatically; this flag is the EXPLICIT * owner + step-up acknowledgment D7 requires ONLY when that fold-in * would empty the vault's writer set entirely (the sole surviving * writer was itself gateway-blocked) — the declared read-only terminal. * Every OTHER case that would empty the writer set is refused * `EPOCH_ROTATION_WOULD_LEAVE_VAULT_UNCOVERED` regardless of this flag; * it widens nothing beyond that one specific, named exception. */ force_empty_writer_set?: boolean; }): Promise; /** * The writer-capable rotation that completes an org membership removal * (gitvault-multi-writer D6: "`member_removed` keeps its automatic * writer-capable path"). The removal itself already advanced the org's * D194 counters and flipped the member to `pending_removal`, so there is * nothing to declare: read the counters off the envelope-recipients read * (the same read the H-partition uses) and rotate under * `reason:"member_removed"`, which needs `gitvault.writer` only — any * surviving writer can run it, no owner step-up. Refuses * `GITVAULT_ROTATION_COUNTERS_UNAVAILABLE` on a gateway that does not yet * carry the counters on that read. */ rotateEpochForMemberRemoval(options?: { client_idempotency_key?: string; }): Promise; /** * The ONE fully self-contained rotation entry point: declares * `reason:"recipient_key_revoked"` (owner + step-up) for `principal_id`, * takes the D194 counters straight off that call's OWN response, and * drives the rotation with them — no external counter source needed. */ rotateEpochForKeyRevocation(principalId: string, options?: { client_idempotency_key?: string; }): Promise; /** * Submit a REF-NEUTRAL `add_writer_key{authorization.kind:"handoff"}` * activation head — no ref/checkpoint/repair changes ride it (protocol * §4.17's own "ref-neutral head shape... for the activation head" rule), * built by `publishGeneration` the SAME way every ordinary no-op push * would be (`refs`/`head_target` carried forward from `base` UNCHANGED), * just with `transition` set. Retries `HEAD_CAS_CONFLICT` like every * other publish path here. The caller (`resume()`) has already built and * locally sanity-checked the grant + acceptance; this method only reads * the FRESHLY-verified `writer_set_pin` (set by the `materialize()` this * same call performs, mirroring `push()`'s own base-then-retry shape) to * compute `base_writer_set`/`next_writer_set`, signs the head under * `this.signer()` (the ADDED key itself — protocol §4.17's "head signed * by the added key" — since `signer()` reads straight from THIS * keystore's identity, and `resume()` mints/loads that identity before * ever reaching this call), and advances the local `writer_set_pin` on * success so a subsequent read reflects the new writer without a second * chain walk. * * Crash-resumable (task 5.6): the chain burns `handoff_id` single-use, so * a naive resubmission after a crash between a prior attempt's successful * admission and this checkout learning about it would be refused * VALIDATION_FAILED. Returns `{outcome:"already_admitted", generation}` * instead of submitting anything whenever the freshly-verified writer set * ALREADY contains `input.addedWriterKeyId` — the caller treats both * outcomes as success (the writer IS active either way) and proceeds. */ submitWriterActivationHead(input: { addedWriterKeyId: string; addedSigningPubkeyB64u: string; addedPrincipalId: string; handoffId: string; grant: Record; acceptance: Record; }): Promise<{ outcome: "activated"; result: GitvaultPublishResult; } | { outcome: "already_admitted"; generation: string; }>; /** * gitvault-multi-writer (task 5.7) — the "writer"-door twin of {@link * submitWriterActivationHead}: an ALREADY-ACTIVE writer (this vault * session's own key) admits `input`, an eligible org member with a * published signing key but no writer standing yet, via a fresh * `add_writer_key{"writer"}` head. No grant/acceptance — the carrying * head's own signer being an active writer at the predecessor generation * IS the authorization (`validateAddWriterKeyPayload`'s `"writer"` * branch), re-verified admission-side by `checkTransitionAdmissible`. * Task 5.8 adds the SAME local pre-check every other head-signing path * gets: if THIS session's own key is not (or is no longer) an active * writer, refuse EARLY and LOCALLY (`GITVAULT_WRITER_NOT_ADMITTED` / * `GITVAULT_WRITER_REMOVED`) rather than let the head reach the gateway * only to be refused there — redundant with `reconcileWriterAdmissions`'s * own upfront gate for that caller, but this method is public and a * direct caller should get the same fast, specific refusal. */ admitPendingWriter(input: { addedWriterKeyId: string; addedSigningPubkeyB64u: string; addedPrincipalId: string; }): Promise<{ outcome: "activated"; result: GitvaultPublishResult; } | { outcome: "already_admitted"; generation: string; }>; /** * gitvault-multi-writer (task 5.7) — the vault's OWN writer-admission * reconcile: reads `pending_writers[]` off the vault record (eligible org * members with no writer standing yet), resolves each candidate's * published signing key off the org's encryption-key directory (the SAME * `/orgs/v1/:org_id/encryption-keys` row {@link * reconcileEnvelopeRecipients} reads, widened D9 to also carry the * signing half), and admits each via {@link admitPendingWriter} — one * `add_writer_key{"writer"}` head per candidate, sequentially: each * admission materializes fresh against the PRIOR one's own updated writer * set, so there is no batched/parallel form. * * Requires THIS session's own key to already be an active writer (the * chain's own authorization rule for the "writer" door) — checked ONCE, * upfront, off the locally-pinned `writer_set_pin` (no network call), * rather than discovered N times over from N identical gateway refusals: * a session that reached this call already pushed successfully as an * active writer in every wired call site (push/snapshot/deploy) OR is a * member that simply is not one yet (session-start/read) — the SAME * "not yet admitted" case {@link Gitvault.push}'s own pre-push check * (task 5.8) names, not a new refusal shape. Returns `{eligible: false}` * with every other field empty (never throws) for that case: every OTHER * `pending_writers[]` entry would fail identically, so there is nothing * this call can usefully do, and "I am not a writer" is not this vault's * fault — but it is distinguishable from "there was nothing pending" * (`eligible: true`, still all empty), which matters to a caller trying * to explain an all-empty result to a human. */ reconcileWriterAdmissions(): Promise; /** * Publish a repair head over `base_generation` (§4.3): superseded tips that * the repaired state no longer reaches enter the retention-root map with * `dropped_at_generation = the repair generation`; the head carries the * mandatory self-contained checkpoint. Coverage that cannot be built → * `REPAIR_TARGET_UNPRESERVABLE`. A repair never crosses an admitted transition. */ repair(input: { base_generation: string; reason: GitvaultRepairDescriptor["reason"]; }): Promise; /** Heads `base..newest` (already chain-verified by `verifyToNewest`) re-read + hash-checked from storage. */ /** * Re-read heads `baseGeneration..newest` and hash-check each against the * verified chain. `transitions` says what an admitted transition head on * the walk means: a repair walks with `"refuse"` (a repair never crosses * one), a whole-chain read such as compaction planning walks with * `"activated"` (`verifyToNewest` already validated every head, so the * only rule left is {@link assertNoTransition}'s fail-closed one — a * `rotate_epoch` or `add_writer_key` head is ordinary chain state a * rev-47 vault with any handoff or member change carries). */ private chainFrom; /** * Pull the newest checkpoint (if any) and every later WAL pack into * `targetRepoDir` (an initialized repository), then verify every canonical * ref + the HEAD target resolves. Returns the materialized refs. * * Design D5 (gitvault-client-round-trips): incremental above the local * `restored_through` marker (this target directory's own local git * config, read/written by {@link readGitvaultRestoreMarker}/{@link * writeGitvaultRestoreMarker}) — replaying only the WAL packs of * generations above it — WHEN the walk from newest back to the marker is * plain WAL the entire way. The moment that walk crosses a * checkpoint-bearing, repair, or transition head (checked BEFORE * including a head, so the marker-boundary and wholesale-boundary checks * share one pass), this falls back to the ORIGINAL wholesale walk below — * re-fetching any head already visited during the aborted attempt is a * cache hit (design D3), never a second network round trip. Coverage * verification and retained-refs reconciliation run UNCHANGED on both * paths; the marker only advances after they both succeed. */ restoreObjectsInto(targetRepoDir: string, reuse?: { marker: GitvaultRestoreMarker | null; state: GitvaultMaterializedState; }): Promise<{ refs: GitvaultRefMap; head_target: GitvaultHeadTarget; generation: string; retained_refs: GitvaultRetainedRefsReconcileResult; }>; /** * The shared restore tail (gitvault-restore-recipe): apply `heads`' * checkpoint (if any) + WAL packs into `targetRepoDir`, verify §4.7 * coverage, reconcile retained refs, and advance the marker — IDENTICALLY * whether `heads` came from {@link restoreObjectsInto}'s own backward walk * or a verified restore plan ({@link tryConsumeRestorePlan}). `incremental` * only affects the checkpoint-coverage LEARNING step (an incremental walk * stops at the marker and learns nothing; a plan-derived call always * passes `false` — a plan is the wholesale shape by construction). * * `precheckedCheckpoint`, when non-null, is a claim set + manifest the * caller ALREADY verified (signature, cross-equality — {@link * verifyRestorePlan}) — skips the network fetch + re-verify this method * would otherwise run for `heads[0].checkpoint`. `planPacks`, when * non-null, is a map of pack bytes the caller already has (untrusted — * only used when its OWN hash matches the carrying head's/manifest's * receipt, exactly `stateDeltaPacks`'s discipline), keyed * `${object_kind}:${object_id}` so it covers BOTH WAL and checkpoint * packs (`stateDeltaPacks` never carries checkpoint packs — a delta span * never crosses a checkpoint boundary). Both are `null` on the ordinary * walk path, in which case every line below is byte-identical to the * pre-gitvault-restore-recipe shape. */ /** * gitvault-multi-writer (rev 47): the key a head's own checkpoint claim set * and carriers were signed under — the head's writer, resolved against the * keystore's persisted `writer_set_pin` (maintained by the chain walk that * always precedes a restore), falling back to `fallback` (the genesis * creator) for a pre-rev-47 keystore or a writer the pin no longer lists. * Before this, a restore verified every checkpoint under the creator key * and a clone of any vault whose newest checkpoint was minted by a handoff * recipient died CHECKPOINT_INCOMPLETE "claim set signature fails". */ private writerKeyForHead; private applyRestoreHeads; /** * `newest.epoch_keys_hex[epoch]`, decoded — the D194 per-carrying-head key * lookup shared by {@link applyRestoreHeads} and {@link verifyRestorePlan}. * `newest.epoch_keys_hex` is the FULL map `materialize()` resolved * (throwing `GITVAULT_EPOCH_NOT_OPENABLE` fail-closed if any needed epoch * could not be opened), so every lookup through this helper is guaranteed * present. */ private epochKeyFor; /** * Verify a restore plan's heads (self-consistency + backward chain-link + * cross-check against the caller's own already-verified `newest`, plus * genesis/boundary linkage) and, when the boundary carries a checkpoint, * its claim set + manifest (signature, cross-equality) — the SAME * obligations {@link applyRestoreHeads}'s ordinary path runs, against * plan-supplied bytes instead of network-fetched ones (design D5: "the * plan is transport, never trust"). Returns `null` on ANY failure — a * self-inconsistent head, a broken link, a wrong newest, a bad signature, * a cross-equality mismatch, anything the reused `decodeCarrierFrame`/ * `checkClaimSetEquality` reject — rather than throwing, so the caller's * fallback to the ordinary walk is unconditional and silent, mirroring * `assembleVaultStateDelta`'s own disqualification posture server-side. * * Heads that verify their OWN chain link (Stage A) are cache-warmed into * {@link walkPrefetch} BEFORE Stage B (the checkpoint) runs, so a * checkpoint failure still leaves the fallback walk with every head this * call already proved — the D3 cache-hit discipline, extended to a failed * plan. */ private verifyRestorePlan; /** * Consume a stashed restore plan (gitvault-restore-recipe design D1-D6): * verify it ({@link verifyRestorePlan}) and, on success, apply it via the * SAME shared tail the backward walk uses ({@link applyRestoreHeads}) — * `null` on absence or ANY verification failure, the caller's cue to run * (or continue) the ordinary walk. Clears `this.stateRestorePlan` * unconditionally: a plan is single-use whether it verified or not (a * failed plan's USABLE heads already rode into `walkPrefetch` inside * `verifyRestorePlan` itself). */ private tryConsumeRestorePlan; /** Thin passthrough to the transport — see {@link GitvaultTransport.openCompactionGrant}. */ openCompactionGrant(): Promise; /** Thin passthrough to the transport — see {@link GitvaultTransport.closeCompactionGrant}. Always safe best-effort; never throws by construction of the route (idempotent). */ closeCompactionGrant(): Promise<{ closed: boolean; }>; } /** §4.7 cross-field equality: covers_through agree; the claim set's ordered pack ids/hashes/sizes/total equal the manifest's (shared stored fields only). */ export declare function checkClaimSetEquality(claimSet: GitvaultCheckpointClaimSet, manifest: GitvaultCheckpointManifest, headCoversThrough: string): void; /** Convenience for the deploy lane: the §6.5 capture binding. */ export declare function captureBinding(captureId: string, applyPlanSha256: string | null, snapshotOidHmac: string): GitvaultCaptureBinding; export type { GitvaultRefUpdate }; //# sourceMappingURL=gitvault-publication.d.ts.map