6.2 KiB
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<op><gravity><dims><modifiers>/<path>.<format>) 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://<bucket>/<domain>/<path>@<format> 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|qmodifiers) into a typedParsedImgproxyUrl. Mirrors the GoparseURLsemantics byte-for-byte; raisesCaddyImgproxyError(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 (hhint takes height-first,whint or default takes width-first). - ADDED:
gravityToImgproxy(code)— translatest|b|l|r|c|tl|tr|bl|brto imgproxy'sno|so|we|ea|ce|nowe|noea|sowe|soea. - ADDED:
operationToImgproxy(op, width, height, gravity)— emits the imgproxy processing string (g:<g>/rs:fill:W:Hfor cover,rs:fit:W:Hfor fit,g:<g>/c:W:Hfor crop,rs:force:W:Hfor stretch,w:W/h:Hfor resize). - ADDED:
signImgproxyPath({ key, salt, path, hash? })— HMAC-SHA256 oversalt || path(raw bytes), base64url no-padding encoded. Uses@suaveplan/crypto-hash'shmac+toBase64Url; injectablehashfor offline / native swaps. - ADDED:
buildImgproxyUrl({ parsed, domain, config })— composes the full imgproxy path including signature prefix (or/unsafewhen no key/salt). Insertsq:<quality>segment when present. - ADDED:
extractDomain(host)— strips port, drops theimages.subdomain. - ADDED:
ImgproxyHandler— orchestrator withrewrite(request): returns{ rewritten: true; targetPath } | { rewritten: false; reason }for downstream proxy clients. Accepts injectableClock,LoggerLike,IdGeneratorfor tracing; tagged spans + counters emitted on every call. - ADDED:
ImgproxyConfigSchema(Zod) andparseConfig(raw)— validatesupstream,s3Bucket,defaultWidth >= 1, hex-encodedkey/salt(even-length, lower-hex). - ADDED:
CADDY_IMGPROXY_CODES+CaddyImgproxyErrorviadefineCodes("CADDY_IMGPROXY", [...])— codesINVALID_URL,UNKNOWN_OPERATION,MISSING_DIMENSIONS,INVALID_OPTIONS,INVALID_KEY,INVALID_SALT,INVALID_HOST. - ADDED: OTel instruments at meter
@suaveplan/caddy-imgproxy— counterscaddy_imgproxy_parse_total{outcome},caddy_imgproxy_rewrite_total{outcome,operation},caddy_imgproxy_sign_total{mode}; histogramcaddy_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
–inSTATUS.mdand absent from anyworkspace:^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).