Skip to content

feat(face): SP-A client-side embedding — production-ready + GPU-less-enforced#222

Merged
ahmetabdullahgultekin merged 4 commits into
mainfrom
feat/sp-a-clientembed-prod
Jun 12, 2026
Merged

feat(face): SP-A client-side embedding — production-ready + GPU-less-enforced#222
ahmetabdullahgultekin merged 4 commits into
mainfrom
feat/sp-a-clientembed-prod

Conversation

@ahmetabdullahgultekin

Copy link
Copy Markdown
Contributor

Summary

Makes the client-side FACE embedding (SP-A) production-ready and GPU-less-enforced. Live prod state: identity flag `app.auth.client-side-embedding=true` (server now 400s the legacy image path) + web flag `VITE_CLIENT_SIDE_EMBEDDING=true`, and the real 47 MB Facenet512 FP16 model is hosted at `app.fivucsas.com/models/facenet512-1ad91552.fp16.onnx`. The CPU-only server must not do image ML, so the embedding must be computed client-side; the old silent image fallback now just fails.

Four cohesive changes, one per commit:

1. Remove the silent image fallback (GPU-less enforcement) — `3baf8cb`

In `MfaStepRenderer.tsx` FACE case, when the flag is ON and `embedCapturedFace` returns null we no longer fall back to `verifyStep(FACE, { image })` (an upload the server now rejects). Instead we surface a clear, retryable error (`mfa.face.clientPrepFailed`, EN+TR) via `onError` — which renders in FaceCaptureStep's existing error Alert — and return without sending the image. The captured frame stays so the user can re-submit (the model cache self-heals + the prefetch re-warms). The legacy `{ image }` path is kept only when the flag is OFF.

2. Prefetch the model (no frozen 47 MB-on-submit) — `038504f`

New `prefetchFacenetModel()` + `scheduleFacenetPrefetch()` in the embedding feature: flag-gated (no-op when OFF), de-duped (concurrent/repeat calls share one in-flight download), never-throws. Wired fire-and-forget via `requestIdleCallback` (fallback `setTimeout`) on the mount of LoginPage (dashboard) AND HostedLoginApp (hosted) AND FaceCaptureStep, with effect-cleanup cancelling a still-pending idle schedule. With the flag ON the ~47 MB model downloads on idle while the user types, so a later FACE step is a cache hit.

3. Progress indicator during embed — `bf96f7d`

`FaceCaptureStep` now `await`s `onSubmit` (type widened to `void | Promise`) and toggles a distinct `preparing` state around it — separate from the verify `loading` — rendering a spinner + the `mfa.face.preparingSecure` status line (role=status, aria-live=polite) on the submit button while the model download + ONNX init + inference run. On mobile the user sees it working, not frozen.

4. Add facenet to `public/models/manifest.json` — `fe65ba9`

Now safe (the model is hosted at the manifest's `base_url_default`), so `prebuild`→`fetch-models` fetches + SHA256-verifies + bundles it into `dist/models` → persistence (a manual upload alone is wiped by the deploy rsync) + fan-out to the verify build. Name/sha256/bytes match the `facenetEmbedder.ts` constants and the live file.

Flag-OFF is byte-identical

With `VITE_CLIENT_SIDE_EMBEDDING` OFF: `scheduleFacenetPrefetch()` returns a no-op immediately (no schedule, no download); the MfaStepRenderer FACE case goes straight to the unchanged `verifyStep(FACE, { image, ...advisory })` line; `preparing` flips within one tick (no flicker). The only flag-OFF-reachable FACE line is unchanged vs origin/main.

Verification

  • `npm test` (vitest): 1218 passed | 25 skipped, 0 failed (123 files). Updated the obsolete fallback test to assert the new enforcement (onError + no verifyStep); added a prefetch unit test (flag-gating, single canonical-URL+SHA fetch, de-dup, never-rejects+retry, requestIdleCallback-vs-setTimeout + cleanup).
  • `SKIP_MODEL_FETCH=1 npm run build`: passes, tsc clean.
  • `npm run fetch-models` against the live host: facenet512 downloaded + SHA256-verified (`failed=0`) — proves the deploy prebuild won't FATAL.
  • `eslint` on all changed files: clean.

Merging auto-deploys app.fivucsas; verify.fivucsas to be rebuilt by the coordinator after.

…istence)

The 47 MB Facenet512 FP16 model is now hosted at the manifest's
base_url_default (app.fivucsas.com/models). Adding it to manifest.json
makes prebuild->fetch-models fetch + SHA256-verify + bundle it into
dist/models so the deploy rsync can't wipe a manual upload, and it
fans out to the verify build. Matches DEFAULT_FACENET_MODEL_URL /
DEFAULT_FACENET_MODEL_SHA256 in facenetEmbedder.ts.
…N (GPU-less enforcement)

The CPU-only server now EXPECTS the embedding and 400s the legacy image
path while app.auth.client-side-embedding is ON. Previously a null
embedding (no model / ORT load fail / inference error) silently fell back
to verifyStep(FACE, { image }) — an upload the server now rejects, so the
login failed with an opaque error.

Now, when the flag is ON and embedCapturedFace returns null, surface a
clear retryable error (mfa.face.clientPrepFailed, EN+TR) via onError so it
renders in FaceCaptureStep's existing error Alert, and return WITHOUT
sending the image. The captured frame stays so the user can re-submit.

Flag OFF is unchanged: byte-identical legacy { image } upload path. Also
adds the mfa.face.preparingSecure key (EN+TR) consumed by the next commit.
…en 47 MB-on-submit)

Adds prefetchFacenetModel() + scheduleFacenetPrefetch() in the embedding
feature: flag-gated (no-op when VITE_CLIENT_SIDE_EMBEDDING is OFF),
de-duped (concurrent/repeat calls share one in-flight download), and
never-throws (a failed warm-up is harmless — embedCapturedFace retries on
submit + GPU-less-enforcement surfaces a real failure there).

Wired fire-and-forget via requestIdleCallback (fallback setTimeout) on
LoginPage (dashboard) and HostedLoginApp (hosted) mount, with effect-
cleanup cancelling a still-pending idle schedule. So with the flag ON the
~47 MB model downloads on idle while the user types their identifier, and
a later FACE step is a cache hit instead of a frozen on-submit download.
Flag OFF: no schedule, no download — surfaces byte-identical to before.

Unit-tested: flag-gating, single canonical-URL+SHA fetch, de-dup,
never-rejects+retry, requestIdleCallback-vs-setTimeout + cleanup.
The FACE submit awaits embedCapturedFace (model download on first use +
ONNX init + inference) BEFORE the verify call, with no feedback — on
mobile the multi-second embed looked frozen.

Adds a distinct `preparing` state (separate from the verify `loading`):
handleSubmit now awaits onSubmit (type widened to void|Promise<void>) and
toggles `preparing` around it, disabling both buttons and rendering a
spinner + the i18n mfa.face.preparingSecure status line (role=status,
aria-live=polite) on the submit button. Also schedules the flag-gated
Facenet prefetch on FACE-step mount so the model is warm by capture time.

Flag OFF: onSubmit returns synchronously, `preparing` flips within one
tick (no visible flicker), and the prefetch is a no-op — unchanged UX.
@ahmetabdullahgultekin ahmetabdullahgultekin merged commit fe18da4 into main Jun 12, 2026
4 checks passed
@ahmetabdullahgultekin ahmetabdullahgultekin deleted the feat/sp-a-clientembed-prod branch June 12, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant