@cliwant/mcp-sam-gov 1.3.0 → 1.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.ja.md +20 -12
- package/README.ko.md +20 -12
- package/README.md +62 -14
- package/dist/bea.d.ts +1 -1
- package/dist/bea.js +1 -1
- package/dist/cbp-border.d.ts +51 -0
- package/dist/cbp-border.d.ts.map +1 -0
- package/dist/cbp-border.js +123 -0
- package/dist/cbp-border.js.map +1 -0
- package/dist/census-economic.d.ts +1 -1
- package/dist/census-economic.d.ts.map +1 -1
- package/dist/census-economic.js +12 -6
- package/dist/census-economic.js.map +1 -1
- package/dist/cms-facility.d.ts +112 -0
- package/dist/cms-facility.d.ts.map +1 -0
- package/dist/cms-facility.js +311 -0
- package/dist/cms-facility.js.map +1 -0
- package/dist/cms-hospital.d.ts +105 -0
- package/dist/cms-hospital.d.ts.map +1 -0
- package/dist/cms-hospital.js +290 -0
- package/dist/cms-hospital.js.map +1 -0
- package/dist/cms-supplier.d.ts +133 -0
- package/dist/cms-supplier.d.ts.map +1 -0
- package/dist/cms-supplier.js +414 -0
- package/dist/cms-supplier.js.map +1 -0
- package/dist/cms-utilization.d.ts +113 -0
- package/dist/cms-utilization.d.ts.map +1 -0
- package/dist/cms-utilization.js +328 -0
- package/dist/cms-utilization.js.map +1 -0
- package/dist/courtlistener.d.ts +115 -0
- package/dist/courtlistener.d.ts.map +1 -0
- package/dist/courtlistener.js +398 -0
- package/dist/courtlistener.js.map +1 -0
- package/dist/cpsc.d.ts +81 -0
- package/dist/cpsc.d.ts.map +1 -0
- package/dist/cpsc.js +283 -0
- package/dist/cpsc.js.map +1 -0
- package/dist/datagov-catalog.d.ts.map +1 -1
- package/dist/datagov-catalog.js +16 -2
- package/dist/datagov-catalog.js.map +1 -1
- package/dist/dol.d.ts +2 -2
- package/dist/dol.js +5 -5
- package/dist/dol.js.map +1 -1
- package/dist/ecfr.d.ts +2 -2
- package/dist/ecfr.d.ts.map +1 -1
- package/dist/ecfr.js +24 -10
- package/dist/ecfr.js.map +1 -1
- package/dist/edgar.d.ts.map +1 -1
- package/dist/edgar.js +26 -6
- package/dist/edgar.js.map +1 -1
- package/dist/epa-envirofacts.d.ts +97 -0
- package/dist/epa-envirofacts.d.ts.map +1 -0
- package/dist/epa-envirofacts.js +305 -0
- package/dist/epa-envirofacts.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -1
- package/dist/far.d.ts.map +1 -1
- package/dist/far.js +3 -1
- package/dist/far.js.map +1 -1
- package/dist/federal-register.d.ts +2 -2
- package/dist/federal-register.d.ts.map +1 -1
- package/dist/federal-register.js +26 -10
- package/dist/federal-register.js.map +1 -1
- package/dist/fema.d.ts +36 -0
- package/dist/fema.d.ts.map +1 -1
- package/dist/fema.js +124 -0
- package/dist/fema.js.map +1 -1
- package/dist/fred.d.ts +1 -1
- package/dist/fred.js +1 -1
- package/dist/gov-domains.d.ts +66 -0
- package/dist/gov-domains.d.ts.map +1 -0
- package/dist/gov-domains.js +211 -0
- package/dist/gov-domains.js.map +1 -0
- package/dist/keys.d.ts +6 -5
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +25 -6
- package/dist/keys.js.map +1 -1
- package/dist/nhtsa.d.ts +91 -0
- package/dist/nhtsa.d.ts.map +1 -0
- package/dist/nhtsa.js +263 -0
- package/dist/nhtsa.js.map +1 -0
- package/dist/nist-controls.d.ts +48 -0
- package/dist/nist-controls.d.ts.map +1 -0
- package/dist/nist-controls.js +174 -0
- package/dist/nist-controls.js.map +1 -0
- package/dist/nonprofit.d.ts +116 -0
- package/dist/nonprofit.d.ts.map +1 -0
- package/dist/nonprofit.js +342 -0
- package/dist/nonprofit.js.map +1 -0
- package/dist/nws-weather.d.ts +57 -0
- package/dist/nws-weather.d.ts.map +1 -0
- package/dist/nws-weather.js +131 -0
- package/dist/nws-weather.js.map +1 -0
- package/dist/openfda-device.d.ts +85 -0
- package/dist/openfda-device.d.ts.map +1 -0
- package/dist/openfda-device.js +277 -0
- package/dist/openfda-device.js.map +1 -0
- package/dist/openfda-drugsfda.d.ts +72 -0
- package/dist/openfda-drugsfda.d.ts.map +1 -0
- package/dist/openfda-drugsfda.js +230 -0
- package/dist/openfda-drugsfda.js.map +1 -0
- package/dist/openfda.d.ts +133 -0
- package/dist/openfda.d.ts.map +1 -0
- package/dist/openfda.js +425 -0
- package/dist/openfda.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +996 -16
- package/dist/server.js.map +1 -1
- package/dist/treasury.d.ts +2 -0
- package/dist/treasury.d.ts.map +1 -1
- package/dist/treasury.js +7 -0
- package/dist/treasury.js.map +1 -1
- package/dist/usaspending.d.ts +32 -1
- package/dist/usaspending.d.ts.map +1 -1
- package/dist/usaspending.js +143 -16
- package/dist/usaspending.js.map +1 -1
- package/package.json +3 -2
- package/src/bea.ts +1 -1
- package/src/cbp-border.ts +177 -0
- package/src/census-economic.ts +12 -6
- package/src/cms-facility.ts +379 -0
- package/src/cms-hospital.ts +344 -0
- package/src/cms-supplier.ts +527 -0
- package/src/cms-utilization.ts +389 -0
- package/src/courtlistener.ts +465 -0
- package/src/cpsc.ts +333 -0
- package/src/datagov-catalog.ts +18 -2
- package/src/dol.ts +5 -5
- package/src/ecfr.ts +27 -10
- package/src/edgar.ts +39 -7
- package/src/epa-envirofacts.ts +358 -0
- package/src/errors.ts +11 -0
- package/src/far.ts +3 -1
- package/src/federal-register.ts +29 -10
- package/src/fema.ts +139 -0
- package/src/fred.ts +1 -1
- package/src/gov-domains.ts +237 -0
- package/src/keys.ts +27 -6
- package/src/nhtsa.ts +352 -0
- package/src/nist-controls.ts +219 -0
- package/src/nonprofit.ts +460 -0
- package/src/nws-weather.ts +167 -0
- package/src/openfda-device.ts +356 -0
- package/src/openfda-drugsfda.ts +313 -0
- package/src/openfda.ts +518 -0
- package/src/server.ts +1127 -27
- package/src/treasury.ts +7 -0
- package/src/usaspending.ts +189 -17
package/dist/nhtsa.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nhtsa.ts — NHTSA VEHICLE SAFETY (api.nhtsa.gov) — the vehicle / parts /
|
|
3
|
+
* fleet supplier PRODUCT-SAFETY vetting lane (ADR-0057). Two keyless tools:
|
|
4
|
+
* • nhtsa_recalls — /recalls/recallsByVehicle?make=&model=&modelYear=
|
|
5
|
+
* • nhtsa_complaints — /complaints/complaintsByVehicle?make=&model=&modelYear=
|
|
6
|
+
* The cross-agency product-safety family alongside openFDA (medical) / CPSC
|
|
7
|
+
* (consumer goods): a manufacturer/component/safety-signal history for B2G
|
|
8
|
+
* supplier vetting.
|
|
9
|
+
*
|
|
10
|
+
* ★ KEYLESS — there is NO API key at all (no parameter, no header). This module
|
|
11
|
+
* touches NO key seam (no KEY_REGISTRY / keys.ts / API_KEYS.md). It REUSES the
|
|
12
|
+
* shared `getJson` (redirect:"error") / `driftError` fetch envelope, the `num`/
|
|
13
|
+
* `str` coercions (null-never-empty-string; a genuine 0 stays 0), and
|
|
14
|
+
* `withMeta`/`buildMeta` — and mirrors datagov-catalog.ts's fixed-host SSRF
|
|
15
|
+
* idiom + schema_drift catch-ladder verbatim.
|
|
16
|
+
*
|
|
17
|
+
* ★ PII — the complaints upstream response carries a `vin` field (an individual
|
|
18
|
+
* vehicle identifier). It is DELIBERATELY OMITTED from the curated output
|
|
19
|
+
* entirely — never surfaced, logged, or stored. The B2G value is the
|
|
20
|
+
* manufacturer / component / safety signal, NOT the VIN.
|
|
21
|
+
*
|
|
22
|
+
* ★ THE HONESTY PILLARS (P1-P4, live-verified 2026-07-15):
|
|
23
|
+
* P1: totalAvailable = `Count` (recalls) / `count` (complaints) — the REAL total.
|
|
24
|
+
* NHTSA returns the COMPLETE filtered set (no pagination), so in the normal
|
|
25
|
+
* case Count === results.length ⇒ complete:true. totalAvailable is NEVER
|
|
26
|
+
* fabricated: a PRESENT numeric Count is trusted verbatim; a MISSING Count
|
|
27
|
+
* falls back to results.length WITH an honest note (never invented).
|
|
28
|
+
* P2: results:[] (Count 0) ⇒ an HONEST EMPTY (returned:0, complete:true) — a bad
|
|
29
|
+
* make/model that returns 200+Count 0 is an honest no-match, NOT an error. A
|
|
30
|
+
* 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROW (never a fake empty); a 200
|
|
31
|
+
* non-JSON body ⇒ schema_drift.
|
|
32
|
+
* P3: booleans (crash/fire/parkIt/parkOutSide/overTheAirUpdate) preserved AS
|
|
33
|
+
* booleans (a non-boolean ⇒ null, never a fabricated false); counts
|
|
34
|
+
* (numberOfInjuries/numberOfDeaths) via `num` (a genuine 0 stays 0, NEVER
|
|
35
|
+
* null-for-0); dates as strings via `str`; Count/count via `num`.
|
|
36
|
+
* P4: `results` non-array ⇒ driftError; a Count/count that is PRESENT but a
|
|
37
|
+
* non-number ⇒ driftError (a broken total contract, never a fabricated empty).
|
|
38
|
+
* SSRF: fixed host `api.nhtsa.gov` (compile-time literal) + post-construction
|
|
39
|
+
* hostname/protocol assertion + redirect:"error"; make/model ride
|
|
40
|
+
* URLSearchParams (module-built, no raw passthrough); modelYear is
|
|
41
|
+
* ^\d{4}$; make/model are charclass-validated (letters/digits/space/hyphen,
|
|
42
|
+
* so a `../` or `%` can never reach the fixed path).
|
|
43
|
+
*/
|
|
44
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
45
|
+
import { getJson, driftError } from "./datasource.js";
|
|
46
|
+
import { num, str } from "./coerce.js";
|
|
47
|
+
import { withMeta } from "./meta.js";
|
|
48
|
+
// ─── Fixed endpoint (SSRF core — compile-time CONSTANTS) ──────────
|
|
49
|
+
export const NHTSA_HOST = "api.nhtsa.gov";
|
|
50
|
+
const RECALLS_PATH = "/recalls/recallsByVehicle";
|
|
51
|
+
const COMPLAINTS_PATH = "/complaints/complaintsByVehicle";
|
|
52
|
+
// HOST+path-only labels (→ ToolError.upstreamEndpoint). Keyless ⇒ no token can
|
|
53
|
+
// ever appear here regardless, but the labels stay host+path for consistency.
|
|
54
|
+
const RECALLS_LABEL = "nhtsa:/recalls/recallsByVehicle";
|
|
55
|
+
const COMPLAINTS_LABEL = "nhtsa:/complaints/complaintsByVehicle";
|
|
56
|
+
// ─── Input validation grammar (SSRF + injection guard) ────────────
|
|
57
|
+
// modelYear: exactly 4 digits. make/model: letters/digits/space/hyphen only —
|
|
58
|
+
// rejects `../`, `%`, `/`, `.`, quotes, so a value can never break out of the
|
|
59
|
+
// URLSearchParams-encoded query onto the fixed host/path.
|
|
60
|
+
export const NHTSA_MODEL_YEAR_RE = /^\d{4}$/;
|
|
61
|
+
export const NHTSA_MAKE_MODEL_RE = /^[A-Za-z0-9 -]+$/;
|
|
62
|
+
const KEYLESS_NOTE = "NHTSA is a keyless public API (api.nhtsa.gov) — no API key is required or accepted.";
|
|
63
|
+
const COMPLETE_SET_NOTE = "NHTSA returns the COMPLETE set of matching records for this make/model/modelYear (no pagination) — totalAvailable is the upstream Count, and returned should equal it.";
|
|
64
|
+
// ─── Shared coercions ─────────────────────────────────────────────
|
|
65
|
+
/** A genuine boolean preserved; anything else ⇒ null (never a fabricated false). */
|
|
66
|
+
function bool(x) {
|
|
67
|
+
return typeof x === "boolean" ? x : null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate the shared make/model/modelYear inputs (belt-and-suspenders behind the
|
|
71
|
+
* server Zod; a DIRECT handler call bypasses Zod). Rejects a bad value PRE-fetch
|
|
72
|
+
* (0 network call) so a `../`/`%` can never reach the fixed host/path.
|
|
73
|
+
*/
|
|
74
|
+
function validateVehicleArgs(args, label) {
|
|
75
|
+
const checks = [
|
|
76
|
+
["make", args.make, NHTSA_MAKE_MODEL_RE],
|
|
77
|
+
["model", args.model, NHTSA_MAKE_MODEL_RE],
|
|
78
|
+
["modelYear", args.modelYear, NHTSA_MODEL_YEAR_RE],
|
|
79
|
+
];
|
|
80
|
+
for (const [name, value, re] of checks) {
|
|
81
|
+
if (typeof value !== "string" || !re.test(value)) {
|
|
82
|
+
throw new ToolErrorCarrier({
|
|
83
|
+
kind: "invalid_input",
|
|
84
|
+
retryable: false,
|
|
85
|
+
message: name === "modelYear"
|
|
86
|
+
? `Invalid modelYear ${JSON.stringify(value)} — expected a 4-digit year (^\\d{4}$), e.g. "2020".`
|
|
87
|
+
: `Invalid ${name} ${JSON.stringify(value)} — expected letters/digits/space/hyphen only (^[A-Za-z0-9 -]+$), e.g. "honda".`,
|
|
88
|
+
upstreamEndpoint: label,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect) ──
|
|
94
|
+
/**
|
|
95
|
+
* GET one NHTSA JSON resource on the FIXED host. Builds
|
|
96
|
+
* `https://api.nhtsa.gov${path}?${params}`, asserts the CONSTRUCTED URL's
|
|
97
|
+
* hostname === the fixed host over https (belt-and-suspenders), and sets
|
|
98
|
+
* `redirect:"error"` (fail closed on any off-host 3xx). Keyless — no header/token.
|
|
99
|
+
*/
|
|
100
|
+
async function getNhtsa(path, label, params) {
|
|
101
|
+
const url = `https://${NHTSA_HOST}${path}?${params.toString()}`;
|
|
102
|
+
const built = new URL(url);
|
|
103
|
+
if (built.hostname !== NHTSA_HOST || built.protocol !== "https:") {
|
|
104
|
+
throw new ToolErrorCarrier({
|
|
105
|
+
kind: "invalid_input",
|
|
106
|
+
retryable: false,
|
|
107
|
+
message: `Constructed NHTSA URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(NHTSA_HOST)} over https — refusing to fetch (SSRF safety).`,
|
|
108
|
+
upstreamEndpoint: label,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return getJson(url, { label, redirect: "error" });
|
|
112
|
+
}
|
|
113
|
+
/** Build the shared make/model/modelYear query (module-built; no raw passthrough). */
|
|
114
|
+
function vehicleParams(args) {
|
|
115
|
+
const params = new URLSearchParams();
|
|
116
|
+
params.set("make", args.make);
|
|
117
|
+
params.set("model", args.model);
|
|
118
|
+
params.set("modelYear", args.modelYear);
|
|
119
|
+
return params;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fetch + parse a NHTSA resource, mirroring datagov-catalog's catch-ladder: a
|
|
123
|
+
* ToolErrorCarrier (host-assert / 4xx-5xx taxonomy) rethrows FIRST (preserving its
|
|
124
|
+
* kind); a 200 non-JSON `.json()` SyntaxError reclassifies to schema_drift; a bare
|
|
125
|
+
* error rethrows LAST.
|
|
126
|
+
*/
|
|
127
|
+
async function fetchNhtsa(path, label, args) {
|
|
128
|
+
try {
|
|
129
|
+
return await getNhtsa(path, label, vehicleParams(args));
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
if (e instanceof ToolErrorCarrier)
|
|
133
|
+
throw e;
|
|
134
|
+
if (e instanceof SyntaxError)
|
|
135
|
+
throw driftError(label, `NHTSA ${label} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`);
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the total from a Count/count field (P1/P4). A PRESENT numeric value is
|
|
141
|
+
* trusted verbatim; a MISSING (undefined/null) value falls back to results.length
|
|
142
|
+
* WITH an honest note (never fabricated); a PRESENT non-number ⇒ driftError (a
|
|
143
|
+
* broken total contract). Returns the total + the fallback flag.
|
|
144
|
+
*/
|
|
145
|
+
function resolveTotal(rawCount, returned, label) {
|
|
146
|
+
if (rawCount === undefined || rawCount === null) {
|
|
147
|
+
// P1 fallback — missing Count ⇒ results.length + an honest note.
|
|
148
|
+
return { total: returned, fellBack: true };
|
|
149
|
+
}
|
|
150
|
+
const n = num(rawCount);
|
|
151
|
+
if (n === null) {
|
|
152
|
+
// P4 — a PRESENT non-number Count is a broken contract, never a fake empty.
|
|
153
|
+
throw driftError(label, `NHTSA ${label} shape drift — the total count field is present but non-numeric.`);
|
|
154
|
+
}
|
|
155
|
+
return { total: n, fellBack: false };
|
|
156
|
+
}
|
|
157
|
+
/** Map ONE /recallsByVehicle row → the curated recall shape. Booleans via `bool`. */
|
|
158
|
+
function mapRecall(row) {
|
|
159
|
+
const r = (row ?? {});
|
|
160
|
+
return {
|
|
161
|
+
campaignNumber: str(r.NHTSACampaignNumber),
|
|
162
|
+
manufacturer: str(r.Manufacturer),
|
|
163
|
+
component: str(r.Component),
|
|
164
|
+
summary: str(r.Summary),
|
|
165
|
+
consequence: str(r.Consequence),
|
|
166
|
+
remedy: str(r.Remedy),
|
|
167
|
+
reportReceivedDate: str(r.ReportReceivedDate),
|
|
168
|
+
parkIt: bool(r.parkIt),
|
|
169
|
+
parkOutside: bool(r.parkOutSide),
|
|
170
|
+
overTheAirUpdate: bool(r.overTheAirUpdate),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Map ONE /complaintsByVehicle row → the curated complaint shape. ★The `vin` field
|
|
175
|
+
* is DELIBERATELY OMITTED (PII — never read into the output). Counts via `num` (a
|
|
176
|
+
* genuine 0 stays 0); booleans via `bool`.
|
|
177
|
+
*/
|
|
178
|
+
function mapComplaint(row) {
|
|
179
|
+
const r = (row ?? {});
|
|
180
|
+
return {
|
|
181
|
+
odiNumber: str(r.odiNumber),
|
|
182
|
+
manufacturer: str(r.manufacturer),
|
|
183
|
+
component: str(r.components),
|
|
184
|
+
summary: str(r.summary),
|
|
185
|
+
crash: bool(r.crash),
|
|
186
|
+
fire: bool(r.fire),
|
|
187
|
+
numberOfInjuries: num(r.numberOfInjuries),
|
|
188
|
+
numberOfDeaths: num(r.numberOfDeaths),
|
|
189
|
+
dateOfIncident: str(r.dateOfIncident),
|
|
190
|
+
dateComplaintFiled: str(r.dateComplaintFiled),
|
|
191
|
+
// ★ NO vin — the PII field is never surfaced, logged, or stored.
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const FILTERS_APPLIED = ["make", "model", "modelYear"];
|
|
195
|
+
// ─── Tool: nhtsa_recalls ──────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Fetch NHTSA safety RECALLS for a make/model/modelYear → curated recall rows +
|
|
198
|
+
* honest `_meta`. KEYLESS. totalAvailable = the upstream `Count` (the REAL total —
|
|
199
|
+
* NHTSA returns the complete set, no pagination). A no-match (Count 0) ⇒ an honest
|
|
200
|
+
* empty; a 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROW; a 200 non-JSON ⇒ drift.
|
|
201
|
+
*/
|
|
202
|
+
export async function recalls(args) {
|
|
203
|
+
validateVehicleArgs(args, RECALLS_LABEL);
|
|
204
|
+
const body = await fetchNhtsa(RECALLS_PATH, RECALLS_LABEL, args);
|
|
205
|
+
const b = (body ?? {});
|
|
206
|
+
// P4 — results MUST be an array (a missing/string/null results is drift).
|
|
207
|
+
if (!Array.isArray(b.results)) {
|
|
208
|
+
throw driftError(RECALLS_LABEL, `NHTSA ${RECALLS_LABEL} shape drift — results must be an array.`);
|
|
209
|
+
}
|
|
210
|
+
const recalls = b.results.map(mapRecall);
|
|
211
|
+
const returned = recalls.length;
|
|
212
|
+
const { total, fellBack } = resolveTotal(b.Count, returned, RECALLS_LABEL);
|
|
213
|
+
const notes = [KEYLESS_NOTE, COMPLETE_SET_NOTE];
|
|
214
|
+
if (fellBack)
|
|
215
|
+
notes.push("NHTSA did not report a Count field — totalAvailable falls back to the number of returned rows (results.length); the true total may differ.");
|
|
216
|
+
return withMeta({ recalls }, {
|
|
217
|
+
source: `${NHTSA_HOST} /recalls/recallsByVehicle (NHTSA vehicle safety recalls; keyless)`,
|
|
218
|
+
keylessMode: true,
|
|
219
|
+
returned,
|
|
220
|
+
totalAvailable: total,
|
|
221
|
+
filtersApplied: FILTERS_APPLIED,
|
|
222
|
+
filtersDropped: [],
|
|
223
|
+
fieldsUnavailable: [],
|
|
224
|
+
notes,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ─── Tool: nhtsa_complaints ───────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Fetch NHTSA consumer COMPLAINTS for a make/model/modelYear → curated complaint
|
|
230
|
+
* rows (★NO vin — PII omitted) + honest `_meta`. KEYLESS. totalAvailable = the
|
|
231
|
+
* upstream `count` (the REAL total). A no-match ⇒ honest empty; a 4xx ⇒
|
|
232
|
+
* invalid_input; a 5xx/timeout ⇒ THROW; a 200 non-JSON ⇒ drift.
|
|
233
|
+
*/
|
|
234
|
+
export async function complaints(args) {
|
|
235
|
+
validateVehicleArgs(args, COMPLAINTS_LABEL);
|
|
236
|
+
const body = await fetchNhtsa(COMPLAINTS_PATH, COMPLAINTS_LABEL, args);
|
|
237
|
+
const b = (body ?? {});
|
|
238
|
+
// P4 — results MUST be an array (a missing/string/null results is drift).
|
|
239
|
+
if (!Array.isArray(b.results)) {
|
|
240
|
+
throw driftError(COMPLAINTS_LABEL, `NHTSA ${COMPLAINTS_LABEL} shape drift — results must be an array.`);
|
|
241
|
+
}
|
|
242
|
+
const complaints = b.results.map(mapComplaint);
|
|
243
|
+
const returned = complaints.length;
|
|
244
|
+
const { total, fellBack } = resolveTotal(b.count, returned, COMPLAINTS_LABEL);
|
|
245
|
+
const notes = [
|
|
246
|
+
KEYLESS_NOTE,
|
|
247
|
+
COMPLETE_SET_NOTE,
|
|
248
|
+
"The NHTSA complaint VIN (an individual vehicle identifier) is intentionally EXCLUDED from this output (PII). The B2G signal is the manufacturer/component/crash/fire/injury/death safety history.",
|
|
249
|
+
];
|
|
250
|
+
if (fellBack)
|
|
251
|
+
notes.push("NHTSA did not report a count field — totalAvailable falls back to the number of returned rows (results.length); the true total may differ.");
|
|
252
|
+
return withMeta({ complaints }, {
|
|
253
|
+
source: `${NHTSA_HOST} /complaints/complaintsByVehicle (NHTSA vehicle safety complaints; keyless)`,
|
|
254
|
+
keylessMode: true,
|
|
255
|
+
returned,
|
|
256
|
+
totalAvailable: total,
|
|
257
|
+
filtersApplied: FILTERS_APPLIED,
|
|
258
|
+
filtersDropped: [],
|
|
259
|
+
fieldsUnavailable: [],
|
|
260
|
+
notes,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=nhtsa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nhtsa.js","sourceRoot":"","sources":["../src/nhtsa.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nist-controls.ts — NIST SP 800-53 Rev 5 security & privacy CONTROLS catalog
|
|
3
|
+
* (OSCAL, keyless). The cyber-compliance controls backbone for FedRAMP / CMMC / RMF
|
|
4
|
+
* work: look up a control (AC-2, SC-7, …) or a family (Access Control, System &
|
|
5
|
+
* Communications Protection, …) and get its title, requirement STATEMENT, discussion
|
|
6
|
+
* guidance, and control enhancements. No other tool here exposes the controls catalog
|
|
7
|
+
* (we have NVD CVEs + CISA KEV, but not the requirement side).
|
|
8
|
+
*
|
|
9
|
+
* SOURCE: NIST's OFFICIAL OSCAL content, published at github.com/usnistgov/oscal-content
|
|
10
|
+
* (the canonical machine-readable release; the .gov PDF is the human copy). NOT a
|
|
11
|
+
* .gov API host, so provenance is disclosed on every response (the ProPublica /
|
|
12
|
+
* CourtListener / get.gov republisher idiom — here NIST is the first-party author).
|
|
13
|
+
*
|
|
14
|
+
* PATTERN: the CISA-KEV static-file idiom (nvd.ts) — a fixed-host const URL fetched
|
|
15
|
+
* via getJson (redirect:"error" + 30s timeout), memoized 6h, with a plausibility
|
|
16
|
+
* FLOOR (a truncated catalog must NEVER read as "control not found"), then
|
|
17
|
+
* client-side filter/lookup. An outage/4xx/timeout THROWS (never a fake empty).
|
|
18
|
+
*
|
|
19
|
+
* SSRF: fixed host `raw.githubusercontent.com` + a fixed, pinned path (no free
|
|
20
|
+
* host/path). Filtering is CLIENT-SIDE over the parsed catalog.
|
|
21
|
+
*/
|
|
22
|
+
import { type MetaBundle } from "./meta.js";
|
|
23
|
+
export declare const OSCAL_HOST = "raw.githubusercontent.com";
|
|
24
|
+
export type NistControl = {
|
|
25
|
+
id: string;
|
|
26
|
+
family: string;
|
|
27
|
+
title: string;
|
|
28
|
+
statement: string;
|
|
29
|
+
guidance: string | null;
|
|
30
|
+
enhancements: {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Look up NIST SP 800-53 Rev 5 controls by controlId (exact, e.g. "AC-2"), family
|
|
37
|
+
* (exact family letter or name, e.g. "AC" / "Access Control"), and/or keyword
|
|
38
|
+
* (case-insensitive substring over title + statement). Client-side filters over the
|
|
39
|
+
* cached OSCAL catalog; honest `_meta` (exact match total; provenance disclosed).
|
|
40
|
+
*/
|
|
41
|
+
export declare function searchControls(args: {
|
|
42
|
+
controlId?: string;
|
|
43
|
+
family?: string;
|
|
44
|
+
keyword?: string;
|
|
45
|
+
limit?: number;
|
|
46
|
+
offset?: number;
|
|
47
|
+
}): Promise<MetaBundle>;
|
|
48
|
+
//# sourceMappingURL=nist-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nist-controls.d.ts","sourceRoot":"","sources":["../src/nist-controls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAEzE,eAAO,MAAM,UAAU,8BAA8B,CAAC;AAkBtD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/C,CAAC;AAsGF;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,UAAU,CAAC,CAqDtB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nist-controls.ts — NIST SP 800-53 Rev 5 security & privacy CONTROLS catalog
|
|
3
|
+
* (OSCAL, keyless). The cyber-compliance controls backbone for FedRAMP / CMMC / RMF
|
|
4
|
+
* work: look up a control (AC-2, SC-7, …) or a family (Access Control, System &
|
|
5
|
+
* Communications Protection, …) and get its title, requirement STATEMENT, discussion
|
|
6
|
+
* guidance, and control enhancements. No other tool here exposes the controls catalog
|
|
7
|
+
* (we have NVD CVEs + CISA KEV, but not the requirement side).
|
|
8
|
+
*
|
|
9
|
+
* SOURCE: NIST's OFFICIAL OSCAL content, published at github.com/usnistgov/oscal-content
|
|
10
|
+
* (the canonical machine-readable release; the .gov PDF is the human copy). NOT a
|
|
11
|
+
* .gov API host, so provenance is disclosed on every response (the ProPublica /
|
|
12
|
+
* CourtListener / get.gov republisher idiom — here NIST is the first-party author).
|
|
13
|
+
*
|
|
14
|
+
* PATTERN: the CISA-KEV static-file idiom (nvd.ts) — a fixed-host const URL fetched
|
|
15
|
+
* via getJson (redirect:"error" + 30s timeout), memoized 6h, with a plausibility
|
|
16
|
+
* FLOOR (a truncated catalog must NEVER read as "control not found"), then
|
|
17
|
+
* client-side filter/lookup. An outage/4xx/timeout THROWS (never a fake empty).
|
|
18
|
+
*
|
|
19
|
+
* SSRF: fixed host `raw.githubusercontent.com` + a fixed, pinned path (no free
|
|
20
|
+
* host/path). Filtering is CLIENT-SIDE over the parsed catalog.
|
|
21
|
+
*/
|
|
22
|
+
import { getJson, driftError } from "./datasource.js";
|
|
23
|
+
import { memoize } from "./cache.js";
|
|
24
|
+
import { withMeta } from "./meta.js";
|
|
25
|
+
export const OSCAL_HOST = "raw.githubusercontent.com";
|
|
26
|
+
const OSCAL_URL = "https://raw.githubusercontent.com/usnistgov/oscal-content/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json";
|
|
27
|
+
const OSCAL_LABEL = "nist-oscal:sp800-53r5";
|
|
28
|
+
const OSCAL_TIMEOUT_MS = 30_000;
|
|
29
|
+
const OSCAL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
30
|
+
// Plausibility floor: 800-53 Rev5 has 20 control families and ~1000 controls. A
|
|
31
|
+
// truncated catalog with far fewer must THROW, never read as "control not found".
|
|
32
|
+
const FAMILY_FLOOR = 15;
|
|
33
|
+
const PROVENANCE_NOTE = "Source: NIST SP 800-53 Rev 5 OSCAL catalog, published at github.com/usnistgov/oscal-content (NIST's canonical machine-readable release; authoritative first-party data served from GitHub, not a .gov API host).";
|
|
34
|
+
const CLIENT_FILTER_NOTE = "The catalog has no query API — the full published OSCAL JSON is fetched (cached 6h) and filtered CLIENT-SIDE (controlId exact, family exact, keyword = case-insensitive substring over title+statement). totalAvailable is the EXACT match count.";
|
|
35
|
+
const REFERENCE_NOTE = "This is the REQUIREMENT catalog (control text), NOT an assessment or an authorization. A control's applicability depends on the system's FIPS-199 impact baseline (Low/Moderate/High) and overlay — which this catalog does not encode.";
|
|
36
|
+
/** OSCAL control id ("ac-2", "ac-2.1") → display id ("AC-2", "AC-2(1)"). */
|
|
37
|
+
function displayId(rawId) {
|
|
38
|
+
const m = /^([a-z]+)-(\d+)(?:\.(\d+))?$/i.exec(rawId.trim());
|
|
39
|
+
if (!m)
|
|
40
|
+
return rawId.toUpperCase();
|
|
41
|
+
const fam = (m[1] ?? "").toUpperCase();
|
|
42
|
+
// Strip leading zeros so a zero-padded input ('AC-02') normalizes to the catalog's
|
|
43
|
+
// canonical unpadded form ('AC-2') — else an exact controlId lookup would miss.
|
|
44
|
+
const num = String(Number(m[2] ?? "0"));
|
|
45
|
+
const enh = m[3] !== undefined ? String(Number(m[3])) : undefined;
|
|
46
|
+
return enh !== undefined ? `${fam}-${num}(${enh})` : `${fam}-${num}`;
|
|
47
|
+
}
|
|
48
|
+
/** Recursively collect a statement part's prose as labelled, indented lines. */
|
|
49
|
+
function collectProse(part, depth, out) {
|
|
50
|
+
const label = part.props?.find((p) => p.name === "label")?.value;
|
|
51
|
+
const indent = " ".repeat(depth);
|
|
52
|
+
const prefix = label ? `${label} ` : "";
|
|
53
|
+
if (part.prose && part.prose.trim().length > 0) {
|
|
54
|
+
out.push(`${indent}${prefix}${part.prose.trim()}`);
|
|
55
|
+
}
|
|
56
|
+
else if (label) {
|
|
57
|
+
out.push(`${indent}${prefix}`.trimEnd());
|
|
58
|
+
}
|
|
59
|
+
for (const sub of part.parts ?? [])
|
|
60
|
+
collectProse(sub, depth + 1, out);
|
|
61
|
+
}
|
|
62
|
+
function partProse(control, name) {
|
|
63
|
+
const part = (control.parts ?? []).find((p) => p.name === name);
|
|
64
|
+
if (!part)
|
|
65
|
+
return null;
|
|
66
|
+
const out = [];
|
|
67
|
+
collectProse(part, 0, out);
|
|
68
|
+
const text = out.join("\n").trim();
|
|
69
|
+
return text.length > 0 ? text : null;
|
|
70
|
+
}
|
|
71
|
+
function mapControl(control, family) {
|
|
72
|
+
return {
|
|
73
|
+
id: displayId(control.id ?? ""),
|
|
74
|
+
family,
|
|
75
|
+
title: (control.title ?? "").trim(),
|
|
76
|
+
statement: partProse(control, "statement") ?? "",
|
|
77
|
+
guidance: partProse(control, "guidance"),
|
|
78
|
+
enhancements: (control.controls ?? []).map((e) => ({
|
|
79
|
+
id: displayId(e.id ?? ""),
|
|
80
|
+
title: (e.title ?? "").trim(),
|
|
81
|
+
})),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fetch + parse the OSCAL catalog into a flat list of TOP-LEVEL controls, memoized
|
|
86
|
+
* 6h. Header/shape guarded: `catalog.groups` MUST be an array with ≥ FAMILY_FLOOR
|
|
87
|
+
* families (else driftError — a truncated catalog is NEVER a fake empty). Enhancements
|
|
88
|
+
* are carried on each control (not indexed as top-level entries).
|
|
89
|
+
*/
|
|
90
|
+
async function loadControls() {
|
|
91
|
+
return memoize("nist:sp800-53r5", async () => {
|
|
92
|
+
const built = new URL(OSCAL_URL);
|
|
93
|
+
if (built.hostname !== OSCAL_HOST || built.protocol !== "https:") {
|
|
94
|
+
throw driftError(OSCAL_LABEL, `Constructed OSCAL URL host ${JSON.stringify(built.hostname)} is not ${OSCAL_HOST} over https — refusing to fetch (SSRF safety).`);
|
|
95
|
+
}
|
|
96
|
+
const body = (await getJson(OSCAL_URL, {
|
|
97
|
+
label: OSCAL_LABEL,
|
|
98
|
+
redirect: "error",
|
|
99
|
+
timeoutMs: OSCAL_TIMEOUT_MS,
|
|
100
|
+
}));
|
|
101
|
+
const groups = body.catalog?.groups;
|
|
102
|
+
if (!Array.isArray(groups) || groups.length < FAMILY_FLOOR) {
|
|
103
|
+
throw driftError(OSCAL_LABEL, `OSCAL catalog.groups missing or implausibly small (${Array.isArray(groups) ? groups.length : "not-an-array"} < ${FAMILY_FLOOR} families) — treating as schema drift / truncation, never a fake-empty catalog.`);
|
|
104
|
+
}
|
|
105
|
+
const controls = [];
|
|
106
|
+
for (const g of groups) {
|
|
107
|
+
const family = `${(g.id ?? "").toUpperCase()} — ${(g.title ?? "").trim()}`;
|
|
108
|
+
for (const c of g.controls ?? [])
|
|
109
|
+
controls.push(mapControl(c, family));
|
|
110
|
+
}
|
|
111
|
+
return controls;
|
|
112
|
+
}, OSCAL_CACHE_TTL_MS);
|
|
113
|
+
}
|
|
114
|
+
// ─── Tool: nist_800_53_controls ───────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Look up NIST SP 800-53 Rev 5 controls by controlId (exact, e.g. "AC-2"), family
|
|
117
|
+
* (exact family letter or name, e.g. "AC" / "Access Control"), and/or keyword
|
|
118
|
+
* (case-insensitive substring over title + statement). Client-side filters over the
|
|
119
|
+
* cached OSCAL catalog; honest `_meta` (exact match total; provenance disclosed).
|
|
120
|
+
*/
|
|
121
|
+
export async function searchControls(args) {
|
|
122
|
+
const limit = args.limit ?? 25;
|
|
123
|
+
const offset = args.offset ?? 0;
|
|
124
|
+
const all = await loadControls();
|
|
125
|
+
const filtersApplied = [];
|
|
126
|
+
const idQ = args.controlId !== undefined ? displayId(args.controlId) : undefined;
|
|
127
|
+
const famQ = args.family?.trim().toLowerCase();
|
|
128
|
+
const kwQ = args.keyword?.trim().toLowerCase();
|
|
129
|
+
if (args.controlId !== undefined)
|
|
130
|
+
filtersApplied.push("controlId");
|
|
131
|
+
if (args.family !== undefined)
|
|
132
|
+
filtersApplied.push("family");
|
|
133
|
+
if (args.keyword !== undefined)
|
|
134
|
+
filtersApplied.push("keyword");
|
|
135
|
+
const matched = all.filter((c) => {
|
|
136
|
+
if (idQ && c.id.toUpperCase() !== idQ.toUpperCase())
|
|
137
|
+
return false;
|
|
138
|
+
if (famQ) {
|
|
139
|
+
// family field is "AC — Access Control"; match either the letter code or a
|
|
140
|
+
// substring of the title (both case-insensitive).
|
|
141
|
+
const fam = c.family.toLowerCase();
|
|
142
|
+
const code = (fam.split("—")[0] ?? "").trim();
|
|
143
|
+
if (code !== famQ && !fam.includes(famQ))
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (kwQ) {
|
|
147
|
+
// Search the title + requirement statement AND each enhancement's title, so a
|
|
148
|
+
// term that lives only in an enhancement (e.g. "multi-factor" → IA-2(1)) still
|
|
149
|
+
// surfaces the parent control. filtersApplied still lists 'keyword'.
|
|
150
|
+
const hay = `${c.title}\n${c.statement}\n${c.enhancements.map((e) => e.title).join("\n")}`.toLowerCase();
|
|
151
|
+
if (!hay.includes(kwQ))
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
const totalAvailable = matched.length;
|
|
157
|
+
const page = matched.slice(offset, offset + limit);
|
|
158
|
+
const returned = page.length;
|
|
159
|
+
const hasMore = offset + returned < totalAvailable;
|
|
160
|
+
const nextOffset = hasMore ? offset + returned : null;
|
|
161
|
+
return withMeta({ controls: page }, {
|
|
162
|
+
source: "NIST SP 800-53 Rev 5 (OSCAL catalog, keyless)",
|
|
163
|
+
keylessMode: true,
|
|
164
|
+
returned,
|
|
165
|
+
totalAvailable,
|
|
166
|
+
truncated: hasMore,
|
|
167
|
+
filtersApplied,
|
|
168
|
+
filtersDropped: [],
|
|
169
|
+
fieldsUnavailable: [],
|
|
170
|
+
pagination: { offset, limit, hasMore, nextOffset },
|
|
171
|
+
notes: [PROVENANCE_NOTE, CLIENT_FILTER_NOTE, REFERENCE_NOTE],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=nist-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nist-controls.js","sourceRoot":"","sources":["../src/nist-controls.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
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 { num } from "./coerce.js";
|
|
58
|
+
import { type MetaBundle } from "./meta.js";
|
|
59
|
+
export { num };
|
|
60
|
+
export declare const NONPROFIT_HOST = "projects.propublica.org";
|
|
61
|
+
export type NonprofitOrgSummary = {
|
|
62
|
+
ein: string | null;
|
|
63
|
+
name: string | null;
|
|
64
|
+
city: string | null;
|
|
65
|
+
state: string | null;
|
|
66
|
+
nteeCode: string | null;
|
|
67
|
+
subsectionCode: string | null;
|
|
68
|
+
};
|
|
69
|
+
export type NonprofitOrganization = {
|
|
70
|
+
ein: string | null;
|
|
71
|
+
name: string | null;
|
|
72
|
+
address: string | null;
|
|
73
|
+
city: string | null;
|
|
74
|
+
state: string | null;
|
|
75
|
+
zip: string | null;
|
|
76
|
+
nteeCode: string | null;
|
|
77
|
+
subsectionCode: string | null;
|
|
78
|
+
rulingDate: string | null;
|
|
79
|
+
statusCode: string | null;
|
|
80
|
+
};
|
|
81
|
+
export type NonprofitFiling = {
|
|
82
|
+
taxYear: number | null;
|
|
83
|
+
formType: string | null;
|
|
84
|
+
revenueUsd: number | null;
|
|
85
|
+
expensesUsd: number | null;
|
|
86
|
+
assetsUsd: number | null;
|
|
87
|
+
liabilitiesUsd: number | null;
|
|
88
|
+
pdfUrl: string | null;
|
|
89
|
+
};
|
|
90
|
+
export type NonprofitSearchArgs = {
|
|
91
|
+
query?: string;
|
|
92
|
+
state?: string;
|
|
93
|
+
ntee?: number;
|
|
94
|
+
page?: number;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Search US tax-exempt nonprofits (IRS Form 990) via ProPublica Nonprofit Explorer
|
|
98
|
+
* (`/nonprofits/api/v2/search.json`) → curated org summaries + honest `_meta`.
|
|
99
|
+
* KEYLESS. ★PROVENANCE: this is ProPublica (a non-profit newsroom) republishing
|
|
100
|
+
* IRS Form 990 public records — NOT a .gov API. ★totalAvailable is the API's REAL
|
|
101
|
+
* `total_results` — never organizations.length; page-based (0-indexed) pagination.
|
|
102
|
+
*/
|
|
103
|
+
export declare function search(args: NonprofitSearchArgs): Promise<MetaBundle>;
|
|
104
|
+
export type NonprofitFinancialsArgs = {
|
|
105
|
+
ein: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Fetch ONE nonprofit's IRS Form 990 profile + financials via ProPublica Nonprofit
|
|
109
|
+
* Explorer (`/nonprofits/api/v2/organizations/{ein}.json`) → curated organization +
|
|
110
|
+
* filings + honest `_meta`. KEYLESS. ★PROVENANCE: ProPublica (a non-profit newsroom)
|
|
111
|
+
* republishing IRS Form 990 public records — NOT a .gov API. An unknown EIN (HTTP
|
|
112
|
+
* 404) ⇒ not_found (never a fabricated empty org). The four Form 990 figures ride
|
|
113
|
+
* num() (null-never-0). totalAvailable = filings.length (the COMPLETE set).
|
|
114
|
+
*/
|
|
115
|
+
export declare function financials(args: NonprofitFinancialsArgs): Promise<MetaBundle>;
|
|
116
|
+
//# sourceMappingURL=nonprofit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonprofit.d.ts","sourceRoot":"","sources":["../src/nonprofit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH,OAAO,EAAE,GAAG,EAAO,MAAM,aAAa,CAAC;AACvC,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAIzE,OAAO,EAAE,GAAG,EAAE,CAAC;AAGf,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAiCxD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAiBF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAmCF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4H3E;AAGD,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAoGrB"}
|