Guarding

GitHub

Guarding the proxy

A base:/dir: app proxies any sub-path under its prefix — that's what lets a whole multi-file bundle work from one registration. But it also means an unauthenticated client can ask for paths that don't exist, or ones you never meant to expose. Left unchecked, a flood of guaranteed-miss names would drive an unbounded stream of outbound fetches. Two defenses keep it bounded:

manifest: opt-in · exact

Declare the exact files an app ships. Anything else is a 404 decided locally, before any upstream fetch. You choose precisely what's reachable.

negative cache always on

Even with no manifest, a definitive upstream 404 is briefly remembered (negative_ttl), so a repeated miss isn't re-fetched — and concurrent upstream fetches are capped.

Probe it

Each row fetches /apps/<name>/<path> through the proxy and shows the status. Watch secret.js: the guarded app blocks it (it's not in the manifest) while the open app serves the very same file. The non-existent views/ghost.mjs is a 404 either way — but the guarded app never fetches it, and the open app negative-caches the miss (re-run and its latency drops).

The allowed files still make a working island

The manifest only blocks what isn't listed — the entry, its lazy view chunks, stylesheet, icon and data file are all allowed, so dashboard-guarded mounts as a real code-split island (click the tabs to stream each chunk in through the proxy):

How it's wired

Same origin, registered twice — the only difference is the manifest: key:

config :keen_phoenix_svelte,
  apps: %{
    # only the files in keen-manifest.json may be served — else 404, no fetch
    "dashboard-guarded" => %{
      base: "…/dashboard/",
      entry: "main.mjs",
      manifest: "keen-manifest.json"
    },

    # no manifest — any sub-path is proxied (misses hit upstream, then cache)
    "dashboard-open" => %{base: "…/dashboard/", entry: "main.mjs"}
  }

The manifest is just the list of servable files — each path relative to base:. Three shapes are auto-detected: a flat JSON array (below), a Vite manifest.json (its output files are pulled out for you), or a newline list. Generate it with the keenManifest() Vite plugin, or hand-write it like this:

// dashboard/keen-manifest.json — the exact files this app ships
["main.mjs", "dashboard.css", "icon.svg", "data.json",
 "views/overview.mjs", "views/charts.mjs", "views/table.mjs"]

How the proxy reads it, per request

  1. The entry (main.mjs) and the manifest file itself are always allowed — even if you forget to list them.
  2. Any other sub-path must be an exact match in the list (paths are normalized; nested paths like views/charts.mjs match at any depth).
  3. A miss is a 404 decided locally, before any upstream fetch — the origin is never touched.
  4. The manifest is fetched and cached like any file, and re-parsed only when the bundle changes (keyed by ETag).
  5. If the manifest itself can't be loaded, the app fails opennegative_ttl and max_concurrent_fetches still bound abuse, so it's a tightening, not a single point of failure.