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.

195 lines • 9.67 kB
/** * `transfers` namespace — the unified project-transfer noun (v1.93+; * owned-org recipient shape v1.96+). * * Exposed as `r.admin.transfers.*`. Project transfer is ONE capability, * body-discriminated by recipient kind: a wallet recipient (`toWallet`, SIWX * bilateral signing), an email recipient (`toEmail`, the recipient accepts into * an org), or an owned org recipient (`toOrgId`, same-actor org move). All ride * the same `/transfers` surface — there is no separate `/handoffs` noun (the * gateway removed it in `unify-project-transfer-surface`). * * Gateway endpoints: * POST /projects/v1/:project_id/transfers — initiate; body { to_wallet } XOR { to_email } XOR { to_org_id } * GET /agent/v1/transfers/incoming — inbox (pending kinds, unioned) * GET /agent/v1/transfers/outgoing — outbox (pending kinds, unioned) * GET /agent/v1/transfers/:transfer_id — preview (kind-agnostic) * POST /agent/v1/transfers/:transfer_id/accept — the ONE completion; the row's recipient kind * picks the credential (wallet: to_wallet's SIWX; * email: a principal whose verified email matches, * body { org_id?, accept_retained_member? }) * POST /agent/v1/transfers/:transfer_id/cancel — cancel (kind-agnostic) * * Owner-side mutations against a project with a pending transfer return * 409 `PROJECT_HAS_PENDING_TRANSFER`. The SDK kernel surfaces that as * {@link TransferFreezeError} so agents can guide the user to cancel. Accepting * with the wrong credential for a row's kind (e.g. a wallet signature on an * email-addressed row) returns 409 `WRONG_COMPLETION_FOR_TRANSFER_KIND`; the * thrown error exposes an `accept_transfer` next action naming the credential * the row needs, on the SAME `transfer_id`. */ import { gateSecret } from "../secret-gate.js"; import { LocalError } from "../errors.js"; // ─── Class ─────────────────────────────────────────────────────────────────── export class Transfers { client; constructor(client) { this.client = client; } async initiate(input) { gateSecret(this.client, "admin.transfers.initiate", input); const toWallet = "toWallet" in input ? input.toWallet : undefined; const toEmail = "toEmail" in input ? input.toEmail : undefined; const toOrgId = "toOrgId" in input ? input.toOrgId : undefined; const hasWallet = typeof toWallet === "string" && toWallet.length > 0; const hasEmail = typeof toEmail === "string" && toEmail.length > 0; const hasOrg = typeof toOrgId === "string" && toOrgId.length > 0; const recipientCount = Number(hasWallet) + Number(hasEmail) + Number(hasOrg); if (recipientCount !== 1) { throw new LocalError("Provide exactly one of toWallet, toEmail, or toOrgId.", "initiating project transfer", { code: "VALIDATION_ERROR", details: { fields: ["toWallet", "toEmail", "toOrgId"] } }); } const path = `/projects/v1/${encodeURIComponent(input.projectId)}/transfers`; if (hasOrg) { rejectDefinedField(input, "billingPolicy", "org"); rejectDefinedField(input, "kysignedRecordId", "org"); rejectDefinedField(input, "retainMember", "org"); const body = { to_org_id: toOrgId }; if (input.message !== undefined) body.message = input.message; const result = await this.client.request(path, { method: "POST", body, context: "initiating project transfer", }); await persistProjectKeys(this.client, result); return result; } if (hasEmail) { rejectDefinedField(input, "billingPolicy", "email"); rejectDefinedField(input, "kysignedRecordId", "email"); const body = { to_email: toEmail }; if (input.message !== undefined) body.message = input.message; const retain = input.retainMember; if (retain !== undefined) body.retain_member = retain; return this.client.request(path, { method: "POST", body, context: "initiating project transfer", }); } const w = input; rejectDefinedField(w, "retainMember", "wallet"); const body = { to_wallet: toWallet }; if (w.billingPolicy !== undefined) body.billing_policy = w.billingPolicy; if (w.message !== undefined) body.message = w.message; if (w.kysignedRecordId !== undefined) body.kysigned_record_id = w.kysignedRecordId; return this.client.request(path, { method: "POST", body, context: "initiating project transfer", }); } /** * Fetch the preview document for a pending or terminal transfer of either * kind. The caller must be a party to it (wallet signer, addressed-email * principal, or offering-org member); other callers receive 403. Preview * lists secret NAMES (not values), custom domains, functions, CI bindings * that will be revoked at completion, the billing implications, and — on * email rows — the `retain_member` offer. */ async preview(transferId) { return this.client.request(`/agent/v1/transfers/${encodeURIComponent(transferId)}`, { context: "previewing project transfer" }); } /** * Accept an incoming transfer — the ONE completion, whatever the row's * address. A wallet-addressed row is accepted by the `to_wallet`'s SIWX * signature; an email-addressed row by a principal (sign-in session or * SIWX) whose verified email matches, into an org it owns (`orgId`) or, * omitted, a newly created wallet-less org. Both paths run the same atomic * transaction: ownership flip, grant/CI revoke, notification enqueue, the * persistent `secrets_rotation_advised` advisory, and an audit row. Both * return the new owner's project keys, which `accept` persists to the * keystore so the new owner can operate the project immediately. */ async accept(transferId, opts = {}) { gateSecret(this.client, "admin.transfers.accept", transferId, opts); const body = {}; if (opts.orgId !== undefined) body.org_id = opts.orgId; if (opts.acceptRetainedMember !== undefined) body.accept_retained_member = opts.acceptRetainedMember; const result = await this.client.request(`/agent/v1/transfers/${encodeURIComponent(transferId)}/accept`, { method: "POST", body, context: "accepting project transfer", }); await persistProjectKeys(this.client, result); return result; } /** * Cancel a pending transfer of any kind. The caller must be authorized for * the row's kind (a wallet signer, or an owner/admin of the offering org / * the addressed-email principal). Already-processed transfers return 409 * `TRANSFER_ALREADY_PROCESSED`. */ async cancel(transferId, opts) { const reason = opts?.reason; const body = {}; if (reason !== undefined) body.reason = reason; return this.client.request(`/agent/v1/transfers/${encodeURIComponent(transferId)}/cancel`, { method: "POST", body, context: "cancelling project transfer", }); } /** Pending transfers OFFERED TO the caller — wallet/email/future org rows, unioned. */ async listIncoming(opts = {}) { const q = buildPagination(opts); const path = q ? `/agent/v1/transfers/incoming?${q}` : "/agent/v1/transfers/incoming"; return this.client.request(path, { context: "listing incoming transfers", }); } /** Pending transfers INITIATED BY the caller — wallet/email/future org rows, unioned. */ async listOutgoing(opts = {}) { const q = buildPagination(opts); const path = q ? `/agent/v1/transfers/outgoing?${q}` : "/agent/v1/transfers/outgoing"; return this.client.request(path, { context: "listing outgoing transfers", }); } } async function persistProjectKeys(client, result) { if (result.project_id && result.anon_key && result.service_key) { const creds = client.credentials; if (creds.saveProject) { await creds.saveProject(result.project_id, { anon_key: result.anon_key, service_key: result.service_key, }); } if (creds.setActiveProject) { await creds.setActiveProject(result.project_id); } } } function rejectDefinedField(input, field, recipient) { if (input[field] !== undefined) { throw new LocalError(`${field} is not supported for ${recipient}-addressed project transfers.`, "initiating project transfer", { code: "VALIDATION_ERROR", details: { field, recipient } }); } } function buildPagination(opts) { const parts = []; if (opts.limit !== undefined) parts.push(`limit=${encodeURIComponent(String(opts.limit))}`); if (opts.after !== undefined) parts.push(`after=${encodeURIComponent(opts.after)}`); return parts.join("&"); } //# sourceMappingURL=transfers.js.map