first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user