@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.
- package/README.ja.md +22 -9
- package/README.ko.md +22 -9
- package/README.md +70 -12
- package/dist/bea.d.ts +105 -0
- package/dist/bea.d.ts.map +1 -0
- package/dist/bea.js +303 -0
- package/dist/bea.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/dol.d.ts +118 -0
- package/dist/dol.d.ts.map +1 -0
- package/dist/dol.js +421 -0
- package/dist/dol.js.map +1 -0
- package/dist/epa-envirofacts.d.ts +97 -0
- package/dist/epa-envirofacts.d.ts.map +1 -0
- package/dist/epa-envirofacts.js +292 -0
- package/dist/epa-envirofacts.js.map +1 -0
- package/dist/fred.d.ts +1 -1
- package/dist/fred.js +1 -1
- package/dist/keys.d.ts +11 -8
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +55 -8
- package/dist/keys.js.map +1 -1
- package/dist/lda.d.ts +105 -0
- package/dist/lda.d.ts.map +1 -0
- package/dist/lda.js +317 -0
- package/dist/lda.js.map +1 -0
- 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/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/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.d.ts +133 -0
- package/dist/openfda.d.ts.map +1 -0
- package/dist/openfda.js +402 -0
- package/dist/openfda.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +872 -6
- package/dist/server.js.map +1 -1
- package/package.json +2 -1
- package/src/bea.ts +372 -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/dol.ts +515 -0
- package/src/epa-envirofacts.ts +342 -0
- package/src/fred.ts +1 -1
- package/src/keys.ts +60 -8
- package/src/lda.ts +385 -0
- package/src/nhtsa.ts +352 -0
- package/src/nonprofit.ts +460 -0
- package/src/openfda-device.ts +356 -0
- package/src/openfda.ts +495 -0
- 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"}
|