asobi_console (asobi v0.84.0)

View Source

The operator console bundle: what priv/console holds, resolved once.

The console is a Vite/React bundle built from console/ and committed to priv/console. This application serves it from the same origin as the ops API it reads - one artefact, no second host and no second container.

A host that has extensions with their own console screens builds a composed bundle instead - rebar3 asobi console, which compiles every installed extension's priv/console into one chunk alongside core's own screens - and points console_bundle_app at the application that bundle was written into. Nothing below changes for it: the composed bundle is read, validated and served by exactly this code, because it is the same shape of Vite output. See guides/console-extensions.md.

Nothing here joins a request path onto a directory. The served set is derived from the build manifest - the entry chunk, its stylesheets, and the assets it references - and each file is read into a map keyed by its own basename before the first request is answered. A request names a key that is in the map or gets a 404, so there is no traversal primitive to get wrong. That is worth the indirection: nova_file_controller:get_dir/1 joins wildcard path segments with no dot-segment check, and this is the shape that has no wildcard to join.

Manifest paths are validated even though the build writes them. A bundler is a third-party program producing a file this module then reads off disk; a name with a slash or a dot segment in it is refused rather than resolved.

The console is off unless console is set true in the asobi application env. Nova starts one listener, so the console shares the game port, and an operator surface appearing on a public port has to be asked for rather than arriving with an upgrade. With no ops_secret configured it can be loaded and cannot be used - there is no default credential (asobi_ops_auth).

Both outcomes of the load are memoised, the failure included. A node whose bundle is missing says so once and then answers 503 from a term, rather than stat-ing an absent directory on every request.

Summary

Functions

One asset by basename, or error for anything not in the bundle.

The resolved bundle, memoised across both outcomes.

Whether this node serves the console.

Read a console bundle out of Dir.

Types

asset()

-type asset() :: #{body := binary(), mime := binary(), etag := binary()}.

bundle()

-type bundle() :: #{script := binary(), styles := [binary()], assets := #{binary() => asset()}}.

problem()

-type problem() ::
          {bundle_app_unavailable, term()} |
          {manifest_unreadable, term()} |
          {manifest_not_json, term()} |
          no_entry |
          {bad_asset_name, binary()} |
          {unknown_asset_type, binary()} |
          {asset_unreadable, binary(), term()}.

Functions

asset/1

-spec asset(binary()) -> {ok, asset()} | error.

One asset by basename, or error for anything not in the bundle.

The lookup is a map read. Name is never used to build a path.

bundle()

-spec bundle() -> {ok, bundle()} | {error, problem()}.

The resolved bundle, memoised across both outcomes.

enabled()

-spec enabled() -> boolean().

Whether this node serves the console.

Default false. See the module doc for why it is not true.

load(Dir)

-spec load(file:filename_all()) -> {ok, bundle()} | {error, problem()}.

Read a console bundle out of Dir.

Pure apart from the reads, so a test can point it at a directory it built itself and check what the hardening refuses.