/** * Shared GraphQL transport for Sitecore tenant APIs. * * Two near-identical transports — the Authoring API (used by `recipe`) * and the Management API (used by `serialization`) — used to ship as * separate copies of timeout/abort/error/redaction plumbing. They live * in different modules because they target different Sitecore endpoints * with different auth contracts, but everything *except* the endpoint * URL, the human-readable label, and whether the bearer token is * required vs optional was duplicated. * * This module owns the wire protocol; `recipe/api/graphql.ts` and * `serialization/api/graphql.ts` are thin wrappers that supply * the per-API constants. */ import type { EnvironmentConfiguration } from "../config/types.js"; type GetAccessToken = (environment: EnvironmentConfiguration) => Promise; export interface GraphQLRequestOptions { timeoutMs?: number; /** * Override retry behaviour for transient failures (429 throttling, 502/503/504, * network blips, abort-but-not-timeout). Defaults to 5 attempts with * exponential backoff + jitter, honoring `Retry-After` on 429/503. */ retry?: RetryOptions; } export interface RetryOptions { /** Total attempts including the initial one. Default 5. */ maxAttempts?: number; /** Base delay in ms; doubled per retry, jittered ±50%. Default 500ms. */ baseDelayMs?: number; /** Cap on a single backoff delay. Default 15s. */ maxDelayMs?: number; /** * Which HTTP status codes are retryable. Defaults to the conservative * "definite-throttle / never-reached-origin" set: 408, 425, 429, 503. * Read-only callers can pass a wider set (`READ_RETRYABLE_STATUSES`) * that includes ambiguous 5xx codes like 500/502/504; mutation callers * keep the conservative default so a transient 5xx-after-success * doesn't trigger a duplicate-write retry. Network blips * (`TypeError: fetch failed`) are always retryable regardless of this * set since no request was sent. */ retryableStatuses?: ReadonlySet; /** * Whether AMBIGUOUS network failures — a client-side abort (our own * timeout) or a `TypeError: fetch failed` — are retryable. Default true. * Reads and idempotent callers leave it on. Mutation callers set it false: * after such a failure the request MAY have applied server-side, so a retry * risks a duplicate write. A server-side "operation was canceled" GraphQL * error is NOT gated by this — a cancelled op is rolled back (never applied) * so it is always safe to retry. */ retryAmbiguousNetwork?: boolean; /** * Backoff schedule (ms per retry) for an authorization refusal — * `AUTH_NOT_AUTHORIZED` inside a 200-OK GraphQL `errors[]` payload. A * freshly minted CM automation client authenticates instantly (Auth0), but * the CM's own user/role mapping settles asynchronously and individual * requests can be refused for a while after provisioning. A refusal is * definitively NOT applied, so it is safe to retry even for writes. This * track is independent of `maxAttempts` (which most mutation callers pin * to 1–3 with sub-second backoff — far too short for role propagation). * Pass `[]` to disable. */ authzSettleDelaysMs?: readonly number[]; } /** * Read-only callers can opt in to a broader retry set that includes * ambiguous 5xx codes. Safe for GETs since reads are idempotent — a * second fetch is harmless. Unsafe for mutations because 500/502/504 * may indicate the server processed the request but failed to respond, * and retrying would create a duplicate. */ export declare const READ_RETRYABLE_STATUSES: Set; export interface GraphQLTransportConfig { /** URL path appended to `environment.host`. */ servicePath: string; /** Human-readable label used in error messages — e.g. `"Authoring"` or `"Management"`. */ label: string; /** * When `true`, the call throws `AUTH_REQUIRED` if no token is available * (Authoring API). When `false`, the request is sent without the * Authorization header (Management API — anonymous access valid for * some queries). */ requireToken: boolean; /** * Strategy for fetching a Bearer token for the request. Wrappers pass * this in so the shared module doesn't reach across layer boundaries * to import auth, and so tests can mock auth at the wrapper's seam * without crossing layers. */ getAccessToken: GetAccessToken; } export declare const classifyCloudflareEdgeError: (body: unknown) => { retry: "safe" | "ambiguous"; summary: string; } | undefined; export declare const runSitecoreGraphQL: (environment: EnvironmentConfiguration, query: string, variables: Record | undefined, transport: GraphQLTransportConfig, options?: GraphQLRequestOptions) => Promise; export {};