Serves a registered app's bundle same-origin by fetching it upstream on
the server. This is the :proxy mode from KeenPhoenixSvelte.Apps — it turns a
cross-origin CDN bundle into a first-party asset so it isn't subject to CORS or
a strict CSP script-src, and lets you gate or cache it.
Mounting
Forward the :proxy_path (defaults to /apps) to this plug in your router:
forward "/apps", KeenPhoenixSvelte.Apps.ProxyA request resolves via KeenPhoenixSvelte.Apps.resolve/1, serves the cached
bundle, and revalidates it upstream when it goes stale. Both shapes are handled:
/apps/<name>— a single-file app (url:), served astext/javascript./apps/<name>/<sub-path>— a base-path app (base:); the sub-path is appended to the upstream directory, so a whole JS + CSS + assets bundle proxies through one registration. Each file'sContent-Typeis derived from its extension (.mjs/.js→text/javascript,.css→text/css, elseMIME), while JS is always forced to a module-friendly type.
The default prefix is the same /apps that local bundles load from. That's
intentional and safe: Plug.Static runs before the router, so local files at
/apps/<name>/main.mjs are served directly, and only unmatched paths
(/apps/<name>, the proxied bundles) fall through to this plug.
Caching & freshness
The bytes are cached and revalidated by KeenPhoenixSvelte.Apps.ProxyCache
(ETS + single-flight conditional GET). Freshness is driven by
the upstream's own Cache-Control / ETag / Last-Modified when present, and
falls back to a :ttl (default 5 min) otherwise — so unversioned upstreams
(cdn/app.js) are re-checked on a cadence instead of being pinned forever. See
KeenPhoenixSvelte.Apps for the :proxy_cache config and per-app ttl /
immutable / client_cache_control overrides.
On the way out this plug forwards an ETag and a revalidate-friendly
Cache-Control, and answers the browser's own If-None-Match with a 304 —
completing a browser → Phoenix → origin conditional-request chain. The
Cache-Control value is resolved most-specific-first: a per-app
client_cache_control: string, else a per-app immutable: true (whose value is
the global immutable_cache_control, defaulting to a 1-year immutable string),
else the global client_cache_control, else the built-in default.
Fetching
The upstream fetch uses Erlang's built-in :httpc by default. Override it for
tests or a different client with an :app_provider — either a 1-arity
fn url -> {:ok, body_binary} end (legacy; always treated as a fresh 200) or
a 2-arity fn url, validators -> {:ok, resp} | :not_modified | {:error, reason} end
that can honor validators.etag / validators.last_modified for conditional
revalidation (resp is a map with :body and optional :etag,
:last_modified, :cache_control):
config :keen_phoenix_svelte,
app_provider: fn _url, _validators -> {:ok, %{body: "export default 1;"}} end