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.
387 lines • 20.3 kB
TypeScript
/**
* vault — 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/<client_creation_id>.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 VaultCreationTransport}
* — task 5.4 wires the HTTP one; tests supply an in-memory bucket.
*/
import { VAULT_TERMINAL_LOSS_DOCTOR_TEXT, VAULT_TERMINAL_LOSS_STATEMENT } from "../namespaces/vault.crypto.js";
import type { VaultAllocation, VaultKeyEnvelopeReceipt, VaultRecoveryReceipt, VaultGenesis } from "../namespaces/vault.types.js";
import { VaultKeystore } from "./vault-keystore.js";
export declare const VAULT_CREATION_STAGES: readonly ["LOCAL_KEYS_PREPARED", "ALLOCATED", "OBJECTS_PREPARED", "OBJECTS_FINALIZED", "GENESIS_PREPARED", "ACTIVE"];
export type VaultCreationStage = (typeof VAULT_CREATION_STAGES)[number];
/** One prepared object: exact bytes + identity, durable before any PUT. */
export interface VaultJournaledObject {
object_kind: "key_envelope";
/** Storage path relative to the vault root (`envelopes/<epoch>/<recipient_fingerprint>`). */
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: VaultKeyEnvelopeReceipt;
/** 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/repos.ts's
* `resolveAllocationAuthz` in run402-private) — see `VaultCreationJournal`'s
* doc comment on why `org_id`/`project_id` are nullable.
*/
export interface VaultPushToCreateAddress {
org_slug: string;
repo_name: string;
}
/**
* `journal/<client_creation_id>.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 VaultCreationJournal {
version: 1;
stage: VaultCreationStage;
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: VaultPushToCreateAddress | null;
/** vault-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: VaultAllocation | null;
/** OBJECTS_PREPARED — K_repo (hex) lives ONLY here until ACTIVE moves it to the repo file. */
k_repo_hex: string | null;
objects: VaultJournaledObject[];
/** GENESIS_PREPARED — the exact signed genesis and its stored-bytes hash. */
genesis: VaultGenesis | null;
genesis_sha256: string | null;
/** ACTIVE — the emitted recovery receipt. */
recovery_receipt: VaultRecoveryReceipt | 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
* `pushToCreateVault` 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<string, unknown>;
} | null;
}
/**
* `POST /vaults/v1` — 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 VaultAllocateRequest = {
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;
/** vault-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 /vaults/v1`, 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 VaultPutObjectRequest {
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;
/** vault-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("./vault-mirror-config.js").VaultMirrorDestination;
credential?: import("./vault-mirror-config.js").VaultMirrorCredential;
};
}
export interface VaultObjectReceipt {
stored_bytes_sha256: string;
size_bytes: string;
}
export interface VaultAdmitGenesisRequest {
repo_id: string;
allocation_generation: string;
stored_bytes: Uint8Array;
stored_bytes_sha256: string;
}
export type VaultAdmitGenesisResult = {
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 VaultCreationTransport {
/** Idempotent on `client_creation_id`; returns the current representation (which may be `superseded`). */
allocate(request: VaultAllocateRequest): Promise<VaultAllocation>;
/** Create-only PUT; on a pre-existing object the server MAY return its receipt instead of overwriting. */
putObject(request: VaultPutObjectRequest): Promise<VaultObjectReceipt>;
/**
* Read an object back (`null` when absent) — the read-and-compare primitive.
*
* `expected_sha256` (vault-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<Uint8Array | null>;
/** Generation-zero admission (the §5A machine at generation zero). */
admitGenesis(request: VaultAdmitGenesisRequest): Promise<VaultAdmitGenesisResult>;
/** Read the admitted genesis bytes back (`null` when none). */
getGenesis(request: {
repo_id: string;
}): Promise<Uint8Array | null>;
}
export interface VaultCreationOptions {
keystore: VaultKeystore;
transport: VaultCreationTransport;
/**
* 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?: VaultPushToCreateAddress;
/** Resume an existing journal, or pin the idempotency key (tests). Fresh CSPRNG when omitted. */
client_creation_id?: string;
/**
* vault-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/vault-byo-probe.js probeVaultByoDestination})
* 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;
/**
* vault-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 `Vault.uploadAll`'s resolver since no `Vault`
* 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("./vault-mirror-config.js").VaultMirrorDestination;
credential?: import("./vault-mirror-config.js").VaultMirrorCredential;
};
/** 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: VaultCreationStage, journal: VaultCreationJournal) => void | Promise<void>;
}
export interface VaultCreationResult {
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: VaultRecoveryReceipt;
journal: VaultCreationJournal;
/** `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 vaultJournalPath(keystore: VaultKeystore, clientCreationId: string): string;
export declare function readVaultJournal(keystore: VaultKeystore, clientCreationId: string): VaultCreationJournal | null;
/** Every journal on disk that has not reached ACTIVE (what `doctor` lists as "creation in progress"). */
export declare function listIncompleteVaultJournals(keystore: VaultKeystore): VaultCreationJournal[];
/**
* 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: `VaultCreation.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 findResumableVaultJournal(keystore: VaultKeystore, orgId: string, projectId: string): VaultCreationJournal | null;
/**
* The push-to-create sibling of {@link findResumableVaultJournal} (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; `pushToCreateVault` 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: VaultKeystore, orgSlug: string, repoName: string): VaultCreationJournal | null;
export declare class VaultCreation {
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: VaultCreationOptions): VaultCreation;
get current(): VaultCreationJournal;
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<VaultCreationResult>;
private verifyAllocation;
}
/** Convenience: open + run in one call. */
export declare function createVault(options: VaultCreationOptions): Promise<VaultCreationResult>;
/** What `doctor`/`status` print about V0-A recovery — the verbatim statement plus the pointers it must carry. */
export interface VaultDoctorRecoveryText {
statement: typeof VAULT_TERMINAL_LOSS_STATEMENT;
doctor_text: typeof VAULT_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 vaultDoctorRecoveryText(): VaultDoctorRecoveryText;
//# sourceMappingURL=vault-creation-journal.d.ts.map