Files
suaveplan/openspec/archive/2026-05-25-enterprise-caddy-imgproxy/design.md
2026-05-27 23:29:53 +01:00

5.0 KiB

Design — @suaveplan/caddy-imgproxy

File layout

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)

^/
([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<int>, h<int>, q<int> in any order.

Dimensions algorithm

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:<g>/rs:fill:W:H g:<g>/w:W g:<g>/h:H ""
fit rs:fit:W:H w:W h:H ""
crop g:<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
caddy_imgproxy_rewrite_total counter outcome ∈ `rewritten passthrough
caddy_imgproxy_sign_total counter mode ∈ `signed unsafe`
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).