Files
UmbraTrans/web
umbra audio c3a3e506ec Add a node from the console, and make curl | sh actually work
The page an administrator uses to bring a node online: name it, get a one-line command
and a six-digit code, hand both to whoever is at the machine. That is the last piece
between "someone runs a command" and self-service onboarding.

POST /api/compute/pairing-script now returns JSON - script, code, expiry and the
command - rather than the bare script. There are two callers with two needs: a person at
a terminal wants a script to pipe to sh, and the console wants all three values so it can
show them once and then forget them. Returning text would have forced the console to
parse a shell script to recover a six-digit number, which works until someone reformats
the script.

The code is shown once and there is no button to fetch it again, because the controller
keeps only an Argon2id hash. That is the server's limit, not a UI choice, and offering a
"show code" button would be a lie about what it can do.

## Three defects the template flow exposed

Running `curl ... | sh` on node 238 - the flow the console hands out - found that the
template could never pair, for three separate reasons:

- It had no prompt and no code. `000000` is the placeholder that means "nobody filled
  this in", so the script now asks. A rendered script from the console has the code
  substituted and skips the prompt, so nobody is asked for a number they do not need.
- It had no node name, and the name is what the code is bound to. It now asks too. The
  first version failed with "check the code has not expired" while the code was fine and
  the *name* was the placeholder - an error pointing at the wrong thing.
- It hardcoded `controller.invalid` as its controller address, so every deployment but
  one got a script that could not resolve its own controller. The address now comes from
  the Host header the template was fetched with, and `UMBRA_CONTROLLER` overrides it.

The error message itself was part of the problem: a DNS failure reported "check the code
has not expired", which is a worse fault than no message because it sends an operator to
check the one thing that was working.

## Verified

`curl -fsSL http://192.168.123.14:8442/api/node/bootstrap | sh` on 238, code and name
typed at the prompts: paired, agent installed, service registered, node `online`.

`/api/node/bootstrap` is unauthenticated and returns only the inert template - a script
that can check a machine and then stop. It is listed by name in the test that pins
unauthenticated node routes, alongside /pair, with the reason each is safe.

## Also found, not fixed

`VIEWS` in web/assets/admin.js is `['dashboard','accounts','organizations','audit']` but
`view-projects` exists in the markup, so the projects view can never be activated. It
predates this work and I have left it alone rather than change navigation behaviour as a
side effect of adding a page.

525 passed, 27 skipped.
2026-10-03 11:10:42 -07:00
..

UmbraTrans web application

The browser client and the account/organization API it talks to.

This is not a mock. Every page calls the real backend under /api. The three routes below are served from the same origin as the API by umbratrans_web, because the session cookie is HttpOnly and the CSRF token travels as a header, and the application deliberately installs no CORS middleware. A separately hosted frontend could not call it at all.

What is real

Surface Backend
Sign in, forced first-login password change, sign out Real, Argon2id and server-side sessions
Accounts: list, filter, provision, suspend, reactivate, deactivate, revoke sessions Real
Organizations: list, create, rename, re-slug, suspend, reactivate, archive Real
Audit log Real
Dashboard counts Real, scoped to the caller
Project workspace contents, maintenance, statistics Not built yet

The unbuilt areas render as explicit empty states that name the missing backend work. No page displays a fabricated name, count, or metric.

Running it

In development the API serves this directory. From the repository root:

cd /home/umbra/src/UmbraTrans
UMBRA_ENV=development \
UMBRATRANS_REPOSITORY=json \
UMBRATRANS_REPOSITORY_PATH=.local/data/development/umbratrans-web/dev-accounts.json \
UMBRATRANS_STATIC_ROOT=/home/umbra/src/UmbraTrans/web \
UMBRATRANS_ALLOWED_HOSTS=127.0.0.1,localhost \
  /home/umbra/.local/bin/uv run python -m umbratrans_web serve \
    --host 127.0.0.1 --port 8400 --repository json

There is no first administrator until one is provisioned through the operator command, which reads the one-time password from a hidden prompt:

UMBRA_ENV=development \
  /home/umbra/.local/bin/uv run umbratrans-bootstrap-admin \
  --username Umbraadmin \
  --repository-path .local/data/development/umbratrans-web/dev-accounts.json

--repository json keeps accounts across restarts. --repository memory starts empty every time.

Routes

  • / — sign in, and the mandatory password change for a temporary-password account
  • /uadmin/ — control panel: dashboard, accounts, organizations, audit
  • /workspace.html — project workspace

Security properties the client relies on

  • No secret is ever sent by a client. POST /api/uadmin/accounts has no password field; the service generates the one-time password. The request schema refuses unknown fields, so a submitted password is rejected before a handler sees it.
  • A one-time password is shown exactly once, in the otp-modal panel. It is held in one variable, cleared when the panel closes and on pagehide, and is never written to localStorage, sessionStorage, a cookie, the URL, the page title, or the console.
  • Every unsafe request carries X-CSRF-Token, read fresh from the readable umbratrans_csrf cookie. GET /api/health on page load bootstraps that cookie, because the middleware does not issue one on an unsafe method and POST /api/auth/login is exempt.
  • No inline script, no inline style, no innerHTML. The server sends script-src 'self'; style-src 'self', so an inline <script> or a style= attribute would be refused by the browser. All server-supplied text is rendered with textContent through the el() helper in api.js.
  • Nothing is trusted from the browser. The UI hides controls a role cannot use, but the service re-checks every one of them; a forged identifier is refused with the same generic error as any other forbidden request.

Not a production deployment

The application fails closed in production: the PostgreSQL adapter does not exist, so create_app raises rather than starting against an unreviewed store. Serving this directory is therefore a development arrangement, and the reviewed migration in umbratrans_web/migrations/ has not been applied.

Systemd

umbratrans-web-prototype.service currently serves this directory with python3 -m http.server, which is static only and cannot reach the API. It exists so the port stays occupied and the public hostname keeps answering. See systemd/umbratrans-web-prototype.service.