Files
suaveplan/openspec/archive/2026-04-19-enterprise-mime/specs/mime/spec.md
2026-05-27 23:29:53 +01:00

9.7 KiB

Capability Spec: MIME

Package: @suaveplan/mime Version: 0.0.0 (genesis) Status: ADDED


ADDED Requirements

MIME-01 — Magic-byte detection via compiled trie

detectFromMagicBytes(bytes) MUST match bytes against a compiled trie built from the registered signature table. Matching MUST be offset-aware (each Signature may declare a non-zero offset) and mask-aware (Signature.mask applies a bitmask before comparison). If bytes is shorter than the minimum bytes required by any registered signature, the function MUST throw InsufficientBytesError with context.needed and context.got populated. On success it MUST return a MagicBytesResult identifying the best-match MIME type and the matched Signature.

MIME-02 — All-matches magic-byte detection

detectAllFromMagicBytes(bytes) MUST return every MagicBytesResult whose signature matches bytes, sorted by specificity (longest matching byte sequence first). It MUST NOT throw InsufficientBytesError; signatures that cannot be evaluated against a too-short buffer are silently skipped.

MIME-03 — RFC 7231 Content-Type parsing

parseContentType(value) MUST parse a Content-Type header value per RFC 7231 §3.1.1.5. It MUST handle:

  • Optional leading/trailing OWS
  • Case-insensitive type/subtype (normalised to lowercase on output)
  • Quoted-string parameter values (including escaped characters per RFC 7230 §3.2.6)
  • Multiple parameters separated by ;
  • charset parameter normalised to lowercase

On parse failure it MUST throw InvalidContentTypeError (code MIME.INVALID_CONTENT_TYPE) with context.raw set to the input string.

MIME-04 — Content-Type round-trip serialisation

formatContentType(parsed) MUST serialise a ParsedContentType to a canonical string. Parameter values that contain token-unsafe characters MUST be quoted. The round-trip invariant parseContentType(formatContentType(p)) MUST yield a ParsedContentType structurally equal to p for any valid p.

MIME-05 — Content-Type equality and matching

contentTypeEquals(a, b) MUST return true iff both parsed values have identical (case-normalised) type, subtype, and the same parameter keys and values regardless of order.

