medOS ultra

Bio-Identity Kit

Hospital-side shared identity + attestation kit (face/voice modalities).

19 min read diagramsUpdated 2026-06-01docs/architecture/bio-identity-kit.md
Field Value
Package @ever-medos/bio-identity-kit (web/packages/bio-identity-kit/)
Status Kernel shipped + verified (strict tsc + runtime assertions). Platform adapters pending.
Date 2026-05-30
Boundary Hospital-side shared layer. The self-sovereign half (did:bio keys, on-device templates) lives in the Ever HealthWallet, NOT here.
Related docs/integrations/ever-patient-wallet.md · docs/architecture/bia-rt-1-genomic-attestation-vocabulary.md · docs/architecture/bia-rt-2-fhir-careplan-profile.md · web/electron/native/biometric.ts · evernetwork.io (BIA-1 core spec)

1. What it is & why

A framework-agnostic TypeScript kernel that gives the hospital a shared bio identity (one record per patient, referenced by every app/touchpoint) and a shared attestation library (verifiable credentials proving identity events). It exists so a captured face — or any biometric — can drive identity across mobile, Electron (laptop), web, edge, and services with one consistent contract.

Driving use cases: medication “five rights” (right-patient check), patient unlock, and plastic-surgery consultation.

The hard rule: this library never stores raw biometrics. It stores DIDs, time-boxed grants, and signed attestations. Raw templates stay on the patient’s device (wallet) or are resolved transiently by a matcher adapter.


2. BIA / did:bio relationship

BIA = Biological Identity Authentication — Ever’s decentralized identity system (did:bio), DNA-anchored via Poseidon commitments, expressed as W3C Verifiable Credentials with BBS+ selective disclosure, held in the Ever HealthWallet. The core spec (BIA-1) lives on evernetwork.io; the repo carries bia-rt-1/rt-2 (radiation-oncology genomic extensions).

Face is modelled as a second biometric modality bound to the same did:bio subject as the genomic BIA-RT claims — one identity hub, many attestation types. This kit reuses the BIA-RT-1 credential envelope shape for biometric attestations.


3. The model & boundary

┌─ Ever HealthWallet (patient device, self-sovereign) ──────────────┐
│  • did:bio subject DID (DNA-anchored, Poseidon commitments)        │
│  • on-device biometric template (face / finger / iris)             │
│  • issues a time-boxed, revocable VerificationGrant at admission   │
└───────────────┬───────────────────────────────────────────────────┘
                │ QR / NFC / deep-link  (grant + sealed reference)
                ▼
┌─ @ever-medos/bio-identity-kit (hospital-side, shared) ────────────┐
│  IdentityStore          → BioIdentity (shared handle ↔ did:bio)    │
│  VerificationGrantStore → active grants (NO raw biometric)         │
│  BioIdentityEngine      → enroll / verify / attest                 │
│  AttestationStore       → Verifiable Credentials = shared library  │
│        ports: BiometricMatcher · CredentialSigner                  │
└───────────────┬───────────────────────────────────────────────────┘
                │ same kernel; only adapters differ per platform
     ┌──────────┼───────────────┬───────────────┐
     ▼          ▼               ▼               ▼
  Electron    Mobile          Web (opt)       NestJS / edge
  (laptop)    (Expo)          dashboards      (server verify)

Three core records:

  • BioIdentity — the shared handle: subjectDid (self-sovereign), patientRef (HN), enrolledModalities, status.
  • VerificationGrant — wallet-issued, time-boxed, revocable capability to run a 1:1 verify during an encounter. Carries an opaque referenceHandle (resolved by the matcher adapter — sealed template / wallet round-trip / enclave key), never a raw template.
  • AttestationEnvelope — a W3C VC (BIA-RT-1 shape): issuer DID, issuance date, credentialSubject (the claim), credentialStatus (revocation pointer), proof. Claim kinds: BiometricEnrollment, BiometricVerification, IdentityBinding.

4. Engine, ports & adapters (hexagonal)

BioIdentityEngine is pure domain logic; everything platform- or infra-specific is an injected port:

