@cliwant/mcp-sam-gov 1.2.0 → 1.4.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.
Files changed (91) hide show
  1. package/README.ja.md +22 -9
  2. package/README.ko.md +22 -9
  3. package/README.md +70 -12
  4. package/dist/bea.d.ts +105 -0
  5. package/dist/bea.d.ts.map +1 -0
  6. package/dist/bea.js +303 -0
  7. package/dist/bea.js.map +1 -0
  8. package/dist/census-economic.d.ts +1 -1
  9. package/dist/census-economic.d.ts.map +1 -1
  10. package/dist/census-economic.js +12 -6
  11. package/dist/census-economic.js.map +1 -1
  12. package/dist/cms-facility.d.ts +112 -0
  13. package/dist/cms-facility.d.ts.map +1 -0
  14. package/dist/cms-facility.js +311 -0
  15. package/dist/cms-facility.js.map +1 -0
  16. package/dist/cms-hospital.d.ts +105 -0
  17. package/dist/cms-hospital.d.ts.map +1 -0
  18. package/dist/cms-hospital.js +290 -0
  19. package/dist/cms-hospital.js.map +1 -0
  20. package/dist/cms-supplier.d.ts +133 -0
  21. package/dist/cms-supplier.d.ts.map +1 -0
  22. package/dist/cms-supplier.js +414 -0
  23. package/dist/cms-supplier.js.map +1 -0
  24. package/dist/cms-utilization.d.ts +113 -0
  25. package/dist/cms-utilization.d.ts.map +1 -0
  26. package/dist/cms-utilization.js +328 -0
  27. package/dist/cms-utilization.js.map +1 -0
  28. package/dist/courtlistener.d.ts +115 -0
  29. package/dist/courtlistener.d.ts.map +1 -0
  30. package/dist/courtlistener.js +398 -0
  31. package/dist/courtlistener.js.map +1 -0
  32. package/dist/cpsc.d.ts +81 -0
  33. package/dist/cpsc.d.ts.map +1 -0
  34. package/dist/cpsc.js +283 -0
  35. package/dist/cpsc.js.map +1 -0
  36. package/dist/dol.d.ts +118 -0
  37. package/dist/dol.d.ts.map +1 -0
  38. package/dist/dol.js +421 -0
  39. package/dist/dol.js.map +1 -0
  40. package/dist/epa-envirofacts.d.ts +97 -0
  41. package/dist/epa-envirofacts.d.ts.map +1 -0
  42. package/dist/epa-envirofacts.js +292 -0
  43. package/dist/epa-envirofacts.js.map +1 -0
  44. package/dist/fred.d.ts +1 -1
  45. package/dist/fred.js +1 -1
  46. package/dist/keys.d.ts +11 -8
  47. package/dist/keys.d.ts.map +1 -1
  48. package/dist/keys.js +55 -8
  49. package/dist/keys.js.map +1 -1
  50. package/dist/lda.d.ts +105 -0
  51. package/dist/lda.d.ts.map +1 -0
  52. package/dist/lda.js +317 -0
  53. package/dist/lda.js.map +1 -0
  54. package/dist/nhtsa.d.ts +91 -0
  55. package/dist/nhtsa.d.ts.map +1 -0
  56. package/dist/nhtsa.js +263 -0
  57. package/dist/nhtsa.js.map +1 -0
  58. package/dist/nonprofit.d.ts +116 -0
  59. package/dist/nonprofit.d.ts.map +1 -0
  60. package/dist/nonprofit.js +342 -0
  61. package/dist/nonprofit.js.map +1 -0
  62. package/dist/openfda-device.d.ts +85 -0
  63. package/dist/openfda-device.d.ts.map +1 -0
  64. package/dist/openfda-device.js +277 -0
  65. package/dist/openfda-device.js.map +1 -0
  66. package/dist/openfda.d.ts +133 -0
  67. package/dist/openfda.d.ts.map +1 -0
  68. package/dist/openfda.js +402 -0
  69. package/dist/openfda.js.map +1 -0
  70. package/dist/server.d.ts.map +1 -1
  71. package/dist/server.js +872 -6
  72. package/dist/server.js.map +1 -1
  73. package/package.json +2 -1
  74. package/src/bea.ts +372 -0
  75. package/src/census-economic.ts +12 -6
  76. package/src/cms-facility.ts +379 -0
  77. package/src/cms-hospital.ts +344 -0
  78. package/src/cms-supplier.ts +527 -0
  79. package/src/cms-utilization.ts +389 -0
  80. package/src/courtlistener.ts +465 -0
  81. package/src/cpsc.ts +333 -0
  82. package/src/dol.ts +515 -0
  83. package/src/epa-envirofacts.ts +342 -0
  84. package/src/fred.ts +1 -1
  85. package/src/keys.ts +60 -8
  86. package/src/lda.ts +385 -0
  87. package/src/nhtsa.ts +352 -0
  88. package/src/nonprofit.ts +460 -0
  89. package/src/openfda-device.ts +356 -0
  90. package/src/openfda.ts +495 -0
  91. package/src/server.ts +995 -6
