68 lines
6.2 KiB
Markdown
68 lines
6.2 KiB
Markdown
# 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|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:<g>/rs:fill:W:H` for cover, `rs:fit:W:H` for fit, `g:<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:<quality>` 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).
|