# Face Local SDK [Documentação em português (Brasil)](../../README.md) A reusable **React Native** SDK with a UI-independent TypeScript core, a Kotlin/Swift Expo module and a photo-library adapter. **Face Local** combines liveness, automatic selfie discovery, Face Match and a structured result in one SDK. It is intended for integration into your applications; it is not published on npm. `private: true` prevents accidental publication and **does not prevent** installation from a local package. ## Get the source code Get the package at [facelocal.dev/en/download-sdk](https://facelocal.dev/en/download-sdk) with a **one-time US$999 Stripe payment** or a valid coupon. After confirmation, download the `.tgz` archive and save your private recovery code to restore download access in another browser. Documentation and the demo APK remain public. Payment covers source-code access: there is no subscription, per-verification fee or online execution key for the SDK in your app. Own-brand use is allowed under the license; reselling the SDK is not permitted. ## What it verifies — and what it does not 1. The engine receives camera frames and performs passive liveness plus movements. 2. **The image from that same approved capture** creates the facial reference in memory. There is no API for turning a gallery selfie into an approved reference. 3. The onboarding flow finds selfies and automatically compares authorized local photos. 4. It returns a positive result when it finds a match, or a negative result after a completed search without a match. Failures, a lack of eligible photos and incomplete searches return an error or an inconclusive result, never an approval or a false negative. ## JSON result with images and decision criteria (0.5+) To integrate the result into another app, use **`runFaceLocalVerification`**, exported from `@workspace/face-local-sdk/expo`. It runs the automatic onboarding flow and returns `VerificationResult`, with `schemaVersion: "1.0"`: - `status`, `approved` and `reason`: approved, not approved, inconclusive, cancelled or error; - `photos.liveness` and `photos.compared`: temporary local copies of the images actually used, never a new selfie or a file selected as the reference; - `faceMatch`: original similarity, applied threshold and a 0–100 display index — **not accuracy or the probability of identity**; - `exif`: Make, Model and DateTimeOriginal read from the same bytes analyzed natively; - `deviceFingerprint`: the model profile used for capture, **not a unique identifier**; - `decision`: engine, liveness, requirements, counts and exclusion reasons. The compared image is the first one that meets the threshold; without a match, it is the best completed comparison, if any. No photos are returned on cancellation or error. Copies require explicit cleanup, expire and never modify the gallery. **Complete documentation:** 1. [Integrate, cancel and release images](INTEGRATION.md). 2. [JSON contract, field definitions and limitations](JSON-RESULT.md). The previous `runFaceLocalOnboarding` flow remains available, without image export. Updating JavaScript alone is not enough for the new API: **rebuild the native app**. Expo Go and browsers do not perform biometrics or simulate approval. ## Structure - Main import: types, policy, `FaceLocalVerifier`, `runVerification`, EXIF validation. - `/expo`: `createExpoEngine`, `photoPermission`, `managePhotoSelection`, `readLocalPhotos`, `runFaceLocalVerification`, `releaseFaceLocalVerification`. - `android/`: Face Local Android implementation, with CameraX, Expo bridge, liveness and local comparison. - `ios/`: Face Local iOS implementation, with AVFoundation/SwiftUI, Expo bridge, ImageIO and Vision. - `app.plugin.js`: permissions, Android backups and Android Release network isolation. - `tests/`: deterministic flow tests. Test doubles are not included in the package. Do not copy the app screens to reuse the SDK. The Android implementation initializes MMKV for native configuration, but does not register embeddings or images in it. Native-library storage behavior still needs to be checked on a physical device. ## Target compatibility | Component | Compatibility | |---|---| | Expo | SDK 57 | | React Native | 0.86 | | Android | API 26+; physical ARM device | | iOS | 16.4+; physical iPhone | | Face Local for Android | Kotlin, CameraX and Expo Modules | | Face Local for iOS | Swift, AVFoundation and Expo Modules | The SDK depends on **Expo Modules**, not Expo Go. A React Native project without Expo can install Expo Modules, follow the native configuration and use the same adapter. Other environments and versions require adaptation and validation; they are not automatically declared compatible. ## Move it into another project From this workspace's root: ```sh pnpm --filter @workspace/face-local-sdk build pnpm --filter @workspace/face-local-sdk pack --pack-destination ../../generated-artifacts ``` Copy the resulting `.tgz` into your other project. Install it from its local path: ```sh pnpm add ./face-local-sdk-0.6.0.tgz pnpm exec expo install expo-modules-core expo-media-library expo-image-picker ``` The import name remains `@workspace/face-local-sdk`; no private registry is needed. ### Model names in EXIF and Android Android's system model code and a photo's commercial model name may differ. Only **explicitly verified equivalences** are accepted, not approximate matches. The verified mapping includes Xiaomi `24117RN76E`, `24117RN76G`, `24117RN76L` and `24117RN76O`, device `tanzanite`, for Redmi Note 14, according to Google's [supported devices list](https://storage.googleapis.com/play_public/supported_devices.csv). This is not a universal equivalence for 5G, Pro or unknown regional variants. `CaptureDevice.model` contains the resolved model; `hardwareModel` preserves the system code when applicable. Validation still reads the native image bytes. This identifies a model profile, not an individual device or authentic EXIF. ### Diagnostics without photos `ScanReport.device` and `summarizeVerification` expose only the model/system code, counts and missing-field diagnostics. They do not include photos, filenames, URIs, dates, GPS, sessions, embeddings or scores. The helpers neither store nor transmit data. Any diagnostic sharing must be explicitly confirmed by the user, for an inconclusive result only; never share the full verification JSON as a diagnostic log. Configure the plugins in `app.json`: ```json { "expo": { "plugins": [ ["expo-media-library", { "granularPermissions": ["photo"], "isAccessMediaLocationEnabled": false, "photosPermission": "Allow access to all photos to find your face on this device." }], ["expo-image-picker", { "cameraPermission": "Use the camera for on-device liveness.", "microphonePermission": false }], "@workspace/face-local-sdk" ] } } ``` **Important:** the plugin disables Android backups and removes `INTERNET` from the **entire Android Release app**, not just the biometric module. Review this policy before integrating into an app that needs a network connection. Keeping internet access requires a configuration review and a native-library audit; do not silently remove the protection. On iOS, ATS does not block all HTTPS traffic. ```sh pnpm exec expo prebuild # Physical Android device: pnpm exec expo run:android --variant release --device # Physical iPhone, on a Mac with Xcode and CocoaPods: pnpm exec expo run:ios --configuration Release --device ``` Do not run `prebuild --clean` over manual native changes without reviewing and backing them up first. ## Usage API Since 0.3, the native capabilities and capture identify the exact manufacturer and model, not a user-defined name, UUID or IDFV. Native code rechecks the EXIF before inference. An unknown model is rejected. ```ts import { FaceLocalVerifier, requireFullPhotoAccess, type Consent } from "@workspace/face-local-sdk"; import { createExpoEngine, photoPermission, readLocalPhotos, } from "@workspace/face-local-sdk/expo"; import { requestCameraPermissionsAsync } from "expo-image-picker"; const engine = createExpoEngine(); const verifier = new FaceLocalVerifier(engine, { matchThreshold: 0.83, requireCaptureMetadata: true, maxPhotos: 100, captureTimeoutSeconds: 15, }); if (!verifier.capabilities.available) { throw new Error(verifier.capabilities.reason); } // Initial consent explains using the face to find photos on the device. // Start with photoLibrary: false; authorize photos only after the OS responds. const consent: Consent = consentFromYourUI; if (!consent.camera || !consent.biometricProcessing) { throw new Error("Consent is required."); } const camera = await requestCameraPermissionsAsync(); if (!camera.granted) throw new Error("Camera permission is required."); try { const receipt = await verifier.capture(consent); // receipt is opaque session metadata. It contains no image or embedding. const libraryPermission = await photoPermission(true); requireFullPhotoAccess(libraryPermission); verifier.authorizePhotoLibrary({ photoLibrary: libraryPermission.granted, biometricProcessing: consent.biometricProcessing, version: consent.version, }); const library = await readLocalPhotos({ limit: 100, signal: abortController.signal, onProgress: count => updateLibraryProgress(count), }); // Automatic comparison. Never use a library photo as the reference. const report = await verifier.scan(library.photos, progress => { updateProgress(progress.processed, progress.total); }); showReportInYourUI(report); } finally { await verifier.revokeConsent(); } ``` `consentFromYourUI`, `abortController`, the progress functions and `showReportInYourUI` belong to the integrating app; they are not SDK exports. ### Automatic onboarding without image export (previous API) ```ts import { runFaceLocalOnboarding } from "@workspace/face-local-sdk/expo"; import { requestCameraPermissionsAsync } from "expo-image-picker"; const result = await runFaceLocalOnboarding({ // Call only after visible consent to use the camera and face for comparison. consent: { camera: true, biometricProcessing: true, photoLibrary: false, version: "1" }, signal: abortController.signal, policy: { maxPhotos: 100, matchThreshold: 0.83, requireCaptureMetadata: true }, requestCameraAccess: async () => { // Permission only; this does not open a still camera or an image picker. if (!(await requestCameraPermissionsAsync()).granted) throw new Error("Camera not authorized."); }, onPhase: phase => updatePhaseInYourUI(phase), onSearchProgress: count => updateLibraryProgress(count), onProgress: progress => updateProgress(progress.processed, progress.total), }); // result.outcome: "positive" | "negative" | "inconclusive" // result.reason explains why; result.report contains the actual comparisons. ``` `updatePhaseInYourUI` also belongs to the integrating UI. The sequence is camera permission → liveness → photo permission → discovery → Face Match. There is no manual image selection. The operating system owns the limited-access option; the app cannot remove or ignore it. Face Local requires full access. With limited access, it asks the person to allow all photos in Settings and ends the session without searching or comparing images. Do not add search, compare or start-confirmation buttons after capture. The Expo adapter uses the actual photo-permission response to continue on its own. In the independent core, `requestPhotoAccess` must return `{ granted, accessPrivileges: "all" | "limited" | "none", canAskAgain? }`. Since 0.4, `granted: true` without an explicit scope and the old `void` return do not authorize discovery, even with initial photo consent. Denial, cancellation or expiry prevents subsequent reads; a permission response arriving after cancellation is ignored. On iPhone, the Selfies collection is identified by its PhotoKit subtype, independently of language. Album names provide additional clues, especially on Android. Discovery prioritizes those collections, then searches the remaining authorized photos without duplicates. A single-face image is a candidate; that does not prove it came from the front camera. No infallible origin classifier is claimed. The limit is at most 1,000 photos per attempt, with a five-minute reference lifetime. The search stops at the first match. Reaching the limit while photos remain unexamined is inconclusive, not a negative result. Unavailable cloud photos, comparison failures and missing EXIF never produce synthetic scores. A negative result concerns only the eligible, authorized local photos examined. ### Local capture protection - Only the native camera creates the reference. The bridge rejects additional input options; the legacy capture accepts a timeout, while the evidence API additionally accepts its TTL and threshold, never an external reference. - Frames must have fresh, increasing timestamps, come from the built-in front camera and pass a single-use sequence gate. - The iOS AVFoundation relay fails closed. - Debug builds, simulators, detected root/jailbreak, debuggers and known hooks are blocked. Use a Release build. - Android uses a secure window, rejects obscured touches and uses overlay protection on Android 12+. iOS cancels on screen recording or loss of focus. - A security abort ends the entire attempt; there is no partial approval. The receipt's `source: "native-camera"` is **not cryptographic attestation**. Do not trust a hostile client simply because it returns this field to a server. Local checks do not guarantee immunity to unknown root techniques, HAL/driver manipulation or deepfakes. There is no external Play Integrity/App Attest attestation, production-signing assurance, complete audit or attack-resistance certification. The test APK uses a test signature. The iOS implementation has been tested on an iPhone. When integrating the SDK or changing native capture, validate the flow in your app, especially the camera relay. Refer to the project's threat model and physical-device test protocol. ### Cancellation and lifecycle - Use an `AbortController` for an in-progress photo scan. - Await `verifier.clear()`; it waits for earlier callbacks to finish. - Call `revokeConsent` when leaving the flow. - There is one native session per process. - Android's capture Activity is not the same as leaving the app. iOS clears the session when the app moves into the background. - Never put photos, EXIF, receipts or embeddings in AsyncStorage, logs or a server. The JSON API explicitly exports up to two JPEGs into private cache for 30–300 seconds; templates remain in native memory. See the [integration and cleanup guide](INTEGRATION.md). ### Results | Result | Meaning | |---|---| | `match` | One face and similarity at or above the threshold | | `different` | One face and similarity below the threshold | | `inconclusive` | Zero or multiple faces; not a non-match | | `skipped` | Cloud photo, forbidden URI or incomplete EXIF under the policy | | `error` | A file or result could not be processed | The score is not a probability or certainty about identity. `report.nativeAttempts` counts native calls, including failures and no-face results. `report.compared` counts only valid single-face comparisons. Photos excluded by metadata do not count as comparisons. `unreadable-photos` and `comparison-failed` are distinct inconclusive reasons, never substitutes for a negative result. The default threshold, `0.83`, has not been calibrated for your use case. The reference is cleared on a scan error. Cancellation and expiry never turn partial processing into a completed result. ### Library and EXIF - Full photo permission is required; denial and revocation are respected. - On iOS, `getAssetInfoAsync` uses `shouldDownloadFromNetwork: false`. - On Android, enumeration does not use `resolveWithFullInfo`; native code reads Make, Model and DateTimeOriginal from MediaStore. It does not depend on `getAssetInfoAsync`, which can require `ACCESS_MEDIA_LOCATION` for full EXIF and report an error even when full photo access was granted. - No location permission, `MANAGE_EXTERNAL_STORAGE` or cloud transfer is used for the filter. - Permission scope is checked after discovery and during native processing. Revocation aborts the attempt and clears the reference. - There is no `ph://` or remote fallback. Android accepts only local MediaStore content, not remote providers. - GPS and other metadata are discarded. Make, Model and DateTimeOriginal are all required. - Manufacturer/model matching normalizes only whitespace and case. It does not accept family names, prefixes, Pro/Max variants or approximate generations. - Android uses `Build.MANUFACTURER` and `Build.MODEL`, with explicitly verified aliases. iOS resolves hardware locally through DeviceKit 5.9.0. Unknown models have no generic fallback. - The capture device is checked against capabilities, and native code rechecks EXIF from the same bytes used for inference, not just JavaScript metadata. Files over 32 MiB are rejected. - `requireCaptureMetadata: false` is invalid; there is no UI switch to disable the filter. Unknown naming differences can exclude legitimate photos; equivalence must not be assumed. - This is **not a unique fingerprint**. Other physical devices share the same model, and EXIF can be edited. It does not prove origin, ownership or identity. - Large libraries are processed in batches with an explicit limit. - Edited or shared copies may lose metadata. This does not indicate fraud. ## Replace the engine Implement `BiometricEngine` with `capabilities`, `captureReference`, `compare` and `clear`. Capture must combine passive and active liveness, with a single face, a reference tied to the approved capture and real native expiry. Do not fabricate scores or compare embedding strings generically. The current adapter calls `evaluateFaceSimiByFeature` on Android and `evaluateSimilarity(f1:f2:)` on iPhone. Native-library internal formats are opaque. To support the JSON API with images, also implement `EvidenceEngine`, validate EXIF from the same inference bytes, and produce copies from the already analyzed images instead of reopening the gallery. Do not advertise `evidenceVersion: 1` without session binding, expiry, copy hashes and verifiable cleanup. ## License and verification You may integrate Face Local into applications under your own brand (white-label). **Selling or reselling the SDK is not permitted**, including modified, repackaged or renamed versions. This restriction does not prevent you from selling your application that uses the SDK. See the [Face Local license](LICENSE.md), included in the package. The new terms do not change permissions for previously distributed versions. There is no anti-spoofing, forensic-evidence or legal-identity certification. Run `pnpm --filter @workspace/face-local-sdk test`. See `docs/TESTES-EM-APARELHO.md` in the workspace for the required physical-device tests.