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.

202 lines • 9.3 kB
import { ApiError, LocalError, NetworkError, NotAuthorizedError, PaymentRequired, Unauthorized } from "../errors.js"; const DEFAULT_SCOPE = "portable-runtime-v1"; const DEFAULT_AUTH = "stubs"; const DEFAULT_CONSISTENCY = "pause-writes"; const DEFAULT_POLL_INTERVAL_MS = 1_000; const DEFAULT_TIMEOUT_MS = 10 * 60_000; export class Archives { client; constructor(client) { this.client = client; } async create(projectId, opts = {}) { assertProjectId(projectId, "creating project archive"); const body = { scope: opts.scope ?? DEFAULT_SCOPE, auth: opts.auth ?? DEFAULT_AUTH, consistency: opts.consistency ?? DEFAULT_CONSISTENCY, }; const headers = opts.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : undefined; return this.client.request(archiveCollectionPath(projectId), { method: "POST", body, ...(headers ? { headers } : {}), authMeta: { method: "archives.create", capability: "project.archives.export", target: { project_id: projectId }, }, context: "creating project archive", }); } async get(projectId, archiveId) { assertProjectId(projectId, "getting project archive"); assertArchiveId(archiveId, "getting project archive"); return this.client.request(archivePath(projectId, archiveId), { authMeta: { method: "archives.get", capability: "project.archives.export", target: { project_id: projectId }, }, context: "getting project archive", }); } async wait(projectId, archiveId, opts = {}) { assertProjectId(projectId, "waiting for project archive"); assertArchiveId(archiveId, "waiting for project archive"); const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 100) { throw new LocalError("archives.wait pollIntervalMs must be an integer >= 100", "waiting for project archive"); } if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) { throw new LocalError("archives.wait timeoutMs must be a positive integer", "waiting for project archive"); } const started = Date.now(); for (;;) { const archive = await this.get(projectId, archiveId); await opts.onProgress?.(progressEvent(projectId, archive, "wait", "archive_export_status", "Archive export status updated.")); if (archive.status === "ready") return archive; if (archive.status === "failed" || archive.status === "expired") return archive; if (Date.now() - started >= timeoutMs) { throw new LocalError(`Timed out waiting for archive ${archiveId}`, "waiting for project archive", { code: "ARCHIVE_WAIT_TIMEOUT", details: { archive_id: archiveId, timeout_ms: timeoutMs } }); } await sleep(pollIntervalMs); } } async download(projectId, archiveId) { assertProjectId(projectId, "downloading project archive"); assertArchiveId(archiveId, "downloading project archive"); const archive = await this.get(projectId, archiveId); if (archive.status !== "ready") { throw new LocalError(`Archive ${archiveId} is ${archive.status}, not ready for download`, "downloading project archive", { code: archive.status === "expired" ? "ARCHIVE_EXPIRED" : "ARCHIVE_NOT_READY", details: { archive_id: archiveId, status: archive.status } }); } const path = `${archivePath(projectId, archiveId)}/download`; const auth = await this.client.credentials.getAuth(path, { method: "archives.download", capability: "project.archives.export", target: { project_id: projectId }, }); const headers = auth ? { ...auth } : {}; let res; try { res = await this.client.fetch(`${this.client.apiBase}${path}`, { headers }); } catch (err) { throw new NetworkError(`Network error while downloading project archive: ${err.message}`, err, "downloading project archive"); } if (!res.ok) { await throwDownloadError(res); } const bytes = new Uint8Array(await res.arrayBuffer()); return { archive, bytes, contentType: res.headers.get("content-type") ?? archive.content_type ?? "application/x-tar", filename: contentDispositionFilename(res.headers.get("content-disposition")) ?? `${archiveId}.r402ar`, }; } async export(projectId, opts = {}) { const created = await this.create(projectId, opts); await opts.onProgress?.(progressEvent(projectId, created, "create", "archive_export_created", "Archive export operation created.")); const archive = created.status === "ready" ? created : await this.wait(projectId, created.archive_id, opts); if (archive.status !== "ready") { throw new LocalError(`Archive export ended with status ${archive.status}`, "exporting project archive", { code: archive.status === "expired" ? "ARCHIVE_EXPIRED" : "ARCHIVE_EXPORT_FAILED", details: { archive_id: archive.archive_id, status: archive.status, error: archive.error } }); } const download = await this.download(projectId, archive.archive_id); await opts.onProgress?.(progressEvent(projectId, archive, "download", "archive_export_downloaded", "Archive bytes downloaded.")); return { ...download, created }; } } function archiveCollectionPath(projectId) { return `/projects/v1/${encodeURIComponent(projectId)}/archives`; } function archivePath(projectId, archiveId) { return `${archiveCollectionPath(projectId)}/${encodeURIComponent(archiveId)}`; } function assertProjectId(value, context) { if (!value || typeof value !== "string") { throw new LocalError("archive helper requires a projectId", context); } } function assertArchiveId(value, context) { if (!value || typeof value !== "string") { throw new LocalError("archive helper requires an archiveId", context); } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function progressEvent(projectId, archive, stage, event, message) { const complete = archive.status === "ready" || archive.status === "failed" || archive.status === "expired"; return { event, stage, resource_type: "project_archive", resource_id: archive.archive_id, project_id: projectId, status: archive.status, completed_units: complete ? 1 : 0, total_units: 1, code: archive.status === "failed" ? archiveCode(archive.error) : null, message, next_action: archive.next_action, retryable: archive.status === "running", context: { byte_count: archive.byte_count, sha256: archive.sha256, expires_at: archive.expires_at, }, }; } function archiveCode(error) { if (error && typeof error === "object" && !Array.isArray(error)) { const code = error.code; return typeof code === "string" ? code : null; } return null; } async function throwDownloadError(res) { const context = "downloading project archive"; const ct = res.headers.get("content-type") ?? ""; const body = ct.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => ""); const message = displayMessage(body, "Archive download failed"); if (res.status === 402) throw new PaymentRequired(`${message} while ${context}`, res.status, body, context); if (res.status === 403 && envelopeCode(body) === "NOT_AUTHORIZED") { throw new NotAuthorizedError(`${message} while ${context} (HTTP ${res.status})`, res.status, body, context); } if (res.status === 401 || res.status === 403) { throw new Unauthorized(`${message} while ${context} (HTTP ${res.status})`, res.status, body, context); } throw new ApiError(`${message} while ${context} (HTTP ${res.status})`, res.status, body, context); } function displayMessage(body, fallback) { if (body && typeof body === "object" && !Array.isArray(body)) { const obj = body; if (typeof obj.message === "string" && obj.message.length > 0) return obj.message; if (typeof obj.error === "string" && obj.error.length > 0) return obj.error; } return fallback; } function envelopeCode(body) { if (!body || typeof body !== "object" || Array.isArray(body)) return null; const code = body.code; return typeof code === "string" ? code : null; } function contentDispositionFilename(value) { if (!value) return null; const match = /filename="([^"]+)"/i.exec(value) ?? /filename=([^;]+)/i.exec(value); return match?.[1]?.trim() || null; } //# sourceMappingURL=archives.js.map