# Face Local SDK [English documentation](docs/en/README.md) SDK reutilizável para **React Native**, com núcleo TypeScript independente da interface, módulo Expo em Kotlin/Swift e adaptador de biblioteca de fotos. O **Face Local** reúne prova de vida, busca automática de selfies, Face Match e resultado estruturado em um único SDK. Destina-se a integração nos apps do usuário; não está publicado no npm. `private: true` evita publicação acidental e **não impede** instalação por pacote local. ## Obter o código-fonte O pacote pode ser obtido em [facelocal.dev/baixar-sdk](https://facelocal.dev/baixar-sdk) com **pagamento único de US$ 999 pelo Stripe** ou usando um cupom válido. Após a confirmação, baixe o arquivo `.tgz` e guarde o código de recuperação privado para acessar o download novamente em outro navegador. A documentação e o APK de demonstração continuam públicos. A cobrança é pelo acesso ao código-fonte: não há assinatura, cobrança por verificação ou chave online para executar o SDK no aplicativo. O uso com marca própria é permitido, conforme a licença; a revenda do SDK não é permitida. ## O que ele verifica — e o que não verifica 1. O motor recebe frames da câmera e executa prova de vida passiva + movimentos. 2. **A imagem da mesma captura aprovada** gera a referência facial em memória. Não há API para promover uma selfie da galeria a referência aprovada. 3. O onboarding busca selfies e compara automaticamente as fotos locais autorizadas. 4. Retorna positivo ao encontrar um match; negativo após uma busca concluída sem match. Falhas, falta de fotos elegíveis e busca incompleta retornam erro ou inconclusivo, nunca uma aprovação ou um falso negativo. ## Retorno JSON com imagens e critérios da decisão (0.5+) Para integrar o resultado em outro app, use **`runFaceLocalVerification`**, exportado por `@workspace/face-local-sdk/expo`. Ele executa o onboarding automático e retorna `VerificationResult`, com `schemaVersion: "1.0"`: - `status`, `approved` e `reason`: aprovação, reprovação, inconclusivo, cancelamento ou erro; - `photos.liveness` e `photos.compared`: cópias locais temporárias das imagens efetivamente usadas, nunca uma nova selfie ou um arquivo escolhido como referência; - `faceMatch`: similaridade original, limiar aplicado e índice de 0 a 100 — **não acurácia nem probabilidade de identidade**; - `exif`: Make, Model e DateTimeOriginal lidos dos mesmos bytes analisados pelo nativo; - `deviceFingerprint`: perfil do modelo usado na captura, **não identificador único**; - `decision`: motor, prova de vida, requisitos, contagens e motivos de exclusão. A imagem comparada é a primeira que atingiu o limiar; sem match, é a melhor comparação concluída, se existir. Em cancelamento ou erro, nenhuma foto é entregue. As cópias exigem descarte explícito, possuem expiração e nunca alteram a galeria. **Documentação completa:** 1. [Integrar, cancelar e descartar as imagens](docs/INTEGRACAO.md). 2. [Contrato JSON, significado de todos os campos e limitações](docs/RESULTADO-JSON.md). O fluxo antigo `runFaceLocalOnboarding` continua disponível, sem exportação de imagens. Atualizar apenas o JavaScript não basta para a nova API: **recompile o aplicativo nativo**. Expo Go e navegador não executam biometria nem simulam aprovação. ## Estrutura - Importação principal: tipos, política, `FaceLocalVerifier`, `runVerification`, validação de EXIF. - `/expo`: `createExpoEngine`, `photoPermission`, `managePhotoSelection`, `readLocalPhotos`, `runFaceLocalVerification`, `releaseFaceLocalVerification`. - `android/`: implementação Android do Face Local, com CameraX, ponte Expo, prova de vida e comparação local. - `ios/`: implementação iOS do Face Local, com AVFoundation/SwiftUI, ponte Expo, ImageIO e Vision. - `app.plugin.js`: permissões, backups Android e isolamento de rede no release Android. - `tests/`: testes determinísticos do fluxo. Os dublês de teste não entram no pacote. Não copie as telas do app para reutilizar o SDK. A implementação Android inicializa MMKV para a configuração nativa, mas não cadastra embeddings nem imagens nele. O comportamento de armazenamento das bibliotecas nativas ainda deve ser verificado em aparelho. ## Compatibilidade-alvo | Componente | Compatibilidade | |---|---| | Expo | SDK 57 | | React Native | 0.86 | | Android | API 26+; aparelho físico ARM | | iOS | 16.4+; iPhone físico | | Face Local para Android | Kotlin, CameraX e Expo Modules | | Face Local para iOS | Swift, AVFoundation e Expo Modules | O SDK depende de **Expo Modules**, não do Expo Go. Um projeto React Native sem Expo pode instalar Expo Modules, seguir a configuração nativa e usar o mesmo adaptador. Outros ambientes/versões exigem adaptação e validação; não são declarados compatíveis automaticamente. ## Levar para outro projeto Na raiz deste workspace: ```sh pnpm --filter @workspace/face-local-sdk build pnpm --filter @workspace/face-local-sdk pack --pack-destination ../../generated-artifacts ``` Copie o `.tgz` resultante para seu outro projeto. Instale-o por caminho local: ```sh pnpm add ./face-local-sdk-0.6.0.tgz pnpm exec expo install expo-modules-core expo-media-library expo-image-picker ``` O nome de importação continua `@workspace/face-local-sdk`; não é necessário um registro privado. Alternativamente, mantenha o pacote como workspace compartilhado. ### Nomes de modelo no EXIF e no Android O nome comercial escrito pela câmera pode diferir de `Build.MODEL`. O módulo nativo resolve somente equivalências explícitas verificadas; não usa o nome editável do celular nem aprende o modelo do aparelho a partir da galeria. O catálogo inicial cobre os códigos completos `24117RN76E`, `24117RN76G`, `24117RN76L` e `24117RN76O`, fabricante Xiaomi e dispositivo `tanzanite`, como **Redmi Note 14**, conforme a [lista oficial de aparelhos do Google](https://storage.googleapis.com/play_public/supported_devices.csv). Não é um catálogo universal de todos os Androids. Outros códigos mantêm a comparação literal; 5G, Pro, Pro+ e códigos regionais desconhecidos não são aceitos por semelhança. Quando há uma equivalência, `CaptureDevice.model` contém o nome resolvido e `hardwareModel` preserva o código real do sistema. Ambos permanecem vinculados à referência de liveness. O filtro aceita o nome verificado ou o código exato daquele aparelho, nunca o código de outra variante. A leitura nativa revalida essa regra nos bytes usados para a comparação. Nenhum desses campos identifica uma unidade física nem autentica o EXIF. ### Diagnóstico sem fotos `ScanReport.device` informa o modelo usado na captura. `summarizeVerification(result)` produz somente modelo/código do aparelho, contagens, motivos de descarte e quantidades de campos ausentes. Não inclui fotos, nomes de arquivos, URIs, datas EXIF, localização, sessão, embeddings ou scores. O helper não grava nem transmite nada. O app oferece compartilhamento opcional, com confirmação e escolha de destino pelo usuário, apenas em resultados inconclusivos. Nunca use `JSON.stringify(result)` para esse fim: o resultado completo contém informações das fotos locais. Acrescente a configuração ao **app.json** do app integrador, preservando seus plugins existentes: ```json { "expo": { "plugins": [ ["expo-media-library", { "granularPermissions": ["photo"], "isAccessMediaLocationEnabled": false, "photosPermission": "Permita acesso a todas as fotos para encontrar seu rosto neste aparelho." }], ["expo-image-picker", { "cameraPermission": "Usar a câmera para prova de vida local.", "microphonePermission": false }], "@workspace/face-local-sdk" ] } } ``` **Atenção:** o plugin desabilita backup do app Android e remove a permissão `INTERNET` do **release inteiro**, não apenas do motor biométrico. Isso é adequado ao app de testes offline. Se seu outro app depende de rede, avalie essa política antes de integrá-lo: manter rede exige auditoria do motor e não permite prometer isolamento de rede pelo sistema operacional. Não remova a proteção silenciosamente. No iPhone não existe uma permissão geral equivalente a `INTERNET` do Android: ATS limita transportes inseguros, mas não bloqueia todo HTTPS. Gere e compile os projetos nativos após instalar: ```sh pnpm exec expo prebuild # Android físico: pnpm exec expo run:android --variant release --device # iPhone físico, em um Mac com Xcode e CocoaPods: pnpm exec expo run:ios --configuration Release --device ``` Não execute `prebuild --clean` em um app existente com alterações nativas manuais sem antes revisá-las e salvá-las. ## API de uso Na versão 0.3, um motor deve informar fabricante/modelo exatos em `capabilities.currentDevice` e no `device` do recibo de captura. Não substitua esses campos por nome personalizado, marca genérica, valor digitado pelo usuário, IDFV ou UUID. Adaptações de outros motores também devem reler o EXIF nativamente antes de inferir sobre cada imagem. Modelos desconhecidos são recusados. ```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); } // O aceite inicial explica o uso do rosto para encontrar as fotos no aparelho. // Comece com photoLibrary: false; só autorize fotos depois do retorno real do SO. const consent: Consent = consentFromYourUI; if (!consent.camera || !consent.biometricProcessing) { throw new Error("Consentimento necessário."); } const camera = await requestCameraPermissionsAsync(); if (!camera.granted) throw new Error("A câmera precisa ser autorizada."); try { const receipt = await verifier.capture(consent); // receipt é metadado de uma sessão opaca. Não contém imagem nem 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), }); // Comparação automática. Nunca usar uma foto da biblioteca como referência. const report = await verifier.scan(library.photos, progress => { updateProgress(progress.processed, progress.total); }); showReportInYourUI(report); } finally { await verifier.revokeConsent(); } ``` `consentFromYourUI`, `abortController`, `updateLibraryProgress`, `updateProgress` e `showReportInYourUI` representam responsabilidades da interface integradora, não funções exportadas pelo SDK. Veja o app Face Local para uma implementação completa. ### Onboarding automático sem exportar imagens (API anterior) ```ts import { runFaceLocalOnboarding } from "@workspace/face-local-sdk/expo"; import { requestCameraPermissionsAsync } from "expo-image-picker"; const result = await runFaceLocalOnboarding({ // Chamar somente após o aceite visível para usar câmera e rosto na comparação. consent: { camera: true, biometricProcessing: true, photoLibrary: false, version: "1" }, signal: abortController.signal, policy: { maxPhotos: 100, matchThreshold: 0.83, requireCaptureMetadata: true }, requestCameraAccess: async () => { // Apenas autorização da câmera; nenhum seletor ou captura estática é aberto. if (!(await requestCameraPermissionsAsync()).granted) throw new Error("Câmera não autorizada."); }, onPhase: phase => updatePhaseInYourUI(phase), onSearchProgress: count => updateLibraryProgress(count), onProgress: progress => updateProgress(progress.processed, progress.total), }); // result.outcome: "positive" | "negative" | "inconclusive" // result.reason explica o motivo; result.report contém as comparações efetivamente feitas. ``` `updatePhaseInYourUI` também é um callback da interface integradora. A sequência é permissão da câmera → liveness → permissão das fotos → descoberta → Face Match. Não há escolha manual de imagens no app. A opção de acesso limitado pertence ao sistema operacional e não pode ser removida ou ignorada pelo aplicativo. O Face Local exige acesso completo: com acesso limitado, orienta a liberar todas as fotos nos Ajustes e encerra a sessão sem buscar ou comparar imagens. Não acrescente botões de buscar, comparar ou confirmar o início da busca após a captura. O adaptador Expo usa o retorno real da permissão de fotos para continuar sozinho. No núcleo independente, `requestPhotoAccess` deve retornar `{ granted, accessPrivileges: "all" | "limited" | "none", canAskAgain? }`. Desde 0.4, `granted: true` sem escopo explícito e o retorno antigo `void` não autorizam a busca, mesmo com consentimento inicial para fotos. Recusa, cancelamento ou expiração impedem qualquer leitura posterior; uma permissão que chega após cancelar é ignorada. No iPhone, a coleção Selfies é identificada pelo subtipo do PhotoKit, sem depender do idioma. Nomes de álbuns são pistas adicionais, especialmente no Android. A busca prioriza essas coleções e depois percorre as demais fotos autorizadas, sem duplicatas. Uma imagem com uma face é candidata; isso não comprova que foi tirada com a câmera frontal. Nenhum classificador de origem infalível é alegado. O teste usa o limite configurado (máximo 1.000 fotos) e referência de 5 minutos. Para ao primeiro match. Se restarem fotos além do limite e nenhum match tiver sido encontrado, retorna `inconclusive`, não `negative`. Fotos na nuvem, sem face, com múltiplas faces ou rejeitadas pelo filtro EXIF não recebem um score inventado. O negativo se refere às fotos locais elegíveis e autorizadas, não a todo conteúdo que possa existir no aparelho ou na nuvem. ### Proteção local da captura - Apenas câmera nativa; a ponte rejeita opções extras, inclusive referências, imagens, frames e scores. A captura anterior aceita apenas `timeoutSeconds`. A captura com exportação acrescenta somente prazo de retenção e limiar de match; nenhum parâmetro permite importar ou substituir uma imagem de referência. - A aprovação exige sequência recente de frames com timestamps crescentes, câmera frontal integrada declarada pelo sistema e gate de uso único. - No iOS, um relay observa o output AVFoundation antes do processamento biométrico. Se não for possível verificar esse fluxo, a captura falha, sem fallback. - Builds de depuração, simuladores e sinais conhecidos de root/jailbreak, depurador ou bibliotecas de instrumentação são bloqueados. Use **Release**. - Android usa janela segura, proteção de toques obscurecidos e bloqueio de overlays no Android 12+. iOS cancela com captura de tela ativa ou perda de foco. - Bloqueios de segurança abortam a sessão inteira; resultados parciais não aprovam. - O recibo deve incluir `source: "native-camera"`. **Não é atestado criptográfico**: não o aceite como prova confiável enviada a um servidor por um cliente hostil. São camadas locais, não imunidade contra injeção. Root oculto, hooks desconhecidos, HAL/driver adulterado e deepfakes adaptativos podem contornar verificações locais. Não há Play Integrity/App Attest validado externamente, assinatura própria de produção, auditoria completa dos binários nativos ou certificação de resistência a injeção. O APK de avaliação continua com assinatura de teste. O iOS foi testado em iPhone. Ao integrar o SDK ou alterar a captura nativa, valide o fluxo no seu app, especialmente o relay de câmera. Consulte o modelo de ameaças e o protocolo físico do projeto. ### Cancelamento e ciclo de vida - Cancele a leitura de fotos com `AbortController.abort()`. - Use `await verifier.clear()` para invalidar a sessão e aguardar callbacks antigos. - Use `await verifier.revokeConsent()` ao encerrar, revogar ou sair do app. - Não inicie uma segunda operação antes de a primeira ser encerrada. - Mantenha uma instância ativa por processo; o adaptador nativo possui uma única referência. - No Android, a câmera nativa é outra Activity. Não a confunda com saída do app. O app de exemplo ignora esse evento durante captura; a própria câmera cancela se perder o foco. No iOS, o módulo também limpa dados ao entrar em background. - Não persista `receipt`, fotos selecionadas, EXIF, resultados ou embeddings em AsyncStorage, analytics, logs, crash reports ou servidores. - A API JSON é uma opção explícita para obter até duas cópias JPEG em cache privado, por 30–300 segundos após a exportação. Não muda a regra de manter templates somente em memória. Veja o ciclo de vida, limites de expiração e responsabilidade sobre Base64 em [INTEGRACAO.md](docs/INTEGRACAO.md). ### Resultados | Estado | Significado | |---|---| | `match` | Uma face e similaridade ≥ limiar | | `different` | Uma face e similaridade abaixo do limiar | | `inconclusive` | Zero ou múltiplas faces; não é um não-match | | `skipped` | Nuvem, URI proibida ou EXIF incompleto segundo a política | | `error` | Arquivo ou resultado não pôde ser processado | `similarity` é uma pontuação do motor, **não uma porcentagem de certeza**. `report.nativeAttempts` conta chamadas ao motor nativo, inclusive tentativas sem face ou com falha. `report.compared` conta somente resultados com uma face e similaridade válida; é esse número que aparece no app como “fotos comparadas”. Fotos descartadas pelo filtro de metadados não entram nessa contagem. Falhas de leitura (`unreadable-photos`) e do motor (`comparison-failed`) são inconclusivas e têm mensagens distintas, nunca são um falso resultado negativo. O limiar padrão 0,83 é um valor inicial e não foi calibrado para seus aparelhos/fotos. Após `scan`, a referência é liberada mesmo se ocorrer erro. Expiração/cancelamento impedem emissão de um relatório parcial como concluído. ### Biblioteca e EXIF - Exige acesso completo (`accessPrivileges: "all"`) e respeita a recusa do usuário: não força permissões nem tenta acessar fotos fora da autorização. - iOS: `getAssetInfoAsync(..., { shouldDownloadFromNetwork: false })`. - Android: enumera sem `resolveWithFullInfo`, abre a URI local do MediaStore e lê apenas Make/Model/DateTimeOriginal por código nativo. Não usa `getAssetInfoAsync`: a versão instalada do Expo também solicita GPS nessa chamada, provocando falhas sem `ACCESS_MEDIA_LOCATION` mesmo com acesso completo à galeria. - Não pede localização, `MANAGE_EXTERNAL_STORAGE` nem baixa originais da nuvem. - Confere o escopo novamente após a descoberta e no nativo durante as comparações; uma revogação aborta o resultado e libera a referência. - Nunca passa URI `ph://` ou URL remota ao renderizador/matcher como fallback. - Android: somente arquivos locais e MediaStore; provedores remotos são recusados. - Descarta GPS e todos os campos EXIF exceto fabricante, modelo e data original. - O filtro exige esses três campos, não apenas a existência de algum EXIF. - Fabricante e modelo devem corresponder exatamente aos da captura, normalizando somente espaços e caixa. Não há aproximação por família, prefixo, Pro/Max ou geração. - O Android usa `Build.MANUFACTURER`/`Build.MODEL`; o iOS usa o mapeamento local do identificador de hardware do DeviceKit 5.9.0. Modelo desconhecido não recebe fallback. - Cada captura traz `receipt.device`; o núcleo confere esse valor com `capabilities.currentDevice`. Os dois adaptadores nativos conferem novamente o EXIF dos próprios bytes locais antes de detectar rostos ou comparar, sem confiar no metadado enviado pelo JavaScript. Arquivos acima de 32 MiB são recusados. - `requireCaptureMetadata: false` agora é uma política inválida. Não há opção na interface para desabilitar o filtro. Modelos com nomes EXIF não correspondentes serão excluídos, mesmo que sejam fotos legítimas; nunca se presume a equivalência. - Esse filtro não é um fingerprint individual: aparelhos iguais compartilham o mesmo modelo e EXIF pode ser editado. Não prova origem, titularidade ou identidade. - Bibliotecas grandes são processadas em lotes com limite explícito. - Cópias editadas/compartilhadas podem perder metadados. Isso não indica fraude. ## Trocar o motor Implemente `BiometricEngine` com `capabilities`, `captureReference`, `compare` e `clear`. A captura deve ser passiva + ativa, com uma única face, referência vinculada à captura aprovada e expiração real no código nativo. Não preencha pontuações fictícias nem use comparação genérica de strings de embedding. O adaptador atual chama `evaluateFaceSimiByFeature` no Android e `evaluateSimilarity(f1:f2:)` no iPhone. Os formatos internos das bibliotecas nativas são opacos. Para suportar a API JSON com imagens, implemente também `EvidenceEngine`, valide EXIF nos mesmos bytes usados na inferência e gere as cópias a partir das imagens já analisadas, não reabrindo a galeria. Não anuncie `evidenceVersion: 1` sem vínculo de sessão, expiração, hash das cópias e limpeza comprovável. ## Licença e verificação Você pode integrar o Face Local em aplicativos com sua própria marca (white-label). **Não é permitido vender ou revender o SDK**, inclusive modificado, reempacotado ou apresentado sob outro nome. Essa restrição não impede a comercialização do seu aplicativo que utiliza o SDK. Consulte a [licença de uso do Face Local](LICENSE), incluída no pacote. As novas condições não alteram as permissões de versões anteriormente distribuídas. Não há certificação de anti-spoofing, prova forense ou identidade civil. Execute `pnpm --filter @workspace/face-local-sdk test`. Consulte `docs/TESTES-EM-APARELHO.md` no workspace para os testes físicos necessários.