The cropper upload POSTs with the session token and no OTP query auth, and the panel shows the profile's domain and picture target. Field pass was verified earlier (upload 0b90075b, saved with no OTP). Merged the image-cropper delta into the main spec and archived the change.
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)
│ ├── fpagent/
│ │ └── install.sh # public /fpagent/install.sh (proxies private releases)
│ └── 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 authenticates the operator with the
signed-in session's PocketBase token, sent to a2loft as a Bearer credential;
the operator never sends credentials to a third party directly. The cropper's
save call uses the same token flow 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 live in dotenvx-encrypted files at the repository root
— .env for development, .env.production for deployment. The fpagent
installer key is read from the same files (see
Installing the agent):
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 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 authenticated with the session token.
The wizard tracks fpagent's scanner availability: the header tag and the capture panel show when the agent is offline or no reader is attached, and capture is disabled until a supported reader is detected. Availability means the reader is attached, not that a capture will succeed; enrolling still fails if the scanner is claimed or a scan does not complete.
Installing the agent
/fpagent/install.sh is a public installer (also served for curl | bash). It
fetches the newest bundle for the host architecture — x86_64 or 32-bit i386,
preferring the bundle built for the machine's Ubuntu codename — from the private
fingerprint-agent
forge, verifies its sha256, then runs the bundle's own installer, which handles
libfprint, the udev rule, plugdev and the systemd service. On a field laptop:
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check # report only
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --tag v1.0.0
Rerunning it is the upgrade path: it installs the newest release (or --tag
for a pinned one), the bundle installer reports the version change, and it
restarts a running service so the new binary takes effect. The version is the
git hash carried in the asset name, stamped next to the binary
(/usr/local/bin/fpagent.version) and compared on the next run. The systemd
service and the self-update timer are on by default; pass --no-service or
--no-updates to leave them off.
The forge is private, so the operator's laptop needs no credentials: the app
reads the release with GITEA_RELEASE_INSTALL_KEY and proxies it through
/fpagent/releases/*. Without that key the installer answers 503.
Configuration
Everything except the 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, forge
https://sources.yrka.app and the domain catalog. Change them there. The only
environment variables are the cropper credentials and the fpagent installer key
GITEA_RELEASE_INSTALL_KEY above.
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.