/** * Checkbook NYC API client * * Wraps the XML-based POST API at https://www.checkbooknyc.com/api * and the smart search web endpoint at /smart_search/citywide * * Docs: https://www.checkbooknyc.com/data-feeds/api */ export type DataDomain = "Contracts" | "Contracts_OGE" | "Contracts_NYCHA" | "Spending" | "Budget" | "Payroll" | "Revenue"; type CriteriaType = "value" | "range"; export interface Criteria { name: string; type: CriteriaType; value?: string; start?: string; end?: string; } interface ApiRequest { type_of_data: DataDomain; records_from?: number; max_records?: number; criteria?: Criteria[]; response_columns?: string[]; } interface ApiResponse { success: boolean; total_records: number; records: Record[]; error?: string; } /** * A SUCCESSFUL Checkbook API call. Deliberately carries no `total_records` or * `records` on any failure path — failures throw `CheckbookApiError` instead * (issue #21). This is the type-level guarantee that an upstream error can never * be reported as `total_records: 0` ("unreachable" misread as "no records exist"). */ export interface ApiSuccess { total_records: number; records: Record[]; } /** * Raised for ANY Checkbook API failure — network/timeout, a non-2xx HTTP status * (incl. a 403 block or an unfollowed 3xx redirect), or an API-level failure * status in the XML. Thrown rather than returned so the count-bearing success * shape is never populated on an error (issue #21). The MCP tool layer's `guard()` * wrapper turns it into an `isError` result, so a model sees a surfaced error, * not an empty result set. */ export declare class CheckbookApiError extends Error { readonly status?: number; readonly kind: "network" | "redirect" | "http" | "api"; constructor(message: string, opts: { status?: number; kind: CheckbookApiError["kind"]; }); } export declare function buildRequestXml(req: ApiRequest): string; export declare function parseResponse(xmlText: string): ApiResponse; export declare function pace(): Promise; /** * Retry policy for {@link fetchWithRetry}. Deliberately conservative: the point * of issues #23/#24 is to REDUCE load on a government edge that is actively * blocking us, so we cap attempts tightly and always space them out. */ export interface RetryPolicy { /** Total attempts including the first (so 3 = initial + 2 retries). */ maxAttempts: number; /** Base backoff before jitter, doubled each attempt. */ baseDelayMs: number; /** Ceiling on a single backoff delay (before honoring Retry-After). */ maxDelayMs: number; /** Overall budget for SCHEDULING retries — a new attempt is not started past this. */ deadlineMs: number; /** Per-request timeout (AbortSignal). */ timeoutMs: number; } export declare const DEFAULT_RETRY_POLICY: RetryPolicy; /** Injection seam for tests ONLY — production uses the real fetch/clock/timers. */ export interface RetryDeps { fetchImpl: typeof fetch; sleepImpl: (ms: number) => Promise; nowImpl: () => number; randomImpl: () => number; makeSignal: () => AbortSignal | undefined; /** Rate pacer. Injected as a no-op in unit tests so they need no real sleeps. */ paceImpl: () => Promise; } /** * A 3xx we deliberately did NOT follow (issue #23). * * Node/undici returns the ACTUAL 3xx response for `redirect: "manual"` — real * status code (e.g. 302) and inspectable `Location` — NOT a browser-style * opaqueredirect. Verified against undici source: "On the web this would return * an `opaqueredirect` response, but that doesn't make sense server side" * (https://github.com/nodejs/undici/issues/1193). We still defensively match a * spec-compliant opaqueredirect (status 0, type set) so the guard holds on any * runtime. */ export declare function isRedirectResponse(response: { status: number; type?: string; }): boolean; /** * Equal-jitter exponential backoff (issue #24). `attempt` is 1-based. * * expo = min(maxDelayMs, baseDelayMs * 2^(attempt-1)) * delay = expo/2 + random()*(expo/2) → always ≥ expo/2 > 0 * * Equal jitter (rather than full jitter) keeps a guaranteed non-zero floor — so * there is never an "immediate" retry — while still de-correlating concurrent * clients. Algorithm: AWS Architecture Blog, "Exponential Backoff And Jitter". */ export declare function computeBackoffMs(attempt: number, policy: RetryPolicy, random?: () => number): number; /** * Parse a `Retry-After` header to milliseconds of delay, or undefined if absent * or unparseable. Per RFC 9110 §10.2.3 / MDN, the value is EITHER `delay-seconds` * (a non-negative integer) OR an `HTTP-date`. Never returns a negative delay. */ export declare function parseRetryAfterMs(headerValue: string | null | undefined, now?: () => number): number | undefined; /** * POST/GET with retries that BACK OFF (issue #24) and do NOT follow redirects * (issue #23). * * - `redirect: "manual"` — a 3xx is surfaced, never chased. One logical call was * measured fanning out to ~40 requests against the Checkbook edge before a * terminal 403; that amplification is the harm this closes. * - Retries ONLY on 5xx and network/timeout errors (the original trigger set), * but now with equal-jitter exponential backoff, an attempt cap, and an overall * deadline. Immediate zero-backoff retry is exactly the pattern the API * operator named as their block trigger. * - NEVER retries a 4xx — 403 is an answer, not a transient failure. * - Honors `Retry-After` on a retryable 5xx when present. * * Returns the terminal Response (2xx, 4xx, or an unfollowed 3xx/5xx); the caller * decides what is an error. Rejects only when every attempt hit a network error. */ export declare function fetchWithRetry(url: string, init: RequestInit, policy?: RetryPolicy, deps?: Partial): Promise; /** * Call the Checkbook XML API. Returns records on success; THROWS * {@link CheckbookApiError} on any failure (issue #21) — never a zero-count * "success". A genuinely empty-but-successful result still returns * `{ total_records: 0, records: [] }`; only errors throw, so "no matches" and * "unreachable" are no longer conflated. */ export declare function callCheckbookApi(req: ApiRequest): Promise; interface SmartSearchResult { type: string; fields: Record; } export interface SmartSearchOutcome { available: boolean; total: number; results: SmartSearchResult[]; reason?: string; fallback?: string; } /** * Detect whether a smart_search HTTP response is usable server-side. * * Verified live 2026-07-06: checkbooknyc.com fronts /smart_search with an * Imperva/Incapsula WAF that answers non-browser clients with a JavaScript * challenge (302 "Loading" interstitial, then 403 with an _Incapsula_Resource * iframe). The results grid itself is also rendered client-side by * JavaScript, so even a passed challenge would return no data in the raw * HTML. This function classifies those failure shapes. */ export declare function classifySmartSearchResponse(status: number, html: string): { usable: boolean; reason?: string; }; /** * Attempt a smart search against the Checkbook NYC web endpoint. * * NOTE: as of 2026-07-06 this endpoint is not usable server-side (Incapsula * WAF JS challenge + client-side-rendered results). The request is still * attempted in case access is restored, but callers should expect * `available: false` with a structured reason and fallback guidance. */ export declare function smartSearch(query: string, limit?: number): Promise; export declare const DEFAULT_COLUMNS: Record; export {}; //# sourceMappingURL=checkbook.d.ts.map