# Enterprise migration: @suaveplan/caddy-imgproxy **Package**: `@suaveplan/caddy-imgproxy` **Status**: PROPOSED **Type**: new-package (migration from upstream) **Date**: 2026-05-25 **Design**: ./design.md **Spec delta**: ./specs/caddy-imgproxy/spec.md ## Why Upstream `../suaveplan/packages/server/caddy-imgproxy/` ships a Go Caddy module that rewrites compact URL grammars (`/u/.`) into signed imgproxy URLs. The TypeScript side of the package is a 4-line placeholder. Genesis needs a real, universal-tier TypeScript port so the same grammar can be consumed from any genesis runtime (CDN workers, edge gateways, build-time tooling, integration tests for Caddy stacks) without leaving the monorepo's enterprise quality bar. The migration re-implements the Go semantics exactly — same regex grammar, same gravity/operation mapping, same HMAC-SHA256 signing, same `s3:////@` layout — and adds the standard genesis layer: stable error codes via `defineCodes`, Zod-validated config, injectable `Clock` for deterministic spans, OTel counters + a duration histogram, and full 100% test coverage. The resulting package is the canonical reference for the URL grammar; Caddy hosts can either delegate to it via a sidecar or treat the Go module as a parallel implementation tested against the same fixtures. ## What changes New package `@suaveplan/caddy-imgproxy` at `packages/media/image/caddy-imgproxy/`. Public surface: - ADDED: `parseImgproxyUrl(path)` — parses the compact grammar (`/u`, `/s`, `v|c|f|s|r`, `tl|tr|bl|br|t|b|l|r|c`, aspect / pixel / square dims, `w|h|q` modifiers) into a typed `ParsedImgproxyUrl`. Mirrors the Go `parseURL` semantics byte-for-byte; raises `CaddyImgproxyError(INVALID_URL | UNKNOWN_OPERATION | MISSING_DIMENSIONS)`. - ADDED: `calculateDimensions(parsed, defaultWidth)` — resolves aspect-ratio + hint into concrete (width, height) pixels with the same precedence as Go (`h` hint takes height-first, `w` hint or default takes width-first). - ADDED: `gravityToImgproxy(code)` — translates `t|b|l|r|c|tl|tr|bl|br` to imgproxy's `no|so|we|ea|ce|nowe|noea|sowe|soea`. - ADDED: `operationToImgproxy(op, width, height, gravity)` — emits the imgproxy processing string (`g:/rs:fill:W:H` for cover, `rs:fit:W:H` for fit, `g:/c:W:H` for crop, `rs:force:W:H` for stretch, `w:W`/`h:H` for resize). - ADDED: `signImgproxyPath({ key, salt, path, hash? })` — HMAC-SHA256 over `salt || path` (raw bytes), base64url no-padding encoded. Uses `@suaveplan/crypto-hash`'s `hmac` + `toBase64Url`; injectable `hash` for offline / native swaps. - ADDED: `buildImgproxyUrl({ parsed, domain, config })` — composes the full imgproxy path including signature prefix (or `/unsafe` when no key/salt). Inserts `q:` segment when present. - ADDED: `extractDomain(host)` — strips port, drops the `images.` subdomain. - ADDED: `ImgproxyHandler` — orchestrator with `rewrite(request)`: returns `{ rewritten: true; targetPath } | { rewritten: false; reason }` for downstream proxy clients. Accepts injectable `Clock`, `LoggerLike`, `IdGenerator` for tracing; tagged spans + counters emitted on every call. - ADDED: `ImgproxyConfigSchema` (Zod) and `parseConfig(raw)` — validates `upstream`, `s3Bucket`, `defaultWidth >= 1`, hex-encoded `key`/`salt` (even-length, lower-hex). - ADDED: `CADDY_IMGPROXY_CODES` + `CaddyImgproxyError` via `defineCodes("CADDY_IMGPROXY", [...])` — codes `INVALID_URL`, `UNKNOWN_OPERATION`, `MISSING_DIMENSIONS`, `INVALID_OPTIONS`, `INVALID_KEY`, `INVALID_SALT`, `INVALID_HOST`. - ADDED: OTel instruments at meter `@suaveplan/caddy-imgproxy` — counters `caddy_imgproxy_parse_total{outcome}`, `caddy_imgproxy_rewrite_total{outcome,operation}`, `caddy_imgproxy_sign_total{mode}`; histogram `caddy_imgproxy_rewrite_duration_ms`. ## Parity Checklist | Capability | Upstream | Migrated | Notes | |---|---|---|---| | URL grammar parsing (`/u`, `/s`, ops, gravity, dims, modifiers) | ✅ (Go) | ✅ | TS port mirrors regex + state machine verbatim | | Aspect-ratio → pixel calculation with `w`/`h` hint | ✅ | ✅ | | | Square-shorthand dims (e.g. `/uv128/avatar.png`) | ✅ | ✅ | | | Quality modifier `q1..q100` | ✅ | ✅ | | | Gravity codes (9 directions) | ✅ | ✅ | | | Operation → imgproxy processing string | ✅ | ✅ | | | HMAC-SHA256 URL signing, base64url no-pad | ✅ | ✅ | Via `@suaveplan/crypto-hash` | | `/unsafe` fallback when no key/salt | ✅ | ✅ | | | S3 path `s3://bucket/domain/path@format` | ✅ | ✅ | | | Domain extraction (`images.example.com` → `example.com`) | ✅ | ✅ | | | Caddyfile directive parsing | ✅ (Go-only) | 🚫 | Caddyfile lives in the Go module; TS package targets the URL-grammar contract. Justified: the Caddyfile is a Caddy-runtime artefact with no TypeScript equivalent; consumers wanting Caddy hosting use the Go module, the TS package serves edge/CDN/test consumers. | | HTTP reverse proxy execution | ✅ (Go) | 🚫 | Same justification: handler runs inside Caddy. TS package returns a rewrite decision; the caller owns the network round trip. | | Structured logging (zap) | ✅ | ⬆️ | Replaced with injectable `LoggerLike` from `@suaveplan/types` | | Defaults (`localhost:8080`, `images`, 800) | ✅ | ✅ | Encoded in `ImgproxyConfigSchema` | | Stable error codes via `defineCodes` | 🚫 | ⬆️ | Genesis-only: typed `CaddyImgproxyError` | | Zod-validated public config | 🚫 | ⬆️ | Genesis-only | | OTel counters + histogram | 🚫 | ⬆️ | Genesis-only | | 100% test coverage | ✅ (Go) | ✅ | TS port re-implements every Go test case | ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/caddy-imgproxy` to enumerate. No existing dependents — fresh package landing. - No dependents (verified by the package being at `–` in `STATUS.md` and absent from any `workspace:^` consumer). ## Out of scope - The Go Caddy module itself is untouched. The Go source remains in `../suaveplan/packages/server/caddy-imgproxy/`. A future change MAY align the two via a shared test-vector corpus, but that work is not in this proposal. - HTTP reverse-proxy delivery (TS package returns rewrite decisions only). - Caddyfile directive parsing (Caddy runtime concern).