tn-elections-mcp 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/api/easyvote-client.d.ts +51 -0
- package/dist/api/easyvote-client.js +254 -0
- package/dist/api/easyvote-client.js.map +1 -0
- package/dist/api/easyvote-client.test.d.ts +1 -0
- package/dist/api/easyvote-client.test.js +47 -0
- package/dist/api/easyvote-client.test.js.map +1 -0
- package/dist/api/gemini-pdf.d.ts +23 -0
- package/dist/api/gemini-pdf.js +261 -0
- package/dist/api/gemini-pdf.js.map +1 -0
- package/dist/api/sos-client.d.ts +15 -0
- package/dist/api/sos-client.js +74 -0
- package/dist/api/sos-client.js.map +1 -0
- package/dist/api/sumner-client.d.ts +12 -0
- package/dist/api/sumner-client.js +57 -0
- package/dist/api/sumner-client.js.map +1 -0
- package/dist/api/tncamp-client.d.ts +61 -0
- package/dist/api/tncamp-client.js +310 -0
- package/dist/api/tncamp-client.js.map +1 -0
- package/dist/data/sumner-cycles.d.ts +3 -0
- package/dist/data/sumner-cycles.js +19 -0
- package/dist/data/sumner-cycles.js.map +1 -0
- package/dist/data/sumner-cycles.test.d.ts +1 -0
- package/dist/data/sumner-cycles.test.js +29 -0
- package/dist/data/sumner-cycles.test.js.map +1 -0
- package/dist/data/tn-counties.d.ts +23 -0
- package/dist/data/tn-counties.js +162 -0
- package/dist/data/tn-counties.js.map +1 -0
- package/dist/data/tn-counties.test.d.ts +1 -0
- package/dist/data/tn-counties.test.js +71 -0
- package/dist/data/tn-counties.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/sos-xlsx.d.ts +24 -0
- package/dist/parsers/sos-xlsx.js +149 -0
- package/dist/parsers/sos-xlsx.js.map +1 -0
- package/dist/parsers/sos-xlsx.test.d.ts +1 -0
- package/dist/parsers/sos-xlsx.test.js +111 -0
- package/dist/parsers/sos-xlsx.test.js.map +1 -0
- package/dist/parsers/sumner-html.d.ts +8 -0
- package/dist/parsers/sumner-html.js +200 -0
- package/dist/parsers/sumner-html.js.map +1 -0
- package/dist/parsers/sumner-html.test.d.ts +1 -0
- package/dist/parsers/sumner-html.test.js +79 -0
- package/dist/parsers/sumner-html.test.js.map +1 -0
- package/dist/parsers/tncamp-form.d.ts +15 -0
- package/dist/parsers/tncamp-form.js +54 -0
- package/dist/parsers/tncamp-form.js.map +1 -0
- package/dist/parsers/tncamp-form.test.d.ts +1 -0
- package/dist/parsers/tncamp-form.test.js +114 -0
- package/dist/parsers/tncamp-form.test.js.map +1 -0
- package/dist/parsers/tncamp-report.d.ts +6 -0
- package/dist/parsers/tncamp-report.js +189 -0
- package/dist/parsers/tncamp-report.js.map +1 -0
- package/dist/parsers/tncamp-results.d.ts +10 -0
- package/dist/parsers/tncamp-results.js +113 -0
- package/dist/parsers/tncamp-results.js.map +1 -0
- package/dist/parsers/tncamp-results.test.d.ts +1 -0
- package/dist/parsers/tncamp-results.test.js +86 -0
- package/dist/parsers/tncamp-results.test.js.map +1 -0
- package/dist/tools/easyvote.d.ts +2 -0
- package/dist/tools/easyvote.js +382 -0
- package/dist/tools/easyvote.js.map +1 -0
- package/dist/tools/sos.d.ts +2 -0
- package/dist/tools/sos.js +130 -0
- package/dist/tools/sos.js.map +1 -0
- package/dist/tools/sumner.d.ts +2 -0
- package/dist/tools/sumner.js +134 -0
- package/dist/tools/sumner.js.map +1 -0
- package/dist/tools/tncamp.d.ts +2 -0
- package/dist/tools/tncamp.js +401 -0
- package/dist/tools/tncamp.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# tn-elections-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for Tennessee elections: county campaign finance (EasyVote), state campaign finance (tncamp), Sumner County PDF-based disclosures, and Secretary of State candidate filings.
|
|
4
|
+
|
|
5
|
+
## Data Sources
|
|
6
|
+
|
|
7
|
+
### EasyVote Campaign Finance (14 counties)
|
|
8
|
+
JSON API at `ecf-api.easyvoteapp.com`. Covers county-level races (mayor, county commission, school board, municipal offices) in: Anderson, Davidson, Greene, Hamblen, Hamilton, Knox, Madison, Marion, Maury, Rutherford, Shelby, Sullivan, Washington, Williamson.
|
|
9
|
+
|
|
10
|
+
### tncamp (State-level)
|
|
11
|
+
HTML scraping of `apps.tn.gov/tncamp`. Covers Governor, state Senate, state House, judges, DAs, and statewide PACs across all Tennessee counties.
|
|
12
|
+
|
|
13
|
+
### Sumner County
|
|
14
|
+
HTML scraping of `votesumnertn.org` for candidate metadata + Gemini Flash for PDF financial data extraction.
|
|
15
|
+
|
|
16
|
+
### TN Secretary of State (Candidate Filings)
|
|
17
|
+
Excel files from `sos.tn.gov/elections`. Official candidate filing lists for state and federal races: Governor, US Senate, US House, TN Senate, TN House, and party executive committees. This is the authoritative "who has filed to run" source, distinct from campaign finance data.
|
|
18
|
+
|
|
19
|
+
## Tools (22)
|
|
20
|
+
|
|
21
|
+
| Tool | Source | Description |
|
|
22
|
+
|------|--------|-------------|
|
|
23
|
+
| `list_easyvote_counties` | EasyVote | List 14 counties with portals |
|
|
24
|
+
| `search_county_filers` | EasyVote | Search candidates/PACs in a county |
|
|
25
|
+
| `list_county_offices` | EasyVote | List offices in a county |
|
|
26
|
+
| `search_county_contributions` | EasyVote | Search contributions by donor/recipient |
|
|
27
|
+
| `search_county_expenditures` | EasyVote | Search expenditures by vendor/candidate |
|
|
28
|
+
| `get_county_election_documents` | EasyVote | Get election documents |
|
|
29
|
+
| `search_all_counties_contributions` | EasyVote | Cross-county donor search |
|
|
30
|
+
| `search_all_counties_expenditures` | EasyVote | Cross-county vendor search |
|
|
31
|
+
| `search_state_candidates` | tncamp | Search by name or by office/district |
|
|
32
|
+
| `list_tncamp_offices` | tncamp | List available offices (courts, DA, etc.) |
|
|
33
|
+
| `list_tncamp_districts` | tncamp | List districts for an office |
|
|
34
|
+
| `get_candidate_reports` | tncamp | Get filed report list |
|
|
35
|
+
| `get_report_detail` | tncamp | Full financial breakdown |
|
|
36
|
+
| `search_state_contributions` | tncamp | Cross-candidate contribution search |
|
|
37
|
+
| `search_state_expenditures` | tncamp | Cross-candidate expenditure search |
|
|
38
|
+
| `get_candidate_financials` | tncamp | Quick candidate financial summary |
|
|
39
|
+
| `list_sumner_election_cycles` | Sumner | List election cycles |
|
|
40
|
+
| `search_sumner_candidates` | Sumner | Search candidates + PDF links |
|
|
41
|
+
| `get_sumner_report` | Sumner | Extract financials from PDF via Gemini |
|
|
42
|
+
| `search_filed_candidates` | TN SOS | Search officially filed candidates |
|
|
43
|
+
| `list_filed_offices` | TN SOS | List office categories with candidate counts |
|
|
44
|
+
| `get_filing_summary` | TN SOS | Aggregated filing stats by office/party |
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g tn-elections-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or use with Claude Code:
|
|
53
|
+
```bash
|
|
54
|
+
claude mcp add tn-elections -- npx tn-elections-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For Sumner County PDF extraction, add the Gemini API key:
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add tn-elections --env GEMINI_API_KEY=your_key -- npx tn-elections-mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or in `.claude.json`:
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"tn-elections": {
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["tn-elections-mcp"],
|
|
68
|
+
"env": {
|
|
69
|
+
"GEMINI_API_KEY": "your-key-here"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without the Gemini key, `get_sumner_report` falls back to returning PDF page images for Claude to read directly.
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm run dev # Hot-reload via tsx
|
|
81
|
+
npm test # Run tests
|
|
82
|
+
npm run build # Compile TypeScript
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyVote Campaign Finance API client.
|
|
3
|
+
*
|
|
4
|
+
* Multi-tenant JSON API at ecf-api.easyvoteapp.com. Each Tennessee county
|
|
5
|
+
* gets its own tenant ID. All endpoints are public (no login required).
|
|
6
|
+
*
|
|
7
|
+
* Critical: every request needs Origin header matching the county subdomain,
|
|
8
|
+
* plus zumo-api-version header. Data endpoints also need the auth header
|
|
9
|
+
* containing UserId and CustomerId (tenant ID).
|
|
10
|
+
*/
|
|
11
|
+
import type { EasyVoteAuthContext, EasyVoteFiler, EasyVoteContribution, EasyVoteDistribution } from '../types.js';
|
|
12
|
+
import type { EasyVoteCountyConfig } from '../data/tn-counties.js';
|
|
13
|
+
export declare function bootstrapCounty(slug: string): Promise<EasyVoteAuthContext | null>;
|
|
14
|
+
/** Get all filers and their documents for a county */
|
|
15
|
+
export declare function getFilers(county: EasyVoteCountyConfig): Promise<EasyVoteFiler[]>;
|
|
16
|
+
/** Get offices for a county */
|
|
17
|
+
export declare function getOffices(county: EasyVoteCountyConfig): Promise<unknown[]>;
|
|
18
|
+
/** Search contribution parameters */
|
|
19
|
+
export interface ContributionSearchParams {
|
|
20
|
+
donorLastName?: string;
|
|
21
|
+
donorFirstName?: string;
|
|
22
|
+
recipientLastName?: string;
|
|
23
|
+
recipientFirstName?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get ALL contributions for a county (cached 10 min).
|
|
27
|
+
*
|
|
28
|
+
* The EasyVote API ignores all query parameters on the contributions endpoint —
|
|
29
|
+
* it always returns the full dataset. All filtering is done client-side.
|
|
30
|
+
* Large counties (Shelby: ~29MB, Davidson: ~25K records) will be cached after
|
|
31
|
+
* the first fetch to avoid repeated large downloads.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getAllContributions(county: EasyVoteCountyConfig): Promise<EasyVoteContribution[]>;
|
|
34
|
+
/** Search contributions with client-side filtering (API ignores query params) */
|
|
35
|
+
export declare function searchContributions(county: EasyVoteCountyConfig, params: ContributionSearchParams): Promise<EasyVoteContribution[]>;
|
|
36
|
+
/** Distribution/expenditure search parameters */
|
|
37
|
+
export interface DistributionSearchParams {
|
|
38
|
+
payeeLastName?: string;
|
|
39
|
+
payeeFirstName?: string;
|
|
40
|
+
candidateLastName?: string;
|
|
41
|
+
candidateFirstName?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get ALL distributions/expenditures for a county (cached 10 min).
|
|
45
|
+
* Same pattern as contributions — API likely ignores query params.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getAllDistributions(county: EasyVoteCountyConfig): Promise<EasyVoteDistribution[]>;
|
|
48
|
+
/** Search distributions with client-side filtering */
|
|
49
|
+
export declare function searchDistributions(county: EasyVoteCountyConfig, params: DistributionSearchParams): Promise<EasyVoteDistribution[]>;
|
|
50
|
+
/** Get election documents for a county */
|
|
51
|
+
export declare function getElectionDocuments(county: EasyVoteCountyConfig): Promise<unknown[]>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyVote Campaign Finance API client.
|
|
3
|
+
*
|
|
4
|
+
* Multi-tenant JSON API at ecf-api.easyvoteapp.com. Each Tennessee county
|
|
5
|
+
* gets its own tenant ID. All endpoints are public (no login required).
|
|
6
|
+
*
|
|
7
|
+
* Critical: every request needs Origin header matching the county subdomain,
|
|
8
|
+
* plus zumo-api-version header. Data endpoints also need the auth header
|
|
9
|
+
* containing UserId and CustomerId (tenant ID).
|
|
10
|
+
*/
|
|
11
|
+
const ECF_BASE = 'https://ecf-api.easyvoteapp.com';
|
|
12
|
+
const TIMEOUT_MS = 10_000;
|
|
13
|
+
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Auth context cache (bootstrapped per county, TTL 30 min)
|
|
16
|
+
// ============================================================
|
|
17
|
+
const AUTH_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
18
|
+
const authCache = new Map();
|
|
19
|
+
function originUrl(slug) {
|
|
20
|
+
return `https://${slug}.easyvotecampaignfinance.com`;
|
|
21
|
+
}
|
|
22
|
+
function buildAuthHeader(ctx) {
|
|
23
|
+
return `UserId:${ctx.UserId}|CustomerId:${ctx.CustomerId}|ZumoToken:null`;
|
|
24
|
+
}
|
|
25
|
+
function buildHeaders(county, ctx) {
|
|
26
|
+
return {
|
|
27
|
+
'Origin': originUrl(county.slug),
|
|
28
|
+
'easy-vote-authenticated-user': buildAuthHeader(ctx),
|
|
29
|
+
'zumo-api-version': '2.0.0',
|
|
30
|
+
'Accept': 'application/json',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ============================================================
|
|
34
|
+
// Fetch with timeout and JSON sanitization
|
|
35
|
+
// ============================================================
|
|
36
|
+
async function fetchWithTimeout(url, headers) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
39
|
+
try {
|
|
40
|
+
return await fetch(url, { headers, signal: controller.signal });
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Strip control characters that break JSON.parse (observed in Knox County responses) */
|
|
47
|
+
function sanitizeJson(raw) {
|
|
48
|
+
return raw.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
49
|
+
}
|
|
50
|
+
async function fetchJson(url, headers) {
|
|
51
|
+
const response = await fetchWithTimeout(url, headers);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
return { data: null, status: response.status };
|
|
54
|
+
}
|
|
55
|
+
const raw = await response.text();
|
|
56
|
+
return { data: JSON.parse(sanitizeJson(raw)), status: response.status };
|
|
57
|
+
}
|
|
58
|
+
// ============================================================
|
|
59
|
+
// Bootstrap: get auth context for a county
|
|
60
|
+
// ============================================================
|
|
61
|
+
export async function bootstrapCounty(slug) {
|
|
62
|
+
const cached = authCache.get(slug);
|
|
63
|
+
if (cached && Date.now() - cached.timestamp < AUTH_CACHE_TTL_MS) {
|
|
64
|
+
return cached.data;
|
|
65
|
+
}
|
|
66
|
+
const url = `${ECF_BASE}/authentication/getwebsiteuser/${slug}`;
|
|
67
|
+
const headers = {
|
|
68
|
+
'Origin': originUrl(slug),
|
|
69
|
+
'Accept': 'application/json',
|
|
70
|
+
};
|
|
71
|
+
const { data: ctx } = await fetchJson(url, headers);
|
|
72
|
+
if (ctx) {
|
|
73
|
+
authCache.set(slug, { data: ctx, timestamp: Date.now() });
|
|
74
|
+
}
|
|
75
|
+
return ctx;
|
|
76
|
+
}
|
|
77
|
+
/** Clear cached auth and re-bootstrap (for retry-on-401) */
|
|
78
|
+
async function rebootstrap(slug) {
|
|
79
|
+
authCache.delete(slug);
|
|
80
|
+
return bootstrapCounty(slug);
|
|
81
|
+
}
|
|
82
|
+
/** Get auth context, retrying bootstrap on failure */
|
|
83
|
+
async function getAuth(county) {
|
|
84
|
+
const ctx = await bootstrapCounty(county.slug);
|
|
85
|
+
if (!ctx) {
|
|
86
|
+
throw new Error(`Failed to bootstrap EasyVote auth for ${county.name} County (${county.slug})`);
|
|
87
|
+
}
|
|
88
|
+
return ctx;
|
|
89
|
+
}
|
|
90
|
+
/** Fetch with retry-on-401 (re-bootstraps auth once) */
|
|
91
|
+
async function fetchJsonWithRetry(county, buildUrl) {
|
|
92
|
+
let ctx = await getAuth(county);
|
|
93
|
+
let headers = buildHeaders(county, ctx);
|
|
94
|
+
const url = buildUrl(ctx.CustomerId);
|
|
95
|
+
const { data, status } = await fetchJson(url, headers);
|
|
96
|
+
if (data !== null)
|
|
97
|
+
return data;
|
|
98
|
+
// Only re-bootstrap on auth errors (401/403), not other failures
|
|
99
|
+
if (status === 401 || status === 403) {
|
|
100
|
+
const freshCtx = await rebootstrap(county.slug);
|
|
101
|
+
if (!freshCtx)
|
|
102
|
+
return null;
|
|
103
|
+
ctx = freshCtx;
|
|
104
|
+
headers = buildHeaders(county, ctx);
|
|
105
|
+
const retry = await fetchJson(buildUrl(ctx.CustomerId), headers);
|
|
106
|
+
return retry.data;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// ============================================================
|
|
111
|
+
// Data caches with size limits (filers + contributions + distributions)
|
|
112
|
+
// ============================================================
|
|
113
|
+
const MAX_CACHE_ENTRIES = 8; // Prevent unbounded memory growth (~250MB max with large counties)
|
|
114
|
+
function cachedGet(cache, key) {
|
|
115
|
+
const entry = cache.get(key);
|
|
116
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS)
|
|
117
|
+
return entry.data;
|
|
118
|
+
if (entry)
|
|
119
|
+
cache.delete(key);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function cachedSet(cache, key, data) {
|
|
123
|
+
// Evict oldest entry if at capacity
|
|
124
|
+
if (cache.size >= MAX_CACHE_ENTRIES) {
|
|
125
|
+
let oldestKey = null;
|
|
126
|
+
let oldestTime = Infinity;
|
|
127
|
+
for (const [k, v] of cache) {
|
|
128
|
+
if (v.timestamp < oldestTime) {
|
|
129
|
+
oldestKey = k;
|
|
130
|
+
oldestTime = v.timestamp;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (oldestKey)
|
|
134
|
+
cache.delete(oldestKey);
|
|
135
|
+
}
|
|
136
|
+
cache.set(key, { data, timestamp: Date.now() });
|
|
137
|
+
}
|
|
138
|
+
const filerCache = new Map();
|
|
139
|
+
const contributionCache = new Map();
|
|
140
|
+
const distributionCache = new Map();
|
|
141
|
+
// ============================================================
|
|
142
|
+
// Public API functions
|
|
143
|
+
// ============================================================
|
|
144
|
+
/** Get all filers and their documents for a county */
|
|
145
|
+
export async function getFilers(county) {
|
|
146
|
+
const cached = cachedGet(filerCache, county.slug);
|
|
147
|
+
if (cached)
|
|
148
|
+
return cached;
|
|
149
|
+
const data = await fetchJsonWithRetry(county, (tenantId) => `${ECF_BASE}/filer/documentsearch/${tenantId}`);
|
|
150
|
+
const filers = data || [];
|
|
151
|
+
cachedSet(filerCache, county.slug, filers);
|
|
152
|
+
return filers;
|
|
153
|
+
}
|
|
154
|
+
/** Get offices for a county */
|
|
155
|
+
export async function getOffices(county) {
|
|
156
|
+
const data = await fetchJsonWithRetry(county, (tenantId) => `${ECF_BASE}/filer/offices/${tenantId}`);
|
|
157
|
+
return data || [];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get ALL contributions for a county (cached 10 min).
|
|
161
|
+
*
|
|
162
|
+
* The EasyVote API ignores all query parameters on the contributions endpoint —
|
|
163
|
+
* it always returns the full dataset. All filtering is done client-side.
|
|
164
|
+
* Large counties (Shelby: ~29MB, Davidson: ~25K records) will be cached after
|
|
165
|
+
* the first fetch to avoid repeated large downloads.
|
|
166
|
+
*/
|
|
167
|
+
export async function getAllContributions(county) {
|
|
168
|
+
const cached = cachedGet(contributionCache, county.slug);
|
|
169
|
+
if (cached)
|
|
170
|
+
return cached;
|
|
171
|
+
const data = await fetchJsonWithRetry(county, (tenantId) => `${ECF_BASE}/advancedsearch/contributions/${tenantId}`);
|
|
172
|
+
const contributions = data || [];
|
|
173
|
+
cachedSet(contributionCache, county.slug, contributions);
|
|
174
|
+
return contributions;
|
|
175
|
+
}
|
|
176
|
+
/** Search contributions with client-side filtering (API ignores query params) */
|
|
177
|
+
export async function searchContributions(county, params) {
|
|
178
|
+
const all = await getAllContributions(county);
|
|
179
|
+
return all.filter(c => {
|
|
180
|
+
if (params.donorLastName) {
|
|
181
|
+
const donor = (c.ContributorLastName || '').toLowerCase();
|
|
182
|
+
if (!donor.includes(params.donorLastName.toLowerCase()))
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (params.donorFirstName) {
|
|
186
|
+
const donor = (c.ContributorFirstName || '').toLowerCase();
|
|
187
|
+
if (!donor.includes(params.donorFirstName.toLowerCase()))
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (params.recipientLastName) {
|
|
191
|
+
const recip = (c.RecipientLastName || '').toLowerCase();
|
|
192
|
+
const committee = (c.RecipientCommitteeName || '').toLowerCase();
|
|
193
|
+
if (!recip.includes(params.recipientLastName.toLowerCase()) &&
|
|
194
|
+
!committee.includes(params.recipientLastName.toLowerCase()))
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (params.recipientFirstName) {
|
|
198
|
+
const recip = (c.RecipientFirstName || '').toLowerCase();
|
|
199
|
+
if (!recip.includes(params.recipientFirstName.toLowerCase()))
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get ALL distributions/expenditures for a county (cached 10 min).
|
|
207
|
+
* Same pattern as contributions — API likely ignores query params.
|
|
208
|
+
*/
|
|
209
|
+
export async function getAllDistributions(county) {
|
|
210
|
+
const cached = cachedGet(distributionCache, county.slug);
|
|
211
|
+
if (cached)
|
|
212
|
+
return cached;
|
|
213
|
+
const data = await fetchJsonWithRetry(county, (tenantId) => `${ECF_BASE}/advancedsearch/distributions/${tenantId}`);
|
|
214
|
+
const distributions = data || [];
|
|
215
|
+
cachedSet(distributionCache, county.slug, distributions);
|
|
216
|
+
return distributions;
|
|
217
|
+
}
|
|
218
|
+
/** Search distributions with client-side filtering */
|
|
219
|
+
export async function searchDistributions(county, params) {
|
|
220
|
+
const all = await getAllDistributions(county);
|
|
221
|
+
return all.filter(d => {
|
|
222
|
+
if (params.payeeLastName) {
|
|
223
|
+
const payee = (d.PayeeLastName || '').toLowerCase();
|
|
224
|
+
const org = (d.PayeeOrganizationName || '').toLowerCase();
|
|
225
|
+
if (!payee.includes(params.payeeLastName.toLowerCase()) &&
|
|
226
|
+
!org.includes(params.payeeLastName.toLowerCase()))
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (params.payeeFirstName) {
|
|
230
|
+
const payee = (d.PayeeFirstName || '').toLowerCase();
|
|
231
|
+
if (!payee.includes(params.payeeFirstName.toLowerCase()))
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (params.candidateLastName) {
|
|
235
|
+
const cand = (d.CandidateLastName || '').toLowerCase();
|
|
236
|
+
const committee = (d.CandidateCommitteeName || '').toLowerCase();
|
|
237
|
+
if (!cand.includes(params.candidateLastName.toLowerCase()) &&
|
|
238
|
+
!committee.includes(params.candidateLastName.toLowerCase()))
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (params.candidateFirstName) {
|
|
242
|
+
const cand = (d.CandidateFirstName || '').toLowerCase();
|
|
243
|
+
if (!cand.includes(params.candidateFirstName.toLowerCase()))
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Get election documents for a county */
|
|
250
|
+
export async function getElectionDocuments(county) {
|
|
251
|
+
const data = await fetchJsonWithRetry(county, (tenantId) => `${ECF_BASE}/documents/elections/${tenantId}`);
|
|
252
|
+
return data || [];
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=easyvote-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easyvote-client.js","sourceRoot":"","sources":["../../src/api/easyvote-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
// Test the sanitizeJson logic directly (replicated here since it's private)
|
|
3
|
+
function sanitizeJson(raw) {
|
|
4
|
+
return raw.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
5
|
+
}
|
|
6
|
+
describe('sanitizeJson', () => {
|
|
7
|
+
it('passes through clean JSON unchanged', () => {
|
|
8
|
+
const clean = '{"name": "John Smith", "amount": 100}';
|
|
9
|
+
expect(sanitizeJson(clean)).toBe(clean);
|
|
10
|
+
});
|
|
11
|
+
it('strips null bytes', () => {
|
|
12
|
+
const dirty = '{"name": "John\x00Smith"}';
|
|
13
|
+
expect(sanitizeJson(dirty)).toBe('{"name": "JohnSmith"}');
|
|
14
|
+
});
|
|
15
|
+
it('strips control characters but preserves tabs and newlines', () => {
|
|
16
|
+
const input = '{"a":\t"b",\n"c":\r\n"d\x01\x02\x03"}';
|
|
17
|
+
const expected = '{"a":\t"b",\n"c":\r\n"d"}';
|
|
18
|
+
expect(sanitizeJson(input)).toBe(expected);
|
|
19
|
+
});
|
|
20
|
+
it('handles Knox County control char pattern', () => {
|
|
21
|
+
// Knox County responses have been observed with embedded control chars
|
|
22
|
+
const input = '{"filerid":"abc\x05def","name":"Test"}';
|
|
23
|
+
const result = sanitizeJson(input);
|
|
24
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
25
|
+
expect(JSON.parse(result).filerid).toBe('abcdef');
|
|
26
|
+
});
|
|
27
|
+
it('handles empty string', () => {
|
|
28
|
+
expect(sanitizeJson('')).toBe('');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('header construction', () => {
|
|
32
|
+
it('origin URL follows expected pattern', () => {
|
|
33
|
+
const slug = 'knoxcountytn';
|
|
34
|
+
const origin = `https://${slug}.easyvotecampaignfinance.com`;
|
|
35
|
+
expect(origin).toBe('https://knoxcountytn.easyvotecampaignfinance.com');
|
|
36
|
+
});
|
|
37
|
+
it('auth header follows pipe-delimited format', () => {
|
|
38
|
+
const userId = 'B7E5F78C-AB24-4799-A754-39FD501306DD';
|
|
39
|
+
const customerId = '4BB07902-FDCA-4D3B-BF6F-EA8F3A3E99A4';
|
|
40
|
+
const header = `UserId:${userId}|CustomerId:${customerId}|ZumoToken:null`;
|
|
41
|
+
expect(header).toContain('UserId:');
|
|
42
|
+
expect(header).toContain('|CustomerId:');
|
|
43
|
+
expect(header).toContain('|ZumoToken:null');
|
|
44
|
+
expect(header.split('|')).toHaveLength(3);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=easyvote-client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easyvote-client.test.js","sourceRoot":"","sources":["../../src/api/easyvote-client.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE7C,4EAA4E;AAC5E,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG,CAAC,OAAO,CAAC,+BAA+B,EAAE,EAAE,CAAC,CAAA;AACzD,CAAC;AAED,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,KAAK,GAAG,uCAAuC,CAAA;QACrD,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,KAAK,GAAG,2BAA2B,CAAA;QACzC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,KAAK,GAAG,uCAAuC,CAAA;QACrD,MAAM,QAAQ,GAAG,2BAA2B,CAAA;QAC5C,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,uEAAuE;QACvE,MAAM,KAAK,GAAG,wCAAwC,CAAA;QACtD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QAC9C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,IAAI,GAAG,cAAc,CAAA;QAC3B,MAAM,MAAM,GAAG,WAAW,IAAI,8BAA8B,CAAA;QAC5D,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAA;IACzE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,MAAM,GAAG,sCAAsC,CAAA;QACrD,MAAM,UAAU,GAAG,sCAAsC,CAAA;QACzD,MAAM,MAAM,GAAG,UAAU,MAAM,eAAe,UAAU,iBAAiB,CAAA;QACzE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;QACnC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;QACxC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;QAC3C,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF financial data extraction.
|
|
3
|
+
*
|
|
4
|
+
* Primary: Gemini Flash (cheap, fast, handles PDFs natively)
|
|
5
|
+
* Fallback: Download PDF → pdftoppm → JPEG images → return as base64
|
|
6
|
+
* for Claude to read directly via MCP image content.
|
|
7
|
+
*
|
|
8
|
+
* The fallback works because these are scanned PDFs (pdftotext returns empty).
|
|
9
|
+
*/
|
|
10
|
+
import type { SumnerReportData } from '../types.js';
|
|
11
|
+
export interface PdfImageFallback {
|
|
12
|
+
type: 'image_fallback';
|
|
13
|
+
pageCount: number;
|
|
14
|
+
images: Array<{
|
|
15
|
+
page: number;
|
|
16
|
+
base64: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
}>;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function extractReportFromPdf(pdfUrl: string, reportName: string): Promise<SumnerReportData | PdfImageFallback>;
|
|
22
|
+
/** Check if a result is the image fallback (not structured data) */
|
|
23
|
+
export declare function isImageFallback(result: SumnerReportData | PdfImageFallback): result is PdfImageFallback;
|