Liking cljdoc? Tell your friends :D

vaelii.impl.serve

Headless EDN-over-HTTP daemon: one JVM owns one KB and serves it to remote clients (vaelii.impl.client). A thin reitit-ring + jetty layer over vaelii.core, the network dual of the in-process API.

Wire format is EDN. A sentence is a symbol s-expression — (dog Muffet), ?x, (genl dog animal) — which EDN round-trips losslessly; JSON would mangle the symbols. The body of every call is {:op <keyword> :args [...]}, and the reply is {:ok true :result …} or {:ok false :error "…"}. EDN is read with clojure.edn/read-string (never clojure.core/read-string), so an untrusted body cannot evaluate code — EDN has no reader-eval.

A refusal's :type is a plain keyword:body-too-large, :not-edn, :cross-origin, :bad-host — and so is the :type an engine ex-info carries through. The protocol is what a client written against another build discriminates on, so it cannot be qualified by the namespace that happens to serve it: a ::-qualified keyword names this namespace, and a client matching on it would be matching on where the daemon's code lives.

The daemon is the single writer (docs/storage.md, the single-writer contract): it owns the one process allowed to mutate the store, so it serializes every op through one monitor. Concurrent client writes therefore apply one at a time and cannot interleave; reads pay the same lock, which is conservative but keeps the contract simple.

Only the allowlisted ops are reachable (ops). Each is a vaelii.core fn with the KB supplied by the daemon — the client sends only the op and the remaining args — so no client can reach an arbitrary var. Sentex records in a result are projected to plain maps before they hit the wire (the sentex-map contract), so the client reads them back without the impl record class.

The change feed is the one thing that is not a vaelii.core fn (feed-ops), and it is a table of its own for that reason: core/watch takes a callback, so what a remote caller holds open instead is a subscription with a cursor:watch, :poll, :unwatch, :watchers, over the per-handler registry app builds (vaelii.impl.subscribe, docs/feed.md). A :poll that waits runs outside the monitor; everything else about them is an ordinary EDN op.

One shared bearer token authenticates the caller. With VAELII_API_TOKEN set (guard/api-token), every request presents Authorization: Bearer <token> or is answered 401 with a WWW-Authenticate: Bearer challenge; GET /health is the one route that answers without it. One token for the process, not a session and not an identity — per-caller identity is a reverse proxy's job, and this is the check that has to exist below it. Binding anything but loopback requires a token (-main refuses to start otherwise); on the loopback default it is optional, and a daemon without one is drivable by every process on the machine.

vaelii.impl.guard covers what a token does not, and matters most on the open loopback daemon: POST /op requires Content-Type: application/edn, refuses a cross-origin Origin, and answers only to a Host naming the interface it was started on. Together those stop a page the operator happens to visit from driving the KB over loopback — which binding to loopback alone does not.

Headless EDN-over-HTTP daemon: one JVM owns one KB and serves it to remote clients
(`vaelii.impl.client`).  A thin reitit-ring + jetty layer over `vaelii.core`, the
network dual of the in-process API.

**Wire format is EDN.**  A sentence is a symbol s-expression — `(dog Muffet)`, `?x`,
`(genl dog animal)` — which EDN round-trips losslessly; JSON would mangle the symbols.
The body of every call is `{:op <keyword> :args [...]}`, and the reply is
`{:ok true :result …}` or `{:ok false :error "…"}`.  EDN is read with
`clojure.edn/read-string` (never `clojure.core/read-string`), so an untrusted body
cannot evaluate code — EDN has no reader-eval.

**A refusal's `:type` is a plain keyword** — `:body-too-large`, `:not-edn`,
`:cross-origin`, `:bad-host` — and so is the `:type` an engine `ex-info` carries
through.  The protocol is what a client written against another build discriminates
on, so it cannot be qualified by the namespace that happens to serve it: a
`::`-qualified keyword names *this* namespace, and a client matching on it would be
matching on where the daemon's code lives.

**The daemon is the single writer** (docs/storage.md, the single-writer contract): it
owns the one process allowed to mutate the store, so it serializes every op through
one monitor.  Concurrent client writes therefore apply one at a time and cannot
interleave; reads pay the same lock, which is conservative but keeps the contract
simple.

**Only the allowlisted ops are reachable** (`ops`).  Each is a `vaelii.core` fn with
the KB supplied by the daemon — the client sends only the op and the remaining args —
so no client can reach an arbitrary var.  Sentex records in a result are projected to
plain maps before they hit the wire (the `sentex`-map contract), so the client reads
them back without the `impl` record class.

**The change feed is the one thing that is not a `vaelii.core` fn** (`feed-ops`), and
it is a table of its own for that reason: `core/watch` takes a callback, so what a
remote caller holds open instead is a subscription with a **cursor** — `:watch`,
`:poll`, `:unwatch`, `:watchers`, over the per-handler registry `app` builds
(`vaelii.impl.subscribe`, docs/feed.md).  A `:poll` that waits runs **outside** the
monitor; everything else about them is an ordinary EDN op.

