@cliwant/mcp-sam-gov 1.4.0 → 1.6.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.
Files changed (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +240 -231
  3. package/README.ko.md +240 -231
  4. package/README.md +725 -706
  5. package/dist/cbp-border.d.ts +51 -0
  6. package/dist/cbp-border.d.ts.map +1 -0
  7. package/dist/cbp-border.js +123 -0
  8. package/dist/cbp-border.js.map +1 -0
  9. package/dist/datagov-catalog.d.ts.map +1 -1
  10. package/dist/datagov-catalog.js +16 -2
  11. package/dist/datagov-catalog.js.map +1 -1
  12. package/dist/ecfr.d.ts +2 -2
  13. package/dist/ecfr.d.ts.map +1 -1
  14. package/dist/ecfr.js +24 -10
  15. package/dist/ecfr.js.map +1 -1
  16. package/dist/edgar.d.ts.map +1 -1
  17. package/dist/edgar.js +26 -6
  18. package/dist/edgar.js.map +1 -1
  19. package/dist/epa-envirofacts.d.ts.map +1 -1
  20. package/dist/epa-envirofacts.js +14 -1
  21. package/dist/epa-envirofacts.js.map +1 -1
  22. package/dist/errors.d.ts +10 -0
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +11 -0
  25. package/dist/errors.js.map +1 -1
  26. package/dist/far.d.ts.map +1 -1
  27. package/dist/far.js +3 -1
  28. package/dist/far.js.map +1 -1
  29. package/dist/federal-register.d.ts +2 -2
  30. package/dist/federal-register.d.ts.map +1 -1
  31. package/dist/federal-register.js +26 -10
  32. package/dist/federal-register.js.map +1 -1
  33. package/dist/feedback.d.ts +64 -0
  34. package/dist/feedback.d.ts.map +1 -0
  35. package/dist/feedback.js +131 -0
  36. package/dist/feedback.js.map +1 -0
  37. package/dist/fema.d.ts +36 -0
  38. package/dist/fema.d.ts.map +1 -1
  39. package/dist/fema.js +124 -0
  40. package/dist/fema.js.map +1 -1
  41. package/dist/gov-domains.d.ts +66 -0
  42. package/dist/gov-domains.d.ts.map +1 -0
  43. package/dist/gov-domains.js +211 -0
  44. package/dist/gov-domains.js.map +1 -0
  45. package/dist/nist-controls.d.ts +48 -0
  46. package/dist/nist-controls.d.ts.map +1 -0
  47. package/dist/nist-controls.js +174 -0
  48. package/dist/nist-controls.js.map +1 -0
  49. package/dist/nws-weather.d.ts +57 -0
  50. package/dist/nws-weather.d.ts.map +1 -0
  51. package/dist/nws-weather.js +131 -0
  52. package/dist/nws-weather.js.map +1 -0
  53. package/dist/openfda-drugsfda.d.ts +72 -0
  54. package/dist/openfda-drugsfda.d.ts.map +1 -0
  55. package/dist/openfda-drugsfda.js +230 -0
  56. package/dist/openfda-drugsfda.js.map +1 -0
  57. package/dist/openfda.d.ts.map +1 -1
  58. package/dist/openfda.js +31 -8
  59. package/dist/openfda.js.map +1 -1
  60. package/dist/server.d.ts.map +1 -1
  61. package/dist/server.js +374 -11
  62. package/dist/server.js.map +1 -1
  63. package/dist/treasury.d.ts +2 -0
  64. package/dist/treasury.d.ts.map +1 -1
  65. package/dist/treasury.js +7 -0
  66. package/dist/treasury.js.map +1 -1
  67. package/dist/usaspending.d.ts +32 -1
  68. package/dist/usaspending.d.ts.map +1 -1
  69. package/dist/usaspending.js +143 -16
  70. package/dist/usaspending.js.map +1 -1
  71. package/package.json +111 -111
  72. package/src/attachments.ts +652 -652
  73. package/src/bea.ts +372 -372
  74. package/src/bls.ts +1943 -1943
  75. package/src/cache.ts +73 -73
  76. package/src/cbp-border.ts +177 -0
  77. package/src/census-economic.ts +431 -431
  78. package/src/census.ts +735 -735
  79. package/src/ckan.ts +495 -495
  80. package/src/clinicaltrials.ts +923 -923
  81. package/src/cms-facility.ts +379 -379
  82. package/src/cms-hospital.ts +344 -344
  83. package/src/cms-supplier.ts +527 -527
  84. package/src/cms-utilization.ts +389 -389
  85. package/src/cms.ts +634 -634
  86. package/src/coerce.ts +47 -47
  87. package/src/courtlistener.ts +465 -465
  88. package/src/cpsc.ts +333 -333
  89. package/src/datagov-catalog.ts +312 -296
  90. package/src/datagov.ts +907 -907
  91. package/src/datagovKey.ts +68 -68
  92. package/src/datasource.ts +721 -721
  93. package/src/disclosure.ts +61 -61
  94. package/src/dol.ts +515 -515
  95. package/src/ecfr.ts +248 -231
  96. package/src/echo.ts +496 -496
  97. package/src/edgar.ts +3046 -3014
  98. package/src/epa-envirofacts.ts +358 -342
  99. package/src/errors.ts +324 -303
  100. package/src/fac.ts +529 -529
  101. package/src/far.ts +1009 -1007
  102. package/src/fdic.ts +2052 -2052
  103. package/src/federal-register.ts +725 -706
  104. package/src/feedback.ts +160 -0
  105. package/src/fema.ts +680 -541
  106. package/src/fpds.ts +620 -620
  107. package/src/fred.ts +464 -464
  108. package/src/gao.ts +744 -744
  109. package/src/gov-domains.ts +237 -0
  110. package/src/govinfo.ts +497 -497
  111. package/src/grants.ts +290 -290
  112. package/src/gsa-csv.ts +992 -992
  113. package/src/gsa-perdiem.ts +361 -361
  114. package/src/integrity.ts +928 -928
  115. package/src/keys.ts +268 -268
  116. package/src/lda.ts +385 -385
  117. package/src/meta.ts +292 -292
  118. package/src/nhtsa.ts +352 -352
  119. package/src/nih.ts +375 -375
  120. package/src/nist-controls.ts +219 -0
  121. package/src/nonprofit.ts +460 -460
  122. package/src/nppes.ts +834 -834
  123. package/src/nsf.ts +706 -706
  124. package/src/nvd.ts +1124 -1124
  125. package/src/nws-weather.ts +167 -0
  126. package/src/ofac.ts +1166 -1166
  127. package/src/openfda-device.ts +356 -356
  128. package/src/openfda-drugsfda.ts +313 -0
  129. package/src/openfda.ts +518 -495
  130. package/src/pricing.ts +1075 -1075
  131. package/src/sam-gov/client.ts +774 -774
  132. package/src/sam-gov/index.ts +32 -32
  133. package/src/sam-gov/types.ts +152 -152
  134. package/src/sba.ts +357 -357
  135. package/src/server.ts +6688 -6297
  136. package/src/snapshot.ts +223 -223
  137. package/src/socrata.ts +532 -532
  138. package/src/treasury.ts +582 -575
  139. package/src/usaspending.ts +2852 -2680
  140. package/src/usitc.ts +420 -420
package/src/snapshot.ts CHANGED
@@ -1,223 +1,223 @@
1
- /**
2
- * snapshot.ts — the snapshot-mirror READER + config (ADR-0045 Phase 2+4).
3
- *
4
- * A snapshot is an HONEST cache of PUBLIC open data: the builder
5
- * (`scripts/build-snapshots.mjs`) pulls high-value, slow-changing reference /
6
- * aggregate data from a CLEAN egress and writes a static JSON envelope
7
- * (`{ asOf, source, license, accessLevel, data }`) to a CDN/object store. A
8
- * client reads that snapshot ONLY when the live source is unreachable from its
9
- * egress (an edge/WAF IP-reputation block). This module is that reader; it slots
10
- * into the Phase-1 `throughPathChain` as a LOWER-priority `ResiliencePath`.
11
- *
12
- * ★DEFAULT-ON (resilience active out of the box): the snapshot base URL comes
13
- * from the env var `SAMGOV_SNAPSHOT_BASE_URL`. When it is UNSET, the reader now
14
- * resolves to `DEFAULT_SNAPSHOT_BASE_URL` — the public, weekly-refreshed GitHub
15
- * mirror — so every user gets offline fallback with zero configuration. An
16
- * operator can point at their own mirror (any custom URL) or DISABLE the
17
- * fallback entirely (pure live-only) with a disable sentinel
18
- * (`SAMGOV_SNAPSHOT_BASE_URL=off`); when disabled, `snapshotPath()` returns
19
- * `null` — the path is simply NOT added to a source's chain, so every source
20
- * stays single-path (live-only) and its output is byte-identical to today.
21
- *
22
- * ★POLICY BOUNDARY (ADR-0045 §"정책 경계", invariant — mirrors datasource.ts):
23
- * • PUBLIC-ONLY (M3/m2): the builder writes ONLY public + redistributable data
24
- * (accessLevel==="public"); the reader trusts that manifest and, defensively,
25
- * REFUSES to serve any envelope whose accessLevel is present and NOT "public".
26
- * • NO auth / paywall / CAPTCHA bypass, NO proxy, NO egress-hunting. If the
27
- * snapshot URL is ITSELF blocked, it fails HONESTLY (no route-around) — the
28
- * path throws and the chain falls to the next path or fails honestly.
29
- * • PROVENANCE + FRESHNESS disclosed (P5): a served snapshot stamps
30
- * `{ dataPath:"snapshot", asOf }`, which `buildMeta` turns into a staleness
31
- * note + gates `complete` off. A snapshot body can NEVER be labelled live.
32
- */
33
-
34
- import { getJson, driftError, type Provenance, type ResiliencePath } from "./datasource.js";
35
- import type { ResponseMeta } from "./meta.js";
36
-
37
- /**
38
- * The static snapshot envelope the builder writes and the reader parses. `data`
39
- * is the source-shaped payload (e.g. a Treasury `{data,meta}` envelope, a USAS
40
- * agency list). The metadata fields carry P5 provenance + the public-only gate.
41
- */
42
- export type SnapshotEnvelope<T = unknown> = {
43
- /** ISO-8601 UTC instant the origin data was retrieved (P5 freshness, m3). */
44
- asOf: string;
45
- /** Human+machine label of the ORIGIN this snapshot mirrors (attribution). */
46
- source: string;
47
- /** The origin's redistribution license (public-domain / CC0 / etc.). */
48
- license: string;
49
- /** Structural public-only gate (M3): the builder only writes "public". */
50
- accessLevel: string;
51
- /** The mirrored payload, in the SOURCE's native shape. */
52
- data: T;
53
- };
54
-
55
- /** Env-driven resilience config. Read at CALL TIME so it is togglable per call. */
56
- export type ResilienceConfig = {
57
- /**
58
- * The snapshot mirror base URL (no trailing slash) — the hosted default when
59
- * the env var is unset, a custom mirror when set to a URL, or `undefined` when
60
- * DISABLED via a sentinel (`off`) ⇒ every source stays live-only.
61
- */
62
- snapshotBaseUrl: string | undefined;
63
- };
64
-
65
- /**
66
- * The public, weekly-refreshed snapshot mirror (see .github/workflows/snapshots.yml);
67
- * read-only public reference data. This is the DEFAULT base URL when
68
- * `SAMGOV_SNAPSHOT_BASE_URL` is unset. Disable the fallback with
69
- * `SAMGOV_SNAPSHOT_BASE_URL=off`.
70
- */
71
- export const DEFAULT_SNAPSHOT_BASE_URL =
72
- "https://raw.githubusercontent.com/cliwant/mcp-sam-gov/snapshots";
73
-
74
- /** Case-insensitive disable sentinels: any of these (or a blank value) means
75
- * "snapshot DISABLED = live-only", resolving to `undefined`. */
76
- const SNAPSHOT_DISABLE_SENTINELS = new Set([
77
- "off",
78
- "none",
79
- "false",
80
- "0",
81
- "disabled",
82
- ]);
83
-
84
- /**
85
- * Resolve `SAMGOV_SNAPSHOT_BASE_URL` at CALL TIME (never cached at module load,
86
- * so a test — or an operator flipping the env — takes effect immediately, and so
87
- * importing this module has zero config side effects). The resolution is
88
- * DEFAULT-ON:
89
- * • env UNSET ⇒ `DEFAULT_SNAPSHOT_BASE_URL` (resilience ON by default).
90
- * • env is a DISABLE sentinel — case-insensitive one of `off` / `none` /
91
- * `false` / `0` / `disabled`, OR blank after trim ⇒ `undefined` (snapshot
92
- * disabled = live-only, byte-identical to pre-ADR output).
93
- * • any other value ⇒ that custom mirror URL, trailing slash stripped so
94
- * `${base}/${key}.json` is well-formed.
95
- */
96
- export function resolveSnapshotBaseUrl(): string | undefined {
97
- const raw = process.env.SAMGOV_SNAPSHOT_BASE_URL;
98
- if (raw === undefined) return DEFAULT_SNAPSHOT_BASE_URL;
99
- const trimmed = raw.trim();
100
- if (trimmed.length === 0) return undefined;
101
- if (SNAPSHOT_DISABLE_SENTINELS.has(trimmed.toLowerCase())) return undefined;
102
- return trimmed.replace(/\/+$/, "");
103
- }
104
-
105
- /** The env-driven resilience config (default = hosted snapshot mirror ON). */
106
- export function resilienceConfig(): ResilienceConfig {
107
- return { snapshotBaseUrl: resolveSnapshotBaseUrl() };
108
- }
109
-
110
- /**
111
- * P5 provenance → `_meta` partial (ADR-0045 B2/M1). The SHARED threading helper
112
- * every opted-in adapter uses so a served body discloses its access path
113
- * IDENTICALLY: it returns the EMPTY object `{}` for a `live` (or absent)
114
- * provenance — so spreading it into a meta partial adds NO keys and the `_meta`
115
- * stays BYTE-IDENTICAL to pre-ADR output (the INERT bar) — and returns
116
- * `{ dataPath, asOf? }` ONLY for a NON-live (`snapshot`) body. This is exactly
117
- * the guarded, `??`-free discipline treasury.ts inlines in `treasuryMeta`,
118
- * factored out so the usaspending/sba reference opt-ins can't drift from it.
119
- * buildMeta then surfaces the fields via its `if (partial.dataPath !== undefined)`
120
- * passthrough (and forces `complete!==true` + `totalIsEstimated` on non-live).
121
- */
122
- export function provenanceMeta(
123
- provenance: Provenance | undefined,
124
- ): Partial<ResponseMeta> {
125
- if (!provenance || provenance.dataPath === "live") return {};
126
- const meta: Partial<ResponseMeta> = { dataPath: provenance.dataPath };
127
- if (provenance.asOf !== undefined) meta.asOf = provenance.asOf;
128
- return meta;
129
- }
130
-
131
- /** A snapshot key is an internal identifier — restrict to a safe charset so it
132
- * can never inject a path segment / traversal into the fetch URL. */
133
- const SNAPSHOT_KEY_RE = /^[a-z0-9_]+$/;
134
-
135
- /**
136
- * Parse + validate a fetched snapshot envelope. Throws `schema_drift` (a
137
- * NON-retryable, NON-hard error — it does NOT trip the breaker and does NOT
138
- * count as an outage) when the envelope is malformed or, per the public-only
139
- * policy gate, when `accessLevel` is present and not "public". `asOf` is required
140
- * (P5: a non-live body MUST disclose its freshness).
141
- */
142
- function parseSnapshotEnvelope<T>(
143
- raw: unknown,
144
- label: string,
145
- ): { data: T; asOf: string } {
146
- if (typeof raw !== "object" || raw === null) {
147
- throw driftError(label, `Snapshot ${label} is not a JSON object.`);
148
- }
149
- const env = raw as Partial<SnapshotEnvelope<T>>;
150
- if (typeof env.asOf !== "string" || env.asOf.length === 0) {
151
- throw driftError(
152
- label,
153
- `Snapshot ${label} is missing a string 'asOf' (P5 freshness is mandatory for a non-live body).`,
154
- );
155
- }
156
- if (!("data" in env)) {
157
- throw driftError(label, `Snapshot ${label} is missing 'data'.`);
158
- }
159
- // ★Public-only gate (M3, defense-in-depth): the reader trusts the builder's
160
- // manifest but REFUSES a non-public envelope rather than serve restricted data.
161
- if (env.accessLevel !== undefined && env.accessLevel !== "public") {
162
- throw driftError(
163
- label,
164
- `Snapshot ${label} accessLevel is ${JSON.stringify(env.accessLevel)}, not "public" — refusing to serve (public-only policy).`,
165
- );
166
- }
167
- return { data: env.data as T, asOf: env.asOf };
168
- }
169
-
170
- /**
171
- * Build a `ResiliencePath` that reads the snapshot for `key` — or `null` when
172
- * the snapshot mirror is DISABLED (`SAMGOV_SNAPSHOT_BASE_URL=off`).
173
- *
174
- * When configured, the path fetches `${base}/${key}.json` via the shipped
175
- * `getJson` with `redirect:"error"` (off-host redirect ⇒ TypeError ⇒ honest
176
- * failure — no SSRF, no route-around). On success it parses the envelope and
177
- * returns `envelope.data` as the body; its `provenance` object is MUTATED in
178
- * place to carry `{ dataPath:"snapshot", asOf }` before the body resolves —
179
- * `throughPathChain` reads `path.provenance` AFTER awaiting `run()`, so the
180
- * per-fetch `asOf` is captured (mirrors the `{body,provenance}` contract).
181
- *
182
- * ★A NULL return is how the DISABLED (live-only) path stays byte-identical
183
- * structurally: the Treasury pilot builds
184
- * `[livePath, snapshotPath(key)].filter(Boolean)`, so when this returns null the
185
- * chain is single-entry (live only) ⇒ `throughPathChain` fast-paths ⇒
186
- * byte-identical to today.
187
- */
188
- export function snapshotPath<T = unknown>(
189
- key: string,
190
- ): ResiliencePath<T> | null {
191
- const base = resolveSnapshotBaseUrl();
192
- if (base === undefined) return null; // DISABLED (live-only) ⇒ no path.
193
- if (!SNAPSHOT_KEY_RE.test(key)) {
194
- // A bad key is a programming error, not a runtime data condition — refuse to
195
- // construct a path rather than build a URL that could traverse.
196
- throw driftError(
197
- `snapshot:${key}`,
198
- `Invalid snapshot key ${JSON.stringify(key)} (must match ${SNAPSHOT_KEY_RE}).`,
199
- );
200
- }
201
- const baseUrl = new URL(base);
202
- const url = `${base}/${key}.json`;
203
- const host = baseUrl.host;
204
- const label = `snapshot:${host}`;
205
- // The provenance object is shared by reference with what run() mutates; the
206
- // chain reads it only after run() resolves, so asOf is populated by then.
207
- const provenance: Provenance = { dataPath: "snapshot" };
208
- return {
209
- host,
210
- provenance,
211
- run: async () => {
212
- // Host-assert: the fetch URL MUST stay on the configured base's host
213
- // (defense-in-depth alongside redirect:"error").
214
- if (new URL(url).host !== host) {
215
- throw driftError(label, `Snapshot URL host drifted from ${host}.`);
216
- }
217
- const raw = await getJson<unknown>(url, { label, redirect: "error" });
218
- const { data, asOf } = parseSnapshotEnvelope<T>(raw, label);
219
- provenance.asOf = asOf;
220
- return data;
221
- },
222
- };
223
- }
1
+ /**
2
+ * snapshot.ts — the snapshot-mirror READER + config (ADR-0045 Phase 2+4).
3
+ *
4
+ * A snapshot is an HONEST cache of PUBLIC open data: the builder
5
+ * (`scripts/build-snapshots.mjs`) pulls high-value, slow-changing reference /
6
+ * aggregate data from a CLEAN egress and writes a static JSON envelope
7
+ * (`{ asOf, source, license, accessLevel, data }`) to a CDN/object store. A
8
+ * client reads that snapshot ONLY when the live source is unreachable from its
9
+ * egress (an edge/WAF IP-reputation block). This module is that reader; it slots
10
+ * into the Phase-1 `throughPathChain` as a LOWER-priority `ResiliencePath`.
11
+ *
12
+ * ★DEFAULT-ON (resilience active out of the box): the snapshot base URL comes
13
+ * from the env var `SAMGOV_SNAPSHOT_BASE_URL`. When it is UNSET, the reader now
14
+ * resolves to `DEFAULT_SNAPSHOT_BASE_URL` — the public, weekly-refreshed GitHub
15
+ * mirror — so every user gets offline fallback with zero configuration. An
16
+ * operator can point at their own mirror (any custom URL) or DISABLE the
17
+ * fallback entirely (pure live-only) with a disable sentinel
18
+ * (`SAMGOV_SNAPSHOT_BASE_URL=off`); when disabled, `snapshotPath()` returns
19
+ * `null` — the path is simply NOT added to a source's chain, so every source
20
+ * stays single-path (live-only) and its output is byte-identical to today.
21
+ *
22
+ * ★POLICY BOUNDARY (ADR-0045 §"정책 경계", invariant — mirrors datasource.ts):
23
+ * • PUBLIC-ONLY (M3/m2): the builder writes ONLY public + redistributable data
24
+ * (accessLevel==="public"); the reader trusts that manifest and, defensively,
25
+ * REFUSES to serve any envelope whose accessLevel is present and NOT "public".
26
+ * • NO auth / paywall / CAPTCHA bypass, NO proxy, NO egress-hunting. If the
27
+ * snapshot URL is ITSELF blocked, it fails HONESTLY (no route-around) — the
28
+ * path throws and the chain falls to the next path or fails honestly.
29
+ * • PROVENANCE + FRESHNESS disclosed (P5): a served snapshot stamps
30
+ * `{ dataPath:"snapshot", asOf }`, which `buildMeta` turns into a staleness
31
+ * note + gates `complete` off. A snapshot body can NEVER be labelled live.
32
+ */
33
+
34
+ import { getJson, driftError, type Provenance, type ResiliencePath } from "./datasource.js";
35
+ import type { ResponseMeta } from "./meta.js";
36
+
37
+ /**
38
+ * The static snapshot envelope the builder writes and the reader parses. `data`
39
+ * is the source-shaped payload (e.g. a Treasury `{data,meta}` envelope, a USAS
40
+ * agency list). The metadata fields carry P5 provenance + the public-only gate.
41
+ */
42
+ export type SnapshotEnvelope<T = unknown> = {
43
+ /** ISO-8601 UTC instant the origin data was retrieved (P5 freshness, m3). */
44
+ asOf: string;
45
+ /** Human+machine label of the ORIGIN this snapshot mirrors (attribution). */
46
+ source: string;
47
+ /** The origin's redistribution license (public-domain / CC0 / etc.). */
48
+ license: string;
49
+ /** Structural public-only gate (M3): the builder only writes "public". */
50
+ accessLevel: string;
51
+ /** The mirrored payload, in the SOURCE's native shape. */
52
+ data: T;
53
+ };
54
+
55
+ /** Env-driven resilience config. Read at CALL TIME so it is togglable per call. */
56
+ export type ResilienceConfig = {
57
+ /**
58
+ * The snapshot mirror base URL (no trailing slash) — the hosted default when
59
+ * the env var is unset, a custom mirror when set to a URL, or `undefined` when
60
+ * DISABLED via a sentinel (`off`) ⇒ every source stays live-only.
61
+ */
62
+ snapshotBaseUrl: string | undefined;
63
+ };
64
+
65
+ /**
66
+ * The public, weekly-refreshed snapshot mirror (see .github/workflows/snapshots.yml);
67
+ * read-only public reference data. This is the DEFAULT base URL when
68
+ * `SAMGOV_SNAPSHOT_BASE_URL` is unset. Disable the fallback with
69
+ * `SAMGOV_SNAPSHOT_BASE_URL=off`.
70
+ */
71
+ export const DEFAULT_SNAPSHOT_BASE_URL =
72
+ "https://raw.githubusercontent.com/cliwant/mcp-sam-gov/snapshots";
73
+
74
+ /** Case-insensitive disable sentinels: any of these (or a blank value) means
75
+ * "snapshot DISABLED = live-only", resolving to `undefined`. */
76
+ const SNAPSHOT_DISABLE_SENTINELS = new Set([
77
+ "off",
78
+ "none",
79
+ "false",
80
+ "0",
81
+ "disabled",
82
+ ]);
83
+
84
+ /**
85
+ * Resolve `SAMGOV_SNAPSHOT_BASE_URL` at CALL TIME (never cached at module load,
86
+ * so a test — or an operator flipping the env — takes effect immediately, and so
87
+ * importing this module has zero config side effects). The resolution is
88
+ * DEFAULT-ON:
89
+ * • env UNSET ⇒ `DEFAULT_SNAPSHOT_BASE_URL` (resilience ON by default).
90
+ * • env is a DISABLE sentinel — case-insensitive one of `off` / `none` /
91
+ * `false` / `0` / `disabled`, OR blank after trim ⇒ `undefined` (snapshot
92
+ * disabled = live-only, byte-identical to pre-ADR output).
93
+ * • any other value ⇒ that custom mirror URL, trailing slash stripped so
94
+ * `${base}/${key}.json` is well-formed.
95
+ */
96
+ export function resolveSnapshotBaseUrl(): string | undefined {
97
+ const raw = process.env.SAMGOV_SNAPSHOT_BASE_URL;
98
+ if (raw === undefined) return DEFAULT_SNAPSHOT_BASE_URL;
99
+ const trimmed = raw.trim();
100
+ if (trimmed.length === 0) return undefined;
101
+ if (SNAPSHOT_DISABLE_SENTINELS.has(trimmed.toLowerCase())) return undefined;
102
+ return trimmed.replace(/\/+$/, "");
103
+ }
104
+
105
+ /** The env-driven resilience config (default = hosted snapshot mirror ON). */
106
+ export function resilienceConfig(): ResilienceConfig {
107
+ return { snapshotBaseUrl: resolveSnapshotBaseUrl() };
108
+ }
109
+
110
+ /**
111
+ * P5 provenance → `_meta` partial (ADR-0045 B2/M1). The SHARED threading helper
112
+ * every opted-in adapter uses so a served body discloses its access path
113
+ * IDENTICALLY: it returns the EMPTY object `{}` for a `live` (or absent)
114
+ * provenance — so spreading it into a meta partial adds NO keys and the `_meta`
115
+ * stays BYTE-IDENTICAL to pre-ADR output (the INERT bar) — and returns
116
+ * `{ dataPath, asOf? }` ONLY for a NON-live (`snapshot`) body. This is exactly
117
+ * the guarded, `??`-free discipline treasury.ts inlines in `treasuryMeta`,
118
+ * factored out so the usaspending/sba reference opt-ins can't drift from it.
119
+ * buildMeta then surfaces the fields via its `if (partial.dataPath !== undefined)`
120
+ * passthrough (and forces `complete!==true` + `totalIsEstimated` on non-live).
121
+ */
122
+ export function provenanceMeta(
123
+ provenance: Provenance | undefined,
124
+ ): Partial<ResponseMeta> {
125
+ if (!provenance || provenance.dataPath === "live") return {};
126
+ const meta: Partial<ResponseMeta> = { dataPath: provenance.dataPath };
127
+ if (provenance.asOf !== undefined) meta.asOf = provenance.asOf;
128
+ return meta;
129
+ }
130
+
131
+ /** A snapshot key is an internal identifier — restrict to a safe charset so it
132
+ * can never inject a path segment / traversal into the fetch URL. */
133
+ const SNAPSHOT_KEY_RE = /^[a-z0-9_]+$/;
134
+
135
+ /**
136
+ * Parse + validate a fetched snapshot envelope. Throws `schema_drift` (a
137
+ * NON-retryable, NON-hard error — it does NOT trip the breaker and does NOT
138
+ * count as an outage) when the envelope is malformed or, per the public-only
139
+ * policy gate, when `accessLevel` is present and not "public". `asOf` is required
140
+ * (P5: a non-live body MUST disclose its freshness).
141
+ */
142
+ function parseSnapshotEnvelope<T>(
143
+ raw: unknown,
144
+ label: string,
145
+ ): { data: T; asOf: string } {
146
+ if (typeof raw !== "object" || raw === null) {
147
+ throw driftError(label, `Snapshot ${label} is not a JSON object.`);
148
+ }
149
+ const env = raw as Partial<SnapshotEnvelope<T>>;
150
+ if (typeof env.asOf !== "string" || env.asOf.length === 0) {
151
+ throw driftError(
152
+ label,
153
+ `Snapshot ${label} is missing a string 'asOf' (P5 freshness is mandatory for a non-live body).`,
154
+ );
155
+ }
156
+ if (!("data" in env)) {
157
+ throw driftError(label, `Snapshot ${label} is missing 'data'.`);
158
+ }
159
+ // ★Public-only gate (M3, defense-in-depth): the reader trusts the builder's
160
+ // manifest but REFUSES a non-public envelope rather than serve restricted data.
161
+ if (env.accessLevel !== undefined && env.accessLevel !== "public") {
162
+ throw driftError(
163
+ label,
164
+ `Snapshot ${label} accessLevel is ${JSON.stringify(env.accessLevel)}, not "public" — refusing to serve (public-only policy).`,
165
+ );
166
+ }
167
+ return { data: env.data as T, asOf: env.asOf };
168
+ }
169
+
170
+ /**
171
+ * Build a `ResiliencePath` that reads the snapshot for `key` — or `null` when
172
+ * the snapshot mirror is DISABLED (`SAMGOV_SNAPSHOT_BASE_URL=off`).
173
+ *
174
+ * When configured, the path fetches `${base}/${key}.json` via the shipped
175
+ * `getJson` with `redirect:"error"` (off-host redirect ⇒ TypeError ⇒ honest
176
+ * failure — no SSRF, no route-around). On success it parses the envelope and
177
+ * returns `envelope.data` as the body; its `provenance` object is MUTATED in
178
+ * place to carry `{ dataPath:"snapshot", asOf }` before the body resolves —
179
+ * `throughPathChain` reads `path.provenance` AFTER awaiting `run()`, so the
180
+ * per-fetch `asOf` is captured (mirrors the `{body,provenance}` contract).
181
+ *
182
+ * ★A NULL return is how the DISABLED (live-only) path stays byte-identical
183
+ * structurally: the Treasury pilot builds
184
+ * `[livePath, snapshotPath(key)].filter(Boolean)`, so when this returns null the
185
+ * chain is single-entry (live only) ⇒ `throughPathChain` fast-paths ⇒
186
+ * byte-identical to today.
187
+ */
188
+ export function snapshotPath<T = unknown>(
189
+ key: string,
190
+ ): ResiliencePath<T> | null {
191
+ const base = resolveSnapshotBaseUrl();
192
+ if (base === undefined) return null; // DISABLED (live-only) ⇒ no path.
193
+ if (!SNAPSHOT_KEY_RE.test(key)) {
194
+ // A bad key is a programming error, not a runtime data condition — refuse to
195
+ // construct a path rather than build a URL that could traverse.
196
+ throw driftError(
197
+ `snapshot:${key}`,
198
+ `Invalid snapshot key ${JSON.stringify(key)} (must match ${SNAPSHOT_KEY_RE}).`,
199
+ );
200
+ }
201
+ const baseUrl = new URL(base);
202
+ const url = `${base}/${key}.json`;
203
+ const host = baseUrl.host;
204
+ const label = `snapshot:${host}`;
205
+ // The provenance object is shared by reference with what run() mutates; the
206
+ // chain reads it only after run() resolves, so asOf is populated by then.
207
+ const provenance: Provenance = { dataPath: "snapshot" };
208
+ return {
209
+ host,
210
+ provenance,
211
+ run: async () => {
212
+ // Host-assert: the fetch URL MUST stay on the configured base's host
213
+ // (defense-in-depth alongside redirect:"error").
214
+ if (new URL(url).host !== host) {
215
+ throw driftError(label, `Snapshot URL host drifted from ${host}.`);
216
+ }
217
+ const raw = await getJson<unknown>(url, { label, redirect: "error" });
218
+ const { data, asOf } = parseSnapshotEnvelope<T>(raw, label);
219
+ provenance.asOf = asOf;
220
+ return data;
221
+ },
222
+ };
223
+ }