react-native-nitro-passkeys 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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/NitroPasskeys.podspec +37 -0
  3. package/README.md +314 -0
  4. package/android/CMakeLists.txt +31 -0
  5. package/android/build.gradle +155 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/margelo/nitro/nitropasskeys/HybridNitroPasskeys.kt +65 -0
  11. package/android/src/main/java/com/margelo/nitro/nitropasskeys/NitroPasskeysPackage.kt +20 -0
  12. package/android/src/main/java/com/margelo/nitro/nitropasskeys/PasskeyErrorToken.kt +64 -0
  13. package/android/src/test/java/com/margelo/nitro/nitropasskeys/PasskeyErrorTokenTest.kt +128 -0
  14. package/ios/Base64URL.swift +37 -0
  15. package/ios/Bridge.h +1 -0
  16. package/ios/HybridNitroPasskeys.swift +290 -0
  17. package/ios/PasskeyErrorToken.swift +32 -0
  18. package/ios/PasskeyJSON.swift +195 -0
  19. package/lib/commonjs/PasskeyError.js +39 -0
  20. package/lib/commonjs/PasskeyError.js.map +1 -0
  21. package/lib/commonjs/index.js +52 -0
  22. package/lib/commonjs/index.js.map +1 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/commonjs/response.js +30 -0
  25. package/lib/commonjs/response.js.map +1 -0
  26. package/lib/commonjs/specs/nitro-passkeys.nitro.js +6 -0
  27. package/lib/commonjs/specs/nitro-passkeys.nitro.js.map +1 -0
  28. package/lib/commonjs/types.js +2 -0
  29. package/lib/commonjs/types.js.map +1 -0
  30. package/lib/commonjs/validate.js +118 -0
  31. package/lib/commonjs/validate.js.map +1 -0
  32. package/lib/module/PasskeyError.js +34 -0
  33. package/lib/module/PasskeyError.js.map +1 -0
  34. package/lib/module/index.js +42 -0
  35. package/lib/module/index.js.map +1 -0
  36. package/lib/module/response.js +25 -0
  37. package/lib/module/response.js.map +1 -0
  38. package/lib/module/specs/nitro-passkeys.nitro.js +4 -0
  39. package/lib/module/specs/nitro-passkeys.nitro.js.map +1 -0
  40. package/lib/module/types.js +2 -0
  41. package/lib/module/types.js.map +1 -0
  42. package/lib/module/validate.js +109 -0
  43. package/lib/module/validate.js.map +1 -0
  44. package/lib/typescript/example/server/index.d.ts +2 -0
  45. package/lib/typescript/example/server/index.d.ts.map +1 -0
  46. package/lib/typescript/src/PasskeyError.d.ts +8 -0
  47. package/lib/typescript/src/PasskeyError.d.ts.map +1 -0
  48. package/lib/typescript/src/index.d.ts +8 -0
  49. package/lib/typescript/src/index.d.ts.map +1 -0
  50. package/lib/typescript/src/response.d.ts +4 -0
  51. package/lib/typescript/src/response.d.ts.map +1 -0
  52. package/lib/typescript/src/specs/nitro-passkeys.nitro.d.ts +12 -0
  53. package/lib/typescript/src/specs/nitro-passkeys.nitro.d.ts.map +1 -0
  54. package/lib/typescript/src/types.d.ts +104 -0
  55. package/lib/typescript/src/types.d.ts.map +1 -0
  56. package/lib/typescript/src/validate.d.ts +14 -0
  57. package/lib/typescript/src/validate.d.ts.map +1 -0
  58. package/nitro.json +24 -0
  59. package/nitrogen/generated/.gitattributes +1 -0
  60. package/nitrogen/generated/android/NitroPasskeys+autolinking.cmake +81 -0
  61. package/nitrogen/generated/android/NitroPasskeys+autolinking.gradle +27 -0
  62. package/nitrogen/generated/android/NitroPasskeysOnLoad.cpp +54 -0
  63. package/nitrogen/generated/android/NitroPasskeysOnLoad.hpp +34 -0
  64. package/nitrogen/generated/android/c++/JHybridNitroPasskeysSpec.cpp +86 -0
  65. package/nitrogen/generated/android/c++/JHybridNitroPasskeysSpec.hpp +64 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitropasskeys/HybridNitroPasskeysSpec.kt +63 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitropasskeys/NitroPasskeysOnLoad.kt +35 -0
  68. package/nitrogen/generated/ios/NitroPasskeys+autolinking.rb +62 -0
  69. package/nitrogen/generated/ios/NitroPasskeys-Swift-Cxx-Bridge.cpp +49 -0
  70. package/nitrogen/generated/ios/NitroPasskeys-Swift-Cxx-Bridge.hpp +111 -0
  71. package/nitrogen/generated/ios/NitroPasskeys-Swift-Cxx-Umbrella.hpp +45 -0
  72. package/nitrogen/generated/ios/NitroPasskeysAutolinking.mm +33 -0
  73. package/nitrogen/generated/ios/NitroPasskeysAutolinking.swift +26 -0
  74. package/nitrogen/generated/ios/c++/HybridNitroPasskeysSpecSwift.cpp +11 -0
  75. package/nitrogen/generated/ios/c++/HybridNitroPasskeysSpecSwift.hpp +93 -0
  76. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  77. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  78. package/nitrogen/generated/ios/swift/HybridNitroPasskeysSpec.swift +56 -0
  79. package/nitrogen/generated/ios/swift/HybridNitroPasskeysSpec_cxx.swift +169 -0
  80. package/nitrogen/generated/shared/c++/HybridNitroPasskeysSpec.cpp +23 -0
  81. package/nitrogen/generated/shared/c++/HybridNitroPasskeysSpec.hpp +64 -0
  82. package/package.json +106 -0
  83. package/src/PasskeyError.ts +58 -0
  84. package/src/index.ts +66 -0
  85. package/src/response.ts +49 -0
  86. package/src/specs/nitro-passkeys.nitro.ts +12 -0
  87. package/src/types.ts +124 -0
  88. package/src/validate.ts +132 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Paredes Alva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ # CocoaPods does not resolve an unpinned dependency on a prerelease version
