/** * @file Shared TypeScript types, constants, and utilities for the ShipStatic platform. * This package is the single source of truth for all shared data structures. */ /** * Deployment status constants */ export declare const DeploymentStatus: { readonly PENDING: "pending"; readonly SUCCESS: "success"; readonly FAILED: "failed"; readonly DELETING: "deleting"; }; export type DeploymentStatusType = (typeof DeploymentStatus)[keyof typeof DeploymentStatus]; /** * Where a deployment came from — the origin-tracking vocabulary. * * A closed set with many authors. It lived in the API's config until * 2026-08-06, where being server-side made it unenforceable in the one * direction that matters — every client wrote a bare string, and a value * outside the set was **silently dropped** by the server, so a typo did not * fail anywhere. It stopped recording where deploys came from and said nothing. * * **The origin law: origin is declared by whatever we control — our code where * we ship code, our URL where we ship only a URL.** One rule decides every * member here and every future one: * * - **Where the platform ships CODE, the code declares it.** `web`, `sdk`, * `cli`, `git`, `n8n` and `vsc` are surfaces this platform authors, so each * names itself in its own source and nothing external is needed to tell them * apart. * - **Where the platform ships only a URL, the URL declares it.** A * marketplace listing runs somebody else's client against a bare endpoint — * every one of them the same server speaking the same protocol, and * indistinguishable in a request. The only thing such a listing's traffic * has in common is the URL its users were handed, so the hosted MCP serves * one DOOR per listing and the door's path IS the value: `gpt`, `cld`, * `crs`. * * **A member names the most specific surface the platform can honestly * claim**, which is what makes the two FALLBACKS fallbacks rather than peers * of the named surfaces. `mcp` is any MCP host that was never handed a door of * its own; `api` is a call that reached the REST API naming nothing at all. * Guessing past either would be inventing attribution rather than recording * it, which is the one thing this vocabulary exists to prevent. * * Every member is three lowercase characters — the property that lets a * channel door's path and its attribution be spelled the same. The suite pins * both the width and the channel members by name. */ export declare const DeploymentVia: { /** The web dashboard. */ readonly WEB: "web"; /** A program embedding the SDK directly. */ readonly SDK: "sdk"; /** The `ship` CLI. */ readonly CLI: "cli"; /** Any MCP host with no door of its own — the stdio server included. The family fallback. */ readonly MCP: "mcp"; /** The GitHub Action. */ readonly GIT: "git"; /** The n8n community node. */ readonly N8N: "n8n"; /** Channel: the ChatGPT App listing → `mcp./gpt`. */ readonly GPT: "gpt"; /** The VS Code extension. */ readonly VSC: "vsc"; /** Channel: the Claude connectors directory listing → `mcp./cld`. */ readonly CLD: "cld"; /** Channel: the Cursor marketplace listing → `mcp./crs`. */ readonly CRS: "crs"; /** * A deploy that reached the REST API naming no origin at all — the * platform-wide fallback, one altitude below `mcp`'s family fallback. * * **The API stamps it, since 2026-08-15.** A deploy that names no origin — * or names one this vocabulary does not know — is stored as `api`, so a * stored `null` now means only that the row predates attribution. * * It was declared one wave ahead of that decision, deliberately: vocabulary * must exist before a consumer can adopt it, and adding a member costs a * full constellation convoy, so the word shipped first and the server * adopted it with no convoy standing between the decision and the deploy. */ readonly API: "api"; }; export type DeploymentViaType = (typeof DeploymentVia)[keyof typeof DeploymentVia]; /** * Core deployment object - used in both API responses and SDK */ export interface Deployment { /** The deployment hostname (e.g., 'happy-cat-abc1234.shipstatic.com') */ readonly deployment: string; /** Full URL to the deployment (e.g., 'https://happy-cat-abc1234.shipstatic.com') */ readonly url: string; /** Number of files in this deployment */ readonly files: number; /** Total size of all files in bytes */ readonly size: number; /** Current deployment status */ status: DeploymentStatusType; /** Whether deployment has a ship.json config */ readonly config: boolean; /** Whether deployment has a password set */ readonly password: boolean; /** Labels for categorization and filtering (lowercase, alphanumeric with separators). Always present, empty array when none. */ labels: string[]; /** * The client/tool that created this deployment. Every deployment created * today names one — {@link DeploymentVia.API} when the caller named nothing * the vocabulary knows — so `null` is historical: the row predates * attribution. * * Deliberately wider than {@link DeploymentViaType}: this is stored data, * and rows predate the vocabulary being closed. Narrowing the ENTITY would * be a claim about every row already in the database; narrowing the * REQUEST option ({@link DeploymentUploadOptions.via}) is a claim about * what a client may send, which is ours to make. */ readonly via: string | null; /** Unix timestamp (seconds) when deployment was created */ readonly created: number; /** Unix timestamp (seconds) when deployment expires, null if never */ expires: number | null; /** Full URL to the deployment screenshot (e.g., 'https://screenshots.shipstatic.com/happy-cat-abc1234/a3f2c1b4d5e6f789') */ readonly screenshot: string; } /** * Response from deployment creation. Extends Deployment with one-time fields * only present on creation (not on subsequent GET requests). */ export interface DeploymentCreateResponse extends Deployment { /** Claim URL for public deployments. Present when deployed without credentials. */ readonly claim?: string; } /** * The half of a list response that is identical on every list. * * `GET /` answers exactly two fields — the collection under its * own plural noun, and this cursor — so the cursor is declared once here and * each response below adds only its noun. `cursor: null` means last page and * is the ENTIRE has-more signal, which is why there is no `has_more`. * * There is deliberately no `total`. A count is an aggregate over a * collection, not a property of a page; producing one would cost a COUNT * beside every page read, which is precisely what keyset pagination exists * to avoid. Counts live on the resource that summarises the collection — * `GET /account`'s `usage` for one caller, `GET /admin/stats` platform-wide. */ export interface ListResponse { /** Opaque cursor from this page; `null` on the last page. */ cursor: string | null; } /** * Pagination options for every list endpoint. The response's `cursor` feeds * the next request; a `null` cursor means the last page. Omitting both * returns the server's default first page. * * A list answers `{ , cursor }` and nothing else — `cursor` * carries the entire has-more signal, so no redundant boolean, and no * `total`. **A count is an aggregate over a collection, not a property of a * page:** including one makes every read pay for a full scan it did not ask * for, which is precisely the cost keyset pagination exists to avoid. * * Counts therefore live on the summary resource that owns them — * `GET /account` (`usage`) for a caller's own totals, `GET /admin/stats` for * platform-wide ones. Ask for a count when you want a count; ask for a page * when you want a page. */ export interface ListOptions { /** Maximum number of items to return in one page. */ limit?: number; /** Opaque cursor from the previous page's response. */ cursor?: string; } /** * Response for listing deployments */ export interface DeploymentListResponse extends ListResponse { /** Array of deployments */ deployments: Deployment[]; } /** * Acknowledgement of `DELETE /deployments/:deployment` — and the shape every * mutation with no entity left to return follows. * * **The law:** a mutation answers with the resource it affected. If the * resource still exists, that means the entity itself (`Deployment`, * `Domain`, …). Otherwise it means this: the resource noun carrying the * item's canonical key, plus the resource's own state field — and ONLY when * the resource survived in a transitional state, as an async deletion's does. * Where the resource is simply gone, the key alone is the whole answer * ({@link DomainDeleteResponse}, {@link TokenDeleteResponse}). * * Put positively: **an acknowledgement is a projection of the resource** — * its key, plus its own state field where the state changed. That is the * test to apply, and it is sharper than "no constant", which this shape * would fail on its own terms: `status` here is the literal `'deleting'` on * every success, exactly as fixed as a `changed: true` would be. * * The difference is not how predictable the value is, it is what the field * IS. `status` is the deployment's own field — the same one `GET * /deployments/:deployment` returns — so this response is `Deployment` * narrowed to two members, and a client renders it with the code it already * has. `changed: true`, `queued: true` and `success: true` are not fields of * any entity; they exist only to assert that the call worked, which the * status code already said. Sync versus accepted is likewise the status * code's job — 200 versus 202 — not a boolean's. * * No prose either (`message`): an acknowledgement is data, and each surface * composes its own copy. */ export interface DeploymentDeleteResponse { /** The deployment hostname that was marked for removal */ readonly deployment: string; /** The state the deployment is in while background cleanup runs */ readonly status: DeploymentStatusType; } /** * Domain status constants * * - PENDING: DNS not configured * - PARTIAL: DNS partially configured * - SUCCESS: DNS fully verified * - PAUSED: Domain paused due to plan enforcement (billing) */ export declare const DomainStatus: { readonly PENDING: "pending"; readonly PARTIAL: "partial"; readonly SUCCESS: "success"; readonly PAUSED: "paused"; }; export type DomainStatusType = (typeof DomainStatus)[keyof typeof DomainStatus]; /** * Core domain object - used in both API responses and SDK */ export interface Domain { /** The domain name */ readonly domain: string; /** Full URL to the domain (e.g., 'https://www.example.com') */ readonly url: string; /** The deployment hostname this domain points to (null = domain added but not yet linked) */ deployment: string | null; /** Current domain status */ status: DomainStatusType; /** Labels for categorization and filtering (lowercase, alphanumeric with separators). Always present, empty array when none. */ labels: string[]; /** Unix timestamp (seconds) when domain was created */ readonly created: number; /** Unix timestamp (seconds) when deployment was last linked, null if never linked */ linked: number | null; /** Total deployment links */ links: number; } /** * Return shape of `domains.set()` — `Domain` plus an SDK-derived flag indicating * whether the underlying `PUT /domains/:name` created the record (HTTP 201) or * updated an existing one (HTTP 200). * * `isCreate` is not part of the wire format — the API returns a plain `Domain` * body. The SDK derives the flag from the HTTP status code so callers (notably * the CLI) can format different output for the create vs repoint paths without * a second round-trip. */ export interface DomainSetResult extends Domain { /** `true` when this call created a new domain; `false` when it updated an existing one. */ isCreate: boolean; } /** * Response for listing domains */ export interface DomainListResponse extends ListResponse { /** Array of domains */ domains: Domain[]; } /** * Acknowledgement of `DELETE /domains/:domain`. The row is gone, so there is * no state to state — the canonical domain name is the whole answer. See * {@link DeploymentDeleteResponse} for the law. */ export interface DomainDeleteResponse { /** The domain name that was removed, normalized */ readonly domain: string; } /** * Acknowledgement of `POST /domains/:domain/verify` (202). The DNS check is * queued, not performed — the accepted status code says so, and the domain's * own status is unchanged until the check runs, which is why none is stated * here. See {@link DeploymentDeleteResponse} for the law. */ export interface DomainVerifyResponse { /** The domain whose DNS verification was queued, normalized */ readonly domain: string; } /** * DNS record types supported for domain configuration */ export type DnsRecordType = 'A' | 'CNAME'; /** * DNS record required for domain configuration */ export interface DnsRecord { /** Record type (A for apex, CNAME for subdomains) */ type: DnsRecordType; /** The DNS name to configure */ name: string; /** The value to set (IP for A, hostname for CNAME) */ value: string; } /** * DNS provider information for a domain */ export interface DnsProvider { /** Provider name (e.g., "Cloudflare", "GoDaddy"), null if unknown */ name: string | null; } /** * Response for domain DNS provider lookup */ /** * What a DNS lookup found for a domain. An envelope rather than a bare * {@link DnsProvider} because a lookup can succeed and learn more than the * provider later; the shape is named so a consumer can hold one. */ export interface DnsLookup { /** The provider serving this domain's DNS, absent when unidentified */ provider?: DnsProvider; } /** * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface DomainDnsResponse { /** The domain name */ domain: string; /** DNS provider information, null if not yet looked up */ dns: DnsLookup | null; } /** * Response for `GET /domains/:domain/share` — the domain plus the salted * hash that lets someone else complete its DNS setup without an account. * * `/admin/domains/:domain/share` answers the same shape, which is the admin * law working: the operator surface is the public grammar with a prefix. * * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface DomainShareResponse { /** The domain the setup link is for */ readonly domain: string; /** The salted setup hash that authorizes the share */ readonly hash: string; } /** * Response for domain DNS records * * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface DomainRecordsResponse { /** The domain name */ domain: string; /** The apex (registered) domain where DNS records are managed */ apex: string; /** Required DNS records for configuration */ records: DnsRecord[]; } /** * The envelope an `Idempotency-Key` must fit, and how long a replay lasts. * * Format lives here rather than on the server alone by the format-vs-policy * rule: a client can decide offline whether a key is well-formed, and the * API would reject the same value the same way. */ export declare const IDEMPOTENCY_KEY_CONSTRAINTS: { /** * HTTP header name. Here for the same reason {@link CALLER.HEADER} is: a * wire header has two ends, and the package that owns the value's format * is the only place both ends can read its name from. */ readonly HEADER: "Idempotency-Key"; readonly MAX_LENGTH: 256; /** How long a stored 201 stays replayable. */ readonly WINDOW_SECONDS: number; }; /** * Normalize a `via` value from any transport — trimmed, lowercased, and a * member of {@link DeploymentVia}, or `undefined`. * * A format rule by this package's own test: a client can decide offline * whether a value is well-formed, and the API reaches the same verdict on the * same input. It lived server-side until 2026-08-06, which meant clients could * only learn their label was unusable by noticing analytics had gone quiet. * * **Not knowing your `via` is not an error** — an unrecognized value yields * `undefined` rather than throwing, because origin tracking is telemetry and a * deploy must never fail over it. A caller that has an honest default should * prefer it (`normalizeVia(process.env.SHIP_VIA) ?? DeploymentVia.CLI`): the * deploy really did come from the CLI, so recording that beats recording * nothing. */ export declare function normalizeVia(value: unknown): DeploymentViaType | undefined; /** * Validate an idempotency key, returning the trimmed value or `undefined` * when none was supplied. Throws {@link ShipError.validation} when the value * cannot be sent — the same verdict the API would reach, reached earlier. */ export declare function validateIdempotencyKey(value: unknown): string | undefined; /** * Response for `GET /labels` — every label in use across the caller's * deployments, domains and tokens, grouped and ordered by last use. * * The one plural noun outside the list contract, deliberately: labels have * no identity, no row and no `created`, so there is nothing for a keyset * cursor to resume after, and its consumer is an autocomplete that wants the * whole set. Bounded by `PAGINATION.GLOBAL_LIMIT` rather than paginated. * * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface LabelsResponse { readonly labels: string[]; } /** * Response for `POST /setup` — the DNS instructions for one domain, written * for a human to follow at their registrar. * * `custom` is the provider-specific walkthrough when the provider is known; * `generic` always answers, so a caller never has nothing to show. * * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface SetupInstructionsResponse { /** The domain the instructions are for — a report names its subject */ readonly domain: string; /** One-line summary of what to do */ readonly tldr: string; /** Provider-specific instructions, null when the provider is unknown */ readonly custom: string | null; /** Provider-agnostic instructions — always present */ readonly generic: string; /** The identified DNS provider, null when unknown */ readonly provider: string | null; } /** * `POST /domains/validate` — a report answering "is this name usable, and if * not, why". * * An unusable name is a legitimate ANSWER, not a failure, so this is a 200 and * the verdict rides the body. `reason` was named `error` until 2026-07-29, * which collided with {@link ErrorResponse}'s reserved key — there `error` is * an `ErrorType` a client branches on, here it is prose a client displays, and * one key cannot mean both. See {@link DeploymentDeleteResponse} for the law. */ export interface DomainValidateResponse { /** Whether the domain is valid */ valid: boolean; /** Normalized domain name, null when invalid */ normalized: string | null; /** Whether the domain is available, null when invalid */ available: boolean | null; /** Why the name is unusable, null when valid — displayed verbatim. */ reason: string | null; } /** * Core deploy token object - used in both API responses and SDK. * * The secret is never here: it is shown once at creation * ({@link TokenCreateResponse.secret}) and never again, so an entity read * carries only the management identifier and lifecycle metadata. */ export interface Token { /** 7-char management identifier (e.g., "a1b2c3d") */ readonly token: string; /** Labels for categorization and filtering. Always present, empty array when none. */ labels: string[]; /** Unix timestamp (seconds) when token was created */ readonly created: number; /** Unix timestamp (seconds) when token expires, null for never */ readonly expires: number | null; /** Unix timestamp (seconds) of the last request authenticated with this token, null if never used */ readonly used: number | null; } /** * Response for listing tokens */ export interface TokenListResponse extends ListResponse { /** Array of tokens (the secret is never among them) */ tokens: Token[]; } /** * Response from token creation. Extends Token with the one field that * exists only on creation — the same shape as * {@link DeploymentCreateResponse}, because a 201 returns the resource it * created plus whatever is knowable only once. */ export interface TokenCreateResponse extends Token { /** The raw credential value (shown once at creation, then never again) */ readonly secret: string; } /** * Acknowledgement of `DELETE /tokens/:token`. The credential is revoked and * its row is gone, so the management identifier is the whole answer. See * {@link DeploymentDeleteResponse} for the law. */ export interface TokenDeleteResponse { /** The 7-char management identifier that was revoked */ readonly token: string; } /** * Account plan constants */ export declare const AccountPlan: { readonly FREE: "free"; readonly STANDARD: "standard"; readonly SPONSORED: "sponsored"; readonly ENTERPRISE: "enterprise"; readonly SUSPENDED: "suspended"; readonly TERMINATING: "terminating"; readonly TERMINATED: "terminated"; }; export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan]; /** * Account usage metrics — always available regardless of billing provider. * * This is where a caller's own totals live. Lists answer pages and carry no * `total` (see {@link ListOptions}); a count is an aggregate over a * collection, so it belongs to the summary resource that owns the * collection. `GET /account` is that resource for one caller, `GET * /admin/stats` for the platform. * * The counted dimensions are the ones the plan caps — deployments and * domains (`PlatformLimits`) — plus the billable custom-domain subset, so a * surface can render "3 of 10" without a second request. */ export interface AccountUsage { /** Number of active custom domains (excludes paused) */ customDomains: number; /** * Deployments counted against the plan's deployment cap — every row * whatever its status, because that is what the cap counts, so a surface * renders "3 of 10" against the denominator the 403 divides by. (`GET * /deployments` lists successful ones only; that is a different question * asked of a different resource.) Optional by the additive-evolution law: * an API predating this field omits it. */ deployments?: number; /** * Domains counted against the plan's domain cap — every domain, platform * and custom alike, unlike `customDomains`. Optional for the same reason. */ domains?: number; } /** * Core account object - used in both API responses and SDK * All fields are readonly to prevent accidental mutations */ export interface Account { /** User email address */ readonly email: string; /** User display name, null if not set */ readonly name: string | null; /** User profile picture URL, null if not set */ readonly picture: string | null; /** Account plan status */ readonly plan: AccountPlanType; /** Account usage metrics (custom domains, etc.) */ readonly usage: AccountUsage; /** Unix timestamp (seconds) when account was created */ readonly created: number; /** Unix timestamp (seconds) when account was activated (first deployment), null if not yet activated */ readonly activated: number | null; /** Last 4 characters of the API key for identification, null when no key generated */ readonly hint: string | null; /** * Unix timestamp (seconds) of the API key's last use, null when never * used or no key generated. Optional on the type by the additive-evolution * law: published SDK versions may predate the field, so consumers read it * when present rather than forcing a lockstep SDK release. */ readonly used?: number | null; /** Grace period expiration (unix seconds), null if no grace period active */ readonly grace: number | null; } /** * Account as returned by `GET /account` — the entity plus how the request * was authorized, so `whoami` can answer "what credential am I holding?". * Request-scoped fields live on the response type, never on the entity * (the `DeploymentCreateResponse` pattern). */ export interface AccountGetResponse extends Account { /** How the request that produced this response was authorized. */ readonly authMethod: AuthMethodType; /** Present (and true) only when the caller is an operator acting as themselves. */ readonly isAdmin?: true; /** Present only during read-only admin impersonation: the operator's account id. */ readonly impersonatedBy?: string; } /** * Acknowledgement of `DELETE /account` (202). Termination is asynchronous — * a cleanup consumer finishes the job — so the account survives long enough * to state the plan it is transitioning through. `plan` is the account's * state field, the way `status` is a deployment's. See * {@link DeploymentDeleteResponse} for the law. */ export interface AccountDeleteResponse { /** The account that was marked for termination */ readonly account: string; /** The plan the account is in while cleanup runs */ readonly plan: AccountPlanType; } /** * Response from `PUT /account/key` — the account's single API key, minted in * place of whatever was there before. * * There is no entity to return: only the key's last-4 `hint` is durable * (`Account.hint`), and the plaintext exists exactly once, in this response. * The raw credential is `secret` on every surface that mints one — the same * field `TokenCreateResponse` carries — because one concept gets one name. * * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface AccountKeyResponse { /** The raw API key (shown once at mint, then never again) */ readonly secret: string; } /** * Account-specific configuration overrides * Allows per-account customization of limits without changing plan */ export interface AccountOverrides { /** Override for maximum number of domains */ domains?: number; /** Override for maximum number of deployments */ deployments?: number; /** Override for maximum individual file size in bytes */ fileSize?: number; /** Override for maximum number of files per deployment */ filesCount?: number; /** Override for maximum total deployment size in bytes */ totalSize?: number; } /** * Every path the public API answers on, declared once. * * The URL surface was written out in four places — the API's mounts, the * SDK's client, the dashboard's client, and the post-deploy smoke — so a * rename meant finding all four. The first three now read this table. * * The smoke (`cloudflare/api/smoke.mjs`) deliberately still spells its own: * five of its nine paths are `/admin/*`, which this table excludes by * design, and splitting one list between a registry and literals reads worse * than keeping it uniform. * * **What this guarantees, exactly.** Collection paths are mounted from here, * so producer and consumer cannot diverge. Item paths are declared here and * consumed by clients, but the API spells them relative to their mount * (`/:deployment/config`), so the table does not *generate* them — it is * held to them by `api/tests/architecture/api-paths.test.ts`, which fails if * any entry names a path no route answers. Some entries have no client yet * (`DEPLOYMENT_CONFIG`, `DOMAIN_PROPAGATION` — endpoints the SDK * deliberately does not reach); the fence is what keeps those honest rather * than merely asserted. * * **The operator surface is deliberately absent.** `/admin/*` paths belong * to `web/my`, for the same reason its row types do: this package is * published, and the operator surface is not public (see `CLAUDE.md`, "Admin * types"). A path here is a promise to every npm consumer; `/admin` is a * promise to one dashboard. * * Item paths are functions rather than templates so the key is interpolated * in one place, encoded the same way by every caller. */ export declare const API_PATHS: { readonly DEPLOYMENTS: "/deployments"; readonly DEPLOYMENT: (deployment: string) => string; readonly DEPLOYMENT_CONFIG: (deployment: string) => string; readonly DOMAINS: "/domains"; readonly DOMAIN: (domain: string) => string; readonly DOMAIN_VERIFY: (domain: string) => string; readonly DOMAIN_DNS: (domain: string) => string; readonly DOMAIN_RECORDS: (domain: string) => string; readonly DOMAIN_SHARE: (domain: string) => string; readonly DOMAIN_PROPAGATION: (domain: string) => string; readonly DOMAINS_VALIDATE: "/domains/validate"; readonly TOKENS: "/tokens"; readonly TOKEN: (token: string) => string; readonly ACCOUNT: "/account"; readonly ACCOUNT_KEY: "/account/key"; readonly ACCOUNT_CLAIM: "/account/claim"; readonly ACTIVITIES: "/activities"; readonly LABELS: "/labels"; readonly LIMITS: "/limits"; readonly PING: "/ping"; readonly SETUP: "/setup"; readonly SPA_CHECK: "/spa-check"; readonly UPLOAD: "/upload"; }; /** * The deploy request's multipart field names — the other half of the wire * surface beside {@link API_PATHS}. `POST /deployments` (and the first-party * `/upload`) is multipart/form-data, and these are the names the API reads. * * Declared once because the body has three independent WRITERS — the SDK's * Node and browser body builders, and the n8n community node's hand-rolled * client (which cannot import this under n8n Cloud's zero-dependency rule, * and fences its restated copy instead) — and until this export every writer * restated the strings the API parses, with nothing comparing them. * * `FILES` carries one entry per file (the API reads it with `getAll`); every * other field is single. The `@internal` flags are serialized as the literal * string `'true'` and belong to first-party surfaces only. */ export declare const DEPLOY_FIELDS: { /** One entry per file — read with `getAll`. */ readonly FILES: "files[]"; /** JSON array of MD5 hex digests, index-aligned with `FILES`. */ readonly CHECKSUMS: "checksums"; /** JSON array of label strings. */ readonly LABELS: "labels"; /** The deploying surface's {@link DeploymentVia} member. */ readonly VIA: "via"; /** Plaintext password — the API hashes it server-side. */ readonly PASSWORD: "password"; /** * Requested lifetime in SECONDS — a duration, never an instant. The API * computes and stores the expiry, so the wire carries no client clock. * See {@link validateTtl}. */ readonly TTL: "ttl"; /** @internal Server-processing flag — first-party `/upload` only. */ readonly BUILD: "build"; /** @internal Server-processing flag — first-party `/upload` only. */ readonly PRERENDER: "prerender"; /** @internal Server-processing flag — first-party `/upload` only. */ readonly SPA: "spa"; /** @internal reCAPTCHA proof — `web/www`'s public uploader only. */ readonly CAPTCHA: "captcha"; }; /** * All possible error types in the ShipStatic platform. * * Developer-friendly key names map to stable wire-format string values. * Both the value and the type are exported under the same name so callers * can use `ErrorType.Validation` (value comparison) and `: ErrorType` (type * annotation) without ceremony — matching the pattern other status objects * (`DeploymentStatus`, `DomainStatus`, `AccountPlan`, `AuthMethod`) follow. */ export declare const ErrorType: { /** * Validation failed. Input shape is wrong. * * Carries 400 when an API judged it — including a client-side pre-check of a * rule the server enforces too, which keeps the error identical wherever it * was caught. **Statusless** when a client rejects something no API judges, * such as a CLI's own command grammar: `status` is documented "(API * contexts)" on `ErrorResponse`, so there is none to report. */ readonly Validation: "validation_failed"; /** Resource not found (404). */ readonly NotFound: "not_found"; /** Authenticated but not allowed (403). User lacks permission for this action. */ readonly Forbidden: "forbidden"; /** Rate limit exceeded (429). */ readonly RateLimit: "rate_limit_exceeded"; /** Authentication required or failed (401). Missing/invalid credentials. */ readonly Authentication: "authentication_failed"; /** Business rule violation. Catch-all for 4xx state-rule errors that aren't more specific. */ readonly Business: "business_logic_error"; /** API server error (500). Generic server-side fault. */ readonly Api: "internal_server_error"; /** * The platform is closed for maintenance (503). A deliberate operator * state, not a fault — nothing errored; the API is refusing work on * purpose, and deployed sites keep serving throughout. * * Distinct from `Api` at 503, which the platform already uses for a * dependency that failed (moderation unavailable). A consumer has to tell * "we closed the door" from "something broke": the two get opposite words * and opposite retry behaviour. */ readonly Maintenance: "maintenance"; /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */ readonly Network: "network_error"; /** * A deadline expired before the exchange completed. Client-side only — set * by HTTP clients when a timeout signal fires; never produced server-side. * * A member of the NETWORK category rather than a sibling of it: * `isNetworkError()` answers "nothing was exchanged", which is true of a * deadline exactly as it is of a refused connection, so every consumer that * retries, declines to report, or declines to relay a wire message on that * category is already right about a timeout. The distinct TYPE exists for * the one decision the category cannot make — what to SAY. "Check your * internet connection" is the wrong sentence for a five-minute deploy * ceiling, and a surface can only tell the two apart by type. * * The same relationship every comparable SDK ships: * `APIConnectionTimeoutError extends APIConnectionError`. */ readonly Timeout: "timeout_error"; /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */ readonly Cancelled: "operation_cancelled"; /** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */ readonly File: "file_error"; /** Configuration error. Client-side only — set by SDK during config parsing/validation; never produced server-side. */ readonly Config: "config_error"; }; export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType]; /** * Standard error response format used everywhere */ export interface ErrorResponse { /** Error type identifier */ error: ErrorType; /** Human-readable error message */ message: string; /** HTTP status code (API contexts) */ status?: number; /** Optional additional error details. Untyped by design — narrow at the read site. */ details?: unknown; } /** * Simple unified error class for both API and SDK */ export declare class ShipError extends Error { readonly type: ErrorType; readonly status?: number | undefined; readonly details?: unknown | undefined; constructor(type: ErrorType, message: string, status?: number | undefined, details?: unknown | undefined); /** Convert to wire format */ toResponse(): ErrorResponse; /** * Construct a `ShipError` from an HTTP error response. * * Best-effort body parse for `{ message, error?, details? }`. Message * resolution: `body.message` → `body.error` → `" failed with * status "`. * * Type resolution: trusts `body.error` when it's a known server-producible * `ErrorType` (preserves the wire's intent — server's * `ShipError.validation(...)` round-trips back to `ErrorType.Validation` * on the client). Falls back to status-derived (401 → Authentication, * 403 → Forbidden, 429 → RateLimit, else → Api) for non-API responses * (CDN errors, intermediaries) or malformed bodies. Client-only types * (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the * trusted set — a misbehaving server claiming one of those is ignored. * * `operationName` (e.g. `"Get account"`) is used to compose the fallback * message. Defaults to `"Request"`. Same convention as `fromFetchError`. * * Async because it reads the response body. Returns rather than throws so * callers can compose; most will `throw await ShipError.fromHttpResponse(...)`. */ static fromHttpResponse(response: Response, operationName?: string): Promise; /** * Construct a `ShipError` from an error caught around a `fetch()` call. * * The mirror of `fromHttpResponse` for the *other* side of the HTTP error * story — the network layer failing (offline, CORS, abort) rather than the * server returning a non-OK response. * * Routing: * - Already a `ShipError` → returned as-is (caller's intent preserved) * - `AbortError` → `ShipError.cancelled(...)` — someone stopped it on purpose * - `TimeoutError` → `ShipError.timeout(...)` — a deadline expired; the * message names the timeout, and the type is in the network CATEGORY * because nothing was exchanged * - A transport failure → `ShipError.network(...)` — see `isTransportFailure` * for what each runtime offers as evidence * - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server) * - Anything else (string, undefined, etc.) → `ShipError(Api, ...)` * * **Abort and timeout are read from `name` BEFORE any `instanceof Error` * gate.** A `DOMException` satisfies that gate in every runtime measured * (Node, Bun, Chromium, Firefox, WebKit, workerd — all six), but the * inheritance is a comparatively recent spec change and this classification * has no reason to depend on it: `name` is where the meaning lives, and * reading it first costs nothing. The suite plants a non-`Error` * `DOMException` shape to hold the arm, since no runtime on the table * produces one. * * A caller's own `AbortSignal.timeout()` is the reachable source of * `TimeoutError` — and the two are NOT interchangeable per runtime: WebKit * reports a fired `AbortSignal.timeout()` as `AbortError`, so on Safari a * deadline is indistinguishable from a cancellation and lands on * `Cancelled`. Recorded rather than worked around; `Cancelled` is honest * there, since the caller's signal is what stopped it. * * The optional `operationName` is composed into the message for context: * `"Get account was cancelled"`, `"Get account failed: ..."`. Defaults to * `"Request"` when omitted. */ static fromFetchError(cause: unknown, operationName?: string): ShipError; static validation(message: string, details?: unknown): ShipError; static notFound(resource: string, id?: string): ShipError; static forbidden(message: string, details?: unknown): ShipError; static rateLimit(message?: string, details?: unknown): ShipError; /** * Construct an Authentication (401) error. * * **Telemetry pattern — `details: { internal: '' }`.** When the * server creates an auth error with an `internal` key in `details` * (e.g. `{ internal: 'session_invalid' }`), `toResponse()` strips the * entire `details` object before serialization. This keeps the wire * response a clean "Authentication failed" while preserving granular * server-side telemetry (which strategy/check failed) for logs and tests. * * Use this pattern in API auth code; do not put client-visible info under * `internal`. Other `details` keys round-trip normally. */ static authentication(message?: string, details?: unknown): ShipError; static business(message: string, status?: number, details?: unknown): ShipError; static network(message: string, details?: unknown): ShipError; /** * A deadline expired before the exchange completed. * * Statusless like its four client-only siblings: no exchange completed, so * there is no HTTP status to report. `isNetworkError()` is true — see * `ErrorType.Timeout` for why the category is shared and the type is not. */ static timeout(message: string, details?: unknown): ShipError; static cancelled(message: string, details?: unknown): ShipError; static file(message: string, details?: unknown): ShipError; static config(message: string, details?: unknown): ShipError; static api(message: string, status?: number, details?: unknown): ShipError; /** * The platform is closed for maintenance (503). * * `message` is REQUIRED and has no default here. The API is the only * producer of that sentence, and a default in this file would be a second * owner of one fact — see CLAUDE.md, "The Constellation Law" (stopping * rule). It is also the one factory whose status is fixed rather than * defaulted: a maintenance refusal is 503 or it is not this error. */ static maintenance(message: string, details?: unknown): ShipError; /** * The caller is at fault — by HTTP's own definition of a 4xx, or by a type * that is client-attributable without ever having a status (`Config`, * `File`, raised locally by the SDK). * * Both arms are load-bearing, because type and status are independent * axes. `fromHttpResponse` trusts `body.error` only when it names a * server-producible type; a non-OK response without one is status-derived, * so a CDN 404 or any intermediary error arrives as `Api` — a server-fault * *type* carrying a client *status*. Judging by type alone would report it * as a platform failure and bury the server's own message. */ isClientError(): boolean; isNetworkError(): boolean; isAuthError(): boolean; isType(errorType: ErrorType): boolean; } /** * Type guard to check if an unknown value is a ShipError. * * Uses structural checking instead of instanceof to handle module duplication * in bundled applications where multiple copies of the ShipError class may exist. * * @example * if (isShipError(error)) { * console.log(error.status, error.message); * } */ export declare function isShipError(error: unknown): error is ShipError; /** * What the platform will refuse, returned by the `/limits` endpoint. * * The SDK fetches this once on first API call to drive client-side validation * that mirrors what the API would enforce server-side. The caps vary by * account plan; the blocklist does not. * * These are the *platform's* posted rules for the current account — server * truth delivered at runtime, never hard-coded on the client. That is the * whole point of the shape: a rule the server owns and may change reaches the * client as data, so a pinned client cannot enforce a policy the platform has * moved on from (`npm/types/CLAUDE.md`, "Validation: format vs policy"). * * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface PlatformLimits { /** Maximum size in bytes for a single file. */ maxFileSize: number; /** Maximum number of files in a single deployment. */ maxFilesCount: number; /** Maximum total size in bytes across all files in a deployment. */ maxTotalSize: number; /** * Lowercase extensions, without the dot, that the platform refuses to host * (`exe`, `dmg`, …). Owned and evolved by the API — see * `cloudflare/api/src/lib/blocklist.ts`. * * **Optional, and the absence is load-bearing.** An API deployed before this * field existed sends nothing, so a client MUST read absence as "no * client-side check" rather than as an empty policy. The hint fails open, * the boundary fails closed: the server refuses the file either way, and a * client that guessed would only ever be wrong in the direction that refuses * a file the platform accepts. * * The optionality follows the additive-evolution law and retires with its * reason: once every environment serves the field, it hardens to required at * the entity's next natural break, and the clients' fail-open spellings * retire with it (tracked in root `backlog.md`). */ readonly blockedExtensions?: readonly string[]; } /** * Whether a file is one the platform refuses to host. * * **The list is not this package's, and that separation is the point.** What * counts as a blocked extension is hosting POLICY — it evolves, it is enforced * at one security boundary, and `virus.exe` is a perfectly well-formed * filename that breaks nothing about the upload→serve round-trip. So the API * owns the list (`cloudflare/api/src/lib/blocklist.ts`) and delivers it as * `PlatformLimits.blockedExtensions`; a client passes what it was given. * * What lives here is the MATCHING RULE, and it earns its place by the * constellation law's own test. The list's drift is loud in both directions — * a stale client uploads a file the API refuses by name, on the first try. * A second *matcher* drifts SILENTLY in the one direction that matters: a * client stricter than the server refuses a legal file without the server ever * being asked, and no error names it. Two holders, silent drift, one owner. * * The `blocked` collection is required rather than defaulted: this predicate * guards a security boundary in the API, and a defaulted-empty argument there * would block nothing while reading as though it did. Callers holding a * possibly-absent wire field spell the fail-open themselves. * * @example * isBlockedExtension('virus.exe', ['exe']) // true * isBlockedExtension('virus.EXE', ['exe']) // true — case-insensitive * isBlockedExtension('style.css', ['exe']) // false * isBlockedExtension('README', ['exe']) // false — no extension */ export declare function isBlockedExtension(filename: string, blocked: ReadonlySet | readonly string[]): boolean; /** * The `accept` attribute value for a browser file picker offering web files. * * **This is a hint, never a rule.** The API's blocklist is the platform's gate * and the only thing that decides what may be hosted; this constant decides * what a *file dialog* shows first. The two are not two halves of one policy, * and this one must never be consulted to accept or reject a file. * * The distinction is structural, not stylistic. `accept` can express only an * allowlist, while the platform's rule is a blocklist — so this list is * necessarily *narrower* than what the platform hosts, and reading it as * authority would reject files the platform serves happily. It is also not * enforcement in the browser's own terms: every file dialog offers an * all-files escape, and **drag-and-drop ignores `accept` entirely**. The * dropzone and the picker must reach the same verdict on the same files, and * they do — because the verdict is `validateFiles`, downstream of both. * * The invariant that matters — the picker must never offer a file the platform * will refuse — is fenced where the authority lives, in the API's own suite * (`cloudflare/api/tests/lib/blocklist.test.ts`), which reads this published * string and holds it against the list it owns. It sat here until the * blocklist became the API's, and moving it was the price of that: a fence * belongs with whichever side can change and break it. */ export declare const WEB_FILE_ACCEPT: string; /** * Characters that are unsafe in filenames for static hosting. * * Blocks only characters that genuinely break the upload→serve round-trip: * - # ? % URL round-trip breakers (fragment, query, encoding ambiguity) * - \ Path separator confusion (upload splits on backslash) * - < > " XSS vectors with zero legitimate use in filenames * - \x00-\x1f \x7f Control characters (header injection, display corruption) * * Everything else is allowed — browser percent-encodes, Worker decodes, R2 matches. */ export declare const UNSAFE_FILENAME_CHARS: RegExp; /** * Check if a filename contains unsafe characters. * * @example * hasUnsafeChars('saved_resource(1).html') // false — parentheses are safe * hasUnsafeChars('page[slug].js') // false — brackets are safe * hasUnsafeChars('file#anchor.html') // true — # breaks URL resolution * hasUnsafeChars('file.html') // true — < is an XSS vector */ export declare function hasUnsafeChars(filename: string): boolean; /** * Path segment names that indicate an unbuilt project was uploaded instead of build output. * Used for early detection in CLI, browser, and server validation. */ export declare const UNBUILT_PROJECT_MARKERS: ReadonlySet; /** * Check if a file path contains an unbuilt project marker. * * @example * hasUnbuiltMarker('node_modules/react/index.js') // true * hasUnbuiltMarker('package.json') // true * hasUnbuiltMarker('dist/index.html') // false */ export declare function hasUnbuiltMarker(filePath: string): boolean; /** * `GET /ping` — a report of the server clock. * * Liveness is the STATUS CODE's answer, not a field's: a 200 means reachable, * and any other outcome throws before a body is read. So the body carries the * one thing a status code cannot — the server's own clock, which is what lets a * client detect skew against a token expiry. It read `{ success: true, * timestamp? }` until 2026-07-29, where `success` was a literal constant in the * route (zero bits, and the platform's own named anti-pattern) while the field * that IS the payload was optional. See {@link DeploymentDeleteResponse} for * the law, and `tests/response-shapes.test.ts` for the fence that holds it. */ export interface PingResponse { /** Server time in unix seconds — the one wire unit for timestamps. */ readonly timestamp: number; } /** * Where human identity is mounted on the API host. The API mounts Better * Auth at this path (sign-in, sign-out, session reads, admin impersonation) * and the web console's auth client posts to it — shared here so the two * halves of the auth pair agree by construction, the same way both sides * already share the credential prefixes below. */ export declare const AUTH_BASE_PATH = "/auth"; /** * The query marker a completed sign-in LANDS with. * * The API's magic-link verify leg stamps `?signing-in=1` onto its success * redirect, and the console boots into its wait screen on seeing it — two * repos, one spelling, which is why it lives here. Success is marked and the * error leg deliberately is NOT: the console gives the marker precedence, so * a marked error would render a wait that resolves to bare doors with the * error's sentence lost. If the spellings ever diverged the failure would be * invisible to every suite — email landings would flash the doors for one * round trip instead of waiting — which is exactly the silent-drift class * this constitution exists to delete. */ export declare const SIGN_IN_RETURN_PARAM = "signing-in"; /** * How a request (or recorded activity) was authorized. * * Client populations: `SESSION` (first-party cookie), `API_KEY` (`ship-` * key), `TOKEN` (`deploy-` deploy token), `AGENT` (anonymous public deploy — * no credential; the platform grants the public-account identity per * request), `OAUTH` (delegated access token). Server populations: `WEBHOOK` * (signed webhook processing), `SYSTEM` (scheduled/background jobs). */ export declare const AuthMethod: { readonly SESSION: "session"; readonly API_KEY: "apiKey"; readonly TOKEN: "token"; readonly AGENT: "agent"; readonly OAUTH: "oauth"; readonly WEBHOOK: "webhook"; readonly SYSTEM: "system"; }; export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod]; /** * Shape constants for API keys (`ship-{32 hex chars}`). * Single source of truth used by validation utilities and auth middleware. */ export declare const API_KEY: { /** Prefix that identifies an API key. */ readonly PREFIX: "ship-"; /** Number of hex characters following the prefix. */ readonly HEX_LENGTH: 32; /** Total length of an API key including prefix (`PREFIX.length + HEX_LENGTH = 37`). */ readonly TOTAL_LENGTH: 37; /** Number of trailing characters used to display a redacted hint (e.g. last 4). */ readonly HINT_LENGTH: 4; }; /** * Shape constants for deploy tokens (`deploy-{32 hex chars}`). * Single source of truth used by validation utilities and auth middleware. * * Deliberately the same width as `API_KEY`: both are minted by one generator * and classified by prefix alone, so a length that differed between them * would be a second thing to know about a credential whose prefix already * says what it is. */ export declare const DEPLOY_TOKEN: { /** Prefix that identifies a deploy token. */ readonly PREFIX: "deploy-"; /** Number of hex characters following the prefix. */ readonly HEX_LENGTH: 32; /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 39`). */ readonly TOTAL_LENGTH: 39; }; /** * Shape constants for OAuth access tokens (`oauth-{32 hex chars}`) — the * delegated population, minted by the platform's own authorization server * for a connected app acting on a user's behalf. * * Same width as the other two, and for the same reason: one entropy standard * across the platform, so "how long is a credential" has one answer. * * **This population is the access token alone.** Refresh tokens, authorization * codes and client secrets are deliberately NOT here and are deliberately not * prefixed by this constant: none of them ever enters the `Authorization: * Bearer` slot — a refresh token is posted as a form field to the token * endpoint, which knows what it is receiving — so `classifyToken` never sees * one and a prefix would name a population no dispatcher dispatches. The same * reasoning that keeps the deployment claim code bare. * * **The prefix must be applied at the MINT, never as a display wrapper.** The * authorization server hashes what it stores and the API hashes what it is * presented, so the prefix has to be inside the hashed string on both sides. * `@better-auth/oauth-provider` offers a `prefix.opaqueAccessToken` option * that prepends AFTER hashing and strips on its own read paths; using it would * store a hash of the UNPREFIXED token and silently break the platform's read * arm. The API therefore mints through `generateOpaqueAccessToken` — recorded * beside the config in `cloudflare/api/src/lib/auth/instance.ts`. */ export declare const OAUTH_TOKEN: { /** Prefix that identifies an OAuth access token. */ readonly PREFIX: "oauth-"; /** Number of hex characters following the prefix. */ readonly HEX_LENGTH: 32; /** Total length including prefix (`PREFIX.length + HEX_LENGTH = 38`). */ readonly TOTAL_LENGTH: 38; }; /** * Shape constants for caller identifiers (the `X-Caller` instance-identity * header — rate-limit bucketing for multi-tenant orchestrators). The API * normalizes case and silently ignores malformed values (the header is * unauthenticated); clients validate at the boundary via `validateCaller`, * so a value the server would drop fails fast instead. */ export declare const CALLER: { /** HTTP header name. */ readonly HEADER: "X-Caller"; /** Maximum identifier length. */ readonly MAX_LENGTH: 128; /** Allowed characters: alphanumeric, dot, underscore, hyphen. */ readonly PATTERN: RegExp; }; /** * Token populations distinguishable by shape. The platform carries every * client token in one wire slot (`Authorization: Bearer `) and * classifies by value, never by a side channel — this is the classifier. * * `API_KEY`, `DEPLOY_TOKEN` and `OAUTH` *are* `AuthMethod.API_KEY`, * `AuthMethod.TOKEN` and `AuthMethod.OAUTH` — the equality is structural, so * a classification flows straight into an auth method and the trio can never * drift. * * `OPAQUE` is any other value, and since 2026-08-14 it names NO population: * every credential this platform mints for the Bearer slot carries a prefix, * so an opaque bearer is a bearer we did not mint. It stays a member rather * than becoming a `null` return because a dispatcher with a total codomain * reads better than one with an absence in it — and because it is where a * future population would land before anyone gave it a shape, which is * exactly what the OAuth token itself did until its prefix existed. */ export declare const TokenKind: { readonly API_KEY: "apiKey"; readonly DEPLOY_TOKEN: "token"; readonly OAUTH: "oauth"; readonly OPAQUE: "opaque"; }; export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind]; /** * Classify a client token by shape. The single dispatch used by both sides * of the wire: API auth middleware (which population is this credential?) * and SDK validation (which format rules apply before sending?). Sharing it * is what guarantees client and server can never disagree on dispatch. */ export declare function classifyToken(token: string): TokenKindType; /** * Read the credential out of an `Authorization` header value — the step * BEFORE `classifyToken`, and the other half of the one wire slot this * section owns. * * Returns the credential's own bytes, or `null` when the header carries a * foreign scheme or nothing after the scheme. * * **The scheme is folded; the credential is not.** RFC 7235 §2.1 makes the * auth-scheme case-insensitive, so `bearer`, `Bearer` and `BEARER` are the * same header. The value after it is opaque and is compared literally * everywhere it is used — `ship-`/`deploy-`/`oauth-` are lowercase hex, and * folding them would make a credential match values it is not. * * **This platform has paid for the rule twice, which is why it has an owner * rather than a convention.** A spec-conformant `bearer ship-…` client was * refused for as long as the API's scheme test was spelled case-sensitively; * and `@better-auth/oauth-provider` carries the same defect in four places * today (`startsWith("Bearer ")`), which is precisely why the platform folds * the scheme itself and hands the provider a bare token. * * **ABSENCE is deliberately not this function's business.** A missing header * and an unreadable one are different facts, and the callers that care split * on them: the API worker's middleware distinguishes `absent` (the only * anonymous path) from `unreadable` (a presented credential that is refused), * and collapsing the two here would take that distinction away from the layer * that needs it. Callers check for the header themselves and pass its value. * * **Why this lives in the constitution rather than in a worker's `shared/`.** * It is the same wire boundary `classifyToken` already owns — one reads the * slot, the other dispatches on what came out — and a rule with two holders * whose drift is silent earns exactly one owner regardless of what the * convoy costs. The estate's recorded refusal to own a `Bearer` CONSTANT * stands and is a different thing: that is RFC vocabulary, the same reason * this package owns no `"POST"`. A parser is not a spelling. */ export declare function readBearerValue(header: string): string | null; /** * OAuth scope vocabulary for delegated third-party access tokens. * Single source of truth used by the authorization server (advertised in * `scopes_supported`), the API's scope-enforcement middleware, and consent UI * copy. The standard `offline_access` scope (refresh tokens) is not platform * vocabulary and is deliberately absent — the middleware never checks it. * * Deliberately absent by design: any `tokens:*` scope, `account:write`, or * admin scope — a delegated app must never mint credentials, delete the * account, or act as admin. */ export declare const OAuthScope: { readonly ACCOUNT_READ: "account:read"; readonly DEPLOYMENTS_READ: "deployments:read"; readonly DEPLOYMENTS_WRITE: "deployments:write"; readonly DOMAINS_READ: "domains:read"; readonly DOMAINS_WRITE: "domains:write"; }; export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope]; export declare const DEPLOYMENT_CONFIG_FILENAME = "ship.json"; /** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */ export declare const SPA_DEFAULT_CONFIG: { readonly rewrites: readonly [{ readonly source: "/(.*)"; readonly destination: "/index.html"; }]; }; /** * The `/spa-check` pre-flight's client-side envelope: which file is the * check's subject, and how large it may be before a client skips the call. * * One fact with three holders until this export — the API's config declared * the cap, the SDK's `checkSPA` hardcoded `100 * 1024`, and prose restated * "100KB". `INDEX_FILE` is the selection rule (the file whose content rides * `SPACheckRequest.index`), restated by every client that builds the request. * * Neither member is a validation boundary: a client over the cap simply * skips the pre-flight, because the server answers an oversized index * `isSPA: false` anyway. A consumer that cannot import this (n8n) needs no * size copy at all — outcome parity is the server's, not the client's. */ export declare const SPA_CHECK_CONSTRAINTS: { /** The file whose content is the check's subject. */ readonly INDEX_FILE: "index.html"; /** Skip the pre-flight above this size — the server would answer false. */ readonly MAX_INDEX_BYTES: number; }; /** * Assert that a ship.json file is *syntactically* loadable. Syntax only — * never schema. * * ship.json is validated and compiled on the server, deliberately: the schema * and the compiler evolve, and a client that judged them would reject configs * a newer platform accepts. That reasoning bounds what a client may check to * the properties which are true of *every* past and future schema: * * 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that * does not parse can never be a valid config; * 2. its top level is an object — ship.json is `{ ... }` in every version. * * Both are monotonic: neither can ever reject something the server would * accept. Everything beyond them (field names, types, rule semantics, which * keys are permitted) stays server-side, where it can change. * * The payoff is the common case. Hand-edited JSON fails on a trailing comma, * a `//` comment, single quotes, unquoted keys, or smart quotes pasted from * documentation — mistakes that otherwise cost a full upload round-trip to * discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped * before parsing rather than rejected, because the server accepts it too; * diverging there would reintroduce exactly the false rejection this * function exists to avoid. * * @throws {ShipError} `ErrorType.Config` — the same type the server's own * config rejection carries, so the error contract is identical wherever the * failure is detected. */ export declare function assertShipJsonSyntax(text: string): void; /** * Validate API key format */ export declare function validateApiKey(apiKey: string): void; /** * Validate deploy token format */ export declare function validateDeployToken(deployToken: string): void; /** * Validate OAuth access token format */ export declare function validateOAuthToken(oauthToken: string): void; /** * Validate a client token of any population. Classifies by shape and applies * the matching format rules: all three prefixed populations are validated * strictly; an OPAQUE token only needs to be non-empty. * * **The OPAQUE arm stays permissive on purpose**, even though the platform no * longer mints an unprefixed credential. It is the fallback for a population * that does not exist yet, and a client refusing a shape the server would * accept is the one failure mode this boundary must never have — the server * decides, and it refuses an unrecognised bearer anyway. Unprefixed OAuth * tokens from before 2026-08-14 land here and are refused server-side, which * is correct: they were revoked by the change, not grandfathered. */ export declare function validateToken(token: string): void; /** * Validate a caller identifier against the `CALLER` shape. The server * silently ignores malformed values (the header is unauthenticated); clients * call this at configuration time so the drop never silently happens. */ export declare function validateCaller(caller: string): void; /** * Validate API URL format */ export declare function validateApiUrl(apiUrl: string): void; /** * Check if a string matches the deployment identifier pattern (word-word-alphanumeric7). * Example: "happy-cat-abc1234.shipstatic.com" */ export declare function isDeployment(input: string): boolean; /** * The envelope a requested lifetime must fit — one word, one grammar, wherever * the platform lets a caller choose how long something lives. * * Two resources wear it: `TokenCreateOptions.ttl` and * `DeploymentUploadOptions.ttl`. It lives here rather than on the server by * the format-vs-policy rule — a client can decide offline whether a duration * is well-formed, and the API rejects the same value the same way. What is * NOT here is any per-plan ceiling: no such policy exists, and one delivered * speculatively through `/limits` would be an owner for a decision nobody has * made. */ export declare const TTL_CONSTRAINTS: { /** * Shortest requestable lifetime, in seconds. One rather than zero: a * deployment that expires the instant it is created is not a shorter lease, * it is a deploy that was never live, and `0` is how an unset variable * arrives. */ readonly MIN_SECONDS: 1; /** Longest requestable lifetime, in seconds — one year. */ readonly MAX_SECONDS: number; }; /** * Validate a requested lifetime in SECONDS and return it, or `undefined` when * none was asked for. * * **A duration, never an instant.** The caller says how long; the server owns * what time it is and stamps the expiry — so a client's clock, however wrong, * cannot shorten or extend a lease. That is the tokens precedent, and it is * why this rule measures a count of seconds rather than checking a timestamp * against `now`. * * Fractions are refused rather than rounded: a caller who wrote `1.5` meant * something the wire cannot carry, and silently choosing `1` or `2` for them * is a decision the platform has no standing to make. * * Single source of truth shared by the API (the tokens route and the deploy * schema), the SDK's request boundary, and the CLI's parser. */ export declare function validateTtl(value: unknown): number | undefined; /** * Request payload for SPA check endpoint */ export interface SPACheckRequest { /** Array of file paths */ files: string[]; /** HTML content of index.html file */ index: string; } /** * Response from SPA check endpoint */ /** * Which of the classifier's tiers reached the verdict, and why. Named rather * than inline so the API's own `checkSPA` can return `SPACheckResponse` * instead of restating its shape. */ export interface SPACheckDebug { /** Which tier made the detection */ tier: 'exclusions' | 'inclusions' | 'scoring' | 'ai' | 'fallback'; /** The reason for the detection result */ reason: string; } /** * A report: it answers a question and carries only the answer (`CLAUDE.md`, * "A report answers a question"). */ export interface SPACheckResponse { /** Whether the project is detected as a Single Page Application */ isSPA: boolean; /** Debugging information about detection */ debug: SPACheckDebug; } /** * Represents a file that has been processed and is ready for deploy. * Used across the platform (API, SDK, CLI) for file operations. */ export interface StaticFile { /** * The content of the file. * In Node.js, this is typically a `Buffer`. * In the browser, this is typically a `File` or `Blob` object. */ content: File | Buffer | Blob; /** * The desired path for the file on the server, relative to the deployment root. * Should include the filename, e.g., `images/photo.jpg`. */ path: string; /** * The original absolute file system path (primarily used in Node.js environments). * This helps in debugging or associating the server path back to its source. */ filePath?: string; /** * The MD5 hash (checksum) of the file's content. * This is calculated by the SDK before deploy if not provided. */ md5?: string; /** The size of the file in bytes. */ size: number; } /** Default API URL if not otherwise configured. */ export declare const DEFAULT_API = "https://api.shipstatic.com"; /** * The Node SDK's ambient configuration pair — the ONLY environment variables * the SDK reads, and therefore the COMPLETE list an embedding host must * scrub (per `npm/ship`'s strict-isolation contract, scrubbing is the host's * job, not the SDK's). A host that derives its scrub from this object's * values — as the VS Code extension's child-process env block does — picks * up a grown contract at the next pin bump instead of by remembered prose. * * Browser builds read no environment at all, and the CLI-only variables * (`SHIP_PASSWORD`, `SHIP_VIA`) are deliberately NOT here: they are the * CLI's operational levers, not the SDK's ambient contract — see * `npm/ship/CLAUDE.md`, "CLI-only env vars". */ export declare const SHIP_ENV: { /** The one credential slot — any platform token. */ readonly TOKEN: "SHIP_TOKEN"; /** The API endpoint override. */ readonly API_URL: "SHIP_API_URL"; }; /** * Where a human creates an API key — the console deep link quoted by every * surface that teaches authentication (the CLI's config wizard, the VS Code * and n8n listings, the n8n rate-limit hint and credential copy). Written * out in five files across three repos until this export. * * Production-branded by design: published artifacts name the product, never * an environment (root `CLAUDE.md`, "Environment-Aware URLs"). */ export declare const MY_API_KEY_URL = "https://my.shipstatic.com/api-key"; /** * How long an anonymous deployment lives before it expires. * * The lifetime of the public tier, and one fact with several readers. The API * stamps a deployment's `expires` from it and gives a claim code exactly the * same window — a live site with a dead claim link is a coherence bug, so the * two are one constant rather than two that agree. Both MCP transports quote * the duration in prose an agent reads, and derive it from here rather than * writing it out, which they did in eight places until this export existed. * * Seconds, spelled in the name: this platform has both second- and * millisecond-valued durations, and the pair is only safe when each says which * it is. */ export declare const PUBLIC_DEPLOYMENT_TTL_SECONDS: number; /** * Universal deploy input — the union of every shape the SDK accepts. * * - **Browser**: `File[]` (typically from `` or drag-and-drop) * - **Node**: `string | string[]` (file or directory path(s) on disk; directories are walked) * * Each platform's SDK narrows its `deploy()` signature to the relevant shape * and rejects anything else at runtime. Use the structural types directly * (`File[]`, `string | string[]`) when writing platform-specific code. */ export type DeployInput = File[] | string | string[]; /** * Options for deployment creation at the API contract level. * SDK implementations may extend with additional options (timeout, signal, callbacks, etc.). */ export interface DeploymentUploadOptions { /** Optional labels for categorization and filtering */ labels?: string[]; /** * Which client is making this deploy. Closed, because the server silently * ignores anything outside the set — so an unchecked string turned a typo * into missing analytics rather than an error. See {@link DeploymentVia}. */ via?: DeploymentViaType; /** * Seconds until this deployment expires; omit for one that never does. * * The platform reclaims it when the time is up — an ephemeral deployment, * chosen by the deployer rather than by the identity. The same word and the * same grammar as {@link TokenCreateOptions.ttl}, bounded by * {@link TTL_CONSTRAINTS}. * * **Requires a credential.** An anonymous deploy has no deployer, and the * platform owns anonymous lifetime as policy * ({@link PUBLIC_DEPLOYMENT_TTL_SECONDS}) — so a ttl on one is refused * rather than honoured or ignored. * * **A deployment carrying one cannot be linked to a domain.** A domain is a * commitment and a deadline is its opposite; the API refuses the link, which * is what keeps the reaper from tearing a live domain's target away. * * Immutable, like every other field of a deployment: to keep something * longer, redeploy. */ ttl?: number; /** * Optional password that protects this deployment. * * Length: {@link PASSWORD_CONSTRAINTS.MIN_LENGTH} to * {@link PASSWORD_CONSTRAINTS.MAX_LENGTH} characters. Leading and trailing * whitespace is trimmed before validation; internal whitespace is * significant. Visitors are prompted to enter the password before they can * view the deployment — including on any custom domains pointing at it. * To remove protection, redeploy without a password. */ password?: string; /** @internal Trigger server-side build. Only available via /upload endpoint. */ build?: boolean; /** @internal Trigger server-side prerender. Only available via /upload endpoint. */ prerender?: boolean; /** @internal Trigger server-side SPA detection. Only available via /upload endpoint. */ spa?: boolean; /** @internal reCAPTCHA proof for the anonymous human deploy channel. Only available via /upload endpoint. */ captcha?: string; /** * Makes this deploy replayable instead of repeatable. * * A deploy is not naturally idempotent: a client-side timeout on a slow * one leaves the caller unable to tell "it never landed" from "it landed * and the response was lost", and retrying produces a second deployment. * Send the same key on the retry and the platform replays the original * 201 verbatim rather than creating anything * ({@link IDEMPOTENCY_KEY_CONSTRAINTS.WINDOW_SECONDS}). * * **Agents are the audience.** A human notices a duplicate; an automated * retry does not. Pick a key that identifies the ATTEMPT — a run id, a * commit sha, a uuid minted before the first try — never one minted fresh * on each retry, which would defeat the point. * * The replay is per-caller, and it stores successes only: a failed deploy * retries fresh under the same key. */ idempotencyKey?: string; } /** * What a caller may change on an existing deployment. * * Labels and nothing else: a deployment's content is immutable by design, so * this is the whole mutable surface rather than a subset someone chose. */ export interface DeploymentSetOptions { labels: string[]; } /** * What `domains.set()` may create or change. Every field is optional because * the call is a natural-key upsert: omitting `deployment` reserves the * domain, naming one links or re-points it, and labels travel either way. * * `deployment` is deliberately not nullable — unlinking is refused (400). * See `npm/ship/CLAUDE.md`, "Domain Write Semantics". */ export interface DomainSetOptions { deployment?: string; labels?: string[]; } /** What a caller may set when minting a deploy token. */ export interface TokenCreateOptions { /** Seconds until expiry; omit for a token that never expires. */ ttl?: number; labels?: string[]; } /** * Deployment resource interface - the contract all implementations must follow. * * The interface defines the minimal wire contract; SDK implementations may * extend the upload options with runtime concerns (timeout, signal, progress * callbacks) by parameterizing: `DeploymentResource`. The * default keeps plain `DeploymentResource` valid for wire-only consumers. */ export interface DeploymentResource { upload: (input: DeployInput, options?: UploadOptions) => Promise; list: (options?: ListOptions) => Promise; get: (id: string) => Promise; set: (id: string, options: DeploymentSetOptions) => Promise; delete: (id: string) => Promise; } /** * Domain resource interface - the contract all implementations must follow */ export interface DomainResource { set: (name: string, options?: DomainSetOptions) => Promise; list: (options?: ListOptions) => Promise; get: (name: string) => Promise; delete: (name: string) => Promise; verify: (name: string) => Promise; validate: (name: string) => Promise; dns: (name: string) => Promise; records: (name: string) => Promise; share: (name: string) => Promise; } /** * Account resource interface - the contract all implementations must follow */ export interface AccountResource { get: () => Promise; } /** * Token resource interface - the contract all implementations must follow */ export interface TokenResource { create: (options?: TokenCreateOptions) => Promise; list: (options?: ListOptions) => Promise; get: (token: string) => Promise; delete: (token: string) => Promise; } /** * Billing status response from GET /billing/status * * Note: The user's `plan` comes from Account, not here. * This endpoint only returns billing-specific data (usage, portal, etc.) * * If `billing` is null, the user has no active billing. */ export interface BillingStatus { /** Creem billing ID, or null if no active billing */ billing: string | null; /** Number of billing units (1 unit = 1 custom domain), null if no billing */ units: number | null; /** Billing status from Creem (active, trialing, canceled, etc.), null if no billing */ status: string | null; /** Link to Creem customer portal for billing management, null if unavailable */ portal: string | null; } /** * Acknowledgement of `POST /billing/cancel`. * * Cancelling leaves no billing entity to return, so it answers with the * account and the one field of the account the call changed — the plan it * landed on. See {@link DeploymentDeleteResponse} for the law. * * This read `{ success: true, message: 'Subscription canceled successfully…' }` * until 2026-07-29, an anonymous shape that `web/my` redeclared inline and * whose prose no surface ever displayed: both callers await the promise and * discard the body, then compose their own toast. The message was written, * serialized, and thrown away on every cancellation. */ export interface BillingCancelResponse { /** The account whose subscription was cancelled */ readonly account: string; /** The plan the account now holds — `free` on a successful cancellation */ readonly plan: AccountPlanType; } /** * Checkout session response from POST /billing/checkout */ export interface CheckoutSession { /** URL to redirect user to Creem checkout page */ url: string; } /** * All activity event types logged in the system. * Uses dot notation consistently: {resource}.{action} */ export type ActivityEvent = 'account.create' | 'account.update' | 'account.delete' | 'account.key.generate' | 'account.plan.paid' | 'account.plan.transition' | 'account.suspended' | 'deployment.create' | 'deployment.update' | 'deployment.delete' | 'deployment.claim' | 'deployment.flagged' | 'deployment.open' | 'domain.create' | 'domain.update' | 'domain.delete' | 'domain.verify' | 'token.create' | 'token.consume' | 'token.delete' | 'admin.account.plan.update' | 'admin.account.ref.update' | 'admin.account.billing.update' | 'admin.account.labels.update' | 'admin.deployment.delete' | 'admin.domain.delete' | 'admin.billing.sync' | 'admin.billing.terminated' | 'admin.impersonate' | 'billing.active' | 'billing.canceled' | 'billing.paused' | 'billing.expired' | 'billing.paid' | 'billing.trialing' | 'billing.scheduled_cancel' | 'billing.unpaid' | 'billing.update' | 'billing.past_due' | 'refund.created' | 'dispute.created' | 'billing.sync' | 'billing.stale' | 'billing.race'; /** * Activity events visible to users in the dashboard */ export type UserVisibleActivityEvent = 'account.create' | 'account.update' | 'account.delete' | 'account.key.generate' | 'account.plan.transition' | 'deployment.create' | 'deployment.update' | 'deployment.delete' | 'deployment.claim' | 'domain.create' | 'domain.update' | 'domain.delete' | 'domain.verify' | 'token.create' | 'token.consume' | 'token.delete'; /** * Activity record returned from the API */ export interface Activity { /** The event type */ event: ActivityEvent; /** Unix timestamp (seconds) when the activity occurred */ created: number; /** Associated deployment ID (if applicable) */ deployment?: string; /** Associated domain name (if applicable) */ domain?: string; /** JSON-encoded metadata (parse with JSON.parse) */ meta?: string; } /** * Parsed activity metadata. * Different events populate different fields. * * Naming convention: meta booleans are event-scoped predicates and carry * their prefix (`isUpdate`, `wasVerified`, `hasConfig`, `hasPassword`), * while entity booleans are bare nouns (`Deployment.config`, * `Deployment.password`). Two vocabularies, each internally consistent — * deliberate, not drift. */ export interface ActivityMeta { /** Number of files in deployment */ files?: number; /** Total size in bytes */ size?: number; /** Whether deployment has a ship.json config */ hasConfig?: boolean; /** Whether deployment has a password set */ hasPassword?: boolean; /** * The client/tool that created the deployment. * * Narrower than {@link Deployment.via}, deliberately: the entity is * `string | null` because stored rows predate the vocabulary, while an * activity is only ever written by code that names one. It is here rather * than read off the deployment because the deployment row is deleted at * expiry or on request and the activity is not — this is where a deploy's * origin stays answerable afterwards. */ via?: DeploymentViaType; /** Whether this was an update (vs create) */ isUpdate?: boolean; /** Whether domain was already verified */ wasVerified?: boolean; /** Previous deployment ID before relinking */ previousDeployment?: string; /** Labels that were set/updated */ labels?: string[]; /** OAuth provider name */ provider?: string; /** Account email */ email?: string; /** Account display name */ name?: string; /** Previous plan */ from?: string; /** New plan */ to?: string; /** Allow additional fields for future use */ [key: string]: unknown; } /** * Response from GET /activities endpoint */ export interface ActivityListResponse extends ListResponse { /** Array of activities */ activities: Activity[]; } /** * File status constants for validation state tracking */ export declare const FileValidationStatus: { /** File is pending validation */ readonly PENDING: "pending"; /** File failed during processing (before validation) */ readonly PROCESSING_ERROR: "processing_error"; /** File was excluded by validation warning (not an error) */ readonly EXCLUDED: "excluded"; /** File failed validation (blocks deployment) */ readonly VALIDATION_FAILED: "validation_failed"; /** File passed validation and is ready for deployment */ readonly READY: "ready"; }; export type FileValidationStatusType = (typeof FileValidationStatus)[keyof typeof FileValidationStatus]; /** * A validation issue with a display-ready message * * Issues are either errors (in errors[] array) or warnings (in warnings[] array). * The array position determines severity - no need to duplicate it in the object. */ export interface ValidationIssue { /** File path that triggered this issue */ file: string; /** Display-ready message explaining the issue */ message: string; } /** * Minimal file interface required for validation */ export interface ValidatableFile { name: string; size: number; status?: FileValidationStatusType; statusMessage?: string; } /** * File validation result with severity-based issue reporting * * Validation checks files against constraints and categorizes issues by severity: * - **Errors**: Block deployment (file too large, invalid type, etc.) * - **Warnings**: Exclude files but allow deployment (empty files, etc.) * * @example * ```typescript * const result = validateFiles(files, config); * * if (!result.canDeploy) { * // Has errors - must fix before deploying * console.error('Deployment blocked:', result.errors); * } else if (result.warnings.length > 0) { * // Has warnings - deployment proceeds, some files excluded * console.warn('Files excluded:', result.warnings); * deploy(result.validFiles); * } else { * // All files valid * deploy(result.validFiles); * } * ``` */ export interface FileValidationResult { /** All files with updated status */ files: T[]; /** Files ready for deployment (status: 'ready') */ validFiles: T[]; /** Blocking errors that prevent deployment */ errors: ValidationIssue[]; /** Non-blocking warnings (files excluded but deployment allowed) */ warnings: ValidationIssue[]; /** Whether deployment can proceed (true if errors.length === 0) */ canDeploy: boolean; } /** * Represents a file that has been uploaded and stored */ export interface UploadedFile { key: string; etag: string; size: number; validated?: boolean; } /** * Check if a domain is a platform domain (subdomain of our platform). * Platform domains are free and don't require DNS verification. * * @example isPlatformDomain("www.shipstatic.com", "shipstatic.com") → true * @example isPlatformDomain("example.com", "shipstatic.com") → false */ export declare function isPlatformDomain(domain: string, platformDomain: string): boolean; /** * Check if a domain is a custom domain (not a platform subdomain). * Custom domains are billable and require DNS verification. * * @example isCustomDomain("example.com", "shipstatic.com") → true * @example isCustomDomain("www.shipstatic.com", "shipstatic.com") → false */ export declare function isCustomDomain(domain: string, platformDomain: string): boolean; /** * Extract subdomain from a platform domain. * Returns null if not a platform domain. * * @example extractSubdomain("www.shipstatic.com", "shipstatic.com") → "www" * @example extractSubdomain("example.com", "shipstatic.com") → null */ export declare function extractSubdomain(domain: string, platformDomain: string): string | null; /** * Generate HTTPS URL for a deployment hostname. */ export declare function generateDeploymentUrl(deployment: string): string; /** * Generate HTTPS URL for a domain. */ export declare function generateDomainUrl(domain: string): string; /** * Label validation constraints shared across UI and API. * These rules define the single source of truth for label validation. */ export declare const LABEL_CONSTRAINTS: { /** Minimum label length in characters */ readonly MIN_LENGTH: 3; /** Maximum label length in characters (concise labels, matches Stack Overflow's original limit) */ readonly MAX_LENGTH: 25; /** Maximum number of labels allowed per resource */ readonly MAX_COUNT: 10; /** Allowed separator characters between label segments */ readonly SEPARATORS: "._-"; }; /** * Label validation pattern. * Must start and end with alphanumeric (a-z, 0-9). * Can contain separators (. _ -) between segments, but not consecutive. * * Valid examples: 'production', 'v1.2.3', 'api_v2', 'us-east-1' * Invalid examples: 'ab' (too short), '-prod' (starts with separator), 'foo--bar' (consecutive separators) */ export declare const LABEL_PATTERN: RegExp; /** * Serialize labels array to JSON string for database storage. * Returns null for empty or undefined arrays. * * @example serializeLabels(['web', 'production']) → '["web","production"]' * @example serializeLabels([]) → null * @example serializeLabels(undefined) → null */ export declare function serializeLabels(labels: string[] | undefined): string | null; /** * Deserialize labels from JSON string to array. * Always returns an array — empty array for null/empty/invalid input. * * @example deserializeLabels('["web","production"]') → ['web', 'production'] * @example deserializeLabels(null) → [] * @example deserializeLabels('') → [] */ export declare function deserializeLabels(labelsJson: string | null): string[]; /** * Length constraints for the optional deployment password * (`DeploymentUploadOptions.password`). Single source of truth shared across * platform consumers. */ export declare const PASSWORD_CONSTRAINTS: { /** Minimum password length in characters */ readonly MIN_LENGTH: 6; /** Maximum password length in characters */ readonly MAX_LENGTH: 128; }; /** * Validate an optional deployment password and return it normalized. * * Absent (`undefined` / `null`) → returns `undefined`. Present → trim * leading/trailing whitespace, then validate against `PASSWORD_CONSTRAINTS` * length bounds (internal whitespace is significant and counts toward * length). Throws `ShipError.validation` on breach; returns the trimmed * value. * * The trim is canonical: at upload, the API hashes the trimmed value; at * unlock, the router trims submissions before hashing. Submission and storage * agree byte-for-byte. Length validation runs on the trimmed value because * that's the user's actual intent — and it disarms a class of invisible * foot-guns (trailing newlines from copy/paste, mobile auto-spacing, * password-manager artifacts). * * Single source of truth shared by SDK (client-side validation, return * ignored) and API (server-side enforcement, return threaded into config). * Length is part of the wire-format contract; strength rules, if added later, * stay server-side. See `CLAUDE.md` "Validation: format vs policy". */ export declare function validatePassword(value: unknown): string | undefined;