---
title: External files and CSP
description: Uploaded files, remote images, external APIs, browser security policy, and resource execution limits in Charming apps.
sidebar:
  label: External files and CSP
  order: 6
---

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](/docs/guides/browser-features) | 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.

```json
{
  "$schema": "https://charm.ing/schema/app-manifest/2026-07-31.json",
  "id": "book-covers",
  "meta": { "name": "Book covers" },
  "capabilities": {
    "imports": ["charming:storage/blob@1.0", "charming:network/fetch@1.0"]
  },
  "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

```js
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](/docs/guides/browser-features) needs no browser capability. Uploading from the UI requires app run access. Reading and listing require app read access. See [Data storage](/docs/capabilities/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

```js
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`:

```js
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/fetch@1.0` 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/fetch@1.0` 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/camera@1.0` or `charming:browser/microphone@1.0` 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](/docs/guides/browser-features). 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](/docs/guides/javascript-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.

## Related

- [Data storage](/docs/capabilities/data-storage)
- [External images](/docs/capabilities/external-images)
- [Use JavaScript modules and libraries](/docs/guides/javascript-libraries)
- [Browser runtime API](/docs/technical-reference/browser-runtime)
- [Browser features in your app](/docs/guides/browser-features)
- [Charming design guide](/docs/prompts/design-an-app)

Found a bug or need a feature? [Tell us](/docs/capabilities/feedback) with `submit_feedback` or `POST /app/{id}/feedback`. Your feedback shapes what we build next.
