/** * gitvault — the six-stage crash-safe creation journal (protocol rev 41 §5.2; * task 5.3). * * LOCAL_KEYS_PREPARED identity + client_creation_id (no ciphertext yet — * ALLOCATED verified allocation: repo_id, nonce, gen repo_id is a KDF input) * OBJECTS_PREPARED exact object ids, nonces, framed bytes, hashes, sizes — fsynced BEFORE any PUT * OBJECTS_FINALIZED server receipts, compared to the local manifest * GENESIS_PREPARED exact genesis stored bytes + hash * ACTIVE admitted; pin + recovery receipt recorded * * The ordering is the round-3 H5 invariant made structural: `K_repo` is * generated and the creator's `key_envelope` is sealed ONLY in the * OBJECTS_PREPARED step, which cannot run until ALLOCATED has journaled a * `repo_id` — because `repo_id` is an HKDF input and an HPKE AAD member, no * ciphertext can exist before allocation. Each stage is written to * `journal/.json` with the keystore's atomic+fsync write * before the next step starts. * * Restart reconciliation (`resume`) dispatches on the durable stage: * - OBJECTS_PREPARED with a PUT possibly in flight → read-and-compare the * object against the journaled bytes; equal → finalized; present-but- * different → `VAULT_CREATION_CONFLICT`; absent → PUT the SAME bytes. * Never re-encrypt under the same id (a fresh seal would be a second * ciphertext under a single-use key label). * - GENESIS_PREPARED → read the admitted genesis back; equal → ACTIVE; * foreign → `VAULT_CREATION_CONFLICT`, never overwritten. * - a `superseded` allocation anywhere → `ALLOCATION_SUPERSEDED`; the client * does not publish. * - anything the journal cannot explain → refuse the destructive retry. * * The control-plane calls ride an injectable {@link GitvaultCreationTransport} * — task 5.4 wires the HTTP one; tests supply an in-memory bucket. */ import { GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT, GITVAULT_TERMINAL_LOSS_STATEMENT } from "../namespaces/gitvault.crypto.js"; import type { GitvaultAllocation, GitvaultKeyEnvelopeReceipt, GitvaultRecoveryReceipt, GitvaultVaultGenesis } from "../namespaces/gitvault.types.js"; import { GitvaultKeystore } from "./gitvault-keystore.js"; export declare const GITVAULT_CREATION_STAGES: readonly ["LOCAL_KEYS_PREPARED", "ALLOCATED", "OBJECTS_PREPARED", "OBJECTS_FINALIZED", "GENESIS_PREPARED", "ACTIVE"]; export type GitvaultCreationStage = (typeof GITVAULT_CREATION_STAGES)[number]; /** One prepared object: exact bytes + identity, durable before any PUT. */ export interface GitvaultJournaledObject { object_kind: "key_envelope"; /** Storage path relative to the vault root (`envelopes//`). */ path: string; /** The exact stored bytes, base64url (the §1 "base64url-jcs" convention). */ stored_bytes_b64u: string; stored_bytes_sha256: string; size_bytes: string; /** The genesis receipt for this object. */ receipt: GitvaultKeyEnvelopeReceipt; /** Set once the server receipt was compared equal to the local manifest. */ finalized: boolean; } /** * Push-to-create addressing (repo-first-onramp task 4.5; design D6). A journal * opened this way does not know its `org_id`/`project_id` until the ALLOCATED * stage's response tells it (the gateway resolves `org_slug` and creates the * project atomically inside the SAME allocate call — see routes/gitvault.ts's * `resolveAllocationAuthz` in run402-private) — see `GitvaultCreationJournal`'s * doc comment on why `org_id`/`project_id` are nullable. */ export interface GitvaultPushToCreateAddress { org_slug: string; repo_name: string; } /** * `journal/.json`. Stage-monotonic; every field from an * earlier stage survives. * * `org_id` / `project_id` are `string | null` because of push-to-create * (repo-first-onramp task 4.5, design D6): a journal opened with * `push_to_create` set does not know either value until the ALLOCATED stage's * response names them (trust-on-first-use — `checkAllocation` skips those two * comparisons while they are `null` and this module pins them from the * allocation the FIRST time, then treats the pin as authoritative on every * later call for this same journal, same as an ordinary `project_id`-addressed * journal always has been). Exactly one of `project_id` or `push_to_create` is * set at open time; `push_to_create` itself never changes across the journal's * life — it is the RESUMABILITY KEY for a push-to-create attempt, the way * `(org_id, project_id)` is for the ordinary path. */ export interface GitvaultCreationJournal { version: 1; stage: GitvaultCreationStage; client_creation_id: string; org_id: string | null; project_id: string | null; /** Set only for a push-to-create journal (task 4.5); `null` otherwise. */ push_to_create: GitvaultPushToCreateAddress | null; /** gitvault-byo-primary-bucket task 3.1 — `"byo"` when this journal requested a BYO vault at open time; `null` (managed, byte-identical to today) otherwise. Set once at open, never changes across the journal's life (mirrors `push_to_create`'s own resumability-key discipline). */ storage_profile: "byo" | null; /** Set iff `storage_profile === "byo"`; `null` otherwise. */ byo_destination: string | null; creator_signing_fingerprint: string; creator_encryption_fingerprint: string; created_at: string; updated_at: string; /** ALLOCATED — the verified allocation verbatim. */ allocation: GitvaultAllocation | null; /** OBJECTS_PREPARED — K_repo (hex) lives ONLY here until ACTIVE moves it to the repo file. */ k_repo_hex: string | null; objects: GitvaultJournaledObject[]; /** GENESIS_PREPARED — the exact signed genesis and its stored-bytes hash. */ genesis: GitvaultVaultGenesis | null; genesis_sha256: string | null; /** ACTIVE — the emitted recovery receipt. */ recovery_receipt: GitvaultRecoveryReceipt | null; /** * A terminal refusal, if reconciliation hit one (the journal is kept for * diagnosis, never retried destructively). `REPO_CREATION_CONFLICT` (task * 4.5) marks a push-to-create journal that lost the atomic name-claim race * — `details.project_id` names the winner, exactly what * `pushToCreateGitvault` needs to resolve to it and proceed as an ordinary * push (design D6: "the loser's work is not lost — it just wasn't the * creator"). */ refusal: { code: "VAULT_CREATION_CONFLICT" | "ALLOCATION_SUPERSEDED" | "REPO_CREATION_CONFLICT"; at: string; details?: Record; } | null; } /** * `POST /gitvault/v1/vaults` — the allocate REQUEST. * * READ THIS BEFORE CHANGING THESE FIELD NAMES. The request carries the raw * PUBLIC KEYS; the signed `allocation` RECORD the gateway returns carries the * FINGERPRINTS derived from them. The two shapes differ on exactly these two * fields, so building the request from `schemas/allocation.json` — the obvious * thing to do — produces a body the gateway ignores, and it answers * `400 VALIDATION_FAILED field=creator_signing_pubkey` as if the field were * simply missing. That is the production failure this comment exists to stop * from recurring. * * Pubkeys, not fingerprints, because a fingerprint is one-way: every later head * signature is verified against the stored creator signing key, and a hash * cannot verify a signature. The gateway derives the fingerprints itself, and * `checkAllocation` compares the record's fingerprints back against this * principal's — which is what proves the keys we sent are the keys it stored. */ export type GitvaultAllocateRequest = { client_creation_id: string; org_id: string; project_id: string; /** Raw Ed25519 public key, canonical base64url (43 chars, decodes to 32 bytes). */ creator_signing_pubkey: string; /** Raw X25519 public key, canonical base64url (43 chars, decodes to 32 bytes). */ creator_encryption_pubkey: string; /** gitvault-byo-primary-bucket task 3.1/3.5 — omitted/absent is byte-identical to today ("managed"). Present only to request a BYO vault; `byo_destination` is the address only, never credential material (D2). */ storage_profile?: "byo"; byo_destination?: string; } | { /** * Push-to-create (repo-first-onramp task 4.4/4.5, design D6): * `{org_slug, repo_name}` in place of `{org_id, project_id}`. The * gateway resolves the slug, atomically claims the name, creates the * project, and allocates the vault — all inside this one call * (`POST /gitvault/v1/vaults`, same route, alternate body shape). */ client_creation_id: string; org_slug: string; repo_name: string; creator_signing_pubkey: string; creator_encryption_pubkey: string; storage_profile?: "byo"; byo_destination?: string; }; export interface GitvaultPutObjectRequest { repo_id: string; path: string; bytes: Uint8Array; /** What the client expects the server to acknowledge — the server's receipt is compared against it. */ expected_sha256: string; expected_size_bytes: string; /** gitvault-byo-primary-bucket task 3.2 — present iff this vault is `storage_profile: "byo"`; the object is written directly to `byo.destination` (never run402's own bucket). Omitted is byte-identical to today. */ byo?: { destination: import("./gitvault-mirror-config.js").GitvaultMirrorDestination; credential?: import("./gitvault-mirror-config.js").GitvaultMirrorCredential; }; } export interface GitvaultObjectReceipt { stored_bytes_sha256: string; size_bytes: string; } export interface GitvaultAdmitGenesisRequest { repo_id: string; allocation_generation: string; stored_bytes: Uint8Array; stored_bytes_sha256: string; } export type GitvaultAdmitGenesisResult = { outcome: "admitted"; admitted_sha256: string; } /** Someone already canonized generation zero — the resume path decides whether it is ours (read-back) or foreign. */ | { outcome: "already_admitted"; admitted_sha256: string; } | { outcome: "allocation_superseded"; }; /** * The control-plane + bucket operations creation needs. Every method is * idempotent from the journal's point of view; the journal never relies on a * call having NOT happened. */ export interface GitvaultCreationTransport { /** Idempotent on `client_creation_id`; returns the current representation (which may be `superseded`). */ allocate(request: GitvaultAllocateRequest): Promise; /** Create-only PUT; on a pre-existing object the server MAY return its receipt instead of overwriting. */ putObject(request: GitvaultPutObjectRequest): Promise; /** * Read an object back (`null` when absent) — the read-and-compare primitive. * * `expected_sha256` (gitvault-small-object-inline design D3) is OPTIONAL, * client-internal plumbing: an `object-reads`-backed implementation MAY * use it to verify a gateway-supplied `inline` reply before trusting it, * falling back to the ordinary fetch on a mismatch — never a NEW * verification obligation, since every real caller already hash-checks * the returned bytes itself before use. Omitted, this is byte-identical * to before that change. */ getObject(request: { repo_id: string; path: string; expected_sha256?: string; }): Promise; /** Generation-zero admission (the §5A machine at generation zero). */ admitGenesis(request: GitvaultAdmitGenesisRequest): Promise; /** Read the admitted genesis bytes back (`null` when none). */ getGenesis(request: { repo_id: string; }): Promise; } export interface GitvaultCreationOptions { keystore: GitvaultKeystore; transport: GitvaultCreationTransport; /** * Exactly ONE addressing form: either `{org_id, project_id}` (the ordinary * path — a project already exists) or `push_to_create` (task 4.5 — the * project does not exist yet; the gateway creates it atomically alongside * the vault). Passing both, or neither, is a local validation error. */ org_id?: string; project_id?: string; /** Push-to-create addressing (repo-first-onramp task 4.5, design D6) — mutually exclusive with `{org_id, project_id}`. */ push_to_create?: GitvaultPushToCreateAddress; /** Resume an existing journal, or pin the idempotency key (tests). Fresh CSPRNG when omitted. */ client_creation_id?: string; /** * gitvault-byo-primary-bucket task 3.1/3.5 — request a BYO vault at * allocation. Omitted (the default) is byte-identical to today. The * caller MUST have already run the allocation-time bucket probe * ({@link ../node/gitvault-byo-probe.js probeGitvaultByoDestination}) * against `byo_destination` BEFORE reaching here — this option only * threads the already-validated request through; it does not probe. */ storage_profile?: "byo"; /** Required iff `storage_profile === "byo"` — the destination ADDRESS only (never credential material, D2). */ byo_destination?: string; /** * gitvault-byo-primary-bucket task 3.1/3.2 — the LOCAL write target that * drives the genesis `key_envelope`'s own PUT (the one object every * vault creates, and the one this class's own `run()` writes directly, * bypassing `GitvaultVault.uploadAll`'s resolver since no `GitvaultVault` * exists yet at creation time). Deliberately NOT persisted into the * journal file (unlike `storage_profile`/`byo_destination` above) — a * credential name is a local, per-invocation fact the caller resupplies * on every call, exactly like `service_public_key` already is. Required * when `storage_profile === "byo"` and this call needs to reach * OBJECTS_PREPARED/OBJECTS_FINALIZED; a resume that omits it fails * closed at the object-PUT step rather than silently writing to * run402's own bucket. */ byo_write_target?: { destination: import("./gitvault-mirror-config.js").GitvaultMirrorDestination; credential?: import("./gitvault-mirror-config.js").GitvaultMirrorCredential; }; /** Clock injection. */ now?: () => Date; /** Optional pinned service public key for allocation-signature verification (5.4 supplies it from the registry). */ service_public_key?: Uint8Array | string; /** Test hook: called after each stage is durable, BEFORE the next step — throw to simulate a crash. */ onStage?: (stage: GitvaultCreationStage, journal: GitvaultCreationJournal) => void | Promise; } export interface GitvaultCreationResult { repo_id: string; /** The owning org/project, resolved by ACTIVE either way — trust-on-first-use for a push-to-create journal (task 4.5). */ org_id: string; project_id: string; genesis_sha256: string; recovery_receipt: GitvaultRecoveryReceipt; journal: GitvaultCreationJournal; /** `created` on the first pass; `reconciled` when a restart completed an earlier attempt. */ how: "created" | "reconciled"; } /** Journal file for one creation attempt. */ export declare function gitvaultJournalPath(keystore: GitvaultKeystore, clientCreationId: string): string; export declare function readGitvaultJournal(keystore: GitvaultKeystore, clientCreationId: string): GitvaultCreationJournal | null; /** Every journal on disk that has not reached ACTIVE (what `doctor` lists as "creation in progress"). */ export declare function listIncompleteGitvaultJournals(keystore: GitvaultKeystore): GitvaultCreationJournal[]; /** * The client-side half of D2's resumability guarantee (repo-first-onramp * task 2.2): find an in-progress LOCAL creation attempt for this exact * (org, project) pair, so a second lazy-create call after a crash RESUMES * that attempt's `client_creation_id` instead of starting a fresh one. * * A fresh random id every call would still converge on one vault in * practice (the gateway's own allocate route is idempotent per project), * but only this local check makes it provable client-side, with no * dependency on that server behavior: interrupt a creation mid-flight, * call this again, and the SAME journal — not a second competing one — * drives to ACTIVE. * * Refused journals (`VAULT_CREATION_CONFLICT` / `ALLOCATION_SUPERSEDED`) are * skipped: `GitvaultCreation.run()` refuses to retry a refused journal * destructively, so resuming one here would only reproduce the refusal. * When more than one live candidate exists (should not happen in practice — * this function is what prevents it — but a manually-edited keystore or a * pre-D2 stray journal could produce one), the most recently updated one is * preferred, on the theory that it is the attempt most likely still moving. */ export declare function findResumableGitvaultJournal(keystore: GitvaultKeystore, orgId: string, projectId: string): GitvaultCreationJournal | null; /** * The push-to-create sibling of {@link findResumableGitvaultJournal} (task * 4.5): find an in-progress LOCAL push-to-create attempt for this exact * `(org_slug, repo_name)` address, so a retry after a crash resumes it rather * than starting a second competing attempt. A journal that already lost the * name-claim race (`REPO_CREATION_CONFLICT`) is excluded — resuming it would * only reproduce the same refusal; `pushToCreateGitvault` starts a fresh * attempt in that case (and typically does not even reach here, since its * own fast-path read resolves the winner's repo directly). */ export declare function findResumablePushToCreateJournal(keystore: GitvaultKeystore, orgSlug: string, repoName: string): GitvaultCreationJournal | null; export declare class GitvaultCreation { private readonly keystore; private readonly transport; private readonly now; private readonly options; private journal; private resumed; private constructor(); /** * Open a creation: resume the journal for `client_creation_id` when one * exists, else write stage 1 (LOCAL_KEYS_PREPARED). Nothing here touches the * network; no ciphertext exists yet. */ static open(options: GitvaultCreationOptions): GitvaultCreation; get current(): GitvaultCreationJournal; private persist; private advance; private refuse; /** * Init idempotence. Reached only from a FRESH * attempt's `allocate()` call (never a resumed one — that path is * `verifyAllocation`/the mid-journal refusals below), so nothing has been * journaled past LOCAL_KEYS_PREPARED and no ciphertext or K_repo exists to * protect: this method either returns the SAME result a resumed journal * would have produced, or throws, and mints nothing either way. * * `details.repo_id` names the vault the gateway says already exists. * Holding a local repo file for it is necessary but not sufficient — * `assess()` re-verifies the pinned genesis hash against the ADMITTED * genesis this same 409 implies exists (read via the ordinary * `transport.getGenesis` path, not trusted blind), so a stale or * corrupted local pin still falls through to the non-holder refusal * rather than a false "reconciled". */ private reconcileAllocateConflict; /** * Drive the journal to ACTIVE from whatever stage is durable. Safe to call * again after any crash; a refused journal stays refused. */ run(): Promise; private verifyAllocation; } /** Convenience: open + run in one call. */ export declare function createGitvault(options: GitvaultCreationOptions): Promise; /** What `doctor`/`status` print about V0-A recovery — the verbatim statement plus the pointers it must carry. */ export interface GitvaultDoctorRecoveryText { statement: typeof GITVAULT_TERMINAL_LOSS_STATEMENT; doctor_text: typeof GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT; /** The platform's custodial restore of deployed artifacts (the deploy lane's CAS) — the support path when every principal envelope is lost. */ cas_restore_pointer: string; } export declare function gitvaultDoctorRecoveryText(): GitvaultDoctorRecoveryText; //# sourceMappingURL=gitvault-creation-journal.d.ts.map