v1.2.0 — Two-factor admin auth, BYOK, and the first image that actually ships the dashboard
LatestHighlights
Two-factor authentication for the admin dashboard. Optional TOTP, ten single-use recovery codes, per-source login lockout, and revocable API tokens for scripts and CI.
BYOK — bring your own key. A team can register its own provider keys, encrypted at rest and routed only for that team's traffic, with optional fall-back to the shared pool or hard isolation.
If you ran the v1.0.0 or v1.1.0 container image, the dashboard returned 404. This release fixes that. Please read the next section.
⚠️ The published image never contained the dashboard
The runtime stage of our Dockerfile copied the compiled server but not the dashboard's static files. @fastify/static only warns when its root directory is missing, so the container started cleanly, passed its healthcheck, and served the API correctly — while returning a bare 404 to anyone who opened it in a browser.
The failure was completely silent. If you tried docker run on v1.0.0 or v1.1.0, saw a 404, and concluded the project was broken: you were right, and we're sorry. The gateway itself was working the whole time.
v1.2.0 is the first published image that contains the dashboard.
docker pull ghcr.io/alayra-systems-pvt-limited/alayra-nexus:1.2.0Installations from source were never affected.
Admin authentication
Signing in now exchanges your password for a short-lived session token at POST /admin/login. The dashboard no longer keeps ADMIN_PASSWORD in browser storage.
Enable TOTP from Settings, or via the API:
# 1. Enrol — returns a secret and an otpauth:// URI for your authenticator app
curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:3000/admin/auth/totp/enrol
# 2. Confirm with a code — returns 10 single-use recovery codes
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"code":"123456"}' http://localhost:3000/admin/auth/totp/confirmEnrolment does not take effect until a code confirms it, so an abandoned enrolment can never lock you out.
Important
Once 2FA is enabled, ADMIN_PASSWORD stops working as a bearer token on /admin/*.
This is deliberate. If the password still authenticated API calls, anyone holding it would bypass the second factor entirely and 2FA would be decorative. Scripts and CI should use an admin API token (POST /admin/tokens) — hashed, listed, and revocable.
Until you enrol a second factor, nothing changes. Upgrading is a no-op.
Lockout. Five failed sign-ins from one address triggers a 15-minute lockout with 429 + Retry-After (configurable via ADMIN_MAX_LOGIN_ATTEMPTS / ADMIN_LOCKOUT_SECONDS). A wrong password and a wrong code are reported identically, so the login form cannot be used as a password oracle.
TOTP is implemented directly on Node's crypto (RFC 6238 over RFC 4226) with no new dependency on the auth path, and is verified against both RFCs' published test vectors.
BYOK — bring your own key
A provider key can now be owned by a team instead of living in the shared pool. Set the owner when adding the key (Pools → + Key → Owner), or pass ownerTeamId to POST /admin/providers/:providerId/keys.
Routing tries the team's own keys first, then the shared pool — unless the team sets byokFallback: false, in which case it is hard-isolated and receives 503 rather than a credential it did not bring.
Owned keys are a scoped pool, not a parallel proxy: they flow through the same admission control, circuit breaker, guardrails, SSRF checks, and analytics pipeline. Two guarantees:
- A caller with no team can never be routed through an owned key, even when the shared pool is exhausted.
- The response cache is partitioned by owner. A response produced by one team's private key is never replayed to another team or to the shared pool.
Responses carry X-Nexus-BYOK: true, and nexus_byok_requests_total{result} tracks own / fallback / isolated_block.
Security
- The admin password, the Nexus API key, and the metrics token are now compared with
crypto.timingSafeEqualover fixed-width digests.===on strings short-circuits at the first differing byte, so rejection latency leaked how many leading bytes of a guess were correct. Team keys were already safe. - Provider names no longer reach inline
onclickhandlers. HTML escaping does not protect a JavaScript string context — a browser decodes an attribute before parsing its contents as code — so a provider namedO'Reilly'); …could break out. Values now travel indata-attributes read by a delegated listener.
Also in this release
- Response caching (optional, off by default) — an identical request is served from Redis, skipping the provider entirely. A real $0 call.
- A missing Postgres or Redis now fails with an instruction, not a retry storm. Startup names the unreachable service, its host and port, and the command that starts it. Connection URLs are reduced to
host:port, so a password inREDIS_URLnever reaches your logs. GET /admin/routing/statusreports per-provider key counts. They were previously summed across a whole tier and stamped onto every provider in it.X-Nexus-Tier-Downgradenow means what it says: a higher tier existed and could not serve the request. It was previously set on every non-premium request, including deployments with no premium provider configured.- Architecture docs at
docs/architecture/— the layering rule, the full request path, and a where-to-look file index. - The dashboard moved from
public/tofrontend/, and the admin API from one 571-line file tosrc/routes/admin/split by resource. No endpoint, request, or response changed.
Upgrading
docker compose pull && docker compose up -dThe container applies migration 0005_admin_auth automatically at startup. Running from source, apply it yourself:
npm run migrateThe response cache key changed to add tenant scoping, so cached entries from an earlier version are ignored and the cache repopulates over one TTL. Nothing else requires action.
Full changelog: v1.1.0...v1.2.0