Skip to content

Asset Protocol ​

User-imported assets (alert images, GIFs) live in userData/assets/ and are referenced by a short opaque asset id, never by a filesystem path. Two delivery channels serve the same files: a privileged Electron protocol for in-app surfaces, and an HTTP route for OBS browser sources. This page is for developers writing custom overlays.

Assets are managed in the Assets Manager; a flow's AlertDisplay.assetId is resolved to an id the overlay can load (see Alert Flow Spec).

Asset id format ​

[a-zA-Z0-9_-]{8,16}

Both delivery channels enforce this exact regex before touching the disk. An id outside this range is rejected with 404 and never reaches a file lookup. This is the first line of defence against path traversal.

Channel 1 - streamoos-asset:// protocol ​

Registered in app/src/main/main.ts before app.whenReady() via registerSchemesAsPrivileged:

typescript
protocol.registerSchemesAsPrivileged([
  {
    scheme: "streamoos-asset",
    privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true },
  },
]);

The handler reads the id from the URL hostname (streamoos-asset://<id>), validates the id regex, resolves the file path via prefsStore.getAssetFilePath(id), then re-checks path containment (the resolved path must stay inside userData/assets/) before streaming the file with net.fetch("file://…"). A traversal attempt is logged and returns 403; a missing asset returns 404.

streamoos-asset://aB3dEf9k

This scheme is allowed in the renderer Content-Security-Policy img-src directive, so dashboard and in-app overlay surfaces can use it directly in <img src>.

Channel 2 - HTTP route on the overlay server ​

OBS browser sources cannot use a custom Electron scheme, so the embedded overlay HTTP server (app/src/main/services/overlay-server.ts, 127.0.0.1:8766) exposes the same assets over plain HTTP:

http://127.0.0.1:8766/overlays/asset/<id>

The route applies the same id regex ([a-zA-Z0-9_-]{8,16}), resolves the path through the injected resolveCustomAssetPath(id) callback, then enforces containment against the allowed asset roots before serving. When no resolver is configured the route 404s. The broader server also blocks any pathname containing .. and validates every resolved path against its allowed roots.

Responses are sent with Access-Control-Allow-Origin: * (for OBS) and aggressive no-cache headers, so a reload always picks up the latest file.

Resolution from a flow ​

When a flow display carries an assetId, resolveDisplayImage() (app/src/main/alerts/resolveDisplayImage.ts) looks the asset up and returns the file basename (e.g. my-gif.gif), which it writes onto AlertDisplay.imageUrl. The overlay's resolveAssetPath() strips any leading path segments and rebuilds a relative path under its own assets directory. The basename, not the id, is what the overlay finally loads. imageUrl is computed at fire time and never persisted.

Security summary ​

Guardstreamoos-asset:///overlays/asset/<id>
Id regex {8,16}yesyes
Path containmentinside userData/assets/inside allowed asset roots
Failure mode403 traversal / 404404
Cachingn/adisabled (no-store)

Both channels share one rule: an id is opaque, validated, and resolved server-side. A custom overlay never constructs a filesystem path itself.

Released under the MIT License.