Skip to content
Charming Docs
Esc
navigateopen⌘Jpreview
On this page

External files and CSP

Uploaded files, remote images, external APIs, browser security policy, and resource execution limits in Charming apps.

Charming gives each app a content security policy (CSP), a browser rule that limits where scripts, images, fonts, media, frames, and network requests can come from. The app manifest can add remote image origins to img-src. It cannot replace the policy or add arbitrary origins to other directives.

Choose a supported path

Need Where it lives Supported app path Browser policy
An app-owned image Charming asset storage Upload it, keep its key, and render it with window.charming.assets.load(key) The returned data: URL is allowed by img-src
Another app-owned file Charming asset storage Use env.assets, upload_asset, or window.charming.assets; keep the key rather than a signed URL Fetches use Charming’s fixed connect-src; inline media playback has separate limits below
A remote image A public HTTPS origin List the exact origin in permissions.browser["img-src"], then call window.charming.images.load(url) The listed origin joins img-src; load() returns an allowed data: URL
A public external API The API provider’s public HTTPS origin Call it from an app route after declaring the network capability and exact server fetch origins Server egress policy applies; this does not change browser connect-src
A font Google Fonts, system fonts, or Charming asset storage Prefer system fonts. Google Fonts is the fixed remote font source. An uploaded font URL is a standalone-only option Fixed style-src and font-src; no manifest font grant
Camera or microphone input The visitor’s device Declare the matching browser capability and use the standard browser API from a claimed app opened on the web Permissions Policy and iframe delegation apply, not media-src

Use Charming asset storage for files the app owns. Store the asset key, not the URL. env.assets.url(key) returns a signed URL that expires after 24 hours. window.charming.assets.getUrl(key) uses the asset token issued with the current page, which has the same lifetime; reload the app to get a new browser token after it expires. Use assets.load(key) for an uploaded image that must render inside a chat embed.

Server and browser APIs. Use env in server routes and window.charming in browser code.

The helpers return https://charm.ing/app/<app-id>/assets/<key> with signed query parameters. Treat the complete URL as opaque: do not construct it, remove its query, or move it to another app. Charming has no /files or /models route.

Use a remote host only when that host should remain the source of the file. Charming does not make a third-party URL stable. For remote images, choose a provider URL that is meant for long-term use and declare only its exact origin.

Least-privilege setup

Least-privilege manifest

This manifest grants one image origin to the browser and one API origin to backend fetch. The two lists are separate because they protect different execution environments.

{
  "$schema": "https://charm.ing/schema/app-manifest/2026-07-31.json",
  "id": "book-covers",
  "meta": { "name": "Book covers" },
  "capabilities": {
    "imports": ["charming:storage/[email protected]", "charming:network/[email protected]"]
  },
  "permissions": {
    "browser": {
      "img-src": ["https://covers.openlibrary.org"]
    },
    "server": {
      "fetch": ["https://openlibrary.org"]
    }
  }
}

Each entry must be a canonical exact HTTPS origin: scheme, hostname, and optional non-default port only. Do not include a trailing slash, path, query, fragment, credentials, wildcard, uppercase hostname, or explicit default port. Known local names and literal loopback, private, link-local, or IPv6 addresses are rejected. Image requests resolve DNS and refuse non-public destinations. Redirects are checked again and must stay within declared origins.

Do not list an origin in both places unless the UI loads images from it and the backend also calls its API.

Uploaded image

const file = fileInput.files[0];
const uploaded = await window.charming.assets.upload(file, { key: 'cover.jpg' });
image.src = await window.charming.assets.load(uploaded.key);

A file picker needs no browser capability. Uploading from the UI requires app run access. Reading and listing require app read access. See Data storage for backend file methods, limits, and accepted file types.

For a PDF, text file, or other download, ask assets.getUrl(key) for a signed URL from the current page and open it with window.charming.openLink(url). Do not save the signed URL in app storage.

Remote image

const remoteUrl = 'https://covers.openlibrary.org/b/isbn/9780140328721-L.jpg';
image.src = await window.charming.images.load(remoteUrl);

images.load() checks the declared origin on Charming’s server, checks every redirect, accepts only image responses, caps the response size, and returns a data: URL. images.proxy() returns a Charming proxy URL that works in the standalone app but is blocked by the outer policy in Claude and ChatGPT embeds.

External API

Call external APIs from a route handler, not directly from ui:

const response = await fetch('https://openlibrary.org/search.json?q=ursula+le+guin');
if (!response.ok) throw new Error('book_search_failed');
return await response.json();

Backend fetch needs both charming:network/[email protected] and a non-empty permissions.server.fetch list. Claimed apps can use it. Calls to an undeclared origin fail with network_blocked. For an API key, use charming:secrets/[email protected] and sealed env.fetch; never put a key in ui or source.

