@iflow-mcp/kitfunso-luminus 0.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/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +488 -0
- package/dist/lib/audit.d.ts +3 -0
- package/dist/lib/audit.js +66 -0
- package/dist/lib/auth.d.ts +26 -0
- package/dist/lib/auth.js +199 -0
- package/dist/lib/cache.d.ts +25 -0
- package/dist/lib/cache.js +38 -0
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.js +10 -0
- package/dist/lib/corine.d.ts +31 -0
- package/dist/lib/corine.js +137 -0
- package/dist/lib/eea-natura2000.d.ts +7 -0
- package/dist/lib/eea-natura2000.js +53 -0
- package/dist/lib/entsoe-client.d.ts +22 -0
- package/dist/lib/entsoe-client.js +69 -0
- package/dist/lib/gis-sources.d.ts +33 -0
- package/dist/lib/gis-sources.js +392 -0
- package/dist/lib/natural-england.d.ts +27 -0
- package/dist/lib/natural-england.js +105 -0
- package/dist/lib/neso-gsp.d.ts +18 -0
- package/dist/lib/neso-gsp.js +113 -0
- package/dist/lib/overpass.d.ts +13 -0
- package/dist/lib/overpass.js +193 -0
- package/dist/lib/profiles.d.ts +23 -0
- package/dist/lib/profiles.js +149 -0
- package/dist/lib/schema-guard.d.ts +22 -0
- package/dist/lib/schema-guard.js +38 -0
- package/dist/lib/tool-handler.d.ts +15 -0
- package/dist/lib/tool-handler.js +95 -0
- package/dist/lib/xml-parser.d.ts +4 -0
- package/dist/lib/xml-parser.js +34 -0
- package/dist/lib/zone-codes.d.ts +12 -0
- package/dist/lib/zone-codes.js +127 -0
- package/dist/tools/acer-remit.d.ts +60 -0
- package/dist/tools/acer-remit.js +154 -0
- package/dist/tools/agricultural-land.d.ts +31 -0
- package/dist/tools/agricultural-land.js +210 -0
- package/dist/tools/ancillary-prices.d.ts +27 -0
- package/dist/tools/ancillary-prices.js +70 -0
- package/dist/tools/auctions.d.ts +15 -0
- package/dist/tools/auctions.js +89 -0
- package/dist/tools/balancing-actions.d.ts +22 -0
- package/dist/tools/balancing-actions.js +151 -0
- package/dist/tools/balancing.d.ts +21 -0
- package/dist/tools/balancing.js +56 -0
- package/dist/tools/carbon.d.ts +21 -0
- package/dist/tools/carbon.js +68 -0
- package/dist/tools/commodity-prices.d.ts +26 -0
- package/dist/tools/commodity-prices.js +100 -0
- package/dist/tools/compare-sites.d.ts +41 -0
- package/dist/tools/compare-sites.js +237 -0
- package/dist/tools/demand-forecast.d.ts +21 -0
- package/dist/tools/demand-forecast.js +56 -0
- package/dist/tools/elexon-bmrs.d.ts +72 -0
- package/dist/tools/elexon-bmrs.js +117 -0
- package/dist/tools/energi-data.d.ts +72 -0
- package/dist/tools/energi-data.js +170 -0
- package/dist/tools/energy-charts.d.ts +103 -0
- package/dist/tools/energy-charts.js +411 -0
- package/dist/tools/entsog.d.ts +71 -0
- package/dist/tools/entsog.js +159 -0
- package/dist/tools/era5-weather.d.ts +39 -0
- package/dist/tools/era5-weather.js +117 -0
- package/dist/tools/eu-gas-price.d.ts +38 -0
- package/dist/tools/eu-gas-price.js +110 -0
- package/dist/tools/fingrid.d.ts +39 -0
- package/dist/tools/fingrid.js +158 -0
- package/dist/tools/flood-risk.d.ts +33 -0
- package/dist/tools/flood-risk.js +166 -0
- package/dist/tools/flows.d.ts +23 -0
- package/dist/tools/flows.js +61 -0
- package/dist/tools/frequency.d.ts +10 -0
- package/dist/tools/frequency.js +35 -0
- package/dist/tools/gas-storage.d.ts +18 -0
- package/dist/tools/gas-storage.js +72 -0
- package/dist/tools/generation.d.ts +17 -0
- package/dist/tools/generation.js +80 -0
- package/dist/tools/grid-connection-intelligence.d.ts +42 -0
- package/dist/tools/grid-connection-intelligence.js +122 -0
- package/dist/tools/grid-connection-queue.d.ts +64 -0
- package/dist/tools/grid-connection-queue.js +198 -0
- package/dist/tools/grid-proximity.d.ts +38 -0
- package/dist/tools/grid-proximity.js +123 -0
- package/dist/tools/hydro-inflows.d.ts +34 -0
- package/dist/tools/hydro-inflows.js +114 -0
- package/dist/tools/hydro.d.ts +18 -0
- package/dist/tools/hydro.js +85 -0
- package/dist/tools/imbalance-prices.d.ts +21 -0
- package/dist/tools/imbalance-prices.js +56 -0
- package/dist/tools/intraday-prices.d.ts +21 -0
- package/dist/tools/intraday-prices.js +57 -0
- package/dist/tools/intraday-spread.d.ts +24 -0
- package/dist/tools/intraday-spread.js +55 -0
- package/dist/tools/land-constraints.d.ts +25 -0
- package/dist/tools/land-constraints.js +148 -0
- package/dist/tools/land-cover.d.ts +18 -0
- package/dist/tools/land-cover.js +64 -0
- package/dist/tools/lng-terminals.d.ts +22 -0
- package/dist/tools/lng-terminals.js +75 -0
- package/dist/tools/net-positions.d.ts +19 -0
- package/dist/tools/net-positions.js +74 -0
- package/dist/tools/nordpool-prices.d.ts +29 -0
- package/dist/tools/nordpool-prices.js +80 -0
- package/dist/tools/outages.d.ts +28 -0
- package/dist/tools/outages.js +107 -0
- package/dist/tools/power-plants.d.ts +26 -0
- package/dist/tools/power-plants.js +224 -0
- package/dist/tools/price-spread-analysis.d.ts +27 -0
- package/dist/tools/price-spread-analysis.js +97 -0
- package/dist/tools/prices.d.ts +23 -0
- package/dist/tools/prices.js +79 -0
- package/dist/tools/realtime-generation.d.ts +19 -0
- package/dist/tools/realtime-generation.js +141 -0
- package/dist/tools/ree-esios.d.ts +78 -0
- package/dist/tools/ree-esios.js +216 -0
- package/dist/tools/regelleistung.d.ts +28 -0
- package/dist/tools/regelleistung.js +71 -0
- package/dist/tools/remit-messages.d.ts +23 -0
- package/dist/tools/remit-messages.js +110 -0
- package/dist/tools/renewable-forecast.d.ts +23 -0
- package/dist/tools/renewable-forecast.js +75 -0
- package/dist/tools/rte-france.d.ts +72 -0
- package/dist/tools/rte-france.js +147 -0
- package/dist/tools/screen-site.d.ts +50 -0
- package/dist/tools/screen-site.js +288 -0
- package/dist/tools/site-revenue.d.ts +50 -0
- package/dist/tools/site-revenue.js +147 -0
- package/dist/tools/smard-data.d.ts +34 -0
- package/dist/tools/smard-data.js +155 -0
- package/dist/tools/solar.d.ts +23 -0
- package/dist/tools/solar.js +69 -0
- package/dist/tools/stormglass.d.ts +56 -0
- package/dist/tools/stormglass.js +172 -0
- package/dist/tools/terna.d.ts +69 -0
- package/dist/tools/terna.js +159 -0
- package/dist/tools/terrain-analysis.d.ts +19 -0
- package/dist/tools/terrain-analysis.js +120 -0
- package/dist/tools/transfer-capacity.d.ts +22 -0
- package/dist/tools/transfer-capacity.js +61 -0
- package/dist/tools/transmission.d.ts +29 -0
- package/dist/tools/transmission.js +159 -0
- package/dist/tools/uk-carbon.d.ts +51 -0
- package/dist/tools/uk-carbon.js +109 -0
- package/dist/tools/uk-grid.d.ts +28 -0
- package/dist/tools/uk-grid.js +70 -0
- package/dist/tools/us-gas.d.ts +30 -0
- package/dist/tools/us-gas.js +100 -0
- package/dist/tools/verify-gis-sources.d.ts +25 -0
- package/dist/tools/verify-gis-sources.js +119 -0
- package/dist/tools/weather.d.ts +27 -0
- package/dist/tools/weather.js +120 -0
- package/package.json +62 -0
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
// ConfigurationError
|
|
7
|
+
export class ConfigurationError extends Error {
|
|
8
|
+
constructor(keyName) {
|
|
9
|
+
super(`API key "${keyName}" is not configured.\n\n` +
|
|
10
|
+
`Resolution order:\n` +
|
|
11
|
+
` 1. Environment variable: ${keyName}\n` +
|
|
12
|
+
` 2. Key file: ${join(homedir(), ".luminus", "keys.json")} → { "${keyName}": "..." }\n\n` +
|
|
13
|
+
`Set one of the above and restart the server.`);
|
|
14
|
+
this.name = "ConfigurationError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Key file schema + cache
|
|
18
|
+
const KeyFileSchema = z.record(z.string(), z.string());
|
|
19
|
+
let cachedKeyFile = null;
|
|
20
|
+
let keyFileChecked = false;
|
|
21
|
+
const KEYS_PATH = join(homedir(), ".luminus", "keys.json");
|
|
22
|
+
async function warnOpenPermissions(filePath) {
|
|
23
|
+
if (process.platform === "win32")
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
const info = await stat(filePath);
|
|
27
|
+
const mode = info.mode & 0o777;
|
|
28
|
+
if (mode & 0o077) {
|
|
29
|
+
process.stderr.write(`[luminus] WARNING: ${filePath} is readable by others (mode ${mode.toString(8)}). ` +
|
|
30
|
+
`Run: chmod 600 ${filePath}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// stat failed — file may not exist, which is fine
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function loadKeyFile() {
|
|
38
|
+
if (keyFileChecked)
|
|
39
|
+
return cachedKeyFile ?? {};
|
|
40
|
+
keyFileChecked = true;
|
|
41
|
+
try {
|
|
42
|
+
await warnOpenPermissions(KEYS_PATH);
|
|
43
|
+
const raw = await readFile(KEYS_PATH, "utf-8");
|
|
44
|
+
const parsed = KeyFileSchema.parse(JSON.parse(raw));
|
|
45
|
+
cachedKeyFile = parsed;
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
// Distinguish "file not found" (normal) from "file exists but is broken" (warn)
|
|
50
|
+
const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
51
|
+
if (!isNotFound) {
|
|
52
|
+
process.stderr.write(`[luminus] WARNING: failed to load ${KEYS_PATH}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
53
|
+
}
|
|
54
|
+
cachedKeyFile = null;
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Resolved key cache (in-memory, per-process)
|
|
59
|
+
const resolvedKeys = new Map();
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an API key by name. Tries environment variables first, then
|
|
62
|
+
* ~/.luminus/keys.json. Throws ConfigurationError if not found.
|
|
63
|
+
* Resolved values are cached in memory for the process lifetime.
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveApiKey(name) {
|
|
66
|
+
const cached = resolvedKeys.get(name);
|
|
67
|
+
if (cached !== undefined)
|
|
68
|
+
return cached;
|
|
69
|
+
// Layer 1: environment variable
|
|
70
|
+
const envValue = process.env[name];
|
|
71
|
+
if (envValue) {
|
|
72
|
+
resolvedKeys.set(name, envValue);
|
|
73
|
+
return envValue;
|
|
74
|
+
}
|
|
75
|
+
// Layer 2: key file
|
|
76
|
+
const fileKeys = await loadKeyFile();
|
|
77
|
+
const fileValue = fileKeys[name];
|
|
78
|
+
if (fileValue) {
|
|
79
|
+
resolvedKeys.set(name, fileValue);
|
|
80
|
+
return fileValue;
|
|
81
|
+
}
|
|
82
|
+
// Not found
|
|
83
|
+
throw new ConfigurationError(name);
|
|
84
|
+
}
|
|
85
|
+
/** Constant-time string comparison. Reserved for future client authentication. */
|
|
86
|
+
export function timingSafeCompare(a, b) {
|
|
87
|
+
const bufA = Buffer.from(a);
|
|
88
|
+
const bufB = Buffer.from(b);
|
|
89
|
+
if (bufA.length !== bufB.length)
|
|
90
|
+
return false;
|
|
91
|
+
return timingSafeEqual(bufA, bufB);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Map of tool name to required API key names.
|
|
95
|
+
* Empty array = public API, no key needed.
|
|
96
|
+
*/
|
|
97
|
+
export const TOOL_KEY_REQUIREMENTS = {
|
|
98
|
+
// ENTSO-E tools
|
|
99
|
+
get_generation_mix: ["ENTSOE_API_KEY"],
|
|
100
|
+
get_day_ahead_prices: ["ENTSOE_API_KEY"],
|
|
101
|
+
get_cross_border_flows: ["ENTSOE_API_KEY"],
|
|
102
|
+
get_carbon_intensity: ["ENTSOE_API_KEY"],
|
|
103
|
+
get_balancing_prices: ["ENTSOE_API_KEY"],
|
|
104
|
+
get_renewable_forecast: ["ENTSOE_API_KEY"],
|
|
105
|
+
get_demand_forecast: ["ENTSOE_API_KEY"],
|
|
106
|
+
get_outages: ["ENTSOE_API_KEY"],
|
|
107
|
+
get_net_positions: ["ENTSOE_API_KEY"],
|
|
108
|
+
get_transfer_capacities: ["ENTSOE_API_KEY"],
|
|
109
|
+
get_hydro_reservoir: ["ENTSOE_API_KEY"],
|
|
110
|
+
get_intraday_prices: ["ENTSOE_API_KEY"],
|
|
111
|
+
get_imbalance_prices: ["ENTSOE_API_KEY"],
|
|
112
|
+
get_intraday_da_spread: ["ENTSOE_API_KEY"],
|
|
113
|
+
get_realtime_generation: ["ENTSOE_API_KEY"],
|
|
114
|
+
get_balancing_actions: ["ENTSOE_API_KEY"],
|
|
115
|
+
get_ancillary_prices: ["ENTSOE_API_KEY"],
|
|
116
|
+
get_remit_messages: ["ENTSOE_API_KEY"],
|
|
117
|
+
get_price_spread_analysis: ["ENTSOE_API_KEY"],
|
|
118
|
+
// GIE (Gas Infrastructure Europe) tools
|
|
119
|
+
get_gas_storage: ["GIE_API_KEY"],
|
|
120
|
+
get_lng_terminals: ["GIE_API_KEY"],
|
|
121
|
+
// EIA (US Energy Information Administration)
|
|
122
|
+
get_us_gas_data: ["EIA_API_KEY"],
|
|
123
|
+
// Fingrid (Finnish grid)
|
|
124
|
+
get_fingrid_data: ["FINGRID_API_KEY"],
|
|
125
|
+
// REE ESIOS (Spanish grid)
|
|
126
|
+
get_ree_esios: ["ESIOS_API_TOKEN"],
|
|
127
|
+
// Storm Glass (marine weather)
|
|
128
|
+
get_stormglass: ["STORMGLASS_API_KEY"],
|
|
129
|
+
// Public APIs — no key required
|
|
130
|
+
get_weather_forecast: [],
|
|
131
|
+
get_uk_carbon_intensity: [],
|
|
132
|
+
get_uk_grid_demand: [],
|
|
133
|
+
get_power_plants: [],
|
|
134
|
+
get_auction_results: [],
|
|
135
|
+
get_solar_irradiance: [],
|
|
136
|
+
get_eu_frequency: [],
|
|
137
|
+
get_transmission_lines: [],
|
|
138
|
+
get_energy_charts: [],
|
|
139
|
+
get_commodity_prices: [],
|
|
140
|
+
get_nordpool_prices: [],
|
|
141
|
+
get_smard_data: [],
|
|
142
|
+
get_entsog_data: [],
|
|
143
|
+
get_elexon_bmrs: [],
|
|
144
|
+
get_era5_weather: [],
|
|
145
|
+
get_regelleistung: [],
|
|
146
|
+
get_rte_france: [],
|
|
147
|
+
get_energi_data: [],
|
|
148
|
+
get_hydro_inflows: [],
|
|
149
|
+
get_acer_remit: [],
|
|
150
|
+
get_terna_data: [],
|
|
151
|
+
get_eu_gas_price: [],
|
|
152
|
+
get_terrain_analysis: [],
|
|
153
|
+
get_grid_proximity: [],
|
|
154
|
+
get_grid_connection_queue: [],
|
|
155
|
+
get_grid_connection_intelligence: [],
|
|
156
|
+
get_land_constraints: [],
|
|
157
|
+
get_agricultural_land: [],
|
|
158
|
+
get_flood_risk: [],
|
|
159
|
+
screen_site: [],
|
|
160
|
+
verify_gis_sources: [],
|
|
161
|
+
compare_sites: [],
|
|
162
|
+
estimate_site_revenue: ["ENTSOE_API_KEY"],
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Check whether all API keys for a tool are resolvable right now.
|
|
166
|
+
* Checks environment variables and the cached key file synchronously.
|
|
167
|
+
* Returns true for tools with no key requirements.
|
|
168
|
+
*/
|
|
169
|
+
export function hasRequiredKeys(toolName) {
|
|
170
|
+
const requirements = TOOL_KEY_REQUIREMENTS[toolName];
|
|
171
|
+
if (!requirements || requirements.length === 0)
|
|
172
|
+
return true;
|
|
173
|
+
return requirements.every((keyName) => {
|
|
174
|
+
// Check in-memory cache first
|
|
175
|
+
if (resolvedKeys.has(keyName))
|
|
176
|
+
return true;
|
|
177
|
+
// Check env
|
|
178
|
+
if (process.env[keyName])
|
|
179
|
+
return true;
|
|
180
|
+
// Check file cache (loaded asynchronously, may not be ready yet)
|
|
181
|
+
if (cachedKeyFile?.[keyName])
|
|
182
|
+
return true;
|
|
183
|
+
return false;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Check whether a specific API key name is available (env or key file). */
|
|
187
|
+
export function isKeyConfigured(keyName) {
|
|
188
|
+
if (resolvedKeys.has(keyName))
|
|
189
|
+
return true;
|
|
190
|
+
if (process.env[keyName])
|
|
191
|
+
return true;
|
|
192
|
+
if (cachedKeyFile?.[keyName])
|
|
193
|
+
return true;
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
/** Pre-load keys.json so hasRequiredKeys works immediately. */
|
|
197
|
+
export async function preloadKeyFile() {
|
|
198
|
+
await loadKeyFile();
|
|
199
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Simple in-memory TTL cache */
|
|
2
|
+
export declare class TtlCache {
|
|
3
|
+
private store;
|
|
4
|
+
get<T>(key: string): T | undefined;
|
|
5
|
+
set<T>(key: string, data: T, ttlMs: number): void;
|
|
6
|
+
clear(): void;
|
|
7
|
+
}
|
|
8
|
+
/** Cache TTL constants in milliseconds */
|
|
9
|
+
export declare const TTL: {
|
|
10
|
+
readonly REALTIME: number;
|
|
11
|
+
readonly PRICES: number;
|
|
12
|
+
readonly CAPACITY: number;
|
|
13
|
+
readonly FLOWS: number;
|
|
14
|
+
readonly STORAGE: number;
|
|
15
|
+
readonly WEATHER: number;
|
|
16
|
+
readonly EIA: number;
|
|
17
|
+
readonly FORECAST: number;
|
|
18
|
+
readonly BALANCING: number;
|
|
19
|
+
readonly STATIC_DATA: number;
|
|
20
|
+
readonly AUCTION: number;
|
|
21
|
+
readonly ANCILLARY: number;
|
|
22
|
+
readonly OUTAGES: number;
|
|
23
|
+
readonly FREQUENCY: number;
|
|
24
|
+
readonly INTRADAY: number;
|
|
25
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Simple in-memory TTL cache */
|
|
2
|
+
export class TtlCache {
|
|
3
|
+
store = new Map();
|
|
4
|
+
get(key) {
|
|
5
|
+
const entry = this.store.get(key);
|
|
6
|
+
if (!entry)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (Date.now() > entry.expiresAt) {
|
|
9
|
+
this.store.delete(key);
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return entry.data;
|
|
13
|
+
}
|
|
14
|
+
set(key, data, ttlMs) {
|
|
15
|
+
this.store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
|
16
|
+
}
|
|
17
|
+
clear() {
|
|
18
|
+
this.store.clear();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Cache TTL constants in milliseconds */
|
|
22
|
+
export const TTL = {
|
|
23
|
+
REALTIME: 5 * 60 * 1000, // 5 min
|
|
24
|
+
PRICES: 60 * 60 * 1000, // 1 hour
|
|
25
|
+
CAPACITY: 24 * 60 * 60 * 1000, // 24 hours
|
|
26
|
+
FLOWS: 5 * 60 * 1000, // 5 min
|
|
27
|
+
STORAGE: 60 * 60 * 1000, // 1 hour
|
|
28
|
+
WEATHER: 30 * 60 * 1000, // 30 min
|
|
29
|
+
EIA: 60 * 60 * 1000, // 1 hour
|
|
30
|
+
FORECAST: 60 * 60 * 1000, // 1 hour
|
|
31
|
+
BALANCING: 5 * 60 * 1000, // 5 min
|
|
32
|
+
STATIC_DATA: 24 * 60 * 60 * 1000, // 24 hours
|
|
33
|
+
AUCTION: 60 * 60 * 1000, // 1 hour
|
|
34
|
+
ANCILLARY: 60 * 60 * 1000, // 1 hour
|
|
35
|
+
OUTAGES: 15 * 60 * 1000, // 15 min
|
|
36
|
+
FREQUENCY: 30 * 1000, // 30 sec
|
|
37
|
+
INTRADAY: 15 * 60 * 1000, // 15 min
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseProfileArg(argv: string[]): string;
|
package/dist/lib/cli.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function parseProfileArg(argv) {
|
|
2
|
+
const idx = argv.indexOf("--profile");
|
|
3
|
+
if (idx === -1)
|
|
4
|
+
return "full";
|
|
5
|
+
const value = argv[idx + 1];
|
|
6
|
+
if (!value || value.startsWith("--")) {
|
|
7
|
+
throw new Error("Missing value for --profile.");
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORINE Land Cover 2018 — EEA ArcGIS REST client.
|
|
3
|
+
*
|
|
4
|
+
* Data source: Copernicus Land Monitoring Service / EEA
|
|
5
|
+
* Licence: Copernicus (free, attribution required)
|
|
6
|
+
* Coverage: EU27 + EEA/EFTA. Great Britain is NOT covered (UK left the
|
|
7
|
+
* programme after CLC 2012). Return null for GB queries.
|
|
8
|
+
*
|
|
9
|
+
* CLC uses a three-level hierarchical nomenclature with 44 classes.
|
|
10
|
+
* We return the raw code (string, e.g. "211"), the human label, the
|
|
11
|
+
* top-level class group, and a conservative `is_planning_exclusion` flag.
|
|
12
|
+
*/
|
|
13
|
+
/** EU member states + EEA/EFTA covered by CORINE 2018. */
|
|
14
|
+
export declare const CORINE_COVERED_COUNTRIES: Set<string>;
|
|
15
|
+
export interface CorineResult {
|
|
16
|
+
/** 3-digit CLC code string, e.g. "211" */
|
|
17
|
+
code: string;
|
|
18
|
+
/** Human-readable label, e.g. "Non-irrigated arable land" */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Top-level CLC class group */
|
|
21
|
+
class_group: string;
|
|
22
|
+
/**
|
|
23
|
+
* True if this land-cover type typically faces strong planning barriers
|
|
24
|
+
* for PV/BESS development. Conservative: only wetlands, water bodies,
|
|
25
|
+
* and semi-natural woodland are flagged. Agricultural land is NOT flagged
|
|
26
|
+
* here — that risk is assessed separately by ALC/national tools.
|
|
27
|
+
*/
|
|
28
|
+
is_planning_exclusion: boolean;
|
|
29
|
+
source: "corine-land-cover-2018";
|
|
30
|
+
}
|
|
31
|
+
export declare function queryCorineAtPoint(lat: number, lon: number): Promise<CorineResult | null>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORINE Land Cover 2018 — EEA ArcGIS REST client.
|
|
3
|
+
*
|
|
4
|
+
* Data source: Copernicus Land Monitoring Service / EEA
|
|
5
|
+
* Licence: Copernicus (free, attribution required)
|
|
6
|
+
* Coverage: EU27 + EEA/EFTA. Great Britain is NOT covered (UK left the
|
|
7
|
+
* programme after CLC 2012). Return null for GB queries.
|
|
8
|
+
*
|
|
9
|
+
* CLC uses a three-level hierarchical nomenclature with 44 classes.
|
|
10
|
+
* We return the raw code (string, e.g. "211"), the human label, the
|
|
11
|
+
* top-level class group, and a conservative `is_planning_exclusion` flag.
|
|
12
|
+
*/
|
|
13
|
+
import { guardArcGisFields } from "./schema-guard.js";
|
|
14
|
+
const CORINE_QUERY_URL = "https://image.discomap.eea.europa.eu/arcgis/rest/services/Corine/CLC2018_WM/MapServer/0/query";
|
|
15
|
+
/** EU member states + EEA/EFTA covered by CORINE 2018. */
|
|
16
|
+
export const CORINE_COVERED_COUNTRIES = new Set([
|
|
17
|
+
"AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
|
|
18
|
+
"FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT",
|
|
19
|
+
"NL", "PL", "PT", "RO", "SE", "SI", "SK",
|
|
20
|
+
// EEA/EFTA (also in CORINE)
|
|
21
|
+
"IS", "LI", "NO", "TR", "AL", "BA", "ME", "MK", "RS", "XK",
|
|
22
|
+
]);
|
|
23
|
+
/** Subset of the 44-class CLC 2018 nomenclature.
|
|
24
|
+
* Codes not in this map fall back to group-level labelling.
|
|
25
|
+
*/
|
|
26
|
+
const CLC_CODES = {
|
|
27
|
+
// 1xx — Artificial surfaces
|
|
28
|
+
"111": { label: "Continuous urban fabric", group: "Artificial surfaces", exclusion: false },
|
|
29
|
+
"112": { label: "Discontinuous urban fabric", group: "Artificial surfaces", exclusion: false },
|
|
30
|
+
"121": { label: "Industrial or commercial units", group: "Artificial surfaces", exclusion: false },
|
|
31
|
+
"122": { label: "Road and rail networks and associated land", group: "Artificial surfaces", exclusion: false },
|
|
32
|
+
"123": { label: "Port areas", group: "Artificial surfaces", exclusion: false },
|
|
33
|
+
"124": { label: "Airports", group: "Artificial surfaces", exclusion: false },
|
|
34
|
+
"131": { label: "Mineral extraction sites", group: "Artificial surfaces", exclusion: false },
|
|
35
|
+
"132": { label: "Dump sites", group: "Artificial surfaces", exclusion: false },
|
|
36
|
+
"133": { label: "Construction sites", group: "Artificial surfaces", exclusion: false },
|
|
37
|
+
"141": { label: "Green urban areas", group: "Artificial surfaces", exclusion: false },
|
|
38
|
+
"142": { label: "Sport and leisure facilities", group: "Artificial surfaces", exclusion: false },
|
|
39
|
+
// 2xx — Agricultural areas
|
|
40
|
+
"211": { label: "Non-irrigated arable land", group: "Agricultural areas", exclusion: false },
|
|
41
|
+
"212": { label: "Permanently irrigated land", group: "Agricultural areas", exclusion: false },
|
|
42
|
+
"213": { label: "Rice fields", group: "Agricultural areas", exclusion: false },
|
|
43
|
+
"221": { label: "Vineyards", group: "Agricultural areas", exclusion: false },
|
|
44
|
+
"222": { label: "Fruit trees and berry plantations", group: "Agricultural areas", exclusion: false },
|
|
45
|
+
"223": { label: "Olive groves", group: "Agricultural areas", exclusion: false },
|
|
46
|
+
"231": { label: "Pastures", group: "Agricultural areas", exclusion: false },
|
|
47
|
+
"241": { label: "Annual crops associated with permanent crops", group: "Agricultural areas", exclusion: false },
|
|
48
|
+
"242": { label: "Complex cultivation patterns", group: "Agricultural areas", exclusion: false },
|
|
49
|
+
"243": { label: "Land principally occupied by agriculture with significant areas of natural vegetation", group: "Agricultural areas", exclusion: false },
|
|
50
|
+
"244": { label: "Agro-forestry areas", group: "Agricultural areas", exclusion: false },
|
|
51
|
+
// 3xx — Forest and semi-natural areas
|
|
52
|
+
"311": { label: "Broad-leaved forest", group: "Forest and semi-natural areas", exclusion: true },
|
|
53
|
+
"312": { label: "Coniferous forest", group: "Forest and semi-natural areas", exclusion: true },
|
|
54
|
+
"313": { label: "Mixed forest", group: "Forest and semi-natural areas", exclusion: true },
|
|
55
|
+
"321": { label: "Natural grasslands", group: "Forest and semi-natural areas", exclusion: false },
|
|
56
|
+
"322": { label: "Moors and heathland", group: "Forest and semi-natural areas", exclusion: false },
|
|
57
|
+
"323": { label: "Sclerophyllous vegetation", group: "Forest and semi-natural areas", exclusion: false },
|
|
58
|
+
"324": { label: "Transitional woodland-shrub", group: "Forest and semi-natural areas", exclusion: false },
|
|
59
|
+
"331": { label: "Beaches, dunes, sands", group: "Forest and semi-natural areas", exclusion: false },
|
|
60
|
+
"332": { label: "Bare rocks", group: "Forest and semi-natural areas", exclusion: false },
|
|
61
|
+
"333": { label: "Sparsely vegetated areas", group: "Forest and semi-natural areas", exclusion: false },
|
|
62
|
+
"334": { label: "Burnt areas", group: "Forest and semi-natural areas", exclusion: false },
|
|
63
|
+
"335": { label: "Glaciers and perpetual snow", group: "Forest and semi-natural areas", exclusion: false },
|
|
64
|
+
// 4xx — Wetlands
|
|
65
|
+
"411": { label: "Inland marshes", group: "Wetlands", exclusion: true },
|
|
66
|
+
"412": { label: "Peat bogs", group: "Wetlands", exclusion: true },
|
|
67
|
+
"421": { label: "Salt marshes", group: "Wetlands", exclusion: true },
|
|
68
|
+
"422": { label: "Salines", group: "Wetlands", exclusion: true },
|
|
69
|
+
"423": { label: "Intertidal flats", group: "Wetlands", exclusion: true },
|
|
70
|
+
// 5xx — Water bodies
|
|
71
|
+
"511": { label: "Water courses", group: "Water bodies", exclusion: true },
|
|
72
|
+
"512": { label: "Water bodies", group: "Water bodies", exclusion: true },
|
|
73
|
+
"521": { label: "Coastal lagoons", group: "Water bodies", exclusion: true },
|
|
74
|
+
"522": { label: "Estuaries", group: "Water bodies", exclusion: true },
|
|
75
|
+
"523": { label: "Sea and ocean", group: "Water bodies", exclusion: true },
|
|
76
|
+
};
|
|
77
|
+
function getGroupFromCode(code) {
|
|
78
|
+
const first = code.charAt(0);
|
|
79
|
+
switch (first) {
|
|
80
|
+
case "1": return "Artificial surfaces";
|
|
81
|
+
case "2": return "Agricultural areas";
|
|
82
|
+
case "3": return "Forest and semi-natural areas";
|
|
83
|
+
case "4": return "Wetlands";
|
|
84
|
+
case "5": return "Water bodies";
|
|
85
|
+
default: return "Unknown";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function mapClcCode(code) {
|
|
89
|
+
const entry = CLC_CODES[code];
|
|
90
|
+
if (entry)
|
|
91
|
+
return { label: entry.label, group: entry.group, exclusion: entry.exclusion };
|
|
92
|
+
const group = getGroupFromCode(code);
|
|
93
|
+
return { label: `CLC code ${code}`, group, exclusion: false };
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// API client
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
export async function queryCorineAtPoint(lat, lon) {
|
|
99
|
+
const url = new URL(CORINE_QUERY_URL);
|
|
100
|
+
const p = url.searchParams;
|
|
101
|
+
p.set("geometry", `${lon},${lat}`);
|
|
102
|
+
p.set("geometryType", "esriGeometryPoint");
|
|
103
|
+
p.set("inSR", "4326");
|
|
104
|
+
p.set("spatialRel", "esriSpatialRelIntersects");
|
|
105
|
+
p.set("outFields", "Code_18");
|
|
106
|
+
p.set("returnGeometry", "false");
|
|
107
|
+
p.set("resultRecordCount", "1");
|
|
108
|
+
p.set("f", "json");
|
|
109
|
+
const response = await fetch(url.toString());
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const body = await response.text();
|
|
112
|
+
throw new Error(`CORINE API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
113
|
+
}
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
const json = await response.json();
|
|
116
|
+
if (json.error) {
|
|
117
|
+
throw new Error(`CORINE API error: ${json.error.message ?? JSON.stringify(json.error)}`);
|
|
118
|
+
}
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
const features = json.features ?? [];
|
|
121
|
+
if (features.length === 0) {
|
|
122
|
+
// No polygon hit — typically offshore, or at the very edge of coverage.
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
guardArcGisFields(features, ["Code_18"], "CORINE Land Cover");
|
|
126
|
+
const code = String(features[0].attributes?.Code_18 ?? "").trim();
|
|
127
|
+
if (!code)
|
|
128
|
+
return null;
|
|
129
|
+
const { label, group, exclusion } = mapClcCode(code);
|
|
130
|
+
return {
|
|
131
|
+
code,
|
|
132
|
+
label,
|
|
133
|
+
class_group: group,
|
|
134
|
+
is_planning_exclusion: exclusion,
|
|
135
|
+
source: "corine-land-cover-2018",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { guardArcGisFields } from "./schema-guard.js";
|
|
2
|
+
const EEA_NATURA2000_BASE = "https://bio.discomap.eea.europa.eu/arcgis/rest/services/ProtectedSites/Natura2000_Dyna_WM/MapServer/0/query";
|
|
3
|
+
const BIRDS_DIRECTIVE_CODES = new Set(["A", "C", "D", "F", "H", "J"]);
|
|
4
|
+
const HABITATS_DIRECTIVE_CODES = new Set(["B", "E", "G", "I", "K", "c"]);
|
|
5
|
+
function buildEnvelopeGeometry(lat, lon, radiusKm) {
|
|
6
|
+
const latDelta = radiusKm / 111.32;
|
|
7
|
+
const lonDelta = radiusKm / (111.32 * Math.cos(lat * (Math.PI / 180)));
|
|
8
|
+
return `${lon - lonDelta},${lat - latDelta},${lon + lonDelta},${lat + latDelta}`;
|
|
9
|
+
}
|
|
10
|
+
function mapSiteType(siteType) {
|
|
11
|
+
const value = String(siteType ?? "").trim();
|
|
12
|
+
if (BIRDS_DIRECTIVE_CODES.has(value))
|
|
13
|
+
return "natura2000_birds";
|
|
14
|
+
if (HABITATS_DIRECTIVE_CODES.has(value))
|
|
15
|
+
return "natura2000_habitats";
|
|
16
|
+
return "natura2000";
|
|
17
|
+
}
|
|
18
|
+
export async function queryNatura2000Layer(lat, lon, radiusKm) {
|
|
19
|
+
const url = new URL(EEA_NATURA2000_BASE);
|
|
20
|
+
const p = url.searchParams;
|
|
21
|
+
p.set("where", "1=1");
|
|
22
|
+
p.set("geometry", buildEnvelopeGeometry(lat, lon, radiusKm));
|
|
23
|
+
p.set("geometryType", "esriGeometryEnvelope");
|
|
24
|
+
p.set("inSR", "4326");
|
|
25
|
+
p.set("spatialRel", "esriSpatialRelIntersects");
|
|
26
|
+
p.set("outFields", "SITECODE,SITENAME,SITETYPE,MS,Area_km2");
|
|
27
|
+
p.set("returnGeometry", "false");
|
|
28
|
+
p.set("resultRecordCount", "50");
|
|
29
|
+
p.set("f", "json");
|
|
30
|
+
const response = await fetch(url.toString());
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const body = await response.text();
|
|
33
|
+
throw new Error(`EEA Natura 2000 API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
34
|
+
}
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
const json = await response.json();
|
|
37
|
+
if (json.error) {
|
|
38
|
+
throw new Error(`EEA Natura 2000 API error: ${json.error.message ?? JSON.stringify(json.error)}`);
|
|
39
|
+
}
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
const features = json.features ?? [];
|
|
42
|
+
guardArcGisFields(features, ["SITECODE", "SITENAME", "SITETYPE", "MS", "Area_km2"], "EEA Natura 2000");
|
|
43
|
+
return features.map((feature) => {
|
|
44
|
+
const attrs = feature.attributes ?? {};
|
|
45
|
+
const areaKm2 = attrs.Area_km2;
|
|
46
|
+
return {
|
|
47
|
+
name: String(attrs.SITENAME ?? attrs.SITECODE ?? "Unknown Natura 2000 site"),
|
|
48
|
+
type: mapSiteType(attrs.SITETYPE),
|
|
49
|
+
area_ha: typeof areaKm2 === "number" ? Math.round(areaKm2 * 100 * 100) / 100 : null,
|
|
50
|
+
source: "eea-natura2000",
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface EntsoeParams {
|
|
2
|
+
documentType: string;
|
|
3
|
+
processType?: string;
|
|
4
|
+
in_Domain?: string;
|
|
5
|
+
out_Domain?: string;
|
|
6
|
+
periodStart: string;
|
|
7
|
+
periodEnd: string;
|
|
8
|
+
[key: string]: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Format a Date as ENTSO-E expects: YYYYMMDDHHmm (UTC) */
|
|
11
|
+
export declare function formatEntsoeDate(date: Date): string;
|
|
12
|
+
/** Build start/end timestamps for a single day query */
|
|
13
|
+
export declare function dayRange(dateStr?: string): {
|
|
14
|
+
periodStart: string;
|
|
15
|
+
periodEnd: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Query the ENTSO-E API.
|
|
19
|
+
* Returns parsed XML as JS object.
|
|
20
|
+
* Caches by URL with the given TTL.
|
|
21
|
+
*/
|
|
22
|
+
export declare function queryEntsoe(params: EntsoeParams, ttlMs?: number): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseXml } from "./xml-parser.js";
|
|
2
|
+
import { TtlCache, TTL } from "./cache.js";
|
|
3
|
+
import { resolveApiKey } from "./auth.js";
|
|
4
|
+
const BASE_URL = "https://web-api.tp.entsoe.eu/api";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
async function getApiKey() {
|
|
7
|
+
try {
|
|
8
|
+
return await resolveApiKey("ENTSOE_API_KEY");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error("ENTSOE_API_KEY is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
|
|
12
|
+
"Get one at https://transparency.entsoe.eu/ (register → email token).");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Format a Date as ENTSO-E expects: YYYYMMDDHHmm (UTC) */
|
|
16
|
+
export function formatEntsoeDate(date) {
|
|
17
|
+
const y = date.getUTCFullYear();
|
|
18
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
19
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
20
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
21
|
+
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
|
22
|
+
return `${y}${m}${d}${h}${min}`;
|
|
23
|
+
}
|
|
24
|
+
/** Build start/end timestamps for a single day query */
|
|
25
|
+
export function dayRange(dateStr) {
|
|
26
|
+
const base = dateStr ? new Date(dateStr + "T00:00:00Z") : new Date();
|
|
27
|
+
const start = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate()));
|
|
28
|
+
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
|
|
29
|
+
return {
|
|
30
|
+
periodStart: formatEntsoeDate(start),
|
|
31
|
+
periodEnd: formatEntsoeDate(end),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Query the ENTSO-E API.
|
|
36
|
+
* Returns parsed XML as JS object.
|
|
37
|
+
* Caches by URL with the given TTL.
|
|
38
|
+
*/
|
|
39
|
+
export async function queryEntsoe(params, ttlMs = TTL.REALTIME) {
|
|
40
|
+
const url = new URL(BASE_URL);
|
|
41
|
+
url.searchParams.set("securityToken", await getApiKey());
|
|
42
|
+
for (const [key, value] of Object.entries(params)) {
|
|
43
|
+
if (value != null) {
|
|
44
|
+
url.searchParams.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const cacheKey = url.toString().replace(/securityToken=[^&]+/, "token=***");
|
|
48
|
+
const cached = cache.get(cacheKey);
|
|
49
|
+
if (cached)
|
|
50
|
+
return cached;
|
|
51
|
+
const response = await fetch(url.toString());
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const body = await response.text();
|
|
54
|
+
// ENTSO-E returns error details in XML
|
|
55
|
+
if (body.includes("Reason")) {
|
|
56
|
+
const parsed = parseXml(body);
|
|
57
|
+
const reason =
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
parsed?.Acknowledgement_MarketDocument?.Reason?.text ??
|
|
60
|
+
body.slice(0, 300);
|
|
61
|
+
throw new Error(`ENTSO-E API error: ${reason}`);
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`ENTSO-E API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
64
|
+
}
|
|
65
|
+
const xml = await response.text();
|
|
66
|
+
const result = parseXml(xml);
|
|
67
|
+
cache.set(cacheKey, result, ttlMs);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GIS data source metadata — provenance, reliability, and caveats.
|
|
3
|
+
*
|
|
4
|
+
* Each GIS tool response includes a `source_metadata` block drawn from
|
|
5
|
+
* these definitions. The goal is to make data quality and upstream
|
|
6
|
+
* limitations visible to callers, not hidden behind a clean API surface.
|
|
7
|
+
*/
|
|
8
|
+
export interface GisSourceMetadata {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly provider: string;
|
|
12
|
+
readonly licence: string;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly api_key_required: boolean;
|
|
15
|
+
readonly coverage: string;
|
|
16
|
+
readonly update_frequency: string;
|
|
17
|
+
readonly reliability: "high" | "medium" | "low";
|
|
18
|
+
readonly caveats: readonly string[];
|
|
19
|
+
readonly attribution: string;
|
|
20
|
+
readonly verified_at?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const GIS_SOURCES: Readonly<Record<string, GisSourceMetadata>>;
|
|
23
|
+
/** Health check endpoint for each GIS source. */
|
|
24
|
+
export interface GisHealthCheckConfig {
|
|
25
|
+
readonly source_id: string;
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly method: "GET" | "POST";
|
|
28
|
+
readonly body?: string;
|
|
29
|
+
readonly timeout_ms: number;
|
|
30
|
+
/** A function that checks whether the response body looks sane. */
|
|
31
|
+
readonly validate: (status: number, body: string) => string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare const GIS_HEALTH_CHECKS: readonly GisHealthCheckConfig[];
|