@cliwant/mcp-sam-gov 1.0.0 → 1.2.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/dist/fred.js ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * fred.ts — FRED (Federal Reserve Economic Data, St. Louis Fed) — the MACRO
3
+ * CONTEXT lane (ADR-0048, Wave-4 source #2). GDP · CPI · interest rates ·
4
+ * unemployment · PPI … — the economy-wide backdrop for B2G bid escalation and
5
+ * market-timing that no contract/spending source carries.
6
+ *
7
+ * ★ THIS IS THE SERVER'S SECOND KEY-REQUIRED SOURCE (Census CBP was the first).
8
+ * FRED has NO keyless tier: every request needs `&api_key=`, and a missing/bad
9
+ * key returns HTTP 400 `{error_code, error_message}`. So, honestly: with NO
10
+ * `FRED_API_KEY` these two tools THROW an `invalid_input` config error BEFORE any
11
+ * fetch (never a fake-empty, never a keyless-pretend). The other 112 tools stay
12
+ * keyless — this key is scoped to this one source. (Contrast the OPTIONAL keys of
13
+ * datagov/bls/nvd, which lift a tier but are not required.)
14
+ *
15
+ * This module MIRRORS the census-economic.ts optional-key precedent: a `fredApiKey()`
16
+ * env seam, a pre-fetch `invalid_input` THROW when unset, the fixed-host SSRF assert
17
+ * + `redirect:"error"`, and the missing-sentinel→null idiom (Census's negative
18
+ * suppression sentinel there; FRED's `value === "."` here — the BLS `"-"` lineage).
19
+ * It writes ZERO coercion/meta code of its own: it REUSES `getJson` (the shared
20
+ * fetch envelope) / `driftError` / `num`·`str` (coerce.ts, null-never-0/empty) /
21
+ * `withMeta`·`buildMeta` (offset pagination via count-exact totals).
22
+ *
23
+ * GET https://api.stlouisfed.org/fred/series/search
24
+ * ?search_text=<q>&limit=&offset=&api_key=<KEY>&file_type=json
25
+ * → { seriess:[{ id,title,frequency,frequency_short,units,seasonal_adjustment,
26
+ * observation_start,observation_end,last_updated,popularity,notes }],
27
+ * count, limit, offset }
28
+ *
29
+ * GET https://api.stlouisfed.org/fred/series/observations
30
+ * ?series_id=<id>&observation_start=&observation_end=&limit=&offset=
31
+ * &sort_order=&api_key=<KEY>&file_type=json
32
+ * → { observations:[{ date, value }], count, … } ★value === "." ⇒ missing (null)
33
+ *
34
+ * ★ HONESTY (ADR-0048 P1–P5):
35
+ * [KEY] no key ⇒ invalid_input THROW pre-fetch (0 fetch); the message names
36
+ * FRED_API_KEY + the free-signup URL. A 400 carrying `{error_message}` (a
37
+ * bad series_id / expired key) ⇒ reclassified to invalid_input CARRYING the
38
+ * FRED error_message — honestly reported, never a fake empty.
39
+ * [P1] both endpoints report `count` (the total) ⇒ totalAvailable = num(count)
40
+ * EXACT; offset pagination (hasMore = offset+returned < count, nextOffset).
41
+ * NEVER fabricated (RED if totalAvailable = returned).
42
+ * [P3] ★the missing crux: an observation `value === "."` ⇒ **null** (FRED's
43
+ * missing sentinel — the BLS `"-"` lineage), NEVER 0. A genuine "0" ⇒ 0.
44
+ * [P2] a 400 ⇒ invalid_input (carrying error_message); a genuine no-match
45
+ * (seriess:[] / observations:[]) ⇒ honest empty; a 5xx ⇒ upstream_unavailable
46
+ * THROW; a 200 non-JSON ⇒ schema_drift.
47
+ * [P4] a body whose `seriess` / `observations` is absent or non-array ⇒ driftError
48
+ * (never a fabricated empty).
49
+ * [SSRF] fixed host `api.stlouisfed.org`; `series_id` charclass `^[A-Za-z0-9._-]+$`;
50
+ * dates `^\d{4}-\d{2}-\d{2}$`; sort_order enum {asc,desc}. All VALUES ride
51
+ * URLSearchParams; the key rides `&api_key=` ONLY — never a label/_meta/note
52
+ * (the K-test).
53
+ */
54
+ import { ToolErrorCarrier } from "./errors.js";
55
+ import { getJson, driftError } from "./datasource.js";
56
+ import { num, str } from "./coerce.js";
57
+ import { withMeta } from "./meta.js";
58
+ // Re-export the shared honesty coercion (single audited copy in ./coerce.js —
59
+ // ADR-0005 v2 FIX-C) so a `num` regression fails together across sources. NO local
60
+ // num/str; the `"."`→null map is a WRAPPER around num, not a fork.
61
+ export { num };
62
+ // ─── SSRF core: the single fixed host + base path ─────────────────
63
+ export const FRED_HOST = "api.stlouisfed.org";
64
+ const FRED_SEARCH_PATH = "/fred/series/search";
65
+ const FRED_OBS_PATH = "/fred/series/observations";
66
+ // HOST+path labels — surface in ToolError.upstreamEndpoint; the key rides ONLY in
67
+ // the &api_key= query param, so no token can ever appear here.
68
+ const FRED_SEARCH_LABEL = "fred:/fred/series/search";
69
+ const FRED_OBS_LABEL = "fred:/fred/series/observations";
70
+ // ─── Validation charclasses (SSRF + "verify the input" honesty) ───
71
+ const SERIES_ID_RE = /^[A-Za-z0-9._-]+$/; // FRED series ids: GDP, CPIAUCSL, DGS10 …
72
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD observation bounds
73
+ const SORT_ORDERS = new Set(["asc", "desc"]);
74
+ const DEFAULT_SEARCH_LIMIT = 25;
75
+ const MAX_SEARCH_LIMIT = 1000;
76
+ const DEFAULT_OBS_LIMIT = 100;
77
+ const MAX_OBS_LIMIT = 100000;
78
+ // ─── Honesty notes (ADR-0048 required set) ────────────────────────
79
+ const KEY_REQUIRED_NOTE = "This source REQUIRES a free FRED_API_KEY (FRED has no keyless tier). The key is sent ONLY as the &api_key= query parameter to api.stlouisfed.org and is NEVER logged, echoed, or placed in this response.";
80
+ const MISSING_VALUE_NOTE = "FRED encodes a MISSING observation as the literal '.' — such values are mapped to null (missing), NEVER 0. A genuine reported 0 is preserved as 0.";
81
+ const COUNT_TOTAL_NOTE = "totalAvailable is FRED's exact reported `count` for the query; page with limit/offset (hasMore/nextOffset are derived from it, never fabricated).";
82
+ // ─── The key seam (REQUIRED; value NEVER leaked past the &api_key= param) ──
83
+ /** Read FRED_API_KEY from env; trim; return the value or undefined (unset/blank). */
84
+ export function fredApiKey() {
85
+ const raw = process.env.FRED_API_KEY;
86
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
87
+ return trimmed ? trimmed : undefined;
88
+ }
89
+ /**
90
+ * num(), but map FRED's missing-observation sentinel `"."` → null (missing). A
91
+ * genuine "0" stays 0 (num("0") === 0); a real numeric string parses. This is the
92
+ * BLS `"-"` lineage — a data-absence marker, never a fabricated 0.
93
+ */
94
+ export function fredValue(v) {
95
+ if (v === ".")
96
+ return null;
97
+ return num(v);
98
+ }
99
+ // ─── Shared SSRF-guarded fetch (REQUIRED key; &api_key= ONLY carrier) ──
100
+ /**
101
+ * GET one FRED JSON resource. The REQUIRED key is checked BEFORE any fetch (an
102
+ * unset FRED_API_KEY ⇒ invalid_input THROW, 0 network call). The query is built on
103
+ * the FIXED host from `params` + `&api_key=` + `&file_type=json` via URLSearchParams
104
+ * (no host/path steer); a post-construction hostname/protocol assertion +
105
+ * `redirect:"error"` lock it (fail closed on any off-host 3xx — it could carry the
106
+ * key away). `label` is host+path only.
107
+ *
108
+ * A 400 carrying `{error_message}` (a bad series_id / expired key) is reclassified
109
+ * to invalid_input CARRYING the FRED message. `getJson`/`fetchWithRetry` discards a
110
+ * non-ok body (it throws before reading it), so to surface FRED's honest reason we
111
+ * re-read the 400 body via a single bare GET on the error path ONLY (the happy /
112
+ * 5xx / 429 / timeout paths keep the shared envelope's retry taxonomy untouched).
113
+ */
114
+ async function getFred(path, label, params, key) {
115
+ params.set("api_key", key);
116
+ params.set("file_type", "json");
117
+ const url = `https://${FRED_HOST}${path}?${params.toString()}`;
118
+ const built = new URL(url);
119
+ if (built.hostname !== FRED_HOST || built.protocol !== "https:") {
120
+ throw new ToolErrorCarrier({
121
+ kind: "invalid_input",
122
+ retryable: false,
123
+ message: `Constructed FRED URL host ${JSON.stringify(built.hostname)} (${built.protocol}) is not ${FRED_HOST} over https — refusing to fetch (SSRF safety).`,
124
+ upstreamEndpoint: label,
125
+ });
126
+ }
127
+ try {
128
+ // The key rides in &api_key= ONLY (never the label/_meta); redirect:"error"
129
+ // (fail closed on any off-host 3xx). A 200 non-JSON body ⇒ getJson's r.json()
130
+ // throws SyntaxError ⇒ the caller reclassifies to schema_drift.
131
+ return await getJson(url, { label, redirect: "error" });
132
+ }
133
+ catch (e) {
134
+ if (e instanceof ToolErrorCarrier) {
135
+ // A 400 (missing/bad key, or a bad series_id) carries FRED's honest
136
+ // `{error_message}`, but fetchWithRetry discarded the body. Re-read it once so
137
+ // the caller learns the REAL reason (never a fake-empty). A body that no longer
138
+ // 400s / is unreadable falls back to the generic 400 carrier unchanged.
139
+ if (e.toolError.upstreamStatus === 400) {
140
+ const fredMsg = await readFredErrorMessage(url);
141
+ throw new ToolErrorCarrier({
142
+ kind: "invalid_input",
143
+ retryable: false,
144
+ message: fredMsg
145
+ ? `FRED rejected the request (HTTP 400): ${fredMsg}. Check FRED_API_KEY and the series_id / parameters.`
146
+ : "FRED rejected the request (HTTP 400) — check FRED_API_KEY and the series_id / parameters.",
147
+ upstreamStatus: 400,
148
+ upstreamEndpoint: label,
149
+ });
150
+ }
151
+ throw e; // 5xx → upstream_unavailable, 404 → not_found, 429 → rate_limited …
152
+ }
153
+ throw e; // SyntaxError (200 non-JSON) → the caller maps it to driftError
154
+ }
155
+ }
156
+ /** Single bare GET to read a FRED 400's `error_message` (error path ONLY). null on any failure. */
157
+ async function readFredErrorMessage(url) {
158
+ try {
159
+ const r = await fetch(url, {
160
+ signal: AbortSignal.timeout(15_000),
161
+ redirect: "error",
162
+ });
163
+ const body = (await r.json());
164
+ return typeof body?.error_message === "string" ? body.error_message : null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Search FRED series (`/fred/series/search`) by `search_text` → curated series
172
+ * rows + honest `_meta`. REQUIRES FRED_API_KEY (throws invalid_input pre-fetch when
173
+ * unset). totalAvailable = FRED's exact `count`; offset pagination.
174
+ */
175
+ export async function searchSeries(args) {
176
+ // ── [KEY] REQUIRED key — throw an honest config error BEFORE any fetch. ──
177
+ const key = fredApiKey();
178
+ if (key === undefined) {
179
+ throw new ToolErrorCarrier({
180
+ kind: "invalid_input",
181
+ retryable: false,
182
+ message: "FRED requires a free API key. Get one at https://fred.stlouisfed.org/docs/api/api_key.html and set FRED_API_KEY.",
183
+ upstreamEndpoint: FRED_SEARCH_LABEL,
184
+ });
185
+ }
186
+ // ── Validate + default (belt-and-suspenders behind the server Zod; a DIRECT
187
+ // handler call bypasses Zod). ──
188
+ const query = args.query ?? "";
189
+ if (query.trim() === "") {
190
+ throw new ToolErrorCarrier({
191
+ kind: "invalid_input",
192
+ retryable: false,
193
+ message: "fred_search_series requires a non-empty `query` (the FRED search_text), e.g. 'unemployment rate' or 'CPI'.",
194
+ upstreamEndpoint: FRED_SEARCH_LABEL,
195
+ });
196
+ }
197
+ const limit = clampLimit(args.limit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT);
198
+ const offset = clampOffset(args.offset);
199
+ const params = new URLSearchParams();
200
+ params.set("search_text", query);
201
+ params.set("limit", String(limit));
202
+ params.set("offset", String(offset));
203
+ let body;
204
+ try {
205
+ body = await getFred(FRED_SEARCH_PATH, FRED_SEARCH_LABEL, params, key);
206
+ }
207
+ catch (e) {
208
+ if (e instanceof SyntaxError) {
209
+ throw driftError(FRED_SEARCH_LABEL, "FRED /series/search returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
210
+ }
211
+ throw e;
212
+ }
213
+ // ── [P4] `seriess` MUST be an array (a missing/non-array is drift, never a
214
+ // fabricated empty). ──
215
+ const b = (body ?? {});
216
+ if (!Array.isArray(b.seriess)) {
217
+ throw driftError(FRED_SEARCH_LABEL, "FRED /series/search shape drift — `seriess` must be an array.");
218
+ }
219
+ const series = b.seriess.map((row) => {
220
+ const s = (row ?? {});
221
+ return {
222
+ id: str(s.id),
223
+ title: str(s.title),
224
+ frequency: str(s.frequency),
225
+ frequencyShort: str(s.frequency_short),
226
+ units: str(s.units),
227
+ seasonalAdjustment: str(s.seasonal_adjustment),
228
+ observationStart: str(s.observation_start),
229
+ observationEnd: str(s.observation_end),
230
+ lastUpdated: str(s.last_updated),
231
+ popularity: num(s.popularity),
232
+ };
233
+ });
234
+ const returned = series.length;
235
+ const totalAvailable = num(b.count); // [P1] EXACT — never returned
236
+ const hasMore = totalAvailable !== null && offset + returned < totalAvailable;
237
+ const nextOffset = hasMore ? offset + returned : null;
238
+ return withMeta({ series }, {
239
+ source: `${FRED_HOST} /fred/series/search (FRED; FRED_API_KEY)`,
240
+ keylessMode: false, // ★KEYED — the second key-required source
241
+ returned,
242
+ totalAvailable,
243
+ filtersApplied: [`query:${query}`],
244
+ filtersDropped: [],
245
+ fieldsUnavailable: [],
246
+ pagination: { offset, limit, hasMore, nextOffset },
247
+ notes: [KEY_REQUIRED_NOTE, COUNT_TOTAL_NOTE],
248
+ });
249
+ }
250
+ /**
251
+ * Fetch a FRED series' time series (`/fred/series/observations`) → date/value rows
252
+ * + honest `_meta`. REQUIRES FRED_API_KEY (throws invalid_input pre-fetch when
253
+ * unset). ★A missing observation (`value === "."`) maps to null, never 0.
254
+ * totalAvailable = FRED's exact `count`; offset pagination.
255
+ */
256
+ export async function seriesObservations(args) {
257
+ // ── [KEY] REQUIRED key — throw an honest config error BEFORE any fetch. ──
258
+ const key = fredApiKey();
259
+ if (key === undefined) {
260
+ throw new ToolErrorCarrier({
261
+ kind: "invalid_input",
262
+ retryable: false,
263
+ message: "FRED requires a free API key. Get one at https://fred.stlouisfed.org/docs/api/api_key.html and set FRED_API_KEY.",
264
+ upstreamEndpoint: FRED_OBS_LABEL,
265
+ });
266
+ }
267
+ // ── Validate (belt-and-suspenders behind the server Zod; a DIRECT handler call
268
+ // bypasses Zod — `series_id` rides the query, dates/sort_order too). ──
269
+ const seriesId = args.seriesId ?? "";
270
+ if (!SERIES_ID_RE.test(seriesId)) {
271
+ throw new ToolErrorCarrier({
272
+ kind: "invalid_input",
273
+ retryable: false,
274
+ message: `Invalid seriesId ${JSON.stringify(seriesId)} — expected a FRED series id (^[A-Za-z0-9._-]+$), e.g. "GDP", "CPIAUCSL", "UNRATE".`,
275
+ upstreamEndpoint: FRED_OBS_LABEL,
276
+ });
277
+ }
278
+ if (args.startDate !== undefined && !DATE_RE.test(args.startDate)) {
279
+ throw new ToolErrorCarrier({
280
+ kind: "invalid_input",
281
+ retryable: false,
282
+ message: `Invalid startDate ${JSON.stringify(args.startDate)} — expected YYYY-MM-DD (^\\d{4}-\\d{2}-\\d{2}$).`,
283
+ upstreamEndpoint: FRED_OBS_LABEL,
284
+ });
285
+ }
286
+ if (args.endDate !== undefined && !DATE_RE.test(args.endDate)) {
287
+ throw new ToolErrorCarrier({
288
+ kind: "invalid_input",
289
+ retryable: false,
290
+ message: `Invalid endDate ${JSON.stringify(args.endDate)} — expected YYYY-MM-DD (^\\d{4}-\\d{2}-\\d{2}$).`,
291
+ upstreamEndpoint: FRED_OBS_LABEL,
292
+ });
293
+ }
294
+ if (args.sortOrder !== undefined && !SORT_ORDERS.has(args.sortOrder)) {
295
+ throw new ToolErrorCarrier({
296
+ kind: "invalid_input",
297
+ retryable: false,
298
+ message: `Invalid sortOrder ${JSON.stringify(args.sortOrder)} — expected one of asc, desc.`,
299
+ upstreamEndpoint: FRED_OBS_LABEL,
300
+ });
301
+ }
302
+ const limit = clampLimit(args.limit, DEFAULT_OBS_LIMIT, MAX_OBS_LIMIT);
303
+ const offset = clampOffset(args.offset);
304
+ const params = new URLSearchParams();
305
+ params.set("series_id", seriesId);
306
+ params.set("limit", String(limit));
307
+ params.set("offset", String(offset));
308
+ const filtersApplied = [`series_id:${seriesId}`];
309
+ if (args.startDate !== undefined) {
310
+ params.set("observation_start", args.startDate);
311
+ filtersApplied.push(`observation_start:${args.startDate}`);
312
+ }
313
+ if (args.endDate !== undefined) {
314
+ params.set("observation_end", args.endDate);
315
+ filtersApplied.push(`observation_end:${args.endDate}`);
316
+ }
317
+ if (args.sortOrder !== undefined) {
318
+ params.set("sort_order", args.sortOrder);
319
+ filtersApplied.push(`sort_order:${args.sortOrder}`);
320
+ }
321
+ let body;
322
+ try {
323
+ body = await getFred(FRED_OBS_PATH, FRED_OBS_LABEL, params, key);
324
+ }
325
+ catch (e) {
326
+ if (e instanceof SyntaxError) {
327
+ throw driftError(FRED_OBS_LABEL, "FRED /series/observations returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
328
+ }
329
+ throw e;
330
+ }
331
+ // ── [P4] `observations` MUST be an array. ──
332
+ const b = (body ?? {});
333
+ if (!Array.isArray(b.observations)) {
334
+ throw driftError(FRED_OBS_LABEL, "FRED /series/observations shape drift — `observations` must be an array.");
335
+ }
336
+ const observations = b.observations.map((row) => {
337
+ const o = (row ?? {});
338
+ return { date: str(o.date), value: fredValue(o.value) }; // ★"." ⇒ null
339
+ });
340
+ const returned = observations.length;
341
+ const totalAvailable = num(b.count); // [P1] EXACT — never returned
342
+ const hasMore = totalAvailable !== null && offset + returned < totalAvailable;
343
+ const nextOffset = hasMore ? offset + returned : null;
344
+ return withMeta({ observations }, {
345
+ source: `${FRED_HOST} /fred/series/observations (FRED; FRED_API_KEY)`,
346
+ keylessMode: false, // ★KEYED
347
+ returned,
348
+ totalAvailable,
349
+ filtersApplied,
350
+ filtersDropped: [],
351
+ fieldsUnavailable: [],
352
+ pagination: { offset, limit, hasMore, nextOffset },
353
+ notes: [KEY_REQUIRED_NOTE, MISSING_VALUE_NOTE, COUNT_TOTAL_NOTE],
354
+ });
355
+ }
356
+ // ─── Small shared clamps (defensive, behind the server Zod bounds) ──
357
+ function clampLimit(v, def, max) {
358
+ if (typeof v !== "number" || !Number.isFinite(v))
359
+ return def;
360
+ const n = Math.floor(v);
361
+ if (n < 1)
362
+ return 1;
363
+ if (n > max)
364
+ return max;
365
+ return n;
366
+ }
367
+ function clampOffset(v) {
368
+ if (typeof v !== "number" || !Number.isFinite(v))
369
+ return 0;
370
+ const n = Math.floor(v);
371
+ return n < 0 ? 0 : n;
372
+ }
373
+ //# sourceMappingURL=fred.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fred.js","sourceRoot":"","sources":["../src/fred.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * gsa-perdiem.ts — GSA Federal Travel Per-Diem lookup (`api.gsa.gov`, base
3
+ * `/travel/perdiem/v2`) — ADR-0050. The lodging + M&IE rate ceilings the federal
4
+ * government reimburses for official travel, by city/state or ZIP for a given year.
5
+ *
6
+ * WHAT IT ADDS: a NEW travel-cost lane (the per-diem authority) on the SAME
7
+ * `api.gsa.gov` host as datagov-catalog.ts, so it REUSES the audited `datagovKey.ts`
8
+ * key seam VERBATIM (keyHeader / keyModeLabel / pushKeyNote) — keyless by default via
9
+ * the shared DEMO_KEY, upgraded by DATA_GOV_API_KEY. The key rides ONLY in the
10
+ * X-Api-Key header — NEVER the URL / label / _meta / a log (the K-test). This module
11
+ * writes ZERO fetch/coercion/error/meta code: it REUSES `getJson` (redirect:"error",
12
+ * the X-Api-Key header) / `driftError` / `num`·`str` (coerce.ts) / `withMeta`·
13
+ * `buildMeta`, and MIRRORS the datagov-catalog schema_drift catch-ladder verbatim.
14
+ *
15
+ * ★ SSRF: the host is a compile-time literal (`GSA_PERDIEM_HOST`). The two lookup
16
+ * modes ride FIXED path templates; every caller value (city/state/zip/year) is
17
+ * charclass-validated THEN `encodeURIComponent`-escaped into a single path segment
18
+ * (no raw passthrough, no query steer). A post-construction hostname/protocol
19
+ * assertion + `redirect:"error"` lock it (fail closed on any off-host 3xx — a 3xx
20
+ * off api.gsa.gov could carry the X-Api-Key header away).
21
+ *
22
+ * ★ HONESTY (ADR-0050 P1–P5, live-verified 2026-07-15):
23
+ * [INPUT] EITHER (city + state) OR zip — supplying BOTH, or NEITHER, ⇒ invalid_input
24
+ * with 0 fetch (an ambiguous/empty lookup is a caller error, never a guess).
25
+ * [P1] the API returns the COMPLETE rate set for the lookup (no pagination) ⇒
26
+ * totalAvailable = the flattened row count, complete:true. NEVER fabricated.
27
+ * [P2] `errors` non-null ⇒ invalid_input surfacing the message (never a fake
28
+ * empty); a genuine no-match (rates:[] / rate:[]) ⇒ honest empty (returned:0,
29
+ * complete:true); a 429 (DEMO_KEY ~10/hr) ⇒ rate_limited THROW honoring
30
+ * Retry-After; a 5xx ⇒ upstream_unavailable THROW; a 200 non-JSON ⇒
31
+ * schema_drift. A DOWN service is NEVER a returned:0.
32
+ * [P3] `value` (monthly max lodging $) / `meals` (M&IE cap $) via `num` (null-
33
+ * never-0 — a genuine 0 stays 0). `standardRate` / `isOconus` are STRING
34
+ * booleans "true"/"false" ⇒ coerced to a real boolean (an unrecognized value
35
+ * ⇒ null, never a fabricated false). The months array is preserved AS-IS
36
+ * (never padded/fabricated to 12).
37
+ * [P4] `rates` / a group's `rate` / `months.month` absent or non-array ⇒
38
+ * driftError (never a fabricated empty).
39
+ */
40
+ import { type MetaBundle } from "./meta.js";
41
+ export declare const GSA_PERDIEM_HOST = "api.gsa.gov";
42
+ export declare const DEFAULT_PERDIEM_YEAR = "2025";
43
+ export type PerdiemMonth = {
44
+ month: number | null;
45
+ monthName: string | null;
46
+ lodgingUsd: number | null;
47
+ };
48
+ export type PerdiemRate = {
49
+ city: string | null;
50
+ county: string | null;
51
+ state: string | null;
52
+ zip: string | null;
53
+ year: number | null;
54
+ isOconus: boolean | null;
55
+ standardRate: boolean | null;
56
+ mealsUsd: number | null;
57
+ monthlyLodgingUsd: PerdiemMonth[];
58
+ };
59
+ export type GsaPerdiemRatesArgs = {
60
+ city?: string;
61
+ state?: string;
62
+ zip?: string;
63
+ year?: string;
64
+ };
65
+ /**
66
+ * Look up GSA Federal Travel per-diem rates by EITHER (city + state) OR zip, for a
67
+ * given `year` (default 2025). Returns flattened rate rows (each outer state/year
68
+ * group × inner city/rate) + honest `_meta`: totalAvailable = the row count (no
69
+ * pagination — P1), lodging/meals as null-never-0 dollars (P3), standardRate/isOconus
70
+ * as real booleans, the months array preserved as-is. The DEMO_KEY rate disclosure
71
+ * rides in the notes.
72
+ */
73
+ export declare function perdiemRates(args: GsaPerdiemRatesArgs): Promise<MetaBundle>;
74
+ //# sourceMappingURL=gsa-perdiem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gsa-perdiem.d.ts","sourceRoot":"","sources":["../src/gsa-perdiem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAOzE,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAU9C,eAAO,MAAM,oBAAoB,SAAS,CAAC;AA+B3C,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iBAAiB,EAAE,YAAY,EAAE,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AA4CF;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,UAAU,CAAC,CAgMrB"}