/** * 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 | 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-`) 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; /** * 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; 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: 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; /** 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; /** 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 = _`; 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>; views?: Array>; rpcs?: Array>; [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; /** Surgical updates — only listed functions change. */ patch?: { set?: Record; 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; retry?: Record; 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 .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; }; /** 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; } | { patch: { put?: FileSet | LocalDirRef; delete?: string[]; }; replace?: never; public_paths?: SitePublicPathsSpec; embedding?: SiteEmbeddingSpec | null; } | { public_paths: SitePublicPathsSpec; replace?: never; patch?: never; embedding?: SiteEmbeddingSpec | null; } | { embedding: SiteEmbeddingSpec | null; replace?: never; patch?: never; public_paths?: 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:` — 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"]; export type RoutePricingNetwork = (typeof ROUTE_PRICING_NETWORKS)[number]; export interface RoutePricingSpec { mode: "always"; amount_usd_micros: number; pay_to: "org_default_payout"; /** Omit to accept production mainnet only. Add `"testnet"` explicitly for testnet acceptance. */ networks?: readonly RoutePricingNetwork[]; /** Ask the function to declare committed fulfillment for a signed merchant receipt. */ receipt?: "on_fulfillment"; } 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; /** Durable acknowledgement for intentional read-only wildcard function routes. * Valid only with final-wildcard function routes whose methods are limited * to GET/HEAD. */ acknowledge_readonly?: true; } /** 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 | (string & {}); export type KnownStaticCacheClass = "html" | "immutable_versioned" | "revalidating_asset"; export type StaticCacheClass = LiteralUnion; export interface StaticManifestMetadata { file_count: number; total_bytes: number; cache_classes: Record; cache_class_sources: Record; 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"; export type DeployResolveMatch = LiteralUnion; 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; 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; 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, "project"> & { project?: string; }) | (Omit, "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; 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>; emergency_fallback?: Record | null; edge_propagation?: EdgePropagationDiagnostics | null; /** 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[]; /** gitvault (protocol §6.5). Present only when the plan declared a capture. */ gitvault?: { /** 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), or `migrations_unchanged` (every declared migration is a * checksum-identical noop already applied to the live project — nothing * would run on the branch). `null` when available. */ reason: null | "no_migrations" | "no_live_release" | "migrations_unchanged"; /** Present with `reason: "no_live_release"` / `"migrations_unchanged"`: 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; `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" | "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 | null; image_format?: string | null; image_info?: Record | null; image_exif?: Record | 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; 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[]; } 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; 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 { 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; 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; 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; } 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; 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; }>; } 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; 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; 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; 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_operator` 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 | 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_operator`). 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 | 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; manifest_ref?: Record; idempotency_key?: string; mode?: "reviewed_plan"; required_plan?: { plan_id: string; plan_fingerprint?: string; }; /** gitvault (protocol §6.5): declares the capture this plan is bound to. */ gitvault?: GitvaultPlanDeclaration; } /** * The capture declaration a gitvault-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 * `gitvault_policy: required` project refuses its commit. */ export interface GitvaultPlanDeclaration { capture_id: string; snapshot_oid_hmac: string; } /** The gitvault block on a commit: an activation token, or an audited override. */ export type GitvaultCommitDeclaration = { activation_token_id: string; } | { allow_unvaulted: true; override_reason: string; }; export interface NormalizedReleaseSpec { project: 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; patch?: { set?: Record; delete?: string[]; }; } export interface NormalizedFunctionSpec { runtime?: "node22"; source?: ContentRef; files?: Record; 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; patch?: never; public_paths?: SitePublicPathsSpec; embedding?: SiteEmbeddingSpec | null; } | { patch: { put?: Record; delete?: string[]; }; replace?: never; public_paths?: SitePublicPathsSpec; embedding?: SiteEmbeddingSpec | null; } | { public_paths: SitePublicPathsSpec; replace?: never; patch?: never; embedding?: SiteEmbeddingSpec | null; } | { embedding: SiteEmbeddingSpec | null; replace?: never; patch?: never; public_paths?: 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; } | { 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; created_at?: string; updated_at?: string; } | { type: "log"; resource: string; stream: "stdout" | "stderr"; line: string; } | { type: "ready"; releaseId: string; urls: Record; slice_kinds?: ("release" | "asset")[]; }; export interface DeployResult { static_continuity?: StaticContinuity; release_id: string; operation_id: string; urls: Record; /** 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_operator` — 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: * `gitvault_policy_required`, offered on every deploy of a vaulted project * whose `gitvault_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; /** Null-prototype object, JSON-serializable, suitable for writing to * disk as a `asset-manifest.json` artifact. */ manifest: Record; 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-`. Null on non-immutable / private * uploads. */ sri: string | null; /** Strong ETag `"sha256-"`. */ etag: string | null; /** RFC 9530 `Content-Digest` value `sha-256=::`. */ 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 | null; image_format?: string | null; image_info?: Record | null; image_exif?: Record | 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"; /** gitvault §6.5 — supplied by `applyWithGitvault`, never by hand. */ gitvault?: GitvaultApplyHooks; } /** * The gitvault handshake `applyWithGitvault` (`@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 GitvaultApplyHooks { declaration: GitvaultPlanDeclaration; authorize(planned: { plan_id: string; operation_id: string; apply_plan_sha256: string | null; }): Promise; } 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?)` operator * 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[]; } 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; /** Resolves with the final result, or rejects with `Run402DeployError`. */ result(): Promise; /** Latest snapshot from the gateway. */ snapshot(): Promise; } /** 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