UNPKG

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.

570 lines • 32.4 kB
/** * 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 { existsSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { LocalError } from "../errors.js"; import { VAULT_GENESIS_EPOCH, VAULT_TERMINAL_LOSS_DOCTOR_TEXT, VAULT_TERMINAL_LOSS_STATEMENT, buildRecoveryReceipt, buildVaultGenesis, bytesToHex, checkAllocation, checkGenesisKeyBindings, formatVaultTimestamp, newHex32, parseVaultStrict, randomBytes, sealKeyEnvelope, sha256Hex, storedBytes, } from "../namespaces/vault.crypto.js"; import { readFileNoFollow, writeFileAtomic0600 } from "./vault-keystore.js"; import { crossProfileVaultHint } from "./vault-profile-scan.js"; // ─── Stages + journal shape ────────────────────────────────────────────────── export const VAULT_CREATION_STAGES = [ "LOCAL_KEYS_PREPARED", "ALLOCATED", "OBJECTS_PREPARED", "OBJECTS_FINALIZED", "GENESIS_PREPARED", "ACTIVE", ]; function fail(code, message, context, details, nextActions) { throw new LocalError(message, context, { code, details, ...(nextActions && nextActions.length > 0 ? { next_actions: nextActions } : {}) }); } function envelopePath(epoch, recipientFingerprint) { return `envelopes/${epoch}/${recipientFingerprint}`; } /** Journal file for one creation attempt. */ export function vaultJournalPath(keystore, clientCreationId) { if (!/^[0-9a-f]{32}$/.test(clientCreationId)) fail("VAULT_BAD_ID", "client_creation_id must be 32 lowercase hex", "resolving vault journal path"); return join(keystore.journalDir, `${clientCreationId}.json`); } export function readVaultJournal(keystore, clientCreationId) { const text = readFileNoFollow(vaultJournalPath(keystore, clientCreationId)); if (!text) return null; const journal = JSON.parse(text); if (journal.version !== 1 || !VAULT_CREATION_STAGES.includes(journal.stage)) { fail("VAULT_JOURNAL_CORRUPT", "creation journal is not a version-1 vault journal", "reading vault creation journal", { client_creation_id: clientCreationId }); } return journal; } /** Every journal on disk that has not reached ACTIVE (what `doctor` lists as "creation in progress"). */ export function listIncompleteVaultJournals(keystore) { if (!existsSync(keystore.journalDir)) return []; const out = []; for (const entry of readdirSync(keystore.journalDir)) { if (!entry.endsWith(".json")) continue; const id = entry.slice(0, -5); if (!/^[0-9a-f]{32}$/.test(id)) continue; const j = readVaultJournal(keystore, id); if (j && j.stage !== "ACTIVE") out.push(j); } return out; } /** * 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 function findResumableVaultJournal(keystore, orgId, projectId) { const candidates = listIncompleteVaultJournals(keystore).filter((j) => j.push_to_create === null && j.org_id === orgId && j.project_id === projectId && j.refusal === null); if (candidates.length === 0) return null; candidates.sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0)); return candidates[0]; } /** * 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 function findResumablePushToCreateJournal(keystore, orgSlug, repoName) { const candidates = listIncompleteVaultJournals(keystore).filter((j) => j.push_to_create !== null && j.push_to_create.org_slug === orgSlug && j.push_to_create.repo_name === repoName && j.refusal === null); if (candidates.length === 0) return null; candidates.sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0)); return candidates[0]; } export class VaultCreation { keystore; transport; now; options; journal; resumed = false; constructor(options, journal, resumed) { this.options = options; this.keystore = options.keystore; this.transport = options.transport; this.now = options.now ?? (() => new Date()); this.journal = journal; this.resumed = resumed; } /** * 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) { const usingPushToCreate = options.push_to_create !== undefined; if (usingPushToCreate === (options.org_id !== undefined || options.project_id !== undefined)) { fail("VAULT_BAD_ADDRESS", "pass exactly one of {org_id, project_id} or push_to_create, never both or neither", "opening vault creation"); } const ks = options.keystore; const identity = ks.ensureIdentity(); const signing = ks.signingKeypair(identity); const encryption = ks.encryptionKeypair(identity); if (!signing || !encryption) { fail("VAULT_READ_ONLY", "this principal cannot create a vault: a private key is missing from identity.json", "opening vault creation", { signing_key_present: Boolean(signing), encryption_key_present: Boolean(encryption) }); } const id = options.client_creation_id ?? newHex32(); const existing = readVaultJournal(ks, id); if (existing) { // Addressing-key mismatch refuses a destructive retry — the ordinary // (org_id, project_id) form and push-to-create's (org_slug, repo_name) // form are each other's cross-check: a journal opened one way must // never silently resume under the other, and a push-to-create journal // compares its OWN address (`push_to_create` never changes across a // journal's life, unlike `org_id`/`project_id`, which start null there // and get pinned from the allocation response). const mismatch = (usingPushToCreate ? existing.push_to_create === null || existing.push_to_create.org_slug !== options.push_to_create.org_slug || existing.push_to_create.repo_name !== options.push_to_create.repo_name : existing.push_to_create !== null || existing.org_id !== options.org_id || existing.project_id !== options.project_id) // vault-byo-primary-bucket task 3.1: a resumed journal's own // storage_profile/byo_destination are the RESUMABILITY KEY, exactly // like push_to_create's address above — a resume that silently // changed which bucket it was asking for would be worse than // refusing and starting a fresh attempt. || existing.storage_profile !== (options.storage_profile ?? null) || existing.byo_destination !== (options.byo_destination ?? null); if (mismatch) { fail("VAULT_CREATION_CONFLICT", "journal belongs to a different org/project (or addressing form)", "resuming vault creation", { client_creation_id: id }); } if (existing.creator_signing_fingerprint !== identity.signing_fingerprint || existing.creator_encryption_fingerprint !== identity.encryption_fingerprint) { fail("VAULT_CREATION_CONFLICT", "journal was prepared by a different identity; refusing the destructive retry", "resuming vault creation", { client_creation_id: id }); } return new VaultCreation(options, existing, true); } const at = formatVaultTimestamp((options.now ?? (() => new Date()))()); const journal = { version: 1, stage: "LOCAL_KEYS_PREPARED", client_creation_id: id, org_id: usingPushToCreate ? null : options.org_id, project_id: usingPushToCreate ? null : options.project_id, push_to_create: options.push_to_create ?? null, storage_profile: options.storage_profile ?? null, byo_destination: options.byo_destination ?? null, creator_signing_fingerprint: identity.signing_fingerprint, creator_encryption_fingerprint: identity.encryption_fingerprint, created_at: at, updated_at: at, allocation: null, k_repo_hex: null, objects: [], genesis: null, genesis_sha256: null, recovery_receipt: null, refusal: null, }; const c = new VaultCreation(options, journal, false); c.persist(); return c; } get current() { return this.journal; } persist() { this.journal = { ...this.journal, updated_at: formatVaultTimestamp(this.now()) }; writeFileAtomic0600(vaultJournalPath(this.keystore, this.journal.client_creation_id), JSON.stringify(this.journal, null, 2)); this.keystore.audit("journal_stage", this.journal.allocation?.repo_id, { client_creation_id: this.journal.client_creation_id, stage: this.journal.stage }); } async advance(stage, patch) { this.journal = { ...this.journal, ...patch, stage }; this.persist(); await this.options.onStage?.(stage, this.journal); } refuse(code, message, details) { this.journal = { ...this.journal, refusal: { code, at: formatVaultTimestamp(this.now()), ...(details ? { details } : {}) } }; this.persist(); fail(code, message, "reconciling vault creation", { client_creation_id: this.journal.client_creation_id, ...details }); } /** * 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". */ async reconcileAllocateConflict(err) { const e = err; const repoId = typeof e?.details?.repo_id === "string" ? e.details.repo_id : null; if (!repoId) throw err; // nothing to reconcile against — the conflict names no repo_id const identity = this.keystore.readIdentity(); const canSign = identity !== null && this.keystore.signingKeypair(identity) !== null; const repoFile = this.keystore.readRepo(repoId); if (repoFile && canSign) { let genesisBytes = null; try { genesisBytes = await this.transport.getGenesis({ repo_id: repoId }); } catch { genesisBytes = null; // an unreachable read is never treated as proof either way — falls through to the non-holder refusal } if (genesisBytes) { const expectedSha = sha256Hex(genesisBytes); const receipt = this.keystore.readRecoveryReceipt(repoId); if (receipt && this.keystore.assess(repoId, expectedSha).state === "ready") { return { repo_id: repoId, org_id: repoFile.org_id, project_id: repoFile.project_id, genesis_sha256: expectedSha, recovery_receipt: receipt, journal: this.journal, how: "reconciled", }; } } } // Not a holder (or verification failed to confirm it): this machine's // OWN org/project addressing reached this repo_id, so — unlike the raw // gateway envelope this replaces — the word "foreign" never appears // here. The vault belongs to this org; this keystore just does not (or // can no longer provably) hold its key. fail("VAULT_CREATION_CONFLICT", `a vault already exists for this project (repo_id ${repoId}), and this machine's keystore does not hold (or cannot verify) its key`, "reconciling vault creation", { repo_id: repoId, org_id: this.journal.org_id, project_id: this.journal.project_id }, [ { action: "restore the vault keystore onto this machine (run402 repos view prints keystore.root; back it up from the machine that holds it)" }, ...crossProfileVaultHint(repoId), ]); } /** * Drive the journal to ACTIVE from whatever stage is durable. Safe to call * again after any crash; a refused journal stays refused. */ async run() { if (this.journal.refusal) { fail(this.journal.refusal.code, "this creation attempt was refused earlier; start a new attempt (the journal is kept for diagnosis)", "resuming vault creation", { client_creation_id: this.journal.client_creation_id, refused_at: this.journal.refusal.at, ...this.journal.refusal.details, }); } const identity = this.keystore.ensureIdentity(); const signing = this.keystore.signingKeypair(identity); const encryption = this.keystore.encryptionKeypair(identity); // ── 2. ALLOCATED ── if (this.journal.stage === "LOCAL_KEYS_PREPARED") { // PUBKEYS on the request; the returned record carries the fingerprints // the gateway derives from them. See VaultAllocateRequest. // vault-byo-primary-bucket task 3.1: the journal's own // storage_profile/byo_destination are the pinned request — set once // at open, resumability-checked above — never re-derived from a // fresh call's options (a resume never re-decides what it is asking // for). const byoFields = this.journal.storage_profile === "byo" ? { storage_profile: "byo", byo_destination: this.journal.byo_destination } : {}; const request = this.journal.push_to_create ? { client_creation_id: this.journal.client_creation_id, org_slug: this.journal.push_to_create.org_slug, repo_name: this.journal.push_to_create.repo_name, creator_signing_pubkey: identity.signing_pubkey, creator_encryption_pubkey: identity.encryption_pubkey, ...byoFields, } : { client_creation_id: this.journal.client_creation_id, org_id: this.journal.org_id, project_id: this.journal.project_id, creator_signing_pubkey: identity.signing_pubkey, creator_encryption_pubkey: identity.encryption_pubkey, ...byoFields, }; let allocation; try { allocation = await this.transport.allocate(request); } catch (e) { // Push-to-create's race (task 4.4/4.5, design D6): the atomic // name-claim was lost to a concurrent pusher. Mark the journal // refused with the winner's project_id so `pushToCreateVault` can // resolve to it and proceed as an ordinary push — "the loser's work // is not lost, it just wasn't the creator." Never retried // destructively from here; a fresh push-to-create attempt (if one is // ever made) starts its own journal. const code = e?.code; if (this.journal.push_to_create && code === "REPO_CREATION_CONFLICT") { const details = e?.details; this.refuse("REPO_CREATION_CONFLICT", "lost the push-to-create name-claim race to a concurrent pusher", details); } // Init idempotence: a fresh creation attempt // (no local journal to resume, e.g. because the ORIGINAL journal // already completed and is long gone) allocates under a BRAND NEW // client_creation_id, and the gateway's allocate route is unique // per project — a project that already has an admitted vault 409s // here, on the very first call, before this journal ever reaches // ALLOCATED. Reconcile it exactly like a resumed journal would: // never mint anything, never call allocate() again. if (!this.journal.push_to_create && code === "VAULT_CREATION_CONFLICT") { return await this.reconcileAllocateConflict(e); } throw e; } // Trust-on-first-use (task 4.5): a push-to-create journal has no // org_id/project_id to check the allocation against yet — `checkAllocation` // skips those two comparisons while `expected.org_id`/`project_id` are // `undefined`, and the (owner-authorized, signature-verified) // allocation's own values become the pin from here on. this.verifyAllocation(allocation); await this.advance("ALLOCATED", { allocation, org_id: this.journal.org_id ?? allocation.org_id, project_id: this.journal.project_id ?? allocation.project_id, }); } const allocation = this.journal.allocation; const repoId = allocation.repo_id; const epoch = VAULT_GENESIS_EPOCH; // ── 3. OBJECTS_PREPARED — the FIRST point at which ciphertext exists ── if (this.journal.stage === "ALLOCATED") { const kRepo = randomBytes(32); const sealed = await sealKeyEnvelope({ k_repo: kRepo, repo_id: repoId, epoch, recipient_public_key: encryption.public_key, signer: signing, created_at: formatVaultTimestamp(this.now()), }); const object = { object_kind: "key_envelope", path: envelopePath(epoch, sealed.receipt.recipient_fingerprint), stored_bytes_b64u: b64u(sealed.stored_bytes), stored_bytes_sha256: sealed.stored_bytes_sha256, size_bytes: sealed.size_bytes, receipt: sealed.receipt, finalized: false, }; await this.advance("OBJECTS_PREPARED", { k_repo_hex: bytesToHex(kRepo), objects: [object] }); } // ── 4. OBJECTS_FINALIZED — PUT (or read-and-compare after a crash), never re-encrypt ── if (this.journal.stage === "OBJECTS_PREPARED") { const objects = this.journal.objects.map((o) => ({ ...o })); for (const object of objects) { if (object.finalized) continue; const bytes = unb64u(object.stored_bytes_b64u); if (sha256Hex(bytes) !== object.stored_bytes_sha256 || String(bytes.length) !== object.size_bytes) { this.refuse("VAULT_CREATION_CONFLICT", "journaled object bytes do not match their journaled hash; refusing the destructive retry", { path: object.path }); } const remote = await this.transport.getObject({ repo_id: repoId, path: object.path }); if (remote) { if (!bytesEqual(remote, bytes)) { this.refuse("VAULT_CREATION_CONFLICT", "an object already exists at this path with different bytes", { path: object.path, remote_sha256: sha256Hex(remote) }); } object.finalized = true; continue; } const receipt = await this.transport.putObject({ repo_id: repoId, path: object.path, bytes, expected_sha256: object.stored_bytes_sha256, expected_size_bytes: object.size_bytes, // vault-byo-primary-bucket task 3.1/3.2: the genesis // key_envelope is a payload object — for a BYO vault it is // written directly to the customer's own destination, never // run402's bucket. `byo_write_target` is in-memory-only // (`VaultCreationOptions`'s own doc comment) — a resume that // omits it fails closed downstream in `upload()` rather than // silently defaulting to the managed path. ...(this.journal.storage_profile === "byo" && this.options.byo_write_target ? { byo: this.options.byo_write_target } : {}), }); if (receipt.stored_bytes_sha256 !== object.stored_bytes_sha256 || receipt.size_bytes !== object.size_bytes) { this.refuse("VAULT_CREATION_CONFLICT", "the server's finalization receipt does not match the local manifest", { path: object.path, receipt }); } object.finalized = true; } await this.advance("OBJECTS_FINALIZED", { objects }); } // ── 5. GENESIS_PREPARED — exact stored bytes + hash journaled before the admission PUT ── if (this.journal.stage === "OBJECTS_FINALIZED") { const envelopeObject = this.journal.objects.find((o) => o.object_kind === "key_envelope"); if (!envelopeObject) this.refuse("VAULT_CREATION_CONFLICT", "journal has no finalized key_envelope"); const genesis = buildVaultGenesis({ repo_id: repoId, // Guaranteed non-null past ALLOCATED, which pins both from the // allocation response either way (ordinary or push-to-create). org_id: this.journal.org_id, project_id: this.journal.project_id, allocation_nonce: allocation.allocation_nonce, creator_signing: signing, creator_encryption_public_key: encryption.public_key, envelope_receipt: envelopeObject.receipt, created_at: formatVaultTimestamp(this.now()), }); const storedEnvelope = parseVaultStrict(new TextDecoder().decode(unb64u(envelopeObject.stored_bytes_b64u))); const bindings = checkGenesisKeyBindings(genesis, storedEnvelope); if (bindings.length > 0) this.refuse("VAULT_CREATION_CONFLICT", "prepared genesis fails its own key bindings", { problems: bindings }); await this.advance("GENESIS_PREPARED", { genesis, genesis_sha256: sha256Hex(storedBytes(genesis)) }); } // ── 6. ACTIVE — admit; on resume read back; foreign genesis is a refusal ── if (this.journal.stage === "GENESIS_PREPARED") { const genesis = this.journal.genesis; const bytes = storedBytes(genesis); const hash = sha256Hex(bytes); if (hash !== this.journal.genesis_sha256) this.refuse("VAULT_CREATION_CONFLICT", "journaled genesis bytes do not match their journaled hash"); const existing = await this.transport.getGenesis({ repo_id: repoId }); if (existing) { if (!bytesEqual(existing, bytes)) { this.refuse("VAULT_CREATION_CONFLICT", "a different genesis is already admitted for this repo_id (foreign genesis); never overwritten", { admitted_sha256: sha256Hex(existing) }); } } else { const result = await this.transport.admitGenesis({ repo_id: repoId, allocation_generation: allocation.allocation_generation, stored_bytes: bytes, stored_bytes_sha256: hash }); if (result.outcome === "allocation_superseded") { this.refuse("ALLOCATION_SUPERSEDED", "the allocation was reclaimed by the owner; this client must not publish", { allocation_generation: allocation.allocation_generation }); } if (result.admitted_sha256 !== hash) { this.refuse("VAULT_CREATION_CONFLICT", "admission canonized a different genesis than the one prepared here", { admitted_sha256: result.admitted_sha256 }); } } const receipt = buildRecoveryReceipt({ repo_id: repoId, org_id: this.journal.org_id, project_id: this.journal.project_id, genesis_sha256: hash, creator_signing: signing, creator_encryption_public_key: encryption.public_key, }); // Pin + K_repo move into the repo file, then the receipt, then the journal flips — so a // crash between them leaves a GENESIS_PREPARED journal whose resume is an idempotent read-back. this.keystore.saveRepo({ repo_id: repoId, org_id: this.journal.org_id, project_id: this.journal.project_id, k_repo_hex: this.journal.k_repo_hex, epoch, genesis_sha256: hash, head_pin: null, last_ref_transaction: null, provenance: "created", }); this.keystore.saveRecoveryReceipt(receipt); await this.advance("ACTIVE", { recovery_receipt: receipt }); } return { repo_id: repoId, org_id: this.journal.org_id, project_id: this.journal.project_id, genesis_sha256: this.journal.genesis_sha256, recovery_receipt: this.journal.recovery_receipt, journal: this.journal, how: this.resumed ? "reconciled" : "created", }; } verifyAllocation(allocation) { const problems = checkAllocation(allocation, { client_creation_id: this.journal.client_creation_id, creator_signing_fingerprint: this.journal.creator_signing_fingerprint, creator_encryption_fingerprint: this.journal.creator_encryption_fingerprint, // `undefined`, not `null`, is what tells checkAllocation to skip a // comparison — trust-on-first-use for a fresh push-to-create journal // (task 4.5). A resumed journal past its first ALLOCATED call already // has these pinned and compares them normally. org_id: this.journal.org_id ?? undefined, project_id: this.journal.project_id ?? undefined, }, this.options.service_public_key); if (problems.length > 0) { fail("VAULT_ALLOCATION_INVALID", "allocation does not match this creation attempt", "verifying vault allocation", { problems }); } if (allocation.status === "superseded") { this.refuse("ALLOCATION_SUPERSEDED", "the allocation was reclaimed by the owner; this client must not resume", { allocation_generation: allocation.allocation_generation }); } } } /** Convenience: open + run in one call. */ export async function createVault(options) { return VaultCreation.open(options).run(); } export function vaultDoctorRecoveryText() { return { statement: VAULT_TERMINAL_LOSS_STATEMENT, doctor_text: VAULT_TERMINAL_LOSS_DOCTOR_TEXT, cas_restore_pointer: "The platform custodially holds the plaintext source artifacts of every DEPLOY in its CAS; ask support for a custodial restore of deployed artifacts. Vault HISTORY (commits never deployed) is not recoverable without a surviving principal keystore.", }; } // ─── helpers ──────────────────────────────────────────────────────────────── function b64u(bytes) { return Buffer.from(bytes).toString("base64url"); } function unb64u(text) { return new Uint8Array(Buffer.from(text, "base64url")); } function bytesEqual(a, b) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; return diff === 0; } //# sourceMappingURL=vault-creation-journal.js.map