/** * Service for the keyless national CAP 1.2 alert feeds — NDMA SACHET * (India), PAGASA (Philippines), and BMKG (Indonesia). * * **Routing position.** `get_alerts` routes by country: US → NOAA, Canada → * GeoMet, MeteoAlarm members → MeteoAlarm, then *this* service, and only * then the optional keyed Google fallback. Reaching Google still proves the * point is in no keyless authority. * * **Contract, not garnish.** An index failure propagates with a fixed * sanitized message; a fabricated "no alerts" from a failed fetch would be a * dangerous lie on safety data. Per-document failures are counted and * disclosed, never silently dropped. * * **The N+1 and why the polygon hop runs after filtering.** SACHET publishes * geometry in a *separate* document per alert (`Polygon URL` parameter), so a * cold refresh is 1 index + N documents + N polygon documents. Polygons are * therefore fetched only for the warnings that survive the current read view * — not for expired or superseded ones. A warning resurrected later (because * the Update that superseded it expired first) renders at country level until * the next index refresh; that is rare, honest, and disclosed by the * country-level block's wording. * * **`(identifier, published stamp)` keying and the SACHET thread guid.** A * SACHET RSS `guid` is the alert *thread* id: `FetchXMLFile?identifier=` * serves whatever the *latest* version of that thread is. Keying the document * cache on the identifier alone would keep serving the version first seen — * an alert extended from 12:00 to 15:00 would read as *no alert* after 12:00, * the forbidden fabricated all-clear. The index `published` stamp changes * exactly when the document does, so the pair is what makes the key * immutable. The pair is encoded as one injective token, because * `Cache.generateKey` joins components with an unescaped `:` and both * identifiers (`urn:uuid:…`) and ISO stamps contain colons. * * **Unfiltered cache, read-time filter.** The country list cache holds the * complete assembled set; `filterActiveCapWarnings` runs on every return, * cached or fresh (the MeteoAlarm precedent). Caching post-filter would stop * an original reappearing when the Update superseding it expires first, and * would re-fetch every expired document on each 5-minute refresh. * * **Network policy.** Every URL taken from a feed body — document URLs and * `Polygon URL` parameters alike — is checked against the feed's own * HTTPS host/path allowlist before it is fetched; redirects are not followed * (a 3xx is an error); the response size is bounded at the transport as well * as after reading. Each feed has a request-start limiter consulted before * *every* request, and a whole refresh is bounded by a deadline. * * All parsing lives in `src/utils/capParse.ts`; this module does fetching, * caching, bounding, and error mapping only. Errors are plain sanitized * `Error`s (the MeteoAlarm/ACIS/NIFC precedent — `ApiServiceName` is a closed * union and this is a peripheral service). No message or log argument ever * carries a URL, a response body, or polygon text. */ import type { NationalCapFeed, NationalCapResult, NormalizedCapIndexEntry } from '../types/cap.js'; /** * The three national CAP feeds, as data rather than code. The only * per-country *code* is the SACHET polygon hop (`polygonSource: * 'linked-parameter'`). Exported so a future tool can reuse the country map. * * `allowedHosts`/`allowedPathPrefixes` are required by the type, so a feed * cannot be added without an allowlist. The path prefixes are deliberately * one level broader than any single observed document family: PAGASA serves * CAP documents under at least `/output/gfa/` and `/output/acp/` (live * 2026-08-23), so `/output/` is both correct and future-proof, where * enumerating the observed families would silently drop an unsampled one. */ export declare const NATIONAL_CAP_FEEDS: Record; /** Whether a country (lowercase ISO 3166-1 alpha-2 code) has a national CAP feed. */ export declare function isNationalCapCountry(countryCode: string): boolean; export interface NationalCapServiceConfig { timeout?: number; /** Feed map override — lets tests point URLs at fixtures without touching the real map. */ feeds?: Record; /** Injectable clock, so read-time filtering is deterministic under test. */ now?: () => Date; /** Injectable retry jitter (default `Math.random`), so backoff is deterministic under test. */ backoffJitter?: () => number; } export declare class NationalCapService { private client; private cache; private feeds; private limiters; private inFlight; private now; private backoffJitter; private documentTimeout; private maxIndexRetries; constructor(config?: NationalCapServiceConfig); /** The limiter for a feed, created on first use. */ private limiterFor; /** * Map a request failure to a fixed message naming the publisher. Never * includes a URL, a body, or a raw axios error. */ private toFeedError; /** Whether a mapped failure is worth retrying. */ private isRetryable; /** One limited GET returning the response body as text. */ private fetchText; /** The index fetch, with MeteoAlarm-style backoff on transient failures. */ private fetchIndex; /** A document/polygon fetch: one retry on a transient failure, none on 4xx/3xx. */ private fetchDocument; /** * The one injective cache-version token for a document/polygon. Both parts * are untrusted strings that routinely contain `:`, and * `Cache.generateKey` joins components with an unescaped `:` — passing them * as two adjacent components would let `('thread:2026-08-23T00', '00:00Z')` * and `('thread', '2026-08-23T00:00:00Z')` collide. */ private versionToken; /** * Fetch, parse and normalise one feed index. Uncached and un-deduped — * exposed for the live integration smoke and for diagnostics, deliberately * not on the `getWarnings` path. */ getIndex(countryCode: string): Promise<{ entries: NormalizedCapIndexEntry[]; trimmed: boolean; dropped: number; }>; /** * Current warnings for a national CAP country. * * The cached set is the complete, **unfiltered** assembly; the returned * view is always filtered against `now`, cached or fresh. * `polygonUnavailableCount` is derived over that returned view on every * call and never cached — a refresh-time count rendered over a re-filtered * list could contradict the block beneath it. * * @throws {Error} If the country has no feed, or the index request/parse fails. */ getWarnings(countryCode: string): Promise; /** Filter an assembled set to the current read view and derive its counts. */ private readView; /** One full refresh: index → documents → polygons for the current view. */ private refresh; /** * Run `worker` over `items` with a bounded fan-out, stopping early when the * deadline fires: no new work starts, and the caller treats anything not * settled as unavailable. */ private runBounded; /** * One index entry → a flattened warning, or `undefined` when the document * is unavailable or structurally unusable. * * The document cache holds the flattened record **without any refresh-time * geometry state**, and every refresh builds fresh objects from it — so a * transient polygon failure can never be frozen into a 24-hour cache entry. */ private loadDocument; /** * Surface an inline ring-cap trim as a bounded-array security event. * * The flattener detects the trim but cannot log it — `capParse.ts` is a * pure zero-I/O module — so the service reports it here, matching what * `applyRings` does for the linked-polygon path. Without this, an inline * feed's over-cap geometry would be dropped silently. */ private noteInlineGeometryTrim; /** Fetch and attach a linked polygon document, caching only a successful parse. */ private loadPolygon; /** Attach a parsed ring set, honouring the ring cap's all-or-nothing rule. */ private applyRings; /** Get cache statistics. */ getCacheStats(): import("../utils/cache.js").CacheStats; /** Clear the cache. */ clearCache(): void; } //# sourceMappingURL=nationalCap.d.ts.map