feat(rayverify): identity capture flow on mobile, and fix the body limit - #195
Merged
Conversation
Two things, because the second makes the first possible. The identity routes accept a base64 selfie, but the app-wide JSON body cap is 100KB, which no photo can fit. Every capture would have returned 413 before reaching the route: the feature shipped dead on arrival and no test caught it because they all posted tiny fixtures. Identity now gets its own 3MB parser mounted ahead of the global one, so the larger limit applies to that path alone rather than raising the ceiling for the whole app, and the decoded cap drops from 4MB to 2MB, which is far more than face matching needs. There is now a test that posts a realistically sized payload. The mobile flow is consent, then enrol a reference photo, then check against it, reachable from the Me tab. Order is enforced server-side too; the screen just makes it legible. Captures are compressed hard on purpose: matching needs resolution around the face, not a printable image, and a small payload uploads far more reliably from a phone in somebody's car. Oversized captures are caught on the device so a caregiver sees a retake prompt instead of a 413. Two things the screen says out loud rather than burying. A failed match never implies fraud: the likeliest cause is lighting or an angle, and an app is the wrong place to make that call. And because liveness is not built, the screen states that the check does not detect a photo of a screen or a printed picture. Withdrawing consent is one tap from the same screen, and a failed deletion says so rather than claiming the photo is gone. expo-camera runs in Expo Go on SDK 54, so this is testable on a phone without a custom native build. Liveness is what would have needed one.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug first, because it makes the rest possible
The identity routes accept a base64 selfie, but the app-wide JSON body cap is 100KB, which no photo can fit. Every capture would have returned 413 before reaching the route. #193 shipped dead on arrival, and none of its 23 tests caught it because they all post tiny fixtures.
Fix: identity gets its own 3MB parser mounted ahead of the global one (same pattern the Stripe webhook already uses), so the larger limit applies to that path alone rather than raising the ceiling for every route in the app. The decoded cap drops from 4MB to 2MB, which is still far more than face matching needs. There is now a test that posts a realistically sized payload, which is the test that would have caught it.
The capture flow
Consent → enrol a reference photo → check against it. Reachable from Me → Identity check. The order is enforced server-side too; the screen just makes it legible.
Captures are compressed hard (quality 0.5) on purpose: matching needs resolution around the face, not a printable image, and a small payload uploads far more reliably from a phone in somebody's car. Oversized captures are caught on the device so a caregiver sees a retake prompt rather than a confusing 413.
Two things the screen says out loud
A failed match never implies fraud. The likeliest cause is lighting or an angle, and an app is the wrong place to make that call. There's a test asserting the copy contains no accusatory wording.
Liveness is not built, and the screen says so: "does not yet detect whether a photo was taken of a screen or a printed picture". The status endpoint already reported
livenessSupported: false; now a human can see it too.Withdrawing consent is one tap from the same screen, and a failed deletion says so rather than claiming the photo is gone.
Expo Go
expo-camera(~17.0.10, SDK 54) runs in Expo Go, so this is testable on a phone without a custom native build. Liveness is the part that would have needed one.Tests
18 new: step derivation including consent-always-first, outcome copy for every branch, unconfigured never presented as a result, upload size guard with headroom under the server's parser limit, and the two body-limit regression tests. All four suites, typecheck, lint,
sql:scan,security:scanpass.Still needed before it works end to end
Owner-side, unchanged from before:
IDENTITY_VERIFICATION_PROVIDER=rekognitionandDOCUMENTS_S3_BUCKETin Vercel prod, AWS IAM withrekognition:CompareFacespluss3:DeleteObject, and an AWS BAA covering Rekognition. Migrations are already applied.Until the provider is set, the flow still works: consent and enrolment succeed, and a check returns
not_configured, which the screen presents as "not switched on, nothing was checked" rather than as a pass or a failure.