Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# @suaveplan/dmarc - DMARC report parser — parses aggregate (RUA) and forensic (RUF) DMARC XML reports into typed structures
|
||||
|
||||
## Summary
|
||||
|
||||
DMARC report parser for `@suaveplan` — parses aggregate (RUA) and forensic (RUF) DMARC XML reports into validated, strongly-typed structures. Accepts raw XML strings or gzip-compressed binary blobs.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [gzip](./src/decoders/gzip.md) — Provides gzip decompression and input-decoding utilities for `@suaveplan/dmarc`. DMARC aggregate and forensic reports a…
|
||||
- [xml](./src/decoders/xml.md) — Wraps `fast-xml-parser` with a fixed, strict configuration tuned for DMARC report XML. The module isolates all parser c…
|
||||
- [errors](./src/errors/dmarc-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [dmarc-error](./src/errors/dmarc-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [dmarc-otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [aggregate](./src/parsers/aggregate.md) — Implements `parseDmarcAggregate`, the primary entry point for converting raw DMARC aggregate (RUA) report data into val…
|
||||
- [forensic](./src/parsers/forensic.md) — Implements `parseDmarcForensic`, the entry point for parsing DMARC forensic (RUF) reports. Forensic reports notify doma…
|
||||
<!-- end:auto-docs --> - [dmarc-error](./src/errors/dmarc-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [dmarc-otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [aggregate](./src/parsers/aggregate.md) — Implements `parseDmarcAggregate`, the primary entry point for converting raw DMARC aggregate (RUA) report data into val…
|
||||
- [forensic](./src/parsers/forensic.md) — Implements `parseDmarcForensic`, the entry point for parsing DMARC forensic (RUF) reports. Forensic reports notify doma…
|
||||
- [dmarc-error](./src/errors/dmarc-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [dmarc-otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [aggregate](./src/parsers/aggregate.md) — Implements `parseDmarcAggregate`, the primary entry point for converting raw DMARC aggregate (RUA) report data into val…
|
||||
- [forensic](./src/parsers/forensic.md) — Implements `parseDmarcForensic`, the entry point for parsing DMARC forensic (RUF) reports. Forensic reports notify doma…
|
||||
- [dmarc-error](./src/errors/dmarc-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [dmarc-otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [aggregate](./src/parsers/aggregate.md) — Implements `parseDmarcAggregate`, the primary entry point for converting raw DMARC aggregate (RUA) report data into val…
|
||||
- [forensic](./src/parsers/forensic.md) — Implements `parseDmarcForensic`, the entry point for parsing DMARC forensic (RUF) reports. Forensic reports notify doma…
|
||||
- [dmarc-error](./src/errors/dmarc-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dmarc-otel](./src/otel/dmarc-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [aggregate](./src/parsers/aggregate.md) — Implements `parseDmarcAggregate`, the primary entry point for converting raw DMARC aggregate (RUA) report data into val…
|
||||
- [forensic](./src/parsers/forensic.md) — Implements `parseDmarcForensic`, the entry point for parsing DMARC forensic (RUF) reports. Forensic reports notify doma…
|
||||
|
||||
## Packages
|
||||
|
||||
| Module | Description |
|
||||
|---|---|
|
||||
| [`src/parsers/aggregate.ts`](src/parsers/aggregate.md) | Parse DMARC aggregate (RUA) reports |
|
||||
| [`src/parsers/forensic.ts`](src/parsers/forensic.md) | Parse DMARC forensic (RUF) reports |
|
||||
| [`src/decoders/gzip.ts`](src/decoders/gzip.md) | Gzip detection and decompression |
|
||||
| [`src/decoders/xml.ts`](src/decoders/xml.md) | XML parsing via fast-xml-parser |
|
||||
| [`src/errors/dmarc-error.ts`](src/errors/dmarc-error.md) | Typed error class and code registry |
|
||||
| [`src/otel/dmarc-otel.ts`](src/otel/dmarc-otel.md) | OpenTelemetry metrics |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { parseDmarcAggregate, parseDmarcForensic } from "@suaveplan/dmarc";
|
||||
|
||||
// Parse an aggregate report from an XML string
|
||||
const aggregate = parseDmarcAggregate(xmlString);
|
||||
console.log(aggregate.reportMetadata.orgName);
|
||||
console.log(aggregate.record[0].row.sourceIp);
|
||||
|
||||
// Parse from gzip-compressed bytes (auto-detected)
|
||||
const { gzipSync, strToU8 } = await import("fflate");
|
||||
const compressed = gzipSync(strToU8(xmlString));
|
||||
const report = parseDmarcAggregate(compressed);
|
||||
|
||||
// Parse a forensic report
|
||||
const forensic = parseDmarcForensic(forensicXml);
|
||||
console.log(forensic.feedback.policyPublished.domain);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```ts
|
||||
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc";
|
||||
|
||||
try {
|
||||
parseDmarcAggregate(input);
|
||||
} catch (err) {
|
||||
if (err instanceof DmarcError) {
|
||||
switch (err.code) {
|
||||
case DMARC_CODES.PARSE_ERROR: // XML could not be parsed
|
||||
case DMARC_CODES.INVALID_REPORT: // Schema validation failed
|
||||
case DMARC_CODES.UNSUPPORTED_ENCODING: // Gzip decompression failed
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# gzip
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides gzip decompression and input-decoding utilities for `@suaveplan/dmarc`. DMARC aggregate and forensic reports are commonly delivered as gzip-compressed XML attachments (`.xml.gz`). This module abstracts the decompression step so that parsers can accept both raw XML strings and compressed binary blobs through a single entry point without depending on Node.js stream APIs or native bindings.
|
||||
|
||||
## Features
|
||||
|
||||
- `isGzip(bytes)` — magic-byte probe to identify gzip streams before attempting decompression
|
||||
- `decompressGzip(bytes)` — synchronous gunzip via `fflate.gunzipSync` + `strFromU8` for zero-dependency UTF-8 conversion
|
||||
- `decodeInput(input)` — unified entry point: pass-through for strings, auto-gunzip for gzip blobs, plain UTF-8 decode for non-gzip bytes
|
||||
- Structured errors via `DmarcError` with code `DMARC.UNSUPPORTED_ENCODING` on decompression failure
|
||||
- No Node.js built-ins (`node:zlib`, `node:fs`) — pure `fflate` implementation works in browsers and edge runtimes
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { decodeInput } from "@suaveplan/dmarc/decoders/gzip";
|
||||
|
||||
// Accepts a plain XML string
|
||||
const xml1 = decodeInput("<feedback>...</feedback>");
|
||||
|
||||
// Accepts a gzip-compressed Uint8Array
|
||||
const gzippedBytes = new Uint8Array([0x1f, 0x8b, /* ... */]);
|
||||
const xml2 = decodeInput(gzippedBytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { isGzip, decompressGzip, decodeInput } from "@suaveplan/dmarc/decoders/gzip";
|
||||
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc/errors";
|
||||
|
||||
async function fetchAndDecode(url: string): Promise<string> {
|
||||
const resp = await fetch(url);
|
||||
const bytes = new Uint8Array(await resp.arrayBuffer());
|
||||
|
||||
if (isGzip(bytes)) {
|
||||
// Explicit probe — useful for logging / metrics before decode
|
||||
console.log("Input is gzip-compressed, decompressing…");
|
||||
return decompressGzip(bytes);
|
||||
}
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
// Error handling
|
||||
try {
|
||||
decodeInput(new Uint8Array([0x1f, 0x8b, 0x00])); // truncated gzip
|
||||
} catch (err) {
|
||||
if (err instanceof DmarcError && err.code === DMARC_CODES.UNSUPPORTED_ENCODING) {
|
||||
console.error("Decompression failed:", err.message, err.cause);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `isGzip(bytes: Uint8Array): boolean`
|
||||
|
||||
Returns `true` if `bytes` starts with the two gzip magic bytes (`0x1f 0x8b`). Returns `false` for any input shorter than 2 bytes.
|
||||
|
||||
### `decompressGzip(bytes: Uint8Array): string`
|
||||
|
||||
Decompresses `bytes` synchronously via `fflate.gunzipSync` and converts the result to a UTF-8 string using `fflate.strFromU8`.
|
||||
|
||||
**Throws:** `DmarcError` with code `DMARC.UNSUPPORTED_ENCODING` if decompression fails.
|
||||
|
||||
### `decodeInput(input: string | Uint8Array): string`
|
||||
|
||||
Unified decoder: returns strings unchanged, decompresses gzip bytes, and UTF-8 decodes non-gzip bytes.
|
||||
|
||||
**Throws:** `DmarcError` with code `DMARC.UNSUPPORTED_ENCODING` if gzip decompression fails.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`fflate.gunzipSync` is chosen over `DecompressionStream` (Streams API) because the synchronous path avoids async complexity in callers and works identically across Node.js, Bun, Deno, and browser environments. `strFromU8` is preferred over `TextDecoder` on the decompressed output because it avoids an extra allocation when fflate has already produced a UTF-8 buffer.
|
||||
@@ -0,0 +1,69 @@
|
||||
# xml
|
||||
|
||||
## Purpose
|
||||
|
||||
Wraps `fast-xml-parser` with a fixed, strict configuration tuned for DMARC report XML. The module isolates all parser configuration in one place so that both aggregate and forensic parsers produce a consistently shaped raw object tree, reducing the surface area for subtle parsing divergences between the two report types.
|
||||
|
||||
## Features
|
||||
|
||||
- Shared `XMLParser` instance configuration: no-op attribute prefix, numeric tag-value coercion, whitespace trimming
|
||||
- `isArray` hook forces `<record>`, `<dkim>`, `<spf>`, and `<error>` nodes to always deserialise as arrays, preventing single-item vs. multi-item inconsistency
|
||||
- Structured errors via `DmarcError` with code `DMARC.PARSE_ERROR` on any parse failure
|
||||
- Pure function — stateless, no module-level side effects
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseXml } from "@suaveplan/dmarc/decoders/xml";
|
||||
|
||||
const raw = parseXml(`
|
||||
<feedback>
|
||||
<version>1.0</version>
|
||||
<report_metadata>
|
||||
<org_name>Example</org_name>
|
||||
</report_metadata>
|
||||
</feedback>
|
||||
`);
|
||||
console.log((raw as any).feedback.version); // "1.0"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseXml } from "@suaveplan/dmarc/decoders/xml";
|
||||
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc/errors";
|
||||
|
||||
function safeParse(xml: string): Record<string, unknown> | null {
|
||||
try {
|
||||
return parseXml(xml);
|
||||
} catch (err) {
|
||||
if (err instanceof DmarcError && err.code === DMARC_CODES.PARSE_ERROR) {
|
||||
console.error("XML parse failed:", err.message);
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Single <record> is still an array
|
||||
const result = parseXml("<feedback><record><row/></record></feedback>");
|
||||
const records = (result as any).feedback.record; // Array, length 1
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseXml(xml: string): Record<string, unknown>`
|
||||
|
||||
Parses a UTF-8 XML string into a raw nested JS object.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `xml` — Well-formed UTF-8 XML string.
|
||||
|
||||
**Returns:** Raw `Record<string, unknown>` mirroring the XML element hierarchy.
|
||||
|
||||
**Throws:** `DmarcError` with code `DMARC.PARSE_ERROR` if parsing fails or the parser returns a non-object value.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`fast-xml-parser` is used in preference to DOM-based parsers (e.g. `DOMParser`) because it works in all JavaScript environments — including Node.js and Bun — without requiring a DOM polyfill. The `isArray` hook is critical for DMARC reports: RFC 7489 allows a single `<record>` element, and without forcing array mode a one-record report would produce an object where `record` is a plain object rather than a length-1 array, breaking downstream mapping code. Numeric coercion (`parseTagValue: true`) handles the `<begin>`, `<end>`, `<count>`, and `<pct>` fields that appear as integer text nodes.
|
||||
@@ -0,0 +1,84 @@
|
||||
# dmarc-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the lightweight error class and stable code registry for `@suaveplan/dmarc`. Because this package carries no `@suaveplan/error` runtime dependency (it is a leaf package with no `@suaveplan/*` runtime deps), it provides its own self-contained error substrate that gives callers the same discriminated-error contract: a strongly-typed `code` field for `switch`/`instanceof` discrimination, plus an idiomatic `cause` chain via the built-in `ErrorOptions`.
|
||||
|
||||
## Features
|
||||
|
||||
- `DMARC_CODES` — three stable string-literal constants (`DMARC.PARSE_ERROR`, `DMARC.INVALID_REPORT`, `DMARC.UNSUPPORTED_ENCODING`)
|
||||
- `DmarcCode` — union type of all valid code values
|
||||
- `DmarcError` — `Error` subclass with a `readonly code: DmarcCode` field and preserved `name`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc/errors";
|
||||
|
||||
try {
|
||||
// ... parse logic
|
||||
} catch (cause) {
|
||||
throw new DmarcError(DMARC_CODES.PARSE_ERROR, "Failed to parse XML", { cause });
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DmarcError, DMARC_CODES, type DmarcCode } from "@suaveplan/dmarc/errors";
|
||||
|
||||
function handleDmarcError(err: unknown): never {
|
||||
if (err instanceof DmarcError) {
|
||||
switch (err.code) {
|
||||
case DMARC_CODES.PARSE_ERROR:
|
||||
console.error("XML parse failure:", err.message, err.cause);
|
||||
break;
|
||||
case DMARC_CODES.INVALID_REPORT:
|
||||
console.error("Schema validation failed:", err.message);
|
||||
break;
|
||||
case DMARC_CODES.UNSUPPORTED_ENCODING:
|
||||
console.error("Cannot decode input:", err.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `DMARC_CODES`
|
||||
|
||||
`{ PARSE_ERROR: "DMARC.PARSE_ERROR"; INVALID_REPORT: "DMARC.INVALID_REPORT"; UNSUPPORTED_ENCODING: "DMARC.UNSUPPORTED_ENCODING" }`
|
||||
|
||||
Immutable code registry. Use these constants instead of raw strings so that typos produce type errors.
|
||||
|
||||
### `DmarcCode`
|
||||
|
||||
`"DMARC.PARSE_ERROR" | "DMARC.INVALID_REPORT" | "DMARC.UNSUPPORTED_ENCODING"`
|
||||
|
||||
Union of all valid code values for use in signatures and `switch` exhaustiveness checks.
|
||||
|
||||
### `DmarcError`
|
||||
|
||||
`class DmarcError extends Error`
|
||||
|
||||
**Constructor:** `new DmarcError(code: DmarcCode, message: string, options?: ErrorOptions)`
|
||||
|
||||
- `code` — one of the `DMARC_CODES` values
|
||||
- `message` — human-readable description
|
||||
- `options.cause` — original error for cause-chain logging
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `code: DmarcCode` — readonly; set during construction
|
||||
- `name: string` — always `"DmarcError"`
|
||||
|
||||
**Throws:** nothing
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The absence of `@suaveplan/error` as a runtime dependency is intentional: this package is a leaf node in the dependency graph and must remain importable in environments where the broader Suaveplan stack is not installed. The same discriminated-error pattern (stable `code` field, `cause` chain, subclassed `Error`) is preserved so that tooling and callers that understand `@suaveplan/error` conventions can handle these errors uniformly.
|
||||
@@ -0,0 +1,69 @@
|
||||
# dmarc-otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides OpenTelemetry metric instruments for `@suaveplan/dmarc`. It gives operators visibility into parse throughput and latency without coupling the parse logic itself to observability concerns. All instruments are created lazily by the OTel SDK's no-op implementation, so importing this module in an environment without an OTel SDK installed is always safe — the calls become no-ops.
|
||||
|
||||
## Features
|
||||
|
||||
- `dmarcParseTotal` — counter tracking total parse invocations by `kind` and `status`
|
||||
- `dmarcParseDurationMs` — histogram recording parse latency in milliseconds by `kind`
|
||||
- `recordParse()` — convenience wrapper that updates both instruments in one call
|
||||
- Zero runtime dependencies beyond `@opentelemetry/api`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { recordParse } from "@suaveplan/dmarc/otel/dmarc-otel";
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
// ... parse logic
|
||||
recordParse("aggregate", "success", performance.now() - start);
|
||||
} catch {
|
||||
recordParse("aggregate", "error", performance.now() - start);
|
||||
throw;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { dmarcParseTotal, dmarcParseDurationMs, recordParse } from "@suaveplan/dmarc/otel/dmarc-otel";
|
||||
|
||||
// Direct instrument access for custom attribute sets
|
||||
dmarcParseTotal.add(1, { kind: "aggregate", status: "success", source: "s3" });
|
||||
dmarcParseDurationMs.record(42, { kind: "forensic", source: "email" });
|
||||
|
||||
// Or use the convenience wrapper for standard usage
|
||||
recordParse("forensic", "success", 38);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `dmarcParseTotal`
|
||||
|
||||
`Counter` — incremented once per parse call. Attributes: `kind` (`"aggregate" | "forensic"`), `status` (`"success" | "error"`).
|
||||
|
||||
### `dmarcParseDurationMs`
|
||||
|
||||
`Histogram` — records elapsed parse time in milliseconds. Attributes: `kind` (`"aggregate" | "forensic"`).
|
||||
|
||||
### `recordParse(kind, status, durationMs): void`
|
||||
|
||||
Convenience function that increments `dmarcParseTotal` and records `durationMs` in `dmarcParseDurationMs` in a single call.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `kind` — `"aggregate"` or `"forensic"`
|
||||
- `status` — `"success"` or `"error"`
|
||||
- `durationMs` — elapsed time in milliseconds
|
||||
|
||||
**Returns:** `void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The meter name is `"@suaveplan/dmarc"` which matches the npm package name, making it trivial to filter metrics by package in dashboards. Histogram bucket boundaries are left to the OTel SDK/exporter configuration rather than hardcoded here, which is the recommended practice per the OTel specification. Importing this module in test environments without an SDK causes zero issues because the OTel API ships a built-in no-op implementation.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,89 @@
|
||||
# forensic
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `parseDmarcForensic`, the entry point for parsing DMARC forensic (RUF) reports. Forensic reports notify domain owners of individual authentication failures; they share the `<feedback>` / `<report_metadata>` / `<policy_published>` structure with aggregate reports but omit the `<record>` block and may include a raw MIME-formatted `auth_failure_report` element. This module provides the same auto-detect-decompress → parse → map → validate pipeline as the aggregate parser but targets the forensic schema.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-detects gzip input via magic-byte probe
|
||||
- Accepts raw XML strings or `Uint8Array` binary blobs
|
||||
- Maps snake_case XML fields to camelCase typed fields
|
||||
- Surfaces `authFailureReport` string when present in the source XML
|
||||
- Validates against `DmarcForensicReportSchema` (Zod)
|
||||
- Emits OTel counter and histogram for both success and error paths
|
||||
- Wraps all failures in typed `DmarcError` with stable codes
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseDmarcForensic } from "@suaveplan/dmarc/parsers/forensic";
|
||||
|
||||
const report = parseDmarcForensic(`
|
||||
<feedback>
|
||||
<version>1.0</version>
|
||||
<report_metadata>
|
||||
<org_name>Example Corp</org_name>
|
||||
<email>dmarc@example.com</email>
|
||||
<report_id>ruf-001</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>
|
||||
</feedback>
|
||||
`);
|
||||
|
||||
console.log(report.feedback.reportMetadata.orgName); // "Example Corp"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseDmarcForensic } from "@suaveplan/dmarc/parsers/forensic";
|
||||
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc/errors";
|
||||
import { gzipSync, strToU8 } from "fflate";
|
||||
|
||||
const compressed = gzipSync(strToU8(xmlString));
|
||||
const report = parseDmarcForensic(compressed);
|
||||
|
||||
// Access optional auth failure report body
|
||||
if (report.authFailureReport) {
|
||||
console.log("Failure report MIME body:", report.authFailureReport);
|
||||
}
|
||||
|
||||
// Discriminated error handling
|
||||
try {
|
||||
parseDmarcForensic("<bad>xml</bad>");
|
||||
} catch (err) {
|
||||
if (err instanceof DmarcError && err.code === DMARC_CODES.INVALID_REPORT) {
|
||||
// Schema validation failed — missing required elements
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseDmarcForensic(input, options?): DmarcForensicReport`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `input: string | Uint8Array` — raw XML string or binary data
|
||||
- `options.decompress?: boolean` — force gzip decompression
|
||||
|
||||
**Returns:** `DmarcForensicReport` — validated forensic report
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `DmarcError(DMARC.PARSE_ERROR)` — XML parse failure
|
||||
- `DmarcError(DMARC.INVALID_REPORT)` — schema validation failure or missing required elements
|
||||
- `DmarcError(DMARC.UNSUPPORTED_ENCODING)` — gzip decompression failure
|
||||
|
||||
### `ParseDmarcForensicOptions`
|
||||
|
||||
`{ decompress?: boolean }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The forensic parser is structurally identical to the aggregate parser except for the output schema. Both share the same `str`/`num` coercion helpers and the same two-phase (map → validate) approach. In practice, RFC 7489 forensic reports are far less common than aggregate reports and are often transmitted as raw MIME messages rather than standalone XML files; the `authFailureReport` field is therefore optional and surfaced as a plain string for callers to process further if needed.
|
||||
Reference in New Issue
Block a user