/** * Pure XML parsing utilities for the national CAP 1.2 alert feeds — SACHET * (India, NDMA), PAGASA (Philippines), and BMKG (Indonesia). * * Zero I/O, zero logging: XML strings in, typed records (or a thrown, fixed, * sanitized message) out. The service layer owns fetching, caching, and * deciding garnish-vs-contract failure handling; this module owns every * shape/size constant and every parsing decision that follows from field * names, never from position. * * Load-bearing behaviours, verified live and in scratch on 2026-08-23: * - `fast-xml-parser`'s own parser is lenient — `parse('')` returns * `{"a":{"b":""}}` without throwing — so it must never be the * well-formedness check. `XMLValidator.validate` is the check, but it too * has gaps: it *accepts* two distinct self-closing roots * (`…`, ``) and it *accepts* two self-closing * roots that share a tag name (``, which the parser then * silently coalesces into a single `alert` key holding an array). Only a * post-parse structural check — exactly one non-PI root key, and that * key's value is not an array — catches every case actually seen. * - SACHET documents use a `cap:` namespace prefix throughout and carry no * ``/Atom `` may parse as a plain string or as * `{ '#text': string, '@_...': string }` when the tag also carries * attributes (SACHET's `…`) — every index * field is read through a text-or-`#text` accessor, never a bare cast. * - An HTTP 200 HTML error page is a *shape* failure, not a DOCTYPE failure — * it gets its own message so a caller doesn't have to pattern-match on the * word "DOCTYPE" to recognise "this wasn't even trying to be CAP". * * Every thrown message here is fixed text — never the input XML, never a * raw parser error — so an oversize or malformed feed can't leak upstream * content into logs or user-facing errors. */ import type { CapAlertDocument, CapAlertInfo, CapIndexEntry, NationalCapFeed, NationalCapWarning, NormalizedCapIndexEntry } from '../types/cap.js'; /** Maximum accepted byte size of a fetched CAP document or feed index. */ export declare const MAX_DOCUMENT_BYTES = 2000000; /** Maximum number of entries kept from one feed index. */ export declare const MAX_INDEX_ITEMS = 200; /** Maximum number of polygon rings kept for one warning's geometry. */ export declare const MAX_RINGS_PER_WARNING = 256; /** Maximum number of coordinate pairs kept for one polygon ring. */ export declare const MAX_POINTS_PER_RING = 10000; /** * Parse an XML document into an untyped structural tree, applying every * pre-parse guard in a fixed order and throwing a fixed, sanitized message — * never the input text, never a raw parser error — for any failure. */ export declare function parseXml(xml: string): unknown; /** * Parse a CAP feed index (RSS or Atom) into flat entries, by field name * only. Root/envelope checks require an **exact** root↔kind pairing and a * non-null envelope object — an rss-configured feed that returns Atom (or * vice versa), or any other unexpected top shape, throws rather than * returning `[]`: a silent empty list on safety data is a fabricated * all-clear. A recognised envelope with no `item`/`entry` array is an * honest empty and returns normally. */ export declare function parseCapIndex(xml: string, kind: 'rss' | 'atom'): { entries: CapIndexEntry[]; trimmed: boolean; }; /** * Drop and count any index entry lacking a non-empty trimmed `identifier` or * `documentUrl`; dedupe duplicate identifiers, first wins. Only validated * values may become cache keys downstream. */ export declare function normalizeIndexEntries(entries: CapIndexEntry[]): { entries: NormalizedCapIndexEntry[]; dropped: number; }; /** * Pure SSRF guard for every feed-supplied URL (index links, CAP document * URLs, SACHET `Polygon URL` parameters alike): `https:` only, hostname * **exactly** in the feed's allowlist (no port, no userinfo — a `user@host` * form is rejected), path starting with one of the feed's allowed prefixes. * Never throws. */ export declare function isAllowedFeedUrl(url: string, feed: Pick): boolean; /** * Parse a CAP alert document. Root must be `alert` (any namespace prefix * already stripped) — else throws. `info[]`/`area[]`/`parameter[]`/ * `polygon[]` are always arrays, never `undefined`, whether the source XML * had zero, one, or many. */ export declare function parseCapDocument(xml: string): CapAlertDocument; /** * Parse SACHET's separate linked-polygon document: ` * ……` siblings. Each ring * through `parseCapPolygon`; invalid rings are dropped silently — this may * legitimately return `rings: []` (the *service* decides what that means: * geometry unavailable). More than `MAX_RINGS_PER_WARNING` valid rings * keeps none and reports `trimmed: true`. */ export declare function parsePolygonDocument(xml: string): { rings: Array>; trimmed: boolean; failed: number; }; /** * Parse one CAP polygon string: whitespace-separated `"lat,lon"` pairs. * Never throws — returns `null` for fewer than 4 points, a non-finite * number, an out-of-range lat/lon, an **unclosed** ring (first point ≠ last * point — never auto-closed), or more than `MAX_POINTS_PER_RING` points. */ export declare function parseCapPolygon(text: string): Array<[number, number]> | null; /** * Select the language variant to render: the first block whose `language` * starts (case-insensitively) with `preferLanguage`, else the first block. * Generalised from `selectEnglishInfo` in `src/services/meteoalarm.ts:116`. * No script detection — a mislabelled `language` renders unmodified, same * as MeteoAlarm. */ export declare function selectPreferredInfo(info: CapAlertInfo[] | undefined, preferLanguage: string): CapAlertInfo | undefined; /** * Extract the referenced identifiers from a CAP `references` value: a * space-separated list of `sender,identifier,sent` triples. Entries without * a comma are taken as bare identifiers (permissive — feeds vary). * * Deliberate copy of `parseReferences` in `src/services/meteoalarm.ts` * (lines 145-161) rather than a shared import: this is a pure util and must * not import a service module, and the MeteoAlarm path — including its * existing tests — is locked and must not change to accommodate this * feature. */ export declare function parseReferences(references: string | undefined): string[]; /** The selected info block's `parameter` whose `valueName === 'Polygon URL'` (SACHET). */ export declare function linkedPolygonUrl(info: CapAlertInfo): string | undefined; /** * Flatten a parsed CAP document to a single-language-variant warning. * Returns `undefined` when there is no identifier or no info block — a * document-shape failure the caller must count as unavailable, not an * honest empty. * * `polygonSource: 'inline'` reads rings from every `area[].polygon[]` in the * *selected* info block. `polygonSource: 'linked-parameter'` sets * `linkedPolygonUrl` and leaves `polygons: []` for the service to fill after * fetching that URL. * * `polygonUnavailable`/`geometryTrimmed` are normally the service's call, * with one exception handled here: an inline document whose `` * elements *all* failed to parse, or whose ring count exceeded * `MAX_RINGS_PER_WARNING`, sets `polygonUnavailable: true` (geometry was * published but is unusable/trimmed) — the trim case also sets * `geometryTrimmed: true`. */ export declare function flattenCapAlert(doc: CapAlertDocument, feed: Pick, countryCode: string): NationalCapWarning | undefined; /** * Read-time filter pipeline, in this exact order: * 1. drop `status` present and ≠ `'Actual'`; * 2. drop `msgType === 'Cancel'`; * 3. drop expired `expires` (unparseable/missing → keep); * 4. build the superseded set from every *surviving* `msgType === 'Update'` * — including `AllClear` ones; * 5. drop superseded; * 6. **last**, drop `responseType` containing `'AllClear'`. * * Steps 4-6 must run in this order: dropping AllClear before building the * superseded set (step 4) would leave a PAGASA-cancelled-but-still-unexpired * advisory live — PAGASA's "Final" advisory is itself `msgType: 'Update'` + * `responseType: ['AllClear']`, and *it* is the message that retires the * prior advisory via `references`. On SACHET, supersession is a no-op: * SACHET republishes a warning under a new numeric identifier with * `references` pointing at the prior version, but that prior version was * never independently indexed, so steps 4-5 never have anything to match — * only step 3 (expiry) or step 6 (an explicit AllClear) retires a SACHET * warning. */ export declare function filterActiveCapWarnings(warnings: NationalCapWarning[], now: Date): NationalCapWarning[]; //# sourceMappingURL=capParse.d.ts.map