Face Local SDK#
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
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#
- O motor recebe frames da câmera e executa prova de vida passiva + movimentos.
- 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.
- O onboarding busca selfies e compara automaticamente as fotos locais autorizadas.
- 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,approvedereason: aprovação, reprovação, inconclusivo, cancelamento ou erro;photos.livenessephotos.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:
- Integrar, cancelar e descartar as imagens.
- Contrato JSON, significado de todos os campos e limitações.
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:
pnpm --filter @workspace/face-local-sdk build
pnpm --filter @workspace/face-local-sdk pack --pack-destination ../../generated-artifactsCopie o .tgz resultante para seu outro projeto. Instale-o por caminho local:
pnpm add ./face-local-sdk-0.6.0.tgz
pnpm exec expo install expo-modules-core expo-media-library expo-image-pickerO 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.
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:
{
"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:
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 --deviceNã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.
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)#
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.
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 usagetAssetInfoAsync: a versão instalada do Expo também solicita GPS nessa chamada, provocando falhas semACCESS_MEDIA_LOCATIONmesmo com acesso completo à galeria. - Não pede localização,
MANAGE_EXTERNAL_STORAGEnem 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 comcapabilities.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: falseagora é 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, 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.