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.
2,248 lines • 89 kB
TypeScript
/**
* Type definitions for the unified `apply` primitive (`unified-deploy`
* capability, gateway routes `POST /apply/v1/plans` + commit + operations).
*
* The user-facing inputs accept polymorphic byte sources (strings, Uint8Array,
* Blob, web streams, plus the Node-only `fileSetFromDir`). The wire shapes
* carry only `ContentRef` objects — the SDK normalizes byte sources into
* refs via the CAS content service before issuing the plan request.
*/
import type { NextAction } from "../errors.js";
import type { OperationActorSnapshot } from "./identity-links.types.js";
/**
* Marker for a filesystem file path. Produced by `fileSetFromDir` from
* `@run402/sdk/node` so the normalizer can stream-hash from disk without
* loading bytes into memory.
*
* Exposed as a discriminated shape rather than a class so it round-trips
* through `JSON.parse(JSON.stringify(...))` without losing its identity.
*/
export interface FsFileSource {
readonly __source: "fs-file";
readonly path: string;
readonly contentType?: string;
}
/**
* Anything the SDK can normalize into a `ContentRef`. Pass UTF-8 strings for
* text, or raw bytes, `Blob`/`File` (web), web `ReadableStream`, an `FsFileSource`
* from `fileSetFromDir`, or a wrapper `{ data, contentType? }` for explicit
* MIME-type control. String sources paired with known binary paths/MIMEs are
* rejected locally with `BINARY_CONTENT_REQUIRES_BYTES` before any request.
*/
export type ContentSource = string | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array> | FsFileSource | {
data: ContentSource;
contentType?: string;
} | ContentRef;
/**
* Wire-level reference to a content-addressed object. Produced by hashing a
* byte source locally and (optionally) negotiating presence via
* `POST /content/v1/plans`. The `sha256` is lowercase hex; `integrity` is
* the browser SRI form (`sha256-<base64>`) when emitted.
*/
export interface ContentRef {
sha256: string;
size: number;
contentType?: string;
integrity?: string;
}
/** A path-keyed set of content. Values may be byte sources (normalized by the
* SDK) or already-resolved `ContentRef` objects (from a prior upload). */
export type FileSet = Record<string, ContentSource>;
/**
* SDK-input sentinel for "walk this directory at submission time." Produced
* by `dir(path)` from `@run402/sdk/node`. Carried in the public `SiteSpec`
* types so editors autocomplete `site: { replace: dir("./dist") }` without
* a cast. The normalizer expands it to a `FileSet` before any plan request;
* it never leaves the client.
*
* The structural shape (not a class) lets the deploy normalizer detect it
* in the isomorphic SDK without importing the Node-only `assets-node`
* module that creates it.
*/
export interface LocalDirRef {
readonly __source: "local-dir";
readonly path: string;
readonly prefix?: string;
readonly ignore?: ReadonlyArray<string>;
readonly includeSensitive?: boolean;
}
/** Caller-facing spec passed to `r.project(id).apply(spec)`. */
export interface ReleaseSpec {
/** JSON Schema metadata for editor-authored specs. Stripped before plan requests. */
$schema?: string;
/** Project id the release belongs to. */
project_id: string;
/** Diff base for the new release. Default: `{ release: "current" }`. Pass
* `{ release: "empty" }` for a fresh deploy that should fail if a release
* already exists, or `{ release_id: "rel_..." }` to pin a specific base. */
base?: {
release: "current" | "empty";
} | {
release_id: string;
};
database?: DatabaseSpec;
secrets?: SecretsSpec;
functions?: FunctionsSpec;
site?: SiteSpec;
subdomains?: SubdomainsSpec;
routes?: ReleaseRoutesSpec;
checks?: SmokeCheck[];
/** v1.48 unified-apply: optional asset slice. When present, the apply
* promotes per-key blob writes inside the same activation transaction
* that flips live_release_id, so asset visibility flips at the exact
* moment release visibility flips. */
assets?: AssetSpec;
/** v2.5+ routed-locale-context: declare supported locales, a default
* locale, and detection sources. The gateway negotiates per request
* and surfaces the result to routed HTTP functions via the
* `x-run402-locale` / `x-run402-default-locale` request headers.
* Omit to carry forward from base; `null` clears the slice. */
i18n?: I18nSpec | null;
}
/**
* Asset slice (v1.48 unified-apply). The SDK accepts the ergonomic forms
* (LocalDirRef from `dir(path)`, `FileSet`, `ContentSource` in `put`) and
* normalizes to wire-shaped AssetPutEntry[] before submitting. The gateway
* sees only AssetPutEntry[].
*
* - Without `prune`, `sync` is additive (semantically equivalent to
* `r.project(id).assets.uploadDir`).
* - With `prune: true`, the apply is destructive — the SDK runs a plan
* first and returns a `PRUNE_CONFIRMATION_REQUIRED` error carrying the
* confirm token shape; the caller acknowledges by retrying with
* `sync.confirm` populated. The activation transaction's drift check
* (HTTP 409 ASSET_SYNC_DRIFT) catches the narrower race where
* inventory mutates between commit and activation.
*/
export interface AssetSpec {
/** Accept either the wire-shaped {@link AssetPutEntry} (`sha256` +
* `size_bytes` set, byte upload already happened or about to happen via
* pre-uploaded CAS) OR the SDK-input shape {@link AssetPutEntryInput}
* (carries `source: ContentSource`; the SDK normalizer hashes the
* bytes, registers a byte-reader, and emits wire-shaped entries before
* submission). The two forms can be mixed in the same array. */
put?: (AssetPutEntry | AssetPutEntryInput)[];
delete?: string[];
sync?: {
prefix: string;
prune: true;
confirm?: AssetSyncPruneConfirm;
};
}
export interface AssetPutEntry {
/** Logical key, e.g. `"static/app.css"`. Same validation rules as
* release-file paths (no leading `/`, no `..`, no NUL/control chars,
* no `_cas/` or `_staging/` reserved prefixes, ≤1024 bytes total). */
key: string;
/** 64 lowercase hex SHA-256 of the bytes. */
sha256: string;
/** Size in bytes — must agree with the bytes when the SDK uploads. */
size_bytes: number;
/** Defaults to `application/octet-stream`. */
content_type?: string;
/** Defaults to `"public"`. Private assets return null for all public URL
* fields; obtain a signed URL via `r.project(id).assets.sign(key)`. */
visibility?: "public" | "private";
/** Defaults to `true`. Immutable puts populate `internal.asset_versions`
* so older content-hashed URLs survive future key mutation/delete (per
* the visibility-aware URL matrix). */
immutable?: boolean;
/** v1.50 wire: caller-provided flat metadata stored alongside the asset.
* Validated client-side (≤4 KB serialized; leaves are
* `string | number | boolean | string[]`). */
metadata?: Record<string, string | number | boolean | string[]>;
/** v1.50 wire: EXIF retention policy applied to image uploads. Defaults
* to `"keep"`. Invalid values are rejected client-side with
* `INVALID_EXIF_POLICY`. */
exif_policy?: "keep" | "strip";
}
/**
* SDK-input shape for an asset put. Carries a `ContentSource` rather than
* a pre-computed `sha256` / `size_bytes`; the SDK normalizer hashes the
* source, registers a byte-reader in the apply's `byteReaders` map (keyed
* by SHA), and emits a wire-shaped {@link AssetPutEntry} before the spec
* is sent to `/apply/v1/plans`. The `source` field NEVER reaches the
* gateway.
*
* Discriminated from {@link AssetPutEntry} by the presence of `source`
* and the absence of `sha256`. Either form may appear in
* `AssetSpec.put`.
*/
export interface AssetPutEntryInput {
key: string;
/** Bytes to upload. Accepts every shape `resolveContent` accepts —
* `string`, `Uint8Array`, `ArrayBuffer`, `Blob`, `FsFileSource`,
* `ReadableStream`, `{ data, contentType }` wrapper, `{ content }`,
* `{ bytes }`. */
source: ContentSource;
content_type?: string;
visibility?: "public" | "private";
immutable?: boolean;
/** v1.50: caller-provided flat metadata stored alongside the asset.
* Validated client-side (≤4 KB serialized; leaves are
* `string | number | boolean | string[]`). Rejected with
* `INVALID_ASSET_METADATA` for nested objects / non-allowed leaves. */
metadata?: Record<string, string | number | boolean | string[]>;
/** v1.50: EXIF retention policy applied to image uploads. Default
* `"keep"`. Invalid values are rejected client-side with
* `INVALID_EXIF_POLICY`. The normalizer converts this camelCase input
* to the wire-shape `exif_policy`. */
exifPolicy?: "keep" | "strip";
}
export interface AssetSyncPruneConfirm {
/** SHA-256 hex returned by a prior `r.project(id).apply.plan` call. */
base_revision: string;
/** SHA-256 hex of the canonical-form sorted planned delete-key list. */
delete_set_digest: string;
/** Expected count of keys to delete. */
expected_delete_count: number;
}
export interface DatabaseSpec {
migrations?: MigrationSpec[];
/** Declarative authorization manifest applied during deploy. */
expose?: ExposeManifest;
/** Opt-in: skip the migrate-gate phase. Only safe when migrations are
* declared backward-compatible (no breaking schema changes). */
zero_downtime?: boolean;
}
export type MigrationSpec = VersionedMigrationSpec | ContentTrackedMigrationSpec;
export interface BaseMigrationSpec {
/** Lowercase hex SHA-256 of the migration SQL. Computed by the SDK from
* `sql` if not provided. */
checksum?: string;
/** Inline SQL (UTF-8). The SDK uploads to CAS and replaces with `sql_ref`
* before the plan request. Either `sql` or `sql_ref` is required. */
sql?: string;
/** Pre-uploaded SQL CAS reference. */
sql_ref?: ContentRef;
/** Default `"required"` — runs in a single advisory-locked transaction.
* `"none"` opts out (and on failure, sends the operation to
* `needs_repair` rather than rolling back). */
transaction?: "required" | "none";
}
export interface VersionedMigrationSpec extends BaseMigrationSpec {
/** Stable migration id, e.g. `"001_init"`. Same id + same checksum across
* re-deploys is a registry noop; same id + different checksum is a hard
* error. Use `name` instead for generated/idempotent SQL whose identity
* should track content changes. */
id: string;
name?: never;
}
export interface ContentTrackedMigrationSpec extends BaseMigrationSpec {
/** Content-tracked migration name for generated/idempotent SQL. The SDK
* compiles this to `id = <name>_<sha256(sql)[0:16]>`; changed content
* applies once under a new id, identical re-deploys noop. SQL declared
* with `name` MUST be idempotent because it re-runs whenever content
* changes against a database where prior versions may already exist. */
name: string;
id?: never;
}
export type ExposePolicy = "user_owns_rows" | "public_read_authenticated_write" | "public_read_append_only" | "public_read_write_UNRESTRICTED" | "custom";
export interface EffectiveAccessPreview {
table: string;
role: "anon" | "authenticated";
declared_operations: string[] | null;
required_privileges: string[] | null;
validation: "pending" | "structural";
row_checks: "runtime";
findings?: string[];
added_privileges?: string[];
}
/** Declarative authorization manifest. Pass-through shape — the gateway
* validates the schema. See https://run402.com/schemas/manifest.v1.json. */
export interface ExposeManifest {
version?: string;
tables?: Array<Record<string, unknown>>;
views?: Array<Record<string, unknown>>;
rpcs?: Array<Record<string, unknown>>;
[key: string]: unknown;
}
export interface SecretsSpec {
/** Keys that must already exist in the project's secret store at commit time.
* Plan-time emits a MISSING_REQUIRED_SECRET warning for absent keys;
* commit-time hard-errors if they are still missing. */
require?: string[];
/** Delete specific secrets by key at activation. Unknown keys hard-error at commit-time gating. */
delete?: string[];
}
export interface FunctionsSpec {
/** The new desired set — functions absent here are removed in the new
* release. */
replace?: Record<string, FunctionSpec>;
/** Surgical updates — only listed functions change. */
patch?: {
set?: Record<string, FunctionSpec>;
delete?: string[];
};
}
export interface FunctionSpec {
runtime?: "node22";
/** Bundled source (single file). Mutually exclusive with `files`. */
source?: ContentSource;
/** Multi-file function (entrypoint + assets). Provide `entrypoint` when
* using this shape. */
files?: FileSet;
/** Required when `files` is set. The relative path of the entrypoint
* within the file set (e.g., `"index.mjs"`). */
entrypoint?: string;
config?: {
timeoutSeconds?: number;
memoryMb?: number;
};
/**
* Capability `apply-v1-function-deps`. Additional npm packages to install
* and bundle with the function. Each entry is an npm spec: a bare name
* (`"lodash"`) resolves to latest at deploy time; a pinned spec
* (`"lodash@4.17.21"`) or range (`"date-fns@^3.0.0"`) is honored verbatim.
* `@run402/functions` (auto-bundled) and native-binary modules are rejected
* by the gateway. Omit for no user dependencies. The resolved concrete
* versions live on the function record (read via the GET-list), not the
* apply/deploy result.
*/
deps?: string[];
/** Durable function triggers. Schedule triggers create durable function runs. */
triggers?: FunctionTriggerSpec[];
/** Legacy direct schedule field. Prefer `triggers[]` schedule entries for new manifests. */
schedule?: string | null;
/**
* v1.52+: function class. When `"ssr"`, the gateway provisions the
* Lambda with SnapStart enabled and routes invocations through the
* origin ISR cache. Defaults to `"standard"` when omitted. Set by
* `@run402/astro` (and other framework adapters) for server-rendering
* entry functions; user-authored functions should usually omit this
* and let the gateway default.
*/
class?: "ssr" | "standard";
/**
* Function capability declarations interpreted by the gateway/runtime.
* Framework adapters use this for runtime contracts such as
* `astro.ssr.v1`; auth helpers can use it for explicit trust gates.
*/
capabilities?: string[];
/**
* v1.51+: when `true`, the Run402 gateway rejects callers without a
* valid project user JWT with `401` before invoking the function.
* Independent from `requireRole` — set `requireRole` alone to imply
* authentication, or set `requireAuth: true` alone for a session
* check with no DB lookup.
*
* When the gate passes, the gateway injects `x-run402-user-id` into
* the request. In-function code reads it with `getUserId(req)` from
* `@run402/functions`.
*
* Gateway is authoritative — the SDK does not validate shape (use
* the canonical `INVALID_SPEC` envelope from the gateway at plan
* time for typos).
*/
requireAuth?: boolean;
/**
* v1.51+: declarative application-role gate. When set, the gateway
* resolves the caller's role from the project-schema table named in
* `table` (FK column `idColumn` matches the JWT `sub`, role string
* read from `roleColumn`) and rejects callers whose role is not in
* `allowed` with `403`. Lookup is RLS-bypass (the gateway is the
* trusted intermediary). Per-`(projectId, userId)` cache with
* configurable TTL.
*
* `requireRole` implies authentication — no valid JWT → `401` (the
* same envelope as `requireAuth`). All `requireRole` blocks in a
* single release must share the same `(table, idColumn, roleColumn)`
* triple (gateway rejects mixed tables at plan time).
*
* When the gate passes, the gateway injects both `x-run402-user-id`
* AND `x-run402-user-role` into the request. In-function code reads
* them with `getUserId(req)` / `getRole(req)` from
* `@run402/functions`.
*
* Pass `null` in `patch.set` to remove an existing gate; omit to
* leave it unchanged.
*/
requireRole?: RequireRoleSpec | null;
}
export interface FunctionTriggerRunSpec {
event_type: string;
payload?: Record<string, unknown>;
retry?: Record<string, unknown>;
expires_after_seconds?: number;
}
export interface FunctionScheduleTriggerSpec {
id: string;
type: "schedule";
cron: string;
timezone?: string;
misfire_policy?: "skip";
overlap_policy?: "allow";
run: FunctionTriggerRunSpec;
}
/**
* `mailbox_suspended` (recovery-event-reachability, gateway 2026-07): fires
* when the mailbox is abuse-suspended, so the project's own agent observes
* the suspension as a durable function run — no public callback URL, no
* polling. The run payload carries `{event: {mailbox_id, suspended_reason,
* suspended_at, evidence, recovery_actions}}` and executes independently of
* the suspended mailbox's send capability.
*/
export type EmailTriggerEvent = "reply_received" | "delivery" | "bounced" | "complained" | "mailbox_suspended";
export interface FunctionEmailTriggerSpec {
id: string;
type: "email";
/** Mailbox id or slug to subscribe to. */
mailbox: string;
/** Email events that should create durable function runs. */
events: EmailTriggerEvent[];
run: FunctionTriggerRunSpec;
}
export type FunctionTriggerSpec = FunctionScheduleTriggerSpec | FunctionEmailTriggerSpec;
/**
* v1.51+: declarative role-gate descriptor for `FunctionSpec.requireRole`.
*
* The gateway runs a `SELECT roleColumn FROM <project_schema>.table WHERE
* idColumn = $jwt.sub` lookup (RLS-bypass), byte-equality-checks the
* result against `allowed`, and caches the answer per
* `(projectId, userId)` for `cacheTtl` seconds.
*
* Identifiers are unquoted; schema-qualified names (e.g. `"public.members"`)
* are rejected at plan time with `INVALID_SPEC`. The project schema is
* resolved server-side from the project record — do not include it here.
*/
export interface RequireRoleSpec {
/** Project-schema table holding role rows (e.g., `"members"`).
* Unqualified. */
table: string;
/** Column in `table` that matches the JWT `sub` claim (the user id).
* Typically `"user_id"`. */
idColumn: string;
/** Column in `table` holding the role string. Typically `"role"`. */
roleColumn: string;
/** Allowed role values. Case-sensitive byte equality. Non-empty.
* Multi-element arrays permit any matching role (OR semantics). */
allowed: string[];
/** Cache TTL in seconds for the resolved role per
* `(projectId, userId)`. Default `60`. Max `600`. Set `0` to disable
* caching (fresh lookup on every request) — use for high-stakes
* operations where instant demotion matters. A demoted user retains
* the cached role until expiry; demotion is NOT broadcast. */
cacheTtl?: number;
/** Unauthorized HTML response mode. Default gateway behavior is an error
* envelope; `"redirect"` sends unauthenticated browser requests to
* `signInPath` for console-style apps. */
onDeny?: "envelope" | "redirect";
/** Same-origin sign-in path used when `onDeny` is `"redirect"`. */
signInPath?: string;
}
export interface PublicStaticPathSpec {
/** Release static asset path, e.g. "events.html". This is not a public URL. */
asset: string;
cache_class?: StaticCacheClass;
}
export type SitePublicPathsSpec = {
mode: "implicit";
replace?: never;
} | {
mode: "explicit";
replace: Record<string, PublicStaticPathSpec>;
};
/** tenant-site-embedding: a platform embedding catalog key. `localhost` expands
* to `http://localhost:*` and `http://127.0.0.1:*`. Raw origins are never
* accepted; the gateway rejects them with INVALID_SPEC naming the valid keys.
* Typed as the known literal plus any string so a new catalog key never needs
* a client release (preserve unknown future strings). */
export type EmbeddingKey = "localhost" | (string & {});
/** tenant-site-embedding: who may put the site in an iframe. Omitted on a later
* apply carries the base release's declaration forward; `null` clears it back
* to the default deny (`frame-ancestors 'none'` + `X-Frame-Options: DENY`). */
export interface SiteEmbeddingSpec {
frame_ancestors: EmbeddingKey[];
}
export type SiteSpec = {
replace: FileSet | LocalDirRef;
patch?: never;
public_paths?: SitePublicPathsSpec;
embedding?: SiteEmbeddingSpec | null;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
patch: {
put?: FileSet | LocalDirRef;
delete?: string[];
};
replace?: never;
public_paths?: SitePublicPathsSpec;
embedding?: SiteEmbeddingSpec | null;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
public_paths: SitePublicPathsSpec;
replace?: never;
patch?: never;
embedding?: SiteEmbeddingSpec | null;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
embedding: SiteEmbeddingSpec | null;
replace?: never;
patch?: never;
public_paths?: never;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
sign_in_path: string | null;
replace?: never;
patch?: never;
public_paths?: never;
embedding?: never;
noindex?: boolean | null;
} | {
noindex: boolean | null;
replace?: never;
patch?: never;
public_paths?: never;
embedding?: never;
sign_in_path?: never;
};
export interface SubdomainsSpec {
/** The exact desired set. Currently limited to one element per project —
* the gateway returns `SUBDOMAIN_MULTI_NOT_SUPPORTED` for multi-element
* arrays. */
set?: string[];
/** Add specific subdomains without disturbing others. */
add?: string[];
/** Remove specific subdomains. */
remove?: string[];
}
/**
* Where the gateway looks for the caller's locale during routed-function
* negotiation. Walked in order; the first match wins. Sources:
*
* - `"accept-language"` — parse per RFC 9110, drop `q=0` and `*`, sort by
* q descending (stable on original order for ties); apply RFC 4647 §3.4
* lookup-style truncation (`zh-Hant-TW` → `zh-Hant` → `zh`); longest
* matching prefix wins. A generic request tag does NOT match a more
* specific `locales[]` entry (`Accept-Language: es` does NOT match
* `locales: ["es-MX"]`).
* - `cookie:<name>` — case-sensitive cookie name lookup; raw value (no
* percent-decode) matched case-insensitively against `locales[]`.
* Cookie name MUST match RFC 6265 grammar
* (`/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/`).
*/
export type I18nDetectSource = "accept-language" | `cookie:${string}`;
/** Backwards-friendly alias — the issue and gateway both call this `DetectSource`. */
export type DetectSource = I18nDetectSource;
/**
* Routed-locale-context release slice (v2.5+). Drives the negotiated
* locale that the gateway surfaces to routed HTTP function invocations
* via the `x-run402-locale` and `x-run402-default-locale` request headers.
*
* Carry-forward semantics (simpler than `routes` — no `{ replace }`
* envelope):
*
* - `i18n` omitted from a ReleaseSpec → carry forward from base release.
* - `i18n: null` → clear the slice on the new release.
* - `i18n: { ... }` → replace the slice with the provided value.
*
* The negotiated locale is returned in the canonical casing supplied here
* in `locales[]`, NOT the request's casing. Static-route hits do NOT
* receive locale negotiation; only routed HTTP function invocations do.
* Run402 does NOT inject `Vary` headers — apps that return public-
* cacheable responses varying by locale must set their own `Vary`.
*/
export interface I18nSpec {
/** Default locale tag. MUST be byte-identical to one entry in
* `locales[]`. The platform does NOT silently canonicalize. */
defaultLocale: string;
/** Supported locale tags. Non-empty, ≤50 entries. Each tag MUST match
* `/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`. Tags are opaque — there is
* NO BCP-47 semantic validation. */
locales: string[];
/** Detection sources, walked in order. Default `["accept-language"]`
* when omitted, ≤10 entries. `[]` is allowed and means "always
* default". */
detect?: I18nDetectSource[];
/** How the gateway handles an otherwise-valid locale value that is not in
* `locales[]`. Omit for the gateway default. */
unknownLocalePolicy?: "default" | "reject";
}
/** Materialized form of `I18nSpec` as it appears on release inventory
* reads (`releases/active`, `releases/:id`). Same shape as `I18nSpec`
* but with `detect` always populated — the gateway materializes the
* default `["accept-language"]` when the spec omitted it, and preserves
* `[]` verbatim. Use this when reading deployed state; use `I18nSpec`
* when constructing a `ReleaseSpec`. */
export interface ReleaseInventoryI18n {
defaultLocale: string;
locales: string[];
detect: I18nDetectSource[];
}
export declare const ROUTE_HTTP_METHODS: readonly ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
export type RouteHttpMethod = (typeof ROUTE_HTTP_METHODS)[number];
export declare const ROUTE_PRICING_NETWORKS: readonly ["mainnet", "testnet", "lightning", "lightning-testnet", "tempo", "tempo-testnet"];
export declare const LIGHTNING_ROUTE_PRICING_NETWORKS: readonly RoutePricingNetwork[];
export type RoutePricingNetwork = (typeof ROUTE_PRICING_NETWORKS)[number];
export interface RoutePricingSpec {
mode: "always";
amount_usd_micros: number;
/**
* Pins the Lightning price in millisatoshis for the route's `lightning` /
* `lightning-testnet` entries; needs a Lightning network. Absent, each
* challenge quotes `amount_usd_micros` into whole satoshis.
*/
amount_msat?: number;
pay_to: "org_default_payout";
/**
* Omit to accept production mainnet only. Add `"testnet"` explicitly for
* testnet acceptance. `"lightning"` / `"lightning-testnet"` sell for sats
* over x402 `exact` on `lnbtc`, settled to the organization's Lightning
* receiver (`run402 orgs lightning-receiver set`); a Lightning-only route
* needs no EVM payout wallet. `"tempo"` / `"tempo-testnet"` also take an
* MPP Tempo charge in pathUSD (`WWW-Authenticate: Payment`), paid to the
* organization's payout address on Tempo mainnet / Moderato.
*/
networks?: readonly RoutePricingNetwork[];
/** Ask the function to declare committed fulfillment for a signed merchant receipt. */
receipt?: "on_fulfillment";
/**
* Opt in to the x402 Bazaar: the 402 challenge carries this metadata and the
* first paid call lists the route. Exact routes with one method only.
*/
discovery?: RoutePricingDiscovery;
}
export interface RoutePricingDiscovery {
/** At most 500 characters, used verbatim. */
description: string;
/** Default `application/json`. */
mime_type?: string;
/** 1-32 printable ASCII characters. */
service_name?: string;
/** At most 4, each 1-32 printable ASCII; Run402 appends `run402`. */
tags?: readonly string[];
/** Absolute https URL on a public host name. */
icon_url?: string;
/** Query parameters for GET/HEAD/DELETE, a body (`body_type` required) for POST/PUT/PATCH. */
input?: {
query_params_schema?: Record<string, unknown>;
body_type?: "json" | "form-data" | "text";
body_schema?: Record<string, unknown>;
example?: unknown;
};
output?: {
example?: unknown;
schema?: Record<string, unknown>;
};
}
export interface FunctionRouteTarget {
type: "function";
/** Materialized release function name, not a file name or handler export. */
name: string;
}
export interface StaticRouteTarget {
type: "static";
/** Materialized static-site file path, relative to the site root. */
file: string;
}
export type RouteTarget = FunctionRouteTarget | StaticRouteTarget;
/** One apply-v1 web route entry. */
export interface RouteSpec {
pattern: string;
/** Omit to allow every supported method. Empty arrays are invalid. */
methods?: readonly RouteHttpMethod[];
target: RouteTarget;
/** Fixed-price tenant x402 policy for function routes. Static routes cannot be priced. */
pricing?: RoutePricingSpec;
}
/** Top-level release route resource. Omit or pass null to carry routes forward. */
export type ReleaseRoutesSpec = null | {
replace: RouteSpec[];
};
export interface SmokeCheck {
name: string;
http?: {
path: string;
method?: string;
expect?: {
status?: number;
};
};
}
type LiteralUnion<T extends string> = T | (string & {});
export type KnownStaticCacheClass = "html" | "immutable_versioned" | "revalidating_asset";
export type StaticCacheClass = LiteralUnion<KnownStaticCacheClass>;
export interface StaticManifestMetadata {
file_count: number;
total_bytes: number;
cache_classes: Record<string, number>;
cache_class_sources: Record<string, number>;
spa_fallback: string | null;
}
export declare const EMPTY_STATIC_MANIFEST_METADATA: StaticManifestMetadata;
export declare function normalizeStaticManifestMetadata(metadata: StaticManifestMetadata | null | undefined): StaticManifestMetadata;
export interface StaticAssetsDiff {
unchanged: number;
changed: number;
added: number;
removed: number;
newly_uploaded_cas_bytes: number;
reused_cas_bytes: number;
deployment_copy_bytes_eliminated: number;
legacy_immutable_warnings: Array<{
path: string;
sha256: string;
reason: string;
}>;
previous_immutable_failures: Array<{
path: string;
previous_sha256: string;
candidate_sha256: string;
}>;
cas_authorization_failures: string[];
}
export type DeployResolveMethod = RouteHttpMethod | (string & {});
export type KnownDeployResolveMatch = "host_missing" | "manifest_missing" | "active_release_missing" | "unsupported_manifest_version" | "path_error" | "none" | "static_exact" | "static_index" | "spa_fallback" | "retained_public_path" | "spa_fallback_missing" | "route_function" | "route_static_alias" | "route_method_miss" | "deployment_host" | "deployment_retired";
export type DeployResolveMatch = LiteralUnion<KnownDeployResolveMatch>;
export type KnownDeployResolveAuthorizationResult = "authorized" | "not_public" | "not_applicable" | "manifest_missing" | "target_missing" | "active_release_missing" | "unsupported_manifest_version" | "path_error" | "missing_cas_object" | "unfinalized_or_deleting_cas_object" | "size_mismatch" | "unauthorized_cas_object";
export type DeployResolveAuthorizationResult = LiteralUnion<KnownDeployResolveAuthorizationResult>;
export type KnownDeployResolveFallbackState = "unavailable" | "active_release_missing" | "unsupported_manifest_version" | "negative_cache_hit" | "path_error" | "method_not_static" | "not_used" | "target_missing" | "used" | "not_configured" | "not_eligible";
export type DeployResolveFallbackState = LiteralUnion<KnownDeployResolveFallbackState>;
export type KnownDeployResolveResult = 200 | 400 | 404 | 405 | 503;
export interface DeployResolveRouteMatch {
pattern: string;
methods: RouteHttpMethod[] | string[] | null;
target: RouteTarget;
}
export interface DeployResolveCasObject {
sha256: string;
exists: boolean;
expected_size: number;
actual_size?: number | null;
[key: string]: unknown;
}
export interface DeployResolveResponseVariant {
kind: string;
varies_by: string | string[];
hostname: string;
release_id: string | null;
release_generation: number | null;
path: string;
raw_static_sha256: string;
variant_inputs_hash: string;
[key: string]: unknown;
}
export type DeployResolveOptions = {
project: string;
url: string | URL;
method?: DeployResolveMethod;
host?: never;
path?: never;
} | {
project: string;
host: string;
path?: string;
method?: DeployResolveMethod;
url?: never;
};
export type ScopedDeployResolveOptions = (Omit<Extract<DeployResolveOptions, {
url: string | URL;
}>, "project"> & {
project?: string;
}) | (Omit<Extract<DeployResolveOptions, {
host: string;
}>, "project"> & {
project?: string;
});
export interface NormalizedDeployResolveRequest {
project: string;
project_scope: "credential_lookup_only";
project_sent_to_gateway: false;
original_url?: string;
host: string;
path: string;
method?: string;
ignored?: {
query?: string;
fragment?: string;
};
}
export interface DeployResolveResponse {
static_continuity?: StaticContinuity;
current_host_release_id?: string | null;
current_host_release_generation?: number | null;
hostname: string;
host_binding_id?: string | null;
binding_status?: string | null;
project_id?: string | null;
channel?: string | null;
release_id?: string | null;
release_generation?: number | null;
route_manifest_sha256?: string | null;
static_manifest_sha256?: string | null;
static_manifest_metadata?: StaticManifestMetadata | null;
/** tenant-site-embedding: the framing opt-in of the release this host serves
* (catalog keys) or `null`; absent on an older gateway. */
embedding?: SiteEmbeddingSpec | null;
/** site.noindex of the release this host serves; absent on an older gateway. */
noindex?: boolean;
normalized_path?: string | null;
match: DeployResolveMatch;
route?: DeployResolveRouteMatch | null;
asset_path?: string | null;
reachability_authority?: StaticReachabilityAuthority | null;
direct?: boolean | null;
static_sha256?: string | null;
content_type?: string | null;
cache_class?: StaticCacheClass | null;
cache_policy?: string | null;
authorization_result?: DeployResolveAuthorizationResult | null;
cas_object?: DeployResolveCasObject | null;
response_variant?: DeployResolveResponseVariant | null;
allow?: RouteHttpMethod[] | string[] | null;
route_pattern?: string | null;
target_type?: LiteralUnion<"function" | "static"> | null;
target_name?: string | null;
target_file?: string | null;
authorized: boolean;
fallback_state: DeployResolveFallbackState;
error_code?: string | null;
legacy_immutable_risk?: Array<Record<string, unknown>>;
emergency_fallback?: Record<string, unknown> | null;
edge_propagation?: EdgePropagationDiagnostics | null;
/** `deployment` when the host is a deployment host (`dpl-*.sites.run402.com`). */
host_kind?: LiteralUnion<"deployment"> | null;
/** Deployment hosts: the `dpl_` deployment the host serves. */
deployment_id?: string | null;
/** Deployment hosts: `live` (the active release or a subdomain serves it),
* `retained` (replaced; serves until `origin_available_until`), or
* `retired` (no longer served). */
deployment_status?: LiteralUnion<"live" | "retained" | "retired"> | null;
/** Deployment hosts: when a replaced deployment stops (or stopped) serving; null while live. */
origin_available_until?: string | null;
/** Deployment hosts: how long a replaced deployment keeps serving. */
origin_retention_seconds?: number | null;
/** Deployment hosts: `true` when the path was resolved against the deployment's
* release (its static manifest and public paths); `false` for a retired host or
* a deployment no release names, which is reported per host. */
path_evaluated?: boolean;
/** Deployment hosts: `false` once the deployment was replaced; its routes no
* longer run there, and a matched route reports result 404. */
routes_active?: boolean;
/** Diagnostic body status. This is not necessarily the HTTP response status. */
result: number;
[key: string]: unknown;
}
export type EdgePropagationStatus = "settled" | "propagating" | "sync_pending";
export type EdgePropagationSource = "present" | "missing" | "unknown";
export interface EdgePropagationDiagnostics {
binding: string;
claimed_at: string;
kvs_synced_at: string | null;
kvs_source: EdgePropagationSource;
status: EdgePropagationStatus;
expected_visible_by: string | null;
hint: string;
}
export interface SubdomainBindingFreshness {
host: string;
claimed_at: string;
kvs_synced_at: string | null;
}
export interface DeployResolveWarning {
code: string;
message: string;
}
export interface DeployResolveNextStep {
code: string;
message: string;
}
export interface DeployResolveSummary {
would_serve: boolean;
diagnostic_status: number;
match: DeployResolveMatch;
category: string;
summary: string;
warnings: DeployResolveWarning[];
next_steps: DeployResolveNextStep[];
}
export declare function normalizeDeployResolveRequest(opts: DeployResolveOptions): NormalizedDeployResolveRequest;
export declare function isDeployResolveStaticHit(response: DeployResolveResponse): response is DeployResolveResponse & {
match: "static_exact" | "static_index" | "spa_fallback" | "retained_public_path";
};
export declare function isDeployResolveRouteHit(response: DeployResolveResponse): response is DeployResolveResponse & {
route: DeployResolveRouteMatch;
};
export declare function buildDeployResolveSummary(response: DeployResolveResponse, request: NormalizedDeployResolveRequest): DeployResolveSummary;
export declare function summarizeDeployResult(result: DeployResult): DeploySummary;
export declare function isModernPlanMigrationDiff(value: unknown): value is PlanMigrationDiff;
export interface PlanResponse {
static_continuity?: StaticContinuity;
effective_access?: EffectiveAccessPreview[];
/** Present on the v2 plan envelope. Older gateways omitted it; the SDK
* preserves backward compatibility and still normalizes both shapes. */
kind?: "plan_response";
schema_version?: "agent-deploy-observability.v1";
actor?: OperationActorSnapshot | null;
/** Null only for local/legacy dry-run and reviewed-plan preview responses. */
plan_id: string | null;
/** Null only for local/legacy dry-run and reviewed-plan preview responses. */
operation_id: string | null;
/** Gateway-reviewed plan identity. Present for reviewed plan mode. */
plan_fingerprint?: string | null;
/** ISO timestamp when a reviewed plan expires. Present for reviewed plan mode. */
plan_expires_at?: string | null;
planner_semantics_version?: string | null;
base_identity?: string | null;
next_actions?: unknown[];
/** vault (protocol §6.5). Present only when the plan declared a capture. */
vault?: {
/** The canonical `apply_plan_canonical/v1` digest the activation token binds. */
apply_plan_sha256?: string | null;
} | null;
base_release_id: string | null;
manifest_digest: string;
is_noop?: boolean;
summary?: string;
expected_events?: string[];
/** Per-ref presence list. The gateway reports which content SHAs the
* project already has and which need to be uploaded. Items with
* `present: false` must be uploaded via `POST /content/v1/plans` before
* the deploy commit will succeed. */
missing_content: PlanContentRef[];
/** SDK-normalized diff convenience. New gateways return these buckets at
* top level; `normalizePlanResponse` folds them back into `diff` so older
* callers and event consumers keep working. */
diff: DeployDiff;
warnings: WarningEntry[];
payment_required?: PaymentRequiredHint | null;
migrations?: PlanMigrationDiff;
site?: SiteDiff;
functions?: FunctionsDiff;
secrets?: SecretsDiff;
subdomains?: SubdomainsDiff;
routes?: RoutesDiff;
static_assets?: StaticAssetsDiff;
/** v1.48 unified-apply: one entry per `assets.put` item, with the
* AssetRef envelope resolved at plan time (URLs are deterministic
* from `(project_public_id, key, content_sha256)` so they're
* computable BEFORE commit). Agents that need URLs
* pre-commit (for HTML injection between plan and commit) read
* these directly. */
asset_entries?: AssetEntryPlanResult[];
/** v1.48 unified-apply: echo of `assets.delete`. */
asset_deletes?: string[];
/** v1.48 unified-apply: destructive confirmation block when the spec
* declared `assets.sync.prune: true`. Carries the values the caller
* must echo back in commit's `assets.sync.confirm` to authorize the
* destructive operation. `sample_keys` is capped at 50 inline;
* larger sets set `over_inline_threshold: true`. Design D10. */
asset_sync?: AssetSyncPlanBlock;
/** v1.48 unified-apply: structured cost preview. Design D18. */
cost?: PlanCostPreview;
/** v1.111+ rehearsal affordance for migration-bearing plans. */
rehearsal?: PlanRehearsalEnvelope;
}
export interface PlanRehearsalEnvelope {
/** True for a persisted, migration-bearing plan on a project with a live
* release to protect. `apply()` rehearses automatically when true. */
available: boolean;
rehearse_url: string | null;
/** Why rehearsal is not offered: `no_migrations` (nothing to rehearse),
* `no_live_release` (a first deploy has nothing to branch from — commit
* directly), `migrations_unchanged` (every declared migration is a
* checksum-identical noop already applied to the live project — nothing
* would run on the branch), or `ci_session_unsupported` (the plan was made
* by a CI session, which the rehearse route refuses — commit directly).
* `null` when available. */
reason: null | "no_migrations" | "no_live_release" | "migrations_unchanged" | "ci_session_unsupported";
/** Present with `reason: "no_live_release"` / `"migrations_unchanged"` /
* `"ci_session_unsupported"`: one `commit_plan` entry. */
next_actions?: Array<{
type: string;
command?: string;
why: string;
}>;
}
/** How `apply()` handled rehearsal for this deploy (first-deploy-agent-dx). */
export interface DeployRehearsalBlock {
status: "passed" | "skipped" | "failed";
/** Set when `status` is `skipped`. `no_live_release`: first deploy;
* `no_migrations`: nothing to rehearse; `migrations_unchanged`: every
* declared migration is a checksum-identical noop already applied (the
* plan's `migrations.new` bucket is empty), so a rehearsal would run
* nothing; `ci_session_unsupported`: a CI session's plan, which the
* gateway does not rehearse; `disabled`: `noRehearse`; `reviewed_plan`: a `requiredPlan`
* was supplied (already reviewed); `unsupported`: the target (Core) has
* no branches. */
reason?: "no_migrations" | "no_live_release" | "migrations_unchanged" | "ci_session_unsupported" | "disabled" | "reviewed_plan" | "unsupported";
/** The gateway's rehearsal report when a rehearsal ran. */
report?: ApplyRehearsalReport;
operation_id?: string;
branch_project_id?: string | null;
}
/** Resolved AssetRef envelope per `assets.put` entry at plan time. */
export interface AssetEntryPlanResult {
key: string;
sha256: string;
size_bytes: number;
content_type: string;
visibility: "public" | "private";
immutable: boolean;
status: "present" | "satisfied_by_plan" | "upload_pending";
asset_ref: ResolvedAssetRef;
}
/** Wire-shape variant entry returned by the gateway plan response (v1.49+). */
export interface ResolvedAssetVariant {
url: string;
cdn_url: string;
width_px: number;
height_px: number;
format: "webp" | "jpeg";
sha256: string;
}
export interface ResolvedAssetRef {
key: string;
sha256: string;
size_bytes: number;
content_type: string;
visibility: "public" | "private";
immutable: boolean;
url: string | null;
immutable_url: string | null;
cdn_url: string | null;
cdn_immutable_url: string | null;
sri: string | null;
etag: string;
content_digest: string;
width_px?: number;
height_px?: number;
blurhash?: string;
variant_spec_version?: string;
display_url?: string;
display_immutable_url?: string;
variants?: {
thumb?: ResolvedAssetVariant;
medium?: ResolvedAssetVariant;
large?: ResolvedAssetVariant;
display_jpeg?: ResolvedAssetVariant;
};
metadata?: Record<string, string | number | boolean | string[]> | null;
image_format?: string | null;
image_info?: Record<string, unknown> | null;
image_exif?: Record<string, unknown> | null;
image_exif_policy?: "keep" | "strip" | null;
blurhash_data_url?: string | null;
asset_schema?: "v1.49" | "v1.50" | "v1.54" | null;
}
export interface AssetSyncPlanBlock {
prefix: string;
prune: true;
base_revision: string;
delete_set_digest: string;
expected_delete_count: number;
sample_keys: string[];
over_inline_threshold: boolean;
}
export interface PlanCostPreview {
storage_bytes_added: number;
storage_bytes_freed: number;
storage_bytes_net: number;
asset_keys_added: number;
asset_keys_removed: number;
asset_keys_after_commit: number;
billable_delta_usd_micros: number;
payment_required: boolean;
quota_before: {
storage_bytes: number;
asset_keys: number;
};
quota_after: {
storage_bytes: number;
asset_keys: number;
};
}
export type WarningEntry = LegacyWarningEntry | DeployObservabilityWarningEntry;
export interface LegacyWarningEntry {
code: string;
severity: "low" | "medium" | "high";
requires_confirmation: boolean;
message: string;
affected?: string[];
details?: Record<string, unknown>;
confidence?: "low" | "medium" | "high";
}
export interface PlanContentRef {
sha256: string;
size: number;
present: boolean;
}
/**
* Upload session entry for a missing content SHA. Returned by
* `POST /content/v1/plans` after the deploy plan reports refs as missing.
* The SDK PUTs bytes to each part's presigned URL; multipart sessions
* complete via `POST /content/v1/plans/:id/commit`.
*/
export interface MissingContent {
sha256: string;
mode: "single" | "multipart";
parts: Array<{
part_number: number;
url: string;
byte_start: number;
byte_end: number;
}>;
part_size_bytes: number;
part_count: number;
upload_id: string;
staging_key: string;
expires_at: string;
}
export interface ContentPlanResponse {
plan_id: string;
expires_at: string;
missing: MissingContent[];
entries: Array<{
sha256: string;
missing: boolean;
}>;
}
export interface PaymentRequiredHint {
amount: string;
asset: string;
payTo: string;
reason: string;
}
export type ReleaseInventoryStatus = "active" | "superseded" | "failed" | "staged";
export type ReleaseInventoryStateKind = "current_live" | "effective" | "desired_manifest";
export interface SitePathEntry {
path: string;
content_sha256: string;
content_type: string;
}
export interface ReleaseFunctionEntry {
name: string;
code_hash: string;
runtime: string;
timeout_seconds: number;
memory_mb: number;
schedule: string | null;
triggers?: FunctionTriggerSpec[];
/** Present when the function's source exports a static `tool` declaration:
* the app serves it as an MCP tool at `https://<host>/_run402/mcp`. */
tool?: ReleaseFunctionTool;
}
/** A function's MCP tool, as release reads report it. Declared in the
* function's source as `export const tool = { description, input, title?,
* annotations? }` (a literal; the gateway parses it at commit and never runs
* the code). A tool call is a POST of the arguments to `route`. */
export interface ReleaseFunctionTool {
/** The function name, which is the tool name. */
name: string;
description: string;
title?: string;
/** JSON Schema for the arguments; root `type` is `"object"`. */
input: Record<string, unknown>;
annotations?: {
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
openWorldHint?: boolean;
};
/** The one exact POST route the tool binds to. */
route: string | null;
}
/** MCP tools added, removed, or changed between two releases, by name. */
export interface ToolsDiff {
added: string[];
removed: string[];
changed: string[];
}
export interface MigrationAppliedEntry {
migration_id: string;
checksum_hex: string;
applied_at: string;
}
export interface RouteEntry {
pattern: string;
kind: "exact" | "prefix";
prefix: string | null;
/** Null means all supported route HTTP methods. */
methods: RouteHttpMethod[] | null;
target: RouteTarget;
}
export interface MaterializedRoutes {
manifest_sha256: string | null;
entries: RouteEntry[];
}
export interface RouteChangeEntry {
pattern: string;
before: RouteEntry;
after: RouteEntry;
fields_changed: Array<"methods" | "target" | "kind" | "prefix">;
}
export type KnownStaticReachabilityAuthority = "implicit_file_path" | "explicit_public_path" | "route_static_alias";
export type StaticReachabilityAuthority = LiteralUnion<KnownStaticReachabilityAuthority>;
export interface StaticPublicPathInventoryEntry {
public_path: string;
asset_path: string;
reachability_authority: StaticReachabilityAuthority;
direct: boolean;
cache_class: StaticCacheClass;
content_type: string;
route_id?: string | null;
methods?: RouteHttpMethod[] | string[] | null;
[key: string]: unknown;
}
export interface RoutesDiff {
manifest_sha256_old?: string | null;
manifest_sha256_new?: string | null;
added: RouteEntry[];
removed: RouteEntry[];
changed: RouteChangeEntry[];
totals?: {
added: number;
removed: number;
changed: number;
};
}
export interface ReleaseInventoryBase<StateKind extends ReleaseInventoryStateKind = ReleaseInventoryStateKind> {
kind: "release_inventory";
schema_version: "agent-deploy-observability.v1";
release_id: string | null;
project_id: string;
parent_id: string | null;
status: ReleaseInventoryStatus | null;
static_continuity?: StaticContinuity;
static_continuity_paths?: {
plan_id: string;
total_count: number;
paths: string[];
entries?: Array<{
public_path: string;
source_release_id: string;
source_release_generation: number;
origin_retention_seconds?: number;
origin_available_until?: string;
}>;
next_cursor: string | null;
};
manifest_digest: string | null;
created_at: string | null;
created_by: string | null;
actor: OperationActorSnapshot | null;
activated_at: string | null;
superseded_at: string | null;
operation_id: string | null;
plan_id: string | null;
events_url: string | null;
effective: boolean;
state_kind: StateKind;
release_generation: number | null;
static_manifest_sha256: string | null;
static_manifest_metadata: StaticManifestMetadata | null;
site: {
paths: SitePathEntry[];
totals?: {
paths: number;
};
};
static_public_paths?: StaticPublicPathInventoryEntry[];
/** tenant-site-embedding: the release's framing opt-in as catalog keys, or
* `null` (deny). Absent on an older gateway = unknown, not null. */
embedding?: SiteEmbeddingSpec | null;
/** site.sign_in_path: the app's own sign-in page for MCP authorization, or
* `null` (the hosted `/auth/sign-in`). Absent on an older gateway = unknown. */
sign_in_path?: string | null;
/** site.noindex: `true` when every response of the host carries
* `X-Robots-Tag: noindex, nofollow, nosnippet`. Absent on an older gateway
* = unknown. */
noindex?: boolean;
functions: ReleaseFunctionEntry[];
secrets: {
keys: string[];
};
subdomains: {
names: string[];
};
routes: MaterializedRoutes;
migrations_applied: MigrationAppliedEntry[];
/** Capability `routed-locale-context`. The materialized i18n slice on
* this release, or `null` when the release has no i18n (clean base or
* cleared via `i18n: null`). Gives callers a positive readback after
* a deploy — `apply()` not throwing is necessary but not sufficient
* verification. Older gateways predating the inventory-i18n change
* may omit the field entirely; treat `undefined` as "unknown, fetch
* on a newer gateway" rather than as "no slice". */
i18n?: ReleaseInventoryI18n | null;
warnings?: DeployObservabilityWarningEntry[];
}
/** Inventory built from the currently live project state. */
export type ActiveReleaseInventory = ReleaseInventoryBase<"current_live">;
/** Inventory for a specific release id. Superseded/active releases are
* materialized effective state; staged/failed releases are desired manifests. */
export type ReleaseSnapshotInventory = ReleaseInventoryBase<"effective" | "desired_manifest">;
export type ReleaseInventory = ActiveReleaseInventory | ReleaseSnapshotInventory;
export interface DeployObservabilityWarningEntry {
code: string;
severity: "info" | "warn" | "high";
requires_confirmation: boolean;
message: string;
affected?: string[];
details?: Record<string, unknown>;
confidence?: "heuristic";
}
export interface PlanMigrationDiff {
new: Array<{
id: string;
checksum_hex: string;
transaction: "default" | "none";
}>;
noop: Array<{
id: string;
checksum_hex: string;
}>;
}
export interface SiteDiff {
added: Array<{
path: string;
sha256: string;
content_type: string;
}>;
removed: string[];
changed: Array<{
path: string;
sha256_old: string;
sha256_new: string;
content_type_old: string;
content_type_new: string;
content_type_inferred?: true;
}>;
totals?: {
added: number;
removed: number;
changed: number;
};
}
export interface FunctionsDiff {
added: string[];
removed: string[];
changed: Array<{
name: string;
fields_changed: Array<"code_hash" | "runtime" | "timeout_seconds" | "memory_mb" | "schedule">;
}>;
}
/** Secrets have no `changed` bucket; values and value-derived signals are
* intentionally absent from deploy observability responses. */
export interface SecretsDiff {
added: string[];
removed: string[];
}
/** Subdomains have no `changed` bucket. */
export interface SubdomainsDiff {
added: string[];
removed: string[];
}
export interface PlanDiffEnvelope {
is_noop: boolean;
summary: string;
warnings: WarningEntry[];
migrations: PlanMigrationDiff;
site: SiteDiff;
functions: FunctionsDiff;
secrets: SecretsDiff;
subdomains: SubdomainsDiff;
routes: RoutesDiff;
static_assets: StaticAssetsDiff;
}
export interface ReleaseToReleaseDiff {
kind: "release_diff";
schema_version: "agent-deploy-observability.v1";
from_release_id: string | null;
to_release_id: string | null;
is_noop: boolean;
summary: string;
warnings: WarningEntry[];
migrations: {
applied_between_releases: string[];
};
site: SiteDiff;
functions: FunctionsDiff;
secrets: SecretsDiff;
subdomains: SubdomainsDiff;
routes: RoutesDiff;
static_assets: StaticAssetsDiff;
/** MCP tools added, removed, or changed (absent from older gateways). */
tools?: ToolsDiff;
}
export type ReleaseDiffTarget = "empty" | "active" | (string & {});
export type ReleaseDiffToTarget = "active" | (string & {});
export interface ReleaseInventoryOptions {
project: string;
/** Maximum number of site path entries to include. Gateway default: 5,000;
* gateway hard maximum: 25,000. */
siteLimit?: number;
}
export interface ReleaseInventoryByIdOptions extends ReleaseInventoryOptions {
staticContinuityPlanId?: string;
staticContinuityCursor?: string;
releaseId: string;
}
export interface ReleaseDiffOptions {
project: string;
from: ReleaseDiffTarget;
to: ReleaseDiffToTarget;
/** Maximum number of entries in each site diff bucket. Gateway default:
* 1,000. */
limit?: number;
}
/** Server-side summary of the diff between the base release and the new
* spec. v1.39+ plans may return the structured `PlanDiffEnvelope`; older
* gateways may still return legacy buckets. Migrations mismatch is a hard
* deploy error in the modern success path, but the legacy array is kept here
* for flag-off/backward compatibility. */
export interface DeployDiff {
resources?: Record<string, unknown>;
is_noop?: boolean;
summary?: string;
warnings?: WarningEntry[];
migrations?: PlanMigrationDiff | Array<{
id: string;
state: "new" | "noop" | "checksum_mismatch";
}>;
site?: SiteDiff;
functions?: FunctionsDiff;
secrets?: SecretsDiff;
routes?: RoutesDiff | Array<{
kind: "added" | "removed";
path: string;
}>;
static_assets?: StaticAssetsDiff;
subdomains?: SubdomainsDiff | Array<{
kind: "added" | "removed";
subdomain: string;
}>;
[key: string]: unknown;
}
export interface DeploySummarySitePaths {
added: number;
changed: number;
removed: number;
unchanged?: number;
total_changed: number;
}
export interface DeploySummarySiteCas {
newly_uploaded_bytes: number;
reused_bytes: number;
deployment_copy_bytes_eliminated: number;
}
export interface DeploySummarySite {
paths?: DeploySummarySitePaths;
cas?: DeploySummarySiteCas;
}
export interface DeploySummaryFunctions {
added: string[];
removed: string[];
changed: Array<{
name: string;
fields_changed: Array<FunctionsDiff["changed"][number]["fields_changed"][number]>;
}>;
}
export interface DeploySummaryMigrations {
new: string[];
noop: string[];
}
export interface DeploySummaryResourceCounts {
added: number;
changed: number;
removed: number;
}
export interface DeploySummaryKeyCounts {
added: number;
removed: number;
}
export interface DeploySummaryWarnings {
count: number;
blocking: number;
codes: string[];
}
export interface DeploySummary {
static_continuity?: StaticContinuity;
schema_version: "deploy-summary.v1";
release_id: string;
operation_id: string;
is_noop?: boolean;
headline: string;
site?: DeploySummarySite;
functions?: DeploySummaryFunctions;
migrations?: DeploySummaryMigrations;
routes?: DeploySummaryResourceCounts;
secrets?: DeploySummaryKeyCounts;
subdomains?: DeploySummaryKeyCounts;
warnings: DeploySummaryWarnings;
}
export type EdgePointerTarget = "kvs" | "cloudfront_invalidation" | "cloudflare_kv";
export type EdgePointerStatus = "pending" | "applied" | "failed" | "not_applicable";
export interface EdgePointerUpdateStatus {
target: EdgePointerTarget;
status: EdgePointerStatus;
attempts?: number;
last_error?: string | null;
updated_at?: string | null;
}
export type EdgeBlockState = "converging" | "coherent" | "unknown" | "not_applicable";
export interface EdgeBlock {
checked_at?: string | null;
received_at?: string;
state: EdgeBlockState;
expected_max_lag_seconds?: number | null;
pointer_updates: Record<EdgePointerTarget, EdgePointerUpdateStatus | undefined>;
verify_url?: string | null;
}
export type EdgeProbePathState = "coherent" | "stale_prior_release" | "unknown" | "error";
export type EdgeProbeObservedConfidence = "identity" | "body_hash" | "weak" | "error";
export interface EdgeCoherencePathObservation {
checked_at?: string;
verification_basis?: "release_identity" | "content_hash" | "weak_metadata";
path: string;
host: string;
state: EdgeProbePathState;
observed_confidence: EdgeProbeObservedConfidence;
expected_release_id: string;
expected_release_generation: number | null;
observed_release_id?: string | null;
observed_release_generation?: number | null;
expected_sha256?: string | null;
observed_sha256?: string | null;
status?: number | null;
content_type?: string | null;
content_length?: number | null;
x_cache?: string | null;
age_seconds?: number | null;
error?: string | null;
}
export interface EdgeCoherenceReport {
checked_at?: string;
verification_basis?: "no_mutable_paths";
coherent: boolean;
operation_id: string;
project_id: string;
release_id: string;
release_generation: number | null;
paths: EdgeCoherencePathObservation[];
pending_count: number;
paths_truncated: boolean;
path_count: number;
total_path_count: number;
vantage: string;
probe_may_have_warmed_cache: boolean;
pointer_updates: Record<EdgePointerTarget, EdgePointerUpdateStatus | undefined>;
next_actions: string[];
probe_basis?: "no_mutable_paths";
}
export interface EdgeCoherencePollEvent {
attempts: number;
elapsedMs: number;
report: EdgeCoherenceReport;
}
export interface EdgeCoherenceWaitOptions {
project: string;
timeoutMs?: number;
intervalMs?: number;
onPoll?: (event: EdgeCoherencePollEvent) => void;
}
export interface EdgeCoherenceWaitResult {
coherent: boolean;
attempts: number;
elapsedMs: number;
report: EdgeCoherenceReport;
}
/** All operation states the gateway exposes. The SDK polls until it reaches
* a terminal state. */
export type OperationStatus = "planning" | "uploading" | "committing" | "staging" | "gating" | "migrating" | "schema_settling" | "activating" | "activation_pending" | "needs_repair" | "ready" | "failed" | "rolled_back";
/** Status returned by the synchronous commit response (a subset of
* OperationStatus — the commit endpoint never returns mid-phase states like
* `gating`/`migrating`; those only appear via subsequent operation polls). */
export type CommitStatus = "running" | "schema_settling" | "activation_pending" | "ready" | "failed";
export interface CommitResponse {
warnings?: WarningEntry[];
static_continuity?: StaticContinuity;
operation_id: string;
status: CommitStatus;
release_id?: string;
urls?: Record<string, string>;
error?: GatewayDeployError | null;
edge?: EdgeBlock;
subdomain_bindings?: SubdomainBindingFreshness[];
restore_point?: CommitRestorePoint;
snapshot_skipped_reason?: string;
actor?: OperationActorSnapshot | null;
/** Gateway riders on a synchronous `ready` commit: the events-feed `poll`
* entry, `watch_errors`, and the `hand_to_member` offer. Passed through
* to {@link DeployResult.next_actions} verbatim. */
next_actions?: NextAction[];
}
export interface CommitRestorePoint {
snapshot_id: string;
restore_url: string;
}
export interface OperationSnapshot {
warnings?: WarningEntry[];
static_continuity?: StaticContinuity;
operation_id: string;
project_id: string;
plan_id: string;
status: OperationStatus;
base_release_id: string | null;
target_release_id: string | null;
release_id: string | null;
urls: Record<string, string> | null;
payment_required: PaymentRequiredHint | null;
error: GatewayDeployError | null;
edge?: EdgeBlock;
subdomain_bindings?: SubdomainBindingFreshness[];
activate_attempts: number;
last_activate_attempt_at: string | null;
created_at: string;
updated_at: string;
rehearsal_report?: ApplyRehearsalReport;
actor?: OperationActorSnapshot | null;
/** Present on a `ready` snapshot only: the same gateway riders a synchronous
* ready commit carries (`poll`, `watch_errors`, `hand_to_member`). A
* polled deploy is never a weaker contract than the commit response. */
next_actions?: NextAction[];
}
export type RehearsalTeardownPolicy = "keep" | "on_pass" | "always";
export type RehearsalStatus = "passed" | "failed";
export type RehearsalStepStatus = "passed" | "failed" | "skipped";
export interface RehearsePlanOptions {
teardown?: RehearsalTeardownPolicy;
project?: string;
}
export interface RehearsalMigrationResult {
id: string;
status: RehearsalStepStatus;
error?: string;
}
export interface RehearsalCheckResult {
name: string;
type: "migration" | "static" | "function" | "declared" | (string & {});
status: RehearsalStepStatus;
target?: string;
method?: string;
expected_status?: number | number[];
actual_status?: number | null;
duration_ms: number;
error?: string;
}
export interface ApplyRehearsalReport {
kind: "rehearsal_report";
status: RehearsalStatus;
operation_id: string;
source_project_id: string;
plan_id: string;
branch_project_id: string | null;
branch_url: string | null;
branch_plan_id: string | null;
branch_operation_id: string | null;
snapshot_id: string | null;
started_at: string;
completed_at: string;
duration_ms: number;
migrations: RehearsalMigrationResult[];
checks: RehearsalCheckResult[];
teardown: {
policy: RehearsalTeardownPolicy;
action: "kept" | "deleted" | "delete_failed" | "skipped" | (string & {});
error?: string;
};
next_actions: Array<{
type: "commit_plan" | "discard_branch" | "keep_branch" | (string & {});
command?: string;
method?: string;
path?: string;
url?: string;
/** `commit_plan` carries the bound commit body: a rehearsed plan commits
* only with this `required_plan`. */
body?: {
required_plan: {
plan_id: string;
plan_fingerprint: string;
};
};
message: string;
}>;
error?: GatewayDeployError;
}
export interface RehearsePlanResult {
operation_id: string;
status: OperationStatus;
poll_url: string;
report: ApplyRehearsalReport;
}
/** Response from `GET /apply/v1/operations`. The gateway may return a
* pagination cursor when there are more operations than the requested
* page size; clients pass it back as `?cursor=` to fetch the next page. */
export interface DeployListOptions {
project: string;
limit?: number;
cursor?: string;
}
export interface DeployListResponse {
operations: OperationSnapshot[];
cursor?: string | null;
}
/** Response from `GET /apply/v1/operations/:operation_id/events`. Returns the
* synthesized phase event stream the gateway has recorded so far for the
* operation. Same shape as the events emitted by `r.project(id).apply.start().events()`
* during an in-flight deploy. */
export interface DeployEventsResponse {
events: DeployEvent[];
cursor?: string | null;
}
/** Wire-shape for a structured deploy error from the gateway. The SDK
* translates this into `Run402DeployError` for callers. The gateway is
* permitted to omit `message` for terse validation errors (e.g. just
* `{code: "invalid_spec"}`); the SDK synthesizes a default in that case. */
export interface GatewayDeployError {
code: string;
phase?: string | null;
resource?: string | null;
message?: string;
category?: string;
retryable?: boolean;
safe_to_retry?: boolean;
mutation_state?: string;
trace_id?: string;
details?: Record<string, unknown> | null;
next_actions?: unknown[];
fix?: {
action: string;
path?: string;
[key: string]: unknown;
} | null;
logs?: string[] | null;
rolled_back?: boolean;
/** Operation id supplied by the gateway when the error is associated with a
* specific deploy operation (e.g., MIGRATION_CHECKSUM_MISMATCH). The SDK
* prefers this over the caller-supplied operation id when constructing
* `Run402DeployError`, so resume hints round-trip correctly. */
operation_id?: string;
/** Plan id supplied by the gateway when the error originates inside a plan
* context. Same precedence rule as `operation_id`. */
plan_id?: string;
[key: string]: unknown;
}
/** What the SDK actually POSTs to `/apply/v1/plans`. Bytes are content
* refs, not inline. The gateway's wire envelope is `{ spec, manifest_ref?,
* idempotency_key? }`. Most callers never construct this directly; it's
* produced by the SDK's normalizer and exposed for the low-level
* `apply.plan` layer. */
export interface PlanRequest {
spec: Record<string, unknown>;
manifest_ref?: Record<string, unknown>;
idempotency_key?: string;
mode?: "reviewed_plan";
required_plan?: {
plan_id: string;
plan_fingerprint?: string;
};
/** vault (protocol §6.5): declares the capture this plan is bound to. */
vault?: VaultPlanDeclaration;
}
/**
* The capture declaration a vault-capable client sends at plan time. The
* gateway answers with the canonical `apply_plan_sha256` the activation token
* is minted against; without this block the plan carries no capture and a
* `vault_policy: required` project refuses its commit.
*/
export interface VaultPlanDeclaration {
capture_id: string;
snapshot_oid_hmac: string;
}
/** The vault block on a commit: an activation token, or an audited override. */
export type VaultCommitDeclaration = {
activation_token_id: string;
} | {
allow_unvaulted: true;
override_reason: string;
};
export interface NormalizedReleaseSpec {
project_id: string;
base?: ReleaseSpec["base"];
database?: NormalizedDatabaseSpec;
secrets?: SecretsSpec;
functions?: NormalizedFunctionsSpec;
site?: NormalizedSiteSpec;
subdomains?: SubdomainsSpec;
routes?: ReleaseRoutesSpec;
checks?: SmokeCheck[];
/** v1.48 unified-apply: post-normalization asset slice. `put` entries
* are wire-shaped `AssetPutEntry[]` (no `source` field — the SDK
* normalizer stripped them after registering byte-readers). */
assets?: NormalizedAssetSpec;
/** v2.5 routed-locale-context: same shape as `ReleaseSpec.i18n`. Passed
* through unchanged from caller to wire so the gateway can plan the
* slice and downstream warnings/diffs can reference it. */
i18n?: I18nSpec | null;
}
export interface NormalizedAssetSpec {
put?: AssetPutEntry[];
delete?: string[];
sync?: {
prefix: string;
prune: true;
confirm?: AssetSyncPruneConfirm;
};
}
export interface NormalizedDatabaseSpec {
migrations?: NormalizedMigrationSpec[];
expose?: ExposeManifest;
zero_downtime?: boolean;
}
export interface NormalizedMigrationSpec {
id: string;
/** Lowercase hex SHA-256 of the migration SQL. Required by the gateway. */
checksum: string;
/** Cloud wire form: migration SQL staged in CAS and referenced by digest. */
sql_ref?: ContentRef;
/** Run402 Core wire form: migration SQL carried inline. */
sql?: string;
transaction?: "required" | "none";
}
export interface NormalizedFunctionsSpec {
replace?: Record<string, NormalizedFunctionSpec>;
patch?: {
set?: Record<string, NormalizedFunctionSpec>;
delete?: string[];
};
}
export interface NormalizedFunctionSpec {
runtime?: "node22";
source?: ContentRef;
files?: Record<string, ContentRef>;
entrypoint?: string;
config?: {
timeoutSeconds?: number;
memoryMb?: number;
};
/** Capability `apply-v1-function-deps` — see `FunctionSpec.deps`. */
deps?: string[];
triggers?: FunctionTriggerSpec[];
schedule?: string | null;
/** v1.51+ — see `FunctionSpec.requireAuth`. */
requireAuth?: boolean;
/** v1.51+ — see `FunctionSpec.requireRole`. */
requireRole?: RequireRoleSpec | null;
/** v1.52+ — see `FunctionSpec.class`. */
class?: "ssr" | "standard";
/** v1.52+ — see `FunctionSpec.capabilities`. */
capabilities?: string[];
}
export type NormalizedSiteSpec = {
replace: Record<string, ContentRef>;
patch?: never;
public_paths?: SitePublicPathsSpec;
embedding?: SiteEmbeddingSpec | null;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
patch: {
put?: Record<string, ContentRef>;
delete?: string[];
};
replace?: never;
public_paths?: SitePublicPathsSpec;
embedding?: SiteEmbeddingSpec | null;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
public_paths: SitePublicPathsSpec;
replace?: never;
patch?: never;
embedding?: SiteEmbeddingSpec | null;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
embedding: SiteEmbeddingSpec | null;
replace?: never;
patch?: never;
public_paths?: never;
sign_in_path?: string | null;
noindex?: boolean | null;
} | {
sign_in_path: string | null;
replace?: never;
patch?: never;
public_paths?: never;
embedding?: never;
noindex?: boolean | null;
} | {
noindex: boolean | null;
replace?: never;
patch?: never;
public_paths?: never;
embedding?: never;
sign_in_path?: never;
};
export type DeployEvent = {
type: "plan.started";
} | {
type: "rehearsal.started";
planId: string;
} | {
type: "rehearsal.finished";
planId: string;
status: "passed" | "failed";
operationId: string;
branchProjectId: string | null;
durationMs: number;
} | {
type: "rehearsal.skipped";
reason: NonNullable<DeployRehearsalBlock["reason"]>;
} | {
type: "plan.diff";
diff: DeployDiff;
} | {
type: "plan.warnings";
warnings: WarningEntry[];
} | {
type: "payment.required";
amount: string;
asset: string;
payTo: string;
reason: string;
} | {
type: "payment.paid";
tx?: string;
} | {
type: "deploy.retry";
attempt: number;
nextAttempt: number;
maxAttempts: number;
delayMs: number;
code: string;
phase: string | null;
resource: string | null;
operationId: string | null;
planId: string | null;
message: string;
} | {
type: "content.upload.skipped";
label: string;
sha256: string;
reason: "present" | "satisfied_by_plan";
/** Which spec slice(s) contributed this SHA. `"release"` for
* database / functions / site / migrations; `"asset"` for the v1.48
* unified-apply asset slice; `"mixed"` when the same SHA was
* registered by both kinds (cross-kind CAS dedup). */
slice_kind?: "release" | "asset" | "mixed";
} | {
type: "content.upload.progress";
label: string;
sha256: string;
done: number;
total: number;
slice_kind?: "release" | "asset" | "mixed";
} | {
type: "commit.phase";
phase: "validate" | "stage" | "migrate-gate" | "migrate" | "schema-settle" | "activate" | "ready";
status: "started" | "done" | "failed";
/** Sorted list of slice kinds the apply's spec carried. Useful for
* agents grouping per-phase telemetry by slice category. Stable
* across phase transitions of the same apply. */
slice_kinds?: ("release" | "asset")[];
} | {
type: "commit.phase.detail";
id?: string;
operation_id?: string;
project_id?: string;
phase: string;
status: "started" | "done" | "failed" | "skipped" | "deferred";
message: string | null;
details: Record<string, unknown>;
created_at?: string;
updated_at?: string;
} | {
type: "log";
resource: string;
stream: "stdout" | "stderr";
line: string;
} | {
type: "ready";
releaseId: string;
urls: Record<string, string>;
slice_kinds?: ("release" | "asset")[];
};
export interface DeployResult {
static_continuity?: StaticContinuity;
release_id: string;
operation_id: string;
urls: Record<string, string>;
/** How rehearsal was handled: `passed` (rehearsed on a branch, then
* committed), or `skipped` with a `reason`. A failed rehearsal never
* reaches a result — `apply()` throws `REHEARSAL_FAILED` carrying the
* report. Cloud only; absent on Core. */
rehearsal?: DeployRehearsalBlock;
/** Public-edge coherence/convergence hint returned by the gateway. When
* `state` is `converging`, call `p.apply.edgeCoherence(operationId)` or
* `p.apply.waitEdgeCoherent(operationId)` before declaring mutable public
* URLs freshly visible everywhere. */
edge?: EdgeBlock;
/** The `diff` from the plan response — useful for "what changed in this
* deploy" UX. */
diff: DeployDiff;
/** Structured plan warnings that were observed before commit. */
warnings: WarningEntry[];
/**
* Advisory follow-ups — never a gateway plan warning (those stay in
* {@link warnings}). Carries the gateway's own riders from a synchronous
* `ready` commit verbatim (`poll` positioned at this deploy's activation
* event, `watch_errors`, and `hand_to_member` — the offer to hand your
* human the site and console links and relay Run402's free promotion),
* plus the one shape the deploy itself synthesizes:
* `vault_policy_required`, offered on every deploy of a vaulted project
* whose `vault_policy` was never set (repo-first-onramp D3). Absent when
* there is nothing to offer. A commit that went asynchronous and was polled to
* `ready` carries the same gateway riders from the ready snapshot.
*/
next_actions?: NextAction[];
/** Freshness hints for stable hosts affected by this deploy. Managed
* subdomains and custom domains are eventually consistent at the edge, so
* app installers can distinguish fresh propagation misses from real verify
* failures. */
subdomain_bindings?: SubdomainBindingFreshness[];
restore_point?: CommitRestorePoint;
snapshot_skipped_reason?: string;
/** v1.48 unified-apply: present when the spec carried an `assets` slice.
* Built from the plan response's `asset_entries[].asset_ref` (gateway-
* authoritative URLs) plus the realised `content.upload.*` event stream
* for `totals.bytes_uploaded` / `bytes_reused`. `undefined` for release-
* only applies. */
assets?: AssetManifest;
}
/**
* Batch result envelope for the assets slice. Mirrors the @run402/sdk/node
* `AssetManifest` shape exposed from the Node entry; defined on the
* isomorphic side so consumers reading `DeployResult.assets` get a typed
* shape without importing the Node entry.
*
* `byKey` and `manifest` MUST be constructed with `Object.create(null)`
* so attacker-controlled or filesystem-derived keys like
* `__proto__` / `constructor` / `toString` don't collide with prototype
* properties.
*/
export interface AssetManifest {
/** Iteration order = submission order. */
list: AssetManifestEntry[];
/** Null-prototype object indexed by asset key. */
byKey: Record<string, AssetManifestEntry>;
/** Null-prototype object, JSON-serializable, suitable for writing to
* disk as a `asset-manifest.json` artifact. */
manifest: Record<string, AssetManifestEntry>;
totals: {
files: number;
bytes_uploaded: number;
bytes_reused: number;
duration_ms: number;
};
/** Populated when the apply carried `assets.sync.prune: true`. */
pruned?: string[];
}
export interface AssetManifestEntry {
key: string;
sha256: string;
size_bytes: number;
content_type: string;
visibility: "public" | "private";
/** Mutable public URL (alias for the deprecated `url` field). */
url: string | null;
/** Content-hashed immutable URL. Null when the entry is private or
* `immutable: false`. */
immutable_url: string | null;
/** CDN-form of the mutable URL (auto-subdomain). Alias maintained for
* parity with single-asset `AssetRef`. */
cdn_url: string | null;
/** CDN-form of the immutable URL. The recommended URL for generated
* HTML/CSS/JS — pair with `sri`. */
cdn_immutable_url: string | null;
/** Browser SRI form: `sha256-<base64>`. Null on non-immutable / private
* uploads. */
sri: string | null;
/** Strong ETag `"sha256-<hex>"`. */
etag: string | null;
/** RFC 9530 `Content-Digest` value `sha-256=:<base64>:`. */
content_digest: string | null;
width_px?: number;
height_px?: number;
blurhash?: string;
variant_spec_version?: string;
display_url?: string;
display_immutable_url?: string;
variants?: {
thumb?: ResolvedAssetVariant;
medium?: ResolvedAssetVariant;
large?: ResolvedAssetVariant;
display_jpeg?: ResolvedAssetVariant;
};
metadata?: Record<string, string | number | boolean | string[]> | null;
image_format?: string | null;
image_info?: Record<string, unknown> | null;
image_exif?: Record<string, unknown> | null;
image_exif_policy?: "keep" | "strip" | null;
blurhash_data_url?: string | null;
asset_schema?: "v1.49" | "v1.50" | "v1.54" | null;
}
export interface ApplyOptions {
/** Synchronous progress callback. Throws inside the callback are caught
* and silently dropped — a buggy consumer cannot abort a deploy. */
onEvent?: (event: DeployEvent) => void;
/** Client-side idempotency key. The SDK passes this to the gateway, which
* combines it with the manifest digest to deduplicate retries. Default:
* the gateway-computed manifest digest itself acts as the key. */
idempotencyKey?: string;
/** Continue past plan warnings that require confirmation. Default false:
* `apply()` aborts before upload/commit so agents can set missing secrets,
* inspect warnings, or use the low-level plan/upload/commit flow. */
allowWarnings?: boolean;
/** Continue past specific confirmation-required warning codes. Every
* blocking warning must be covered by this list or by `allowWarnings`. */
allowWarningCodes?: string[];
/** Skip the automatic rehearsal `apply()` runs for a migration-bearing plan
* against a project with a live release. Default false: when the plan's
* `rehearsal.available` is true the plan is rehearsed on a contained
* branch and committed only on a passing report. */
noRehearse?: boolean;
/** Bind this apply to a reviewed plan returned by `plan(..., { mode: "reviewedPlan" })`. */
requiredPlan?: {
planId: string;
planFingerprint?: string;
};
/** Automatic safe-race retries after the initial `apply()` attempt.
* Default: 2 retries (3 total attempts). Pass 0 to disable automatic
* retry and surface the first safe deploy race to the caller. */
maxRetries?: number;
/** Runtime target. CLI sets this automatically from `run402 init --api-base`.
* Cloud keeps CAS content plans and operation polling; Core uses the
* self-hosted gateway's direct content staging and immediate commit result. */
target?: "cloud" | "core";
/** vault §6.5 — supplied by `applyWithVault`, never by hand. */
vault?: VaultApplyHooks;
}
/**
* The vault handshake `applyWithVault` (`@run402/sdk/node`) injects into
* `apply()`. The declaration rides the plan; `authorize` is called once the
* plan exists and its content is uploaded, and returns the block the commit
* presents. Throwing from `authorize` aborts the apply with NOTHING committed
* — which is how `SNAPSHOT_MOVED_DURING_DEPLOY` stops a deploy.
*
* An apply carrying these hooks does not auto-retry: a retry would re-plan
* under a NEW operation, and an activation token is minted for exactly one.
*/
export interface VaultApplyHooks {
declaration: VaultPlanDeclaration;
authorize(planned: {
plan_id: string;
operation_id: string;
apply_plan_sha256: string | null;
}): Promise<VaultCommitDeclaration>;
}
export interface StartOptions {
idempotencyKey?: string;
/** Receives progress events for plan diff, content uploads, commit phases,
* warnings, payment requirements, and final release activation. Callback
* exceptions are swallowed so UI/logging hooks cannot fail the deploy. */
onEvent?: (event: DeployEvent) => void;
/** By default, warnings with `requires_confirmation` stop before
* upload/commit. Set true only after inspecting the warnings. */
allowWarnings?: boolean;
/** Continue past specific confirmation-required warning codes. */
allowWarningCodes?: string[];
requiredPlan?: {
planId: string;
planFingerprint?: string;
};
}
/**
* Options for the `r.project(id).apply.promote(releaseId, opts?)`
* pointer-swap operation. Mirrors `ApplyOptions` for the parts that apply
* (`allowWarnings`, `allowWarningCodes`); skips the parts that don't
* (`onEvent` — promote is a single-shot operation, no per-phase events;
* `idempotencyKey` — gateway derives idempotency from `(project, release_id)`;
* `maxRetries` — no plan-time race window).
*/
export interface PromoteOptions {
/** Continue past confirmation-required warnings (e.g. MIGRATIONS_NOT_REVERSIBLE).
* Default false: the gateway aborts before the pointer swap when a
* blocking warning isn't covered. */
allowWarnings?: boolean;
/** Cover specific confirmation-required warning codes. Every blocking
* warning must be covered by this list or by `allowWarnings`. */
allowWarningCodes?: string[];
}
/**
* Result envelope returned by `r.project(id).apply.promote(releaseId, opts?)`.
* The promote operation is a single-shot pointer swap; no phase events,
* no payment-required hook (promote uses existing-release content).
*/
export interface PromoteResult {
static_continuity?: StaticContinuity;
status: "ok";
/** The release id now live on the project. Equal to the input releaseId. */
release_id: string;
/** The new `internal.apply_operations` row id (created with kind='promote'). */
operation_id: string;
/** The release that was live BEFORE the swap. */
previous_release_id: string;
/** Structured diff between previous and new live release. */
diff: PromoteDiff;
/** Any structured warnings produced — including ones the caller acked. */
warnings: WarningEntry[];
/**
* Functions re-deployed to the target release's code after the pointer
* swap (functions are not versioned per release). `failed` entries keep the
* code they had and also appear as a `FUNCTION_REDEPLOY_FAILED` warning.
* Absent from gateways that predate function rollback.
*/
functions?: FunctionRollbackOutcome;
}
export interface FunctionRollbackOutcome {
redeployed: string[];
failed: Array<{
name: string;
code: "FUNCTION_SOURCE_NOT_FOUND" | "FUNCTION_REDEPLOY_FAILED" | (string & {});
message: string;
}>;
}
export interface PromoteDiff {
functions: {
only_in_current: string[];
only_in_target: string[];
changed: string[];
};
migrations: {
only_in_current: string[];
only_in_target: string[];
};
site_paths: {
added_in_current: number;
removed_in_current: number;
};
}
export interface DeployOperation {
/** The operation id (also exposed via the snapshot). */
readonly id: string;
/** Async iterable of events for as long as the operation is non-terminal. */
events(): AsyncIterable<DeployEvent>;
/** Resolves with the final result, or rejects with `Run402DeployError`. */
result(): Promise<DeployResult>;
/** Latest snapshot from the gateway. */
snapshot(): Promise<OperationSnapshot>;
}
/** Automatic origin continuity for previously public non-HTML paths. */
export interface StaticContinuity {
mode: 'absent_public_paths';
origin_retention_seconds: 3600;
scope: 'previously_public_non_html';
source_release_id: string | null;
source_release_generation: number | null;
retained_path_count: number;
/** Omitted for plans; null when this release has no retired origin deadline. */
origin_available_until?: string | null;
}
export {};
//# sourceMappingURL=deploy.types.d.ts.map