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.

534 lines • 19.5 kB
import { LocalError } from "./errors.js"; export const RUN402_APP_SCHEMA_ID = "https://run402.com/schemas/run402-app.v1.schema.json"; export const RUN402_APP_MANIFEST_FILENAME = "run402.json"; export const RUN402_RELEASE_MANIFEST_FILENAME = "run402.release.json"; export const RUN402_APP_SPEC_VERSION = 1; export const RUN402_APP_INSTALL_GRAPH_VERSION = "run402.app_install_graph.v1"; export const RUN402_APP_UP_RESULT_KIND = "run402.up.result"; export const RUN402_APP_UP_RESULT_SCHEMA_VERSION = "run402.up.result.v1"; export const RUN402_APP_UP_RESULT_SCHEMA_URL = "https://run402.com/schemas/run402-up-result.v1.schema.json"; export const RUN402_BINDING_CLASSES = [ "generated_config_binding", "generated_secret_binding", "user_secret", ]; export const RUN402_BINDING_SCOPES = [ "build", "runtime", "client", "verify", "template", ]; const LOGICAL_RESOURCE_NAME = /^[a-z][a-z0-9_]*$/; const TEMPLATE_REF = /\$\{([^}]+)\}/g; const APP_SPEC_ERROR_CONTEXT = "compiling Run402 app manifest"; export async function compileRun402AppInstallGraph(spec, opts = {}) { assertRun402AppSpec(spec); const specDigest = await sha256Canonical(spec); const bindings = compileBindings(spec); validateReleaseEmailTriggerMailboxRefs(spec.release, appInstallBindingNames(bindings)); const nodes = []; await addNode(nodes, opts, { id: "discover", kind: "discover", mutation: "none", depends_on: [], intent: { source: opts.source ?? null, app_id: spec.app.id, spec_digest: specDigest, }, }); await addNode(nodes, opts, { id: "account.ensure", kind: "account.ensure", mutation: "remote", depends_on: ["discover"], spend: true, intent: { tier: "ready" }, }); await addNode(nodes, opts, { id: "project.ensure", kind: "project.ensure", mutation: "remote", depends_on: ["account.ensure"], intent: { name: opts.name ?? spec.project.name ?? null, project_id: spec.project.id ?? null, app_id: spec.app.id, }, }); const projectReadyNode = spec.project.origin ? "origin.ensure" : "project.ensure"; if (spec.project.origin) { await addNode(nodes, opts, { id: "origin.ensure", kind: "origin.ensure", mutation: "remote", depends_on: ["project.ensure"], intent: spec.project.origin, }); } const mailboxNames = sortedKeys(spec.resources?.mailboxes); for (const name of mailboxNames) { assertLogicalResourceName(name); await addNode(nodes, opts, { id: `mailbox.${name}.ensure`, kind: "mailbox.ensure", mutation: "remote", depends_on: ["project.ensure"], intent: { logical_name: name, bindings: mailboxRuntimeBindings(name), ...spec.resources?.mailboxes?.[name], }, }); } const bindingDeps = mailboxNames.length > 0 ? mailboxNames.map((name) => `mailbox.${name}.ensure`) : [projectReadyNode]; await addNode(nodes, opts, { id: "bindings.resolve", kind: "bindings.resolve", mutation: "none", depends_on: bindingDeps, intent: bindings, }); const secretNames = sortedKeys(spec.secrets); if (secretNames.length > 0) { await addNode(nodes, opts, { id: "secrets.ensure", kind: "secrets.ensure", mutation: "remote", depends_on: ["project.ensure"], intent: Object.fromEntries(secretNames.map((name) => [ name, { required: spec.secrets?.[name]?.required ?? false, source_env: spec.secrets?.[name]?.source_env ?? name, }, ])), }); } const buildMode = spec.build?.mode ?? "local"; if (spec.build) { await addNode(nodes, opts, { id: `build.${buildMode}`, kind: "build", mutation: buildMode === "local" ? "local" : "remote", depends_on: [ "bindings.resolve", ...(secretNames.length > 0 ? ["secrets.ensure"] : []), ], intent: spec.build, }); } await addNode(nodes, opts, { id: "release.apply", kind: "release.apply", mutation: "remote", depends_on: [ spec.build ? `build.${buildMode}` : "bindings.resolve", ...(secretNames.length > 0 && !spec.build ? ["secrets.ensure"] : []), ], intent: spec.release, }); const webhookNames = sortedKeys(spec.resources?.webhooks); for (const name of webhookNames) { assertLogicalResourceName(name); const webhook = spec.resources?.webhooks?.[name]; await addNode(nodes, opts, { id: `webhook.${name}.ensure`, kind: "webhook.ensure", mutation: "remote", depends_on: [ "release.apply", `mailbox.${webhook?.mailbox}.ensure`, ], intent: { logical_name: name, ...webhook, }, }); } for (const check of spec.verify?.http ?? []) { await addNode(nodes, opts, { id: `verify.http.${check.id}`, kind: "verify.http", mutation: "none", depends_on: webhookNames.length > 0 ? webhookNames.map((name) => `webhook.${name}.ensure`) : ["release.apply"], intent: check, }); } const graphDigest = await sha256Canonical(nodes.map((node) => ({ id: node.id, kind: node.kind, depends_on: node.depends_on, input_digest: node.input_digest, mutation: node.mutation, destructive: node.destructive, spend: node.spend, }))); return { schema_version: RUN402_APP_INSTALL_GRAPH_VERSION, app_id: spec.app.id, source: opts.source ?? null, spec_digest: specDigest, graph_digest: graphDigest, nodes, release_spec: spec.release, bindings, }; } export function generatedMailboxBindings(logicalName) { assertLogicalResourceName(logicalName); const suffix = logicalName.toUpperCase(); return { id: `RUN402_MAILBOX_${suffix}_ID`, address: `RUN402_MAILBOX_${suffix}_ADDRESS`, }; } export function mailboxRuntimeBindings(logicalName) { const names = generatedMailboxBindings(logicalName); return [ { env: names.id, binding_class: "generated_config_binding", scopes: ["runtime", "template"], source: `resources.mailboxes.${logicalName}.id`, }, { env: names.address, binding_class: "generated_config_binding", scopes: ["runtime", "template"], source: `resources.mailboxes.${logicalName}.address`, }, ]; } export function platformMetadataBindings(opts = {}) { const bindings = [ { env: "RUN402_PROJECT_ID", binding_class: "generated_config_binding", scopes: ["runtime", "verify", "template"], source: "project.id", }, { env: "RUN402_API_BASE_URL", binding_class: "generated_config_binding", scopes: ["runtime", "verify", "template"], source: "platform.api_base_url", }, { env: "RUN402_RELEASE_ID", binding_class: "generated_config_binding", scopes: ["runtime", "verify", "template"], source: "release.id", }, { env: "RUN402_DEPLOYMENT_ID", binding_class: "generated_config_binding", scopes: ["runtime", "verify", "template"], source: "deployment.id", }, ]; if (opts.publicOriginKnown !== false) { bindings.push({ env: "RUN402_PUBLIC_ORIGIN", binding_class: "generated_config_binding", scopes: ["runtime", "verify", "template"], source: "project.public_origin", }); } return bindings; } export function createRun402AppUpResult(input) { const endedAt = input.ended_at ?? new Date().toISOString(); const started = Date.parse(input.started_at); const ended = Date.parse(endedAt); const diagnostics = input.diagnostics ?? []; const nextActions = input.next_actions ?? []; const steps = input.graph.nodes.map((node) => { const nodeDiagnostics = diagnostics.filter((diagnostic) => diagnostic.node_id === node.id); const nodeNextActions = nextActions.filter((action) => action.node_id === node.id); const status = input.blocked_node_id === node.id ? "blocked" : node.status; return { id: node.id, kind: node.kind, status, mutation: node.mutation, destructive: node.destructive, spend: node.spend, idempotent: node.idempotent, ...(node.ensure_key ? { ensure_key: node.ensure_key } : {}), input_digest: node.input_digest, depends_on: node.depends_on, ...(nodeDiagnostics.length > 0 ? { diagnostics: nodeDiagnostics } : {}), ...(nodeNextActions.length > 0 ? { next_actions: nodeNextActions } : {}), }; }); return { kind: RUN402_APP_UP_RESULT_KIND, schema_version: RUN402_APP_UP_RESULT_SCHEMA_VERSION, schema_url: RUN402_APP_UP_RESULT_SCHEMA_URL, status: input.status, operation_id: input.operation_id ?? null, install_id: input.install_id ?? null, spec_digest: input.graph.spec_digest, graph_digest: input.graph.graph_digest, source: input.graph.source, manifest_path: input.manifest_path, started_at: input.started_at, ended_at: endedAt, duration_ms: Number.isFinite(started) && Number.isFinite(ended) ? Math.max(0, ended - started) : 0, approval_policy: { yes: input.approval_policy?.yes ?? false, allow_prune: input.approval_policy?.allow_prune ?? false, max_spend_usd: input.approval_policy?.max_spend_usd ?? null, build_mode: input.approval_policy?.build_mode ?? null, shell_build_approved: input.approval_policy?.shell_build_approved ?? false, }, dry_run: input.dry_run, project: { id: input.project_id ?? null, name: input.project_name ?? null, public_origin: input.public_origin ?? null, }, steps, resources: appResourceSummary(input.graph), release: { operation_id: null, release_id: null, spec: input.graph.release_spec, }, verification: { http: verificationSummary(input.graph), }, ...(input.verify ? { verify: input.verify } : {}), diagnostics, next_actions: nextActions, graph: input.graph, }; } function compileBindings(spec) { const mailboxes = {}; for (const name of sortedKeys(spec.resources?.mailboxes)) { mailboxes[name] = generatedMailboxBindings(name); } return { mailboxes }; } function appInstallBindingNames(bindings) { const names = new Set(); for (const binding of Object.values(bindings.mailboxes)) { names.add(binding.id); names.add(binding.address); } return names; } function validateReleaseEmailTriggerMailboxRefs(release, bindings) { for (const [name, fn] of Object.entries(release.functions?.replace ?? {})) { validateFunctionEmailTriggerMailboxRefs(fn, `release.functions.replace.${name}`, bindings); } for (const [name, fn] of Object.entries(release.functions?.patch?.set ?? {})) { validateFunctionEmailTriggerMailboxRefs(fn, `release.functions.patch.set.${name}`, bindings); } } function validateFunctionEmailTriggerMailboxRefs(fn, resource, bindings) { if (!isPlainRecord(fn) || !Array.isArray(fn.triggers)) return; for (let i = 0; i < fn.triggers.length; i++) { const trigger = fn.triggers[i]; if (!isPlainRecord(trigger) || trigger.type !== "email") continue; validateTemplateRefsInString(typeof trigger.mailbox === "string" ? trigger.mailbox : undefined, `${resource}.triggers.${i}.mailbox`, bindings); } } function validateTemplateRefsInString(value, resource, allowed) { if (value === undefined) return; for (const match of value.matchAll(TEMPLATE_REF)) { const ref = match[1]; if (!ref || !allowed.has(ref)) { throw appSpecInvalid(`${resource} has unresolved template reference '${ref ?? ""}'`, { field_path: resource, reference: ref ?? "", }); } } } function appResourceSummary(graph) { const mailboxes = {}; const webhooks = {}; const allBindings = platformMetadataBindings(); for (const name of sortedKeys(graph.bindings.mailboxes)) { const bindings = mailboxRuntimeBindings(name); mailboxes[name] = { id: null, address: null, bindings, }; allBindings.push(...bindings); } for (const node of graph.nodes) { if (node.kind !== "webhook.ensure" || !isPlainRecord(node.details)) continue; const logicalName = String(node.details.logical_name ?? ""); if (!logicalName) continue; webhooks[logicalName] = { id: null, mailbox: String(node.details.mailbox ?? ""), url: String(node.details.url ?? ""), events: Array.isArray(node.details.events) ? node.details.events.map((event) => String(event)) : [], enabled: node.details.enabled !== false, }; } const userSecrets = {}; const secretsNode = graph.nodes.find((node) => node.id === "secrets.ensure"); if (secretsNode && isPlainRecord(secretsNode.details)) { for (const name of sortedKeys(secretsNode.details)) { const entry = secretsNode.details[name]; userSecrets[name] = { required: isPlainRecord(entry) ? entry.required === true : false, source_env: isPlainRecord(entry) && typeof entry.source_env === "string" ? entry.source_env : null, satisfied: false, }; } } return { mailboxes, webhooks, bindings: allBindings, user_secrets: userSecrets, }; } function verificationSummary(graph) { return graph.nodes .filter((node) => node.kind === "verify.http") .map((node) => { const details = isPlainRecord(node.details) ? node.details : {}; const expect = isPlainRecord(details.expect) ? details.expect : {}; return { id: String(details.id ?? node.id.replace(/^verify\.http\./, "")), status: node.status, ...(typeof details.path === "string" ? { path: details.path } : {}), ...(typeof details.url === "string" ? { url: details.url } : {}), expected_status: typeof expect.status === "number" ? expect.status : 200, }; }); } async function addNode(nodes, opts, input) { const inputDigest = await sha256Canonical({ id: input.id, kind: input.kind, intent: input.intent, }); const idempotent = input.mutation === "remote"; nodes.push({ id: input.id, kind: input.kind, depends_on: input.depends_on, input_digest: inputDigest, status: "planned", mutation: input.mutation, idempotent, ...(idempotent ? { ensure_key: ensureKey(opts, input.id, inputDigest) } : {}), destructive: input.destructive ?? false, spend: input.spend ?? false, details: isPlainRecord(input.intent) ? input.intent : { value: input.intent }, }); } function ensureKey(opts, nodeId, inputDigest) { const root = opts.root_idempotency_key ?? opts.name ?? "run402-app-up"; return `appup:${root}:${nodeId}:${inputDigest.slice("sha256:".length, "sha256:".length + 16)}`; } async function sha256Canonical(value) { const bytes = new TextEncoder().encode(canonicalizeJson(value)); const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes); const hex = [...new Uint8Array(digest)] .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); return `sha256:${hex}`; } function canonicalizeJson(value) { if (value === null) return "null"; if (typeof value === "boolean") return value ? "true" : "false"; if (typeof value === "number") { if (!Number.isFinite(value)) { throw appSpecInvalid("canonicalizeJson: unsupported non-finite number"); } return JSON.stringify(value); } if (typeof value === "string") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(canonicalizeJson).join(",")}]`; if (isPlainRecord(value)) { const keys = Object.keys(value) .filter((key) => value[key] !== undefined) .sort(); return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalizeJson(value[key])}`).join(",")}}`; } throw appSpecInvalid("canonicalizeJson: unsupported value type"); } function sortedKeys(value) { return Object.keys(value ?? {}).sort(); } function assertLogicalResourceName(value) { if (!LOGICAL_RESOURCE_NAME.test(value)) { throw appSpecInvalid(`logical resource name must match [a-z][a-z0-9_]*: ${value}`, { value, }); } } function appSpecInvalid(message, details) { return new LocalError(message, APP_SPEC_ERROR_CONTEXT, { code: "APP_SPEC_INVALID", details, }); } function assertRun402AppSpec(value) { if (!isPlainRecord(value)) { throw appSpecInvalid("Run402 app manifest must be a JSON object", { field_path: "$", expected: "object", }); } if (value.spec_version !== RUN402_APP_SPEC_VERSION) { throw appSpecInvalid(`spec_version must be ${RUN402_APP_SPEC_VERSION}`, { field_path: "spec_version", expected: RUN402_APP_SPEC_VERSION, observed: value.spec_version ?? null, }); } if (!isPlainRecord(value.app) || typeof value.app.id !== "string" || value.app.id.trim() === "") { throw appSpecInvalid("app.id must be a non-empty string", { field_path: "app.id", }); } if (!isPlainRecord(value.project)) { throw appSpecInvalid("project must be an object", { field_path: "project", expected: "object", }); } if (!isPlainRecord(value.release)) { throw appSpecInvalid("release must be an object", { field_path: "release", expected: "object", }); } } function isPlainRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } //# sourceMappingURL=app-up.js.map