**One shared bearer token authenticates the caller.**  With `VAELII_API_TOKEN` set
(`guard/api-token`), every request presents `Authorization: Bearer <token>` or is
answered 401 with a `WWW-Authenticate: Bearer` challenge; `GET /health` is the one
route that answers without it.  One token for the process, not a session and not an
identity — per-caller identity is a reverse proxy's job, and this is the check that
has to exist below it.  Binding anything but loopback **requires** a token (`-main`
refuses to start otherwise); on the loopback default it is optional, and a daemon
without one is drivable by every process on the machine.

`vaelii.impl.guard` covers what a token does not, and matters most on the open
loopback daemon: `POST /op` requires `Content-Type: application/edn`, refuses a
cross-origin `Origin`, and answers only to a `Host` naming the interface it was
started on.  Together those stop a page the operator happens to visit from driving
the KB over loopback — which binding to loopback alone does not.
raw docstring

-mainclj

(-main & args)

Run the daemon in the foreground. Args: [port [dir]] [--listen ADDR], in any order — dir selects the durable :disk backend (recovered on open, so it persists across restarts); with no dir the KB is in-memory and lives only as long as the process.

lein run -m vaelii.impl.serve 4200 /var/lib/vaelii lein run -m vaelii.impl.serve 4200 /var/lib/vaelii --listen 0.0.0.0 ; opt-in

It binds loopback unless --listen says otherwise, for the reason on loopback above: POST /op writes, and it is the KB's only writer. What it binds and what it requires are one decision (auth-posture), so they are stated together:

  • --listen names a non-loopback address ⇒ VAELII_API_TOKEN is required. Without one it is a line on stderr and exit 2, a code of its own so a supervisor tells a missing credential from the configuration typos below.
  • Loopback — the default, and --listen 127.0.0.1 said out loud — ⇒ the token is used when set, and its absence is a startup warning naming the flag that would require one.

Naming an address also drops the Host allowlist (guard/allowed-hosts), since the name you reach it by is then yours to know; set VAELII_ALLOWED_HOSTS to keep the check. Left unset, the daemon starts anyway — a reverse proxy setting its own Host needs exactly this — and host-posture turns the gap into a startup warning rather than a silence. A --listen with no address, an unknown flag, or a stray argument is one line and exit 1, like the port typo below.

Run the daemon in the foreground.  Args: `[port [dir]] [--listen ADDR]`, in any
order — `dir` selects the durable `:disk` backend (recovered on open, so it
persists across restarts); with no `dir` the KB is in-memory and lives only as
long as the process.

  lein run -m vaelii.impl.serve 4200 /var/lib/vaelii
  lein run -m vaelii.impl.serve 4200 /var/lib/vaelii --listen 0.0.0.0   ; opt-in

It binds **loopback** unless `--listen` says otherwise, for the reason on `loopback`
above: `POST /op` writes, and it is the KB's only writer.  What it binds and what it
requires are one decision (`auth-posture`), so they are stated together:

- `--listen` names a **non-loopback** address ⇒ `VAELII_API_TOKEN` is **required**.
  Without one it is a line on stderr and exit **2**, a code of its own so a
  supervisor tells a missing credential from the configuration typos below.
- **Loopback** — the default, and `--listen 127.0.0.1` said out loud — ⇒ the token
  is used when set, and its absence is a startup warning naming the flag that would
  require one.

Naming an address also drops the `Host` allowlist (`guard/allowed-hosts`), since the
name you reach it by is then yours to know; set `VAELII_ALLOWED_HOSTS` to keep the
check.  Left unset, the daemon starts anyway — a reverse proxy setting its own `Host`
needs exactly this — and `host-posture` turns the gap into a startup warning rather
than a silence.  A `--listen` with no address, an unknown flag, or a stray argument
is one line and exit 1, like the port typo below.
sourceraw docstring

appclj

(app kb)
(app kb {:keys [host] :or {host loopback} :as opts})

The ring handler for a KB — pure request -> response, so it is tested without a socket. One monitor per handler serializes the ops (the single-writer contract).

:host names the interface this handler will be served on, which fixes the Host values it answers to (guard/allowed-hosts). On the loopback default that refuses a rebound DNS name, the one attack same-origin? cannot see.

:token is the shared bearer token every request must present. Omitted, it is VAELII_API_TOKEN (guard/api-token), so a daemon and a client on one host agree without either being configured; an explicit nil serves open, which is what a test of the other refusals needs — a handler that 401s first exercises none of them.

The ring handler for a KB — pure `request -> response`, so it is tested without a
socket.  One monitor per handler serializes the ops (the single-writer contract).

