@cliwant/mcp-sam-gov 1.4.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 +11 -11
- package/README.ko.md +11 -11
- package/README.md +21 -13
- 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/datagov-catalog.d.ts.map +1 -1
- package/dist/datagov-catalog.js +16 -2
- package/dist/datagov-catalog.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.map +1 -1
- package/dist/epa-envirofacts.js +14 -1
- package/dist/epa-envirofacts.js.map +1 -1
- 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/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/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/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-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.map +1 -1
- package/dist/openfda.js +31 -8
- package/dist/openfda.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +331 -10
- 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 +2 -2
- package/src/cbp-border.ts +177 -0
- package/src/datagov-catalog.ts +18 -2
- package/src/ecfr.ts +27 -10
- package/src/edgar.ts +39 -7
- package/src/epa-envirofacts.ts +17 -1
- 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/gov-domains.ts +237 -0
- package/src/nist-controls.ts +219 -0
- package/src/nws-weather.ts +167 -0
- package/src/openfda-drugsfda.ts +313 -0
- package/src/openfda.ts +30 -7
- package/src/server.ts +352 -10
- package/src/treasury.ts +7 -0
- package/src/usaspending.ts +189 -17
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nws-weather.ts — National Weather Service active alerts (api.weather.gov, KEYLESS)
|
|
3
|
+
* — the DISASTER / CLIMATE-READINESS lane. Current watches, warnings, and advisories
|
|
4
|
+
* (event, severity, urgency, area, effective/expires window, instructions). Pairs
|
|
5
|
+
* directly with the FEMA tools (disaster declarations → public assistance → hazard
|
|
6
|
+
* mitigation → LIVE active weather) to complete a disaster-response-readiness view:
|
|
7
|
+
* where severe-weather events are active NOW, ahead of the declarations/contracts
|
|
8
|
+
* that follow.
|
|
9
|
+
*
|
|
10
|
+
* SOURCE: NWS api.weather.gov (a .gov host), keyless. Requires a descriptive
|
|
11
|
+
* User-Agent (NWS policy) — sent from a fixed constant; no token. Returns a GeoJSON
|
|
12
|
+
* FeatureCollection. This is REAL-TIME data (currently-active alerts) — freshness is
|
|
13
|
+
* disclosed and never implied to be a historical series.
|
|
14
|
+
*
|
|
15
|
+
* HONESTY: fixed host + redirect:"error" (SSRF); `state` is a 2-letter charclass (the
|
|
16
|
+
* server-side ?area= filter); event/severity are applied CLIENT-SIDE over the returned
|
|
17
|
+
* set; a non-FeatureCollection / non-array body ⇒ driftError; an outage/4xx/timeout
|
|
18
|
+
* THROWS. A genuine no-active-alerts result ⇒ an HONEST EMPTY (returned:0), never an
|
|
19
|
+
* error. totalAvailable = the EXACT count of matched active alerts. Every scalar via
|
|
20
|
+
* `str` (null-never-empty-string); dates preserved as ISO strings.
|
|
21
|
+
*/
|
|
22
|
+
import { type MetaBundle } from "./meta.js";
|
|
23
|
+
export declare const NWS_HOST = "api.weather.gov";
|
|
24
|
+
export type NwsAlert = {
|
|
25
|
+
id: string | null;
|
|
26
|
+
event: string | null;
|
|
27
|
+
headline: string | null;
|
|
28
|
+
severity: string | null;
|
|
29
|
+
urgency: string | null;
|
|
30
|
+
certainty: string | null;
|
|
31
|
+
category: string | null;
|
|
32
|
+
status: string | null;
|
|
33
|
+
messageType: string | null;
|
|
34
|
+
areaDesc: string | null;
|
|
35
|
+
effective: string | null;
|
|
36
|
+
onset: string | null;
|
|
37
|
+
expires: string | null;
|
|
38
|
+
ends: string | null;
|
|
39
|
+
senderName: string | null;
|
|
40
|
+
description: string | null;
|
|
41
|
+
instruction: string | null;
|
|
42
|
+
response: string | null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* List CURRENTLY-ACTIVE NWS weather alerts, optionally scoped by `state` (server-side
|
|
46
|
+
* ?area=) and filtered client-side by `event` (substring) and/or `severity` (exact).
|
|
47
|
+
* Honest `_meta` (exact match total + real-time freshness; a no-alerts result is an
|
|
48
|
+
* honest empty).
|
|
49
|
+
*/
|
|
50
|
+
export declare function activeAlerts(args: {
|
|
51
|
+
state?: string;
|
|
52
|
+
event?: string;
|
|
53
|
+
severity?: string;
|
|
54
|
+
limit?: number;
|
|
55
|
+
offset?: number;
|
|
56
|
+
}): Promise<MetaBundle>;
|
|
57
|
+
//# sourceMappingURL=nws-weather.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nws-weather.d.ts","sourceRoot":"","sources":["../src/nws-weather.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAEzE,eAAO,MAAM,QAAQ,oBAAoB,CAAC;AAa1C,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AA+CF;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgDtB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nws-weather.ts — National Weather Service active alerts (api.weather.gov, KEYLESS)
|
|
3
|
+
* — the DISASTER / CLIMATE-READINESS lane. Current watches, warnings, and advisories
|
|
4
|
+
* (event, severity, urgency, area, effective/expires window, instructions). Pairs
|
|
5
|
+
* directly with the FEMA tools (disaster declarations → public assistance → hazard
|
|
6
|
+
* mitigation → LIVE active weather) to complete a disaster-response-readiness view:
|
|
7
|
+
* where severe-weather events are active NOW, ahead of the declarations/contracts
|
|
8
|
+
* that follow.
|
|
9
|
+
*
|
|
10
|
+
* SOURCE: NWS api.weather.gov (a .gov host), keyless. Requires a descriptive
|
|
11
|
+
* User-Agent (NWS policy) — sent from a fixed constant; no token. Returns a GeoJSON
|
|
12
|
+
* FeatureCollection. This is REAL-TIME data (currently-active alerts) — freshness is
|
|
13
|
+
* disclosed and never implied to be a historical series.
|
|
14
|
+
*
|
|
15
|
+
* HONESTY: fixed host + redirect:"error" (SSRF); `state` is a 2-letter charclass (the
|
|
16
|
+
* server-side ?area= filter); event/severity are applied CLIENT-SIDE over the returned
|
|
17
|
+
* set; a non-FeatureCollection / non-array body ⇒ driftError; an outage/4xx/timeout
|
|
18
|
+
* THROWS. A genuine no-active-alerts result ⇒ an HONEST EMPTY (returned:0), never an
|
|
19
|
+
* error. totalAvailable = the EXACT count of matched active alerts. Every scalar via
|
|
20
|
+
* `str` (null-never-empty-string); dates preserved as ISO strings.
|
|
21
|
+
*/
|
|
22
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
23
|
+
import { getJson, driftError } from "./datasource.js";
|
|
24
|
+
import { str } from "./coerce.js";
|
|
25
|
+
import { withMeta } from "./meta.js";
|
|
26
|
+
export const NWS_HOST = "api.weather.gov";
|
|
27
|
+
const NWS_ACTIVE_URL = "https://api.weather.gov/alerts/active";
|
|
28
|
+
const NWS_LABEL = "nws:/alerts/active";
|
|
29
|
+
const NWS_TIMEOUT_MS = 15_000;
|
|
30
|
+
// NWS asks every client to send a descriptive User-Agent (with contact). No token.
|
|
31
|
+
const NWS_USER_AGENT = "cliwant-mcp-sam-gov (https://github.com/cliwant/mcp-sam-gov)";
|
|
32
|
+
const STATE_RE = /^[A-Za-z]{2}$/;
|
|
33
|
+
const PROVENANCE_NOTE = "Source: NWS api.weather.gov active-alerts feed (keyless; a descriptive User-Agent is sent per NWS policy).";
|
|
34
|
+
const FRESHNESS_NOTE = "REAL-TIME: these are the alerts ACTIVE at request time (a live snapshot, not a historical archive). Read effective/onset/expires/ends for each alert's window; an expired alert is not returned. A no-active-alerts result is an HONEST empty (returned:0), never an error.";
|
|
35
|
+
function mapAlert(feature) {
|
|
36
|
+
const p = (feature ?? {}).properties ?? {};
|
|
37
|
+
return {
|
|
38
|
+
id: str(p.id),
|
|
39
|
+
event: str(p.event),
|
|
40
|
+
headline: str(p.headline),
|
|
41
|
+
severity: str(p.severity),
|
|
42
|
+
urgency: str(p.urgency),
|
|
43
|
+
certainty: str(p.certainty),
|
|
44
|
+
category: str(p.category),
|
|
45
|
+
status: str(p.status),
|
|
46
|
+
messageType: str(p.messageType),
|
|
47
|
+
areaDesc: str(p.areaDesc),
|
|
48
|
+
effective: str(p.effective),
|
|
49
|
+
onset: str(p.onset),
|
|
50
|
+
expires: str(p.expires),
|
|
51
|
+
ends: str(p.ends),
|
|
52
|
+
senderName: str(p.senderName),
|
|
53
|
+
description: str(p.description),
|
|
54
|
+
instruction: str(p.instruction),
|
|
55
|
+
response: str(p.response),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function loadActiveAlerts(state) {
|
|
59
|
+
const params = new URLSearchParams();
|
|
60
|
+
if (state !== undefined)
|
|
61
|
+
params.set("area", state.toUpperCase());
|
|
62
|
+
const url = params.toString() ? `${NWS_ACTIVE_URL}?${params.toString()}` : NWS_ACTIVE_URL;
|
|
63
|
+
const built = new URL(url);
|
|
64
|
+
if (built.hostname !== NWS_HOST || built.protocol !== "https:") {
|
|
65
|
+
throw driftError(NWS_LABEL, `Constructed NWS URL host ${JSON.stringify(built.hostname)} is not ${NWS_HOST} over https — refusing to fetch (SSRF safety).`);
|
|
66
|
+
}
|
|
67
|
+
const body = (await getJson(url, {
|
|
68
|
+
label: NWS_LABEL,
|
|
69
|
+
redirect: "error",
|
|
70
|
+
timeoutMs: NWS_TIMEOUT_MS,
|
|
71
|
+
headers: { "User-Agent": NWS_USER_AGENT, Accept: "application/geo+json" },
|
|
72
|
+
}));
|
|
73
|
+
if (body.type !== "FeatureCollection" || !Array.isArray(body.features)) {
|
|
74
|
+
throw driftError(NWS_LABEL, "NWS active-alerts body is not a GeoJSON FeatureCollection with a features[] array — schema drift, never a fake-empty result.");
|
|
75
|
+
}
|
|
76
|
+
return body.features.map(mapAlert);
|
|
77
|
+
}
|
|
78
|
+
// ─── Tool: nws_active_alerts ──────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* List CURRENTLY-ACTIVE NWS weather alerts, optionally scoped by `state` (server-side
|
|
81
|
+
* ?area=) and filtered client-side by `event` (substring) and/or `severity` (exact).
|
|
82
|
+
* Honest `_meta` (exact match total + real-time freshness; a no-alerts result is an
|
|
83
|
+
* honest empty).
|
|
84
|
+
*/
|
|
85
|
+
export async function activeAlerts(args) {
|
|
86
|
+
if (args.state !== undefined && !STATE_RE.test(args.state)) {
|
|
87
|
+
throw new ToolErrorCarrier({
|
|
88
|
+
kind: "invalid_input",
|
|
89
|
+
retryable: false,
|
|
90
|
+
message: `Invalid state ${JSON.stringify(args.state)} — expected a 2-letter US state/territory code (^[A-Za-z]{2}$), e.g. "CA".`,
|
|
91
|
+
upstreamEndpoint: NWS_LABEL,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const limit = args.limit ?? 50;
|
|
95
|
+
const offset = args.offset ?? 0;
|
|
96
|
+
const all = await loadActiveAlerts(args.state);
|
|
97
|
+
const filtersApplied = [];
|
|
98
|
+
if (args.state !== undefined)
|
|
99
|
+
filtersApplied.push("state");
|
|
100
|
+
const eventQ = args.event?.trim().toLowerCase();
|
|
101
|
+
const sevQ = args.severity?.trim().toLowerCase();
|
|
102
|
+
if (args.event !== undefined)
|
|
103
|
+
filtersApplied.push("event");
|
|
104
|
+
if (args.severity !== undefined)
|
|
105
|
+
filtersApplied.push("severity");
|
|
106
|
+
const matched = all.filter((a) => {
|
|
107
|
+
if (eventQ && !(a.event ?? "").toLowerCase().includes(eventQ))
|
|
108
|
+
return false;
|
|
109
|
+
if (sevQ && (a.severity ?? "").toLowerCase() !== sevQ)
|
|
110
|
+
return false;
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
const totalAvailable = matched.length;
|
|
114
|
+
const page = matched.slice(offset, offset + limit);
|
|
115
|
+
const returned = page.length;
|
|
116
|
+
const hasMore = offset + returned < totalAvailable;
|
|
117
|
+
const nextOffset = hasMore ? offset + returned : null;
|
|
118
|
+
return withMeta({ alerts: page }, {
|
|
119
|
+
source: "api.weather.gov active alerts (NWS, keyless)",
|
|
120
|
+
keylessMode: true,
|
|
121
|
+
returned,
|
|
122
|
+
totalAvailable,
|
|
123
|
+
truncated: hasMore,
|
|
124
|
+
filtersApplied,
|
|
125
|
+
filtersDropped: [],
|
|
126
|
+
fieldsUnavailable: [],
|
|
127
|
+
pagination: { offset, limit, hasMore, nextOffset },
|
|
128
|
+
notes: [PROVENANCE_NOTE, FRESHNESS_NOTE],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=nws-weather.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nws-weather.js","sourceRoot":"","sources":["../src/nws-weather.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openfda-drugsfda.ts — openFDA DRUG APPROVALS (Drugs@FDA, api.fda.gov) — the
|
|
3
|
+
* PHARMA REGULATORY-APPROVAL lane. FDA-approved drug applications (NDA/ANDA/BLA):
|
|
4
|
+
* the sponsor, the application number, each approved product (brand/generic name,
|
|
5
|
+
* dosage form, route, marketing status), and the submission/approval history.
|
|
6
|
+
* Answers "what drugs did sponsor X get approved, and which are still marketed" —
|
|
7
|
+
* pharma vendor product/approval intelligence.
|
|
8
|
+
*
|
|
9
|
+
* ★ SAME SOURCE + ENVELOPE + CRUX as openfda.ts / openfda-device.ts (ADR-0054/0056).
|
|
10
|
+
* Same host (api.fda.gov), same envelope `{ meta:{ results:{ skip,limit,total }},
|
|
11
|
+
* results:[…] }`, same ★no-match→HTTP-404-NOT_FOUND-as-honest-empty crux, same
|
|
12
|
+
* optional query-key K-test, same fixed-host SSRF idiom, same structured-only (no
|
|
13
|
+
* raw Lucene passthrough) search. REUSES openfda.ts's `fetchOpenfda`,
|
|
14
|
+
* `readOpenfdaError`, `luceneQuote`, `openfdaApiKey`, `OPENFDA_HOST` verbatim.
|
|
15
|
+
*
|
|
16
|
+
* GET https://api.fda.gov/drug/drugsfda.json?search=<lucene>&limit=&skip=[&api_key=]
|
|
17
|
+
* → { meta:{ results:{ skip,limit,total }}, results:[ { application_number,
|
|
18
|
+
* sponsor_name, products:[…], submissions:[…] } ] }.
|
|
19
|
+
*
|
|
20
|
+
* ★ HONESTY (mirrors the siblings exactly): [P1] totalAvailable = meta.results.total
|
|
21
|
+
* EXACT (never results.length); skip/limit offset pagination. [★P2] 404 NOT_FOUND
|
|
22
|
+
* ⇒ honest empty (never thrown); other 4xx ⇒ invalid_input surfacing openFDA's
|
|
23
|
+
* message; 5xx/429 ⇒ THROW. [P3] every scalar via `str` (null-never-""); nested
|
|
24
|
+
* products/submissions arrays default to [] and each field is str (never fabricated).
|
|
25
|
+
* [P4] meta.results / results absent-or-mis-shaped ⇒ driftError. [K-test] the
|
|
26
|
+
* OPTIONAL OPENFDA_API_KEY rides ONLY &api_key=; label/source name mode only.
|
|
27
|
+
* [SSRF] fixed host; all VALUES Lucene-escaped + phrase-quoted via URLSearchParams.
|
|
28
|
+
*/
|
|
29
|
+
import { type MetaBundle } from "./meta.js";
|
|
30
|
+
export type DrugsfdaProduct = {
|
|
31
|
+
brandName: string | null;
|
|
32
|
+
genericIngredients: {
|
|
33
|
+
name: string | null;
|
|
34
|
+
strength: string | null;
|
|
35
|
+
}[];
|
|
36
|
+
dosageForm: string | null;
|
|
37
|
+
route: string | null;
|
|
38
|
+
marketingStatus: string | null;
|
|
39
|
+
};
|
|
40
|
+
export type DrugsfdaSubmission = {
|
|
41
|
+
submissionType: string | null;
|
|
42
|
+
submissionNumber: string | null;
|
|
43
|
+
submissionStatus: string | null;
|
|
44
|
+
submissionStatusDate: string | null;
|
|
45
|
+
submissionClass: string | null;
|
|
46
|
+
};
|
|
47
|
+
export type DrugsfdaApplication = {
|
|
48
|
+
applicationNumber: string | null;
|
|
49
|
+
sponsorName: string | null;
|
|
50
|
+
products: DrugsfdaProduct[];
|
|
51
|
+
submissions: DrugsfdaSubmission[];
|
|
52
|
+
};
|
|
53
|
+
export type DrugsfdaFilters = {
|
|
54
|
+
sponsorName?: string;
|
|
55
|
+
brandName?: string;
|
|
56
|
+
activeIngredient?: string;
|
|
57
|
+
applicationNumber?: string;
|
|
58
|
+
};
|
|
59
|
+
export declare function buildDrugsfdaSearch(f: DrugsfdaFilters): string;
|
|
60
|
+
export type DrugApprovalsArgs = DrugsfdaFilters & {
|
|
61
|
+
limit?: number;
|
|
62
|
+
skip?: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Search openFDA Drugs@FDA drug-approval applications with structured filters →
|
|
66
|
+
* curated application rows (sponsor, application number, approved products, submission
|
|
67
|
+
* history) + honest `_meta`. KEYLESS (OPTIONAL OPENFDA_API_KEY only raises the rate
|
|
68
|
+
* limit). totalAvailable = meta.results.total (EXACT); skip/limit pagination. A
|
|
69
|
+
* no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty, never a throw.
|
|
70
|
+
*/
|
|
71
|
+
export declare function drugApprovals(args: DrugApprovalsArgs): Promise<MetaBundle>;
|
|
72
|
+
//# sourceMappingURL=openfda-drugsfda.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openfda-drugsfda.d.ts","sourceRoot":"","sources":["../src/openfda-drugsfda.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AA4BzE,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;IACvE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AACF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AACF,MAAM,MAAM,mBAAmB,GAAG;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC,CAAC;AAsCF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAW9D;AAGD,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAmHhF"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openfda-drugsfda.ts — openFDA DRUG APPROVALS (Drugs@FDA, api.fda.gov) — the
|
|
3
|
+
* PHARMA REGULATORY-APPROVAL lane. FDA-approved drug applications (NDA/ANDA/BLA):
|
|
4
|
+
* the sponsor, the application number, each approved product (brand/generic name,
|
|
5
|
+
* dosage form, route, marketing status), and the submission/approval history.
|
|
6
|
+
* Answers "what drugs did sponsor X get approved, and which are still marketed" —
|
|
7
|
+
* pharma vendor product/approval intelligence.
|
|
8
|
+
*
|
|
9
|
+
* ★ SAME SOURCE + ENVELOPE + CRUX as openfda.ts / openfda-device.ts (ADR-0054/0056).
|
|
10
|
+
* Same host (api.fda.gov), same envelope `{ meta:{ results:{ skip,limit,total }},
|
|
11
|
+
* results:[…] }`, same ★no-match→HTTP-404-NOT_FOUND-as-honest-empty crux, same
|
|
12
|
+
* optional query-key K-test, same fixed-host SSRF idiom, same structured-only (no
|
|
13
|
+
* raw Lucene passthrough) search. REUSES openfda.ts's `fetchOpenfda`,
|
|
14
|
+
* `readOpenfdaError`, `luceneQuote`, `openfdaApiKey`, `OPENFDA_HOST` verbatim.
|
|
15
|
+
*
|
|
16
|
+
* GET https://api.fda.gov/drug/drugsfda.json?search=<lucene>&limit=&skip=[&api_key=]
|
|
17
|
+
* → { meta:{ results:{ skip,limit,total }}, results:[ { application_number,
|
|
18
|
+
* sponsor_name, products:[…], submissions:[…] } ] }.
|
|
19
|
+
*
|
|
20
|
+
* ★ HONESTY (mirrors the siblings exactly): [P1] totalAvailable = meta.results.total
|
|
21
|
+
* EXACT (never results.length); skip/limit offset pagination. [★P2] 404 NOT_FOUND
|
|
22
|
+
* ⇒ honest empty (never thrown); other 4xx ⇒ invalid_input surfacing openFDA's
|
|
23
|
+
* message; 5xx/429 ⇒ THROW. [P3] every scalar via `str` (null-never-""); nested
|
|
24
|
+
* products/submissions arrays default to [] and each field is str (never fabricated).
|
|
25
|
+
* [P4] meta.results / results absent-or-mis-shaped ⇒ driftError. [K-test] the
|
|
26
|
+
* OPTIONAL OPENFDA_API_KEY rides ONLY &api_key=; label/source name mode only.
|
|
27
|
+
* [SSRF] fixed host; all VALUES Lucene-escaped + phrase-quoted via URLSearchParams.
|
|
28
|
+
*/
|
|
29
|
+
import { ToolErrorCarrier, errorFromResponse } from "./errors.js";
|
|
30
|
+
import { driftError } from "./datasource.js";
|
|
31
|
+
import { str } from "./coerce.js";
|
|
32
|
+
import { withMeta } from "./meta.js";
|
|
33
|
+
import { OPENFDA_HOST, fetchOpenfda, readOpenfdaError, luceneQuote, openfdaApiKey, } from "./openfda.js";
|
|
34
|
+
const DEFAULT_LIMIT = 25;
|
|
35
|
+
const MAX_LIMIT = 100;
|
|
36
|
+
/** host+path-only label (→ ToolError.upstreamEndpoint); NEVER carries the key. */
|
|
37
|
+
const DRUGSFDA_LABEL = "openfda:/drug/drugsfda";
|
|
38
|
+
// ─── Honesty notes ────────────────────────────────────────────────
|
|
39
|
+
const NOT_DETERMINATION_NOTE = "openFDA Drugs@FDA records are FDA-published drug-application data; treat approval/marketing status as of the source's last publication. This is reference data, not a live regulatory determination.";
|
|
40
|
+
const KEYLESS_NOTE = "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/).";
|
|
41
|
+
const KEYED_NOTE = "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.";
|
|
42
|
+
const NO_FILTER_NOTE = "No structured filters were applied — this is an unscoped scan of the WHOLE Drugs@FDA collection. Add sponsorName / brandName / activeIngredient / applicationNumber to scope the result set.";
|
|
43
|
+
const MARKETING_NOTE = "products[].marketingStatus is the FDA marketing category (e.g. 'Prescription', 'Over-the-counter', 'Discontinued', 'None (Tentative Approval)') — a 'Discontinued' product is NOT an approval revocation. submissions[] is the application's action history (ORIG approval + subsequent supplements).";
|
|
44
|
+
/** Map ONE Drugs@FDA result row → the curated shape. Every scalar via `str`. */
|
|
45
|
+
function mapApplication(row) {
|
|
46
|
+
const r = (row ?? {});
|
|
47
|
+
const products = Array.isArray(r.products) ? r.products : [];
|
|
48
|
+
const submissions = Array.isArray(r.submissions) ? r.submissions : [];
|
|
49
|
+
return {
|
|
50
|
+
applicationNumber: str(r.application_number),
|
|
51
|
+
sponsorName: str(r.sponsor_name),
|
|
52
|
+
products: products.map((p) => {
|
|
53
|
+
const pr = (p ?? {});
|
|
54
|
+
const ings = Array.isArray(pr.active_ingredients) ? pr.active_ingredients : [];
|
|
55
|
+
return {
|
|
56
|
+
brandName: str(pr.brand_name),
|
|
57
|
+
genericIngredients: ings.map((a) => {
|
|
58
|
+
const ai = (a ?? {});
|
|
59
|
+
return { name: str(ai.name), strength: str(ai.strength) };
|
|
60
|
+
}),
|
|
61
|
+
dosageForm: str(pr.dosage_form),
|
|
62
|
+
route: str(pr.route),
|
|
63
|
+
marketingStatus: str(pr.marketing_status),
|
|
64
|
+
};
|
|
65
|
+
}),
|
|
66
|
+
submissions: submissions.map((s) => {
|
|
67
|
+
const su = (s ?? {});
|
|
68
|
+
return {
|
|
69
|
+
submissionType: str(su.submission_type),
|
|
70
|
+
submissionNumber: str(su.submission_number),
|
|
71
|
+
submissionStatus: str(su.submission_status),
|
|
72
|
+
submissionStatusDate: str(su.submission_status_date),
|
|
73
|
+
submissionClass: str(su.submission_class_code_description),
|
|
74
|
+
};
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function buildDrugsfdaSearch(f) {
|
|
79
|
+
const clauses = [];
|
|
80
|
+
if (f.sponsorName !== undefined)
|
|
81
|
+
clauses.push(`sponsor_name:${luceneQuote(f.sponsorName)}`);
|
|
82
|
+
if (f.brandName !== undefined)
|
|
83
|
+
clauses.push(`products.brand_name:${luceneQuote(f.brandName)}`);
|
|
84
|
+
if (f.activeIngredient !== undefined)
|
|
85
|
+
clauses.push(`products.active_ingredients.name:${luceneQuote(f.activeIngredient)}`);
|
|
86
|
+
if (f.applicationNumber !== undefined)
|
|
87
|
+
clauses.push(`application_number:${luceneQuote(f.applicationNumber)}`);
|
|
88
|
+
return clauses.join(" AND ");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Search openFDA Drugs@FDA drug-approval applications with structured filters →
|
|
92
|
+
* curated application rows (sponsor, application number, approved products, submission
|
|
93
|
+
* history) + honest `_meta`. KEYLESS (OPTIONAL OPENFDA_API_KEY only raises the rate
|
|
94
|
+
* limit). totalAvailable = meta.results.total (EXACT); skip/limit pagination. A
|
|
95
|
+
* no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty, never a throw.
|
|
96
|
+
*/
|
|
97
|
+
export async function drugApprovals(args) {
|
|
98
|
+
const limit = clampLimit(args.limit);
|
|
99
|
+
const skip = clampSkip(args.skip);
|
|
100
|
+
const filters = {
|
|
101
|
+
sponsorName: args.sponsorName,
|
|
102
|
+
brandName: args.brandName,
|
|
103
|
+
activeIngredient: args.activeIngredient,
|
|
104
|
+
applicationNumber: args.applicationNumber,
|
|
105
|
+
};
|
|
106
|
+
const search = buildDrugsfdaSearch(filters);
|
|
107
|
+
const filtersApplied = [];
|
|
108
|
+
if (args.sponsorName !== undefined)
|
|
109
|
+
filtersApplied.push("sponsorName");
|
|
110
|
+
if (args.brandName !== undefined)
|
|
111
|
+
filtersApplied.push("brandName");
|
|
112
|
+
if (args.activeIngredient !== undefined)
|
|
113
|
+
filtersApplied.push("activeIngredient");
|
|
114
|
+
if (args.applicationNumber !== undefined)
|
|
115
|
+
filtersApplied.push("applicationNumber");
|
|
116
|
+
const params = new URLSearchParams();
|
|
117
|
+
if (search !== "")
|
|
118
|
+
params.set("search", search);
|
|
119
|
+
params.set("limit", String(limit));
|
|
120
|
+
params.set("skip", String(skip));
|
|
121
|
+
const key = openfdaApiKey();
|
|
122
|
+
if (key !== undefined)
|
|
123
|
+
params.set("api_key", key); // OPTIONAL — &api_key= ONLY
|
|
124
|
+
const url = `https://${OPENFDA_HOST}/drug/drugsfda.json?${params.toString()}`;
|
|
125
|
+
const res = await fetchOpenfda(url, DRUGSFDA_LABEL);
|
|
126
|
+
if (res.status === 404) {
|
|
127
|
+
const { code, message } = await readOpenfdaError(res);
|
|
128
|
+
if (code === "NOT_FOUND") {
|
|
129
|
+
return emptyResult(limit, skip, filtersApplied, key !== undefined);
|
|
130
|
+
}
|
|
131
|
+
throw new ToolErrorCarrier({
|
|
132
|
+
...errorFromResponse(res, DRUGSFDA_LABEL),
|
|
133
|
+
message: message
|
|
134
|
+
? `openFDA returned HTTP 404 at ${DRUGSFDA_LABEL}: ${message}`
|
|
135
|
+
: `Resource not found at ${DRUGSFDA_LABEL} (HTTP 404).`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
139
|
+
const { message } = await readOpenfdaError(res);
|
|
140
|
+
throw new ToolErrorCarrier({
|
|
141
|
+
kind: "invalid_input",
|
|
142
|
+
retryable: false,
|
|
143
|
+
message: message
|
|
144
|
+
? `openFDA rejected the request (HTTP ${res.status}) at ${DRUGSFDA_LABEL}: ${message}`
|
|
145
|
+
: `Bad request (HTTP ${res.status}) at ${DRUGSFDA_LABEL}.`,
|
|
146
|
+
upstreamStatus: res.status,
|
|
147
|
+
upstreamEndpoint: DRUGSFDA_LABEL,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
throw new ToolErrorCarrier(errorFromResponse(res, DRUGSFDA_LABEL));
|
|
152
|
+
}
|
|
153
|
+
let body;
|
|
154
|
+
try {
|
|
155
|
+
body = await res.json();
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
if (e instanceof SyntaxError) {
|
|
159
|
+
throw driftError(DRUGSFDA_LABEL, `openFDA ${DRUGSFDA_LABEL} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`);
|
|
160
|
+
}
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
const b = (body ?? {});
|
|
164
|
+
const meta = (b.meta ?? {});
|
|
165
|
+
const metaResults = meta.results;
|
|
166
|
+
if (metaResults === undefined || metaResults === null || typeof metaResults !== "object") {
|
|
167
|
+
throw driftError(DRUGSFDA_LABEL, `openFDA ${DRUGSFDA_LABEL} shape drift — meta.results (the skip/limit/total carrier) is missing.`);
|
|
168
|
+
}
|
|
169
|
+
if (!Array.isArray(b.results)) {
|
|
170
|
+
throw driftError(DRUGSFDA_LABEL, `openFDA ${DRUGSFDA_LABEL} shape drift — results must be an array.`);
|
|
171
|
+
}
|
|
172
|
+
const applications = b.results.map(mapApplication);
|
|
173
|
+
const returned = applications.length;
|
|
174
|
+
const rawTotal = metaResults.total;
|
|
175
|
+
const totalAvailable = typeof rawTotal === "number" && Number.isFinite(rawTotal) ? rawTotal : null;
|
|
176
|
+
const hasMore = totalAvailable !== null && skip + returned < totalAvailable;
|
|
177
|
+
const nextOffset = hasMore ? skip + returned : null;
|
|
178
|
+
const notes = [NOT_DETERMINATION_NOTE, MARKETING_NOTE, keyNote(key !== undefined)];
|
|
179
|
+
if (filtersApplied.length === 0)
|
|
180
|
+
notes.push(NO_FILTER_NOTE);
|
|
181
|
+
return withMeta({ applications }, {
|
|
182
|
+
source: `${OPENFDA_HOST} /drug/drugsfda (openFDA Drugs@FDA approvals; ${key !== undefined ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"})`,
|
|
183
|
+
keylessMode: true,
|
|
184
|
+
returned,
|
|
185
|
+
totalAvailable,
|
|
186
|
+
filtersApplied,
|
|
187
|
+
filtersDropped: [],
|
|
188
|
+
fieldsUnavailable: [],
|
|
189
|
+
pagination: { offset: skip, limit, hasMore, nextOffset },
|
|
190
|
+
notes,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// ─── Small helpers ────────────────────────────────────────────────
|
|
194
|
+
function keyNote(hasKey) {
|
|
195
|
+
return hasKey ? KEYED_NOTE : KEYLESS_NOTE;
|
|
196
|
+
}
|
|
197
|
+
function emptyResult(limit, skip, filtersApplied, hasKey) {
|
|
198
|
+
return withMeta({ applications: [] }, {
|
|
199
|
+
source: `${OPENFDA_HOST} /drug/drugsfda (openFDA Drugs@FDA approvals; ${hasKey ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"})`,
|
|
200
|
+
keylessMode: true,
|
|
201
|
+
returned: 0,
|
|
202
|
+
totalAvailable: 0,
|
|
203
|
+
filtersApplied,
|
|
204
|
+
filtersDropped: [],
|
|
205
|
+
fieldsUnavailable: [],
|
|
206
|
+
pagination: { offset: skip, limit, hasMore: false, nextOffset: null },
|
|
207
|
+
notes: [
|
|
208
|
+
"No Drugs@FDA applications matched this query (openFDA returned HTTP 404 NOT_FOUND — the source's honest no-match). This is an exact empty, not an error.",
|
|
209
|
+
NOT_DETERMINATION_NOTE,
|
|
210
|
+
keyNote(hasKey),
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function clampLimit(v) {
|
|
215
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
216
|
+
return DEFAULT_LIMIT;
|
|
217
|
+
const n = Math.floor(v);
|
|
218
|
+
if (n < 1)
|
|
219
|
+
return 1;
|
|
220
|
+
if (n > MAX_LIMIT)
|
|
221
|
+
return MAX_LIMIT;
|
|
222
|
+
return n;
|
|
223
|
+
}
|
|
224
|
+
function clampSkip(v) {
|
|
225
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
226
|
+
return 0;
|
|
227
|
+
const n = Math.floor(v);
|
|
228
|
+
return n < 0 ? 0 : n;
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=openfda-drugsfda.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openfda-drugsfda.js","sourceRoot":"","sources":["../src/openfda-drugsfda.ts"],"names":[],"mappings":"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"}
|
package/dist/openfda.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openfda.d.ts","sourceRoot":"","sources":["../src/openfda.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAGzE,eAAO,MAAM,YAAY,gBAAgB,CAAC;AAG1C,eAAO,MAAM,kBAAkB,qCAAsC,CAAC;AACtE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAIlE,eAAO,MAAM,uBAAuB,+CAAgD,CAAC;
|
|
1
|
+
{"version":3,"file":"openfda.d.ts","sourceRoot":"","sources":["../src/openfda.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAGzE,eAAO,MAAM,YAAY,gBAAgB,CAAC;AAG1C,eAAO,MAAM,kBAAkB,qCAAsC,CAAC;AACtE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAIlE,eAAO,MAAM,uBAAuB,+CAAgD,CAAC;AAmCrF,wFAAwF;AACxF,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAIlD;AAGD,MAAM,MAAM,aAAa,GAAG;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC,CAAC;AAqBF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,iEAAiE;AACjE,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAarD;AAGD;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyChF;AAED,+FAA+F;AAC/F,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAY1D;AAGD,MAAM,MAAM,sBAAsB,GAAG,cAAc,GAAG;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAuLrB"}
|
package/dist/openfda.js
CHANGED
|
@@ -81,6 +81,13 @@ const NOT_DETERMINATION_NOTE = "openFDA recall/enforcement records are FDA-publi
|
|
|
81
81
|
const KEYLESS_NOTE = "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/).";
|
|
82
82
|
const KEYED_NOTE = "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.";
|
|
83
83
|
const NO_FILTER_NOTE = "No structured filters were applied — this is an unscoped scan of the WHOLE recall/enforcement category. Add firm / product / reason / classification / status / state to scope the result set.";
|
|
84
|
+
// dogfooding 2026-07-16: `category` defaults to 'drug'. A DEVICE- or FOOD-intent
|
|
85
|
+
// caller who omits it silently searches the DRUG dataset (a separate openFDA index
|
|
86
|
+
// with its own, much smaller total — e.g. "pacemaker" returns 1 drug recall vs 197
|
|
87
|
+
// device recalls). The default is convenient but load-bearing, so when it is TAKEN
|
|
88
|
+
// (not explicitly chosen) we say so and point to the alternatives. `category` is
|
|
89
|
+
// also echoed in filtersApplied so the drug/device/food choice is never invisible.
|
|
90
|
+
const CATEGORY_DEFAULT_NOTE = "category was NOT specified and DEFAULTED to 'drug' — this searched the DRUG recall dataset (/drug/enforcement) ONLY. For medical DEVICES pass category:'device'; for FOOD pass category:'food'. Each category is a SEPARATE openFDA dataset with its own total, so a device/food-intent query left on the default MISSES all of those recalls.";
|
|
84
91
|
// ─── The OPTIONAL key seam (value NEVER leaked past the &api_key= param) ──
|
|
85
92
|
/** Read OPENFDA_API_KEY from env; trim; return the value or undefined (unset/blank). */
|
|
86
93
|
export function openfdaApiKey() {
|
|
@@ -265,6 +272,14 @@ export async function enforcement(args) {
|
|
|
265
272
|
filtersApplied.push("status");
|
|
266
273
|
if (args.state !== undefined)
|
|
267
274
|
filtersApplied.push("state");
|
|
275
|
+
// NO_FILTER_NOTE gates on the STRUCTURED filters only — capture that BEFORE the
|
|
276
|
+
// always-present category selector is appended below.
|
|
277
|
+
const hasStructuredFilter = filtersApplied.length > 0;
|
|
278
|
+
// The category (drug|device|food) is the ENDPOINT selector — always applied and
|
|
279
|
+
// consequential — so echo it in filtersApplied to make the choice VISIBLE (a
|
|
280
|
+
// device query left on the drug default must not look identical to a device query).
|
|
281
|
+
const categoryDefaulted = args.category === undefined;
|
|
282
|
+
filtersApplied.push(`category:${category}`);
|
|
268
283
|
const params = new URLSearchParams();
|
|
269
284
|
if (search !== "")
|
|
270
285
|
params.set("search", search);
|
|
@@ -283,7 +298,7 @@ export async function enforcement(args) {
|
|
|
283
298
|
const { code, message } = await readOpenfdaError(res);
|
|
284
299
|
if (code === "NOT_FOUND") {
|
|
285
300
|
// Honest empty — NOT thrown, NOT not_found (the openFDA no-match idiom).
|
|
286
|
-
return emptyResult(category, limit, skip, filtersApplied, key !== undefined);
|
|
301
|
+
return emptyResult(category, limit, skip, filtersApplied, key !== undefined, categoryDefaulted);
|
|
287
302
|
}
|
|
288
303
|
// A non-NOT_FOUND 404 ⇒ the shared not_found taxonomy (never a fake-empty).
|
|
289
304
|
throw new ToolErrorCarrier({
|
|
@@ -346,8 +361,10 @@ export async function enforcement(args) {
|
|
|
346
361
|
const hasMore = totalAvailable !== null && skip + returned < totalAvailable;
|
|
347
362
|
const nextOffset = hasMore ? skip + returned : null;
|
|
348
363
|
const notes = [NOT_DETERMINATION_NOTE, keyNote(key !== undefined)];
|
|
349
|
-
if (
|
|
364
|
+
if (!hasStructuredFilter)
|
|
350
365
|
notes.push(NO_FILTER_NOTE);
|
|
366
|
+
if (categoryDefaulted)
|
|
367
|
+
notes.push(CATEGORY_DEFAULT_NOTE);
|
|
351
368
|
return withMeta({ recalls }, {
|
|
352
369
|
// MODE only — never the key value (K-test).
|
|
353
370
|
source: `${OPENFDA_HOST} /${category}/enforcement (openFDA recall enforcement; ${key !== undefined ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"})`,
|
|
@@ -366,7 +383,17 @@ function keyNote(hasKey) {
|
|
|
366
383
|
return hasKey ? KEYED_NOTE : KEYLESS_NOTE;
|
|
367
384
|
}
|
|
368
385
|
/** An honest empty result (★P2: a 404 NOT_FOUND no-match) — returned:0, total:0. */
|
|
369
|
-
function emptyResult(category, limit, skip, filtersApplied, hasKey) {
|
|
386
|
+
function emptyResult(category, limit, skip, filtersApplied, hasKey, categoryDefaulted) {
|
|
387
|
+
const notes = [
|
|
388
|
+
"No recall/enforcement records matched this query (openFDA returned HTTP 404 NOT_FOUND — the source's honest no-match). This is an exact empty, not an error.",
|
|
389
|
+
NOT_DETERMINATION_NOTE,
|
|
390
|
+
keyNote(hasKey),
|
|
391
|
+
];
|
|
392
|
+
// A defaulted category on an EMPTY result is the most misleading case — a device
|
|
393
|
+
// analyst reads "0 recalls" as "clean" when they actually searched the wrong
|
|
394
|
+
// dataset. Surface the default + the alternatives.
|
|
395
|
+
if (categoryDefaulted)
|
|
396
|
+
notes.push(CATEGORY_DEFAULT_NOTE);
|
|
370
397
|
return withMeta({ recalls: [] }, {
|
|
371
398
|
source: `${OPENFDA_HOST} /${category}/enforcement (openFDA recall enforcement; ${hasKey ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"})`,
|
|
372
399
|
keylessMode: true,
|
|
@@ -376,11 +403,7 @@ function emptyResult(category, limit, skip, filtersApplied, hasKey) {
|
|
|
376
403
|
filtersDropped: [],
|
|
377
404
|
fieldsUnavailable: [],
|
|
378
405
|
pagination: { offset: skip, limit, hasMore: false, nextOffset: null },
|
|
379
|
-
notes
|
|
380
|
-
"No recall/enforcement records matched this query (openFDA returned HTTP 404 NOT_FOUND — the source's honest no-match). This is an exact empty, not an error.",
|
|
381
|
-
NOT_DETERMINATION_NOTE,
|
|
382
|
-
keyNote(hasKey),
|
|
383
|
-
],
|
|
406
|
+
notes,
|
|
384
407
|
});
|
|
385
408
|
}
|
|
386
409
|
function clampLimit(v) {
|