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.
132 lines • 6.72 kB
JavaScript
/**
* vault D6 — push-to-create (repo-first-onramp task 4.5).
*
* `pushToCreateVault` is the primitive the remote helper and `vault
* snapshot` drive when a slug-form remote (`run402::<org-slug>/<name>`)
* resolves to nothing yet: pushing to a name that does not exist allocates it
* (design D6), atomically, through the gateway's `POST /vaults/v1
* {org_slug, repo_name}` push-to-create form — the SAME route the ordinary
* `{project_id}` allocate uses, so the six-stage creation journal
* ({@link createVault}) drives it exactly as it drives an ordinary
* creation, just addressed by `push_to_create` instead of `project_id`.
*
* SHAPE, mirroring the gateway's own `pushToCreateRepo` (run402-private
* `services/repo-names.ts`) one layer up:
*
* 1. Fast path: does `org-slug/name` already resolve
* ({@link VaultTransport.findVaultByRepo})? If so this is an
* ordinary push to an existing repo — return its vault record, allocate
* nothing. This is ALSO how a race LOSER's next call resolves cleanly
* (see step 3): the repo now exists, so a retry never re-enters the
* journal at all.
* 2. Not found: drive the six-stage journal, addressed by
* `push_to_create: {org_slug, repo_name}`. Resumable exactly like the
* ordinary path — {@link findResumablePushToCreateJournal} finds an
* INCOMPLETE local attempt for this exact address and resumes its
* `client_creation_id` rather than starting a second competing one.
* 3. `REPO_CREATION_CONFLICT` (the gateway's atomic name-claim raced a
* concurrent pusher): resolve to the WINNER's project_id (named in the
* error's `details.project_id`) via `findVaultByProject`, and report
* `found: true` — an ordinary push to the repo that now exists. Design
* D6: "no transparent retargeting" — this client never silently
* continues against the winner's genesis; it re-resolves and the
* NEXT call (an ordinary push) proceeds normally. Nothing here loses
* the loser's actual working-tree changes; only its bid to be the
* creator.
* 4. `SLUG_RELEASED` (the org slug is in its post-rename cooldown): NEVER
* auto-followed — rethrown unchanged so the caller sees the typed
* refusal (successor slug named, cooldown_until stated).
*/
import { createVault, findResumablePushToCreateJournal } from "./vault-creation-journal.js";
function isNotFound(e) {
const err = e;
return Boolean(err && (err.status === 404 || err.code === "RESOURCE_NOT_FOUND" || err.code === "ROUTE_NOT_FOUND"));
}
function repoCreationConflictWinner(e) {
const err = e;
if (!err || err.code !== "REPO_CREATION_CONFLICT")
return null;
const winner = err.details?.project_id;
return typeof winner === "string" && winner.length > 0 ? winner : null;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* A bounded poll for the winner's vault to become resolvable after a lost
* `REPO_CREATION_CONFLICT` race — the winner's own creation may still be
* mid-flight. Budget mirrors the gateway's own analogous poll
* (`services/repo-names.ts` in run402-private: 20 attempts, 50ms apart);
* still-unresolvable after the budget rethrows the ORIGINAL not-found
* failure unchanged, never a synthesized one.
*/
async function pollFindVaultByProject(transport, projectId, attempts = 20, intervalMs = 50) {
let lastError;
for (let i = 0; i < attempts; i += 1) {
try {
return await transport.findVaultByProject({ project_id: projectId });
}
catch (e) {
if (!isNotFound(e))
throw e;
lastError = e;
await sleep(intervalMs);
}
}
throw lastError;
}
/**
* Resolve `(org_slug, repo_name)`, allocating it (project + vault, atomically)
* when it does not exist yet.
*/
export async function pushToCreateVault(options) {
// ── 1. fast path: already exists? ──
try {
const record = await options.transport.findVaultByRepo({ org_slug: options.org_slug, repo_name: options.repo_name });
return { found: true, repo_id: record.repo_id, project_id: record.project_id, org_id: record.org_id, created: null };
}
catch (e) {
if (!isNotFound(e))
throw e; // includes SLUG_RELEASED — never auto-followed
}
// ── 2. drive the six-stage journal, addressed by push_to_create ──
const resumable = options.client_creation_id ? null : findResumablePushToCreateJournal(options.keystore, options.org_slug, options.repo_name);
try {
const created = await createVault({
keystore: options.keystore,
transport: options.transport,
push_to_create: { org_slug: options.org_slug, repo_name: options.repo_name },
...(options.client_creation_id !== undefined
? { client_creation_id: options.client_creation_id }
: resumable
? { client_creation_id: resumable.client_creation_id }
: {}),
...(options.service_public_key !== undefined ? { service_public_key: options.service_public_key } : {}),
});
return {
found: false,
repo_id: created.repo_id,
project_id: created.project_id,
org_id: created.org_id,
created: { deduplicated: created.how === "reconciled", recovery_receipt: created.recovery_receipt, genesis_sha256: created.genesis_sha256 },
};
}
catch (e) {
// ── 3. lost the race: resolve to the winner, proceed as an ordinary push ──
const winnerProjectId = repoCreationConflictWinner(e);
if (winnerProjectId) {
// The winner's OWN creation (object upload, genesis admission) may
// still be in flight the instant this client learns it lost — the
// gateway's `REPO_CREATION_CONFLICT` only proves the PROJECT exists,
// not that the winner's vault has finished creating. A short bounded
// poll (mirroring the gateway's own "poll briefly for the winner's
// finalized project_id" one layer up, `services/repo-names.ts` in
// run402-private) covers that ordinary window; a still-unresolvable
// vault after the budget is a genuine failure, surfaced unchanged.
const record = await pollFindVaultByProject(options.transport, winnerProjectId);
return { found: true, repo_id: record.repo_id, project_id: record.project_id, org_id: record.org_id, created: null };
}
throw e;
}
}
//# sourceMappingURL=vault-push-to-create.js.map