/** * Shared REST transport for Sitecore Cloud JSON APIs. * * Sister module to `shared/graphql.ts`. Where that one owns the GraphQL * wire protocol (POST a query, unwrap `data`/`errors`), this one owns the * plain-REST wire protocol: bearer auth, JSON request/response, a * timeout/abort guard, optional retry-with-backoff, and a JSON-parse step * that tolerates empty / non-JSON bodies. * * Several REST clients used to hand-roll this plumbing — `deployRequest` * (the canonical one), plus near-identical copies in `sites/api/request.ts`, * `brand/api/client.ts`, and `publishing/api/client.ts`. They target * different hosts with different auth and different error-body shapes, but * everything *except* those per-API concerns was duplicated. * * This module owns the spine. The per-API specifics — how to obtain a * bearer token, which status codes retry, how to turn a non-2xx body into * a `ScaiError`, and which empty-body statuses mean "no content" — are * injected via {@link RestRequestConfig}. The four clients become thin * wrappers that supply those constants. * * Hard-leaf constraint: `src/shared/` imports no domain area. Auth is * *injected* (a callback or a pre-resolved header), never imported, so * this module never reaches across a layer boundary. Type-only `@/config` * imports are allowed (graphql.ts follows the same rule). */ import { type ScaiError } from "./errors.js"; /** * Best-effort human-readable message out of an arbitrary JSON error body. * Handles the three shapes scai's upstreams emit: a bare string, an * ASP.NET `ProblemDetails` (`detail`/`title`/`errors`), and a * FastAPI/pydantic `detail: [{ loc, msg }]` array. The generic summary * (`detail`/`message`/`title`/`error`) is captured but never allowed to * short-circuit ahead of the field-level `errors` detail, which is the * part that actually says what was wrong. * * Previously lived in `deploy/api/common/request.ts`; lifted here so the * deploy and sites clients share one implementation. */ export declare const extractErrorMessage: (body: unknown) => string | undefined; /** * Read a response body once and parse it as JSON, tolerating empty and * non-JSON bodies. Returns `undefined` for an empty body, the parsed * value for valid JSON, and the raw text for anything that fails to * parse (e.g. an HTML 500 page). Used on both the success and error * paths so a single `text()` read covers both. * * Previously lived in `deploy/api/common/request.ts`; lifted here as the * single source for every REST client. */ export declare const parseJsonIfPossible: (response: Response) => Promise; export interface RestRetryConfig { /** * Total retries (in addition to the first attempt) on transient * failures. Default 0 — no retry — so clients keep their current * "fail fast" semantics unless they opt in. */ maxRetries?: number; /** Base backoff in ms; doubled per attempt and jittered ±50%. Default 500. */ retryBaseMs?: number; /** * Decide whether a non-2xx `Response` is worth retrying. Called with * the status and the request method. Defaults to never. Deploy/sites * pass a GET-only "429 or 5xx" predicate here. */ shouldRetryStatus?: (status: number, method: string) => boolean; /** * Whether a *network* error (fetch rejection — DNS, ECONNRESET, an * abort) is worth retrying. Called with the request method. Defaults * to never. Deploy retries network errors on GET only. */ shouldRetryNetworkError?: (method: string) => boolean; } /** * Per-attempt auth + auth-failure recovery. Lets a client mint a bearer * token lazily and, on an auth-failure status, refresh it and retry once * — the Brand API's documented 24h-token-expiry recovery. Kept separate * from {@link RestRetryConfig} (which does backoff on transient statuses) * because an auth refresh is a credential operation, not a backoff wait, * and it must run *between* attempts so the retry carries a fresh token. */ export interface RestAuthConfig { /** Resolve the `Authorization` header value (e.g. `Bearer `) for an attempt. */ getAuthHeader: () => Promise; /** * Status that signals a stale token (typically `401`). When the first * attempt returns this, the transport calls {@link onAuthFailure}, * re-resolves the header via {@link getAuthHeader}, and retries exactly * once. A second occurrence is surfaced as an error. */ refreshOnStatus: number; /** Invoked before the single auth retry — e.g. clear the cached token. */ onAuthFailure: () => Promise; } export interface RestRequestConfig { /** * Fully-resolved request URL (host + path + query). Callers build this * — the various clients disagree on query encoding and base-URL * joining, and that's their concern, not the transport's. */ url: string; /** HTTP method. Defaults to `GET`. */ method?: string; /** * Serialized request body. Already a string (or `undefined`); the * caller decides how to stringify so each client controls its own * `Content-Type` handling. */ body?: string; /** * Request headers. When {@link RestRequestConfig.auth} is set, the * `Authorization` header is filled per-attempt from `auth.getAuthHeader` * and merged on top of these; otherwise this must carry a static * `Authorization` already. */ headers: Record; /** * Lazy auth + auth-failure recovery (Brand API's 401-clear-and-retry). * Omit for static-token clients that pre-set the `Authorization` header. */ auth?: RestAuthConfig; /** AbortSignal for caller-driven cancellation, merged with the timeout. */ signal?: AbortSignal; /** * Per-attempt timeout in ms. `0` (or negative) disables the guard. * Each client resolves this from its own env-var fallbacks before * calling in. */ timeoutMs?: number; /** Retry behaviour. Omit for no retry. */ retry?: RestRetryConfig; /** * Human-readable label for the API, used by the default network-error * message — e.g. `"Sites API"`. Wrappers that map their own errors via * `mapNetworkError` can ignore this. */ label: string; /** * Statuses whose body is empty by contract and should resolve to * `undefined` instead of being parsed (e.g. `204`, `202`). Default * `[204]`. */ emptyStatuses?: ReadonlySet; /** * Map a non-2xx `Response` to a `ScaiError`. The body has already been * read + JSON-parsed (via {@link parseJsonIfPossible}) and is passed in * so each client maps its own error-body shape and error code. Must * return (not throw) the error to throw. */ mapHttpError: (response: Response, body: unknown) => ScaiError; /** * Map a fetch rejection (network error / abort) to a `ScaiError`. * Defaults to a redacted `NETWORK` error built from `label`. */ mapNetworkError?: (error: unknown) => ScaiError; } export declare const runSitecoreRest: (config: RestRequestConfig) => Promise;