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.

433 lines • 19 kB
/** * `admin` namespace — operations that don't fit a public * resource namespace cleanly: messages/contact plus internal finance reads. * * (The compound `init` and `status` flows live at the MCP/CLI edge because * they stitch together multiple SDK namespaces + local state.) */ import { gateSecret } from "../secret-gate.js"; import { LocalError } from "../errors.js"; import { Transfers } from "./transfers.js"; const FINANCE_WINDOWS = new Set(["24h", "7d", "30d", "90d"]); /** Request options carrying both proofs, or nothing when no session is at hand. */ export function sessionProofRequest(proofs) { if (!proofs) return {}; return { headers: { "SIGN-IN-WITH-X": proofs.siwx, Authorization: `Bearer ${proofs.token}` }, withAuth: false }; } /** * `r.admin.channels` — the Telegram notification-channel binding lifecycle * (connect / list / revoke). Mutations (`connectTelegram`, `revokeTelegram`) * require `operator_passkey` assurance; `connectTelegram` additionally * requires a VERIFIED contact email (bindings are addressed to it). See * `r.admin.setAgentContact` / `r.admin.verifyAgentContactEmail` and * `r.admin.startContactPasskeyEnrollment` (or `run402 login`) to reach that assurance level — * same ladder as {@link Admin.rotateWebhookSecret}. */ export class Channels { client; constructor(client) { this.client = client; } /** * Start binding a Telegram chat. Returns two single-use, 15-minute deep * links — `connect_url` for a private chat, `connect_group_url` for a * group — plus a `pending` binding id. A human taps ONE of the links and * starts the bot; poll {@link Channels.list} until the binding's `status` * flips to `"active"` (or `code_expires_at` passes and it's swept back to * `"revoked"`). * * Throws (via the generic SDK error hierarchy — check `err.code`) HTTP 503 * `TELEGRAM_CHANNEL_NOT_CONFIGURED` until the platform's dedicated * notification bot is provisioned, and HTTP 412 * `CONTACT_EMAIL_NOT_VERIFIED` when the caller has no verified email yet. */ async connectTelegram(opts = {}, proofs) { gateSecret(this.client, "admin.channels.connectTelegram", opts, proofs); const body = {}; if (opts.label !== undefined) body.label = opts.label; return this.client.request("/agent/v1/notifications/channels/telegram", { method: "POST", body, context: "connecting a Telegram notification channel", ...sessionProofRequest(proofs) }); } /** List every notification channel — email, webhook, and every live * (non-revoked) Telegram binding — for the authenticated wallet. */ async list() { return this.client.request("/agent/v1/notifications/channels", { context: "listing notification channels" }); } /** * Revoke a Telegram binding. Missing / already-revoked / another * caller's binding id all return the SAME not-found error * (authorize-before-reveal) — no existence oracle. */ async revokeTelegram(bindingId, proofs) { gateSecret(this.client, "admin.channels.revokeTelegram", bindingId, proofs); return this.client.request(`/agent/v1/notifications/channels/telegram/${encodeURIComponent(bindingId)}`, { method: "DELETE", context: "revoking a Telegram notification channel", ...sessionProofRequest(proofs) }); } } /** * `r.admin.rules` — Telegram routing rules: one match (ANDed * dimensions; an omitted dimension is a wildcard) → one Telegram binding. * Rules govern the Telegram channel ONLY in v1 — email/webhook keep their * existing preference-toggle semantics untouched. Mutations require * `operator_passkey` assurance. */ export class Rules { client; constructor(client) { this.client = client; } /** List the caller's routing rules, newest first. */ async list() { return this.client.request("/agent/v1/notifications/rules", { context: "listing notification routing rules" }); } /** * Create a routing rule. `telegramBindingId` must reference a binding this * caller owns and that is currently usable (`status: "active"`); an * unusable or foreign binding id returns the same 404 as a nonexistent one * (authorize-before-reveal). */ async create(input, proofs) { gateSecret(this.client, "admin.rules.create", input, proofs); const body = { telegram_binding_id: input.telegramBindingId }; if (input.projectId !== undefined) body.project_id = input.projectId; if (input.source !== undefined) body.source = input.source; if (input.eventTypes !== undefined) body.event_types = input.eventTypes; if (input.classes !== undefined) body.classes = input.classes; return this.client.request("/agent/v1/notifications/rules", { method: "POST", body, context: "creating a notification routing rule", ...sessionProofRequest(proofs) }); } /** * Patch a routing rule. PATCH semantics: only fields PRESENT on `patch` * are sent — `{ projectId: null }` clears that dimension back to * wildcard; omitting a field leaves it unchanged (see * {@link UpdateRoutingRulePatch}). */ async update(ruleId, patch, proofs) { gateSecret(this.client, "admin.rules.update", ruleId, patch, proofs); const body = {}; if ("projectId" in patch) body.project_id = patch.projectId; if ("source" in patch) body.source = patch.source; if ("eventTypes" in patch) body.event_types = patch.eventTypes; if ("classes" in patch) body.classes = patch.classes; if ("telegramBindingId" in patch) body.telegram_binding_id = patch.telegramBindingId; if ("enabled" in patch) body.enabled = patch.enabled; return this.client.request(`/agent/v1/notifications/rules/${encodeURIComponent(ruleId)}`, { method: "PATCH", body, context: "updating a notification routing rule", ...sessionProofRequest(proofs) }); } /** Delete a routing rule. */ async delete(ruleId, proofs) { gateSecret(this.client, "admin.rules.delete", ruleId, proofs); return this.client.request(`/agent/v1/notifications/rules/${encodeURIComponent(ruleId)}`, { method: "DELETE", context: "deleting a notification routing rule", ...sessionProofRequest(proofs) }); } } export class Admin { client; /** * Project transfer sub-namespace — unified wallet, email, and owned-org * project transfer surface. Access via `r.admin.transfers.{initiate, * preview, accept, claim, cancel, listIncoming, listOutgoing}`. */ transfers; /** * Telegram notification-channel binding lifecycle. Access via * `r.admin.channels.{connectTelegram, list, revokeTelegram}`. */ channels; /** * Telegram routing rules — one match (ANDed dimensions) to one binding. * Access via `r.admin.rules.{list, create, update, delete}`. */ rules; constructor(client) { this.client = client; this.transfers = new Transfers(client); this.channels = new Channels(client); this.rules = new Rules(client); } /** * Staff-scoped sub-client for an org id — the staff analog of * `r.org(id)`, kept on `r.admin` because these actions require platform-admin * (`X-Admin-Mode`) auth, a different principal from the member-facing * `r.org(id)`. Exposes `pinLease()` / `unpinLease()`. Lazy and synchronous. */ org(orgId) { return new ScopedAdminOrg(this, orgId); } /** * Staff-scoped sub-client for a project id. Exposes `archive(opts?)`, * `reactivate()`, and `finance(opts?)` with the id pre-bound. Lazy and * synchronous. */ project(projectId) { return new ScopedAdminProject(this, projectId); } /** * Send feedback to the Run402 developers. Requires an active tier. * * WRITE-ONLY: there is no inbox and no reply path. When an answer from a * human is required, raise an escalation instead. * * Also the way a promotion consent is relayed: after a deploy response * carries a `hand_to_member` next action, ask your human yes or no, and * on yes call `sendFeedback("promote: yes", { project_id, handle })`. */ async sendFeedback(message, opts) { const body = { message }; if (opts?.project_id) body.project_id = opts.project_id; if (opts?.handle) body.handle = opts.handle; return this.client.request("/feedback/v1", { method: "POST", body, context: "sending feedback", }); } /** * @deprecated Renamed to {@link sendFeedback}. Kept so code written against * the old name keeps compiling; it now posts to `/feedback/v1` like its * replacement. The `message` vocabulary is being freed for addressed * agent/human messaging, which is a different capability with a return path. */ async sendMessage(message, opts) { return this.sendFeedback(message, opts); } /** Register agent contact info and start email verification when needed. */ async setAgentContact(contact) { const body = { name: contact.name }; if (contact.email) body.email = contact.email; if (contact.webhook) body.webhook = contact.webhook; return this.client.request("/agent/v1/contact", { method: "POST", body, context: "setting agent contact", }); } /** Read the current agent contact assurance state for the wallet. */ async getAgentContactStatus() { return this.client.request("/agent/v1/contact/status", { method: "GET", context: "fetching agent contact status", }); } /** Start or resend the contact email reply challenge. */ async verifyAgentContactEmail() { return this.client.request("/agent/v1/contact/verify-email", { method: "POST", context: "starting agent contact email verification", }); } /** Email a passkey enrollment link to the verified contact email. */ async startContactPasskeyEnrollment() { return this.client.request("/agent/v1/contact/passkey/enroll", { method: "POST", context: "starting contact passkey enrollment", }); } // --------------------------------------------------------------------------- // Owner notifications (v1.55). // --------------------------------------------------------------------------- /** List notification audit rows (paginated, filterable). */ async listNotifications(opts = {}) { const q = []; if (opts.type) q.push(`type=${encodeURIComponent(opts.type)}`); if (opts.since) q.push(`since=${encodeURIComponent(opts.since)}`); if (opts.limit != null) q.push(`limit=${opts.limit}`); if (opts.after != null) q.push(`after=${encodeURIComponent(opts.after)}`); const url = q.length > 0 ? `/agent/v1/notifications?${q.join("&")}` : "/agent/v1/notifications"; return this.client.request(url, { method: "GET", context: "listing notifications", }); } /** Retrieve a single notification audit row by id. */ async getNotification(id) { return this.client.request(`/agent/v1/notifications/${encodeURIComponent(id)}`, { method: "GET", context: "fetching notification" }); } /** Read the current notification preferences. */ async getNotificationPreferences() { return this.client.request("/agent/v1/notifications/preferences", { method: "GET", context: "fetching notification preferences" }); } /** Patch notification preferences (assurance ladder applies). */ async setNotificationPreferences(patch, proofs) { gateSecret(this.client, "admin.setNotificationPreferences", patch, proofs); return this.client.request("/agent/v1/notifications/preferences", { method: "PATCH", body: patch, context: "updating notification preferences", ...sessionProofRequest(proofs), }); } /** * Trigger a real test notification. Sends a sample `project_past_due` * event through the normal worker pipeline (email/webhook); the audit row * is marked `is_test: true`. ALSO delivers a synthetic event through the * caller's Telegram routing rules end-to-end (binding + rule + render + * send) and reports a per-destination outcome in `telegram.destinations` * — pass `opts.source` / `opts.eventType` to target a specific rule's * filters instead of the default sample event. Rate-limited per wallet at * 1/min. */ async testNotification(opts = {}) { const body = {}; if (opts.source !== undefined) body.source = opts.source; if (opts.eventType !== undefined) body.event_type = opts.eventType; return this.client.request("/agent/v1/notifications/test", { method: "POST", body, context: "triggering test notification" }); } /** * Rotate the caller's webhook signing secret. The new plaintext secret * is returned EXACTLY once. The previous secret remains valid for 24 * hours (dual-secret grace window). Requires `operator_passkey` assurance. */ async rotateWebhookSecret(proofs) { gateSecret(this.client, "admin.rotateWebhookSecret", proofs); return this.client.request("/agent/v1/webhook-secret/rotate", { method: "POST", context: "rotating webhook signing secret", ...sessionProofRequest(proofs) }); } /** * Fetch per-project finance for staff. * * This is the same admin-only surface used by the Run402 Finance tab. It is * gated by platform-admin auth; project service keys are not sufficient. * Use the Node SDK with an admin wallet, pass `cookie` for browser * session auth, or provide a credential provider whose `getAuth()` returns * suitable admin headers. */ async getProjectFinance(projectId, opts = {}) { const window = opts.window ?? "30d"; if (!FINANCE_WINDOWS.has(window)) { throw new LocalError(`Invalid finance window: ${String(window)}. Expected one of: 24h, 7d, 30d, 90d.`, "fetching project finance"); } const headers = { "X-Admin-Mode": "1" }; if (opts.cookie) headers.Cookie = opts.cookie; return this.client.request(`/admin/api/finance/project/${encodeURIComponent(projectId)}?window=${encodeURIComponent(window)}`, { headers, context: "fetching project finance", }); } // ------------------------------------------------------------------------- // Staff-only project + organization actions (v1.57). // ------------------------------------------------------------------------- /** * Toggle a organization's `lease_perpetual` flag — the staff escape * hatch that pins every project in the organization (replaces the v1.56 * per-project `pin` removed in v1.57). When enabling on an organization in a * grace state, the gateway reactivates inline and reports it via * `reactivated: true`. * * Platform-admin only. Calls * `POST /orgs/v1/admin/:org_id/lease-perpetual`. */ /** * Shared, non-deprecated implementation behind {@link setLeasePerpetual} and * the `r.admin.org(id).pinLease()/unpinLease()` handle. * @internal */ _setLeasePerpetual(organizationId, perpetual) { return this.client.request(`/orgs/v1/admin/${encodeURIComponent(organizationId)}/lease-perpetual`, { method: "POST", headers: { "X-Admin-Mode": "1" }, body: { lease_perpetual: perpetual }, context: "setting organization lease_perpetual", }); } /** * Staff moderation action — archive a single project (ToS / abuse). * Sets `projects.archived_at` to NOW(). Independent of organization-level * lifecycle; the rest of the organization's projects continue serving. * * Platform-admin only. Calls `POST /projects/v1/admin/:id/archive`. */ async archiveProject(projectId, opts = {}) { const body = {}; if (opts.reason !== undefined) body.reason = opts.reason; return this.client.request(`/projects/v1/admin/${encodeURIComponent(projectId)}/archive`, { method: "POST", headers: { "X-Admin-Mode": "1" }, body, context: "archiving project", }); } /** * Staff "un-archive" — flips `projects.archived_at` back to NULL. It does * not touch organization-level lifecycle. To reactivate a grace-state * organization, either set a tier or set `lease_perpetual: true` * via {@link setLeasePerpetual}. * * Platform-admin only. Calls `POST /projects/v1/admin/:id/reactivate`. */ async reactivateProject(projectId) { return this.client.request(`/projects/v1/admin/${encodeURIComponent(projectId)}/reactivate`, { method: "POST", headers: { "X-Admin-Mode": "1" }, context: "reactivating project", }); } } /** * Staff-scoped sub-client for a single org, returned by `r.admin.org(id)`. * Replaces the boolean `admin.setLeasePerpetual(orgId, perpetual)` with two * intent-named verbs. Carries platform-admin auth; staff-only errors surface * at call time. */ export class ScopedAdminOrg { admin; orgId; constructor(admin, orgId) { this.admin = admin; this.orgId = orgId; } /** Pin the org's lease (`lease_perpetual = true`). */ pinLease() { return this.admin._setLeasePerpetual(this.orgId, true); } /** Unpin the org's lease (`lease_perpetual = false`). */ unpinLease() { return this.admin._setLeasePerpetual(this.orgId, false); } } /** * Staff-scoped sub-client for a single project, returned by * `r.admin.project(id)`. The project id is pre-bound; methods carry * platform-admin auth. */ export class ScopedAdminProject { admin; projectId; constructor(admin, projectId) { this.admin = admin; this.projectId = projectId; } /** Archive the project (staff moderation). */ archive(opts = {}) { return this.admin.archiveProject(this.projectId, opts); } /** Un-archive the project. */ reactivate() { return this.admin.reactivateProject(this.projectId); } /** Read per-project finance (staff Finance tab). */ finance(opts = {}) { return this.admin.getProjectFinance(this.projectId, opts); } } //# sourceMappingURL=admin.js.map