@cliwant/mcp-sam-gov 0.3.0 → 1.0.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/LICENSE +21 -21
- package/README.ja.md +48 -16
- package/README.ko.md +48 -16
- package/README.md +226 -64
- package/dist/attachments.d.ts +63 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +576 -0
- package/dist/attachments.js.map +1 -0
- package/dist/bls.d.ts +267 -0
- package/dist/bls.d.ts.map +1 -0
- package/dist/bls.js +1549 -0
- package/dist/bls.js.map +1 -0
- package/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +4 -0
- package/dist/cache.js.map +1 -1
- package/dist/census.d.ts +182 -0
- package/dist/census.d.ts.map +1 -0
- package/dist/census.js +576 -0
- package/dist/census.js.map +1 -0
- package/dist/ckan.d.ts +141 -0
- package/dist/ckan.d.ts.map +1 -0
- package/dist/ckan.js +393 -0
- package/dist/ckan.js.map +1 -0
- package/dist/clinicaltrials.d.ts +180 -0
- package/dist/clinicaltrials.d.ts.map +1 -0
- package/dist/clinicaltrials.js +730 -0
- package/dist/clinicaltrials.js.map +1 -0
- package/dist/cms.d.ts +140 -0
- package/dist/cms.d.ts.map +1 -0
- package/dist/cms.js +482 -0
- package/dist/cms.js.map +1 -0
- package/dist/coerce.d.ts +32 -0
- package/dist/coerce.d.ts.map +1 -0
- package/dist/coerce.js +50 -0
- package/dist/coerce.js.map +1 -0
- package/dist/datagov-catalog.d.ts +84 -0
- package/dist/datagov-catalog.d.ts.map +1 -0
- package/dist/datagov-catalog.js +233 -0
- package/dist/datagov-catalog.js.map +1 -0
- package/dist/datagov.d.ts +146 -0
- package/dist/datagov.d.ts.map +1 -0
- package/dist/datagov.js +689 -0
- package/dist/datagov.js.map +1 -0
- package/dist/datagovKey.d.ts +36 -0
- package/dist/datagovKey.d.ts.map +1 -0
- package/dist/datagovKey.js +60 -0
- package/dist/datagovKey.js.map +1 -0
- package/dist/datasource.d.ts +313 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/datasource.js +551 -0
- package/dist/datasource.js.map +1 -0
- package/dist/disclosure.d.ts +55 -0
- package/dist/disclosure.d.ts.map +1 -0
- package/dist/disclosure.js +57 -0
- package/dist/disclosure.js.map +1 -0
- package/dist/ecfr.d.ts +4 -2
- package/dist/ecfr.d.ts.map +1 -1
- package/dist/ecfr.js +74 -1
- package/dist/ecfr.js.map +1 -1
- package/dist/echo.d.ts +143 -0
- package/dist/echo.d.ts.map +1 -0
- package/dist/echo.js +424 -0
- package/dist/echo.js.map +1 -0
- package/dist/edgar.d.ts +377 -0
- package/dist/edgar.d.ts.map +1 -0
- package/dist/edgar.js +2418 -0
- package/dist/edgar.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +69 -1
- package/dist/errors.js.map +1 -1
- package/dist/fac.d.ts +180 -0
- package/dist/fac.d.ts.map +1 -0
- package/dist/fac.js +416 -0
- package/dist/fac.js.map +1 -0
- package/dist/far.d.ts +170 -0
- package/dist/far.d.ts.map +1 -0
- package/dist/far.js +804 -0
- package/dist/far.js.map +1 -0
- package/dist/fdic.d.ts +599 -0
- package/dist/fdic.d.ts.map +1 -0
- package/dist/fdic.js +1624 -0
- package/dist/fdic.js.map +1 -0
- package/dist/federal-register.d.ts +139 -2
- package/dist/federal-register.d.ts.map +1 -1
- package/dist/federal-register.js +419 -3
- package/dist/federal-register.js.map +1 -1
- package/dist/fema.d.ts +181 -0
- package/dist/fema.d.ts.map +1 -0
- package/dist/fema.js +436 -0
- package/dist/fema.js.map +1 -0
- package/dist/fpds.d.ts +108 -0
- package/dist/fpds.d.ts.map +1 -0
- package/dist/fpds.js +519 -0
- package/dist/fpds.js.map +1 -0
- package/dist/gao.d.ts +64 -0
- package/dist/gao.d.ts.map +1 -0
- package/dist/gao.js +640 -0
- package/dist/gao.js.map +1 -0
- package/dist/govinfo.d.ts +111 -0
- package/dist/govinfo.d.ts.map +1 -0
- package/dist/govinfo.js +422 -0
- package/dist/govinfo.js.map +1 -0
- package/dist/grants.d.ts +27 -4
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +111 -6
- package/dist/grants.js.map +1 -1
- package/dist/gsa-csv.d.ts +249 -0
- package/dist/gsa-csv.d.ts.map +1 -0
- package/dist/gsa-csv.js +784 -0
- package/dist/gsa-csv.js.map +1 -0
- package/dist/integrity.d.ts +212 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +707 -0
- package/dist/integrity.js.map +1 -0
- package/dist/meta.d.ts +165 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +162 -0
- package/dist/meta.js.map +1 -0
- package/dist/nih.d.ts +117 -0
- package/dist/nih.d.ts.map +1 -0
- package/dist/nih.js +291 -0
- package/dist/nih.js.map +1 -0
- package/dist/nppes.d.ts +157 -0
- package/dist/nppes.d.ts.map +1 -0
- package/dist/nppes.js +648 -0
- package/dist/nppes.js.map +1 -0
- package/dist/nsf.d.ts +176 -0
- package/dist/nsf.d.ts.map +1 -0
- package/dist/nsf.js +554 -0
- package/dist/nsf.js.map +1 -0
- package/dist/nvd.d.ts +176 -0
- package/dist/nvd.d.ts.map +1 -0
- package/dist/nvd.js +912 -0
- package/dist/nvd.js.map +1 -0
- package/dist/ofac.d.ts +205 -0
- package/dist/ofac.d.ts.map +1 -0
- package/dist/ofac.js +919 -0
- package/dist/ofac.js.map +1 -0
- package/dist/pricing.d.ts +110 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +843 -0
- package/dist/pricing.js.map +1 -0
- package/dist/sam-gov/client.d.ts +60 -2
- package/dist/sam-gov/client.d.ts.map +1 -1
- package/dist/sam-gov/client.js +320 -54
- package/dist/sam-gov/client.js.map +1 -1
- package/dist/sam-gov/index.d.ts +1 -1
- package/dist/sam-gov/index.d.ts.map +1 -1
- package/dist/sam-gov/index.js +1 -1
- package/dist/sam-gov/index.js.map +1 -1
- package/dist/sam-gov/types.d.ts +24 -0
- package/dist/sam-gov/types.d.ts.map +1 -1
- package/dist/sba.d.ts +72 -0
- package/dist/sba.d.ts.map +1 -0
- package/dist/sba.js +281 -0
- package/dist/sba.js.map +1 -0
- package/dist/server.d.ts +14 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3889 -296
- package/dist/server.js.map +1 -1
- package/dist/snapshot.d.ts +98 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +146 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/socrata.d.ts +157 -0
- package/dist/socrata.d.ts.map +1 -0
- package/dist/socrata.js +448 -0
- package/dist/socrata.js.map +1 -0
- package/dist/treasury.d.ts +143 -0
- package/dist/treasury.d.ts.map +1 -0
- package/dist/treasury.js +436 -0
- package/dist/treasury.js.map +1 -0
- package/dist/usaspending.d.ts +260 -65
- package/dist/usaspending.d.ts.map +1 -1
- package/dist/usaspending.js +1629 -201
- package/dist/usaspending.js.map +1 -1
- package/dist/usitc.d.ts +142 -0
- package/dist/usitc.d.ts.map +1 -0
- package/dist/usitc.js +339 -0
- package/dist/usitc.js.map +1 -0
- package/package.json +24 -2
- package/src/attachments.ts +652 -0
- package/src/bls.ts +1943 -0
- package/src/cache.ts +5 -0
- package/src/census.ts +735 -0
- package/src/ckan.ts +495 -0
- package/src/clinicaltrials.ts +923 -0
- package/src/cms.ts +634 -0
- package/src/coerce.ts +47 -0
- package/src/datagov-catalog.ts +296 -0
- package/src/datagov.ts +907 -0
- package/src/datagovKey.ts +68 -0
- package/src/datasource.ts +721 -0
- package/src/disclosure.ts +61 -0
- package/src/ecfr.ts +98 -1
- package/src/echo.ts +496 -0
- package/src/edgar.ts +3014 -0
- package/src/errors.ts +84 -1
- package/src/fac.ts +529 -0
- package/src/far.ts +1007 -0
- package/src/fdic.ts +2052 -0
- package/src/federal-register.ts +512 -3
- package/src/fema.ts +541 -0
- package/src/fpds.ts +620 -0
- package/src/gao.ts +744 -0
- package/src/govinfo.ts +497 -0
- package/src/grants.ts +139 -7
- package/src/gsa-csv.ts +992 -0
- package/src/integrity.ts +928 -0
- package/src/meta.ts +292 -0
- package/src/nih.ts +375 -0
- package/src/nppes.ts +834 -0
- package/src/nsf.ts +706 -0
- package/src/nvd.ts +1124 -0
- package/src/ofac.ts +1166 -0
- package/src/pricing.ts +1075 -0
- package/src/sam-gov/client.ts +345 -63
- package/src/sam-gov/index.ts +5 -1
- package/src/sam-gov/types.ts +22 -0
- package/src/sba.ts +357 -0
- package/src/server.ts +4551 -328
- package/src/snapshot.ts +192 -0
- package/src/socrata.ts +532 -0
- package/src/treasury.ts +575 -0
- package/src/usaspending.ts +1946 -214
- package/src/usitc.ts +420 -0
package/dist/nih.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIH RePORTER v2 — federal research-GRANT project records (keyless).
|
|
3
|
+
*
|
|
4
|
+
* A NEW capability axis: **federal research-funding footprint by organization /
|
|
5
|
+
* UEI / state** — awarded NIH project money keyed to a recipient org (with UEI +
|
|
6
|
+
* DUNS), a recipient-ENRICHMENT layer orthogonal to the spend/contract/regulatory
|
|
7
|
+
* sources. It joins the SAM/USAspending recipient graph via `primary_uei` (the
|
|
8
|
+
* SAME UEI space) — but the award nature DIFFERS (grants fund research, not
|
|
9
|
+
* goods/services), so every response carries the grant-vs-contract caveat (M2).
|
|
10
|
+
*
|
|
11
|
+
* ON-DOMAIN HONESTY: this is research-funding / grants-adjacent, the AWARDED-money
|
|
12
|
+
* sibling of grants.gov opportunities and the biomedical-R&D slice of USAspending
|
|
13
|
+
* assistance awards — NOT core procurement. Positioned as recipient-enrichment /
|
|
14
|
+
* R&D market intel, never as a contract source.
|
|
15
|
+
*
|
|
16
|
+
* THE R2 PORT'S FIRST NON-GET CONSUMER (ADR-0014): NIH RePORTER is a
|
|
17
|
+
* POST-with-JSON-body API. `getJson` (ADR-0005) gained a byte-neutral,
|
|
18
|
+
* backward-compatible `method`/`body` passthrough for exactly this; this module
|
|
19
|
+
* writes ZERO fetch/coercion/error code — it REUSES `getJson` / `driftError` /
|
|
20
|
+
* `num`·`str` / `withMeta` and COPIES (does not import) the fixed-host SSRF +
|
|
21
|
+
* honesty PATTERN from CKAN/ECHO/FPDS, adapted:
|
|
22
|
+
*
|
|
23
|
+
* ★ SSRF GUARD (policy① — the SMALLEST surface of any source): the request URL is
|
|
24
|
+
* a compile-time CONSTANT (fixed host `api.reporter.nih.gov` + fixed path
|
|
25
|
+
* `/v2/projects/search`); NO caller input touches it. All filters ride in the POST
|
|
26
|
+
* body, which is MODULE-BUILT from a validated typed criteria object then
|
|
27
|
+
* `JSON.stringify`'d — there is NO raw-body / raw-criteria passthrough, so a
|
|
28
|
+
* caller value cannot break out of its criterion into another key. A
|
|
29
|
+
* post-construction hostname/protocol assertion + `redirect:"error"` lock it
|
|
30
|
+
* (fail closed on any off-host 3xx; its body is never read).
|
|
31
|
+
*
|
|
32
|
+
* ★ FILTER HONESTY (M1 — the load-bearing discipline): a criterion is shipped ONLY
|
|
33
|
+
* after being LIVE-CONFIRMED to actually narrow the result set. The confirmed
|
|
34
|
+
* v1 set is `orgStates` + `orgNames` + `fiscalYears` (each live-verified to
|
|
35
|
+
* reduce the total below the unfiltered baseline). `agency_ic_codes` is EXCLUDED
|
|
36
|
+
* — it is silently DROPPED upstream (a filter that no-ops is never presented as
|
|
37
|
+
* applied). A shipped filter goes in `_meta.filtersApplied`; a silent-drop filter
|
|
38
|
+
* is never listed there and never exposed in the schema.
|
|
39
|
+
*
|
|
40
|
+
* ★ 15,000-RECORD RETRIEVAL WINDOW (M3+M4 — the critical disclosure): NIH caps
|
|
41
|
+
* keyless retrieval at `offset 0..14,999` (`limit ≤ 500`), so only the first
|
|
42
|
+
* 15,000 records of any result set are reachable. This is a cap on RETRIEVAL, NOT
|
|
43
|
+
* on the COUNT: `meta.total` stays EXACT (never truncated to 15,000, never marked
|
|
44
|
+
* `totalIsLowerBound`). `offset >= 15,000` is refused pre-fetch (`invalid_input`);
|
|
45
|
+
* after a page, `nextOffset` is null once `offset+returned` reaches the cap or the
|
|
46
|
+
* exact total (never a dead-end `nextOffset=15000`). `truncated` is derived by
|
|
47
|
+
* buildMeta from `returned < totalAvailable`; when the count exceeds the window a
|
|
48
|
+
* `_meta.notes` line discloses the unreachable remainder and recommends narrowing.
|
|
49
|
+
*
|
|
50
|
+
* ★ HONESTY `_meta`: shape-guard FIRST (a 200 body that is not `{meta,results:[]}`
|
|
51
|
+
* → driftError, NEVER a fake empty); a non-numeric/absent `meta.total` →
|
|
52
|
+
* driftError BEFORE `num(meta.total)`; genuine-empty (`total:0`) → complete:true /
|
|
53
|
+
* totalAvailable:0; outage/5xx/timeout → getJson throws; 400 (bad offset/limit/
|
|
54
|
+
* type) → invalid_input (surfaced, never `[]`). `num` is null-never-0 on
|
|
55
|
+
* `award_amount` (a real $0 award is 0; an absent amount is null). Every response
|
|
56
|
+
* discloses the grant-vs-contract caveat + a data-currency note.
|
|
57
|
+
*/
|
|
58
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
59
|
+
import { getJson, driftError } from "./datasource.js";
|
|
60
|
+
import { num, str } from "./coerce.js";
|
|
61
|
+
import { withMeta } from "./meta.js";
|
|
62
|
+
// Re-export the shared honesty coercion (single audited copy in ./coerce.js —
|
|
63
|
+
// ADR-0005 v2 FIX-C) so the fault suite's num-parity guard resolves the SAME
|
|
64
|
+
// `num` (nih.num === coerce.num === treasury.num — a num regression fails together).
|
|
65
|
+
export { num };
|
|
66
|
+
// ─── Fixed endpoint (SSRF core — a compile-time CONSTANT) ─────────
|
|
67
|
+
const NIH_HOST = "api.reporter.nih.gov";
|
|
68
|
+
const NIH_PATH = "/v2/projects/search";
|
|
69
|
+
const NIH_PROJECTS_URL = `https://${NIH_HOST}${NIH_PATH}`;
|
|
70
|
+
// HOST-only-ish label (path is fixed + carries no token — keyless). Surfaces in
|
|
71
|
+
// ToolError.upstreamEndpoint; no secret can appear here (the API is anonymous).
|
|
72
|
+
const NIH_LABEL = "nih:/v2/projects/search";
|
|
73
|
+
// ─── The 15,000-record RETRIEVAL window (live-verified: offset ≤ 14,999,
|
|
74
|
+
// limit ≤ 500). A cap on RETRIEVAL, not on the exact meta.total count. The
|
|
75
|
+
// limit ≤ 500 ceiling is enforced by the server's Zod schema; this module owns
|
|
76
|
+
// the offset-window guard + the never-a-dead-end nextOffset boundary. ──
|
|
77
|
+
const RETRIEVAL_CAP = 15_000;
|
|
78
|
+
// ─── Frozen US state/territory 2-letter USPS enum (UPPERCASE-only) ─
|
|
79
|
+
// Built FROM this array by the Zod enum in server.ts (single source of truth).
|
|
80
|
+
// It is BOTH an org_states value guard AND the silent-zero guard: live, an unknown
|
|
81
|
+
// but well-typed value (e.g. "ZZ") returns a genuine total:0 indistinguishable
|
|
82
|
+
// from a real empty, and a lowercase "ca" silently returns zeros — so a typo must
|
|
83
|
+
// be an invalid_input, never read as "no NIH funding" (a silent honesty failure).
|
|
84
|
+
export const NIH_ORG_STATES = [
|
|
85
|
+
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FL",
|
|
86
|
+
"GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME",
|
|
87
|
+
"MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH",
|
|
88
|
+
"NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI",
|
|
89
|
+
"SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI",
|
|
90
|
+
"WY", "AS", "GU", "MP", "PR", "VI",
|
|
91
|
+
];
|
|
92
|
+
// ─── Disclosure constants (honesty obligations) ──────────────────
|
|
93
|
+
/** M2 — the mandatory grant-vs-contract caveat carried in EVERY response. */
|
|
94
|
+
const NIH_GRANT_CAVEAT = "NIH RePORTER records are RESEARCH GRANTS awarded by NIH, NOT federal procurement contracts. The primary_uei joins to SAM/USAspending recipient records, but the award nature differs (grants fund research, not goods/services) — do not present these amounts as contract awards.";
|
|
95
|
+
/** m-currency — a conservative data-currency note (not API-verifiable). */
|
|
96
|
+
const DATA_CURRENCY_NOTE = "NIH RePORTER updates on a rolling basis; per-record refresh lag is not API-verifiable.";
|
|
97
|
+
/** The UEI join disclosure — how to bridge to the SAM/USAspending recipient graph. */
|
|
98
|
+
const UEI_JOIN_NOTE = "organization.primaryUei is the join key to SAM entities and USAspending recipients (same UEI space); a grant recipient is not necessarily a federal contractor.";
|
|
99
|
+
/** M3+M4 — the retrieval-window disclosure when the count exceeds the 15,000 cap. */
|
|
100
|
+
function retrievalCapNote(total) {
|
|
101
|
+
return `NIH caps keyless retrieval at the first ${RETRIEVAL_CAP} of ${total} matching records; the count is EXACT but records beyond ${RETRIEVAL_CAP} cannot be retrieved via this API — narrow criteria (org, fiscal year, state) to bring the target set under ${RETRIEVAL_CAP}.`;
|
|
102
|
+
}
|
|
103
|
+
const SOURCE = "api.reporter.nih.gov v2 (keyless)";
|
|
104
|
+
/** "true"/true → true, "false"/false → false, absent/other → null (never a
|
|
105
|
+
* fabricated false). */
|
|
106
|
+
function boolOrNull(x) {
|
|
107
|
+
if (x === true)
|
|
108
|
+
return true;
|
|
109
|
+
if (x === false)
|
|
110
|
+
return false;
|
|
111
|
+
if (x === "true")
|
|
112
|
+
return true;
|
|
113
|
+
if (x === "false")
|
|
114
|
+
return false;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/** A 2-letter uppercase string array from a mixed value, else []. */
|
|
118
|
+
function strArray(x) {
|
|
119
|
+
if (!Array.isArray(x))
|
|
120
|
+
return [];
|
|
121
|
+
return x.map((v) => str(v)).filter((v) => v !== null);
|
|
122
|
+
}
|
|
123
|
+
/** Map ONE NIH results[] row → the curated enrichment shape. Every scalar is
|
|
124
|
+
* null-never-fabricated (str/num); `award_amount` is num (a real $0 → 0, absent
|
|
125
|
+
* → null). Field names are NIH v2 documented keys; an absent field maps to null
|
|
126
|
+
* (honest "unknown"), never a crash. */
|
|
127
|
+
function mapProject(raw) {
|
|
128
|
+
const r = (raw ?? {});
|
|
129
|
+
const org = (r.organization ?? {});
|
|
130
|
+
const ic = (r.agency_ic_admin ?? {});
|
|
131
|
+
const pis = Array.isArray(r.principal_investigators)
|
|
132
|
+
? r.principal_investigators
|
|
133
|
+
: [];
|
|
134
|
+
return {
|
|
135
|
+
projectNum: str(r.project_num),
|
|
136
|
+
projectTitle: str(r.project_title),
|
|
137
|
+
fiscalYear: num(r.fiscal_year),
|
|
138
|
+
awardAmount: num(r.award_amount),
|
|
139
|
+
awardType: str(r.award_type),
|
|
140
|
+
activityCode: str(r.activity_code),
|
|
141
|
+
isActive: boolOrNull(r.is_active),
|
|
142
|
+
organization: {
|
|
143
|
+
name: str(org.org_name),
|
|
144
|
+
city: str(org.org_city),
|
|
145
|
+
state: str(org.org_state),
|
|
146
|
+
country: str(org.org_country),
|
|
147
|
+
primaryUei: str(org.primary_uei),
|
|
148
|
+
primaryDuns: str(org.primary_duns),
|
|
149
|
+
ueis: strArray(org.org_ueis),
|
|
150
|
+
duns: strArray(org.org_duns),
|
|
151
|
+
},
|
|
152
|
+
principalInvestigators: pis.map((p) => ({
|
|
153
|
+
profileId: num(p.profile_id),
|
|
154
|
+
firstName: str(p.first_name),
|
|
155
|
+
lastName: str(p.last_name),
|
|
156
|
+
fullName: str(p.full_name),
|
|
157
|
+
isContactPi: boolOrNull(p.is_contact_pi),
|
|
158
|
+
title: str(p.title),
|
|
159
|
+
})),
|
|
160
|
+
contactPiName: str(r.contact_pi_name),
|
|
161
|
+
fundingIc: {
|
|
162
|
+
code: str(ic.code),
|
|
163
|
+
abbreviation: str(ic.abbreviation),
|
|
164
|
+
name: str(ic.name),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Search awarded NIH research projects (POST /v2/projects/search). Structured,
|
|
170
|
+
* LIVE-CONFIRMED-narrowing criteria only (orgStates / orgNames / fiscalYears),
|
|
171
|
+
* AND-combined in a MODULE-BUILT criteria object → JSON.stringify (no raw
|
|
172
|
+
* passthrough). Returns curated recipient-enrichment rows + honest `_meta`:
|
|
173
|
+
* exact totalAvailable, exact within-window pagination, the 15,000-retrieval-cap
|
|
174
|
+
* disclosure when the count exceeds it, and the mandatory grant-vs-contract
|
|
175
|
+
* caveat. Disclose-not-refuse: an unscoped query is NOT refused — it returns the
|
|
176
|
+
* first page + the exact total + a note recommending a criterion when the total
|
|
177
|
+
* exceeds the window.
|
|
178
|
+
*/
|
|
179
|
+
export async function searchProjects(args) {
|
|
180
|
+
const limit = args.limit ?? 50;
|
|
181
|
+
const offset = args.offset ?? 0;
|
|
182
|
+
// ── 15k window pre-fetch guard (M3): offset >= 15,000 is UNREACHABLE — refuse
|
|
183
|
+
// BEFORE any fetch (also enforced by the server's Zod .max(14_999); this is
|
|
184
|
+
// the belt-and-suspenders module guard so a direct caller can't slip past). ──
|
|
185
|
+
if (offset >= RETRIEVAL_CAP) {
|
|
186
|
+
throw new ToolErrorCarrier({
|
|
187
|
+
kind: "invalid_input",
|
|
188
|
+
message: `NIH RePORTER caps pagination at the first ${RETRIEVAL_CAP} records (offset 0..${RETRIEVAL_CAP - 1}); offset ${offset} is unreachable via this keyless API. Narrow criteria (org, fiscal year, state) to bring the target set under ${RETRIEVAL_CAP}.`,
|
|
189
|
+
retryable: false,
|
|
190
|
+
upstreamEndpoint: NIH_LABEL,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// ── Build the criteria object from VALIDATED typed input (SSRF: no raw
|
|
194
|
+
// passthrough; every value is enum/typed by the server's Zod before here). A
|
|
195
|
+
// filter is added — and listed in filtersApplied — ONLY when it is one of the
|
|
196
|
+
// LIVE-CONFIRMED narrowing criteria (M1). agency_ic_codes is never built (it
|
|
197
|
+
// silently no-ops upstream and is not in the schema). ──
|
|
198
|
+
const criteria = {};
|
|
199
|
+
const filtersApplied = [];
|
|
200
|
+
if (args.fiscalYears && args.fiscalYears.length > 0) {
|
|
201
|
+
criteria.fiscal_years = args.fiscalYears;
|
|
202
|
+
filtersApplied.push("fiscalYears");
|
|
203
|
+
}
|
|
204
|
+
if (args.orgStates && args.orgStates.length > 0) {
|
|
205
|
+
criteria.org_states = args.orgStates;
|
|
206
|
+
filtersApplied.push("orgStates");
|
|
207
|
+
}
|
|
208
|
+
if (args.orgNames && args.orgNames.length > 0) {
|
|
209
|
+
criteria.org_names = args.orgNames;
|
|
210
|
+
filtersApplied.push("orgNames");
|
|
211
|
+
}
|
|
212
|
+
const payload = { criteria, limit, offset };
|
|
213
|
+
// ── SSRF belt-and-suspenders: the URL is a compile-time constant, but assert it
|
|
214
|
+
// cannot have drifted (a future typo) — exactly the CKAN/FPDS post-construction
|
|
215
|
+
// check, adapted (no URLSearchParams: there are no query params). ──
|
|
216
|
+
const built = new URL(NIH_PROJECTS_URL);
|
|
217
|
+
if (built.hostname !== NIH_HOST || built.protocol !== "https:") {
|
|
218
|
+
throw new ToolErrorCarrier({
|
|
219
|
+
kind: "invalid_input",
|
|
220
|
+
message: `Constructed NIH URL host ${JSON.stringify(built.hostname)} (${built.protocol}) is not ${NIH_HOST} over https — refusing to fetch (SSRF safety).`,
|
|
221
|
+
retryable: false,
|
|
222
|
+
upstreamEndpoint: NIH_LABEL,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// ── The R2 port's first POST call. Content-Type is MANDATORY for NIH (415
|
|
226
|
+
// without it, 405 on GET). redirect:"error" fails closed on any off-host 3xx
|
|
227
|
+
// (its body is never read). The body is the module-built, JSON.stringify'd
|
|
228
|
+
// typed payload — never string-concatenated. ──
|
|
229
|
+
const body = await getJson(NIH_PROJECTS_URL, {
|
|
230
|
+
label: NIH_LABEL,
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: { "Content-Type": "application/json" },
|
|
233
|
+
body: JSON.stringify(payload),
|
|
234
|
+
redirect: "error",
|
|
235
|
+
});
|
|
236
|
+
// ── Shape guard FIRST → THROW (never fake-empty). A 200 body must be an OBJECT
|
|
237
|
+
// with a `meta` object and an array `results`. An array body (a 200-with-array
|
|
238
|
+
// drift), a missing `meta`, or a non-array `results` → driftError; NEVER read
|
|
239
|
+
// `results` as []. ──
|
|
240
|
+
if (typeof body !== "object" ||
|
|
241
|
+
body === null ||
|
|
242
|
+
Array.isArray(body)) {
|
|
243
|
+
throw driftError(NIH_LABEL, "NIH /v2/projects/search returned a 200 body that is not an object {meta,results} (an array or scalar) — refusing to report it as an empty result.");
|
|
244
|
+
}
|
|
245
|
+
const b = body;
|
|
246
|
+
if (typeof b.meta !== "object" || b.meta === null || !Array.isArray(b.results)) {
|
|
247
|
+
throw driftError(NIH_LABEL, "NIH /v2/projects/search returned an unexpected shape (expected an object with a `meta` object and an array `results`).");
|
|
248
|
+
}
|
|
249
|
+
const meta = b.meta;
|
|
250
|
+
// ── m-total-guard: a non-number `meta.total` (or absent) is drift — num() cannot
|
|
251
|
+
// tell a non-number from an absent one, so typeof-check BEFORE num() (CKAN m6).
|
|
252
|
+
// Nothing trustworthy to report as a total → THROW, never proceed with null. ──
|
|
253
|
+
if (typeof meta.total !== "number") {
|
|
254
|
+
throw driftError(NIH_LABEL, "NIH meta.total absent/non-numeric — nothing trustworthy to report as a total (treating as schema drift).");
|
|
255
|
+
}
|
|
256
|
+
// EXACT total (§1a) — NEVER results.length, NEVER a lower bound. num() defensively
|
|
257
|
+
// (a finite number passes through; a non-finite would be caught, but the typeof
|
|
258
|
+
// guard above already rejected non-numbers).
|
|
259
|
+
const totalAvailable = num(meta.total);
|
|
260
|
+
const projects = b.results.map(mapProject);
|
|
261
|
+
const returned = projects.length;
|
|
262
|
+
// ── Pagination (M3+M4): NEVER hand a dead-end nextOffset=15000. nextOffset is
|
|
263
|
+
// null once offset+returned reaches the cap OR the exact total; hasMore mirrors
|
|
264
|
+
// it. NO mid-page clamp (offset+limit>15000 does NOT 400 — only offset≥15000). ──
|
|
265
|
+
const candidateNext = offset + returned;
|
|
266
|
+
const nextOffset = candidateNext >= RETRIEVAL_CAP ||
|
|
267
|
+
(totalAvailable !== null && candidateNext >= totalAvailable)
|
|
268
|
+
? null
|
|
269
|
+
: candidateNext;
|
|
270
|
+
const hasMore = nextOffset !== null;
|
|
271
|
+
const notes = [NIH_GRANT_CAVEAT, UEI_JOIN_NOTE, DATA_CURRENCY_NOTE];
|
|
272
|
+
// Disclose-not-refuse + the reachability-cap disclosure: fire whenever the exact
|
|
273
|
+
// count exceeds the reachable window (records beyond 15,000 are UNREACHABLE — the
|
|
274
|
+
// honest superset of "truncated by the cap" and "an unscoped broad query").
|
|
275
|
+
if (totalAvailable !== null && totalAvailable > RETRIEVAL_CAP) {
|
|
276
|
+
notes.push(retrievalCapNote(totalAvailable));
|
|
277
|
+
}
|
|
278
|
+
const metaOut = {
|
|
279
|
+
source: SOURCE,
|
|
280
|
+
keylessMode: true,
|
|
281
|
+
returned,
|
|
282
|
+
totalAvailable,
|
|
283
|
+
filtersApplied,
|
|
284
|
+
filtersDropped: [],
|
|
285
|
+
fieldsUnavailable: [],
|
|
286
|
+
pagination: { offset, limit, hasMore, nextOffset },
|
|
287
|
+
notes,
|
|
288
|
+
};
|
|
289
|
+
return withMeta({ projects }, metaOut);
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=nih.js.map
|
package/dist/nih.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nih.js","sourceRoot":"","sources":["../src/nih.ts"],"names":[],"mappings":"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"}
|
package/dist/nppes.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NPPES NPI Registry — CMS/HHS healthcare-provider identity/credentialing lane
|
|
3
|
+
* (keyless). Source #27 on the R2 `getJson` port (ADR-0036).
|
|
4
|
+
*
|
|
5
|
+
* ONE tool `nppes_lookup_provider` over the CMS/HHS National Plan & Provider
|
|
6
|
+
* Enumeration System (`https://npiregistry.cms.hhs.gov/api/?version=2.1`) — the
|
|
7
|
+
* authoritative PUBLIC registry of every US healthcare provider (individual
|
|
8
|
+
* NPI-1 + organization NPI-2). The B2G unlock: vet a healthcare
|
|
9
|
+
* subcontractor/provider/org for a VA/HHS/CMS contract — validate an NPI,
|
|
10
|
+
* confirm taxonomy (specialty), enumeration status (Active), practice state, and
|
|
11
|
+
* org/name match for credentialing / teaming due-diligence.
|
|
12
|
+
*
|
|
13
|
+
* TWO modes, inferred from `number` (NO mode flag):
|
|
14
|
+
* - EXACT-NPI (`number` supplied): validate `^\d{10}$` + the CMS Luhn
|
|
15
|
+
* (Luhn over `80840` + the first 9 digits, 14 total) CLIENT-SIDE →
|
|
16
|
+
* `invalid_input` on failure (a typo must NEVER fake a not-found; NPPES
|
|
17
|
+
* validates ONLY length, so the Luhn pre-check is LOAD-BEARING). ★ M1: the
|
|
18
|
+
* outgoing query carries `number` (+`version`) as the SOLE param — a
|
|
19
|
+
* co-supplied filter is NEVER forwarded (NPPES AND-combines a number with
|
|
20
|
+
* filters, so `number=<active NPI>&last_name=Zztypo` → result_count:0, a
|
|
21
|
+
* FALSE "does not exist"). Co-filters are dropped from the wire and surfaced
|
|
22
|
+
* as a CLIENT-SIDE post-match annotation (`data.filterMatch`).
|
|
23
|
+
* - SEARCH: by first_name / last_name / organization_name /
|
|
24
|
+
* taxonomy_description / city / postal_code (REQUIRED-one set), refined by
|
|
25
|
+
* state / enumeration_type (REFINERS — never sufficient alone, S2),
|
|
26
|
+
* paginated (limit ≤ 200, skip ≤ 1000 — our POLICY reach cap, S3).
|
|
27
|
+
*
|
|
28
|
+
* HONESTY (writes ZERO fetch/coerce/error/meta code — REUSES getJson/throughGate/
|
|
29
|
+
* driftError + coerce.num/str + withMeta/buildMeta):
|
|
30
|
+
* P1 result_count === results.length (else driftError); NPPES exposes NO
|
|
31
|
+
* grand-total field → totalAvailable is a LOWER BOUND on a full page
|
|
32
|
+
* (totalIsLowerBound) + the ≤200/≤1000 policy caps disclosed.
|
|
33
|
+
* P2 `^\d{10}$` + CMS-Luhn → invalid_input (typo never fakes not-found); a
|
|
34
|
+
* genuine {result_count:0} → honest found:false; a {Errors:[…]} 200 body
|
|
35
|
+
* (NO results key — the NSF serviceNotification twin) → THROW; any
|
|
36
|
+
* 4xx/5xx/timeout/off-host-redirect/non-JSON → THROW.
|
|
37
|
+
* P3 active = basic.status === "A" (deactivated/absent ⇒ NOT active); epochs
|
|
38
|
+
* (created_epoch/last_updated_epoch, ms numeric STRINGS) via coerce.num
|
|
39
|
+
* (null-never-0).
|
|
40
|
+
* P4 no silent filter drop (per-mode filtersApplied/filtersDropped; the M1
|
|
41
|
+
* exact-mode drop disclosed via filterMatch + a note).
|
|
42
|
+
*
|
|
43
|
+
* SSRF (the NSF fixed-host idiom, COPIED not imported): host + path + version are
|
|
44
|
+
* compile-time CONSTANTS; every caller input rides in a MODULE-BUILT
|
|
45
|
+
* URLSearchParams assembled key-by-key from validated typed args (NO raw-query
|
|
46
|
+
* passthrough) + a post-construction hostname/protocol assert + redirect:"error".
|
|
47
|
+
*
|
|
48
|
+
* PII boundary (S3): NPPES public professional-registration data is IN-SCOPE per
|
|
49
|
+
* the shipped NSF-PI-name precedent (src/nsf.ts surfaces PI names/emails),
|
|
50
|
+
* bounded to a per-query targeted lookup (the ≤1,200 reach is a courtesy cap;
|
|
51
|
+
* cross-query enumeration is NOT architecturally prevented, matching NSF). The
|
|
52
|
+
* mandatory not-a-fitness/cross-check-SAM+OFAC caveat rides EVERY response.
|
|
53
|
+
*/
|
|
54
|
+
import { num } from "./coerce.js";
|
|
55
|
+
import { type MetaBundle } from "./meta.js";
|
|
56
|
+
export { num };
|
|
57
|
+
export declare const NPPES_STATES: readonly ["AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY", "AS", "GU", "MP", "PR", "VI"];
|
|
58
|
+
export type NppesState = (typeof NPPES_STATES)[number];
|
|
59
|
+
/**
|
|
60
|
+
* ★ S3 — the mandatory not-a-fitness-determination caveat carried in EVERY
|
|
61
|
+
* response (mirrors OFAC_NOT_DETERMINATION_NOTE). Kept verbatim so the fault
|
|
62
|
+
* suite can assert it. It discloses (1) not a determination + cross-check SAM/OFAC;
|
|
63
|
+
* (2) individual (NPI-1) records may surface personal/home addresses + phone/fax
|
|
64
|
+
* verbatim, with NO enrichment or cross-source join.
|
|
65
|
+
*/
|
|
66
|
+
export declare const NPPES_NOT_DETERMINATION_NOTE = "Public professional-registration data (CMS NPPES NPI Registry). Confirms enumeration / identity / taxonomy only \u2014 it is NOT a fitness, exclusion, licensure, or sanctions determination. Cross-check SAM exclusions + OFAC for debarment/sanctions and the state licensing board for licensure. Individual (NPI-1) records may include personal / home practice or mailing addresses plus telephone/fax surfaced VERBATIM from the public registry; this tool performs NO enrichment and NO cross-source join on them.";
|
|
67
|
+
/** ★ S3 — the per-query reach-cap POLICY disclosure carried on EVERY response. */
|
|
68
|
+
export declare const NPPES_REACH_CAP_NOTE = "This vetting tool reaches at most the first ~1,200 matches per query (limit \u2264 200, skip \u2264 1,000) as a deliberate targeted-lookup boundary \u2014 NPPES itself no longer enforces a skip ceiling. This is a PER-QUERY cap only; cross-query enumeration (iterating name/city/postal filters) is NOT architecturally prevented (inherent to any search API), matching the NSF precedent. Narrow your filters (name + state + taxonomy) for a complete, targeted result set rather than paging deeper.";
|
|
69
|
+
export type NppesAddress = {
|
|
70
|
+
purpose: string | null;
|
|
71
|
+
address1: string | null;
|
|
72
|
+
address2: string | null;
|
|
73
|
+
city: string | null;
|
|
74
|
+
state: string | null;
|
|
75
|
+
postalCode: string | null;
|
|
76
|
+
telephone: string | null;
|
|
77
|
+
fax: string | null;
|
|
78
|
+
countryCode: string | null;
|
|
79
|
+
countryName: string | null;
|
|
80
|
+
addressType: string | null;
|
|
81
|
+
};
|
|
82
|
+
export type NppesTaxonomy = {
|
|
83
|
+
code: string | null;
|
|
84
|
+
desc: string | null;
|
|
85
|
+
primary: boolean | null;
|
|
86
|
+
state: string | null;
|
|
87
|
+
license: string | null;
|
|
88
|
+
taxonomyGroup: string | null;
|
|
89
|
+
};
|
|
90
|
+
export type NppesProvider = {
|
|
91
|
+
number: string | null;
|
|
92
|
+
enumerationType: string | null;
|
|
93
|
+
active: boolean;
|
|
94
|
+
status: string | null;
|
|
95
|
+
basic: {
|
|
96
|
+
firstName: string | null;
|
|
97
|
+
lastName: string | null;
|
|
98
|
+
middleName: string | null;
|
|
99
|
+
namePrefix: string | null;
|
|
100
|
+
nameSuffix: string | null;
|
|
101
|
+
credential: string | null;
|
|
102
|
+
sex: string | null;
|
|
103
|
+
soleProprietor: string | null;
|
|
104
|
+
organizationName: string | null;
|
|
105
|
+
organizationalSubpart: string | null;
|
|
106
|
+
authorizedOfficialFirstName: string | null;
|
|
107
|
+
authorizedOfficialLastName: string | null;
|
|
108
|
+
authorizedOfficialMiddleName: string | null;
|
|
109
|
+
authorizedOfficialTitleOrPosition: string | null;
|
|
110
|
+
authorizedOfficialTelephoneNumber: string | null;
|
|
111
|
+
status: string | null;
|
|
112
|
+
enumerationDate: string | null;
|
|
113
|
+
certificationDate: string | null;
|
|
114
|
+
lastUpdated: string | null;
|
|
115
|
+
};
|
|
116
|
+
taxonomies: NppesTaxonomy[];
|
|
117
|
+
addresses: NppesAddress[];
|
|
118
|
+
/** ★ S1 — additional practice sites, surfaced as their own array (a provider
|
|
119
|
+
* can practice in a state that appears ONLY here). NEVER merged into addresses[]. */
|
|
120
|
+
practiceLocations: NppesAddress[];
|
|
121
|
+
identifiers: unknown[];
|
|
122
|
+
otherNames: unknown[];
|
|
123
|
+
endpoints: unknown[];
|
|
124
|
+
createdEpoch: number | null;
|
|
125
|
+
lastUpdatedEpoch: number | null;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Validate the CMS NPI check digit: the Luhn algorithm over `80840` + the first 9
|
|
129
|
+
* NPI digits (14 digits total, ISO/IEC 7812) must reproduce the 10th NPI digit.
|
|
130
|
+
* `npi` MUST already be `^\d{10}$`. A Luhn-FAILING 10-digit string is provably
|
|
131
|
+
* NOT a valid NPI ⇒ invalid_input (a typo must NOT read as found:false).
|
|
132
|
+
*/
|
|
133
|
+
export declare function cmsLuhnValid(npi: string): boolean;
|
|
134
|
+
export type NppesLookupArgs = {
|
|
135
|
+
number?: string;
|
|
136
|
+
enumeration_type?: string;
|
|
137
|
+
first_name?: string;
|
|
138
|
+
last_name?: string;
|
|
139
|
+
organization_name?: string;
|
|
140
|
+
taxonomy_description?: string;
|
|
141
|
+
city?: string;
|
|
142
|
+
state?: string;
|
|
143
|
+
postal_code?: string;
|
|
144
|
+
limit?: number;
|
|
145
|
+
skip?: number;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* `nppes_lookup_provider` — keyless NPPES NPI Registry lookup. Mode inferred from
|
|
149
|
+
* `number`: EXACT-NPI detail (Luhn-validated, number-only wire, M1 co-filter
|
|
150
|
+
* annotation) OR a filtered SEARCH (required-one gate + refiners, S2; ≤200/≤1000
|
|
151
|
+
* pagination policy caps, S3). NEVER fakes a not-found: a typo'd NPI ⇒
|
|
152
|
+
* invalid_input; a {Errors} body ⇒ THROW; a genuine {result_count:0} ⇒ honest
|
|
153
|
+
* found:false / empty. The not-a-fitness caveat + reach-cap policy ride EVERY
|
|
154
|
+
* response.
|
|
155
|
+
*/
|
|
156
|
+
export declare function lookupProvider(args: NppesLookupArgs): Promise<MetaBundle<unknown>>;
|
|
157
|
+
//# sourceMappingURL=nppes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nppes.d.ts","sourceRoot":"","sources":["../src/nppes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAIH,OAAO,EAAE,GAAG,EAAO,MAAM,aAAa,CAAC;AACvC,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAKzE,OAAO,EAAE,GAAG,EAAE,CAAC;AA8Bf,eAAO,MAAM,YAAY,2VAOf,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAmCvD;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,ggBACid,CAAC;AAE3f,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,kfACic,CAAC;AAUne,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;QACrC,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3C,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1C,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5C,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;QACjD,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;QACjD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;IACF,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B;0FACsF;IACtF,iBAAiB,EAAE,YAAY,EAAE,CAAC;IAClC,WAAW,EAAE,OAAO,EAAE,CAAC;IACvB,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,SAAS,EAAE,OAAO,EAAE,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,CAAC;AAgHF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAgBjD;AAqMD,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAGF;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CA+P9B"}
|