Skip to content

fix(recraft): re-host generated images to signed artifact URLs (F4)#112

Merged
vinsonconsulting merged 1 commit into
mainfrom
feat/f4-recraft-rehost
Jun 28, 2026
Merged

fix(recraft): re-host generated images to signed artifact URLs (F4)#112
vinsonconsulting merged 1 commit into
mainfrom
feat/f4-recraft-rehost

Conversation

@vinsonconsulting

Copy link
Copy Markdown
Owner

F4 — Recraft re-host

limner_generate_recraft returned Recraft's ephemeral (~24h) CDN url directly, while DALL·E, upscale, vectorize, and compose all re-host their output bytes to a signed Limner /artifact capability URL (#63, #93). Root cause: the generate handler never requested inline bytes, so the transport returned (and preferred) Recraft's hosted url, which has no bytes to re-host.

Fix

Request response_format=b64_json on the generation call (text-to-image and image-to-image), mirroring upscale/vectorize (pipelines.ts), so the bytes flow through the existing maybeDeliveruploadArtifact path and out as a signed URL. The responseFormat hint is internal (set by the MCP tool layer), not exposed on the user-facing tool schema.

Two supporting fixes:

  • SVG-aware mime sniffing — a vector_illustration b64 result is stamped image/svg+xml (via looksLikeSvg) instead of the raster default, so the delivered artifact's extension/content-type match the bytes.
  • Url-only fallback — if a style ignores response_format and Recraft still returns a hosted url, the transport fetches those bytes through the existing SSRF-guarded fetchInputImage, so no provider CDN url ever reaches the client.

Tests

  • Rewrote the recraft dispatch test (which had encoded the bug — it asserted the raw img.recraft.ai url) to assert re-hosting + a b64_json request.
  • Added the url-only fallback case.
  • Added transport tests for response_format forwarding (JSON + multipart) and SVG sniffing.

Suite 665 → 670 passing. Typecheck + lint clean.

Verification

After deploy, limner_generate_recraft returns a signed mcp.limner.us/artifact/... URL (no recraft.ai host); tampered/expired sig → 403 like the other artifact paths.

🤖 Generated with Claude Code

limner_generate_recraft returned Recraft's ephemeral (~24h) CDN url
directly, while DALL·E, upscale, vectorize, and compose all re-host their
output bytes to a signed Limner /artifact capability URL. The cause: the
generate handler never requested inline bytes, so the transport returned
(and preferred) Recraft's hosted url, which has no bytes to re-host.

Request response_format=b64_json on the generation call (text-to-image and
image-to-image), mirroring upscale/vectorize, so the bytes flow through the
existing maybeDeliver -> uploadArtifact path and out as a signed URL. Two
supporting fixes:

- SVG-aware mime sniffing: a vector_illustration b64 result is now stamped
  image/svg+xml (via looksLikeSvg) instead of the raster default, so the
  delivered artifact's extension/content-type match the bytes.
- Url-only fallback: if a style ignores response_format and Recraft still
  returns a hosted url, the transport fetches those bytes through the
  existing SSRF-guarded fetchInputImage, so no provider CDN url ever reaches
  the client.

The responseFormat hint is internal (set by the MCP tool layer), not exposed
on the user-facing tool schema.

Tests: rewrote the recraft dispatch test (which had encoded the bug) to
assert re-hosting + a b64_json request; added the url-only fallback case;
added transport tests for response_format forwarding (JSON + multipart) and
SVG sniffing. Suite 665 -> 670.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jim Vinson <jim@vinson.org>
@vinsonconsulting vinsonconsulting merged commit a09feee into main Jun 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant