joetjen.net
EN DE
Application

What Lictor 0.2 does

Lictor 0.2.0, released on 19 October 2026 with Praxis v0.63.0, serves its site and lets somebody in by invite. Everything on this page was run against that release.

In development · v0.2

Running it

From the lictor/ directory of the repository:

LICTOR_PORT=4123 LICTOR_BASE_URL=http://localhost:4123 prx run --sname lictor --no-halt
tailwind: stylesheet built
posthorn mailbox on http://localhost:64064
lictor: development admin seeded -- log in as admin, password admin. This account exists in development only: any other environment refuses it.
lictor listening on http://localhost:4123

Two addresses: the site, on LICTOR_PORT (4000 unless it says otherwise; this run chose 4123), and the development mailbox, on whichever port the system had free. --no-halt keeps the server running after main returns; --sname lictor makes the instance a node, which is how prx lictor invite reaches it. In development an admin, admin/admin, is seeded and announced — and any other environment refuses to start with it switched on.

Registration by invite

The beta is invite-only, and registration is reached through an invite and nothing else — there is no /register. In another terminal:

prx lictor invite Ada Lovelace
http://localhost:4123/invite/WPt3PZvpeo3FLslP6ugrKtO55X74XI7F2wkM_23G-Tc

The running Lictor makes the link: while the accounts are in memory an invite exists only in the instance that made it, so the task joins its node and asks Kether there. The link works once, and for a week.

  • GET /invite/<token> shows the form: a username, an email address, a password of twelve characters or more. Asking does not use the invite.
  • POST /invite/<token> registers: *Check your mail*, and one message in the mailbox. A username taken, an address already registered, a username that is an address or a short password answer the form again with 422, saying which field — and never writing the password back.
  • GET /verify/<token> — the link in the mail, valid a day — answers *Your account is ready*. An invite or verification link used, unknown or expired answers *no longer valid* with 410; a form without its CSRF token is a 403.

Walked through by hand against 0.2.0: the form posted with a five-character password answered 422 with *A password needs at least 12 characters.*, an address as the username 422 with *A username cannot be an email address.*, a correct form *Check your mail*; the invite link opened again then answered 410, the mailed link *Your account is ready*, the same link again 410, and /register 404.

The development mailbox

Nothing is sent. Every mail the running Lictor would send is kept by posthorn’s development mailbox and listed, newest first, on the page prx run printed — on 127.0.0.1 only, each mail opening to its text with the verification link in it. prx posthorn --node lictor serves the same page from another terminal; the mailbox is found because it advertises itself as it starts.

Routes

The routes are data in lictor.elysium, and prx routes reads the table back:

prx routes lictor.elysium
GET   /               lictor.elysium/front-page
GET   /invite/:token  lictor.elysium/invite-form
POST  /invite/:token  lictor.elysium/register
GET   /verify/:token  lictor.elysium/verify-address
POST  /theme          lictor.elysium/choose-theme

Anything else is a 404. /assets/ serves the stylesheet, the seal and the icons. POST /theme takes *light*, *dark* or *system*, remembered in a cookie for a year with no JavaScript; *system* follows the browser.

Configuration

Configuration is CASC, config/config.casc, with an overlay per environment, read by cooper_prx before main runs. Each application reads its own block and nothing else’s:

config/config.casc
#@version = 1.0

lictor_elysium {
  port = "${LICTOR_PORT:4000}"
  base-url = "${LICTOR_BASE_URL:http://localhost:4000}"
  mail-from = "lictor@localhost"
}

lictor_kether {
}

import "${PRX_ENV:dev}/*.casc"
Kether settingDefault
invite-validity604800a week, in seconds
verification-validity86400a day
minimum-password12characters
password-iterations600000the PBKDF2 cost of shibboleth

The comments are left out of the file above. The development overlay names the environment and switches the admin seed on.

Testing

Every journey starts as an acceptance test that asks the application the way a browser would, without a server — with the store, the mailbox and the clock handed in, so a test moves a week on without waiting for one.

apps/lictor_elysium/tests/lictor/elysium-test.prax
(deftest the-site-answers-at-its-root-with-a-page-that-names-lictor
  (let [answer ((elysium/app) (test/request :get "/"))]
    (assert= (:status answer) 200)
    (assert (str/contains? (:body answer) "Lictor"))))
prx test         # the suite's tests
prx precommit    # compile, fmt --check, test
  ok    an-invite-link-leads-to-a-verified-account
  ok    an-invite-works-once
  …
213 test(s), 0 failure(s)