@neodaten/passkey-expo 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # @neodaten/passkey-expo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Es un módulo Expo que firma con la passkey del sistema (Face ID, Touch ID o huella). Implementa el `ProveedorDePasskey` de `@neodaten/auth-native` para entrar desde la bienvenida sin abrir el login hospedado.
4
+
5
+ - **iOS 16 o superior:** `ASAuthorizationPlatformPublicKeyCredentialProvider`, con verificación del usuario obligatoria. Pide solo credenciales disponibles al momento, así que no muestra la hoja del QR.
6
+ - **Android 9 o superior:** Credential Manager (`androidx.credentials` 1.6.0).
7
+
8
+ Va en un paquete aparte para que las apps React Native sin Expo (Luup) puedan seguir usando `auth-native` sin este módulo nativo.
9
+
10
+ ## Uso
11
+
12
+ ```tsx
13
+ import { crearClienteDeIdentidadNativa } from '@neodaten/auth-native';
14
+ import { BienvenidaConUltimaCuenta } from '@neodaten/auth-native/bienvenida';
15
+ import { crearProveedorDePasskeyExpo } from '@neodaten/passkey-expo';
16
+
17
+ const cliente = crearClienteDeIdentidadNativa({
18
+ ...opciones,
19
+ proveedorDePasskey: crearProveedorDePasskeyExpo(),
20
+ });
21
+
22
+ <BienvenidaConUltimaCuenta
23
+ cliente={cliente}
24
+ marca={marcaDeLaApp}
25
+ alEntrar={irAlInicio}
26
+ alAbrirElLogin={abrirLoginHospedado}
27
+ sinUltimaCuenta={<BienvenidaDeSiempre />}
28
+ />;
29
+ ```
30
+
31
+ ## Requisitos de la app
32
+
33
+ - **iOS:** `ios.associatedDomains: ["webcredentials:id.neodaten.com"]` (en dev, `id.dev.neodaten.com`). El App ID debe estar en el `apple-app-site-association` del dominio de identidad.
34
+ - **Android:** la huella SHA-256 del certificado debe estar en `PASSKEYS_ANDROID_APPS` del core, que publica `assetlinks.json`.
35
+ - **Core:** la aplicación tiene `settings.passkeyEnLaBienvenida`.
36
+
37
+ ## Errores
38
+
39
+ `firmar` rechaza con estos códigos:
40
+
41
+ - `acceso_cancelado`: la persona canceló.
42
+ - `sin_passkey`: no hay credencial en el dispositivo. En iOS, una cancelación en menos de un segundo, sin interfaz, cuenta como `sin_passkey`.
43
+ - `passkey_fallida`: cualquier otro fallo.
44
+
45
+ El proveedor los convierte en `ErrorDeIdentidad`. `useBienvenida` abre el login hospedado cuando no hay passkey o la passkey falla.
46
+
47
+ ## Publicación
48
+
49
+ `pnpm --filter @neodaten/passkey-expo empaquetar` compila a `dist/` con `tsc`. El workflow `publicar-passkey-expo.yml` publica en npm con la publicación de confianza, sin procedencia porque el repositorio es privado, cuando se empuja el tag `passkey-expo-v<versión>` sobre un commit de `main`. La versión del tag tiene que coincidir con la de `package.json`. En npm hay que dar de alta la publicación de confianza para este paquete y este workflow. Primero se publica `@neodaten/auth-native`, que es dependencia par.
@@ -0,0 +1,18 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'expo-module-gradle-plugin'
3
+
4
+ group = 'mx.neodaten.passkey'
5
+ version = '0.1.0'
6
+
7
+ android {
8
+ namespace 'mx.neodaten.passkey'
9
+ defaultConfig {
10
+ versionCode 1
11
+ versionName '0.1.0'
12
+ }
13
+ }
14
+
15
+ dependencies {
16
+ implementation 'androidx.credentials:credentials:1.6.0'
17
+ implementation 'androidx.credentials:credentials-play-services-auth:1.6.0'
18
+ }
@@ -0,0 +1,69 @@
1
+ package mx.neodaten.passkey
2
+
3
+ import android.content.pm.PackageManager
4
+ import android.os.Build
5
+ import androidx.credentials.CredentialManager
6
+ import androidx.credentials.GetCredentialRequest
7
+ import androidx.credentials.GetPublicKeyCredentialOption
8
+ import androidx.credentials.PublicKeyCredential
9
+ import androidx.credentials.exceptions.GetCredentialCancellationException
10
+ import androidx.credentials.exceptions.GetCredentialException
11
+ import androidx.credentials.exceptions.NoCredentialException
12
+ import expo.modules.kotlin.exception.CodedException
13
+ import expo.modules.kotlin.functions.Coroutine
14
+ import expo.modules.kotlin.modules.Module
15
+ import expo.modules.kotlin.modules.ModuleDefinition
16
+
17
+ private const val PRIMERA_VERSION_CON_PASSKEYS = Build.VERSION_CODES.P
18
+
19
+ class PasskeyNativaModule : Module() {
20
+ override fun definition() =
21
+ ModuleDefinition {
22
+ Name("PasskeyNativa")
23
+
24
+ AsyncFunction("biometria") { biometriaDelDispositivo() }
25
+
26
+ AsyncFunction("firmar") Coroutine { opcionesJson: String -> firmar(opcionesJson) }
27
+ }
28
+
29
+ private fun biometriaDelDispositivo(): String? {
30
+ if (Build.VERSION.SDK_INT < PRIMERA_VERSION_CON_PASSKEYS) return null
31
+ val paquetes = appContext.reactContext?.packageManager ?: return null
32
+ return when {
33
+ paquetes.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) -> "huella"
34
+ paquetes.hasSystemFeature(PackageManager.FEATURE_FACE) -> "rostro"
35
+ else -> "otra"
36
+ }
37
+ }
38
+
39
+ private suspend fun firmar(opcionesJson: String): String {
40
+ val actividad = appContext.currentActivity ?: throw ErrorDeLaPasskey.sinActividad()
41
+ val pedido = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(opcionesJson)))
42
+ val credencial =
43
+ try {
44
+ CredentialManager.create(actividad).getCredential(actividad, pedido).credential
45
+ } catch (error: GetCredentialCancellationException) {
46
+ throw ErrorDeLaPasskey.cancelada(error)
47
+ } catch (error: NoCredentialException) {
48
+ throw ErrorDeLaPasskey.sinPasskey(error)
49
+ } catch (error: GetCredentialException) {
50
+ throw ErrorDeLaPasskey.fallida(error)
51
+ }
52
+ if (credencial !is PublicKeyCredential) throw ErrorDeLaPasskey.fallida(null)
53
+ return credencial.authenticationResponseJson
54
+ }
55
+ }
56
+
57
+ private object ErrorDeLaPasskey {
58
+ fun cancelada(causa: Throwable): CodedException =
59
+ CodedException("acceso_cancelado", "Se canceló la passkey.", causa)
60
+
61
+ fun sinPasskey(causa: Throwable): CodedException =
62
+ CodedException("sin_passkey", "No hay passkey en este dispositivo.", causa)
63
+
64
+ fun sinActividad(): CodedException =
65
+ CodedException("passkey_fallida", "No hay una pantalla activa para pedir la passkey.", null)
66
+
67
+ fun fallida(causa: Throwable?): CodedException =
68
+ CodedException("passkey_fallida", "No se pudo usar la passkey.", causa)
69
+ }
@@ -0,0 +1,4 @@
1
+ import type { ProveedorDePasskey } from '@neodaten/auth-native';
2
+ export { proveedorDePasskeyCon } from './proveedor-de-passkey';
3
+ export type { ModuloPasskeyNativa } from './modulo-passkey-nativa';
4
+ export declare const crearProveedorDePasskeyExpo: () => ProveedorDePasskey;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import { NOMBRE_DEL_MODULO } from './modulo-passkey-nativa';
3
+ import { proveedorDePasskeyCon } from './proveedor-de-passkey';
4
+ export { proveedorDePasskeyCon } from './proveedor-de-passkey';
5
+ export const crearProveedorDePasskeyExpo = () => proveedorDePasskeyCon(requireNativeModule(NOMBRE_DEL_MODULO));
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAE,iBAAiB,EAA4B,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAuB,EAAE,CAClE,qBAAqB,CAAC,mBAAmB,CAAsB,iBAAiB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface ModuloPasskeyNativa {
2
+ biometria(): Promise<string | null>;
3
+ firmar(opcionesJson: string): Promise<string>;
4
+ }
5
+ export declare const NOMBRE_DEL_MODULO = "PasskeyNativa";
6
+ export declare const CODIGO_DE_CANCELACION = "acceso_cancelado";
7
+ export declare const CODIGO_SIN_PASSKEY = "sin_passkey";
@@ -0,0 +1,4 @@
1
+ export const NOMBRE_DEL_MODULO = 'PasskeyNativa';
2
+ export const CODIGO_DE_CANCELACION = 'acceso_cancelado';
3
+ export const CODIGO_SIN_PASSKEY = 'sin_passkey';
4
+ //# sourceMappingURL=modulo-passkey-nativa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modulo-passkey-nativa.js","sourceRoot":"","sources":["../src/modulo-passkey-nativa.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type ProveedorDePasskey } from '@neodaten/auth-native';
2
+ import { type ModuloPasskeyNativa } from './modulo-passkey-nativa';
3
+ export declare const proveedorDePasskeyCon: (modulo: ModuloPasskeyNativa) => ProveedorDePasskey;
@@ -0,0 +1,32 @@
1
+ import { ErrorDeIdentidad, } from '@neodaten/auth-native';
2
+ import { CODIGO_DE_CANCELACION, CODIGO_SIN_PASSKEY, } from './modulo-passkey-nativa';
3
+ const BIOMETRIAS = new Set([
4
+ 'rostro',
5
+ 'huella',
6
+ 'otra',
7
+ ]);
8
+ const esBiometria = (valor) => BIOMETRIAS.has(valor);
9
+ const codigoDe = (error) => typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
10
+ const comoErrorDeIdentidad = (error) => {
11
+ const codigo = codigoDe(error);
12
+ if (codigo === CODIGO_DE_CANCELACION)
13
+ return new ErrorDeIdentidad('acceso_cancelado');
14
+ if (codigo === CODIGO_SIN_PASSKEY)
15
+ return new ErrorDeIdentidad('sin_passkey');
16
+ return new ErrorDeIdentidad('respuesta_invalida', { cause: error });
17
+ };
18
+ export const proveedorDePasskeyCon = (modulo) => ({
19
+ async biometria() {
20
+ const leida = await modulo.biometria();
21
+ return leida !== null && esBiometria(leida) ? leida : null;
22
+ },
23
+ async firmar(opcionesJson) {
24
+ try {
25
+ return await modulo.firmar(opcionesJson);
26
+ }
27
+ catch (error) {
28
+ throw comoErrorDeIdentidad(error);
29
+ }
30
+ },
31
+ });
32
+ //# sourceMappingURL=proveedor-de-passkey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proveedor-de-passkey.js","sourceRoot":"","sources":["../src/proveedor-de-passkey.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GAGjB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,GAEnB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,UAAU,GAAwB,IAAI,GAAG,CAA0B;IACvE,QAAQ;IACR,QAAQ;IACR,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,CAAC,KAAa,EAAoC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAE/F,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAW,EAAE,CAC3C,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAE1F,MAAM,oBAAoB,GAAG,CAAC,KAAc,EAAoB,EAAE;IAChE,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,MAAM,KAAK,qBAAqB;QAAE,OAAO,IAAI,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;IACtF,IAAI,MAAM,KAAK,kBAAkB;QAAE,OAAO,IAAI,gBAAgB,CAAC,aAAa,CAAC,CAAC;IAC9E,OAAO,IAAI,gBAAgB,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AACtE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,MAA2B,EAAsB,EAAE,CAAC,CAAC;IACzF,KAAK,CAAC,SAAS;QACb,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;QACvC,OAAO,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7D,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,YAAY;QACvB,IAAI,CAAC;YACH,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,oBAAoB,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["PasskeyNativaModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["mx.neodaten.passkey.PasskeyNativaModule"]
8
+ }
9
+ }
@@ -0,0 +1,18 @@
1
+ import Foundation
2
+
3
+ enum Base64Url {
4
+ static func codificar(_ datos: Data) -> String {
5
+ datos.base64EncodedString()
6
+ .replacingOccurrences(of: "+", with: "-")
7
+ .replacingOccurrences(of: "/", with: "_")
8
+ .replacingOccurrences(of: "=", with: "")
9
+ }
10
+
11
+ static func decodificar(_ texto: String) -> Data? {
12
+ let base64 = texto
13
+ .replacingOccurrences(of: "-", with: "+")
14
+ .replacingOccurrences(of: "_", with: "/")
15
+ let relleno = String(repeating: "=", count: (4 - base64.count % 4) % 4)
16
+ return Data(base64Encoded: base64 + relleno)
17
+ }
18
+ }
@@ -0,0 +1,76 @@
1
+ import AuthenticationServices
2
+ import UIKit
3
+
4
+ @available(iOS 16.0, *)
5
+ final class CeremoniaDeLaPasskey: NSObject, ASAuthorizationControllerDelegate,
6
+ ASAuthorizationControllerPresentationContextProviding
7
+ {
8
+ private static let cancelacionSinInterfazEnSegundos: TimeInterval = 1
9
+
10
+ private let alTerminar: (Result<String, ErrorDeLaPasskey>) -> Void
11
+ private var iniciadaEn = Date()
12
+ private var controlador: ASAuthorizationController?
13
+
14
+ init(alTerminar: @escaping (Result<String, ErrorDeLaPasskey>) -> Void) {
15
+ self.alTerminar = alTerminar
16
+ }
17
+
18
+ func firmar(desafio: Data, rpId: String) {
19
+ let proveedor = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
20
+ let pedido = proveedor.createCredentialAssertionRequest(challenge: desafio)
21
+ pedido.userVerificationPreference = .required
22
+ let controlador = ASAuthorizationController(authorizationRequests: [pedido])
23
+ controlador.delegate = self
24
+ controlador.presentationContextProvider = self
25
+ self.controlador = controlador
26
+ iniciadaEn = Date()
27
+ controlador.performRequests(options: .preferImmediatelyAvailableCredentials)
28
+ }
29
+
30
+ func authorizationController(
31
+ controller: ASAuthorizationController,
32
+ didCompleteWithAuthorization authorization: ASAuthorization
33
+ ) {
34
+ guard let asercion = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
35
+ terminar(.failure(.fallida("El sistema no devolvió una passkey.")))
36
+ return
37
+ }
38
+ do {
39
+ terminar(.success(try RespuestaDeLaPasskey.json(asercion)))
40
+ } catch {
41
+ terminar(.failure(.fallida(error.localizedDescription)))
42
+ }
43
+ }
44
+
45
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
46
+ terminar(.failure(errorDe(error)))
47
+ }
48
+
49
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
50
+ let escenas = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
51
+ return escenas.flatMap(\.windows).first(where: \.isKeyWindow) ?? ASPresentationAnchor()
52
+ }
53
+
54
+ private func errorDe(_ error: Error) -> ErrorDeLaPasskey {
55
+ guard let error = error as? ASAuthorizationError else {
56
+ return .fallida(error.localizedDescription)
57
+ }
58
+ switch error.code {
59
+ case .canceled:
60
+ return fueSinInterfaz() ? .sinPasskey : .cancelada
61
+ case .notInteractive:
62
+ return .sinPasskey
63
+ default:
64
+ return .fallida(error.localizedDescription)
65
+ }
66
+ }
67
+
68
+ private func fueSinInterfaz() -> Bool {
69
+ Date().timeIntervalSince(iniciadaEn) < Self.cancelacionSinInterfazEnSegundos
70
+ }
71
+
72
+ private func terminar(_ resultado: Result<String, ErrorDeLaPasskey>) {
73
+ controlador = nil
74
+ alTerminar(resultado)
75
+ }
76
+ }
@@ -0,0 +1,21 @@
1
+ import ExpoModulesCore
2
+
3
+ enum ErrorDeLaPasskey: Error {
4
+ case opcionesInvalidas
5
+ case cancelada
6
+ case sinPasskey
7
+ case fallida(String)
8
+
9
+ var excepcion: Exception {
10
+ switch self {
11
+ case .cancelada:
12
+ return Exception(name: "acceso_cancelado", description: "Se canceló la passkey.", code: "acceso_cancelado")
13
+ case .sinPasskey:
14
+ return Exception(name: "sin_passkey", description: "No hay passkey en este dispositivo.", code: "sin_passkey")
15
+ case .opcionesInvalidas:
16
+ return Exception(name: "passkey_fallida", description: "Las opciones de la passkey no son válidas.", code: "passkey_fallida")
17
+ case .fallida(let detalle):
18
+ return Exception(name: "passkey_fallida", description: detalle, code: "passkey_fallida")
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,14 @@
1
+ import Foundation
2
+
3
+ struct OpcionesDeLaPasskey: Decodable {
4
+ let challenge: String
5
+ let rpId: String
6
+
7
+ static func leer(_ json: String) throws -> (desafio: Data, rpId: String) {
8
+ let opciones = try JSONDecoder().decode(OpcionesDeLaPasskey.self, from: Data(json.utf8))
9
+ guard let desafio = Base64Url.decodificar(opciones.challenge) else {
10
+ throw ErrorDeLaPasskey.opcionesInvalidas
11
+ }
12
+ return (desafio, opciones.rpId)
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'PasskeyNativa'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = 'UNLICENSED'
10
+ s.author = 'Neodaten'
11
+ s.homepage = 'https://github.com/Neodaten/n360'
12
+ s.platforms = { :ios => '16.0' }
13
+ s.swift_version = '5.9'
14
+ s.source = { :git => 'https://github.com/Neodaten/n360.git' }
15
+ s.static_framework = true
16
+
17
+ s.dependency 'ExpoModulesCore'
18
+
19
+ s.source_files = '**/*.swift'
20
+ end
@@ -0,0 +1,51 @@
1
+ import ExpoModulesCore
2
+ import LocalAuthentication
3
+
4
+ public class PasskeyNativaModule: Module {
5
+ private var ceremoniaEnCurso: AnyObject?
6
+
7
+ public func definition() -> ModuleDefinition {
8
+ Name("PasskeyNativa")
9
+
10
+ AsyncFunction("biometria") { () -> String? in
11
+ biometriaDelDispositivo()
12
+ }
13
+
14
+ AsyncFunction("firmar") { (opcionesJson: String, promesa: Promise) in
15
+ self.firmar(opcionesJson: opcionesJson, promesa: promesa)
16
+ }.runOnQueue(.main)
17
+ }
18
+
19
+ private func firmar(opcionesJson: String, promesa: Promise) {
20
+ guard #available(iOS 16.0, *) else {
21
+ promesa.reject(ErrorDeLaPasskey.sinPasskey.excepcion)
22
+ return
23
+ }
24
+ do {
25
+ let (desafio, rpId) = try OpcionesDeLaPasskey.leer(opcionesJson)
26
+ let ceremonia = CeremoniaDeLaPasskey { [weak self] resultado in
27
+ self?.ceremoniaEnCurso = nil
28
+ switch resultado {
29
+ case .success(let respuesta): promesa.resolve(respuesta)
30
+ case .failure(let error): promesa.reject(error.excepcion)
31
+ }
32
+ }
33
+ ceremoniaEnCurso = ceremonia
34
+ ceremonia.firmar(desafio: desafio, rpId: rpId)
35
+ } catch {
36
+ promesa.reject(ErrorDeLaPasskey.opcionesInvalidas.excepcion)
37
+ }
38
+ }
39
+ }
40
+
41
+ private func biometriaDelDispositivo() -> String? {
42
+ guard #available(iOS 16.0, *) else { return nil }
43
+ let contexto = LAContext()
44
+ var error: NSError?
45
+ guard contexto.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { return nil }
46
+ switch contexto.biometryType {
47
+ case .faceID: return "rostro"
48
+ case .touchID: return "huella"
49
+ default: return "otra"
50
+ }
51
+ }
@@ -0,0 +1,25 @@
1
+ import AuthenticationServices
2
+
3
+ enum RespuestaDeLaPasskey {
4
+ static func json(_ asercion: ASAuthorizationPlatformPublicKeyCredentialAssertion) throws -> String {
5
+ let identificador = Base64Url.codificar(asercion.credentialID)
6
+ let respuesta: [String: Any] = [
7
+ "id": identificador,
8
+ "rawId": identificador,
9
+ "type": "public-key",
10
+ "authenticatorAttachment": "platform",
11
+ "clientExtensionResults": [String: Any](),
12
+ "response": [
13
+ "clientDataJSON": Base64Url.codificar(asercion.rawClientDataJSON),
14
+ "authenticatorData": Base64Url.codificar(asercion.rawAuthenticatorData),
15
+ "signature": Base64Url.codificar(asercion.signature),
16
+ "userHandle": Base64Url.codificar(asercion.userID),
17
+ ],
18
+ ]
19
+ let datos = try JSONSerialization.data(withJSONObject: respuesta)
20
+ guard let texto = String(data: datos, encoding: .utf8) else {
21
+ throw ErrorDeLaPasskey.fallida("No se pudo armar la respuesta de la passkey.")
22
+ }
23
+ return texto
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@neodaten/passkey-expo",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Módulo Expo que firma con la passkey del sistema (Face ID, Touch ID o huella) para entrar desde la bienvenida con @neodaten/auth-native.",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "ios",
18
+ "android/build.gradle",
19
+ "android/src",
20
+ "expo-module.config.json",
21
+ "README.md"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/Neodaten/n360.git",
26
+ "directory": "packages/passkey-expo"
27
+ },
28
+ "publishConfig": {
29
+ "registry": "https://registry.npmjs.org",
30
+ "access": "public"
31
+ },
32
+ "peerDependencies": {
33
+ "@neodaten/auth-native": ">=0.1.0",
34
+ "expo-modules-core": ">=57.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "expo-modules-core": "57.0.25",
38
+ "react": "^19.2.0",
39
+ "react-native": "0.86.3",
40
+ "typescript": "^6.0.3",
41
+ "vitest": "^4.1.0",
42
+ "@neodaten/auth-native": "0.1.0",
43
+ "@neodaten/eslint-config": "0.0.0",
44
+ "@neodaten/tsconfig": "0.0.0"
45
+ },
46
+ "scripts": {
47
+ "empaquetar": "tsc -p tsconfig.empaquetado.json",
48
+ "test": "vitest run"
49
+ }
6
50
  }