Port Responsibility Stub / reference Production
BiometricMatcher compare probe vs reference; liveness exact-equality stub ArcFace / InsightFace cosine
CredentialSigner sign / verify VC envelopes djb2 hash stub Ed25519 / BBS+ selective disclosure
AttestationStore the shared VC library in-memory Supabase / NestJS
IdentityStore shared identity records in-memory Supabase / NestJS
VerificationGrantStore active grants in-memory Supabase / NestJS
Clock / IdGenerator determinism / portability fixed + seq system + uuid

Engine API: registerIdentity · enroll · acceptVerificationGrant · verify · verifyAttestation · getAttestationsForSubject · getAttestationsForTarget · revokeGrant · revokeIdentity.


5. Use-case flows

Five-rights “right patient” (the high-value one):

Nurse scans wristband (AN)                 ── claim: "this is patient X"
  → staff device captures patient face     → BiometricProbe (embedding)
  → engine.verify({ context:'med-admin-five-rights', subjectDid, probe, targetRef:<MA id> })
       1. resolve active BioIdentity
       2. find active VerificationGrant      (else → reason 'no-active-grant')
       3. liveness gate                       (else → reason 'liveness-failed')
       4. matcher.compare(probe, grant.referenceHandle)
       5. on pass → sign + store BiometricVerificationClaim
  → attestation attached to the MedicationAdministration (safety gate + signed audit)

Patient unlock — native enclave releases the wallet key (no custom ML); optionally emits a verification attestation. Surgical consult (plastic) — NextFace 3D morphology captured as patient-owned imaging (wallet Bucket B), identity-bound by an attestation. (NextFace = 3D reconstruction for morphology, NOT identity matching — matching is ArcFace embeddings.)


6. Invariants

  1. No verify without an active wallet grant — the self-sovereign boundary; hospital cannot verify unilaterally.
  2. Only successful verifications are attested — a failed match files nothing.
  3. Attestations are tamper-evident — altering an envelope fails verifyAttestation.
  4. Verifications carry a targetRef — attach to the record they protect (e.g. a MedicationAdministration), queryable.
  5. No raw biometrics in this layer — DIDs, grants, and VCs only.
  6. Grants are time-boxed + revocable — expire automatically; revoke stops all future verifies.

7. Platform plan

The kernel already runs on all targets unchanged (pure TS; RN/Hermes-safe via guarded/injectable UUID). Difficulty lives in adapters.

Key split: unlock (patient on own device) needs no custom ML — use the native enclave everywhere; clinical capture (staff scans patient) needs ArcFace and happens on a bedside tablet/laptop. This is why web is optional.

Camera Embedding (ArcFace) Enclave unlock Secure storage Verdict
Electron (laptop) getUserMedia onnxruntime-node web/electron/native/biometric.ts safeStorage Build first
Mobile (Expo) expo-camera (dep) onnxruntime-react-native (EAS build) expo-local-authentication expo-secure-store Feasible, native build
Web (optional) getUserMedia onnxruntime-web (WASM/WebGPU) WebAuthn only ❌ no enclave Capture/match OK; no on-device template custody → WebAuthn + wallet/server reference

Uniform move: one ArcFace .onnx model behind the BiometricMatcher port, with onnxruntime-{node,react-native,web} bindings. Embedding runs on-device → only the vector leaves the device, never the raw face. Compare can run on-device or server-side (NestJS + onnxruntime-node).


8. File inventory (shipped)

