/** * @fileoverview OpenFEC API service. Wraps all FEC REST API interactions * with timeout, retry, and pagination handling. Single service used by * all tools and resources. * @module src/services/openfec/openfec-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { ElectionSummary, FecParams, LegalResult, PageResult, SeekResult } from './types.js'; /** * The identity a keyset cursor is bound to: the tool that issued it plus the * caller arguments that shape the result set. OpenFEC silently ignores keyset * keys that do not match the active sort, so a cursor replayed against a * different query restarts at page one without any signal — binding the two * together lets `decodeCursor` reject the replay instead. */ export interface CursorQuery { /** Caller arguments that shape the keyset, stringified. */ args: Record; /** Tool name that issued the cursor. Blocks replay across the itemized tools. */ scope: string; } /** * Normalize a tool's parsed input into the identity its cursors are bound to. * Derived from the caller's own arguments rather than the outbound FEC params * so a mismatch names fields the caller can actually see in the tool schema. */ export declare function cursorQuery(scope: string, input: Record): CursorQuery; /** * Encode `last_indexes` and the issuing query into an opaque cursor. * Index values are stringified first — OpenFEC returns some of them as raw * numbers (Schedule E's `last_office_total_ytd`), and they go back out as * query params either way. */ export declare function encodeCursor(lastIndexes: Record, query: CursorQuery): string; /** * Decode an opaque cursor back to `last_indexes` query params. * Throws a `validationError` when the cursor is malformed (`invalid_cursor`) * or was issued for a different query (`cursor_query_mismatch`). */ export declare function decodeCursor(cursor: string, expected: CursorQuery): Record; /** * Throw when an outbound parameter name is not one the endpoint accepts. * Only the names are reported — values may carry caller data. */ export declare function assertKnownParams(path: string, params: FecParams): void; export declare class OpenFecService { private readonly config; constructor(); /** * Build a full URL with query params, injecting the API key. * Every outbound request funnels through here, so this is where parameter * names are checked against the endpoint's accepted set. */ private buildUrl; /** * Fetch JSON from a page-based endpoint with retry. * Wraps the full pipeline (fetch + JSON parse) in the retry boundary. */ private fetchPage; /** * Fetch JSON from a keyset (SEEK) endpoint with retry. * Returns a `nextCursor` from `last_indexes` when more results exist, bound * to `query` so a replay under different arguments is rejected on decode. */ private fetchSeek; /** Fetch legal search results with retry. Normalizes type-keyed arrays. */ private fetchLegalSearch; /** Validate that the API returned a recognizable envelope, not an HTML error page. */ private validateEnvelope; searchCandidates(params: FecParams, ctx: Context): Promise; getCandidate(candidateId: string, ctx: Context): Promise; getCandidateTotals(params: FecParams, ctx: Context): Promise; getCandidateCommittees(candidateId: string, params: FecParams, ctx: Context): Promise; searchCommittees(params: FecParams, ctx: Context): Promise; getCommittee(committeeId: string, ctx: Context): Promise; /** * Per-cycle financial totals for one committee. OpenFEC answers 404 for every * miss here — an ID that does not exist, a cycle the committee did not file, * and a committee that has never filed a Form 3/3X/3P alike — so its own * not-found response is normalized to an empty page. "No totals on file" is a * result the caller reports, not an API-path error. */ getCommitteeTotals(committeeId: string, params: FecParams, ctx: Context): Promise; /** Committee totals grouped by entity type — a page of committees, not one committee. */ getCommitteeTotalsByEntityType(entityType: string, params: FecParams, ctx: Context): Promise; searchContributions(params: FecParams, query: CursorQuery, ctx: Context): Promise; getContributionAggregates(mode: string, params: FecParams, ctx: Context): Promise; searchDisbursements(params: FecParams, query: CursorQuery, ctx: Context): Promise; getDisbursementAggregates(mode: string, params: FecParams, ctx: Context): Promise; searchExpenditures(params: FecParams, query: CursorQuery, ctx: Context): Promise; getExpendituresByCandidate(params: FecParams, ctx: Context): Promise; searchCoordinatedExpenditures(params: FecParams, ctx: Context): Promise; searchFilings(params: FecParams, ctx: Context): Promise; searchElections(params: FecParams, ctx: Context): Promise; /** Search elections with ZIP support — uses /elections/search/ which accepts zip. */ searchElectionsByZip(params: FecParams, ctx: Context): Promise; /** Fetch election summary — flat response (no pagination wrapper). */ getElectionSummary(params: FecParams, ctx: Context): Promise; searchLegal(params: FecParams, ctx: Context): Promise; /** * Fetch one legal document by type and number. Neither of the two envelopes * this endpoint answers with is the `results` array `fetchPage` expects, so * it needs its own fetch. Resolves to null when no such record exists. */ getLegalDocument(docType: string, no: string, ctx: Context): Promise | null>; getCalendarDates(params: FecParams, ctx: Context): Promise; getReportingDates(params: FecParams, ctx: Context): Promise; getElectionDates(params: FecParams, ctx: Context): Promise; } export declare function initOpenFecService(): void; export declare function getOpenFecService(): OpenFecService; //# sourceMappingURL=openfec-service.d.ts.map