CSP directive map

permissions.browser["img-src"] is the only manifest field that widens the browser CSP. permissions.server.fetch controls backend egress, not connect-src.

Resource or operation CSP directive What an app can configure
Images, CSS background images, icons img-src Exact remote HTTPS origins through permissions.browser["img-src"]
Browser fetch, XHR, runtime calls, and asset reads connect-src No external browser origins. Charming supplies its app and relay origins
Inline app code, external scripts, modules, and worker fallback script-src No origins. Charming supplies the inline ui, its pinned UnoCSS runtime, and blob: for host-owned modules
App CSS and stylesheets style-src No origins. Put CSS in styles; Charming supplies inline styles and Google Fonts CSS
Font bytes font-src No origins. Charming supplies its app origin and Google Fonts bytes
Nested frames frame-src No origins. Charming controls app framing
Audio and video files media-src, falling back to default-src when absent No origins. Inline playback is not a supported app contract
Dedicated and shared workers worker-src, falling back through CSP rules when absent No origins. Workers are not a supported app contract
WebAssembly script-src plus a WebAssembly execution source No grant. The policy has neither wasm-unsafe-eval nor unsafe-eval

The renderer emits default-src, script-src, style-src, font-src, connect-src, frame-src, and img-src. media-src and worker-src are not emitted, and the manifest has no fields for them. A permissive origin that appears in a host-owned directive is not a public dependency contract. In particular, do not treat the origin of Charming’s pinned UnoCSS runtime as permission to import another package from that CDN.

Direct browser and chat embeds

The standalone app URL and a chat embed do not have the same browser authority.

Standalone app. A claimed app that declares charming:browser/[email protected] or charming:browser/[email protected] can run on a per-app secure origin. Charming adds the matching iframe permission and Permissions Policy entry. Other browser capabilities use the delivery rule in the browser features guide. The visitor still controls each browser prompt.

Chat embed. Charming’s production MCP renderer uses an opaque srcdoc frame and does not pass the per-app capability origin into the viewer. Camera, microphone, display capture, geolocation, and similar delegated APIs are blocked there. Provide an Open in web path. File pickers and uploaded images do not need those device permissions and work in embeds when they use the runtime paths above.

A camera stream is a device API result, not an external media file. The camera capability can work at the standalone URL even though the app CSP has no media-src grant. An uploaded or remote audio or video file is different: storage accepts those files, but the generated app policy does not support playing them through <audio> or <video>.

An MCP host also applies its own outer CSP. The browser enforces both policies, and the narrower rule wins. assets.load() and images.load() return data: image URLs to survive that intersection. A CSP entry in the app manifest cannot weaken the host’s policy.

Validation and errors

The dated manifest schema uses additionalProperties: false at every manifest object level. Unknown keys such as dependencies, csp, connect-src, script-src, or a second browser permission fail create or update with invalid_manifest_schema (HTTP status 422 on the HTTP API).

Malformed exact origins also fail with invalid_manifest_schema. A syntactically exact known-local or literal private origin fails with invalid_image_hosts or invalid_fetch_hosts. Image loading also resolves DNS and returns an unsafe-URL error for a non-public destination. Origins listed under permissions.server.fetch without the network capability are stored but inactive; create or update returns a warning and backend fetch stays blocked.

At runtime, images.load() rejects when the request, access check, or image response-type check fails. assets.load() converts any successfully served asset to a data: URL; use it as an image source only for an image asset. Treat a rejection or failed element render as a failed load and show a fallback; plain error messages are not a stable branching contract. A raw resource blocked by CSP raises a browser securitypolicyviolation event, which Charming reports as a diag_report event in the app activity log.

Current limitations

External files are not package installation. The current manifest has no dependency field, and window.charming.deps is undefined. Charming does not provide deps.load, deps.url, a package catalog, or dependency-scoped CSP.

  • Scripts and modules. Do not add external <script> tags, module imports, or CDN imports. The ui field is one inline classic script. If a task needs a small self-contained browser library, follow Use JavaScript modules and libraries to review, pin, and inline its UMD or IIFE bytes. Inlined code receives the same window.charming authority as the rest of the UI.
  • Workers. There is no worker API or manifest grant. The blob: allowance exists for Charming’s host-owned modules and is not a promise that app-authored workers will work across hosts.
  • WebAssembly and models. application/wasm uploads are rejected, browser WebAssembly execution lacks the required CSP source, and Charming has no model loader or same-origin model endpoint. A generic JSON or binary asset remains a file; storing it does not install a model runtime.
  • MediaPipe. MediaPipe Tasks needs script or module loading, WebAssembly execution, model files, and often workers. Charming does not support that dependency set. Do not claim that adding a CDN origin or a manifest dependency enables it.

Was this page helpful?