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.
599 lines • 25.4 kB
TypeScript
/**
* `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 type { Client } from "../kernel.js";
import { Transfers } from "./transfers.js";
export interface AgentContact {
name: string;
email?: string;
webhook?: string;
}
export type AgentEmailVerificationStatus = "none" | "pending" | "verified";
export type AgentPasskeyBindingStatus = "none" | "pending" | "verified";
export type AgentAssuranceLevel = "wallet_only" | "email_pending" | "email_verified" | "passkey_pending" | "operator_passkey";
export interface AgentContactResult {
wallet: string;
name: string;
email: string | null;
webhook: string | null;
email_verification_status: AgentEmailVerificationStatus;
passkey_binding_status: AgentPasskeyBindingStatus;
assurance_level: AgentAssuranceLevel;
email_verified_at: string | null;
email_verified_message_id: string | null;
email_challenge_sent_at: string | null;
passkey_bound_at: string | null;
active_operator_passkey_id: string | null;
updated_at: string;
verification_retry_after_seconds?: number;
enrollment_sent_to?: string;
}
export interface SendMessageResult {
status: string;
}
export interface FeedbackSendOptions {
/**
* Project this feedback concerns. Required to relay a promotion consent
* (a `hand_to_member` `next_actions` entry from a deploy response) —
* the server resolves the project's site URL, org, and the sender's live
* presence name for the delivered message.
*/
project_id?: string;
/** Sender's X/Twitter handle, at most 64 characters. Delivered as-is; stored nowhere else. */
handle?: string;
}
export type AdminFinanceWindow = "24h" | "7d" | "30d" | "90d";
export interface AdminProjectFinanceOptions {
/** Time window for the finance rollup. Defaults to "30d". */
window?: AdminFinanceWindow;
/**
* Optional admin session cookie header. Staff can pass the value of
* RUN402_ADMIN_COOKIE when they want browser-session auth; otherwise the
* credential provider's normal auth headers are used.
*/
cookie?: string;
}
export interface AdminProjectFinanceResult {
project_id: string;
project_name: string;
window: AdminFinanceWindow;
revenue_usd_micros: number;
direct_cost_usd_micros: number;
direct_margin_usd_micros: number;
revenue_breakdown: {
tier_fees_usd_micros: number;
email_packs_usd_micros: number;
kms_rental_usd_micros: number;
kms_sign_fees_usd_micros: number;
per_call_sku_usd_micros: number;
};
direct_cost_breakdown: Array<{
category: string;
cost_usd_micros: number;
}>;
notes: string;
}
export type NotificationKind = "digest" | "lifecycle_event" | "threshold_alert" | "missing_verified_recipient";
export type NotificationChannel = "email" | "webhook" | "skipped";
export type NotificationDeliveryStatus = "delivered" | "failed_transient" | "failed_permanent" | "skipped_no_recipient" | "skipped_disabled";
export interface NotificationRow {
id: string;
recipient_email: string | null;
kind: NotificationKind;
event_type: string | null;
channel: NotificationChannel;
delivery_status: NotificationDeliveryStatus;
delivery_error: string | null;
attempt_count: number;
is_test: boolean;
related_project_id: string | null;
related_org_id: string | null;
related_wallet_address: string | null;
created_at: string;
redacted_at: string | null;
/** Payload JSON, or null when the row has been redacted. */
payload: Record<string, unknown> | null;
}
export interface ListNotificationsOptions {
type?: string;
/** ISO timestamp; only notifications created at or after this time. */
since?: string;
/** Default 50, max 200. */
limit?: number;
/** Opaque keyset cursor — page forward from a prior page's `next_cursor`. */
after?: string;
}
export interface ListNotificationsResult {
notifications: NotificationRow[];
has_more: boolean;
next_cursor: string | null;
}
export interface NotificationPreferences {
channels: {
email: boolean;
webhook: boolean;
};
webhook_url: string | null;
webhook_signing_secret_configured: boolean;
digest_cadence: "off" | "daily" | "weekly" | "monthly";
/** 1=Monday..7=Sunday. */
digest_day_of_week: number;
/** 0..23 UTC. */
digest_hour_utc: number;
threshold_alerts: "off" | "digest_only" | "immediate";
lifecycle_events: "off" | "critical_only" | "all";
/** Schema-enforced; always "always". */
security_events: "always";
/** BCP-47 (e.g. "en-US"). */
locale: string;
/** IANA timezone (e.g. "UTC"). */
timezone: string;
}
export type NotificationPreferencesPatch = Partial<Omit<NotificationPreferences, "webhook_signing_secret_configured" | "security_events">> & {
/** Cannot be changed away from "always" — server returns 400. */
security_events?: "always";
};
export interface TestNotificationOptions {
/**
* Route the synthetic test event as if it came from the app lane
* (`project_events.source = 'app'`) or the platform. Defaults to
* `"platform"` on the gateway when omitted.
*/
source?: "app" | "platform";
/**
* Synthetic `event_type` override (flat snake_case,
* `^[a-z][a-z0-9_]{2,63}$`). Use this to exercise a specific routing rule's
* `event_types` filter precisely. Defaults to the gateway's built-in
* sample event when omitted.
*/
eventType?: string;
}
/** One Telegram destination's outcome from a `testNotification()` call —
* present only when the caller has a routing rule matching the synthetic
* event. Empty `telegram.destinations` is Faithful (no matching rule), not
* an error. */
export interface TestNotificationDestination {
binding_id: string;
label: string | null;
delivered: boolean;
/** Present on failures. `true` = retryable (429/5xx/timeout/rate-limited);
* `false` = permanent (bad chat, bot blocked/removed). */
transient?: boolean;
description?: string;
}
export interface TestNotificationResult {
status: "delivered" | "skipped" | "queued";
source_event_id: string;
drained: {
claimed: number;
delivered: number;
skipped: number;
failed_transient: number;
failed_permanent: number;
};
/** Telegram delivery report for the synthetic event, routed through the
* caller's normal rules — the full binding + rule + render + send
* chain, not just email/webhook. */
telegram: {
destinations: TestNotificationDestination[];
};
note: string;
}
export interface RotateWebhookSecretResult {
webhook_signing_secret: string;
rotated_at: string;
grace_window_hours: number;
note: string;
}
export type TelegramBindingStatus = "pending" | "active" | "revoked";
/**
* One Telegram binding as returned by `GET /agent/v1/notifications/channels`
* (`telegram[]`) and `r.admin.channels.list()`. Never carries the raw
* connect code — codes are single-use, hashed at rest, and returned only
* once, inline in {@link ConnectTelegramResult}.
*/
export interface TelegramChannelBinding {
id: string;
recipient_email: string;
status: TelegramBindingStatus;
chat_id: number | null;
chat_type: string | null;
chat_title: string | null;
label: string | null;
consecutive_failures: number;
/** Set once auto-disabled after 10 consecutive hard delivery failures. */
disabled_at: string | null;
/** Only set while `status === "pending"` — the connect code's 15-min TTL. */
code_expires_at: string | null;
created_at: string;
activated_at: string | null;
}
export interface ConnectTelegramOptions {
/** Human-readable label for the chat (e.g. `"kychon alerts"`), 1-64 chars. */
label?: string;
}
/**
* one-passkey-per-person: the two proofs a wallet-authenticated notification
* mutation may carry on ONE request — the `SIGN-IN-WITH-X` wallet signature
* (which contact) and the person's sign-in session bearer (the passkey
* assurance). A person signed in with `run402 login` never enrolls a second
* passkey: the gateway accepts a passkey-fresh session for the wallet
* contact's verified email as `operator_passkey` assurance (the stored
* assurance level's wire spelling). When omitted, the request carries the
* provider's wallet auth alone.
*/
export interface SessionProofs {
/** The `SIGN-IN-WITH-X` header value for the target path. */
siwx: string;
/** The sign-in session token (`Authorization: Bearer`). */
token: string;
}
/** Request options carrying both proofs, or nothing when no session is at hand. */
export declare function sessionProofRequest(proofs?: SessionProofs): {
headers?: Record<string, string>;
withAuth?: boolean;
};
export interface ConnectTelegramNextAction {
type: string;
method?: string;
path?: string;
why?: string;
}
export interface ConnectTelegramResult {
binding_id: string;
status: "pending";
/** `t.me/<bot>?start=<code>` — tap to bind a PRIVATE chat. Single-use, 15-min TTL. */
connect_url: string;
/** `t.me/<bot>?startgroup=<code>` — tap to bind a GROUP chat. Same code/TTL as {@link connect_url} (whichever is tapped first consumes it). */
connect_group_url: string;
code_expires_at: string;
label: string | null;
next_actions: ConnectTelegramNextAction[];
}
export interface NotificationChannelsResult {
email: {
address: string | null;
verified: boolean;
};
webhook: {
configured: boolean;
url: string | null;
secret_configured: boolean;
};
/** Every live (non-revoked) Telegram binding for this caller, newest first. */
telegram: TelegramChannelBinding[];
}
export interface RevokeTelegramResult {
status: "revoked";
binding_id: string;
}
/**
* `"app"` = app-emitted business events (`project_events.source = 'app'`,
* e.g. `events.emit(...)` from `@run402/functions`); `"platform"` = every
* non-app platform event (deploys, lifecycle, verification, ...). Absent /
* `null` on a rule is a wildcard — matches both.
*/
export type RoutingRuleSource = "app" | "platform";
/**
* Wire-shaped routing rule. Every match dimension (`project_id`, `source`,
* `event_types`, `classes`) is ANDed; `null` is a wildcard for that
* dimension. An explicit empty array (`event_types: []` / `classes: []`)
* matches NOTHING — Postgres `TEXT[]` semantics, deliberately different from
* the "`[]` means unfiltered" convention used by some read-filter query
* params elsewhere in this SDK. One rule always targets exactly one Telegram
* binding; overlapping rules that resolve to the same binding are deduped by
* the gateway at delivery time (one message, not one per matching rule).
*/
export interface RoutingRule {
id: string;
recipient_email: string;
project_id: string | null;
source: RoutingRuleSource | null;
event_types: string[] | null;
classes: string[] | null;
channel: "telegram";
telegram_binding_id: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
/**
* `r.admin.rules.create(...)` input. Every match dimension is optional
* (absent = wildcard); `telegramBindingId` is the only required field. An
* all-wildcard rule (every event on every project routes to one chat) is
* legal.
*/
export interface CreateRoutingRuleInput {
telegramBindingId: string;
projectId?: string | null;
source?: RoutingRuleSource | null;
eventTypes?: string[] | null;
classes?: string[] | null;
}
/**
* `r.admin.rules.update(...)` patch. PATCH semantics: a field OMITTED from
* this object leaves the stored value unchanged; a field explicitly set to
* `null` CLEARS that dimension back to wildcard. There is no wire difference
* between "omitted" and "set to `undefined`" — both drop the key from the
* JSON request body, so the gateway sees no instruction to change it.
*/
export interface UpdateRoutingRulePatch {
projectId?: string | null;
source?: RoutingRuleSource | null;
eventTypes?: string[] | null;
classes?: string[] | null;
telegramBindingId?: string;
enabled?: boolean;
}
export interface ListRoutingRulesResult {
rules: RoutingRule[];
}
export interface CreateRoutingRuleResult extends RoutingRule {
next_actions: ConnectTelegramNextAction[];
}
export interface DeleteRoutingRuleResult {
deleted: true;
rule_id: string;
}
/**
* `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 declare class Channels {
private readonly client;
constructor(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.
*/
connectTelegram(opts?: ConnectTelegramOptions, proofs?: SessionProofs): Promise<ConnectTelegramResult>;
/** List every notification channel — email, webhook, and every live
* (non-revoked) Telegram binding — for the authenticated wallet. */
list(): Promise<NotificationChannelsResult>;
/**
* 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.
*/
revokeTelegram(bindingId: string, proofs?: SessionProofs): Promise<RevokeTelegramResult>;
}
/**
* `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 declare class Rules {
private readonly client;
constructor(client: Client);
/** List the caller's routing rules, newest first. */
list(): Promise<ListRoutingRulesResult>;
/**
* 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).
*/
create(input: CreateRoutingRuleInput, proofs?: SessionProofs): Promise<CreateRoutingRuleResult>;
/**
* 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}).
*/
update(ruleId: string, patch: UpdateRoutingRulePatch, proofs?: SessionProofs): Promise<RoutingRule>;
/** Delete a routing rule. */
delete(ruleId: string, proofs?: SessionProofs): Promise<DeleteRoutingRuleResult>;
}
export interface SetLeasePerpetualResult {
status: "ok";
org_id: string;
lease_perpetual: boolean;
/**
* `true` when the toggle was `lease_perpetual: true` AND the account was in
* a grace state (past_due / frozen / dormant) and got pulled back to
* `active` inline. `false` otherwise (account was already active, or the
* toggle disabled perpetual).
*/
reactivated: boolean;
}
export interface ArchiveProjectOptions {
/** Free-text moderation reason recorded in the audit log. */
reason?: string;
}
export interface ArchiveProjectResult {
status: "ok";
project_id: string;
/** ISO timestamp of the archive action. Absent when the project was already archived. */
archived_at?: string;
/** Echoes the moderator-supplied reason when the project was newly archived. */
reason?: string;
/** Set when the project was already archived; archived_at is then omitted. */
note?: "already archived";
}
export interface ReactivateProjectResult {
status: "ok";
project_id: string;
/** `true` when the call un-archived a previously archived project. */
reactivated?: true;
/** Set when the project was not archived to begin with — the call is a no-op. */
note?: "not archived";
}
export declare class Admin {
private readonly 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}`.
*/
readonly transfers: Transfers;
/**
* Telegram notification-channel binding lifecycle. Access via
* `r.admin.channels.{connectTelegram, list, revokeTelegram}`.
*/
readonly channels: Channels;
/**
* Telegram routing rules — one match (ANDed dimensions) to one binding.
* Access via `r.admin.rules.{list, create, update, delete}`.
*/
readonly rules: Rules;
constructor(client: 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: string): ScopedAdminOrg;
/**
* 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: string): ScopedAdminProject;
/**
* 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 })`.
*/
sendFeedback(message: string, opts?: FeedbackSendOptions): Promise<SendMessageResult>;
/**
* @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.
*/
sendMessage(message: string, opts?: FeedbackSendOptions): Promise<SendMessageResult>;
/** Register agent contact info and start email verification when needed. */
setAgentContact(contact: AgentContact): Promise<AgentContactResult>;
/** Read the current agent contact assurance state for the wallet. */
getAgentContactStatus(): Promise<AgentContactResult>;
/** Start or resend the contact email reply challenge. */
verifyAgentContactEmail(): Promise<AgentContactResult>;
/** Email a passkey enrollment link to the verified contact email. */
startContactPasskeyEnrollment(): Promise<AgentContactResult>;
/** List notification audit rows (paginated, filterable). */
listNotifications(opts?: ListNotificationsOptions): Promise<ListNotificationsResult>;
/** Retrieve a single notification audit row by id. */
getNotification(id: string): Promise<NotificationRow>;
/** Read the current notification preferences. */
getNotificationPreferences(): Promise<NotificationPreferences>;
/** Patch notification preferences (assurance ladder applies). */
setNotificationPreferences(patch: NotificationPreferencesPatch, proofs?: SessionProofs): Promise<NotificationPreferences>;
/**
* 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.
*/
testNotification(opts?: TestNotificationOptions): Promise<TestNotificationResult>;
/**
* 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.
*/
rotateWebhookSecret(proofs?: SessionProofs): Promise<RotateWebhookSecretResult>;
/**
* 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.
*/
getProjectFinance(projectId: string, opts?: AdminProjectFinanceOptions): Promise<AdminProjectFinanceResult>;
/**
* 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: string, perpetual: boolean): Promise<SetLeasePerpetualResult>;
/**
* 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`.
*/
archiveProject(projectId: string, opts?: ArchiveProjectOptions): Promise<ArchiveProjectResult>;
/**
* 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`.
*/
reactivateProject(projectId: string): Promise<ReactivateProjectResult>;
}
/**
* 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 declare class ScopedAdminOrg {
private readonly admin;
private readonly orgId;
constructor(admin: Admin, orgId: string);
/** Pin the org's lease (`lease_perpetual = true`). */
pinLease(): Promise<SetLeasePerpetualResult>;
/** Unpin the org's lease (`lease_perpetual = false`). */
unpinLease(): Promise<SetLeasePerpetualResult>;
}
/**
* 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 declare class ScopedAdminProject {
private readonly admin;
private readonly projectId;
constructor(admin: Admin, projectId: string);
/** Archive the project (staff moderation). */
archive(opts?: ArchiveProjectOptions): Promise<ArchiveProjectResult>;
/** Un-archive the project. */
reactivate(): Promise<ReactivateProjectResult>;
/** Read per-project finance (staff Finance tab). */
finance(opts?: AdminProjectFinanceOptions): Promise<AdminProjectFinanceResult>;
}
//# sourceMappingURL=admin.d.ts.map