File Role
web/packages/bio-identity-kit/package.json · tsconfig.json @ever-medos/* package (mirrors form-registry-types)
src/types.ts BioIdentity, AttestationEnvelope, VerificationGrant, claim types
src/ports.ts adapter interfaces (matcher, signer, stores, clock, ids)
src/engine.ts BioIdentityEngine
src/adapters/memory.ts in-memory IdentityStore / AttestationStore / VerificationGrantStore
src/adapters/stub.ts deterministic stub matcher + signer, clocks, id generators
src/index.ts barrel + createInMemoryEngine()
src/bio-identity-engine.test.ts 8 tests (enroll, five-rights, no-grant, revoke, no-match, patientRef resolve, tamper, expired-grant)

9. Verification

  • Strict typechecktsc --strict clean (TypeScript 5.9.2).
  • Runtime — assertion script exercises enroll → grant → five-rights verify → target query → no-grant → revoke → tamper → expired-grant → createInMemoryEngine; all pass.
  • Headless library — no UI/browser surface to verify.

10. Roadmap

  1. Capture port + EmbeddingMatcher (pure TS, additive) — the seam every platform adapter implements.
  2. Electron adaptergetUserMedia + onnxruntime-node, reuse biometric.ts for unlock. First working bedside capture→verify.
  3. Mobile adapter (Expo EAS) — expo-camera + onnxruntime-react-native + expo-local-authentication + expo-secure-store.
  4. Real stores — Supabase migration (bio_identities, bio_attestations, verification_grants) behind the ports.
  5. Real signer — Ed25519 / BBS+ to match BIA-RT-1.
  6. Five-rights wiring — call verify from the e-MAR admin path; surface the attestation on the record.
  7. Web (optional)onnxruntime-web + WebAuthn; dashboards (read-only attestation views) work today via the kernel.

11. Deferred & caveats

  • 1:N identify (walk-in / unconscious, no wallet) is the only thing needing a central searchable gallery. Deferred. If built: a separate, fenced-off service holding cancelable templates (revocable transforms, never raw embeddings), consent-gated, with its own DPIA — kept out of the attestation library.
  • Regulatory: faces are special-category data (GDPR Art. 9 / Thailand PDPA) → explicit consent + DPIA regardless of platform. Biometrics are irrevocable, which is the core reason for on-device custody + the grant model over a central gallery.
  • Liveness is weak against video replay — treat the capture gesture as quality-of-capture UX, not anti-spoofing. Real anti-spoofing is a separate passive/depth layer.

12. Update — bounded 1:N identify, opt-in config, list-patient on web (2026-05-30)

identify (bounded 1:N). engine.identify({ candidates, modality, context, probe, targetRef? }) matches a probe against a supplied candidate set (e.g. the visible list-patient rows). Safety: only candidates with an active grant are eligible (consent gate); a confident result needs a threshold pass and a margin over the runner-up, else ambiguous; a maxCandidates cap blocks unbounded search. Emits a BiometricVerification attestation on a confident match.

Opt-in configuration. BioIdentityConfig (src/config.ts) is a per-context capability matrix: enabled, allowed modalities, and per-context { enabled, operations:[verify|identify], requireLiveness, thresholds, identify:{maxCandidates,margin} }. The engine gates every verify/identify through it. Product default = disabledConfig() (off; a deployment opts in, sourced from a bio_identity_configs matrix / admin UI). permissiveConfig() is for dev/tests/sandbox; the engine constructor config? defaults to permissive for ergonomics — the product supplies a restrictive matrix.

Real matcher + capture seam. EmbeddingMatcher (src/adapters/embedding.ts) — cosine similarity over encoded embedding vectors (encodeEmbedding/decodeEmbedding), mapped to [0,1]. BiometricCapture port added to ports.ts for the camera→embedding step platform adapters implement.

list-patient on web — feasible. /list-patient (web/src/containers/list-patient/) is staff-facing, so the web “no enclave” limitation (which is about custodying a patient’s own template in their browser) does not apply — reference embeddings come from wallet grants, not browser storage. Two operations: 1:1 verify on a selected row (engine.verify) and bounded 1:N identify against the listed rows (engine.identify). Web specifics: embed in-browser via onnxruntime-web (light model, e.g. MobileFaceNet) so only the vector leaves; candidates = listed patients with an active grant; treat 1:N as a confirm-suggestion, never auto-select; bound the candidate set (current list/search), never hospital-wide.

New files: src/config.ts, src/adapters/embedding.ts. Tests: identify (correct / ambiguous / consent-gate skip) + config (disabled / per-context op restriction). All green under strict tsc + the tsx runtime proof.


13. Capture layer — Electron + web (2026-05-30)

src/capture.ts adds the camera→probe seam so one pipeline serves every platform:

  • FaceEmbedder (frame → vector) + CaptureSource (camera → frame) interfaces.
  • EmbeddingCapture implements BiometricCapture — composes a source + embedder into a BiometricProbe that flows straight into engine.verify / engine.identify.
  • FunctionFaceEmbedder — wraps any (frame) => vector; the integration point for onnxruntime-web (browser/Electron renderer), onnxruntime-node (Electron main), or onnxruntime-react-native (mobile).
  • MockFaceEmbedder + MockCaptureSource for headless tests.

src/adapters/browser-capture.tsBrowserCameraSource (getUserMedia → canvas frame). One source serves both web and the Electron renderer (Chromium). DOM-dependent, so imported directly, not from the root barrel (keeps DOM out of the RN/edge kernel).

No new dependencies added — the ML model is a pluggable FaceEmbedder; wiring onnxruntime-web + a model file (e.g. MobileFaceNet) is the one remaining per-platform step. Capture orchestration is verified via the tsx proof (MockCaptureSource → MockFaceEmbedder → identify); the getUserMedia + onnx parts need a real browser + model to exercise.

Still open: mobile CaptureSource (expo-camera) + the real onnxruntime embedders (deps + manual camera testing); Supabase-backed stores.


14. Staff face-attestation login (2026-05-30)

The engine is now subject-generic: BioIdentity.subjectType: 'patient' | 'practitioner', patientRefsubjectRef (HN for patients, staff id for staff), new staff-login context.

Authorization differs by subject — same engine:

  • Patients → wallet-issued, time-boxed VerificationGrant (self-sovereign, source: 'wallet').
  • Staff → no patient wallet, so enroll({ reference, standing }) creates a hospital-held standing grant (source: 'enrollment') from the enrolled reference. This is the enterprise-biometric path (employee, enrolled with consent/policy), distinct from the patient self-sovereign model.

Login flow (1:1 verify): staff claims identity (badge/username) → face scan → verify({ context: 'staff-login', subjectDid | subjectRef, probe }) → on match the app issues its session (JWT via services/ever-aaa) and the engine files a staff-login BiometricVerification attestation. A staff member who hasn’t enrolled has no standing grant → no-active-grant (cannot log in by face). Proven in tests + the tsx proof.

Not yet wired: the real login screen + ever-aaa JWT bridge, the enrollment admin flow, and (as everywhere) a real embedder + camera. Staff enrollment stores a biometric reference centrally → same template-protection / consent / DPIA obligations as any enterprise biometric; prefer a cancelable transform over a raw embedding at go-live.


15. Persistence — Supabase stores (2026-05-31)

Migration infrastructure/medbase/migrations/20260531a_bio_identity_attestation.sql creates three RLS service-role-only tables modeling patient and staff:

  • bio_identities (subject_did pk, subject_type, subject_ref, enrolled_modalities, status)
  • verification_grants (grant_id pk, source wallet|enrollment, expires_at, scope_contexts, revoked_at)
  • bio_attestations (id pk, denormalized subject/issuer/kind/context/target_ref/score + the full envelope jsonb)

src/adapters/supabase.ts implements all three ports (SupabaseIdentityStore / SupabaseVerificationGrantStore / SupabaseAttestationStore). Dependency-free — typed against a minimal BioSupabaseClient interface that the real @supabase/supabase-js client satisfies (pass a service-role client; cast if TS flags the structural match). Verified by running the full engine (patient verify + revoke + bounded identify + staff-login + subjectRef resolve) through a mock client — identical behavior to the in-memory stores.

Apply: run the migration manually (SQL editor / psql -f), per repo convention. Still open: page wiring (#2), camera widget (#3), real onnxruntime model (#4), and the ever-aaa JWT bridge for staff login.


16. Sandbox demo (2026-05-31)

web/sandbox/targets/BioIdentityListPatientTarget.tsx (registry key BioIdentityListPatient) — a clickable demo of the kit on a list-patient surface, wired end-to-end with the in-memory engine + a MOCK embedder (no real recognition, no DB). Run from web/: pnpm exec vite --config sandbox/vite.config.tshttp://localhost:5179/?target=BioIdentityListPatient.

Shows: a patient list (each with an active wallet grant); “simulate scan as” → bounded 1:N identify against the list (matched row highlights + confidence/score/margin + “confirm before proceeding”); and a staff member → 1:1 staff-login verify. Optional camera toggle exercises BrowserCameraSource (getUserMedia); matching uses the simulated probe so the demo is deterministic. Imports the kit by relative path (sandbox fs.allow = web/); no sandbox config changed.

Verified via a Playwright smoke: boot sandbox → scan default patient → asserts “Matched … Somchai, confidence high, score 0.998, margin 0.487”, zero console/page errors. (Staff-login + the rest of the engine are covered by the unit proof; the camera path is hand-off to a real browser.)

Still open (unchanged): real-page wiring (/list-patient, login), real onnxruntime model + migration applied, ever-aaa JWT bridge.


17. Real face embedder — onnxruntime (2026-05-31)

src/adapters/onnx-face-embedder.ts — the recognition piece, dependency-free (ort runtime + model session injected, like the Supabase client):

  • preprocessFaceFrame(frame, opts) — RGBA frame → normalized float tensor (nearest-neighbor resize to size², mean/std, NCHW/NHWC, RGB/BGR). MobileFaceNet/ArcFace defaults: 112², 127.5/128, NCHW, RGB. Pure + unit-tested (exact tensor asserted).
  • l2normalize(vec) — pure + tested.
  • OnnxFaceEmbedder implements FaceEmbedder — preprocess → new Tensor(...)session.run() → L2-normalize. Tested via an injected fake session.

Activate (the one remaining piece for real recognition):

const session  = await ort.InferenceSession.create('/models/mobilefacenet.onnx')
const embedder = new OnnxFaceEmbedder({ session, Tensor: ort.Tensor, inputName: 'input', outputName: 'output' })
new EmbeddingCapture({ source: new BrowserCameraSource(), embedder })  // → engine.identify / verify

Needs: pnpm add onnxruntime-web + a MobileFaceNet/ArcFace .onnx in web/public/models/ + browser testing. Preprocessing/normalization are verified here; session.run() correctness needs the real model.

Truly remaining: (a) add dep + model file; (b) wire /list-patient + login pages (mount EmbeddingCapture, call identify/verify); © apply the migration; (d) ever-aaa JWT bridge for staff login.


18. Face-login demo + the ever-aaa contract (2026-05-31)

web/sandbox/targets/FaceLoginTarget.tsx (registry key FaceLogin) — staff and patient face login via the kit. Staff → staff-login verify (enrollment standing grant); patient → patient-unlock verify (wallet grant). On success the engine emits a signed BiometricVerification attestation; the demo shows the envelope a client would POST for a JWT. A “present wrong face” toggle demonstrates the deny gate. Verified via Playwright smoke: grant (staff authenticated, score 1.000) + deny (wrong face → no-match, no session/attestation) + zero console errors.

The real session (what a demo can’t fake) — proposed ever-aaa contract:

POST /auth/face-login   { "attestation": <signed BiometricVerification VC> }
  1. verify VC signature (issuer = this hospital's DID) + freshness (issuanceDate within N sec)
  2. confirm context = 'staff-login', acceptable confidenceClass, liveness passed
  3. map credentialSubject.id (did) → user account
  4. issue the normal JWT (same path as password login)

This is new, security-critical auth work (a biometric auth strategy in services/auth / ever-api-aaa) — design + review + deploy required; not to be wired blind. Until it exists, face login is a verified UX demo, not a live auth path.

Truly remaining for live face login: the ever-aaa endpoint above; real model (onnxruntime + .onnx); editing the real login page to mount the capture + call it; migration applied.


19. Optional staff face-login endpoint + RUDS logging (2026-05-31)

Endpoint: POST /auth/face/sign-in (Moleculer action auth.faceSignIn) in services/auth. Optional — env FACE_LOGIN_ENABLED (default OFF); disabled → 403 FACE_LOGIN_DISABLED.

Flow (server-side verify = the trust anchor): client posts { subjectRef | subjectDid, template } (template = face embedding computed client-side) → server reads the active verification_grants reference from Supabase, cosine-matches (FACE_LOGIN_MIN_SCORE, default 0.7 mapped) → on match maps subjectRef → user (by username) → issues the same JWT as password signIn. Files: auth.service.ts#faceSignIn, bioFaceLogin.helper.ts, dto/faceSignIn.dto.ts, auth.controller.mixin.ts.

RUDS logging (the “face detection tag”): every attempt → user_action_events (FACE_LOGIN_SUCCESS / FACE_LOGIN_DENIED, service auth) with a faceDetection metadata tag (context, modality, outcome, matchScore, reason, subjectDid/Ref) — detection rules / SOC dashboard / risk baselines see it like any auth action. Reusable builder shipped in security-kit (packages/security-kit/src/ruds/faceLogin.ts: buildFaceLoginEvent, emitUserActionEvent, FACE_LOGIN_ACTIONS) for the gateway/edge to emit consistently.

Verified: whole auth service compiles clean (tsc -p → 0 errors) with the new module — deploy-safe; security-kit helper typechecks. Flag OFF = inert even if deployed.

SECURITY — must clear before enabling (FACE_LOGIN_ENABLED=true):

  • Liveness / anti-spoofing NOT enforced server-side — a replayed embedding passes. Gate on a signed capture/liveness token first.
  • Embedding is client-computed → bounded trust.
  • reference_handle should be a cancelable transform / encrypted at rest.
  • Tune FACE_LOGIN_MIN_SCORE against the real model (measure FAR/FRR); needs staff enrollment to populate verification_grants (source enrollment, scope staff-login).
  • Apply migration 20260531a; security review of the auth path before deploy.

20. Liveness capture-challenge gate (2026-05-31)

Server-side anti-replay gate for face login (the #1 precondition from §19). New endpoint POST /auth/face/challenge (action auth.faceChallenge) issues a single-use, short-lived challenge { challengeId, nonce, gesture, expiresAt } (TTL FACE_LOGIN_CHALLENGE_TTL_SEC, default 120s; gesture ∈ turn-head-left/right, look-up/down, blink-twice). faceSignIn now requires challengeId (when FACE_LOGIN_REQUIRE_CHALLENGE !== 'false') and consumes it atomically (single UPDATE … WHERE consumed_at IS NULL AND expires_at > now) before verifying the face — so a challenge can’t be reused. Missing / expired / used → RUDS FACE_LOGIN_DENIED (reason challenge-*) + 400/401. New table bio_capture_challenges added to migration 20260531a (RLS service-role). Files: bioFaceLogin.helper.ts (issueCaptureChallenge / consumeCaptureChallenge / isChallengeRequired), auth.service.ts#faceChallenge + the gate in faceSignIn, dto/faceSignIn.dto.ts (FaceChallengeRequestDto + challengeId).

Flow: client POST /auth/face/challenge → performs the gesture (e.g. the head-rotation rig) → POST /auth/face/sign-in { challengeId, template }. Verified: whole auth service still compiles clean (0 errors).

This is anti-replay + freshness (weak active liveness via the gesture), NOT full anti-spoofing. A printed photo / replayed video performing the gesture still defeats it. True spoof-resistance needs a passive depth/texture liveness layer (or a trusted-enclave capture) — the remaining hard precondition before production enable.


21. Configurable liveness layer + UI-wiring status (2026-05-31)

web/packages/bio-identity-kit/src/liveness.tsLivenessProvider port + LivenessMode (off | gesture | passive) + providers: NoOpLivenessProvider, GestureLivenessProvider (weak active — gesture completed), OnnxLivenessProvider (passive anti-spoof via an injected onnx model, dep-free), and resolveLivenessProvider(mode, deps) (passive → gesture fallback when no model is wired). Plugs into EmbeddingCapture.assessLiveness / the engine’s requireLiveness. Verified (tsc + proof + tests). The sandbox FaceLogin demo now has a configurable liveness selector (off/gesture/passive) — smoke still green (success + deny + 0 errors).

Face-login HTTP contract the real login page will call: POST /v2/aaa/auth/face/challenge{ challengeId, gesture, expiresAt }; POST /v2/aaa/auth/face/sign-in { subjectRef, template, challengeId }{ token } (then store auth_token + getMe, exactly like login()).

⚠️ Production login-page wiring is BLOCKED on a config change I won’t make unsolicited. The kit (web/packages/bio-identity-kit) is NOT in web’s module resolution — the sandbox reaches it via a relative path + fs.allow, but web/src can’t import it without adding it to web’s pnpm workspace deps + a tsconfig.json path alias + a vite.config.ts alias. Those are the explicitly-guardrailed build-config files. To wire face login into the real LoginForm, grant permission to add the kit to web’s workspace + aliases (then it’s a flag-gated `` mount + the requestFaceChallenge/faceSignIn client). Until then, in-app face login is demo-only; the backend endpoint + liveness gate + kit are done + verified.


22. Production login-page wiring (2026-05-31)

With permission, the kit was added to web’s module resolution: @ever-medos/bio-identity-kit alias in vite.config.ts, tsconfig.json (+ /*), and the sandbox config (mirrors the existing @*-kit pattern). Then:

  • FaceLoginButton (web/src/common/components/system-administration/login/FaceLoginButton.tsx) — config-gated (VITE_FACE_LOGIN_ENABLED), self-contained, injected client + capture (no hard model dependency; renders a disabled “requires a configured face model” state when capture is null). Flow: requestChallenge → gesture → capture → faceSignIn. Sandbox-verified (?target=FaceLoginButton, mock client + capture → “Authenticated”, 0 console errors).
  • requestFaceChallenge / faceSignIn in user.service.ts — HTTP client mirroring login() (POST /v2/aaa/auth/face/challenge + /face/sign-in, store auth_token, getMe).
  • Mounted in LoginForm (after the Sign In button), flag-gated — default OFF, capture={null}, so the production login is unchanged until both VITE_FACE_LOGIN_ENABLED=true AND a real capture (onnxruntime + model) are wired.

Net: face login is now wired front-to-back and configurable (engine · persistence · endpoint · liveness gate · liveness providers · login UI). The only thing between here and a working production login is the real face model (onnxruntime-web + .onnx, your dep approval) feeding a real capture — plus the security preconditions (server-side passive liveness, staff enrollment, migration applied, security review).


23. Model wiring — code-complete (2026-05-31)

web/src/services/ever-aaa/faceCapture.tsgetFaceCapture() builds BrowserCameraSource + OnnxFaceEmbedder (+ optional OnnxLivenessProvider) from a model URL, via a variable-specifier dynamic import of onnxruntime-web so the build never hard-requires it. Returns null (→ disabled button) until configured. onnxruntime-web declared in web/package.json. FaceLoginButton now takes a lazy getCapture (model loads on first click); LoginForm passes getFaceCapture (flag-gated). Re-verified: sandbox smoke green, 0 errors.

The feature is now code-complete end-to-end (engine · persistence · /auth/face/* endpoint · liveness challenge gate · configurable liveness providers · login UI · model-loading capture factory), all flag-gated OFF by default.

To turn it on (all external / operator steps — no more code):

  1. pnpm install (onnxruntime-web is declared).
  2. Drop a face-recognition model (e.g. web/public/models/mobilefacenet.onnx); set VITE_FACE_MODEL_URL (+ optional VITE_FACE_LIVENESS_MODEL_URL + tensor-name envs).
  3. Apply migration 20260531a; enroll staff faces (populate verification_grants, source enrollment).
  4. Backend FACE_LOGIN_ENABLED=true (+ FACE_LOGIN_REQUIRE_CHALLENGE, FACE_LOGIN_MIN_SCORE); frontend VITE_FACE_LOGIN_ENABLED=true.
  5. Security review (server-side passive liveness, FAR/FRR threshold tuning, cancelable templates) + a real browser/camera test.

Nothing is committed/pushed.

Ask Anything