first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# aggregate
## Purpose
Implements `parseDmarcAggregate`, the primary entry point for converting raw DMARC aggregate (RUA) report data into validated, strongly-typed `DmarcAggregateReport` objects. It handles the full pipeline: gzip auto-detection and decompression, XML parsing, snake_case-to-camelCase field mapping, single-vs-multiple element normalisation, and Zod schema validation with structured error reporting.
## Features
- Auto-detects gzip input via magic-byte probe — no caller configuration needed for compressed reports
- Accepts both raw XML strings and `Uint8Array` binary blobs in a single overload
- Full snake_case → camelCase mapping for all RFC 7489 field names
- Handles variable-arity elements: `<record>`, `<dkim>`, `<spf>` can be single or multiple
- Validates the mapped structure with `DmarcAggregateReportSchema` (Zod)
- Emits OTel counter and histogram via `recordParse()` on both success and error paths
- Wraps all failures in typed `DmarcError` with stable codes
## Usage: Basic Example
```ts
import { parseDmarcAggregate } from "@suaveplan/dmarc/parsers/aggregate";
const report = parseDmarcAggregate(`
<feedback>
<version>1.0</version>
<report_metadata>
<org_name>Example Corp</org_name>
<email>dmarc@example.com</email>
<report_id>12345</report_id>
<date_range><begin>1609459200</begin><end>1609545600</end></date_range>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<adkim>r</adkim><aspf>r</aspf><p>none</p><pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>192.0.2.1</source_ip><count>5</count>
<policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated>
</row>
<identifiers><header_from>example.com</header_from></identifiers>
<auth_results>
<dkim><domain>example.com</domain><result>pass</result></dkim>
<spf><domain>example.com</domain><result>pass</result></spf>
</auth_results>
</record>
</feedback>
`);
console.log(report.reportMetadata.orgName); // "Example Corp"
console.log(report.record[0].row.count); // 5
```
## Usage: Advanced Example
```ts
import { parseDmarcAggregate } from "@suaveplan/dmarc/parsers/aggregate";
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc/errors";
import { gzipSync, strToU8 } from "fflate";
// Parse from gzip-compressed bytes
const xmlBytes = strToU8(xmlString);
const compressed = gzipSync(xmlBytes);
const report = parseDmarcAggregate(compressed);
// Error discrimination
try {
parseDmarcAggregate("not xml");
} catch (err) {
if (err instanceof DmarcError) {
switch (err.code) {
case DMARC_CODES.PARSE_ERROR:
// XML could not be parsed
break;
case DMARC_CODES.INVALID_REPORT:
// Parsed but failed schema validation
break;
case DMARC_CODES.UNSUPPORTED_ENCODING:
// Gzip decompression failed
break;
}
}
}
```
## API Reference
### `parseDmarcAggregate(input, options?): DmarcAggregateReport`
**Parameters:**
- `input: string | Uint8Array` — raw XML string or binary data (plain or gzip-compressed)
- `options.decompress?: boolean` — force gzip decompression even without magic bytes
**Returns:** `DmarcAggregateReport` — fully validated aggregate report
**Throws:**
- `DmarcError(DMARC.PARSE_ERROR)` — XML parse failure
- `DmarcError(DMARC.INVALID_REPORT)` — missing required elements or schema validation failure
- `DmarcError(DMARC.UNSUPPORTED_ENCODING)` — gzip decompression failure
### `ParseDmarcAggregateOptions`
`{ decompress?: boolean }`
## Implementation Notes
The parser uses a two-phase approach: structural mapping first (XML → plain object), validation second (Zod). This separation means that when validation fails the error message includes Zod's structured issue list rather than a raw type error, making debugging significantly easier. The `performance.now()` timing wraps the entire pipeline so OTel histograms reflect the true end-to-end cost including decompression and schema validation.