@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,398 @@
1
+ /**
2
+ * courtlistener.ts — US FEDERAL COURT OPINIONS via CourtListener (ADR-0055) — the
3
+ * LITIGATION / case-law lane. Federal court decisions (opinions/clusters) — who
4
+ * sued whom in which court, the nature of suit, the disposition — the judicial
5
+ * signal no contract/spending/lobbying source carries (e.g. bid-protest and
6
+ * contract-claim opinions from the US Court of Federal Claims `uscfc` and the
7
+ * Federal Circuit `cafc`).
8
+ *
9
+ * ★ PROVENANCE — THIS IS NOT A .gov API (must be disclosed). The DATA is US federal
10
+ * court PUBLIC RECORDS, but the API is **CourtListener**, operated by the **Free
11
+ * Law Project** (a non-profit) which republishes those records KEYLESS. The .gov
12
+ * primary source (PACER) is PAYWALLED. So every response's `_meta.source` AND a
13
+ * note name CourtListener/Free Law Project and disclose the PACER-paywall — the
14
+ * tool never presents itself as a government API.
15
+ *
16
+ * ★ THIS IS A KEYLESS TOOL WITH AN *OPTIONAL* RATE-LIMIT TOKEN (the lda.ts /
17
+ * socrata app-token lineage, NOT the census/fred/bea key-REQUIRED lineage).
18
+ * Anonymous GETs return HTTP 200 — it works with NO token. A free
19
+ * COURTLISTENER_API_TOKEN only RAISES the shared rate limit; when set it rides
20
+ * ONLY as the `Authorization: Token <value>` request header (never the
21
+ * URL/label/_meta/notes/log — the K-test). When unset, NO auth header is sent
22
+ * (genuine keyless). This mirrors lda.ts's optional-Authorization discipline.
23
+ *
24
+ * GET https://www.courtlistener.com/api/rest/v4/search/
25
+ * ?q=&court=&filed_after=&filed_before=&type=o&order_by=<order>[&cursor=]
26
+ * → { count, next, previous, results:[{ caseName, court, court_id, dateFiled,
27
+ * docketNumber, suitNature, status, judge, citation, absolute_url, … }] }
28
+ *
29
+ * ★ HONESTY (ADR-0055 P1–P5):
30
+ * [P1] totalAvailable = `count` (the API's REAL total for the filter, e.g. the
31
+ * uscfc opinion corpus ~10595) — NEVER results.length. ★CURSOR pagination:
32
+ * `next` is a FULL URL carrying an opaque `cursor=` param (NOT page/offset).
33
+ * We EXTRACT the `cursor` value out of `next` and return it as `nextCursor`
34
+ * (offset/nextOffset null — a numeric offset is meaningless); hasMore = next
35
+ * is a non-null string. CourtListener v4 stops counting on deep cursor pages
36
+ * (`count:null`) — that is DISCLOSED (totalAvailable:null + a note), never
37
+ * fabricated as results.length.
38
+ * [P2] a genuine no-match (results:[]) ⇒ honest empty (returned:0). A 400 (bad
39
+ * param) ⇒ invalid_input surfacing the API's message. A 429 (unauth
40
+ * throttle) ⇒ rate_limited THROW (Retry-After honored, never routed around).
41
+ * A 5xx/timeout ⇒ upstream_unavailable THROW. A 200 non-JSON ⇒ schema_drift.
42
+ * [P3] dates are strings; `citation` may be an array/object ⇒ flattened to a safe
43
+ * string / string[] (never fabricated); judge / natureOfSuit / docketNumber
44
+ * are null when absent (never empty-string); every scalar null-never-empty.
45
+ * [P4] `results` non-array, or `count` neither a number NOR null ⇒ driftError
46
+ * (never a fabricated empty/total).
47
+ * [K-test] OPTIONAL token: when COURTLISTENER_API_TOKEN is set it rides ONLY the
48
+ * `Authorization: Token …` header — NEVER the URL/label/_meta/notes/log.
49
+ * Unset ⇒ anonymous (no auth header at all).
50
+ * [SSRF] fixed host `www.courtlistener.com`; a post-construction hostname/protocol
51
+ * assert + `redirect:"error"`; `court` charclass `^[a-z0-9]+$`; the dates
52
+ * charclass `^\d{4}-\d{2}-\d{2}$`; every VALUE rides URLSearchParams. The
53
+ * `cursor` is opaque but charclass-guarded; and when we EXTRACT the cursor
54
+ * from a `next` URL we RE-ASSERT that URL's host is courtlistener.com — an
55
+ * off-host `next` is REFUSED (schema_drift), never followed.
56
+ */
57
+ import { ToolErrorCarrier } from "./errors.js";
58
+ import { getJson, driftError } from "./datasource.js";
59
+ import { str } from "./coerce.js";
60
+ import { withMeta } from "./meta.js";
61
+ // ─── SSRF core: the single fixed host + base path ─────────────────
62
+ export const COURTLISTENER_HOST = "www.courtlistener.com";
63
+ const COURTLISTENER_SEARCH_PATH = "/api/rest/v4/search/";
64
+ // HOST+path label — surfaces in ToolError.upstreamEndpoint; the optional token rides
65
+ // ONLY in the Authorization header, so no token can ever appear here.
66
+ const COURTLISTENER_SEARCH_LABEL = "courtlistener:/api/rest/v4/search";
67
+ // ─── Validation (SSRF + "verify the input" honesty) ───────────────
68
+ // A CourtListener court id — lowercase alphanumerics only (e.g. uscfc, cafc, scotus).
69
+ const COURT_RE = /^[a-z0-9]+$/;
70
+ // An ISO calendar date (→ filed_after / filed_before).
71
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
72
+ // The opaque cursor grammar (SSRF + injection guard). CourtListener's v4 cursor is a
73
+ // URL-safe token; we return the DECODED value (extracted via URLSearchParams), so it
74
+ // carries NO `%`. Reject spaces / `&` / `#` / `?` / quotes / angle-brackets / `\`
75
+ // BEFORE it rides `cursor=` (defense-in-depth — the value can never steer the
76
+ // fixed-host authority, but a malformed cursor must never reach the query).
77
+ export const COURTLISTENER_CURSOR_RE = /^[A-Za-z0-9\-._~=+/:]{1,8192}$/;
78
+ const DEFAULT_ORDER = "dateFiled desc";
79
+ // ─── Honesty notes (ADR-0055 required set) ────────────────────────
80
+ const PROVENANCE_NOTE = "Data = US FEDERAL COURT PUBLIC RECORDS served by CourtListener (Free Law Project, a non-profit) — NOT a .gov API. CourtListener republishes these records keyless; the .gov primary source (PACER) is PAYWALLED. Treat opinions as of CourtListener's last ingest.";
81
+ const KEYLESS_NOTE = "Keyless by default (anonymous CourtListener access returns HTTP 200). An optional free COURTLISTENER_API_TOKEN only RAISES the rate limit; when set it is sent ONLY as the `Authorization: Token …` request header and is NEVER logged, echoed, or placed in this response.";
82
+ const COUNT_TOTAL_NOTE = "totalAvailable is CourtListener's real `count` — the total match count for the filter (NOT the rows on this page). Pagination is an OPAQUE cursor: pass _meta.nextCursor back as the `cursor` argument (offset/nextOffset are meaningless/null). nextCursor:null / hasMore:false means this is the last page.";
83
+ const DEEP_PAGE_NO_COUNT_NOTE = "CourtListener did not report a `count` on this cursor page (v4 stops counting on deep pages to save cost) — totalAvailable is unknown (null), NOT results.length. Use nextCursor to continue.";
84
+ const NATURE_OF_SUIT_QUERY_NOTE = "natureOfSuit was applied as a full-text query term (the v4 opinions search has no verified dedicated nature-of-suit filter), so it matches that text anywhere in the document rather than an exact suitNature-field equality.";
85
+ // ─── The optional-token seam (value NEVER leaked past the Authorization header) ──
86
+ /**
87
+ * The optional Authorization header (keyless-first, lda.ts/socrata app-token
88
+ * lineage). Present ONLY when COURTLISTENER_API_TOKEN is set (non-blank); the value
89
+ * is NEVER logged / never placed in the URL, label, `_meta`, or a note. When unset,
90
+ * `{}` (no header ⇒ genuine anonymous).
91
+ */
92
+ export function courtlistenerAuthHeader() {
93
+ const raw = process.env.COURTLISTENER_API_TOKEN;
94
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
95
+ return trimmed ? { Authorization: `Token ${trimmed}` } : {};
96
+ }
97
+ /** true iff a CourtListener token is configured (for the `_meta` note — never the value). */
98
+ export function courtlistenerTokenPresent() {
99
+ const raw = process.env.COURTLISTENER_API_TOKEN;
100
+ return typeof raw === "string" && raw.trim().length > 0;
101
+ }
102
+ /**
103
+ * Flatten CourtListener's `citation` (which may be a string, an array of strings,
104
+ * or an object) to a safe `string | string[] | null` — NEVER fabricated. An array
105
+ * ⇒ the non-null strings (or null when none); a string ⇒ that string; an object ⇒
106
+ * its non-null string VALUES (or null when none); absent ⇒ null. `[object Object]`
107
+ * can never leak (we never `String()` an object).
108
+ */
109
+ export function flattenCitation(x) {
110
+ if (x === null || x === undefined)
111
+ return null;
112
+ if (typeof x === "string")
113
+ return str(x);
114
+ if (Array.isArray(x)) {
115
+ const parts = x.map((v) => str(v)).filter((v) => v !== null);
116
+ return parts.length === 0 ? null : parts;
117
+ }
118
+ if (typeof x === "object") {
119
+ const parts = Object.values(x)
120
+ .map((v) => str(v))
121
+ .filter((v) => v !== null);
122
+ return parts.length === 0 ? null : parts;
123
+ }
124
+ return null;
125
+ }
126
+ /** Prefix a relative absolute_url with the fixed host; pass an already-absolute URL through. */
127
+ function resolveAbsoluteUrl(raw) {
128
+ const path = str(raw);
129
+ if (path === null)
130
+ return null;
131
+ if (/^https?:\/\//i.test(path))
132
+ return path;
133
+ return `https://${COURTLISTENER_HOST}${path.startsWith("/") ? path : `/${path}`}`;
134
+ }
135
+ /** Map ONE `results[]` opinion row → the curated shape. Every scalar via `str`. */
136
+ function mapOpinion(raw) {
137
+ const o = (raw ?? {});
138
+ return {
139
+ caseName: str(o.caseName) ?? str(o.caseNameFull),
140
+ // court_citation_string is the short display name; fall back to the full `court`.
141
+ court: str(o.court_citation_string) ?? str(o.court),
142
+ courtId: str(o.court_id),
143
+ dateFiled: str(o.dateFiled),
144
+ docketNumber: str(o.docketNumber),
145
+ natureOfSuit: str(o.suitNature),
146
+ status: str(o.status),
147
+ judge: str(o.judge),
148
+ citation: flattenCitation(o.citation),
149
+ absoluteUrl: resolveAbsoluteUrl(o.absolute_url),
150
+ };
151
+ }
152
+ /**
153
+ * Extract the opaque `cursor` value from a CourtListener `next` URL. RE-ASSERTS the
154
+ * `next` URL's host is courtlistener.com — an OFF-HOST `next` is REFUSED (drift),
155
+ * never parsed/followed (SSRF: a poisoned `next` could otherwise steer the next
156
+ * page off-host). Returns the decoded cursor string, or null when `next` is null /
157
+ * carries no cursor. A non-string / unparseable `next` ⇒ null (treated as "no more").
158
+ */
159
+ export function extractNextCursor(next, label) {
160
+ if (next === null || next === undefined)
161
+ return null;
162
+ if (typeof next !== "string" || next === "")
163
+ return null;
164
+ let u;
165
+ try {
166
+ u = new URL(next);
167
+ }
168
+ catch {
169
+ // A malformed `next` is drift — never fabricate a continuation from it.
170
+ throw driftError(label, `CourtListener returned a malformed \`next\` URL (${JSON.stringify(next).slice(0, 120)}) — refusing to derive a cursor from it.`);
171
+ }
172
+ if (u.hostname !== COURTLISTENER_HOST || u.protocol !== "https:") {
173
+ // An OFF-HOST next must be refused, not followed (SSRF safety).
174
+ throw driftError(label, `CourtListener \`next\` points off-host (${JSON.stringify(u.hostname)} ${u.protocol}) — refusing to follow it (SSRF safety).`);
175
+ }
176
+ return u.searchParams.get("cursor");
177
+ }
178
+ /**
179
+ * Search US federal court opinions via CourtListener (`/api/rest/v4/search/`,
180
+ * type=o) → curated opinion rows + honest `_meta`. KEYLESS (an optional
181
+ * COURTLISTENER_API_TOKEN only raises the rate limit, sent as the Authorization
182
+ * header only). ★PROVENANCE: this is CourtListener/Free Law Project (a non-profit),
183
+ * NOT a .gov API — PACER (the .gov source) is paywalled. totalAvailable is the
184
+ * API's REAL `count`; CURSOR pagination (nextCursor extracted from `next`).
185
+ */
186
+ export async function searchOpinions(args) {
187
+ const label = COURTLISTENER_SEARCH_LABEL;
188
+ // ── Validate + default (belt-and-suspenders behind the server Zod; a DIRECT
189
+ // handler call bypasses Zod). court / dates are charclass-guarded; the cursor
190
+ // grammar-guarded; the free-text values ride URLSearchParams (encoded). ──
191
+ if (args.court !== undefined && !COURT_RE.test(args.court)) {
192
+ throw new ToolErrorCarrier({
193
+ kind: "invalid_input",
194
+ retryable: false,
195
+ message: `Invalid court ${JSON.stringify(args.court)} — expected a CourtListener court id (lowercase alphanumerics, ^[a-z0-9]+$), e.g. "uscfc", "cafc", "scotus".`,
196
+ upstreamEndpoint: label,
197
+ });
198
+ }
199
+ if (args.dateFiledAfter !== undefined &&
200
+ !DATE_RE.test(args.dateFiledAfter)) {
201
+ throw new ToolErrorCarrier({
202
+ kind: "invalid_input",
203
+ retryable: false,
204
+ message: `Invalid dateFiledAfter ${JSON.stringify(args.dateFiledAfter)} — expected an ISO date (^\\d{4}-\\d{2}-\\d{2}$), e.g. "2020-01-01".`,
205
+ upstreamEndpoint: label,
206
+ });
207
+ }
208
+ if (args.dateFiledBefore !== undefined &&
209
+ !DATE_RE.test(args.dateFiledBefore)) {
210
+ throw new ToolErrorCarrier({
211
+ kind: "invalid_input",
212
+ retryable: false,
213
+ message: `Invalid dateFiledBefore ${JSON.stringify(args.dateFiledBefore)} — expected an ISO date (^\\d{4}-\\d{2}-\\d{2}$), e.g. "2024-12-31".`,
214
+ upstreamEndpoint: label,
215
+ });
216
+ }
217
+ if (args.cursor !== undefined &&
218
+ !COURTLISTENER_CURSOR_RE.test(args.cursor)) {
219
+ throw new ToolErrorCarrier({
220
+ kind: "invalid_input",
221
+ retryable: false,
222
+ message: `Invalid cursor (opaque continuation token) — must be a ≤8192-char URL-safe token (no spaces, '%', or steering characters). Pass back the _meta.nextCursor from the previous page.`,
223
+ upstreamEndpoint: label,
224
+ });
225
+ }
226
+ // ── Build the query from VALIDATED typed args, key-by-key (SSRF: no raw
227
+ // passthrough; every VALUE is URLSearchParams-encoded). type=o is FIXED. ──
228
+ const params = new URLSearchParams();
229
+ const filtersApplied = [];
230
+ // natureOfSuit has no verified dedicated filter on the v4 opinions search, so it
231
+ // is folded into the `q` full-text query (disclosed via NATURE_OF_SUIT_QUERY_NOTE).
232
+ const qParts = [];
233
+ if (args.query !== undefined && args.query !== "") {
234
+ qParts.push(args.query);
235
+ filtersApplied.push("query");
236
+ }
237
+ let natureOfSuitApplied = false;
238
+ if (args.natureOfSuit !== undefined && args.natureOfSuit !== "") {
239
+ qParts.push(args.natureOfSuit);
240
+ filtersApplied.push("natureOfSuit");
241
+ natureOfSuitApplied = true;
242
+ }
243
+ if (qParts.length > 0)
244
+ params.set("q", qParts.join(" "));
245
+ if (args.court !== undefined) {
246
+ params.set("court", args.court);
247
+ filtersApplied.push("court");
248
+ }
249
+ if (args.dateFiledAfter !== undefined) {
250
+ params.set("filed_after", args.dateFiledAfter);
251
+ filtersApplied.push("dateFiledAfter");
252
+ }
253
+ if (args.dateFiledBefore !== undefined) {
254
+ params.set("filed_before", args.dateFiledBefore);
255
+ filtersApplied.push("dateFiledBefore");
256
+ }
257
+ params.set("type", "o"); // FIXED — opinions only
258
+ params.set("order_by", args.order && args.order !== "" ? args.order : DEFAULT_ORDER);
259
+ if (args.cursor !== undefined) {
260
+ params.set("cursor", args.cursor);
261
+ filtersApplied.push("cursor");
262
+ }
263
+ const url = `https://${COURTLISTENER_HOST}${COURTLISTENER_SEARCH_PATH}?${params.toString()}`;
264
+ // Belt-and-suspenders: the fixed host + strictly-built query leave nothing to
265
+ // steer the authority; assert the built URL cannot have been moved off-host.
266
+ const built = new URL(url);
267
+ if (built.hostname !== COURTLISTENER_HOST || built.protocol !== "https:") {
268
+ throw new ToolErrorCarrier({
269
+ kind: "invalid_input",
270
+ retryable: false,
271
+ message: `Constructed CourtListener URL host ${JSON.stringify(built.hostname)} (${built.protocol}) is not ${COURTLISTENER_HOST} over https — refusing to fetch (SSRF safety).`,
272
+ upstreamEndpoint: label,
273
+ });
274
+ }
275
+ // ── Fetch through the shared envelope. The optional token rides the Authorization
276
+ // header ONLY (never the URL/label/_meta); redirect:"error" fails closed on any
277
+ // off-host 3xx. A 429 ⇒ rate_limited THROW (Retry-After honored by the shared
278
+ // taxonomy, never routed around); a 5xx/timeout ⇒ upstream_unavailable THROW; a
279
+ // 400 ⇒ invalid_input (re-read below to surface the API message); a 200 non-JSON
280
+ // ⇒ getJson's r.json() throws a SyntaxError ⇒ schema_drift. ──
281
+ const headers = courtlistenerAuthHeader();
282
+ let body;
283
+ try {
284
+ body = await getJson(url, {
285
+ label,
286
+ headers,
287
+ redirect: "error",
288
+ });
289
+ }
290
+ catch (e) {
291
+ if (e instanceof SyntaxError) {
292
+ throw driftError(label, "CourtListener /api/rest/v4/search returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
293
+ }
294
+ // [P2] A 400 (bad param) carries a DRF error body; fetchWithRetry discarded it.
295
+ // Re-read once on the error path ONLY so the caller learns the REAL reason (never
296
+ // a fake-empty). 5xx/429/404/timeout keep their taxonomy.
297
+ if (e instanceof ToolErrorCarrier && e.toolError.upstreamStatus === 400) {
298
+ const apiMsg = await readCourtlistenerErrorMessage(url, headers);
299
+ throw new ToolErrorCarrier({
300
+ kind: "invalid_input",
301
+ retryable: false,
302
+ message: apiMsg
303
+ ? `CourtListener rejected the request (HTTP 400): ${apiMsg}. Check the filter parameters (court, dateFiledAfter/Before, order, cursor).`
304
+ : "CourtListener rejected the request (HTTP 400) — check the filter parameters (court, dateFiledAfter/Before, order, cursor).",
305
+ upstreamStatus: 400,
306
+ upstreamEndpoint: label,
307
+ });
308
+ }
309
+ throw e; // 5xx → upstream_unavailable, 404 → not_found, 429 → rate_limited …
310
+ }
311
+ // ── [P4] `results` MUST be an array; `count` MUST be a number OR null (v4 returns
312
+ // count:null on deep cursor pages — a documented behavior, NOT drift). A
313
+ // non-array results, or a count that is neither a number nor null (e.g. a
314
+ // string/object), is drift — never a fabricated empty/total. ──
315
+ const b = (body ?? {});
316
+ if (!Array.isArray(b.results)) {
317
+ throw driftError(label, "CourtListener /api/rest/v4/search shape drift — `results` must be an array.");
318
+ }
319
+ const rawCount = b.count;
320
+ const countIsNumber = typeof rawCount === "number" && Number.isFinite(rawCount);
321
+ if (!countIsNumber && rawCount !== null && rawCount !== undefined) {
322
+ throw driftError(label, "CourtListener /api/rest/v4/search shape drift — `count` must be the total match count (a number) or null (deep cursor page).");
323
+ }
324
+ const opinions = b.results.map(mapOpinion);
325
+ const returned = opinions.length;
326
+ // ── [P1] totalAvailable is the API's REAL count, NEVER results.length. count:null
327
+ // (deep cursor page) ⇒ totalAvailable:null + a disclosure note. ★CURSOR
328
+ // pagination: hasMore from `next` presence; nextCursor EXTRACTED from the `next`
329
+ // URL (host re-asserted). ──
330
+ const totalAvailable = countIsNumber ? rawCount : null;
331
+ const nextCursor = extractNextCursor(b.next, label);
332
+ const hasMore = nextCursor !== null;
333
+ const notes = [PROVENANCE_NOTE, COUNT_TOTAL_NOTE];
334
+ if (!countIsNumber)
335
+ notes.push(DEEP_PAGE_NO_COUNT_NOTE);
336
+ if (natureOfSuitApplied)
337
+ notes.push(NATURE_OF_SUIT_QUERY_NOTE);
338
+ notes.push(KEYLESS_NOTE);
339
+ notes.push(`CourtListener token: ${courtlistenerTokenPresent() ? "present (Authorization: Token … sent; value never logged)" : "absent (keyless; a free COURTLISTENER_API_TOKEN lifts the rate limit)"}.`);
340
+ return withMeta({ opinions }, {
341
+ source: `${COURTLISTENER_HOST} /api/rest/v4/search (CourtListener (Free Law Project) — US federal court records; PACER (.gov) is paywalled; keyless)`,
342
+ keylessMode: true, // ★KEYLESS — the optional token only raises the rate limit
343
+ returned,
344
+ totalAvailable,
345
+ filtersApplied,
346
+ filtersDropped: [],
347
+ fieldsUnavailable: [],
348
+ // Cursor page: offset/nextOffset null (no numeric offset); continuation is
349
+ // nextCursor (the opaque token extracted from `next`, passed back as `cursor`).
350
+ pagination: { offset: null, limit: returned, hasMore, nextOffset: null },
351
+ nextCursor,
352
+ notes,
353
+ });
354
+ }
355
+ /**
356
+ * Single bare GET to read a CourtListener 400's DRF error body (error path ONLY).
357
+ * Returns a compact human-readable message, or null on any failure. Sends the SAME
358
+ * headers (so a keyed re-read honors the token) + redirect:"error"; the token stays
359
+ * header-only.
360
+ */
361
+ async function readCourtlistenerErrorMessage(url, headers) {
362
+ try {
363
+ const r = await fetch(url, {
364
+ signal: AbortSignal.timeout(15_000),
365
+ headers,
366
+ redirect: "error",
367
+ });
368
+ const body = (await r.json());
369
+ return summarizeDrfError(body);
370
+ }
371
+ catch {
372
+ return null;
373
+ }
374
+ }
375
+ /**
376
+ * Summarize a Django-REST-Framework 400 error body into a compact string. DRF emits
377
+ * `{ detail: "…" }` OR `{ field: ["message", …], … }`. Returns null for a shape we
378
+ * can't read (⇒ the caller falls back to the generic 400 message).
379
+ */
380
+ function summarizeDrfError(body) {
381
+ if (typeof body === "string")
382
+ return str(body);
383
+ if (body === null || typeof body !== "object")
384
+ return null;
385
+ const obj = body;
386
+ if (typeof obj.detail === "string")
387
+ return str(obj.detail);
388
+ const parts = [];
389
+ for (const [k, v] of Object.entries(obj)) {
390
+ const msg = Array.isArray(v)
391
+ ? v.map((x) => str(x)).filter((x) => x !== null).join("; ")
392
+ : str(v);
393
+ if (msg)
394
+ parts.push(`${k}: ${msg}`);
395
+ }
396
+ return parts.length > 0 ? parts.join(" | ") : null;
397
+ }
398
+ //# sourceMappingURL=courtlistener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"courtlistener.js","sourceRoot":"","sources":["../src/courtlistener.ts"],"names":[],"mappings":"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"}
package/dist/cpsc.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * cpsc.ts — CPSC CONSUMER-PRODUCT RECALLS (www.saferproducts.gov) — the consumer
3
+ * goods / import product-safety vetting lane (ADR-0058). ONE keyless tool:
4
+ * • cpsc_recalls — /RestWebServices/Recall?format=json + date/product/manufacturer/
5
+ * recallNumber filters.
6
+ * The third leg of the cross-agency product-safety family alongside NHTSA (vehicles)
7
+ * and openFDA (medical/food): a manufacturer / product / hazard recall history for
8
+ * B2G supplier and import 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 `str`
13
+ * coercion (null-never-empty-string), and `withMeta`/`buildMeta` — and mirrors
14
+ * nhtsa.ts / datagov-catalog.ts's fixed-host SSRF idiom + schema_drift
15
+ * catch-ladder verbatim.
16
+ *
17
+ * ★ THE HONESTY PILLARS (P1-P5, live-verified 2026-07-15):
18
+ * P1: the /Recall response is a BARE JSON ARRAY with NO count field and NO server
19
+ * pagination — it returns the COMPLETE set matching the filter. So
20
+ * totalAvailable = results.length and complete:true, WITH a disclosing note
21
+ * that CPSC reports no total-count field / no pagination. A total is NEVER
22
+ * fabricated (there is no upstream total to trust; the honest total is the
23
+ * length of the complete set).
24
+ * P2: an empty array `[]` ⇒ an HONEST EMPTY (returned:0, complete:true) — a filter
25
+ * that matches nothing is an honest no-match, NOT an error. A 4xx ⇒
26
+ * invalid_input; a 5xx/timeout ⇒ THROW (never a fake empty); a 200 non-JSON
27
+ * body OR a non-array body ⇒ schema_drift (never a fabricated empty).
28
+ * P3: dates stay STRINGS (via `str`); NumberOfUnits is free text ("About 6,500")
29
+ * kept as a STRING; nested arrays (Products/Manufacturers/Retailers/Hazards/
30
+ * Remedies/Injuries/ManufacturerCountries) are flattened to string arrays by
31
+ * extracting each object's `.Name` (★ManufacturerCountries uses `.Country`,
32
+ * NOT `.Name`), SKIPPING an empty `{}` object (never a fabricated entry); a
33
+ * genuinely-absent nested array ⇒ []; null-never-empty-string throughout.
34
+ * P4: the top-level body MUST be an array — a non-array (object/string/null) ⇒
35
+ * driftError (a broken response contract, never a fabricated empty).
36
+ * DEFAULT-WINDOW: with NO filter given, the unfiltered result is huge, so the tool
37
+ * defaults RecallDateStart to ~90 days ago and DISCLOSES the default in a note
38
+ * — it NEVER silently fetches the whole dataset.
39
+ * SSRF: fixed host `www.saferproducts.gov` (compile-time literal) + post-construction
40
+ * hostname/protocol assertion + redirect:"error"; every filter rides a
41
+ * module-built URLSearchParams (no raw passthrough); dates are ^\d{4}-\d{2}-\d{2}$;
42
+ * recallNumber is charclass-validated (letters/digits/hyphen only), so a
43
+ * `../` or `%` can never reach the fixed path.
44
+ */
45
+ import { type MetaBundle } from "./meta.js";
46
+ export declare const CPSC_HOST = "www.saferproducts.gov";
47
+ export declare const CPSC_DATE_RE: RegExp;
48
+ export declare const CPSC_RECALL_NUMBER_RE: RegExp;
49
+ export type CpscRecall = {
50
+ recallNumber: string | null;
51
+ recallDate: string | null;
52
+ title: string | null;
53
+ description: string | null;
54
+ url: string | null;
55
+ products: string[];
56
+ numberOfUnits: string | null;
57
+ manufacturers: string[];
58
+ retailers: string[];
59
+ hazards: string[];
60
+ remedies: string[];
61
+ injuries: string[];
62
+ manufacturerCountries: string[];
63
+ };
64
+ export type CpscRecallsArgs = {
65
+ dateStart?: string;
66
+ dateEnd?: string;
67
+ productName?: string;
68
+ manufacturer?: string;
69
+ recallNumber?: string;
70
+ };
71
+ /**
72
+ * Fetch CPSC consumer-product RECALLS → curated recall rows + honest `_meta`.
73
+ * KEYLESS. All filters are optional; with NO filter given, RecallDateStart defaults
74
+ * to ~90 days ago (disclosed in a note) so the whole dataset is never silently
75
+ * fetched. The response is a bare array with no total-count field / no pagination
76
+ * ⇒ totalAvailable = the number of returned recalls, complete:true. An empty array
77
+ * ⇒ an honest empty; a 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROW; a 200 non-JSON
78
+ * OR a non-array body ⇒ schema_drift.
79
+ */
80
+ export declare function recalls(args: CpscRecallsArgs): Promise<MetaBundle>;
81
+ //# sourceMappingURL=cpsc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpsc.d.ts","sourceRoot":"","sources":["../src/cpsc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAGzE,eAAO,MAAM,SAAS,0BAA0B,CAAC;AAUjD,eAAO,MAAM,YAAY,QAAwB,CAAC;AAClD,eAAO,MAAM,qBAAqB,QAAoB,CAAC;AAgDvD,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC,CAAC;AA4BF,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AA8EF;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAuFxE"}