Initial Commit
This commit is contained in:
142
openspec/archive/2026-05-25-enterprise-caddy-imgproxy/design.md
Normal file
142
openspec/archive/2026-05-25-enterprise-caddy-imgproxy/design.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 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<int>`, `h<int>`, `q<int>` 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:<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|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).
|
||||
Reference in New Issue
Block a user