contentTypeMatches(actual, pattern) MUST return true when pattern.subtype === '*' and actual.type === pattern.type, or when both type and subtype match exactly (case-insensitive). Wildcard */* MUST match any media type.

MIME-06 — Curated IANA-sourced registry

BUILTIN_DB MUST contain a comprehensive, read-only set of MimeEntry records covering at minimum: all major image formats (PNG, JPEG, GIF, WebP, AVIF, SVG, TIFF, BMP, ICO), audio formats (MP3, OGG, FLAC, WAV, AAC, OPUS), video formats (MP4, WebM, OGV, AVI, MKV), common application types (JSON, XML, PDF, ZIP, GZIP, WASM, CBOR, MessagePack, JavaScript, CSS, HTML, plain text), and font formats (WOFF, WOFF2, TTF, OTF). Each entry MUST include mimeType, category, and extensions; compressible and charset are optional.

MIME-07 — Runtime registry extension

registerMimeEntry(entry) MUST validate entry against MimeEntrySchema before insertion and MUST throw ParseError on schema violation. After registration, getMimeEntry(entry.mimeType) MUST return the registered entry, and getEntryByExtension(ext) MUST return the entry for every extension listed in entry.extensions. listMimeEntries() MUST return a sorted, deduplicated snapshot including all built-in and registered entries.

MIME-08 — Reverse lookups

toAllExtensions(mimeType) MUST return all extensions associated with the given MIME type string (empty array if unknown). toDefaultExtension(mimeType) MUST return the first extension from the matching MimeEntry.extensions array, or undefined. toCategory(mimeType) MUST return the MimeCategory for the MIME type, or undefined.

MIME-09 — BOM charset sniffing

sniffBom(bytes) MUST detect and return a BomDetection ({ charset, bomLength }) for the following BOM sequences, checked in order:

  1. 00 00 FE FF → UTF-32BE, bomLength 4
  2. FF FE 00 00 → UTF-32LE, bomLength 4
  3. FE FF → UTF-16BE, bomLength 2
  4. FF FE → UTF-16LE, bomLength 2
  5. EF BB BF → UTF-8, bomLength 3

It MUST return null when no BOM is found. It MUST NOT throw for any input.

MIME-10 — HTML meta charset sniffing

sniffHtmlMeta(bytes) MUST scan up to the first 1 024 bytes for <meta charset="…"> and <meta http-equiv="Content-Type" content="…; charset=…"> patterns using case-insensitive matching. It MUST return the charset string (lowercased) or null if not found. It MUST NOT throw for any input.

MIME-11 — XML declaration charset sniffing

sniffXmlDecl(bytes) MUST extract the encoding attribute from an XML declaration (<?xml … encoding="…" … ?>) if present within the first 128 bytes. It MUST return the encoding name (uppercased to match XML convention) or null. It MUST NOT throw for any input.

MIME-12 — Unified detect()

detect(input) MUST resolve to a DetectResult by applying the following priority order:

  1. If input.bytes is provided, attempt detectFromMagicBytes; on InsufficientBytesError fall through.
  2. If input.extension is provided, attempt detectFromExtension; on UnknownMimeTypeError fall through.
  3. If input.contentType is provided, attempt detectFromContentType.
  4. If all sources are exhausted, throw UnknownMimeTypeError.

detect MUST check input.signal before each source attempt and reject with a structured abort error if cancelled.

MIME-13 — detectFromStream() interim shim

detectFromStream(stream, opts?) MUST be exported with the same signature as the main branch. It MUST read the first available chunk from the provided ReadableStream or async iterable, respecting opts.signal. It MUST delegate to detectFromMagicBytes with the chunk bytes and wrap the result as StreamDetectionResult. It MUST document in its co-located .md that full streaming integration is deferred pending @suaveplan/streams.

MIME-14 — Signature registry extension

registerSignature(sig) MUST validate sig against SignatureSchema before insertion and MUST throw ParseError on invalid shape. After registration, the compiled trie MUST be rebuilt (lazily on next detection call) to include the new signature. registerSignature MUST be idempotent for identical sig objects.

MIME-15 — Structured errors

All thrown errors MUST extend MimeError which extends SuaveplanError. Error codes MUST be registered via registerMimeErrorCodes() which is called as a side-effect of importing src/index.ts. The following codes MUST be defined:

  • MIME.PARSE_ERROR — generic parse failure
  • MIME.INSUFFICIENT_BYTES — buffer too short for magic-byte detection
  • MIME.UNKNOWN_SIGNATURE — unrecognised or conflicting signature
  • MIME.UNKNOWN_TYPE — MIME type not found in registry
  • MIME.INVALID_CONTENT_TYPE — Content-Type header failed to parse

MIME-16 — Zod schemas

The following schemas MUST be exported from their respective *.schemas.ts files:

  • ParsedContentTypeSchema, MediaTypeSchema — from src/schemas/content-type.schemas.ts
  • MimeCategorySchema, MimeEntrySchema — from src/schemas/db.schemas.ts
  • SignatureSchema, DetectInputSchema, DetectResultSchema (discriminated union by source), MagicBytesResultSchema, ExtensionDetectionResultSchema, ContentTypeDetectionResultSchema, StreamDetectionResultSchema — from src/schemas/detection.schemas.ts

All schemas MUST be used for boundary validation in registerMimeEntry, registerSignature, and detect.

MIME-17 — OTel instrumentation

The following metrics MUST be emitted via @opentelemetry/api using meter name @suaveplan/mime:

  • Counter mime.detect.total with attribute source (magic / extension / content-type / stream)
  • Histogram mime.detect.duration_ms recorded on every detect() call
  • Counter mime.parse.total with attribute outcome (success / error) recorded on every parseContentType() call

All instruments MUST be no-ops when no OTel metric exporter is configured.

MIME-18 — Legacy shim

getMimeType(ext) MUST be exported and MUST delegate to getEntryByExtension(ext)?.mimeType ?? undefined. It MUST never throw. This export is retained solely for API surface completeness.

MIME-19 — Property-based tests

The following fast-check properties MUST be included in the test suite:

  1. Round-trip: for any synthetically valid Content-Type string, formatContentType(parseContentType(s)) parses back to a ParsedContentType that is structurally equal to the original parse result.
  2. Magic-byte exhaustiveness: detectFromMagicBytes(bytes) with any Uint8Array either returns a MagicBytesResult or throws exactly InsufficientBytesError — no other error class is acceptable.
  3. Registry round-trip: any MimeEntry satisfying MimeEntrySchema that is passed to registerMimeEntry is retrievable via getMimeEntry with an identical shape.

MIME-20 — Benchmarks

The following mitata benchmark suites MUST be included under src/__benchmarks__/:

  • parseContentType — 10 000 iterations over a representative set of realistic Content-Type strings
  • detect from a 4 KB Uint8Array — 10 000 iterations
  • getMimeEntry lookup — 100 000 iterations against the built-in database

Baseline numbers from the genesis run MUST be published in README.md.

MIME-21 — sideEffects: false + 100% coverage

"sideEffects": false MUST be set in package.json and enforced by a tree-shake guard test in tests/. Vitest statement/branch/function/line thresholds MUST all be set to 100. No .skip or .only is permitted in src/.