`:host` names the interface this handler will be served on, which fixes the `Host`
values it answers to (`guard/allowed-hosts`).  On the loopback default that refuses
a rebound DNS name, the one attack `same-origin?` cannot see.

`:token` is the shared bearer token every request must present.  Omitted, it is
`VAELII_API_TOKEN` (`guard/api-token`), so a daemon and a client on one host agree
without either being configured; an explicit nil serves **open**, which is what a
test of the other refusals needs — a handler that 401s first exercises none of them.
sourceraw docstring

feed-opsclj

The change-feed operations, keyed by op keyword — (fn [ctx args]) over a ctx of {:kb :registry :monitor}.

Each says its own relationship to the daemon's write monitor, which is the one thing about them that is not like an engine op. :watch and :unwatch take it: they are instantaneous, and taking it makes the boundary exact — every settle that finished before a :watch returned is outside the subscription's feed, and every one that starts after it is inside. :poll must not, because a long poll parks: inside the monitor it would block every writer for the duration of its wait, turning a feature about liveness into a global stall. :watchers does not either — it reads the registry and expires what has expired, and a listing that had to queue behind a bulk load is a listing an operator asks for while the daemon is busy.

The change-feed operations, keyed by op keyword — `(fn [ctx args])` over a `ctx` of
`{:kb :registry :monitor}`.

Each says its own relationship to the daemon's write monitor, which is the one thing
about them that is not like an engine op.  `:watch` and `:unwatch` take it: they are
instantaneous, and taking it makes the boundary exact — every settle that finished
before a `:watch` returned is outside the subscription's feed, and every one that
starts after it is inside.  `:poll` **must not**, because a long poll parks: inside
the monitor it would block every writer for the duration of its wait, turning a
feature about liveness into a global stall.  `:watchers` does not either — it reads
the registry and expires what has expired, and a listing that had to queue behind a
bulk load is a listing an operator asks for while the daemon is busy.
sourceraw docstring

http-threadsclj

How many worker threads the daemon's HTTP server runs.

Stated rather than defaulted, because it is one half of a pair: a parked long poll holds one of these for the length of its wait, so subscribe/max-parked has to stay well under it or the feature that exists for liveness becomes the thing that stalls the daemon. Left implicit the two numbers were 50 and 64, the wrong way round, and nothing said so — 55 parked polls took /health from 62 ms to 26 s. serve_test pins the relationship.

How many worker threads the daemon's HTTP server runs.

Stated rather than defaulted, because it is one half of a pair: a parked long poll
holds one of these for the length of its wait, so `subscribe/max-parked` has to stay
well under it or the feature that exists for liveness becomes the thing that stalls
the daemon.  Left implicit the two numbers were 50 and 64, the wrong way round, and
nothing said so — 55 parked polls took `/health` from 62 ms to 26 s.  `serve_test`
pins the relationship.
sourceraw docstring

op-namesclj

Every op keyword this daemon answers, sorted — the vaelii.core allowlist and the daemon's own together. What an :unknown-op refusal hands back, so a caller discovering the surface sees one roster rather than the larger half of two.

Every op keyword this daemon answers, sorted — the `vaelii.core` allowlist and the
daemon's own together.  What an `:unknown-op` refusal hands back, so a caller
discovering the surface sees one roster rather than the larger half of two.
sourceraw docstring

opsclj

The reachable operations, keyed by op keyword. Reads, writes, and introspection — the working set a remote caller needs; extend by adding a vaelii.core fn here.

The reachable operations, keyed by op keyword.  Reads, writes, and introspection —
the working set a remote caller needs; extend by adding a `vaelii.core` fn here.
sourceraw docstring

portclj

(port server)

The actual TCP port a started Server is listening on — the ephemeral one when it was started with :port 0, read off its first connector.

The actual TCP port a started `Server` is listening on — the ephemeral one when it
was started with `:port 0`, read off its first connector.
sourceraw docstring

startclj

(start kb {:keys [port host] :or {port 4200 host loopback} :as opts})

Start the daemon over kb and return the running jetty Server (:join? false, so the caller controls its lifetime — a test stops it in a finally). :port 0 binds an ephemeral port; read the actual one with port.

:host defaults to loopback; pass an address ("0.0.0.0") to bind publicly, and read the note on loopback before doing so. :token is app's, forwarded only when the key is there, so an omitted one still reads VAELII_API_TOKEN and an explicit nil still serves open.

Start the daemon over `kb` and return the running jetty `Server` (`:join? false`, so
the caller controls its lifetime — a test stops it in a `finally`).  `:port 0` binds
an ephemeral port; read the actual one with `port`.

`:host` defaults to loopback; pass an address (`"0.0.0.0"`) to bind publicly, and
read the note on `loopback` before doing so.  `:token` is `app`'s, forwarded only
when the key is there, so an omitted one still reads `VAELII_API_TOKEN` and an
explicit nil still serves open.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close