run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
2,632 lines • 143 kB
TypeScript
/**
* vault — 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 VaultTransport} — the control-plane + bucket operations the
* vault needs (extends task 5.3's creation transport) — and
* {@link createVaultHttpTransport}, the `fetch`-backed implementation
* over the SDK kernel (upload sessions → presigned PUT with
* `If-None-Match: *` → finalize);
* - {@link Vault} — 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 VaultMirrorBackend } from "./vault-mirror-backend.js";
import type { VaultMirrorCredential, VaultMirrorDestination } from "./vault-mirror-config.js";
import { type VaultCheckpointStaleness } from "../namespaces/repos.js";
import type { VaultActivationToken, VaultCaptureBinding, VaultCaptureReceipt, VaultCheckpointBlock, VaultCheckpointClaimSet, VaultCheckpointManifest, VaultDigestLabel, VaultHead, VaultHeadTarget, VaultHeadsListingPage, VaultHeadsListingRequest, VaultOpenReceipt, VaultRecipientConfirmationReceipt, VaultRefState, VaultRefTransaction, VaultRefUpdate, VaultRepairDescriptor, VaultRetentionCutoff, VaultRetentionCutoffReceipt, VaultRetentionRoot, VaultRetentionRoots, VaultRetentionRootsReceipt, VaultRotateEpochPayload, VaultRotationAttemptDescriptor, VaultRotationReason, VaultGenesis } from "../namespaces/vault.types.js";
import type { VaultCreationTransport } from "./vault-creation-journal.js";
import { VaultKeystore, type VaultRepoFile } from "./vault-keystore.js";
import type { VaultPruneIntentRecord } from "./vault-prune.js";
export declare const VAULT_MAX_CANONICAL_REFS = 10000;
export declare const VAULT_MAX_REF_UPDATES_PER_TRANSACTION = 1000;
export declare const VAULT_MAX_RETENTION_ROOT_ENTRIES = 50000;
export declare const VAULT_MAX_REPAIR_ADDED_ROOTS = 10000;
export declare const VAULT_MAX_WAL_RECEIPTS_PER_HEAD = 64;
export declare const VAULT_MAX_CHECKPOINT_PACKS = 4096;
export declare const VAULT_MAX_CHECKPOINT_TOTAL_STORED_BYTES = 858993459200n;
export declare const VAULT_MULTI_OBJECT_PACK_TARGET_BYTES = 201326592;
export declare const VAULT_MAX_REF_STATE_OBJECT_BYTES = 33554432;
export declare const VAULT_MAX_HEADS_PER_LISTING_PAGE = 1000;
export declare const VAULT_VERIFICATION_BUDGET_HEADS = 100000;
export declare const VAULT_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 VAULT_PUSH_CONFLICT_RETRIES = 5;
/**
* `run402@<version>/<entrypoint>` — 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 vaultReaderEntrypoint(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 VaultRefMap = Record<string, string>;
/** A tip that left the canonical map in this transaction — it enters `retention_roots` in the same generation. */
export interface VaultDroppedTip {
ref: string;
oid: string;
reason: "deleted" | "force_displaced";
}
export interface VaultRefTransactionEvaluation {
refs: VaultRefMap;
dropped: VaultDroppedTip[];
}
export interface VaultEvaluateRefTransactionOptions {
/** Ancestry oracle: true iff `ancestor` is reachable from `descendant`. */
isAncestor: (ancestor: string, descendant: string) => Promise<boolean> | 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 VaultRefUpdateFailure {
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: VaultRefMap, transaction: VaultRefTransaction, options: VaultEvaluateRefTransactionOptions): Promise<VaultRefTransactionEvaluation>;
/** §6.5 bound: ≤ 10 000 canonical refs and the serialized map ≤ 32 MiB. */
export declare function assertRefMapCardinality(refs: VaultRefMap): void;
/** The §4.4 deploy-ref move: force-with-lease `refs/run402/deploys/latest` → `oid` (creation when absent). */
export declare function deployRefTransaction(current: VaultRefMap, oid: string): VaultRefTransaction;
/** `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 VaultEvolveRootsOptions {
/** 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: VaultRetentionRoot, b: VaultRetentionRoot): 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: VaultRetentionRoot[], options: VaultEvolveRootsOptions): VaultRetentionRoot[];
/**
* The client-local, protocol-owned namespace (design D4). Distinct from the
* VAULT-side `refs/run402/*` (e.g. `VAULT_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 VAULT_R402_REF_NAMESPACE = "refs/r402/";
/** Where a retained (branch-unreachable) deploy-capture tip gets its local ref (design D1/D2). */
export declare const VAULT_RETAIN_REF_PREFIX = "refs/r402/retain/";
/**
* D1's ref-identity choice, recorded here because the design doc's own
* assumption did not hold: `VaultRetentionRoot` carries no per-capture
* stable id (only `{ref, oid, dropped_at_generation}`) — the capture id that
* DOES exist (`VaultHead.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 vaultRetainedRefName(oid: string): string;
export interface VaultRetainedRefsReconcileResult {
/** `refs/r402/retain/<oid>` 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/<oid>` 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 Vault.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 VAULT_PACK_SCRATCH_PREFIX = ".run402-vault-packs-";
/**
* Allocate the pack-objects scratch directory on the OBJECT STORE's own
* filesystem: `<git-common-dir>/.run402-vault-packs-XXXXXX`, never under
* `os.tmpdir()`.
*
* `git pack-objects <base>` writes its temporary pack under
* `<git-common-dir>/objects/pack/` and then `rename(2)`s it onto
* `<base>-<hash>.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<string>;
export declare function reconcileRetainedTipRefs(repoDir: string, state: {
refs: VaultRefMap;
roots: readonly VaultRetentionRoot[];
head_target: VaultHeadTarget;
}): Promise<VaultRetainedRefsReconcileResult>;
export interface VaultListingProgress {
/** 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: VaultHeadsListingRequest): 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 → `VAULT_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: VaultHeadsListingPage, request: VaultHeadsListingRequest, progress: VaultListingProgress, expectedRepoId?: string): VaultListingProgress;
/**
* 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: VaultHeadsListingRequest, page: VaultHeadsListingPage): VaultHeadsListingRequest | 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 VaultOpenBindingRecord {
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: VaultOpenBindingRecord): 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
* `VAULT_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: VaultOpenBindingRecord, issuanceOpenBindingSha256: string, options?: {
retry?: boolean;
}): void;
export interface VaultChainLinkInput {
head: VaultHead;
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) — `VAULT_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: VaultChainLinkInput): 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
* (`Vault.verifyToNewest`) handle it fully. `add_writer_key` is
* EXEMPT as of rev 47 (vault-multi-writer) the same way — {@link
* parseAddWriterKeyPayload} + `validateAddWriterKeyPayload`
* (`vault-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: VaultHead): void;
/** One object to upload — identity fixed BEFORE the PUT; the receipt is compared against it. */
export interface VaultUploadObject {
/**
* 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 VaultUploadReceipt {
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 `vaultPaths` strings onto that identity so the rest of
* the vault can keep addressing objects the way §3 describes them.
*/
export type VaultWireRef = {
kind: "head";
generation: string;
} | {
kind: "admission";
generation: string;
} | {
kind: "object";
read: VaultObjectReadRequest;
};
/** One entry of a `POST …/object-reads` batch. */
export interface VaultObjectReadRequest {
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 vaultWireRefForPath(path: string): VaultWireRef | null;
/** The manifest entry for one upload — closed-key, exactly what the control plane validates. */
export declare function vaultManifestEntry(object: VaultUploadObject): VaultObjectReadRequest & {
sha256: string;
size_bytes: string;
base_generation?: string;
};
/** Mirrors the gateway's `VAULT_INLINE_UPLOAD_MAX_OBJECT_BYTES` (`services/vault/upload-sessions.ts`). */
export declare const VAULT_INLINE_UPLOAD_MAX_OBJECT_BYTES = 262144;
/** Mirrors the gateway's `VAULT_INLINE_UPLOAD_MAX_REQUEST_BYTES`. */
export declare const VAULT_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 (`VaultUploadObject[]`
* 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 vaultInlineUploadEligible(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/vault/epoch-rotation.ts) exactly; drift here breaks receipt
* pairing at upload finalize for a rotation-attempt envelope or a pin
* manifest.
*/
export declare function vaultLedgerId(read: VaultObjectReadRequest): string;
export interface VaultAdmitHeadRequest {
repo_id: string;
generation: string;
stored_bytes: Uint8Array;
stored_bytes_sha256: string;
}
export type VaultAdmitHeadResult = {
outcome: "admitted";
admission_record_sha256: string;
capture_receipt: VaultCaptureReceipt | null;
} | {
outcome: "conflict";
winner: {
generation: string;
stored_bytes_sha256: string;
};
};
export interface VaultRetentionCutoffIssued {
ticket: VaultRetentionCutoff;
receipt: VaultRetentionCutoffReceipt;
}
/**
* 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 VaultResourceBinding = {
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 VaultMaintenanceLeaseRequest {
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 VaultMaintenanceLease {
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 (vault-checkpoint-cadence design D3). */
export interface VaultCompactionGrant {
/**
* 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 VaultTransport extends VaultCreationTransport {
/**
* 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 (vault-
* 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 VaultCreationTransport.getObject}
* gives for one object. Callers that need exactly one object still use
* {@link VaultCreationTransport.getObject} — this is for the plural
* case only, so a single-object caller never pays a batch's overhead.
*
* `expected` (vault-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<string | undefined>;
}): Promise<Array<Uint8Array | null>>;
/**
* OPTIONAL per-object settlement over the SAME batch shape as
* {@link getObjects} (vault-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`
* (vault-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<string | undefined>;
}): Promise<Array<Promise<Uint8Array | null>>>;
/**
* Read the EXACT stored bytes of many generation-addressed heads in ONE
* POST (`…/head-reads`, vault-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<Uint8Array[] | null>;
listHeads(request: VaultHeadsListingRequest & {
repo_id: string;
}): Promise<VaultHeadsListingPage>;
/**
* Session → create-only presigned PUTs (`If-None-Match: *`) → finalize;
* receipts in request order.
*
* vault-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: VaultUploadObject[];
resource_binding?: VaultResourceBinding;
byo?: {
destination: VaultMirrorDestination;
credential?: VaultMirrorCredential;
};
}): Promise<VaultUploadReceipt[]>;
admitHead(request: VaultAdmitHeadRequest): Promise<VaultAdmitHeadResult>;
requestRetentionCutoff(request: {
repo_id: string;
base_head_sha256: string;
}): Promise<VaultRetentionCutoffIssued>;
exchangeActivationToken(request: {
repo_id: string;
operation_id: string;
capture_receipt: VaultCaptureReceipt;
}): Promise<VaultActivationToken>;
submitOverrideCompletion(request: {
repo_id: string;
operation_id: string;
capture_receipt: VaultCaptureReceipt;
}): Promise<{
cleared: boolean;
}>;
/** The vault record — policy, allocation generation, storage + maintenance state (§9.2). */
getVaultRecord(request: {
repo_id: string;
}): Promise<VaultRecord>;
/**
* `GET …/state` (vault-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
* VaultCreationTransport.getObject} gives for one object — the caller
* treats it identically, never a route-level distinction).
*
* {@link Vault} 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` (vault-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` (vault-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 Vault}
* 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<VaultState>;
/** Resolve a project's vault without local state (the cold-restart entry point). */
findVaultByProject(request: {
project_id: string;
}): Promise<VaultRecord>;
/**
* Resolve a vault by its address-form `org-slug/name` (repo-first-onramp
* task 4.3, design D6) — `GET /vaults/v1?repo=<org-slug>/<name>`.
* `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<VaultRecord>;
acquireMaintenanceLease(request: VaultMaintenanceLeaseRequest): Promise<VaultMaintenanceLease>;
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` (vault-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 `VAULT_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<VaultCompactionGrant>;
/**
* `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<VaultPruneIntentRecord & {
stored: boolean;
}>;
/** `GET …/prune-intents/:id` — the intent's state and, once signed, its completion. */
getPruneIntent(request: {
repo_id: string;
prune_intent_object_id: string;
}): Promise<VaultPruneIntentRecord | null>;
/**
* The org's directory of envelope-capable principals (vault-human-
* envelopes design D7, `GET /orgs/v1/:org_id/encryption-keys`) — every
* active human member who has published an encryption key. Read by
* {@link Vault.reconcileEnvelopeRecipients} to diff against a
* vault's current recipient set.
*/
listOrgEncryptionKeys(request: {
org_id: string;
}): Promise<VaultOrgEncryptionKeyDirectory>;
/**
* The `ek_` fingerprints already covering this vault, at any epoch
* (`GET /vaults/v1/: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<VaultEnvelopeRecipientsResponse>;
/**
* `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: VaultRotationAttemptDescriptor;
}): Promise<{
rotation_id: string;
descriptor: VaultRotationAttemptDescriptor;
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<VaultRecipientConfirmationReceipt>;
/** `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<VaultRecipientConfirmationReceipt>;
/** `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
* `VAULT_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: VaultOpenReceipt;
deduplicated: boolean;
}>;
}
/**
* One row of the org's envelope-capable-principal directory
* ({@link VaultTransport.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 VaultEnvelopeRecipientsResponse}:
* 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 Vault.reconcileEnvelopeRecipients},
* never a hardcoded assumption or a thrown error.
*/
export interface VaultOrgEncryptionKeyEntry {
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;
/**
* vault-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 VaultTransport.listOrgEncryptionKeys}'s result. */
export interface VaultOrgEncryptionKeyDirectory {
org_id: string;
keys: VaultOrgEncryptionKeyEntry[];
}
/**
* 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 vault
* 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 VaultDesiredRecipientEntry {
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 /vaults/v1/:vault_id/envelope-recipients` — {@link VaultTransport.listEnvelopeRecipients}'s result. */
export interface VaultEnvelopeRecipientsResponse {
vault_id: string;
recipient_fingerprints: string[];
/**
* The org's desired-recipient state, cross-referenced against this
* vault's coverage — see {@link VaultDesiredRecipientEntry}. 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?: VaultDesiredRecipientEntry[];
/** 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 Vault.rotateEpochForMemberRemoval}). OPTIONAL:
* absent on a gateway that predates them.
*/
recipient_state_version?: string;
recipient_revocation_version?: string;
}
/** `GET /vaults/v1/:vault_id` — the shape `reads.ts:getVaultRecord` returns. */
export interface VaultRecord {
repo_id: string;
project_id: string;
org_id: string;
vault_policy: "required" | "grandfathered" | null;
vault_policy_version: string;
vault_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;
/**
* vault-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<string, string>;
};
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;
};
/**
* vault-multi-writer (rev 47) task 3.7 — the writer chain state's read
* surface, mirrored client-side from the gateway's `VaultRecord` (see
* `services/vault/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: VaultWriterSetEntry[];
};
pending_writers?: VaultPendingWriter[];
ineligible_members?: VaultIneligibleMember[];
/** `vault_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;
/**
* vault-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).
* vault-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 VaultRecord.writer_set} — the on-chain shape only; `gateway_blocked_at` (gateway-only) rides {@link VaultIneligibleMember} instead. */
export interface VaultWriterSetEntry {
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 VaultRecord.writer_set}'s `writers` — a candidate for the "writer"-door `add_writer_key` ({@link Vault.reconcileWriterAdmissions}). */
export interface VaultPendingWriter {
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 VaultIneligibleMember {
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 VaultTransport.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 VaultState {
vault: VaultRecord;
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 (vault-delta-fetch); absent otherwise, including on every older gateway. */
delta?: VaultStateDelta | null;
/** Present only when the caller declared `restore: true` AND the gateway located a qualifying checkpoint boundary (vault-restore-recipe); absent otherwise, including on every older gateway. */
restore_plan?: VaultRestorePlan | null;
}
/**
* The state read's restore recipe (vault-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
* Vault} 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 VaultRestorePlan {
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 (vault-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 VaultStateDelta {
heads: Array<{
generation: string;
stored_bytes: Uint8Array;
stored_bytes_sha256: string;
}>;
packs: Array<{
object_id: string;
bytes: Uint8Array;
}>;
}
export interface VaultHttpTransportOptions {
/** Wire shape: every vault-scoped route is `/vaults/v1/:vault_id/...`; `vault_id` is the `repo_id` unless a mapping is supplied (D185). */
vaultIdFor?: (repoId: string) => string;
/**
* vault-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/<repo_id>.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
* `VAULT_BYO_NOT_CONFIGURED` by name — never a silent "absent". Absent
* option: same as `null`. Managed vaults never consult it.
*/
byoBackend?: (repoId: string) => VaultMirrorBackend | null;
/**
* vault-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 `vault-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 `VaultKeystore.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 vault 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 VAULT_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<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
/**
* 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 createVaultHttpTransport(client: Client, options?: VaultHttpTransportOptions): VaultTransport;
export declare const vaultPaths: {
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/<vr>.json` — plaintext-structured, uploaded before a prune intent may reference it (§7.3). */
readonly verifierReceipt: (id: string) => string;
/**
* `envelopes/<epoch>/<recipient_fingerprint>` — mirrors
* `vault-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/<epoch>/<rotation_id>/<recipient_fingerprint>` — 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/<pin_manifest_version>.json` (D197, rev 42) — version-addressed, plaintext-structured, writer-signed. */
readonly pinManifest: (pinManifestVersion: string) => string;
};
export interface VaultRestoreMarker {
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 readVaultRestoreMarker(targetRepoDir: string): Promise<VaultRestoreMarker | null>;
/** 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 VAULT_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 readVaultAutoGcThreshold(targetRepoDir: string): Promise<number>;
/** Set this checkout's auto-gc threshold. `0` disables auto-gc entirely. */
export declare function writeVaultAutoGcThreshold(targetRepoDir: string, generations: number): Promise<void>;
/**
* {@link Vault.ensureRepoState}'s report when a cold keystore was
* restored from its own envelope (vault-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 VaultColdOpenResult {
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 VaultOptions {
keystore: VaultKeystore;
transport: VaultTransport;
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 VaultCutoffOptions {
/**
* `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 Vault.verifyToNewest} walked over, KEYLESS (structural only — no envelope opened yet). */
export interface VaultEncounteredRotation {
/** The rotate_epoch head's own generation. */
generation: string;
/** == `payload.new_epoch` == the head's own `epoch`. */
epoch: string;
payload: VaultRotateEpochPayload;
/**
* vault-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 `VAULT_EPOCH_NOT_OPENABLE` / decrypt stop point (Part C: `repos fsck`'s honest `chain_verified_to` vs `decryptable_to` split). */
export interface VaultEpochDecryptFailure {
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 VaultDecryptValidationResult {
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: VaultRefState | null;
retention_roots: VaultRetentionRoots | 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<string, string>;
failure: VaultEpochDecryptFailure | null;
}
export interface VaultVerifiedState {
generation: string;
head_sha256: string;
/** `null` at generation zero. */
head: VaultHead | null;
genesis: VaultGenesis;
/** Every admitted `rotate_epoch` transition walked THIS call, oldest first — keyless (Part A's structural half; D202's join predicate needs only this). */
rotations: VaultEncounteredRotation[];
/** Present iff `decryptValidate` was requested. */
decrypt: VaultDecryptValidationResult | null;
}
export interface VaultMaterializedState extends VaultVerifiedState {
ref_state: VaultRefState | null;
retention_roots: VaultRetentionRoots | null;
refs: VaultRefMap;
roots: VaultRetentionRoot[];
head_target: VaultHeadTarget;
/** 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<string, string>;
}
/** One directory entry {@link Vault.reconcileEnvelopeRecipients} wrapped a fresh `key_envelope` for. */
export interface VaultReconcileEnvelopeRecipientsWrapped {
principal_id: string;
ek_fingerprint: string;
}
/** Why {@link Vault.reconcileEnvelopeRecipients} did NOT wrap a directory entry it otherwise would have. */
export type VaultReconcileEnvelopeRecipientsSkipReason = "missing_public_key" | "invalid_public_key" | "pinned_key_mismatch";
export interface VaultReconcileEnvelopeRecipientsSkipped {
principal_id: string;
ek_fingerprint: string;
reason: VaultReconcileEnvelopeRecipientsSkipReason;
/** `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<string, unknown>;
}
/** {@link Vault.reconcileEnvelopeRecipients}'s full per-recipient breakdown. */
export interface VaultReconcileEnvelopeRecipientsResult {
repo_id: string;
org_id: string;
/** The epoch every wrap in this call used (V0: always {@link VAULT_GENESIS_EPOCH}). */
epoch: string;
/** Directory entries this call itself wrapped a NEW `key_envelope` for. */
wrapped: VaultReconcileEnvelopeRecipientsWrapped[];
/** `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: VaultReconcileEnvelopeRecipientsSkipped[];
}
/** One `pending_writers[]` entry {@link Vault.reconcileWriterAdmissions} admitted via a fresh `add_writer_key{"writer"}` head. */
export interface VaultReconcileWriterAdmissionsAdmitted {
principal_id: string;
writer_key_id: string;
generation: string;
}
/** Why {@link Vault.reconcileWriterAdmissions} did NOT admit a `pending_writers[]` entry it otherwise would have. */
export type VaultReconcileWriterAdmissionsSkipReason = "missing_signing_pubkey" | "invalid_signing_pubkey";
export interface VaultReconcileWriterAdmissionsSkipped {
principal_id: string;
writer_key_id: string;
reason: VaultReconcileWriterAdmissionsSkipReason;
}
/**
* {@link Vault.reconcileWriterAdmissions}'s full per-candidate
* breakdown (task 5.7). Structurally parallel to
* {@link VaultReconcileEnvelopeRecipientsResult} (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 VaultReconcileWriterAdmissionsResult {
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: VaultReconcileWriterAdmissionsAdmitted[];
/** `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: VaultReconcileWriterAdmissionsSkipped[];
}
export interface VaultPushOptions {
transaction: VaultRefTransaction;
/** New `HEAD` target for the published ref_state; carried forward when omitted. */
head_target?: VaultHeadTarget;
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?: VaultCutoffOptions;
/** Built lazily at head-sign time — the deploy lane may still be computing the plan digest. */
capture_binding?: VaultCaptureBinding | (() => Promise<VaultCaptureBinding | null> | VaultCaptureBinding | null);
/**
* A caller-supplied base (vault-client-round-trips design D1) — used
* VERBATIM for the first attempt instead of calling {@link Vault.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?: VaultMaterializedState;
}
export interface VaultPublishResult {
generation: string;
head_sha256: string;
head: VaultHead;
admission_record_sha256: string;
capture_receipt: VaultCaptureReceipt | null;
/** `wal` = direct WAL receipts; `checkpoint` = the delta shipped as a checkpoint set. */
form: "wal" | "checkpoint";
conflicts_retried: number;
refs: VaultRefMap;
/**
* vault-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: VaultCheckpointStaleness;
}
/** {@link Vault.rotateEpoch}'s result (D193-D203, rev 42). */
export interface VaultRotationResult {
outcome: "admitted";
generation: string;
head_sha256: string;
new_epoch: string;
rotation_id: string;
reason: VaultRotationReason;
included: {
principal_id: string;
ek_fingerprint: string;
}[];
excluded_keyless_principal_ids: string[];
excluded_unconfirmed_principal_ids: string[];
admission_record_sha256: string;
capture_receipt: VaultCaptureReceipt | null;
/**
* `"passed"` — this principal is itself an included recipient and its own
* new-epoch envelope opened (via {@link import("../namespaces/vault.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/vault/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;
/**
* vault-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 Vault.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 VaultPushPlan {
/** 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: VaultRefMap;
head_target: VaultHeadTarget;
/** 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: VaultUploadObject["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 Vault.verifyStoredCheckpoint} observed. Every boolean is
* a FINDING, not a promise: a `false` here is what makes a truthful negative
* `verifier_receipt` possible.
*/
export interface VaultStoredCheckpointAttestation {
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 VaultBuiltCheckpoint {
manifest: VaultCheckpointManifest;
claim_set: VaultCheckpointClaimSet;
claim_set_receipt: VaultCheckpointBlock["claim_set"];
objects: VaultUploadObject[];
/** 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 Vault {
#private;
readonly keystore: VaultKeystore;
readonly transport: VaultTransport;
readonly repoId: string;
readonly repoDir: string | null;
private readonly now;
private readonly budget;
/**
* vault-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 (vault-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 (vault-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;
/**
* vault-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: VaultOptions);
static open(options: VaultOptions): Vault;
repoFile(): VaultRepoFile;
/**
* vault-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 `VAULT_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, `VAULT_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 vault 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).
*/
/**
* vault-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<VaultColdOpenResult | null>;
/** {@link open}, but a missing repo file triggers {@link ensureRepoState} first. */
static openOrRestore(options: VaultOptions): Promise<{
vault: Vault;
restored: VaultColdOpenResult | null;
}>;
private kRepo;
private epoch;
/**
* vault-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 (vault-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 vault-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 VAULT_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: VaultGenesis;
sha256: string;
}>;
/**
* vault-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 `VaultKeystore`'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 (vault-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 `VAULT_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<VaultVerifiedState>;
/**
* 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
* `VAULT_EPOCH_NOT_OPENABLE`, never a bare `VAULT_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<VaultMaterializedState>;
/**
* 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 `VAULT_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/<current epoch>/<recipient fingerprint>` 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 VaultOrgEncryptionKeyEntry}), 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. `VAULT_READ_ONLY` when this principal holds no signing key).
* Callers that want "never block on this" (the deploy hook) wrap the call
* themselves — see `Repos.capture`'s `#tryReconcileEnvelopeRecipients`.
*/
reconcileEnvelopeRecipients(): Promise<VaultReconcileEnvelopeRecipientsResult>;
/**
* `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 `VAULT_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;
/**
* vault-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
* `VAULT_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
* `VAULT_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 `VAULT_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<Uint8Array[]>;
/** Sorted unique object ids reachable from `tips` (the `"objectset"` content). */
objectSet(tips: string[]): Promise<string[]>;
/**
* 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<string[]>;
/**
* 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: VaultRetentionRootsReceipt): Promise<VaultRetentionRoots>;
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: VaultDigestLabel, content: unknown): string;
/** Strip the single top-level signature — the commitment preimage shape carriers use. */
digestPreimage<T extends {
signature: string;
}>(o: T): Omit<T, "signature">;
private withoutSignature;
/** Coverage tips (§4.7): canonical refs ∪ unexpired roots ∪ the HEAD target (detached commit; an unborn symref contributes nothing). */
static coverageTips(refs: VaultRefMap, roots: VaultRetentionRoot[], headTarget: VaultHeadTarget): 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: VaultRefState;
retention_roots: VaultRetentionRoots;
}): Promise<VaultBuiltCheckpoint>;
/** §4.7 acceptance: restore from the set ALONE into an empty scratch; every covered ref resolves; fsck connectivity; recompute the three commitments. */
acceptCheckpoint(built: VaultBuiltCheckpoint, refState: VaultRefState, roots: VaultRetentionRoots): Promise<void>;
/**
* 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: VaultHead, headSha256: string): Promise<VaultStoredCheckpointAttestation>;
/**
* 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<Array<{
head: VaultHead;
head_sha256: string;
claim_set: VaultCheckpointClaimSet | null;
}>>;
/**
* vault-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 `VAULT_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: VaultPushOptions): Promise<VaultPublishResult>;
/**
* 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 `Repos.planCapture` in
* `../namespaces/repos.js`, which reports `allocation_needed: true` in
* that case instead of calling this method).
*/
planPush(options: VaultPushOptions): Promise<VaultPushPlan>;
/**
* 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 repos gc`: 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?: VaultCutoffOptions | false;
}): Promise<VaultPublishResult>;
/** 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 VaultRepoFile.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 Vault.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: VaultRecipientConfirmationReceipt;
}): Promise<VaultPublishResult>;
/**
* 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.vault_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 VaultTransport.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/vault/reads.ts:getVaultRecord` and
* every `routes/vault*.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 VaultRotationResult.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: VaultRotationReason;
recipient_state_version: string;
recipient_revocation_version: string;
client_idempotency_key?: string;
ikm_e?: Uint8Array;
pending_confirmations?: {
principal_id: string;
ek_fingerprint: string;
receipt: VaultRecipientConfirmationReceipt;
}[];
/**
* vault-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<VaultRotationResult>;
/**
* The writer-capable rotation that completes an org membership removal
* (vault-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
* `VAULT_ROTATION_COUNTERS_UNAVAILABLE` on a gateway that does not yet
* carry the counters on that read.
*/
rotateEpochForMemberRemoval(options?: {
client_idempotency_key?: string;
}): Promise<VaultRotationResult>;
/**
* 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<VaultRotationResult>;
/**
* 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<string, unknown>;
acceptance: Record<string, unknown>;
}): Promise<{
outcome: "activated";
result: VaultPublishResult;
} | {
outcome: "already_admitted";
generation: string;
}>;
/**
* vault-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 (`VAULT_WRITER_NOT_ADMITTED` /
* `VAULT_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: VaultPublishResult;
} | {
outcome: "already_admitted";
generation: string;
}>;
/**
* vault-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 Repos.capture}'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<VaultReconcileWriterAdmissionsResult>;
/**
* 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: VaultRepairDescriptor["reason"];
}): Promise<VaultPublishResult>;
/** 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 (vault-client-round-trips): incremental above the local
* `restored_through` marker (this target directory's own local git
* config, read/written by {@link readVaultRestoreMarker}/{@link
* writeVaultRestoreMarker}) — 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: VaultRestoreMarker | null;
state: VaultMaterializedState;
}): Promise<{
refs: VaultRefMap;
head_target: VaultHeadTarget;
generation: string;
retained_refs: VaultRetainedRefsReconcileResult;
}>;
/**
* The shared restore tail (vault-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-vault-restore-recipe shape.
*/
/**
* vault-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 `VAULT_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 (vault-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 VaultTransport.openCompactionGrant}. */
openCompactionGrant(): Promise<VaultCompactionGrant>;
/** Thin passthrough to the transport — see {@link VaultTransport.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: VaultCheckpointClaimSet, manifest: VaultCheckpointManifest, headCoversThrough: string): void;
/** Convenience for the deploy lane: the §6.5 capture binding. */
export declare function captureBinding(captureId: string, applyPlanSha256: string | null, snapshotOidHmac: string): VaultCaptureBinding;
export type { VaultRefUpdate };
//# sourceMappingURL=vault-publication.d.ts.map