Skip to content
Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

External images

Load images from another site by listing its address; Charming allows only the addresses you list through the browser security policy that blocks everything else.

What your app can show

Photos, covers, and icons from other sites. By default, a Charming app’s UI can’t show an image hosted somewhere else: the browser’s security policy blocks it. Your agent lists the exact web addresses the images come from when it builds or updates the app, and Charming can request images from those addresses. The image provider can still refuse the request or remove the file.

Technical

The app declares its allowed image origins in the manifest, and window.charming.images handles loading them in the UI.

Copy this prompt for your agent

Add external images to my Charming app. Read
https://charm.ing/docs/capabilities/external-images.md first. List
the exact HTTPS origins the image URLs come from in the app's
manifest, then load each image with window.charming.images.load()
so it renders in every embed.
If you haven't built a Charming app before, read
https://charm.ing/docs/build-mcp.md (or build-http.md for the HTTP API)
for the app skeleton first.

How an agent performs this job

Your agent lists the exact HTTPS origins the app’s images come from in manifest.permissions.browser["img-src"] (full origins, not bare hostnames) when it creates or updates the app. Charming appends each origin to the app’s img-src content security policy; any origin not on the list stays blocked. Wildcards, paths, credentials, loopback, private, and link-local targets aren’t allowed. A bad entry rejects create_app or update_app with invalid_manifest_schema.

permissions: {
  browser: {
    "img-src": [
      "https://images.unsplash.com",
      "https://covers.openlibrary.org",
      "https://archive.org",
    ],
  },
}

https://archive.org is listed alongside https://covers.openlibrary.org because an Open Library cover request redirects there: the cover files live at the Internet Archive, which then hands the request to one of its numbered download nodes. See the redirect rule below.

In the UI, the agent renders each image through window.charming.images.load(url) or window.charming.images.proxy(url) rather than a raw <img src="https://…">, which fails silently under some embeds’ content security policy even when the origin is on the allowlist.

The contract

window.charming.images.load(url); // Promise<string> - resolves to a data: URL
window.charming.images.proxy(url); // string - same-origin proxy URL
  • window.charming.images.load(url) fetches the image through Charming’s server-side image proxy and resolves to a data: URL. It works in every embed, including Claude and ChatGPT, both of which inject their own outer content security policy that forbids a cross-origin <img src> but permits data:. Prefer this whenever the app may be embedded.
  • window.charming.images.proxy(url) returns the same-origin proxy URL. It works standalone, but not inside either Claude’s or ChatGPT’s inline embed.

Limits, access, and deletion

  • Allowlist enforcement. Both load and proxy check the permissions.browser["img-src"] allowlist on Charming’s server; the proxy refuses an undeclared host even when called directly. Neither bypasses the allowlist.
  • Redirects. Many image hosts redirect, and the requested URL has to match a listed origin exactly. Each redirect hop is checked too, but a hop may also land on a subdomain of a listed origin under the same scheme and port, so https://archive.org covers the https://ia600703.us.archive.org node a cover request ends up on. A hop to a domain you did not list returns redirect_origin_not_allowed, so list every domain the chain passes through. Each hop still has to resolve to a public address; a redirect to a private or link-local target is refused.
  • Operator controls. Charming can deny an image origin across all apps during an incident. A denied origin returns image_origin_denied, even if the app declared it, and the check also applies to redirects and server-side cache hits. A deny covers the named host and everything beneath it, so denying https://archive.org also stops its download nodes.
  • Rate limits. Each server process limits requests per app and per destination origin, including redirect destinations. A limit returns rate_limited with RateLimit-* and Retry-After headers. Use an edge rate limit when a deployment needs one budget shared across all server processes.
  • Undeclared origin. An image URL whose origin isn’t on the allowlist doesn’t render, and a raw <img> tag to it fails with no visible error: the image just stays broken.
  • Finding broken images. Blocked <img> requests and other content-security-policy violations surface as diag_report events on /app/<id>/activity, so you can find them without waiting for a user report.

When an image provider fails

An allowed origin does not guarantee that the provider will serve an image. A provider can deny access, remove a file, limit requests, or become unavailable. Charming keeps these failures separate from its own allowlist and access checks.

images.load() still rejects when loading fails, so your app can try another source or show an accessible fallback. Charming does not return a replacement image as a successful response. Show “Image unavailable” with a link to the original source; offer a retry for a temporary failure, not as the only action for a denied or missing image.

The next get_app read includes external image failures in the current revision’s recentIssues summary, even when the app catches the rejected promise. The summary identifies the source origin, the provider’s HTTP status when known, and a recovery suggestion. Do not call every failure a provider block: HTTP 403 establishes denied access, while a timeout or an unknown response does not.

To restore the image, verify another permitted source or upload a permitted copy as a Charming asset. Verify that the actual image renders before treating the repair as complete.

Operator configuration

CHARMING__IMAGES__DENY_ORIGINS is a comma-separated list of exact HTTPS origins. Entries accept the same compatible forms as app image grants, including a bare hostname, mixed case, or a trailing slash; Charming normalizes each valid entry before matching it. The proxy reads this setting on every request, so changing it does not require app republishing.

The in-process token buckets read these positive-integer settings at server start:

Setting Default Meaning
CHARMING__IMAGES__RATE_LIMIT_PER_APP_CAPACITY 120 Requests available to one app
CHARMING__IMAGES__RATE_LIMIT_PER_APP_WINDOW_MS 60000 Per-app refill window in milliseconds
CHARMING__IMAGES__RATE_LIMIT_PER_DESTINATION_CAPACITY 300 Requests available to one destination origin across apps
CHARMING__IMAGES__RATE_LIMIT_PER_DESTINATION_WINDOW_MS 60000 Per-destination refill window in milliseconds

Malformed rate settings warn once and use the default. The token buckets refill linearly, reset on process restart, and are not shared between server processes.

Was this page helpful?