Initial Commit
This commit is contained in:
159
openspec/archive/2026-04-19-enterprise-mime/proposal.md
Normal file
159
openspec/archive/2026-04-19-enterprise-mime/proposal.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# OpenSpec Change: enterprise-mime
|
||||
|
||||
- **Change ID**: enterprise-mime
|
||||
- **Package**: `@suaveplan/mime`
|
||||
- **Category**: universal
|
||||
- **Target path**: `packages/universal/mime`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-19
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/mime.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Greenfield implementation of `@suaveplan/mime` for the genesis branch. This package delivers structured MIME-type detection, parsing, and registry at enterprise quality: magic-byte sniffing via a compiled trie over a signature table, RFC 7231 `Content-Type` parsing (quoted-string parameters, charset normalisation, round-trip serialisation), a curated IANA-sourced type database with extension↔type↔category lookups, charset sniffing (BOM, HTML meta, XML declaration), an extensible signature registry, structured errors, Zod schemas, OTel instrumentation, property-based tests, and benchmarks. Zero browser/Node/React dependencies — pure TypeScript runnable in Bun, Node, and browser.
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Available |
|
||||
|---------|-----------|
|
||||
| `@suaveplan/error` | ✅ |
|
||||
| `@suaveplan/types` | ✅ |
|
||||
| `@suaveplan/logger` | ✅ |
|
||||
| `@suaveplan/opentelemetry` | ❌ not yet built — use `@opentelemetry/api` directly |
|
||||
| `@suaveplan/schema-registry` | ❌ not yet built — export Zod schemas directly |
|
||||
| `@suaveplan/streams` | ❌ not yet built — `detectFromStream` is a thin wrapper over `detectFromMagicBytes` with `AbortSignal` |
|
||||
|
||||
External deps (via catalog):
|
||||
|
||||
- `zod` → `catalog:forms`
|
||||
- `@opentelemetry/api` → `catalog:observability`
|
||||
- `tslib` → `catalog:build`
|
||||
|
||||
Dev deps (via catalog):
|
||||
|
||||
- `vitest`, `@vitest/coverage-istanbul` → `catalog:testing`
|
||||
- `fast-check` → `catalog:testing`
|
||||
- `mitata` → `catalog:testing`
|
||||
|
||||
## Preserved public API surface (from main branch)
|
||||
|
||||
### Charset
|
||||
|
||||
- `type BomDetection` — detected charset from a BOM prefix
|
||||
- `sniffBom(bytes)` → `BomDetection | null` — detect charset from BOM
|
||||
- `sniffHtmlMeta(bytes)` → `string | null` — detect charset from HTML `<meta>` tag
|
||||
- `sniffXmlDecl(bytes)` → `string | null` — detect charset from XML declaration
|
||||
|
||||
### Content-type
|
||||
|
||||
- `contentTypeEquals(a, b)` → `boolean` — structural equality ignoring parameter order
|
||||
- `contentTypeMatches(actual, pattern)` → `boolean` — wildcard `*` subtype matching
|
||||
- `formatContentType(parsed)` → `string` — canonical round-trip serialisation
|
||||
- `type MediaType` — `{ type: string; subtype: string }`
|
||||
- `type ParsedContentType` — `{ mediaType: MediaType; parameters: Record<string, string> }`
|
||||
- `parseContentType(value)` → `ParsedContentType` — RFC 7231 parser with quoted-string support
|
||||
|
||||
### Database
|
||||
|
||||
- `BUILTIN_DB` — the built-in curated IANA-sourced registry (read-only reference)
|
||||
- `getEntryByExtension(ext)` → `MimeEntry | undefined`
|
||||
- `getMimeEntry(mimeType)` → `MimeEntry | undefined`
|
||||
- `listMimeEntries()` → `MimeEntry[]`
|
||||
- `type MimeCategory` — `'image' | 'audio' | 'video' | 'text' | 'application' | 'font' | 'model' | 'message' | 'multipart' | 'chemical'`
|
||||
- `type MimeEntry` — `{ mimeType: string; category: MimeCategory; extensions: string[]; compressible?: boolean; charset?: string }`
|
||||
- `registerMimeEntry(entry)` → `void` — add or override an entry at runtime
|
||||
|
||||
### Detection
|
||||
|
||||
- `type ContentTypeDetectionResult` — detection result sourced from a Content-Type header
|
||||
- `type DetectInput` — `{ bytes?: Uint8Array; extension?: string; contentType?: string; signal?: AbortSignal }`
|
||||
- `type DetectResult` — union of `MagicBytesResult | ExtensionDetectionResult | ContentTypeDetectionResult | StreamDetectionResult`
|
||||
- `type DetectSource` — `'magic' | 'extension' | 'content-type' | 'stream'`
|
||||
- `detect(input)` → `Promise<DetectResult>` — unified detection: tries magic bytes first, then extension, then Content-Type header
|
||||
- `detectAllFromMagicBytes(bytes)` → `MagicBytesResult[]` — all matching signatures for a buffer
|
||||
- `detectFromContentType(contentType)` → `ContentTypeDetectionResult` — detection from parsed header alone
|
||||
- `detectFromExtension(ext)` → `ExtensionDetectionResult` — detection via extension lookup
|
||||
- `detectFromMagicBytes(bytes)` → `MagicBytesResult` — best-match detection from buffer
|
||||
- `detectFromStream(stream, opts?)` → `Promise<StreamDetectionResult>` — thin AbortSignal wrapper over `detectFromMagicBytes`; defers full streaming integration until `@suaveplan/streams` ships
|
||||
- `type ExtensionDetectionResult` — detection result sourced from a file extension
|
||||
- `type MagicBytesResult` — detection result sourced from magic bytes
|
||||
- `registerSignature(sig)` → `void` — add a custom signature to the trie at runtime
|
||||
- `type Signature` — `{ mimeType: string; bytes: Uint8Array; offset?: number; mask?: Uint8Array }`
|
||||
- `type StreamDetectionResult` — detection result sourced from a readable stream
|
||||
|
||||
### Errors
|
||||
|
||||
- `MIME_ERROR_METADATA` — error code metadata map for `@suaveplan/error` `defineCodes`
|
||||
- `type MimeErrorCode` — union of all `MIME.*` code strings
|
||||
- `registerMimeErrorCodes()` — registers all MIME error codes with the process-wide registry
|
||||
- `InsufficientBytesError` extends `MimeError` — thrown when the buffer is too short for magic-byte detection
|
||||
- `type InsufficientBytesErrorCode`
|
||||
- `type InsufficientBytesErrorContext` — `{ needed: number; got: number }`
|
||||
- `type InsufficientBytesErrorOptions`
|
||||
- `InvalidContentTypeError` extends `MimeError` — thrown when a Content-Type string fails to parse
|
||||
- `type InvalidContentTypeErrorCode`
|
||||
- `type InvalidContentTypeErrorContext` — `{ raw: string }`
|
||||
- `type InvalidContentTypeErrorOptions`
|
||||
- `UnknownMimeTypeError` extends `MimeError` — thrown when a MIME type string has no registry entry
|
||||
- `type UnknownMimeTypeErrorCode`
|
||||
- `type UnknownMimeTypeErrorContext` — `{ mimeType: string }`
|
||||
- `type UnknownMimeTypeErrorOptions`
|
||||
|
||||
### Reverse lookups (main branch additions to preserve)
|
||||
|
||||
- `toAllExtensions(mimeType)` → `string[]` — all extensions mapped to a MIME type
|
||||
- `toCategory(mimeType)` → `MimeCategory | undefined` — category lookup by MIME type string
|
||||
- `toDefaultExtension(mimeType)` → `string | undefined` — canonical primary extension
|
||||
|
||||
### Legacy
|
||||
|
||||
- `getMimeType(ext)` → `string | undefined` — simple extension-to-mime-type lookup (legacy shim; retained for API surface completeness)
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### Structured errors
|
||||
|
||||
- `MimeError` base class extends `SuaveplanError` with codes under the `MIME.*` namespace
|
||||
- Error codes: `MIME.PARSE_ERROR`, `MIME.INSUFFICIENT_BYTES`, `MIME.UNKNOWN_SIGNATURE`, `MIME.UNKNOWN_TYPE`, `MIME.INVALID_CONTENT_TYPE`
|
||||
- `InsufficientBytesError`, `InvalidContentTypeError`, `UnknownMimeTypeError`, `ParseError`, `UnknownSignatureError` all extend `MimeError`
|
||||
- Error codes registered via side-effect `import "./error-codes.js"` in `src/index.ts`
|
||||
|
||||
### Zod schemas
|
||||
|
||||
- `ParsedContentTypeSchema` — validates `ParsedContentType` at the boundary
|
||||
- `MimeEntrySchema` — validates `MimeEntry` and `MimeCategory`
|
||||
- `SignatureSchema` — validates a `Signature` before insertion into the trie
|
||||
- `DetectResultSchema` — discriminated union validator for `DetectResult`
|
||||
- `DetectInputSchema` — validates `DetectInput` options
|
||||
|
||||
### OTel instrumentation (via `@opentelemetry/api`, no-op without exporter)
|
||||
|
||||
- Counter `mime.detect.total` — by `source` label (`magic` / `extension` / `content-type` / `stream`)
|
||||
- Histogram `mime.detect.duration_ms` — per `detect()` call
|
||||
- Counter `mime.parse.total` — by `outcome` label (`success` / `error`)
|
||||
|
||||
### detectFromStream — deferred streaming, interim shim
|
||||
|
||||
`detectFromStream` is exported and typed as in the main branch but is implemented as a thin wrapper that reads the first chunk from the provided async iterable (or `ReadableStream`) with an `AbortSignal` and delegates to `detectFromMagicBytes`. Full streaming integration is deferred until `@suaveplan/streams` ships.
|
||||
|
||||
### Property-based tests (fast-check)
|
||||
|
||||
- `parseContentType → formatContentType` round-trip: for any valid Content-Type string, `formatContentType(parseContentType(s))` parses back to the same `ParsedContentType`
|
||||
- Magic-byte detection: `detectFromMagicBytes(bytes)` always returns a result or throws `InsufficientBytesError` — never throws any other error class
|
||||
- Registry round-trip: any entry registered via `registerMimeEntry(e)` is immediately retrievable via `getMimeEntry(e.mimeType)` with identical shape
|
||||
|
||||
### Benchmarks (mitata)
|
||||
|
||||
- `parseContentType` — 10 000 iterations over a representative set of realistic Content-Type strings
|
||||
- `detect` from `Uint8Array` — 10 000 iterations with a 4 KB buffer covering a variety of magic-byte signatures
|
||||
- `getMimeEntry` lookup — 100 000 iterations over the built-in database
|
||||
|
||||
## Impact
|
||||
|
||||
- Adds one new package: `packages/universal/mime`
|
||||
- Adds one new spec capability: `mime`
|
||||
- No existing package consumers on genesis yet
|
||||
146
openspec/archive/2026-04-19-enterprise-mime/specs/mime/spec.md
Normal file
146
openspec/archive/2026-04-19-enterprise-mime/specs/mime/spec.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# 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/`.
|
||||
114
openspec/archive/2026-04-19-enterprise-mime/tasks.md
Normal file
114
openspec/archive/2026-04-19-enterprise-mime/tasks.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Tasks: enterprise-mime
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/universal/mime/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
|
||||
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
|
||||
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
|
||||
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
|
||||
|
||||
## 2. Error types
|
||||
|
||||
- [x] 2.1 Implement `src/errors/mime-error.ts` — `MimeError` base extends `SuaveplanError`
|
||||
- [x] 2.2 Implement error code constants in `src/error-codes.ts` — `MIME_ERROR_METADATA`, `MimeErrorCode`, `registerMimeErrorCodes()` using `defineCodes` for `MIME.PARSE_ERROR`, `MIME.INSUFFICIENT_BYTES`, `MIME.UNKNOWN_SIGNATURE`, `MIME.UNKNOWN_TYPE`, `MIME.INVALID_CONTENT_TYPE`
|
||||
- [x] 2.3 Implement `src/errors/insufficient-bytes-error.ts` — `InsufficientBytesError` extends `MimeError`; `InsufficientBytesErrorCode`, `InsufficientBytesErrorContext` (`{ needed: number; got: number }`), `InsufficientBytesErrorOptions`
|
||||
- [x] 2.4 Implement `src/errors/invalid-content-type-error.ts` — `InvalidContentTypeError` extends `MimeError`; context `{ raw: string }`
|
||||
- [x] 2.5 Implement `src/errors/unknown-mime-type-error.ts` — `UnknownMimeTypeError` extends `MimeError`; context `{ mimeType: string }`
|
||||
- [x] 2.6 Implement `src/errors/parse-error.ts` — `ParseError` extends `MimeError` (code `MIME.PARSE_ERROR`)
|
||||
- [x] 2.7 Implement `src/errors/unknown-signature-error.ts` — `UnknownSignatureError` extends `MimeError` (code `MIME.UNKNOWN_SIGNATURE`)
|
||||
- [x] 2.8 Tests: construction, code narrowing, context field shapes, `instanceof MimeError`
|
||||
- [x] 2.9 Co-located `.md` for each error file
|
||||
|
||||
## 3. Zod schemas
|
||||
|
||||
- [x] 3.1 Implement `src/schemas/content-type.schemas.ts` — `ParsedContentTypeSchema`, `MediaTypeSchema`
|
||||
- [x] 3.2 Implement `src/schemas/db.schemas.ts` — `MimeCategorySchema`, `MimeEntrySchema`
|
||||
- [x] 3.3 Implement `src/schemas/detection.schemas.ts` — `SignatureSchema`, `DetectInputSchema`, `MagicBytesResultSchema`, `ExtensionDetectionResultSchema`, `ContentTypeDetectionResultSchema`, `StreamDetectionResultSchema`, `DetectResultSchema` (discriminated union)
|
||||
- [x] 3.4 Tests: valid and invalid inputs for every schema; discriminated union branches
|
||||
- [x] 3.5 Co-located `.md` for each schemas file
|
||||
|
||||
## 4. Content-type parser
|
||||
|
||||
- [x] 4.1 Implement `src/content-type/parse-content-type.ts` — `parseContentType(value)` → `ParsedContentType`; RFC 7231 compliant; handles quoted-string parameters, case-insensitive type/subtype, OWS trimming; throws `InvalidContentTypeError` on parse failure
|
||||
- [x] 4.2 Implement `src/content-type/format-content-type.ts` — `formatContentType(parsed)` → `string`; canonical round-trip serialisation; quotes parameter values that require it
|
||||
- [x] 4.3 Implement `src/content-type/content-type-equals.ts` — `contentTypeEquals(a, b)` → `boolean`; structural equality ignoring parameter order and case
|
||||
- [x] 4.4 Implement `src/content-type/content-type-matches.ts` — `contentTypeMatches(actual, pattern)` → `boolean`; wildcard `*` subtype matching
|
||||
- [x] 4.5 Define `src/content-type/content-type.types.ts` — `MediaType`, `ParsedContentType`
|
||||
- [x] 4.6 Unit tests: RFC 7231 edge cases (quoted strings, OWS, empty params, case), equality, matching, round-trip
|
||||
- [x] 4.7 Property-based tests: `formatContentType(parseContentType(s))` round-trip invariant
|
||||
- [x] 4.8 Co-located `.md` for each content-type file
|
||||
|
||||
## 5. MIME database
|
||||
|
||||
- [x] 5.1 Implement `src/db/mime-entry.types.ts` — `MimeCategory`, `MimeEntry`
|
||||
- [x] 5.2 Implement `src/db/builtin-db.ts` — `BUILTIN_DB`: curated, IANA-sourced, comprehensive set of `MimeEntry` records covering all major categories
|
||||
- [x] 5.3 Implement `src/db/registry.ts` — mutable runtime registry layered over `BUILTIN_DB`; `getMimeEntry(mimeType)`, `getEntryByExtension(ext)`, `listMimeEntries()`, `registerMimeEntry(entry)` — throws `UnknownMimeTypeError` on missing lookup when strict mode requested
|
||||
- [x] 5.4 Implement `src/reverse/reverse.ts` — `toAllExtensions(mimeType)`, `toCategory(mimeType)`, `toDefaultExtension(mimeType)`
|
||||
- [x] 5.5 Implement `src/legacy/get-mime-type.ts` — `getMimeType(ext)` legacy shim delegating to `getEntryByExtension`
|
||||
- [x] 5.6 Unit tests: lookups hit BUILTIN_DB; `registerMimeEntry` overrides; `listMimeEntries` returns sorted copy; reverse lookups; legacy shim
|
||||
- [x] 5.7 Property-based tests: `getMimeEntry(registerMimeEntry(e).mimeType)` round-trip
|
||||
- [x] 5.8 Co-located `.md` for each db file
|
||||
|
||||
## 6. Magic-byte detection
|
||||
|
||||
- [x] 6.1 Define `src/detection/signature.types.ts` — `Signature` (`{ mimeType: string; bytes: Uint8Array; offset?: number; mask?: Uint8Array }`); `MagicBytesResult`
|
||||
- [x] 6.2 Implement `src/detection/trie.ts` — compiled byte trie built from the signature table; supports offset-aware matching and bitmask application; exported as `buildTrie`, `matchTrie`
|
||||
- [x] 6.3 Implement `src/detection/builtin-signatures.ts` — built-in signature table covering common formats: PNG, JPEG, GIF, WebP, AVIF, PDF, ZIP, GZIP, BZIP2, XZ, WASM, ELF, MP4/ISO-BMFF, OGG, FLAC, MP3, WAV, DOCX/XLSX/PPTX (ZIP-based), 7z, RAR, TAR, SQLite, ICO, BMP, TIFF, PSD, SWF, RTF, XML, HTML, and others
|
||||
- [x] 6.4 Implement `src/detection/signature-registry.ts` — `registerSignature(sig)`, `getSignatures()`, `resetSignatures()` (test-only); validates via `SignatureSchema` before insertion; throws `UnknownSignatureError` on duplicate with conflict
|
||||
- [x] 6.5 Implement `src/detection/detect-from-magic-bytes.ts` — `detectFromMagicBytes(bytes)` → `MagicBytesResult`; uses compiled trie; throws `InsufficientBytesError` if buffer is shorter than the shortest registered signature
|
||||
- [x] 6.6 Implement `src/detection/detect-all-from-magic-bytes.ts` — `detectAllFromMagicBytes(bytes)` → `MagicBytesResult[]`; returns all matching signatures, sorted by specificity (longest match first)
|
||||
- [x] 6.7 Unit tests: known-format buffers match expected MIME types; insufficient buffer throws `InsufficientBytesError`; masked signatures; offset signatures
|
||||
- [x] 6.8 Property-based tests: `detectFromMagicBytes(bytes)` is always a result or `InsufficientBytesError` — never another error class
|
||||
- [x] 6.9 Co-located `.md` for each detection file in this step
|
||||
|
||||
## 7. Charset detection
|
||||
|
||||
- [x] 7.1 Implement `src/charset/sniff-bom.ts` — `sniffBom(bytes)` → `BomDetection | null`; recognises UTF-8 BOM (EF BB BF), UTF-16 BE (FE FF), UTF-16 LE (FF FE), UTF-32 BE/LE (00 00 FE FF / FF FE 00 00)
|
||||
- [x] 7.2 Implement `src/charset/sniff-html-meta.ts` — `sniffHtmlMeta(bytes)` → `string | null`; scans up to 1 024 bytes for `<meta charset="…">` and `<meta http-equiv="Content-Type" content="…; charset=…">` patterns (case-insensitive)
|
||||
- [x] 7.3 Implement `src/charset/sniff-xml-decl.ts` — `sniffXmlDecl(bytes)` → `string | null`; extracts `encoding="…"` from the XML declaration if present within the first 128 bytes
|
||||
- [x] 7.4 Define `src/charset/charset.types.ts` — `BomDetection` (`{ charset: string; bomLength: number }`)
|
||||
- [x] 7.5 Unit tests: known BOM sequences; HTML meta with various attribute orderings; XML declaration with and without encoding; non-matching inputs return null
|
||||
- [x] 7.6 Co-located `.md` for each charset file
|
||||
|
||||
## 8. Extension + content-type detection
|
||||
|
||||
- [x] 8.1 Implement `src/detection/detect-from-extension.ts` — `detectFromExtension(ext)` → `ExtensionDetectionResult`; delegates to `getEntryByExtension`; normalises leading dot; throws `UnknownMimeTypeError` on unknown extension
|
||||
- [x] 8.2 Implement `src/detection/detect-from-content-type.ts` — `detectFromContentType(contentType)` → `ContentTypeDetectionResult`; calls `parseContentType` then `getMimeEntry`; throws `InvalidContentTypeError` or `UnknownMimeTypeError`
|
||||
- [x] 8.3 Define `src/detection/detection.types.ts` — `DetectSource`, `DetectInput`, `DetectResult`, `ExtensionDetectionResult`, `ContentTypeDetectionResult`, `StreamDetectionResult`
|
||||
- [x] 8.4 Unit tests: known extensions map to correct MIME types; unknown extension throws; invalid Content-Type throws; round-trip through `formatContentType`
|
||||
- [x] 8.5 Co-located `.md` for each file
|
||||
|
||||
## 9. Unified detect() and detectFromStream()
|
||||
|
||||
- [x] 9.1 Implement `src/detection/detect.ts` — `detect(input)` → `Promise<DetectResult>`; priority order: magic bytes → extension → content-type header; respects `input.signal`; validates input via `DetectInputSchema`
|
||||
- [x] 9.2 Implement `src/detection/detect-from-stream.ts` — `detectFromStream(stream, opts?)` → `Promise<StreamDetectionResult>`; reads the first chunk from an async iterable or `ReadableStream`, then delegates to `detectFromMagicBytes`; honours `AbortSignal`; documents deferred-streaming intent
|
||||
- [x] 9.3 Unit tests: all three detection paths exercised; signal cancellation; empty input handled gracefully
|
||||
- [x] 9.4 Co-located `.md` for each file
|
||||
|
||||
## 10. OTel instrumentation
|
||||
|
||||
- [x] 10.1 Implement `src/telemetry/meters.ts` — create meter `@suaveplan/mime`; define counter `mime.detect.total` (attribute: `source`), histogram `mime.detect.duration_ms`, counter `mime.parse.total` (attribute: `outcome`)
|
||||
- [x] 10.2 Wire instrumentation into `detect`, `detectFromMagicBytes`, `detectFromExtension`, `detectFromContentType`, `detectFromStream`, `parseContentType`
|
||||
- [x] 10.3 Tests: confirm no-op behaviour when no OTel exporter is registered (no throw, no side-effects visible)
|
||||
- [x] 10.4 Co-located `src/telemetry/meters.md`
|
||||
|
||||
## 11. Property tests + benchmarks
|
||||
|
||||
- [x] 11.1 `src/__benchmarks__/parse-content-type.bench.ts` — `parseContentType` 10 000 iterations (mitata)
|
||||
- [x] 11.2 `src/__benchmarks__/detect.bench.ts` — `detect` from 4 KB `Uint8Array` 10 000 iterations
|
||||
- [x] 11.3 `src/__benchmarks__/get-mime-entry.bench.ts` — `getMimeEntry` lookup 100 000 iterations
|
||||
- [x] 11.4 Publish baseline numbers in `README.md`
|
||||
|
||||
## 12. Docs
|
||||
|
||||
- [x] 12.1 Co-located `.md` for every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes)
|
||||
- [x] 12.2 Rewrite `README.md` with full public API table, detection priority diagram, database category table, charset detection guide, consumer quick-start, and benchmark results
|
||||
- [x] 12.3 `bun run docs:check --filter=@suaveplan/mime` ≥ 90%
|
||||
|
||||
## 13. Verify and publish
|
||||
|
||||
- [x] 13.1 `bunx turbo lint typecheck test --filter=@suaveplan/mime` green
|
||||
- [x] 13.2 `bunx turbo test:coverage --filter=@suaveplan/mime` at 100/100/100/100
|
||||
- [x] 13.3 `bunx turbo build --filter=@suaveplan/mime` green
|
||||
- [x] 13.4 `bun scripts/publish.ts --package @suaveplan/mime` to Verdaccio at 0.0.0
|
||||
- [x] 13.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-mime/`
|
||||
Reference in New Issue
Block a user