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,88 @@
# attachment-processor
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Normalises and validates `EmailAttachment` payloads emerging from
`parseEmail`. Each attachment is decoded to bytes, validated against a
caller-supplied size + MIME-type allowlist, hashed with SHA-256 over
the Web Crypto API, and packaged as a `ProcessedAttachment` carrying
the original metadata plus a deterministic content hash for dedup,
provenance, and dehydrated-template workflows.
## Features
- Three input shapes: `ArrayBuffer`, base64 string (per `encoding`),
or plain UTF-8 string — falls back to `Uint8Array(0)` for unknown
shapes (matches upstream behaviour) so callers do not see crashes
on rare attachment encodings.
- Size limit defaults to 25 MiB; overrides validated via Zod.
- MIME allowlist supports both exact match (`application/pdf`) and
wildcard (`image/*`).
- SHA-256 via `crypto.subtle.digest` — works in browsers, Node, Bun
and Deno without polyfills. Hash failures surface as
`EMAIL_PROCESSING.HASH_FAILURE` with the underlying error on
`cause`.
- OTel counters for processed vs rejected attachments (attributed by
reason) and a duration histogram for hash + metadata extraction.
## Usage: Basic
```ts
import { processAttachment } from "@suaveplan/email-processing/attachments";
const out = await processAttachment(parsed.attachments[0]);
if (out.valid) {
await store({ hash: out.metadata.contentHash, bytes: out.content });
}
```
## Usage: Advanced
```ts
import {
processAttachments,
type ProcessedAttachment,
} from "@suaveplan/email-processing/attachments";
const results = await processAttachments(parsed.attachments, {
maxSize: 5 * 1024 * 1024,
allowedMimeTypes: ["image/*", "application/pdf"],
});
const valid = results.filter((r): r is ProcessedAttachment => r.valid);
const rejected = results.filter((r) => !r.valid);
log.info({
valid: valid.length,
rejected: rejected.length,
hashes: valid.map((v) => v.metadata.contentHash),
});
```
## API Reference
- `AttachmentMetadata` — filename, size, MIME type, disposition,
contentHash (hex SHA-256), optional contentId + description.
- `ProcessedAttachment` — `{ metadata, content, valid, error? }`.
- `AttachmentProcessorOptions` — `{ allowedMimeTypes?, maxSize? }`,
Zod-validated.
- `processAttachment(att, options?)` — returns `ProcessedAttachment`.
- `processAttachments(list, options?)` — returns
`ProcessedAttachment[]`.
Errors: `INVALID_OPTIONS` for bad options, `HASH_FAILURE` if the Web
Crypto API throws (e.g. in environments where the digest is
unavailable).
## Implementation Notes
`crypto.subtle.digest` requires the source buffer to satisfy the
`BufferSource` type; passing a fresh `Uint8Array` works in every
runtime tested. The duration histogram uses `performance.now()` when
available and falls back to `Date.now()` for environments without it
(older Node test isolates). Errors surfaced inside `extractMetadata`
are propagated upward by `processAttachment` without being swallowed,
so callers do not lose root-cause information when hashing fails.