@@ -0,0 +1,342 @@
1
+ /**
2
+ * nonprofit.ts — US TAX-EXEMPT NONPROFITS (IRS Form 990) — the nonprofit /
3
+ * grantee / subcontractor vetting lane (ADR-0060). Who a tax-exempt organization
4
+ * IS (EIN, NTEE code, subsection, ruling date, status) and what its Form 990
5
+ * FINANCIALS look like (revenue, expenses, assets, liabilities by tax year) —
6
+ * the 501(c) signal no contract/spending/grant/lobbying source carries.
7
+ *
8
+ * ★ PROVENANCE — THIS IS NOT A .gov API (must be disclosed). The DATA is IRS Form
9
+ * 990 filings — FEDERAL tax-exempt PUBLIC RECORDS — but the API is **ProPublica
10
+ * Nonprofit Explorer**, operated by **ProPublica** (a non-profit newsroom) which
11
+ * republishes those records KEYLESS. The IRS itself offers NO clean query API
12
+ * (only bulk downloads / a web UI). So every response's `_meta.source` AND a note
13
+ * name "IRS Form 990 data via ProPublica Nonprofit Explorer" — the tool NEVER
14
+ * presents itself as a government API.
15
+ *
16
+ * ★ KEYLESS — no key of any kind. Anonymous GETs return HTTP 200. There is NO
17
+ * KEY_REGISTRY / keys.ts / API_KEYS.md entry for this source.
18
+ *
19
+ * The module writes ZERO fetch/coercion/error/meta code of its own: it REUSES
20
+ * `getJson` (the shared fetch envelope, redirect:"error") / `driftError` /
21
+ * `num`·`str` (coerce.ts, null-never-0/empty) / `withMeta`·`buildMeta`.
22
+ *
23
+ * SEARCH GET https://projects.propublica.org/nonprofits/api/v2/search.json
24
+ * ?q=&state[id]=&ntee[id]=&page=
25
+ * → { total_results, organizations:[{ ein, name, sub_name, city, state,
26
+ * ntee_code, subseccd, score }], num_pages, cur_page, per_page,
27
+ * page_offset }
28
+ * DETAIL GET https://projects.propublica.org/nonprofits/api/v2/organizations/{ein}.json
29
+ * → { organization:{ ein, name, address, city, state, zipcode, ntee_code,
30
+ * subsection_code, ruling_date, exempt_organization_status_code,
31
+ * foundation_code }, filings_with_data:[{ tax_prd_yr, formtype, pdf_url,
32
+ * totrevenue, totfuncexpns, totassetsend, totliabend }] }
33
+ *
34
+ * ★ HONESTY (ADR-0060 P1–P5):
35
+ * [P1] SEARCH totalAvailable = `total_results` (the API's REAL total for the
36
+ * query) — NEVER organizations.length. Page pagination (page is 0-based):
37
+ * hasMore = (cur_page+1) < num_pages; the next page number is surfaced in
38
+ * a note. DETAIL totalAvailable = filings.length (the COMPLETE filing set
39
+ * from the one detail doc — no pagination). Reverting the search total to
40
+ * organizations.length must go RED.
41
+ * [P2] SEARCH a genuine no-match (organizations:[]) ⇒ honest empty (returned:0,
42
+ * complete:true). DETAIL an unknown EIN (HTTP 404) ⇒ not_found (NEVER a
43
+ * fabricated empty org). A 4xx ⇒ invalid_input; a 5xx/timeout ⇒
44
+ * upstream_unavailable THROW; a 200 non-JSON ⇒ schema_drift.
45
+ * [P3] The four Form 990 figures (totrevenue/totfuncexpns/totassetsend/
46
+ * totliabend) ride `num()` — a genuine 0 STAYS 0, an absent figure ⇒ null
47
+ * (NEVER 0-faked). EIN + the codes are strings; ruling_date is a string.
48
+ * [P4] SEARCH `organizations` non-array OR `total_results` non-number ⇒
49
+ * driftError. DETAIL `organization` non-object OR `filings_with_data`
50
+ * non-array ⇒ driftError (never a fabricated empty/total).
51
+ * [SSRF] fixed host `projects.propublica.org`; a post-construction hostname/
52
+ * protocol assert + `redirect:"error"`; the query VALUES ride
53
+ * URLSearchParams (incl. the `state[id]`/`ntee[id]` bracket keys);
54
+ * `ein` charclass `^\d{1,9}$` (path segment); `state` `^[A-Za-z]{2}$`;
55
+ * `ntee` an integer 1..10.
56
+ */
57
+ import { ToolErrorCarrier } from "./errors.js";
58
+ import { getJson, driftError } from "./datasource.js";
59
+ import { num, str } from "./coerce.js";
60
+ import { withMeta } from "./meta.js";
61
+ // Re-export the shared honesty coercion (single audited copy in ./coerce.js —
62
+ // ADR-0005 v2 FIX-C) so a `num` regression fails together across sources.
63
+ export { num };
64
+ // ─── SSRF core: the single fixed host + base path ─────────────────
65
+ export const NONPROFIT_HOST = "projects.propublica.org";
66
+ const NONPROFIT_BASE = "/nonprofits/api/v2";
67
+ // HOST+path labels — surface in ToolError.upstreamEndpoint. No token exists for
68
+ // this keyless source, so no secret can ever appear here.
69
+ const NONPROFIT_SEARCH_LABEL = "propublica-nonprofit:/nonprofits/api/v2/search";
70
+ const NONPROFIT_ORG_LABEL = "propublica-nonprofit:/nonprofits/api/v2/organizations";
71
+ // ─── Validation (SSRF + "verify the input" honesty) ───────────────
72
+ const STATE_RE = /^[A-Za-z]{2}$/; // a 2-letter US state/territory code
73
+ const EIN_RE = /^\d{1,9}$/; // a numeric EIN (1..9 digits), rides the PATH
74
+ // ★ProPublica's not-found SENTINEL (live-verified, NOT in the ADR): an EIN with no
75
+ // matching IRS record does NOT always 404 — an in-range unknown EIN (e.g. 999999999)
76
+ // returns HTTP 200 carrying a SYNTHETIC placeholder org `{ name:"Unknown Organization",
77
+ // …all-null }` with ZERO filings_with_data. Surfacing that verbatim would present a
78
+ // FABRICATED empty org as a real hit (a P2 honesty violation). We detect the exact
79
+ // sentinel name + empty structured filings and map it to not_found, EXACTLY like a 404.
80
+ const PROPUBLICA_NOT_FOUND_NAME = "Unknown Organization";
81
+ const NTEE_MIN = 1;
82
+ const NTEE_MAX = 10; // the NTEE major-category filter, 1..10
83
+ const DEFAULT_PAGE = 0; // the API's page is 0-BASED
84
+ const FALLBACK_PER_PAGE = 25; // the API's fixed page size (~25); a defensive fallback
85
+ // ─── Honesty notes (ADR-0060 required set) ────────────────────────
86
+ const PROVENANCE_NOTE = "Data = IRS Form 990 filings (federal tax-exempt public records), served by ProPublica Nonprofit Explorer (ProPublica, a non-profit newsroom, which republishes them keyless) — NOT a .gov API. The IRS itself has no clean query API (only bulk downloads / a web UI). Treat figures as of ProPublica's last IRS ingest.";
87
+ const SEARCH_TOTAL_NOTE = "totalAvailable is the API's real total_results — the total match count for the query (NOT the organizations on this page). Pagination is page-based and 0-INDEXED (pass page=cur_page+1 for the next page while hasMore).";
88
+ const FINANCIALS_TOTAL_NOTE = "totalAvailable is filings.length — the COMPLETE set of Form 990 filings-with-data carried by this organization's detail document (there is no pagination; this is the whole set, not a page).";
89
+ const FINANCIALS_MONEY_NOTE = "revenueUsd / expensesUsd / assetsUsd / liabilitiesUsd are parsed from the Form 990 totrevenue / totfuncexpns / totassetsend / totliabend. A genuine reported 0 is preserved as 0; an absent figure maps to null — NEVER 0.";
90
+ /** Map ONE search `organizations[]` row → the curated summary shape. */
91
+ function mapOrgSummary(raw) {
92
+ const o = (raw ?? {});
93
+ return {
94
+ // EIN + codes are IDENTIFIERS ⇒ strings (never num-coerced).
95
+ ein: str(o.ein),
96
+ name: str(o.name),
97
+ city: str(o.city),
98
+ state: str(o.state),
99
+ nteeCode: str(o.ntee_code),
100
+ subsectionCode: str(o.subseccd),
101
+ };
102
+ }
103
+ /** Map the detail `organization` object → the curated organization shape. */
104
+ function mapOrganization(raw) {
105
+ const o = (raw ?? {});
106
+ return {
107
+ ein: str(o.ein),
108
+ name: str(o.name),
109
+ address: str(o.address),
110
+ city: str(o.city),
111
+ state: str(o.state),
112
+ zip: str(o.zipcode),
113
+ nteeCode: str(o.ntee_code),
114
+ subsectionCode: str(o.subsection_code),
115
+ rulingDate: str(o.ruling_date),
116
+ statusCode: str(o.exempt_organization_status_code),
117
+ };
118
+ }
119
+ /** Map ONE `filings_with_data[]` row → the curated filing shape (money via num). */
120
+ function mapFiling(raw) {
121
+ const f = (raw ?? {});
122
+ return {
123
+ taxYear: num(f.tax_prd_yr),
124
+ formType: str(f.formtype),
125
+ // [P3] a genuine 0 STAYS 0; absent ⇒ null (NEVER 0-faked).
126
+ revenueUsd: num(f.totrevenue),
127
+ expensesUsd: num(f.totfuncexpns),
128
+ assetsUsd: num(f.totassetsend),
129
+ liabilitiesUsd: num(f.totliabend),
130
+ pdfUrl: str(f.pdf_url),
131
+ };
132
+ }
133
+ /**
134
+ * Search US tax-exempt nonprofits (IRS Form 990) via ProPublica Nonprofit Explorer
135
+ * (`/nonprofits/api/v2/search.json`) → curated org summaries + honest `_meta`.
136
+ * KEYLESS. ★PROVENANCE: this is ProPublica (a non-profit newsroom) republishing
137
+ * IRS Form 990 public records — NOT a .gov API. ★totalAvailable is the API's REAL
138
+ * `total_results` — never organizations.length; page-based (0-indexed) pagination.
139
+ */
140
+ export async function search(args) {
141
+ const label = NONPROFIT_SEARCH_LABEL;
142
+ // ── Validate + default (belt-and-suspenders behind the server Zod; a DIRECT
143
+ // handler call bypasses Zod). state/ntee/page are charclass/range-guarded;
144
+ // the free-text query rides URLSearchParams (encoded). ──
145
+ if (args.state !== undefined && !STATE_RE.test(args.state)) {
146
+ throw new ToolErrorCarrier({
147
+ kind: "invalid_input",
148
+ retryable: false,
149
+ message: `Invalid state ${JSON.stringify(args.state)} — expected a 2-letter US state/territory code (^[A-Za-z]{2}$), e.g. "VA".`,
150
+ upstreamEndpoint: label,
151
+ });
152
+ }
153
+ if (args.ntee !== undefined &&
154
+ (!Number.isInteger(args.ntee) || args.ntee < NTEE_MIN || args.ntee > NTEE_MAX)) {
155
+ throw new ToolErrorCarrier({
156
+ kind: "invalid_input",
157
+ retryable: false,
158
+ message: `Invalid ntee ${JSON.stringify(args.ntee)} — expected an integer NTEE major category 1..10.`,
159
+ upstreamEndpoint: label,
160
+ });
161
+ }
162
+ const page = clampPage(args.page);
163
+ // ── Build the query from VALIDATED typed args, key-by-key (SSRF: no raw
164
+ // passthrough; every VALUE is URLSearchParams-encoded, incl. the bracket keys
165
+ // `state[id]`/`ntee[id]`). ──
166
+ const params = new URLSearchParams();
167
+ const filtersApplied = [];
168
+ if (args.query !== undefined && args.query !== "") {
169
+ params.set("q", args.query);
170
+ filtersApplied.push("query");
171
+ }
172
+ if (args.state !== undefined) {
173
+ params.set("state[id]", args.state.toUpperCase());
174
+ filtersApplied.push("state");
175
+ }
176
+ if (args.ntee !== undefined) {
177
+ params.set("ntee[id]", String(args.ntee));
178
+ filtersApplied.push("ntee");
179
+ }
180
+ params.set("page", String(page));
181
+ const url = `https://${NONPROFIT_HOST}${NONPROFIT_BASE}/search.json?${params.toString()}`;
182
+ assertOnHost(url, label);
183
+ // ── Fetch through the shared envelope. redirect:"error" fails closed on any
184
+ // off-host 3xx. A 4xx ⇒ invalid_input; a 5xx/timeout ⇒ upstream_unavailable
185
+ // THROW; a 429 ⇒ rate_limited THROW; a 200 non-JSON ⇒ getJson's r.json()
186
+ // throws a SyntaxError ⇒ schema_drift. ──
187
+ let body;
188
+ try {
189
+ body = await getJson(url, { label, redirect: "error" });
190
+ }
191
+ catch (e) {
192
+ if (e instanceof SyntaxError) {
193
+ throw driftError(label, "ProPublica Nonprofit search returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
194
+ }
195
+ throw e; // 5xx → upstream_unavailable, 4xx → invalid_input, 429 → rate_limited …
196
+ }
197
+ // ── [P4] `organizations` MUST be an array and `total_results` MUST be a number
198
+ // (a missing/wrong-typed either is drift, never a fabricated empty/total). ──
199
+ const b = (body ?? {});
200
+ if (!Array.isArray(b.organizations)) {
201
+ throw driftError(label, "ProPublica Nonprofit search shape drift — `organizations` must be an array.");
202
+ }
203
+ if (typeof b.total_results !== "number" || !Number.isFinite(b.total_results)) {
204
+ throw driftError(label, "ProPublica Nonprofit search shape drift — `total_results` (the total match count) must be a number.");
205
+ }
206
+ const organizations = b.organizations.map(mapOrgSummary);
207
+ const returned = organizations.length;
208
+ // ── [P1] totalAvailable is the API's REAL total_results, NEVER organizations.length.
209
+ // Page-based + 0-INDEXED: hasMore = (cur_page+1) < num_pages; surface the next
210
+ // page. cur_page/num_pages/per_page/page_offset via num() (defensive fallbacks). ──
211
+ const totalAvailable = b.total_results;
212
+ const curPage = num(b.cur_page) ?? page;
213
+ const numPages = num(b.num_pages);
214
+ const perPage = num(b.per_page) ?? (returned > 0 ? returned : FALLBACK_PER_PAGE);
215
+ const hasMore = numPages !== null ? curPage + 1 < numPages : false;
216
+ const offset = num(b.page_offset) ?? curPage * perPage;
217
+ const nextOffset = hasMore ? (curPage + 1) * perPage : null;
218
+ const notes = [PROVENANCE_NOTE, SEARCH_TOTAL_NOTE];
219
+ if (hasMore && numPages !== null) {
220
+ notes.push(`This is page ${curPage} (0-indexed) of ${numPages} — pass page=${curPage + 1} for the next page.`);
221
+ }
222
+ return withMeta({ organizations }, {
223
+ source: `${NONPROFIT_HOST} /nonprofits/api/v2/search (IRS Form 990 data via ProPublica Nonprofit Explorer — not a .gov API; keyless)`,
224
+ keylessMode: true,
225
+ returned,
226
+ totalAvailable,
227
+ filtersApplied,
228
+ filtersDropped: [],
229
+ fieldsUnavailable: [],
230
+ pagination: { offset, limit: perPage, hasMore, nextOffset },
231
+ notes,
232
+ });
233
+ }
234
+ /**
235
+ * Fetch ONE nonprofit's IRS Form 990 profile + financials via ProPublica Nonprofit
236
+ * Explorer (`/nonprofits/api/v2/organizations/{ein}.json`) → curated organization +
237
+ * filings + honest `_meta`. KEYLESS. ★PROVENANCE: ProPublica (a non-profit newsroom)
238
+ * republishing IRS Form 990 public records — NOT a .gov API. An unknown EIN (HTTP
239
+ * 404) ⇒ not_found (never a fabricated empty org). The four Form 990 figures ride
240
+ * num() (null-never-0). totalAvailable = filings.length (the COMPLETE set).
241
+ */
242
+ export async function financials(args) {
243
+ const label = NONPROFIT_ORG_LABEL;
244
+ // ── Validate (belt-and-suspenders behind the server Zod). ein is charclass-
245
+ // guarded PRE-fetch — it rides the URL PATH, so it MUST be digits-only. ──
246
+ if (typeof args.ein !== "string" || !EIN_RE.test(args.ein)) {
247
+ throw new ToolErrorCarrier({
248
+ kind: "invalid_input",
249
+ retryable: false,
250
+ message: `Invalid ein ${JSON.stringify(args.ein)} — expected a numeric EIN of 1..9 digits (^\\d{1,9}$), e.g. "530196605".`,
251
+ upstreamEndpoint: label,
252
+ });
253
+ }
254
+ // ein is digits-only (EIN_RE) ⇒ safe as a path segment; no separators can steer
255
+ // the authority. Build + re-assert the host (SSRF belt-and-suspenders).
256
+ const url = `https://${NONPROFIT_HOST}${NONPROFIT_BASE}/organizations/${args.ein}.json`;
257
+ assertOnHost(url, label);
258
+ // ── Fetch through the shared envelope. A 404 (unknown EIN) ⇒ not_found (the
259
+ // shared taxonomy — never a fabricated empty org); a 4xx ⇒ invalid_input; a
260
+ // 5xx/timeout ⇒ upstream_unavailable THROW; a 200 non-JSON ⇒ schema_drift. ──
261
+ let body;
262
+ try {
263
+ body = await getJson(url, { label, redirect: "error" });
264
+ }
265
+ catch (e) {
266
+ if (e instanceof SyntaxError) {
267
+ throw driftError(label, "ProPublica Nonprofit organization detail returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
268
+ }
269
+ throw e; // 404 → not_found, 5xx → upstream_unavailable, 4xx → invalid_input …
270
+ }
271
+ // ── [P4] `organization` MUST be an object and `filings_with_data` MUST be an
272
+ // array (a missing/wrong-typed either is drift, never a fabricated empty). ──
273
+ const b = (body ?? {});
274
+ if (b.organization === null ||
275
+ typeof b.organization !== "object" ||
276
+ Array.isArray(b.organization)) {
277
+ throw driftError(label, "ProPublica Nonprofit organization detail shape drift — `organization` must be an object.");
278
+ }
279
+ if (!Array.isArray(b.filings_with_data)) {
280
+ throw driftError(label, "ProPublica Nonprofit organization detail shape drift — `filings_with_data` must be an array.");
281
+ }
282
+ const organization = mapOrganization(b.organization);
283
+ const filings = b.filings_with_data.map(mapFiling);
284
+ const returned = filings.length;
285
+ // ── [P2] ★not-found SENTINEL: ProPublica returns HTTP 200 + a synthetic
286
+ // `{ name:"Unknown Organization", …all-null }` placeholder (zero
287
+ // filings_with_data) for an in-range EIN with no IRS record. That is a
288
+ // FABRICATED empty org — surface it as not_found (identical to a 404), NEVER
289
+ // as a real hit. Gated on BOTH the exact sentinel name AND empty structured
290
+ // filings, so a real org (which would carry its true name / filings) is safe. ──
291
+ if (organization.name === PROPUBLICA_NOT_FOUND_NAME && returned === 0) {
292
+ throw new ToolErrorCarrier({
293
+ kind: "not_found",
294
+ retryable: false,
295
+ message: `No IRS Form 990 record for EIN ${args.ein} — ProPublica returned its "${PROPUBLICA_NOT_FOUND_NAME}" placeholder (no matching tax-exempt organization). Verify the EIN.`,
296
+ upstreamEndpoint: label,
297
+ });
298
+ }
299
+ // ── [P1] totalAvailable = filings.length — the COMPLETE filing set from the one
300
+ // detail document (no pagination). ──
301
+ const notes = [
302
+ PROVENANCE_NOTE,
303
+ FINANCIALS_TOTAL_NOTE,
304
+ FINANCIALS_MONEY_NOTE,
305
+ ];
306
+ return withMeta({ organization, filings }, {
307
+ source: `${NONPROFIT_HOST} /nonprofits/api/v2/organizations (IRS Form 990 data via ProPublica Nonprofit Explorer — not a .gov API; keyless)`,
308
+ keylessMode: true,
309
+ returned,
310
+ totalAvailable: returned,
311
+ filtersApplied: [],
312
+ filtersDropped: [],
313
+ fieldsUnavailable: [],
314
+ // The complete set is in one document — no pagination, hasMore:false.
315
+ pagination: { offset: 0, limit: returned, hasMore: false, nextOffset: null },
316
+ notes,
317
+ });
318
+ }
319
+ // ─── SSRF host assert (shared by both tools) ──────────────────────
320
+ /**
321
+ * Belt-and-suspenders: the fixed host + strictly-built URL leave nothing to steer
322
+ * the authority; assert the built URL cannot have been moved off-host / downgraded.
323
+ */
324
+ function assertOnHost(url, label) {
325
+ const built = new URL(url);
326
+ if (built.hostname !== NONPROFIT_HOST || built.protocol !== "https:") {
327
+ throw new ToolErrorCarrier({
328
+ kind: "invalid_input",
329
+ retryable: false,
330
+ message: `Constructed ProPublica Nonprofit URL host ${JSON.stringify(built.hostname)} (${built.protocol}) is not ${NONPROFIT_HOST} over https — refusing to fetch (SSRF safety).`,
331
+ upstreamEndpoint: label,
332
+ });
333
+ }
334
+ }
335
+ // ─── Small clamp (defensive, behind the server Zod bounds) ─────────
336
+ function clampPage(v) {
337
+ if (typeof v !== "number" || !Number.isFinite(v))
338
+ return DEFAULT_PAGE;
339
+ const n = Math.floor(v);
340
+ return n < 0 ? 0 : n;
341
+ }
342
+ //# sourceMappingURL=nonprofit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonprofit.js","sourceRoot":"","sources":["../src/nonprofit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * openfda-device.ts — openFDA 510(k) DEVICE CLEARANCES (api.fda.gov) — the
3
+ * MEDICAL-DEVICE REGULATORY lane (ADR-0056). The FDA's premarket-notification
4
+ * (510(k)) clearances: the applicant, the device, the clearance number (K-number),
5
+ * the decision (date + description), the clearance type, the product code, the
6
+ * advisory committee, and geography.
7
+ *
8
+ * ★ THIS IS THE SAME SOURCE + ENVELOPE + CRUX as openfda.ts (ADR-0054). Same host
9
+ * (api.fda.gov), same response envelope `{ meta:{ results:{ skip, limit, total }},
10
+ * results:[…] }`, same ★no-match→HTTP-404-NOT_FOUND-as-honest-empty crux, same
11
+ * optional query-key K-test, same fixed-host SSRF idiom, same structured-only
12
+ * (no raw Lucene passthrough) search assembly. It REUSES openfda.ts's `fetchOpenfda`
13
+ * (the SSRF-guarded classified fetch) and `readOpenfdaError` (the error-body reader)
14
+ * verbatim — the 404-reclassification logic is NOT reinvented — plus `luceneQuote`
15
+ * (the phrase-escape), `openfdaApiKey` (the optional-key env seam), and `OPENFDA_HOST`.
16
+ *
17
+ * GET https://api.fda.gov/device/510k.json
18
+ * ?search=<lucene>&limit=<1..100>&skip=<offset>[&api_key=<KEY>]
19
+ * → { meta:{ disclaimer, results:{ skip, limit, total }}, results:[ {…} ] }
20
+ * (live total ~175507).
21
+ *
22
+ * ★ HONESTY (mirrors openfda.ts exactly):
23
+ * [P1] totalAvailable = `meta.results.total` EXACT (e.g. 175507), NEVER
24
+ * results.length. skip/limit offset pagination:
25
+ * hasMore = skip + returned < total; nextOffset = hasMore ? skip+returned : null.
26
+ * [★P2] a 404 whose body is `{error:{code:"NOT_FOUND"}}` (a no-match query OR an
27
+ * unknown field) ⇒ HONEST EMPTY (returned:0, totalAvailable:0) — NOT thrown,
28
+ * NOT not_found. Any OTHER 4xx (e.g. a 400 syntax error) ⇒ invalid_input
29
+ * surfacing openFDA's message. 5xx/timeout ⇒ upstream_unavailable THROW. A
30
+ * 200 non-JSON body ⇒ schema_drift. (Reuses openfda.ts's exact fetch path.)
31
+ * [P3] dates (`decision_date`, YYYY-MM-DD) and every scalar surfaced as a STRING
32
+ * via `str` (null-never-empty-string) — no numeric coercion; never fabricated.
33
+ * [P4] `meta.results` or `results` absent / non-array ⇒ driftError (never a
34
+ * fabricated empty).
35
+ * [K-test] OPTIONAL OPENFDA_API_KEY: when set it rides `&api_key=` ONLY (openFDA has
36
+ * no header option — the query-key is inherent). Mitigation: the `label` is
37
+ * host+path ONLY (`openfda:/device/510k`, NO query), so no token reaches
38
+ * ToolError.upstreamEndpoint; `_meta.source` names the MODE only; the key is
39
+ * ABSENT from the serialized {data,_meta}, notes, and any log. Unset ⇒ keyless.
40
+ * [SSRF] fixed host `api.fda.gov` (fetchOpenfda asserts hostname/protocol +
41
+ * redirect:"error"); all filter VALUES are Lucene-escaped + phrase-quoted and
42
+ * ride URLSearchParams `search=`; limit/skip are integers; state is charclass
43
+ * `^[A-Za-z]{2}$`. No raw Lucene passthrough — structured only, injection-safe.
44
+ */
45
+ import { type MetaBundle } from "./meta.js";
46
+ export type OpenfdaClearance = {
47
+ applicant: string | null;
48
+ deviceName: string | null;
49
+ kNumber: string | null;
50
+ decisionDate: string | null;
51
+ decisionDescription: string | null;
52
+ clearanceType: string | null;
53
+ productCode: string | null;
54
+ advisoryCommittee: string | null;
55
+ state: string | null;
56
+ };
57
+ /** The structured filter set → openFDA `field:value` clauses (510(k) fields). */
58
+ export type OpenfdaDeviceFilters = {
59
+ applicant?: string;
60
+ deviceName?: string;
61
+ productCode?: string;
62
+ clearanceType?: string;
63
+ kNumber?: string;
64
+ state?: string;
65
+ };
66
+ /**
67
+ * Assemble the openFDA `search=` Lucene string from structured 510(k) filters — each
68
+ * value Lucene-escaped + phrase-quoted (reusing openfda.ts's `luceneQuote`), joined by
69
+ * ` AND `. Returns "" when no filter is present (openFDA then returns the whole
70
+ * collection). The clause → field mapping is FIXED here; a caller can never inject a
71
+ * raw field:value (no raw Lucene passthrough).
72
+ */
73
+ export declare function buildDeviceSearch(f: OpenfdaDeviceFilters): string;
74
+ export type OpenfdaDeviceClearancesArgs = OpenfdaDeviceFilters & {
75
+ limit?: number;
76
+ skip?: number;
77
+ };
78
+ /**
79
+ * Search openFDA 510(k) DEVICE CLEARANCES with structured filters → curated clearance
80
+ * rows + honest `_meta`. KEYLESS (an OPTIONAL OPENFDA_API_KEY only raises the rate
81
+ * limit). totalAvailable = meta.results.total (EXACT); skip/limit offset pagination.
82
+ * ★A no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty, never a throw.
83
+ */
84
+ export declare function deviceClearances(args: OpenfdaDeviceClearancesArgs): Promise<MetaBundle>;
85
+ //# sourceMappingURL=openfda-device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openfda-device.d.ts","sourceRoot":"","sources":["../src/openfda-device.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AA6BzE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAmBF,iFAAiF;AACjF,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAejE;AAGD,MAAM,MAAM,2BAA2B,GAAG,oBAAoB,GAAG;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,2BAA2B,GAChC,OAAO,CAAC,UAAU,CAAC,CAyJrB"}