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.
183 lines • 8.49 kB
JavaScript
import { ApiError, LocalError } from "../errors.js";
import { waitFor } from "../wait.js";
/** How long `restore()` polls before giving up (the restore itself keeps running). */
const DEFAULT_RESTORE_WAIT_MS = 15 * 60 * 1000;
export class Snapshots {
client;
constructor(client) {
this.client = client;
}
/**
* Create a manual snapshot. `label` and `metadata` are immutable and live
* outside the tenant schema, so a restore never rewinds them: the snapshot
* list is a ledger of restore points that survives restores.
*/
async create(projectId, opts = {}) {
assertProjectId(projectId, "creating project snapshot");
return this.client.request(snapshotCollectionPath(projectId), {
method: "POST",
body: {
...(opts.label !== undefined ? { label: opts.label } : {}),
...(opts.metadata !== undefined ? { metadata: opts.metadata } : {}),
},
authMeta: {
method: "snapshots.create",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context: "creating project snapshot",
});
}
async list(projectId, opts = {}) {
assertProjectId(projectId, "listing project snapshots");
const qs = new URLSearchParams();
if (opts.limit !== undefined)
qs.set("limit", String(opts.limit));
if (opts.after !== undefined)
qs.set("after", opts.after);
if (opts.kind !== undefined)
qs.set("kind", opts.kind);
const query = qs.toString();
return this.client.request(`${snapshotCollectionPath(projectId)}${query ? `?${query}` : ""}`, {
authMeta: {
method: "snapshots.list",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context: "listing project snapshots",
});
}
async get(projectId, snapshotId) {
assertProjectId(projectId, "getting project snapshot");
assertSnapshotId(snapshotId, "getting project snapshot");
return this.client.request(snapshotPath(projectId, snapshotId), {
authMeta: {
method: "snapshots.get",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context: "getting project snapshot",
});
}
async delete(projectId, snapshotId) {
assertProjectId(projectId, "deleting project snapshot");
assertSnapshotId(snapshotId, "deleting project snapshot");
await this.client.request(snapshotPath(projectId, snapshotId), {
method: "DELETE",
authMeta: {
method: "snapshots.delete",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context: "deleting project snapshot",
});
}
/**
* Plan a restore without mutating anything. Pass the same `release` and
* `includeAuth` to `restore` that you plan with; the confirm token binds them.
*/
async restorePlan(projectId, snapshotId, opts = {}) {
assertProjectId(projectId, "planning project snapshot restore");
assertSnapshotId(snapshotId, "planning project snapshot restore");
return this.client.request(`${snapshotPath(projectId, snapshotId)}/restore`, {
method: "POST",
body: restoreBody(opts),
authMeta: {
method: "snapshots.restorePlan",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context: "planning project snapshot restore",
});
}
async restore(projectId, snapshotId, confirm, opts = {}) {
const context = "restoring project snapshot";
assertProjectId(projectId, context);
assertSnapshotId(snapshotId, context);
if (!confirm || typeof confirm !== "string") {
throw new LocalError("snapshots.restore requires a confirm token from snapshots.restorePlan", context);
}
const started = await this.client.request(`${snapshotPath(projectId, snapshotId)}/restore`, {
method: "POST",
body: { ...restoreBody(opts), confirm, wait: false },
authMeta: {
method: "snapshots.restore",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context,
});
// A gateway that predates asynchronous restore ignores `wait` and answers
// with the terminal result synchronously.
if (started.status !== "running")
return started;
const handle = started;
if (opts.wait === false)
return handle;
const pollMs = Math.max(opts.pollIntervalMs ?? (handle.retry_after_seconds ?? 5) * 1000, 1000);
const waited = await waitFor(() => this.getRestore(projectId, snapshotId, handle.restore_id),
// `ready` can precede the function re-deploy (the flip commits first);
// the result is final once nothing is `pending`.
(status) => status.status !== "running" && !(status.result?.functions?.pending?.length), { pollMs, timeoutMs: opts.timeoutMs ?? DEFAULT_RESTORE_WAIT_MS });
const status = waited.state;
if (status.status === "ready" && status.result)
return status.result;
if (status.status === "failed") {
throw new ApiError(`${status.error?.message ?? "Restore failed"} while ${context}`, null, {
code: status.error?.code ?? "SNAPSHOT_RESTORE_FAILED",
message: status.error?.message ?? "Restore failed",
details: { restore_id: status.restore_id, snapshot_id: snapshotId, project_id: projectId },
next_actions: status.next_actions,
}, context);
}
// Still running when the wait ran out (or ready from a gateway that does
// not record the result yet): the restore keeps going server-side.
throw new LocalError(status.status === "running"
? `Restore ${handle.restore_id} is still running after ${Math.round(waited.elapsedMs / 1000)}s; it continues server-side`
: `Restore ${handle.restore_id} is ready; read it with getRestore for its outcome`, context, {
code: status.status === "running" ? "SNAPSHOT_RESTORE_WAIT_TIMEOUT" : "SNAPSHOT_RESTORE_RESULT_UNAVAILABLE",
details: { restore_id: handle.restore_id, snapshot_id: snapshotId, project_id: projectId, status: status.status },
next_actions: handle.next_actions,
});
}
/** Status and outcome of one restore, including a background (`wait: false`) restore. */
async getRestore(projectId, snapshotId, restoreId) {
const context = "reading project snapshot restore";
assertProjectId(projectId, context);
assertSnapshotId(snapshotId, context);
if (!restoreId || typeof restoreId !== "string") {
throw new LocalError("snapshots.getRestore requires a restoreId", context);
}
return this.client.request(`${snapshotPath(projectId, snapshotId)}/restores/${encodeURIComponent(restoreId)}`, {
authMeta: {
method: "snapshots.getRestore",
capability: "project.snapshots.manage",
target: { project_id: projectId },
},
context,
});
}
}
function snapshotCollectionPath(projectId) {
return `/projects/v1/${encodeURIComponent(projectId)}/snapshots`;
}
function snapshotPath(projectId, snapshotId) {
return `${snapshotCollectionPath(projectId)}/${encodeURIComponent(snapshotId)}`;
}
function restoreBody(opts) {
return {
...(opts.includeAuth ? { include: ["auth"] } : {}),
...(opts.release !== undefined ? { release: opts.release } : {}),
};
}
function assertProjectId(value, context) {
if (!value || typeof value !== "string") {
throw new LocalError("snapshots helper requires a projectId", context);
}
}
function assertSnapshotId(value, context) {
if (!value || typeof value !== "string") {
throw new LocalError("snapshots helper requires a snapshotId", context);
}
}
//# sourceMappingURL=snapshots.js.map