@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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openfda-device.ts — openFDA 510(k) DEVICE CLEARANCES (api.fda.gov) — the
|
|
3
|
+
* MEDICAL-DEVICE REGULATORY lane (ADR-0056). The FDA's premarket-notification
|
|
4
|
+
* (510(k)) clearances: the applicant, the device, the clearance number (K-number),
|
|
5
|
+
* the decision (date + description), the clearance type, the product code, the
|
|
6
|
+
* advisory committee, and geography.
|
|
7
|
+
*
|
|
8
|
+
* ★ THIS IS THE SAME SOURCE + ENVELOPE + CRUX as openfda.ts (ADR-0054). Same host
|
|
9
|
+
* (api.fda.gov), same response envelope `{ meta:{ results:{ skip, limit, total }},
|
|
10
|
+
* results:[…] }`, same ★no-match→HTTP-404-NOT_FOUND-as-honest-empty crux, same
|
|
11
|
+
* optional query-key K-test, same fixed-host SSRF idiom, same structured-only
|
|
12
|
+
* (no raw Lucene passthrough) search assembly. It REUSES openfda.ts's `fetchOpenfda`
|
|
13
|
+
* (the SSRF-guarded classified fetch) and `readOpenfdaError` (the error-body reader)
|
|
14
|
+
* verbatim — the 404-reclassification logic is NOT reinvented — plus `luceneQuote`
|
|
15
|
+
* (the phrase-escape), `openfdaApiKey` (the optional-key env seam), and `OPENFDA_HOST`.
|
|
16
|
+
*
|
|
17
|
+
* GET https://api.fda.gov/device/510k.json
|
|
18
|
+
* ?search=<lucene>&limit=<1..100>&skip=<offset>[&api_key=<KEY>]
|
|
19
|
+
* → { meta:{ disclaimer, results:{ skip, limit, total }}, results:[ {…} ] }
|
|
20
|
+
* (live total ~175507).
|
|
21
|
+
*
|
|
22
|
+
* ★ HONESTY (mirrors openfda.ts exactly):
|
|
23
|
+
* [P1] totalAvailable = `meta.results.total` EXACT (e.g. 175507), NEVER
|
|
24
|
+
* results.length. skip/limit offset pagination:
|
|
25
|
+
* hasMore = skip + returned < total; nextOffset = hasMore ? skip+returned : null.
|
|
26
|
+
* [★P2] a 404 whose body is `{error:{code:"NOT_FOUND"}}` (a no-match query OR an
|
|
27
|
+
* unknown field) ⇒ HONEST EMPTY (returned:0, totalAvailable:0) — NOT thrown,
|
|
28
|
+
* NOT not_found. Any OTHER 4xx (e.g. a 400 syntax error) ⇒ invalid_input
|
|
29
|
+
* surfacing openFDA's message. 5xx/timeout ⇒ upstream_unavailable THROW. A
|
|
30
|
+
* 200 non-JSON body ⇒ schema_drift. (Reuses openfda.ts's exact fetch path.)
|
|
31
|
+
* [P3] dates (`decision_date`, YYYY-MM-DD) and every scalar surfaced as a STRING
|
|
32
|
+
* via `str` (null-never-empty-string) — no numeric coercion; never fabricated.
|
|
33
|
+
* [P4] `meta.results` or `results` absent / non-array ⇒ driftError (never a
|
|
34
|
+
* fabricated empty).
|
|
35
|
+
* [K-test] OPTIONAL OPENFDA_API_KEY: when set it rides `&api_key=` ONLY (openFDA has
|
|
36
|
+
* no header option — the query-key is inherent). Mitigation: the `label` is
|
|
37
|
+
* host+path ONLY (`openfda:/device/510k`, NO query), so no token reaches
|
|
38
|
+
* ToolError.upstreamEndpoint; `_meta.source` names the MODE only; the key is
|
|
39
|
+
* ABSENT from the serialized {data,_meta}, notes, and any log. Unset ⇒ keyless.
|
|
40
|
+
* [SSRF] fixed host `api.fda.gov` (fetchOpenfda asserts hostname/protocol +
|
|
41
|
+
* redirect:"error"); all filter VALUES are Lucene-escaped + phrase-quoted and
|
|
42
|
+
* ride URLSearchParams `search=`; limit/skip are integers; state is charclass
|
|
43
|
+
* `^[A-Za-z]{2}$`. No raw Lucene passthrough — structured only, injection-safe.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { ToolErrorCarrier, errorFromResponse } from "./errors.js";
|
|
47
|
+
import { driftError } from "./datasource.js";
|
|
48
|
+
import { str } from "./coerce.js";
|
|
49
|
+
import { withMeta, type MetaBundle, type ResponseMeta } from "./meta.js";
|
|
50
|
+
import {
|
|
51
|
+
OPENFDA_HOST,
|
|
52
|
+
fetchOpenfda,
|
|
53
|
+
readOpenfdaError,
|
|
54
|
+
luceneQuote,
|
|
55
|
+
openfdaApiKey,
|
|
56
|
+
} from "./openfda.js";
|
|
57
|
+
|
|
58
|
+
// state filter charclass (a 2-letter US state/territory postal code).
|
|
59
|
+
const STATE_RE = /^[A-Za-z]{2}$/;
|
|
60
|
+
|
|
61
|
+
const DEFAULT_LIMIT = 25;
|
|
62
|
+
const MAX_LIMIT = 100;
|
|
63
|
+
|
|
64
|
+
/** host+path-only label (→ ToolError.upstreamEndpoint); NEVER carries the key. */
|
|
65
|
+
const DEVICE_LABEL = "openfda:/device/510k";
|
|
66
|
+
|
|
67
|
+
// ─── Honesty notes (ADR-0056 required set) ────────────────────────
|
|
68
|
+
const NOT_DETERMINATION_NOTE =
|
|
69
|
+
"openFDA 510(k) device clearances are FDA-published premarket-notification records; treat the decision/date/clearance-type as of the source's last publication. This is reference data, not a live regulatory determination.";
|
|
70
|
+
const KEYLESS_NOTE =
|
|
71
|
+
"Keyless: no OPENFDA_API_KEY is set. openFDA allows ~1000 requests/day without a key; a free key raises the rate limit (get one at https://open.fda.gov/apis/authentication/).";
|
|
72
|
+
const KEYED_NOTE =
|
|
73
|
+
"OPENFDA_API_KEY is set — it rides ONLY the &api_key= query parameter to api.fda.gov (openFDA has no header option), raising the rate limit. Its value is NEVER logged, echoed, or placed in this response.";
|
|
74
|
+
const NO_FILTER_NOTE =
|
|
75
|
+
"No structured filters were applied — this is an unscoped scan of the WHOLE 510(k) device-clearance collection. Add applicant / deviceName / productCode / clearanceType / kNumber / state to scope the result set.";
|
|
76
|
+
|
|
77
|
+
// ─── Curated clearance row shape ──────────────────────────────────
|
|
78
|
+
export type OpenfdaClearance = {
|
|
79
|
+
applicant: string | null;
|
|
80
|
+
deviceName: string | null;
|
|
81
|
+
kNumber: string | null; // the 510(k) clearance number (K-number)
|
|
82
|
+
decisionDate: string | null; // YYYY-MM-DD — preserved as a STRING (P3)
|
|
83
|
+
decisionDescription: string | null;
|
|
84
|
+
clearanceType: string | null;
|
|
85
|
+
productCode: string | null;
|
|
86
|
+
advisoryCommittee: string | null;
|
|
87
|
+
state: string | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Map ONE openFDA 510(k) result row → the curated shape. Every scalar via `str`. */
|
|
91
|
+
function mapClearance(row: unknown): OpenfdaClearance {
|
|
92
|
+
const r = (row ?? {}) as Record<string, unknown>;
|
|
93
|
+
return {
|
|
94
|
+
applicant: str(r.applicant),
|
|
95
|
+
deviceName: str(r.device_name),
|
|
96
|
+
kNumber: str(r.k_number),
|
|
97
|
+
decisionDate: str(r.decision_date),
|
|
98
|
+
decisionDescription: str(r.decision_description),
|
|
99
|
+
clearanceType: str(r.clearance_type),
|
|
100
|
+
productCode: str(r.product_code),
|
|
101
|
+
advisoryCommittee: str(r.advisory_committee),
|
|
102
|
+
state: str(r.state),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Lucene search assembly (structured-only; injection-safe) ─────
|
|
107
|
+
/** The structured filter set → openFDA `field:value` clauses (510(k) fields). */
|
|
108
|
+
export type OpenfdaDeviceFilters = {
|
|
109
|
+
applicant?: string; // → applicant
|
|
110
|
+
deviceName?: string; // → device_name
|
|
111
|
+
productCode?: string; // → product_code
|
|
112
|
+
clearanceType?: string; // → clearance_type
|
|
113
|
+
kNumber?: string; // → k_number
|
|
114
|
+
state?: string; // → state (2-letter)
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Assemble the openFDA `search=` Lucene string from structured 510(k) filters — each
|
|
119
|
+
* value Lucene-escaped + phrase-quoted (reusing openfda.ts's `luceneQuote`), joined by
|
|
120
|
+
* ` AND `. Returns "" when no filter is present (openFDA then returns the whole
|
|
121
|
+
* collection). The clause → field mapping is FIXED here; a caller can never inject a
|
|
122
|
+
* raw field:value (no raw Lucene passthrough).
|
|
123
|
+
*/
|
|
124
|
+
export function buildDeviceSearch(f: OpenfdaDeviceFilters): string {
|
|
125
|
+
const clauses: string[] = [];
|
|
126
|
+
if (f.applicant !== undefined)
|
|
127
|
+
clauses.push(`applicant:${luceneQuote(f.applicant)}`);
|
|
128
|
+
if (f.deviceName !== undefined)
|
|
129
|
+
clauses.push(`device_name:${luceneQuote(f.deviceName)}`);
|
|
130
|
+
if (f.productCode !== undefined)
|
|
131
|
+
clauses.push(`product_code:${luceneQuote(f.productCode)}`);
|
|
132
|
+
if (f.clearanceType !== undefined)
|
|
133
|
+
clauses.push(`clearance_type:${luceneQuote(f.clearanceType)}`);
|
|
134
|
+
if (f.kNumber !== undefined)
|
|
135
|
+
clauses.push(`k_number:${luceneQuote(f.kNumber)}`);
|
|
136
|
+
if (f.state !== undefined)
|
|
137
|
+
clauses.push(`state:${luceneQuote(f.state.toUpperCase())}`);
|
|
138
|
+
return clauses.join(" AND ");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Tool: openfda_device_clearances ──────────────────────────────
|
|
142
|
+
export type OpenfdaDeviceClearancesArgs = OpenfdaDeviceFilters & {
|
|
143
|
+
limit?: number; // 1..100 (default 25)
|
|
144
|
+
skip?: number; // offset ≥ 0 (default 0)
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Search openFDA 510(k) DEVICE CLEARANCES with structured filters → curated clearance
|
|
149
|
+
* rows + honest `_meta`. KEYLESS (an OPTIONAL OPENFDA_API_KEY only raises the rate
|
|
150
|
+
* limit). totalAvailable = meta.results.total (EXACT); skip/limit offset pagination.
|
|
151
|
+
* ★A no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty, never a throw.
|
|
152
|
+
*/
|
|
153
|
+
export async function deviceClearances(
|
|
154
|
+
args: OpenfdaDeviceClearancesArgs,
|
|
155
|
+
): Promise<MetaBundle> {
|
|
156
|
+
// ── Validate + default the inputs (belt-and-suspenders behind the server Zod; a
|
|
157
|
+
// DIRECT handler call bypasses Zod). ──
|
|
158
|
+
if (args.state !== undefined && !STATE_RE.test(args.state)) {
|
|
159
|
+
throw new ToolErrorCarrier({
|
|
160
|
+
kind: "invalid_input",
|
|
161
|
+
retryable: false,
|
|
162
|
+
message: `Invalid state ${JSON.stringify(args.state)} — expected a 2-letter US state/territory postal code (^[A-Za-z]{2}$), e.g. "CA".`,
|
|
163
|
+
upstreamEndpoint: DEVICE_LABEL,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const limit = clampLimit(args.limit);
|
|
167
|
+
const skip = clampSkip(args.skip);
|
|
168
|
+
|
|
169
|
+
// ── Assemble the query (structured-only; all VALUES via URLSearchParams — no
|
|
170
|
+
// host/path steer). The OPTIONAL key rides ONLY here in &api_key=. ──
|
|
171
|
+
const filters: OpenfdaDeviceFilters = {
|
|
172
|
+
applicant: args.applicant,
|
|
173
|
+
deviceName: args.deviceName,
|
|
174
|
+
productCode: args.productCode,
|
|
175
|
+
clearanceType: args.clearanceType,
|
|
176
|
+
kNumber: args.kNumber,
|
|
177
|
+
state: args.state,
|
|
178
|
+
};
|
|
179
|
+
const search = buildDeviceSearch(filters);
|
|
180
|
+
const filtersApplied: string[] = [];
|
|
181
|
+
if (args.applicant !== undefined) filtersApplied.push("applicant");
|
|
182
|
+
if (args.deviceName !== undefined) filtersApplied.push("deviceName");
|
|
183
|
+
if (args.productCode !== undefined) filtersApplied.push("productCode");
|
|
184
|
+
if (args.clearanceType !== undefined) filtersApplied.push("clearanceType");
|
|
185
|
+
if (args.kNumber !== undefined) filtersApplied.push("kNumber");
|
|
186
|
+
if (args.state !== undefined) filtersApplied.push("state");
|
|
187
|
+
|
|
188
|
+
const params = new URLSearchParams();
|
|
189
|
+
if (search !== "") params.set("search", search);
|
|
190
|
+
params.set("limit", String(limit));
|
|
191
|
+
params.set("skip", String(skip));
|
|
192
|
+
const key = openfdaApiKey();
|
|
193
|
+
if (key !== undefined) params.set("api_key", key); // OPTIONAL — &api_key= ONLY
|
|
194
|
+
|
|
195
|
+
const url = `https://${OPENFDA_HOST}/device/510k.json?${params.toString()}`;
|
|
196
|
+
|
|
197
|
+
// ── Fetch + classify. ★P2 CRUX: a 404 whose body is {error:{code:"NOT_FOUND"}} is
|
|
198
|
+
// a genuine no-match (or an unknown field) ⇒ an HONEST EMPTY, never a thrown
|
|
199
|
+
// not_found. Any OTHER 4xx (e.g. 400 syntax) ⇒ invalid_input surfacing openFDA's
|
|
200
|
+
// message; a 5xx/429 ⇒ the shared taxonomy THROWS. (fetchOpenfda/readOpenfdaError
|
|
201
|
+
// are the SAME helpers as openfda.ts — the 404-reclassify logic is not reinvented.) ──
|
|
202
|
+
const res = await fetchOpenfda(url, DEVICE_LABEL);
|
|
203
|
+
|
|
204
|
+
if (res.status === 404) {
|
|
205
|
+
const { code, message } = await readOpenfdaError(res);
|
|
206
|
+
if (code === "NOT_FOUND") {
|
|
207
|
+
// Honest empty — NOT thrown, NOT not_found (the openFDA no-match idiom).
|
|
208
|
+
return emptyResult(limit, skip, filtersApplied, key !== undefined);
|
|
209
|
+
}
|
|
210
|
+
// A non-NOT_FOUND 404 ⇒ the shared not_found taxonomy (never a fake-empty).
|
|
211
|
+
throw new ToolErrorCarrier({
|
|
212
|
+
...errorFromResponse(res, DEVICE_LABEL),
|
|
213
|
+
message: message
|
|
214
|
+
? `openFDA returned HTTP 404 at ${DEVICE_LABEL}: ${message}`
|
|
215
|
+
: `Resource not found at ${DEVICE_LABEL} (HTTP 404).`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
220
|
+
// A 4xx OTHER than 404/429 (e.g. 400 syntax) ⇒ invalid_input surfacing the
|
|
221
|
+
// openFDA error message (a caller-fixable request, never a fake-empty).
|
|
222
|
+
const { message } = await readOpenfdaError(res);
|
|
223
|
+
throw new ToolErrorCarrier({
|
|
224
|
+
kind: "invalid_input",
|
|
225
|
+
retryable: false,
|
|
226
|
+
message: message
|
|
227
|
+
? `openFDA rejected the request (HTTP ${res.status}) at ${DEVICE_LABEL}: ${message}`
|
|
228
|
+
: `Bad request (HTTP ${res.status}) at ${DEVICE_LABEL}.`,
|
|
229
|
+
upstreamStatus: res.status,
|
|
230
|
+
upstreamEndpoint: DEVICE_LABEL,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
// 429 → rate_limited; 5xx → upstream_unavailable (a DOWN service is NEVER an empty
|
|
236
|
+
// result). Delegated to the shared errors.ts taxonomy.
|
|
237
|
+
throw new ToolErrorCarrier(errorFromResponse(res, DEVICE_LABEL));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── 200 ⇒ parse JSON; a non-JSON body ⇒ r.json() SyntaxError ⇒ schema_drift (never
|
|
241
|
+
// read as an empty result). ──
|
|
242
|
+
let body: unknown;
|
|
243
|
+
try {
|
|
244
|
+
body = await res.json();
|
|
245
|
+
} catch (e) {
|
|
246
|
+
if (e instanceof SyntaxError) {
|
|
247
|
+
throw driftError(
|
|
248
|
+
DEVICE_LABEL,
|
|
249
|
+
`openFDA ${DEVICE_LABEL} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── [P4] meta.results (the total carrier) and results (the rows) MUST be present +
|
|
256
|
+
// well-shaped; anything else is drift, never a fabricated empty. ──
|
|
257
|
+
const b = (body ?? {}) as { meta?: unknown; results?: unknown };
|
|
258
|
+
const meta = (b.meta ?? {}) as { results?: unknown };
|
|
259
|
+
const metaResults = meta.results as { total?: unknown } | undefined;
|
|
260
|
+
if (
|
|
261
|
+
metaResults === undefined ||
|
|
262
|
+
metaResults === null ||
|
|
263
|
+
typeof metaResults !== "object"
|
|
264
|
+
) {
|
|
265
|
+
throw driftError(
|
|
266
|
+
DEVICE_LABEL,
|
|
267
|
+
`openFDA ${DEVICE_LABEL} shape drift — meta.results (the skip/limit/total carrier) is missing.`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (!Array.isArray(b.results)) {
|
|
271
|
+
throw driftError(
|
|
272
|
+
DEVICE_LABEL,
|
|
273
|
+
`openFDA ${DEVICE_LABEL} shape drift — results must be an array.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const clearances = (b.results as unknown[]).map(mapClearance);
|
|
278
|
+
const returned = clearances.length;
|
|
279
|
+
|
|
280
|
+
// ── [P1] totalAvailable = meta.results.total EXACT (the REAL total), NEVER
|
|
281
|
+
// results.length. skip/limit offset pagination. ──
|
|
282
|
+
const rawTotal = metaResults.total;
|
|
283
|
+
const totalAvailable =
|
|
284
|
+
typeof rawTotal === "number" && Number.isFinite(rawTotal) ? rawTotal : null;
|
|
285
|
+
const hasMore = totalAvailable !== null && skip + returned < totalAvailable;
|
|
286
|
+
const nextOffset = hasMore ? skip + returned : null;
|
|
287
|
+
|
|
288
|
+
const notes: string[] = [NOT_DETERMINATION_NOTE, keyNote(key !== undefined)];
|
|
289
|
+
if (filtersApplied.length === 0) notes.push(NO_FILTER_NOTE);
|
|
290
|
+
|
|
291
|
+
return withMeta(
|
|
292
|
+
{ clearances },
|
|
293
|
+
{
|
|
294
|
+
// MODE only — never the key value (K-test).
|
|
295
|
+
source: `${OPENFDA_HOST} /device/510k (openFDA 510(k) device clearances; ${
|
|
296
|
+
key !== undefined ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"
|
|
297
|
+
})`,
|
|
298
|
+
keylessMode: true, // a keyless tool; the optional key only raises the rate limit
|
|
299
|
+
returned,
|
|
300
|
+
totalAvailable,
|
|
301
|
+
filtersApplied,
|
|
302
|
+
filtersDropped: [],
|
|
303
|
+
fieldsUnavailable: [],
|
|
304
|
+
pagination: { offset: skip, limit, hasMore, nextOffset },
|
|
305
|
+
notes,
|
|
306
|
+
} satisfies Partial<ResponseMeta>,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Small helpers ────────────────────────────────────────────────
|
|
311
|
+
function keyNote(hasKey: boolean): string {
|
|
312
|
+
return hasKey ? KEYED_NOTE : KEYLESS_NOTE;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** An honest empty result (★P2: a 404 NOT_FOUND no-match) — returned:0, total:0. */
|
|
316
|
+
function emptyResult(
|
|
317
|
+
limit: number,
|
|
318
|
+
skip: number,
|
|
319
|
+
filtersApplied: string[],
|
|
320
|
+
hasKey: boolean,
|
|
321
|
+
): MetaBundle {
|
|
322
|
+
return withMeta(
|
|
323
|
+
{ clearances: [] as OpenfdaClearance[] },
|
|
324
|
+
{
|
|
325
|
+
source: `${OPENFDA_HOST} /device/510k (openFDA 510(k) device clearances; ${
|
|
326
|
+
hasKey ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"
|
|
327
|
+
})`,
|
|
328
|
+
keylessMode: true,
|
|
329
|
+
returned: 0,
|
|
330
|
+
totalAvailable: 0,
|
|
331
|
+
filtersApplied,
|
|
332
|
+
filtersDropped: [],
|
|
333
|
+
fieldsUnavailable: [],
|
|
334
|
+
pagination: { offset: skip, limit, hasMore: false, nextOffset: null },
|
|
335
|
+
notes: [
|
|
336
|
+
"No 510(k) device clearances matched this query (openFDA returned HTTP 404 NOT_FOUND — the source's honest no-match). This is an exact empty, not an error.",
|
|
337
|
+
NOT_DETERMINATION_NOTE,
|
|
338
|
+
keyNote(hasKey),
|
|
339
|
+
],
|
|
340
|
+
} satisfies Partial<ResponseMeta>,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function clampLimit(v: unknown): number {
|
|
345
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return DEFAULT_LIMIT;
|
|
346
|
+
const n = Math.floor(v);
|
|
347
|
+
if (n < 1) return 1;
|
|
348
|
+
if (n > MAX_LIMIT) return MAX_LIMIT;
|
|
349
|
+
return n;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function clampSkip(v: unknown): number {
|
|
353
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return 0;
|
|
354
|
+
const n = Math.floor(v);
|
|
355
|
+
return n < 0 ? 0 : n;
|
|
356
|
+
}
|