Serve /fpagent/install.sh (also curl|bash) and proxy the private fingerprint-agent releases through /fpagent/releases/* using GITEA_RELEASE_INSTALL_KEY, so a field laptop needs no forge credentials. The installer picks the offline bundle for the host codename when present, verifies the published sha256, then hands off to the bundle's own installer.
YOUreka Tools
The web front-end for YOUreka ID field operations. A small Express app that hosts an authentication-aware landing page, an Image Cropper, a Fingerprint Agent enrollment wizard, and a public Help page.
Fingerprint captures run through the local fpagent
binary (Socket.IO on 127.0.0.1:9000); this app handles login (YOUreka Auth),
per-domain permissions, and the a2loft integration that reads and stores records.
It is stateless — the operator's PocketBase session lives in an httpOnly cookie,
so the container can run anywhere while fpagent stays on the field laptop.
Tools at a glance
| Page | Path | Access | Purpose |
|---|---|---|---|
| Landing | / |
public | Links the tools when signed in; shows a sign-in call to action when not |
| Fingerprint Agent | /fingerprint |
signed in | Enroll right-thumb and right-index fingerprints via the attached scanner |
| Image Cropper | /cropper |
signed in + crop_image |
Review, crop, clean and save identity photos from the a2loft queue |
| Help | /help |
public | Intro plus a per-tool how-to (Carbon accordion) |
Navigation
Every page shares one Carbon ui-shell header and mobile side nav, rendered server-side:
- Home →
/ - Image Cropper, Fingerprint Agent → shown only when signed in
- a divider, then Help
- Sign in (anonymous) or Sign out (signed in; confirmed via a modal)
The landing page hides the two service tiles until you sign in, and the sign-in
callback returns you to /.
Layout
tools/
├── server.js # Express app: routes, session middleware, caching, proxies
├── yrka-auth.js # YOUreka Auth cookie session → PocketBase (authRefresh)
├── permit.js # OpenFGA permission checks + 5-minute cache
├── cropper.js # /cropper router (queue, review, save) — injectable clients
├── public/
│ ├── index.html # landing page shell (tiles injected per session)
│ └── assets/
│ ├── style.css # shared Carbon-styled layout + g100 theme
│ ├── help.html # /help docs page
│ ├── fp/ # /fingerprint wizard (Vue 2 via CDN)
│ ├── cropper/ # /cropper page (Vue 2 + vue-croppa via CDN)
│ └── youreka-logo.png, youreka-favicon.png
└── test/ # node --test suite (fake portal / PocketBase / a2loft), 48 tests
openspec/ at the repository root holds the design specs and the in-flight
change proposals (see Design docs).
Running
Prerequisites: Node.js 22+. For the Fingerprint Agent you also need fpagent
installed and running on this laptop (see the fingerprint-agent repository) with
the U.are.U 4500 scanner attached.
cd tools
npm install
npm start # → http://localhost:4173
Open http://localhost:4173 and sign in with your YOUreka account. Started this
way (no cropper credentials) everything works except /cropper, which answers
503.
To run locally with the cropper, supply the credentials via dotenvx from the repository root:
dotenvx run -f .env -- node tools/server.js
fpagent must allow the tools origin. On a dev machine run it with:
fpagent -allowed-origins http://localhost:4173
How it works
Authentication and sessions
Sign-in is the YOUreka portal web-auth flow: the app opens a portal auth session
with a local callback URL, redirects the operator to the portal, and on callback
redeems the session id for a PocketBase cookie. That value is stored under the
app's own httpOnly session cookie (30-day max age) alongside the operator's
profile. On every request the session is recovered server-side and refreshed
when the token is invalid or expires within 5 minutes; an unrecoverable session
is treated as anonymous and the stale cookie cleared.
Permissions
- Fingerprint captures require
capture_biometricper domain. The hardcoded catalog (UNICAL, Metropolitan Hotel, UCTH, UNICAL Radio International, YOUreka) is filtered against the portal's OpenFGA endpoint, cached for 5 minutes; the Refresh domains control unlocks once the cache expires. fpagent re-authorizes every capture regardless. - Image Cropper actions require
crop_image. Reads may use the 5-minute cached verdict; the save always verifies live.
a2loft proxy
Staff lookup and the fingerprint save are proxied to https://apis.a2loft.net
under same-origin routes. The proxy attaches the operator's phone from the
server-side session and forwards the OTP supplied by the page; the operator
never sends credentials to a third party directly. The cropper's save call
instead authenticates with the session token (Bearer) after a live permission
check.
The Image Cropper
The /cropper page (Smart Photo Editor) lets operators work through the a2loft
id-images queue: Load next queued or Load by upload ID, review the
record, crop the photo, optionally Clean background, then accept or reject
with reasons. Reads may use the cached crop_image verdict; saving re-verifies
live and uploads through the queue.
Its service credentials are the only environment variables this app uses.
They live in dotenvx-encrypted files at the repository root — .env for
development, .env.production for deployment:
MINIO_ACCESS_KEY=… MINIO_SECRET_KEY=…
AZURE_STORAGE_CONNECTION_STRING=…
NODERED_HTTP_USERNAME=… NODERED_HTTP_PASSWORD=…
CARVEKIT_API_KEY=…
Without them the rest of the app works and /cropper answers 503. The private
decryption keys sit in .env.keys (gitignored, never commit it).
dotenvx run -f .env -- node tools/server.js # local, from the repo root
dotenvx set MINIO_SECRET_KEY "<value>" -f .env # add/rotate a value
The Fingerprint Agent
The /fingerprint wizard walks the operator through:
- Locate profile — pick a permitted domain, enter the staff reference and OTP, and look up the record through the a2loft proxy.
- Capture — right thumb and right index finger, one capture at a time, with
per-scan progress and cancel. The PocketBase token, domain and reference
travel with every
fprint-enrollto fpagent. - Save — both templates are posted to a2loft with a fresh OTP.
Configuration
Everything except the cropper credentials is hardcoded in tools/server.js by
design — port 4173, portal https://portal.yrka.app, PocketBase
https://pb.itcass.net, a2loft https://apis.a2loft.net, agent
http://127.0.0.1:9000, and the domain catalog. Change them there; there are no
other environment variables or config files.
Deployment
Cache busting. Each process start generates a deploy id
(DEPLOY_VERSION) that is appended to every /assets/... URL in served pages
(?v=…), so browsers fetch fresh CSS/JS/images after a deploy. Versioned assets
are cached hard (max-age=1y, immutable); the .html templates and all page
routes are served no-store.
Stateless container. The container only serves the web front-end; fpagent
keeps running on the operator's laptop (the wizard reaches it at
http://127.0.0.1:9000 from the browser, wherever the container lives). The
image bakes in the encrypted .env.production and runs through dotenvx; only
the private key is supplied at runtime (never baked in):
docker build -t yreka-tools .
docker run -d -p 4173:4173 --name yreka-tools \
--env DOTENV_PRIVATE_KEY_PRODUCTION="$(grep -oP '^DOTENV_PRIVATE_KEY_PRODUCTION=\K.*' .env.keys)" \
yreka-tools
dotenvx run --strict makes the container exit rather than start with
undecryptable credentials. Note that --env values are visible to
docker inspect; on a shared host prefer an env file with restrictive
permissions (--env-file deploy.keys) or your platform's secret store.
Behind a TLS-terminating proxy the app honours X-Forwarded-Proto
(trust proxy is enabled), so session cookies get the Secure flag
automatically.
Tests
cd tools
npm test # node --test — 48 tests, no network or hardware required
The suite covers the auth round-trip, session refresh, permission caching, the cropper router and token auth, and the a2loft proxies against fakes. A full field walkthrough (real portal, PocketBase, a2loft, and U.are.U 4500) — login → domain filtering → staff lookup → two captures with cancel path → save → reset — has been verified end to end via Playwright.
Design docs
openspec/ holds the source-of-truth specs under openspec/specs/
(tools-app, fingerprint-wizard, image-cropper) and change proposals under
openspec/changes/. See openspec/config.yaml for the workflow.