Per PR #37 discussion, internal/imgcache/ currently bundles many distinct concerns into a single package. This issue tracks splitting it into focused, independently testable packages.
Already Done
imageprocessor → internal/imageprocessor/ — image format conversion and resizing (extracted in PR #37)
allowlist → internal/allowlist/ — host allow list (extracted in PR #41, renamed from whitelist per sneak's feedback)
Uses ImageRequest, Size, ImageFormat, FitMode — would need to import shared types, or define standalone result types the caller maps onto the imgcache vocabulary
Move urlparser_test.go with it; keep coverage of the path-traversal rejection cases intact
This is the last extraction in scope for this issue. Once it lands, this issue closes.
Tightly Coupled (keep together, out of scope)
These share significant state and data flow with the service orchestrator. Splitting them further would require significant interface surgery and may not be worth it:
cache (cache.go) — cache lookup, variant storage, negative cache, stats. Tightly coupled with service.go and storage.go.
storage (storage.go) — content-addressed ContentStorage and MetadataStorage. Created and managed by Cache.
service (service.go) — the core orchestrator that wires cache, fetcher, and processor together.
types/interfaces (imgcache.go) — ImageRequest, ImageResponse, ImageFormat, FitMode, Size, etc. These are the shared vocabulary used across all subpackages.
Definition of Done
internal/urlparser/ exists and contains the URL/path parsing logic and its tests; internal/imgcache/urlparser.go and urlparser_test.go are gone.
No import cycle: urlparser does not import imgcache.
All call sites updated; no behavior change to parsing or to path-traversal rejection.
Constructor naming per sneak's feedback on PR #41: no stuttering (New(), not NewURLParser()).
make check green and docker build . passes.
Approach
Each extraction should:
Create the new package under internal/
Move types and functions, defining standalone types where needed to avoid circular imports
One PR per package extraction to keep reviews manageable
Per [PR #37 discussion](https://git.eeqj.de/sneak/pixa/pulls/37#issuecomment-1173), `internal/imgcache/` currently bundles many distinct concerns into a single package. This issue tracks splitting it into focused, independently testable packages.
## Already Done
- [x] **imageprocessor** → `internal/imageprocessor/` — image format conversion and resizing (extracted in [PR #37](https://git.eeqj.de/sneak/pixa/pulls/37))
- [x] **allowlist** → `internal/allowlist/` — host allow list (extracted in [PR #41](https://git.eeqj.de/sneak/pixa/pulls/41), renamed from whitelist per sneak's feedback)
- [x] **magic** → `internal/magic/` — magic byte detection (extracted in [PR #42](https://git.eeqj.de/sneak/pixa/pulls/42))
- [x] **fetcher** → `internal/httpfetcher/` — SSRF-safe fetcher (extracted in [PR #43](https://git.eeqj.de/sneak/pixa/pulls/43))
- [x] **signature** → `internal/signature/` — HMAC signing and verification (extracted in [PR #46](https://git.eeqj.de/sneak/pixa/pulls/46))
## Remaining
- [ ] **urlparser** (`urlparser.go`) → `internal/urlparser/`
- `ParsedURL`, `ParseImagePath`, `ParseImageURL`, path traversal validation, size/format parsing
- Uses `ImageRequest`, `Size`, `ImageFormat`, `FitMode` — would need to import shared types, or define standalone result types the caller maps onto the imgcache vocabulary
- Move `urlparser_test.go` with it; keep coverage of the path-traversal rejection cases intact
This is the last extraction in scope for this issue. Once it lands, this issue closes.
## Tightly Coupled (keep together, out of scope)
These share significant state and data flow with the service orchestrator. Splitting them further would require significant interface surgery and may not be worth it:
- **cache** (`cache.go`) — cache lookup, variant storage, negative cache, stats. Tightly coupled with `service.go` and `storage.go`.
- **storage** (`storage.go`) — content-addressed `ContentStorage` and `MetadataStorage`. Created and managed by `Cache`.
- **service** (`service.go`) — the core orchestrator that wires cache, fetcher, and processor together.
- **types/interfaces** (`imgcache.go`) — `ImageRequest`, `ImageResponse`, `ImageFormat`, `FitMode`, `Size`, etc. These are the shared vocabulary used across all subpackages.
## Definition of Done
- `internal/urlparser/` exists and contains the URL/path parsing logic and its tests; `internal/imgcache/urlparser.go` and `urlparser_test.go` are gone.
- No import cycle: `urlparser` does not import `imgcache`.
- All call sites updated; no behavior change to parsing or to path-traversal rejection.
- Constructor naming per sneak's feedback on PR #41: no stuttering (`New()`, not `NewURLParser()`).
- `make check` green and `docker build .` passes.
## Approach
Each extraction should:
1. Create the new package under `internal/`
2. Move types and functions, defining standalone types where needed to avoid circular imports
3. Update all import sites
4. Ensure `docker build .` passes (fmt, lint, test, build)
5. One PR per package extraction to keep reviews manageable
Opened PR #46 (refactor/extract-signature) extracting signature -> internal/signature/, based on current main. It is independent of the httpfetcher move, so no dependency on other in-flight branches.
Moved Signer (New, Sign, Verify, GenerateSignedURL), ParseParams, and the error sentinels. To keep the import edge one-way, the package defines a standalone signature.Request; imgcache projects its ImageRequest onto it via a small helper (same approach magic used for ImageFormat).
No-stutter renames: NewSigner -> New, ParseSignatureParams -> ParseParams, ErrSignature{Required,Invalid,Expired} -> Err{Required,Invalid,Expired}. HMAC input bytes are unchanged, so existing signatures still verify.
urlparser (urlparser.go) -> internal/urlparser/. Note it is consumed outside imgcache (by internal/handlers/image.go via ParseImagePath/ParsedURL) and couples to ImageRequest, Size, ImageFormat, FitMode, so it needs standalone shared types on the extracted side. This is the natural next PR once #46 lands.
The "tightly coupled" group (cache, storage, service, and the shared types in imgcache.go) stays together as planned.
Progress update on the `internal/imgcache` split.
Done and merged into `main`:
- `imageprocessor` -> `internal/imageprocessor/` (PR #37)
- `allowlist` -> `internal/allowlist/` (PR #41)
- `magic` -> `internal/magic/` (PR #42)
- `httpfetcher` -> `internal/httpfetcher/` (PR #43, merged 2026-07-25)
This pass:
- Opened PR #46 (`refactor/extract-signature`) extracting `signature` -> `internal/signature/`, based on current `main`. It is independent of the `httpfetcher` move, so no dependency on other in-flight branches.
- Moved `Signer` (`New`, `Sign`, `Verify`, `GenerateSignedURL`), `ParseParams`, and the error sentinels. To keep the import edge one-way, the package defines a standalone `signature.Request`; `imgcache` projects its `ImageRequest` onto it via a small helper (same approach `magic` used for `ImageFormat`).
- No-stutter renames: `NewSigner` -> `New`, `ParseSignatureParams` -> `ParseParams`, `ErrSignature{Required,Invalid,Expired}` -> `Err{Required,Invalid,Expired}`. HMAC input bytes are unchanged, so existing signatures still verify.
- `docker build .` passes (fmt-check, lint, test, build). Labelled `needs-review`, awaiting merge into protected `main`.
Remaining "easily separable" extraction:
- `urlparser` (`urlparser.go`) -> `internal/urlparser/`. Note it is consumed outside `imgcache` (by `internal/handlers/image.go` via `ParseImagePath`/`ParsedURL`) and couples to `ImageRequest`, `Size`, `ImageFormat`, `FitMode`, so it needs standalone shared types on the extracted side. This is the natural next PR once #46 lands.
The "tightly coupled" group (`cache`, `storage`, `service`, and the shared types in `imgcache.go`) stays together as planned.
Direction for reworking PR #46 (currently labelled needs-rework): the two suggestions the previous review marked "non-blocking" are blocking, per sneak's comment on the PR. Both must land on refactor/extract-signature before re-review:
Golden known-answer test in internal/signature: a test with a fixed signing key and a fully-specified Request, asserting the exact base64url signature string, plus a companion assertion for the exact generated signed URL. This pins the on-the-wire byte format (host:path:query:width:height:format:expiration) so any future change to the signed bytes fails loudly instead of silently invalidating issued URLs.
Finish the allowlist terminology migration inside internal/imgcache: rename WithNoWhitelist() and every other whitelist-named identifier, test function name, and comment in the package to allowlist equivalents, consistent with internal/allowlist. User-facing YAML config keys stay unchanged in this PR; if any whitelist-named config surface remains, note it on the PR for a follow-up.
Process: work on the existing refactor/extract-signature branch, one logical change per commit, make check green before each commit, push when done, then summarize the rework in a comment on PR #46. All further discussion of the rework itself belongs on the PR.
Direction for reworking PR #46 (currently labelled `needs-rework`): the two suggestions the previous review marked "non-blocking" are blocking, per sneak's comment on the PR. Both must land on `refactor/extract-signature` before re-review:
1. **Golden known-answer test** in `internal/signature`: a test with a fixed signing key and a fully-specified `Request`, asserting the exact base64url signature string, plus a companion assertion for the exact generated signed URL. This pins the on-the-wire byte format (`host:path:query:width:height:format:expiration`) so any future change to the signed bytes fails loudly instead of silently invalidating issued URLs.
2. **Finish the allowlist terminology migration inside `internal/imgcache`**: rename `WithNoWhitelist()` and every other whitelist-named identifier, test function name, and comment in the package to allowlist equivalents, consistent with `internal/allowlist`. User-facing YAML config keys stay unchanged in this PR; if any whitelist-named config surface remains, note it on the PR for a follow-up.
Process: work on the existing `refactor/extract-signature` branch, one logical change per commit, `make check` green before each commit, push when done, then summarize the rework in a comment on PR #46. All further discussion of the rework itself belongs on the PR.
Additional rework direction for PR #46, per sneak's comment there (2026-08-07): the whitelist→allowlist rename must cover the user-facing config surface in the same pass — pre-1.0, there is no installed base and no deprecation window. Concretely, on refactor/extract-signature:
YAML key whitelist_hosts → allowlist_hosts (no back-compat alias for the old key).
Config.WhitelistHosts → Config.AllowlistHosts in internal/config/config.go, plus every reference (including any existing test call sites — this mechanical rename is explicitly authorized by the one-pass mandate; test assertions themselves must not be weakened).
config.example.yml and README.md updated to the new key. Note README.md currently documents the key as source_host_whitelist, which does not match the actual whitelist_hosts key — fix the README to the real, renamed key while there.
Whole-repo grep for remaining whitelist/Whitelist afterward; the only acceptable survivors are historical entries in TODO.md/commit history.
Same process as before: one logical commit, make check green (script/cibuild for the authoritative lint), push, then summarize on PR #46.
Additional rework direction for PR #46, per sneak's comment there (2026-08-07): the whitelist→allowlist rename must cover the user-facing config surface in the same pass — pre-1.0, there is no installed base and no deprecation window. Concretely, on `refactor/extract-signature`:
- YAML key `whitelist_hosts` → `allowlist_hosts` (no back-compat alias for the old key).
- `Config.WhitelistHosts` → `Config.AllowlistHosts` in `internal/config/config.go`, plus every reference (including any existing test call sites — this mechanical rename is explicitly authorized by the one-pass mandate; test assertions themselves must not be weakened).
- `config.example.yml` and `README.md` updated to the new key. Note `README.md` currently documents the key as `source_host_whitelist`, which does not match the actual `whitelist_hosts` key — fix the README to the real, renamed key while there.
- Whole-repo grep for remaining `whitelist`/`Whitelist` afterward; the only acceptable survivors are historical entries in `TODO.md`/commit history.
- Same process as before: one logical commit, `make check` green (`script/cibuild` for the authoritative lint), push, then summarize on PR #46.
Status checkpoint (session handoff — a fresh session will pick up from the tracker):
PR #46 (signature extraction + full whitelist→allowlist rename including the allowlist_hosts config key) is merged into main at 6573b9d. Done extractions: imageprocessor, allowlist, magic, httpfetcher, signature.
Remaining separable piece for this issue: urlparser (urlparser.go) → internal/urlparser/. Note from the earlier analysis: it is consumed outside imgcache (by internal/handlers/image.go via ParseImagePath/ParsedURL) and couples to ImageRequest, Size, ImageFormat, FitMode, so it needs standalone types on the extracted side, mirroring how signature.Request and magic's ImageFormat were handled. That is the natural next PR here; the tightly coupled remainder (cache, storage, service, shared types) stays together, after which this issue can close.
Open question for sneak from the #46 review, still unanswered: the sole remaining whitelist string in the repo is the illustrative naming example in CLAUDE.md line 51 (SourceHostWhitelist as a good-naming example). Say the word and it becomes SourceHostAllowlist in a trivial follow-up.
Status checkpoint (session handoff — a fresh session will pick up from the tracker):
- PR #46 (`signature` extraction + full whitelist→allowlist rename including the `allowlist_hosts` config key) is **merged** into `main` at `6573b9d`. Done extractions: `imageprocessor`, `allowlist`, `magic`, `httpfetcher`, `signature`.
- Remaining separable piece for this issue: **`urlparser`** (`urlparser.go`) → `internal/urlparser/`. Note from the earlier analysis: it is consumed outside `imgcache` (by `internal/handlers/image.go` via `ParseImagePath`/`ParsedURL`) and couples to `ImageRequest`, `Size`, `ImageFormat`, `FitMode`, so it needs standalone types on the extracted side, mirroring how `signature.Request` and `magic`'s `ImageFormat` were handled. That is the natural next PR here; the tightly coupled remainder (`cache`, `storage`, `service`, shared types) stays together, after which this issue can close.
- Open question for sneak from the #46 review, still unanswered: the sole remaining `whitelist` string in the repo is the illustrative naming example in `CLAUDE.md` line 51 (`SourceHostWhitelist` as a good-naming example). Say the word and it becomes `SourceHostAllowlist` in a trivial follow-up.
clawbot
added this to the 1.0.0 milestone 2026-08-09 03:37:44 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Per PR #37 discussion,
internal/imgcache/currently bundles many distinct concerns into a single package. This issue tracks splitting it into focused, independently testable packages.Already Done
internal/imageprocessor/— image format conversion and resizing (extracted in PR #37)internal/allowlist/— host allow list (extracted in PR #41, renamed from whitelist per sneak's feedback)internal/magic/— magic byte detection (extracted in PR #42)internal/httpfetcher/— SSRF-safe fetcher (extracted in PR #43)internal/signature/— HMAC signing and verification (extracted in PR #46)Remaining
urlparser.go) →internal/urlparser/ParsedURL,ParseImagePath,ParseImageURL, path traversal validation, size/format parsingImageRequest,Size,ImageFormat,FitMode— would need to import shared types, or define standalone result types the caller maps onto the imgcache vocabularyurlparser_test.gowith it; keep coverage of the path-traversal rejection cases intactThis is the last extraction in scope for this issue. Once it lands, this issue closes.
Tightly Coupled (keep together, out of scope)
These share significant state and data flow with the service orchestrator. Splitting them further would require significant interface surgery and may not be worth it:
cache.go) — cache lookup, variant storage, negative cache, stats. Tightly coupled withservice.goandstorage.go.storage.go) — content-addressedContentStorageandMetadataStorage. Created and managed byCache.service.go) — the core orchestrator that wires cache, fetcher, and processor together.imgcache.go) —ImageRequest,ImageResponse,ImageFormat,FitMode,Size, etc. These are the shared vocabulary used across all subpackages.Definition of Done
internal/urlparser/exists and contains the URL/path parsing logic and its tests;internal/imgcache/urlparser.goandurlparser_test.goare gone.urlparserdoes not importimgcache.New(), notNewURLParser()).make checkgreen anddocker build .passes.Approach
Each extraction should:
internal/docker build .passes (fmt, lint, test, build)Progress update on the
internal/imgcachesplit.Done and merged into
main:imageprocessor->internal/imageprocessor/(PR #37)allowlist->internal/allowlist/(PR #41)magic->internal/magic/(PR #42)httpfetcher->internal/httpfetcher/(PR #43, merged 2026-07-25)This pass:
refactor/extract-signature) extractingsignature->internal/signature/, based on currentmain. It is independent of thehttpfetchermove, so no dependency on other in-flight branches.Signer(New,Sign,Verify,GenerateSignedURL),ParseParams, and the error sentinels. To keep the import edge one-way, the package defines a standalonesignature.Request;imgcacheprojects itsImageRequestonto it via a small helper (same approachmagicused forImageFormat).NewSigner->New,ParseSignatureParams->ParseParams,ErrSignature{Required,Invalid,Expired}->Err{Required,Invalid,Expired}. HMAC input bytes are unchanged, so existing signatures still verify.docker build .passes (fmt-check, lint, test, build). Labelledneeds-review, awaiting merge into protectedmain.Remaining "easily separable" extraction:
urlparser(urlparser.go) ->internal/urlparser/. Note it is consumed outsideimgcache(byinternal/handlers/image.goviaParseImagePath/ParsedURL) and couples toImageRequest,Size,ImageFormat,FitMode, so it needs standalone shared types on the extracted side. This is the natural next PR once #46 lands.The "tightly coupled" group (
cache,storage,service, and the shared types inimgcache.go) stays together as planned.Direction for reworking PR #46 (currently labelled
needs-rework): the two suggestions the previous review marked "non-blocking" are blocking, per sneak's comment on the PR. Both must land onrefactor/extract-signaturebefore re-review:Golden known-answer test in
internal/signature: a test with a fixed signing key and a fully-specifiedRequest, asserting the exact base64url signature string, plus a companion assertion for the exact generated signed URL. This pins the on-the-wire byte format (host:path:query:width:height:format:expiration) so any future change to the signed bytes fails loudly instead of silently invalidating issued URLs.Finish the allowlist terminology migration inside
internal/imgcache: renameWithNoWhitelist()and every other whitelist-named identifier, test function name, and comment in the package to allowlist equivalents, consistent withinternal/allowlist. User-facing YAML config keys stay unchanged in this PR; if any whitelist-named config surface remains, note it on the PR for a follow-up.Process: work on the existing
refactor/extract-signaturebranch, one logical change per commit,make checkgreen before each commit, push when done, then summarize the rework in a comment on PR #46. All further discussion of the rework itself belongs on the PR.Additional rework direction for PR #46, per sneak's comment there (2026-08-07): the whitelist→allowlist rename must cover the user-facing config surface in the same pass — pre-1.0, there is no installed base and no deprecation window. Concretely, on
refactor/extract-signature:whitelist_hosts→allowlist_hosts(no back-compat alias for the old key).Config.WhitelistHosts→Config.AllowlistHostsininternal/config/config.go, plus every reference (including any existing test call sites — this mechanical rename is explicitly authorized by the one-pass mandate; test assertions themselves must not be weakened).config.example.ymlandREADME.mdupdated to the new key. NoteREADME.mdcurrently documents the key assource_host_whitelist, which does not match the actualwhitelist_hostskey — fix the README to the real, renamed key while there.whitelist/Whitelistafterward; the only acceptable survivors are historical entries inTODO.md/commit history.make checkgreen (script/cibuildfor the authoritative lint), push, then summarize on PR #46.Status checkpoint (session handoff — a fresh session will pick up from the tracker):
signatureextraction + full whitelist→allowlist rename including theallowlist_hostsconfig key) is merged intomainat6573b9d. Done extractions:imageprocessor,allowlist,magic,httpfetcher,signature.urlparser(urlparser.go) →internal/urlparser/. Note from the earlier analysis: it is consumed outsideimgcache(byinternal/handlers/image.goviaParseImagePath/ParsedURL) and couples toImageRequest,Size,ImageFormat,FitMode, so it needs standalone types on the extracted side, mirroring howsignature.Requestandmagic'sImageFormatwere handled. That is the natural next PR here; the tightly coupled remainder (cache,storage,service, shared types) stays together, after which this issue can close.whiteliststring in the repo is the illustrative naming example inCLAUDE.mdline 51 (SourceHostWhitelistas a good-naming example). Say the word and it becomesSourceHostAllowlistin a trivial follow-up.