6
+ # (e.g. `0.0.0-development` or `1.0.0-next.1`), so the pod version drops any
7
+ # prerelease suffix. The git tag keeps the full npm version.
8
+ pod_version = package["version"].sub(/[-+].*\z/, "")
9
+
10
+ Pod::Spec.new do |s|
11
+ s.name = "NitroPasskeys"
12
+ s.version = pod_version
13
+ s.summary = package["description"]
14
+ s.homepage = package["homepage"]
15
+ s.license = package["license"]
16
+ s.authors = package["author"]
17
+
18
+ s.platforms = { :ios => '16.0' }
19
+ s.source = { :git => "https://github.com/aparedes/react-native-nitro-passkeys.git", :tag => "#{package["version"]}" }
20
+
21
+ s.source_files = [
22
+ # Implementation (Swift)
23
+ "ios/**/*.{swift}",
24
+ # Autolinking/Registration (Objective-C++)
25
+ "ios/**/*.{m,mm}",
26
+ ]
27
+
28
+ s.frameworks = 'AuthenticationServices'
29
+
30
+ load 'nitrogen/generated/ios/NitroPasskeys+autolinking.rb'
31
+ add_nitrogen_files(s)
32
+
33
+ s.dependency 'React-jsi'
34
+ s.dependency 'React-callinvoker'
35
+
36
+ install_modules_dependencies(s)
37
+ end
package/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # react-native-nitro-passkeys
2
+
3
+ WebAuthn passkeys for bare React Native, built on [Nitro Modules](https://nitro.margelo.com).
4
+ JSON in, JSON out: the option and response shapes are the standard WebAuthn JSON types, so they
5
+ plug straight into `@simplewebauthn/server` v14 (or any server that speaks WebAuthn JSON).
6
+
7
+ - iOS 16+ via `AuthenticationServices`
8
+ - Android minSdk 28 via Credential Manager
9
+
10
+ ```ts
11
+ import {
12
+ createPasskey,
13
+ getPasskey,
14
+ isPasskeySupported,
15
+ PasskeyError,
16
+ } from 'react-native-nitro-passkeys';
17
+ ```
18
+
19
+ ## 1. Install
20
+
21
+ ```sh
22
+ bun add react-native-nitro-passkeys react-native-nitro-modules
23
+ cd ios && pod install
24
+ ```
25
+
26
+ `react-native-nitro-modules` is a peer dependency (`>=0.37.1 <0.38.0`).
27
+
28
+ Minimums: **iOS 16** and **Android minSdk 28**. The module has no lower-minSdk mode: a host app
29
+ with a `minSdkVersion` below 28 fails the manifest merge by design, so raise it to 28.
30
+
31
+ No R8/ProGuard rule is needed: `androidx.credentials:credentials-play-services-auth` ships its own.
32
+
33
+ ## 2. The rpID must equal the domain
34
+
35
+ The `rp.id` in the creation options and the `rpId` in the request options must be **exactly** the
36
+ domain listed in the iOS `webcredentials:` entitlement and serving `assetlinks.json`: the bare
37
+ host, with no scheme and no port.
38
+
39
+ The rpID is **required**. The module rejects options without it (`invalidOptions`), so pass
40
+ `rpID` to `generateAuthenticationOptions` as well as `generateRegistrationOptions`.
41
+
42
+ ## 3. iOS Associated Domains
43
+
44
+ 1. Add `webcredentials:<rpID>` to the app's Associated Domains entitlement.
45
+ 2. Serve `https://<rpID>/.well-known/apple-app-site-association` as `application/json`, over
46
+ HTTPS, with no redirects:
47
+
48
+ ```json
49
+ { "webcredentials": { "apps": ["<TEAMID>.<bundle id>"] } }
50
+ ```
51
+
52
+ 3. During development, use the `?mode=developer` suffix (`webcredentials:<rpID>?mode=developer`)
53
+ and enable Associated Domains Development on the device (Settings > Developer). This makes
54
+ the device fetch the file directly from your server instead of through Apple's CDN.
55
+
56
+ ## 4. Android Digital Asset Links
57
+
58
+ Serve `https://<rpID>/.well-known/assetlinks.json`:
59
+
60
+ ```json
61
+ [
62
+ {
63
+ "relation": ["delegate_permission/common.get_login_creds"],
64
+ "target": {
65
+ "namespace": "android_app",
66
+ "package_name": "<applicationId>",
67
+ "sha256_cert_fingerprints": ["<AA:BB:...>"]
68
+ }
69
+ }
70
+ ]
71
+ ```
72
+
73
+ List every signing certificate that installs the app: the debug keystore for development and the
74
+ **Play App Signing** key (not the upload key) for Play builds.
75
+
76
+ ## 5. The Android origin
77
+
78
+ Android assertions carry the origin `android:apk-key-hash:<base64url(sha256(cert))>`, not
79
+ `https://<rpID>`. iOS uses `https://<rpID>` as its origin. Your server must therefore accept both:
80
+
81
+ ```ts
82
+ expectedOrigin: ['https://example.com', 'android:apk-key-hash:...'];
83
+ ```
84
+
85
+ Compute the hash from the certificate's SHA-256 fingerprint (the colon-separated hex that
86
+ `keytool -list -v` or the Play Console shows): hex, to bytes, to **unpadded** base64url.
87
+
88
+ ```ts
89
+ const fingerprint = '4A:5B:...'; // SHA-256 cert fingerprint
90
+ const origin = `android:apk-key-hash:${Buffer.from(fingerprint.replaceAll(':', ''), 'hex').toString('base64url')}`;
91
+ ```
92
+
93
+ Node and Bun's `'base64url'` encoding is already unpadded. If you compute it elsewhere, strip
94
+ the trailing `=` and use `-`/`_` instead of `+`/`/`. Add one origin per signing certificate.
95
+
96
+ ## 6. Usage
97
+
98
+ Get options from your server, call `createPasskey` / `getPasskey`, then post the result back for
99
+ `verifyRegistrationResponse` / `verifyAuthenticationResponse`.
100
+
101
+ Server (`@simplewebauthn/server` v14), passing `rpID` to **both** option generators:
102
+
103
+ ```ts
104
+ import {
105
+ generateAuthenticationOptions,
106
+ generateRegistrationOptions,
107
+ verifyAuthenticationResponse,
108
+ verifyRegistrationResponse,
109
+ } from '@simplewebauthn/server';
110
+
111
+ const rpID = 'example.com';
112
+ const expectedOrigin = ['https://example.com', 'android:apk-key-hash:...'];
113
+
114
+ // POST /register/options
115
+ const registrationOptions = await generateRegistrationOptions({
116
+ rpName: 'Example',
117
+ rpID,
118
+ userName: 'alice',
119
+ excludeCredentials, // the user's existing credentials: { id, transports }[]
120
+ });
121
+
122
+ // POST /login/options
123
+ const authenticationOptions = await generateAuthenticationOptions({ rpID });
124
+
125
+ // POST /register/verify
126
+ const registration = await verifyRegistrationResponse({
127
+ response, // the RegistrationResponseJSON posted by the app
128
+ expectedChallenge,
129
+ expectedOrigin,
130
+ expectedRPID: rpID,
131
+ });
132
+
133
+ // POST /login/verify
134
+ const authentication = await verifyAuthenticationResponse({
135
+ response, // the AuthenticationResponseJSON posted by the app
136
+ expectedChallenge,
137
+ expectedOrigin,
138
+ expectedRPID: rpID,
139
+ credential, // the stored credential: { id, publicKey, counter, transports }
140
+ });
141
+ ```
142
+
143
+ App:
144
+
145
+ ```ts
146
+ import {
147
+ createPasskey,
148
+ getPasskey,
149
+ isPasskeySupported,
150
+ } from 'react-native-nitro-passkeys';
151
+
152
+ if (isPasskeySupported()) {
153
+ // Register
154
+ const options = await post('/register/options', { username: 'alice' });
155
+ const registration = await createPasskey(options);
156
+ await post('/register/verify', { username: 'alice', response: registration });
157
+
158
+ // Sign in
159
+ const requestOptions = await post('/login/options');
160
+ const assertion = await getPasskey(requestOptions);
161
+ await post('/login/verify', { response: assertion });
162
+ }
163
+ ```
164
+
165
+ `isPasskeySupported()` is synchronous. `createPasskey` and `getPasskey` also reject with
166
+ `unsupported` when it is `false`. The option and response types
167
+ (`PublicKeyCredentialCreationOptionsJSON`, `RegistrationResponseJSON`, and so on) are exported
168
+ from the package.
169
+
170
+ ## 7. Errors
171
+
172
+ Every rejection from `createPasskey` and `getPasskey` is a `PasskeyError` with a `code`:
173
+
174
+ | `code` | Meaning |
175
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
176
+ | `canceled` | The user dismissed the sheet (iOS `ASAuthorizationError.canceled`; Android cancellation, or a DOM `NotAllowedError`/`AbortError`/`TimeoutError`). |
177
+ | `noCredentials` | No matching passkey (Android `NoCredentialException`). iOS never returns this. |
178
+ | `alreadyRegistered` | An `excludeCredentials` entry matched (`createPasskey` only). See below. |
179
+ | `domainNotAssociated` | The rpID is not verified for this app (missing or wrong AASA / `assetlinks.json`). |
180
+ | `unsupported` | Passkeys unavailable: `isPasskeySupported()` is `false`, or no credential provider is available (Android). |
181
+ | `invalidOptions` | The options failed validation (JS or native), for example a missing `rp.id` / `rpId` or invalid base64url. |
182
+ | `unknown` | Anything else, including a concurrent request, a malformed native response and unmapped OS errors. |
183
+
184
+ ```ts
185
+ import { createPasskey, PasskeyError } from 'react-native-nitro-passkeys';
186
+
187
+ try {
188
+ await createPasskey(options);
189
+ } catch (e) {
190
+ if (e instanceof PasskeyError) {
191
+ if (e.code === 'canceled') return;
192
+ if (e.code === 'alreadyRegistered') {
193
+ showMessage('This device already has a passkey');
194
+ return;
195
+ }
196
+ }
197
+ throw e;
198
+ }
199
+ ```
200
+
201
+ `alreadyRegistered` comes only from `createPasskey`. It means this device or credential provider
202
+ already holds one of the `excludeCredentials`. Show "this device already has a passkey" rather
203
+ than a generic failure.
204
+
205
+ Native mapping details:
206
+
207
+ | Platform | Native error | `code` |
208
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
209
+ | iOS | `ASAuthorizationError.canceled` (1001) | `canceled` |
210
+ | iOS | `.failed` (1004) whose description contains `is not associated with domain` | `domainNotAssociated` |
211
+ | iOS | `.matchedExcludedCredential` (1006, iOS 18+, registration with `excludedCredentials` only) | `alreadyRegistered` |
212
+ | iOS | other `ASAuthorizationError` codes (1000, 1002, 1003, 1004, 1005 and future codes) | `unknown` |
213
+ | iOS | options JSON or base64url decode failure, missing `rp.id` / `rpId` | `invalidOptions` |
214
+ | iOS | missing attestation object, authenticator data or signature; wrong credential class; no window; a request already in progress | `unknown` |
215
+ | Android | `Create/GetCredentialCancellationException`; DOM `NotAllowedError`, `AbortError`, `TimeoutError` | `canceled` |
216
+ | Android | `NoCredentialException` | `noCredentials` |
217
+ | Android | DOM `SecurityError` (rpID not verified against `assetlinks.json`) | `domainNotAssociated` |
218
+ | Android | provider configuration / unsupported / no-create-option exceptions; DOM `NotSupportedError` | `unsupported` |
219
+ | Android | `IllegalArgumentException` or `JSONException` building the request; DOM `DataError`, `EncodingError`, `SyntaxError` | `invalidOptions` |
220
+ | Android | `CreatePublicKeyCredentialDomException` `InvalidStateError` (an `excludeCredentials` match) | `alreadyRegistered` |
221
+ | Android | interrupted / unknown exceptions, other DOM errors, non-passkey result, no Activity, request in progress, any other `Throwable` | `unknown` |
222
+ | JS | `isPasskeySupported()` is `false` | `unsupported` |
223
+ | JS | option validation failure | `invalidOptions` |
224
+ | JS | unparseable or malformed native response JSON | `unknown` |
225
+
226
+ ## 8. Platform notes
227
+
228
+ - **`excludeCredentials` and `alreadyRegistered` depend on the platform:**
229
+ - iOS 18+ and Android (when the credential provider honours the list) report a match as
230
+ `alreadyRegistered`.
231
+ - iOS 17.4 to 17.x honours the list but never reports a match as `alreadyRegistered`
232
+ (typically `unknown`), because error 1006 does not exist before iOS 18.
233
+ - iOS 16.0 to 17.3 ignores the list and registers anyway.
234
+
235
+ Either way, the server must refuse duplicate registrations itself.
236
+
237
+ - iOS ignores `timeout`, `hints`, `attestationFormats`, `extensions`, `residentKey`,
238
+ `requireResidentKey` and `authenticatorAttachment`, and always returns
239
+ `clientExtensionResults: {}`.
240
+ - On iOS, `domainNotAssociated` detection is best-effort (a string match on the OS error).
241
+ - iOS never returns `noCredentials`; it shows the hybrid (QR code) sheet instead.
242
+ - Only one request can be in flight at a time. A second concurrent call rejects with `unknown`
243
+ ("already in progress").
244
+ - Android passes the JSON through untouched, so its responses may carry extra fields
245
+ (`publicKey`, `publicKeyAlgorithm`, `authenticatorData`). `@simplewebauthn/server` ignores them.
246
+ - On Android, `isPasskeySupported()` is `true` for SDK 28 and above.
247
+
248
+ ## Example app & test server
249
+
250
+ `example/` is a bare React Native app that runs the full register and sign-in flows against
251
+ `example/server`, a small in-memory `@simplewebauthn/server` server that also serves the
252
+ `apple-app-site-association` and `assetlinks.json` files. See
253
+ [`example/server/README.md`](example/server/README.md) for the full details.
254
+
255
+ ```sh
256
+ env RP_ID=passkeys.example.com APPLE_APP_ID=TEAMID.com.nitropasskeysexample \
257
+ bun run example:server --record
258
+ ```
259
+
260
+ Passkeys need HTTPS on the real `RP_ID`, so expose port `8787` (the default `PORT`) through a
261
+ tunnel whose host is `RP_ID`. Then:
262
+
263
+ - Set `SERVER_URL` in `example/src/config.ts` to `https://<RP_ID>`.
264
+ - iOS: set the `PASSKEYS_RP_ID` build setting on the app target in Xcode to `<RP_ID>`. The
265
+ entitlement is `webcredentials:$(PASSKEYS_RP_ID)?mode=developer`, so enable Associated Domains
266
+ Development on the device, and sign with your team.
267
+ - Android: the server reads the debug keystore fingerprint automatically (override with
268
+ `ANDROID_CERT_SHA256`) and prints the computed `android:apk-key-hash:` origin on startup.
269
+
270
+ Server environment variables: `RP_ID` (required), `PORT` (`8787`), `EXPECTED_ORIGINS`,
271
+ `APPLE_APP_ID`, `ANDROID_PACKAGE`, `ANDROID_CERT_SHA256`.
272
+
273
+ `--record` writes real device output to `src/__tests__/fixtures/`; once committed,
274
+ `bun test src` runs `roundtrip.test.ts` against it (the test is skipped until the fixtures exist).
275
+
276
+ ## Development
277
+
278
+ ```sh
279
+ bun test src # unit tests (validation, PasskeyError, response parsing)
280
+ bun run typecheck
281
+ bun run build
282
+ bun run codegen # nitrogen + build
283
+ ```
284
+
285
+ Native unit tests:
286
+
287
+ ```sh
288
+ # iOS XCTest
289
+ xcodebuild test -workspace example/ios/NitroPasskeysExample.xcworkspace \
290
+ -scheme NitroPasskeysExample -destination 'platform=iOS Simulator,name=iPhone 17'
291
+
292
+ # Android JUnit (run from example/android)
293
+ ./gradlew :react-native-nitro-passkeys:testDebugUnitTest
294
+ ```
295
+
296
+ [React Native Harness](https://www.react-native-harness.dev) integration tests exercise the
297
+ JS to Nitro to native path without system sheets: JS validation through the real entry point,
298
+ `PasskeyError` across the bridge, and (iOS only) an unassociated domain returning
299
+ `domainNotAssociated`:
300
+
301
+ ```sh
302
+ env HARNESS_IOS_VERSION=27.0 bun run --cwd example test:harness:ios
303
+ bun run --cwd example test:harness:android
304
+ ```
305
+
306
+ `HARNESS_IOS_VERSION` selects the simulator runtime (CI uses `26.4`).
307
+
308
+ Successful create/get ceremonies, `canceled`, `alreadyRegistered` and the Android
309
+ `domainNotAssociated` case cannot be driven without system sheets and are verified manually
310
+ with the example app and server.
311
+
312
+ ## License
313
+
314
+ MIT
@@ -0,0 +1,31 @@
1
+ project(NitroPasskeys)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set (PACKAGE_NAME NitroPasskeys)
5
+ set (CMAKE_VERBOSE_MAKEFILE ON)
6
+ set (CMAKE_CXX_STANDARD 20)
7
+
8
+ # Enable Raw Props parsing in react-native (for Nitro Views)
9
+ add_compile_options(-DRN_SERIALIZABLE_STATE=1)
10
+
11
+ # Define C++ library and add all sources
12
+ add_library(${PACKAGE_NAME} SHARED
13
+ src/main/cpp/cpp-adapter.cpp
14
+ )
15
+
16
+ # Add Nitrogen specs :)
17
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroPasskeys+autolinking.cmake)
18
+
19
+ # Set up local includes
20
+ include_directories(
21
+ "src/main/cpp"
22
+ )
23
+
24
+ find_library(LOG_LIB log)
25
+
26
+ # Link all libraries together
27
+ target_link_libraries(
28
+ ${PACKAGE_NAME}
29
+ ${LOG_LIB}
30
+ android # <-- Android core
31
+ )
@@ -0,0 +1,155 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+
7
+ dependencies {
8
+ classpath "com.android.tools.build:gradle:8.8.0"
9
+ }
10
+ }
11
+
12
+ def reactNativeArchitectures() {
13
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
14
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
15
+ }
16
+
17
+ def isNewArchitectureEnabled() {
18
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
+ }
20
+
21
+ apply plugin: "com.android.library"
22
+ apply plugin: 'org.jetbrains.kotlin.android'
23
+ apply from: '../nitrogen/generated/android/NitroPasskeys+autolinking.gradle'
24
+ apply from: "./fix-prefab.gradle"
25
+
26
+ if (isNewArchitectureEnabled()) {
27
+ apply plugin: "com.facebook.react"
28
+ }
29
+
30
+ def getExtOrDefault(name) {
31
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroPasskeys_" + name]
32
+ }
33
+
34
+ def getExtOrIntegerDefault(name) {
35
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroPasskeys_" + name]).toInteger()
36
+ }
37
+
38
+ android {
39
+ namespace "com.margelo.nitro.nitropasskeys"
40
+
41
+ ndkVersion getExtOrDefault("ndkVersion")
42
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
43
+
44
+ defaultConfig {
45
+ // Hard-coded: hosts below 28 must fail the manifest merge (no lower-minSdk mode).
46
+ minSdkVersion 28
47
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
48
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
49
+
50
+ externalNativeBuild {
51
+ cmake {
52
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
53
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
54
+ abiFilters (*reactNativeArchitectures())
55
+
56
+ buildTypes {
57
+ debug {
58
+ cppFlags "-O1 -g"
59
+ }
60
+ release {
61
+ cppFlags "-O2"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ externalNativeBuild {
69
+ cmake {
70
+ path "CMakeLists.txt"
71
+ }
72
+ }
73
+
74
+ packagingOptions {
75
+ excludes = [
76
+ "META-INF",
77
+ "META-INF/**",
78
+ "**/libc++_shared.so",
79
+ "**/libfbjni.so",
80
+ "**/libjsi.so",
81
+ "**/libfolly_json.so",
82
+ "**/libfolly_runtime.so",
83
+ "**/libglog.so",
84
+ "**/libhermes.so",
85
+ "**/libhermes-executor-debug.so",
86
+ "**/libhermes_executor.so",
87
+ "**/libreactnative.so",
88
+ "**/libreactnativejni.so",
89
+ "**/libturbomodulejsijni.so",
90
+ "**/libreact_nativemodule_core.so",
91
+ "**/libjscexecutor.so"
92
+ ]
93
+ }
94
+
95
+ testOptions {
96
+ unitTests.returnDefaultValues = true
97
+ }
98
+
99
+ buildFeatures {
100
+ buildConfig true
101
+ prefab true
102
+ }
103
+
104
+ buildTypes {
105
+ release {
106
+ minifyEnabled false
107
+ }
108
+ }
109
+
110
+ lintOptions {
111
+ disable "GradleCompatible"
112
+ }
113
+
114
+ compileOptions {
115
+ sourceCompatibility JavaVersion.VERSION_1_8
116
+ targetCompatibility JavaVersion.VERSION_1_8
117
+ }
118
+
119
+ sourceSets {
120
+ main {
121
+ if (isNewArchitectureEnabled()) {
122
+ java.srcDirs += [
123
+ // React Codegen files
124
+ "${project.buildDir}/generated/source/codegen/java"
125
+ ]
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ repositories {
132
+ mavenCentral()
133
+ google()
134
+ }
135
+
136
+
137
+ dependencies {
138
+ //noinspection GradleDynamicVersion
139
+ implementation "com.facebook.react:react-native:+"
140
+ implementation project(":react-native-nitro-modules")
141
+ implementation "androidx.credentials:credentials:1.6.0"
142
+ implementation "androidx.credentials:credentials-play-services-auth:1.6.0"
143
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2" // CredentialManager APIs are suspend funs
144
+
145
+ testImplementation "junit:junit:4.13.2"
146
+ testImplementation "org.json:json:20240303" // Android's org.json is a stub on the JVM
147
+ }
148
+
149
+ if (isNewArchitectureEnabled()) {
150
+ react {
151
+ jsRootDir = file("../src/")
152
+ libraryName = "NitroPasskeys"
153
+ codegenJavaPackageName = "com.margelo.nitro.nitropasskeys"
154
+ }
155
+ }
@@ -0,0 +1,51 @@
1
+ tasks.configureEach { task ->
2
+ // Make sure that we generate our prefab publication file only after having built the native library
3
+ // so that not a header publication file, but a full configuration publication will be generated, which
4
+ // will include the .so file
5
+
6
+ def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
7
+ def matcher = task.name =~ prefabConfigurePattern
8
+ if (matcher.matches()) {
9
+ def variantName = matcher[0][1]
10
+ task.outputs.upToDateWhen { false }
11
+ task.dependsOn("externalNativeBuild${variantName}")
12
+ }
13
+ }
14
+
15
+ afterEvaluate {
16
+ def abis = reactNativeArchitectures()
17
+ rootProject.allprojects.each { proj ->
18
+ if (proj === rootProject) return
19
+
20
+ def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
21
+ config.dependencies.any { dep ->
22
+ dep.group == project.group && dep.name == project.name
23
+ }
24
+ }
25
+ if (!dependsOnThisLib && proj != project) return
26
+
27
+ if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
28
+ return
29
+ }
30
+
31
+ def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
32
+ // Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
33
+ // generate a libnameConfig.cmake file that will contain our native library (.so).
34
+ // See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
35
+ variants.all { variant ->
36
+ def variantName = variant.name
37
+ abis.each { abi ->
38
+ def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
39
+ if (!searchDir.exists()) return
40
+ def matches = []
41
+ searchDir.eachDir { randomDir ->
42
+ def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
43
+ if (prefabFile.exists()) matches << prefabFile
44
+ }
45
+ matches.each { prefabConfig ->
46
+ prefabConfig.setLastModified(System.currentTimeMillis())
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,5 @@
1
+ NitroPasskeys_kotlinVersion=2.1.20
2
+ NitroPasskeys_minSdkVersion=28
3
+ NitroPasskeys_targetSdkVersion=36
4
+ NitroPasskeys_compileSdkVersion=36
5
+ NitroPasskeys_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,9 @@
1
+ #include <jni.h>
2
+ #include <fbjni/fbjni.h>
3
+ #include "NitroPasskeysOnLoad.hpp"
4
+
5
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
6
+ return facebook::jni::initialize(vm, []() {
7
+ margelo::nitro::nitropasskeys::registerAllNatives();
8
+ });
9
+ }