# Design — @suaveplan/caddy-imgproxy ## File layout ```text packages/media/image/caddy-imgproxy/ package.json # 0.1.0, exports per feature module vite.config.ts # multi-entry build mirroring exports vitest.config.ts # provider: istanbul, thresholds 100/100/100/100 tsconfig.lib.json README.md tests/setup/polyfills.ts src/ index.ts # explicit named re-exports parse/ parse-url.ts # parseImgproxyUrl parse-url.test.ts parse-url.md index.ts # explicit named barrel dimensions/ calculate-dimensions.ts # calculateDimensions calculate-dimensions.test.ts calculate-dimensions.md index.ts gravity/ gravity.ts # gravityToImgproxy + GRAVITY_MAP gravity.test.ts gravity.md index.ts operation/ operation.ts # operationToImgproxy operation.test.ts operation.md index.ts signing/ sign-path.ts # signImgproxyPath, HashLike injection sign-path.test.ts sign-path.md index.ts url-builder/ url-builder.ts # buildImgproxyUrl orchestrator url-builder.test.ts url-builder.md index.ts domain/ extract-domain.ts extract-domain.test.ts extract-domain.md index.ts handler/ handler.ts # ImgproxyHandler orchestrator handler.test.ts handler.md index.ts config/ config.ts # ImgproxyConfigSchema, parseConfig config.test.ts config.md index.ts error-codes/ error-codes.ts # CADDY_IMGPROXY_CODES + CaddyImgproxyError error-codes.test.ts error-codes.md index.ts telemetry/ telemetry.ts # counters + histogram telemetry.test.ts telemetry.md index.ts types/ types.ts # ParsedImgproxyUrl, ImgproxyConfig, RewriteResult index.ts ``` ## URL grammar (verbatim port of Go regex) ```text ^/ ([su]) # 1 signed | unsigned ([vcfsr])? # 2 op (v/c/f/s/r) opt (tl|tr|bl|br|[tblrc])? # 3 gravity opt (?: ([1-9]\d*):([1-9]\d*) # 4,5 aspect W:H | ([1-9]\d*)x(\d+) # 6,7 pixel WxH (W>0) | (0)x([1-9]\d*) # 8,9 pixel 0xH (auto-width) | ([1-9]\d*) # 10 square shorthand )? ((?:[wh][1-9]\d*|q(?:100|[1-9]\d?))*) # 11 modifiers /(.+)\.([a-z]+)$ # 12 path / 13 format ``` Modifier sub-regex: `([whq])(\d+)` — extract `w`, `h`, `q` in any order. ## Dimensions algorithm ```ts if (!hasDims) return { width: 0, height: 0 }; if (!isAspect) return { width: pixelW, height: pixelH }; // aspect path if (hintType === "h" && hintValue > 0) { height = hintValue; width = Math.trunc((height * aspectW) / aspectH); } else { width = hintValue > 0 ? hintValue : defaultWidth; height = Math.trunc((width * aspectH) / aspectW); } ``` (`Math.trunc` matches Go integer division.) ## Operation → imgproxy table | op | both W&H | only W | only H | none | |---|---|---|---|---| | cover | `g:/rs:fill:W:H` | `g:/w:W` | `g:/h:H` | `""` | | fit | `rs:fit:W:H` | `w:W` | `h:H` | `""` | | crop | `g:/c:W:H` | `""` | `""` | `""` | | stretch | `rs:force:W:H` | `""` | `""` | `""` | | resize | — | `w:W` | `h:H` | `""` | | `""` (passthrough) | `""` | `""` | `""` | `""` | ## Signing `HMAC-SHA256(keyBytes, saltBytes || pathBytes)` → base64url, no padding. Implementation uses `hmac` from `@suaveplan/crypto-hash` with `Uint8Array` inputs. Hex key/salt decoded via `fromHex`. ## Telemetry | Instrument | Kind | Tags | Where | |---|---|---|---| | `caddy_imgproxy_parse_total` | counter | `outcome` ∈ `ok|invalid|unknown_op|missing_dims` | `parseImgproxyUrl` | | `caddy_imgproxy_rewrite_total` | counter | `outcome` ∈ `rewritten|passthrough|invalid`, `operation` | `ImgproxyHandler.rewrite` | | `caddy_imgproxy_sign_total` | counter | `mode` ∈ `signed|unsafe` | `buildImgproxyUrl` | | `caddy_imgproxy_rewrite_duration_ms` | histogram | — | `ImgproxyHandler.rewrite` | ## Tier & deps Universal-tier. Deps: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/crypto-hash`, `@suaveplan/opentelemetry`, `@opentelemetry/api`, `tslib`, `zod`. DevDeps: `@suaveplan/testing`, `@vitest/coverage-istanbul`, `typescript`, `vite`, `vite-plugin-dts`, `vitest`, `@types/node`. ## Test plan Re-port every `imgproxy_test.go` case to `parse-url.test.ts` + `dimensions/...test.ts` + `operation/...test.ts` + `gravity/...test.ts` + `domain/...test.ts` + `url-builder/...test.ts`. Plus genesis-only tests: Zod boundary failures, error-code reachability, telemetry emission via a real `MeterProvider`, signing KAT against a fixed key/salt pair (mirrors Go `signPath` output).