@breeztech/breez-sdk-spark-react-native 0.21.0 → 0.22.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.
@@ -0,0 +1,33 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(BreezSdkSparkPasskey, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(deriveSeeds:(NSArray *)salts
6
+ rpId:(NSString *)rpId
7
+ rpName:(NSString *)rpName
8
+ userName:(NSString *)userName
9
+ userDisplayName:(NSString *)userDisplayName
10
+ autoRegister:(BOOL)autoRegister
11
+ allowCredentials:(NSArray *)allowCredentials
12
+ preferImmediatelyAvailableCredentials:(nullable NSNumber *)preferImmediatelyAvailableCredentials
13
+ resolve:(RCTPromiseResolveBlock)resolve
14
+ reject:(RCTPromiseRejectBlock)reject)
15
+
16
+ RCT_EXTERN_METHOD(createPasskey:(NSString *)rpId
17
+ rpName:(NSString *)rpName
18
+ userName:(NSString *)userName
19
+ userDisplayName:(NSString *)userDisplayName
20
+ excludeCredentials:(NSArray *)excludeCredentials
21
+ registerSalts:(NSArray *)registerSalts
22
+ resolve:(RCTPromiseResolveBlock)resolve
23
+ reject:(RCTPromiseRejectBlock)reject)
24
+
25
+ RCT_EXTERN_METHOD(isSupported:(RCTPromiseResolveBlock)resolve
26
+ reject:(RCTPromiseRejectBlock)reject)
27
+
28
+ RCT_EXTERN_METHOD(checkDomainAssociation:(NSString *)rpId
29
+ teamId:(NSString *)teamId
30
+ resolve:(RCTPromiseResolveBlock)resolve
31
+ reject:(RCTPromiseRejectBlock)reject)
32
+
33
+ @end
@@ -0,0 +1,201 @@
1
+ import AuthenticationServices
2
+ import Foundation
3
+ import React
4
+
5
+ /// React Native native module for passkey PRF operations on iOS.
6
+ ///
7
+ /// Thin RCT_EXTERN_MODULE bridge over the shared `PasskeyAssertionCore`.
8
+ /// Behavioral parity with the upstream Swift `PasskeyProvider` and the
9
+ /// Flutter plugin is enforced by routing all ASAuthorizationController
10
+ /// orchestration through the same canonical core.
11
+ @available(iOS 18.0, *)
12
+ @objc(BreezSdkSparkPasskey)
13
+ class BreezSdkSparkPasskey: NSObject {
14
+
15
+ /// Shared across the per-call cores so the post-create PRF-readiness
16
+ /// grace armed by `createPasskey` survives to an immediately-following
17
+ /// `deriveSeeds` (sparing a second prompt). The RN module is a singleton
18
+ /// per bridge, so this persists across calls; mirrors the Android module.
19
+ private let graceTracker = PostCreateGraceTracker()
20
+
21
+ @objc
22
+ static func requiresMainQueueSetup() -> Bool {
23
+ return false
24
+ }
25
+
26
+ /// Derive multiple PRF seeds in a single ceremony when supported.
27
+ ///
28
+ /// Walks `salts` two-at-a-time using a dual-salt assertion. Falls back
29
+ /// to per-salt single-salt assertion if the authenticator drops the
30
+ /// second salt. The `salts.count == 1` case short-circuits to a
31
+ /// single-salt assertion under the hood (one prompt).
32
+ @objc
33
+ func deriveSeeds(
34
+ _ salts: [String],
35
+ rpId: String,
36
+ rpName: String,
37
+ userName: String,
38
+ userDisplayName: String,
39
+ autoRegister: Bool,
40
+ allowCredentials: [String],
41
+ preferImmediatelyAvailableCredentials: NSNumber?,
42
+ resolve: @escaping RCTPromiseResolveBlock,
43
+ reject: @escaping RCTPromiseRejectBlock
44
+ ) {
45
+ // Encode salts as UTF-8 bytes; the OS uses these directly as
46
+ // PRF eval inputs (saltInput1, saltInput2).
47
+ let saltDatas: [Data] = salts.compactMap { $0.data(using: .utf8) }
48
+ guard saltDatas.count == salts.count else {
49
+ reject("ERR_PASSKEY", "Failed to encode salts as UTF-8", nil)
50
+ return
51
+ }
52
+
53
+ let allowIds: [Data] = allowCredentials.compactMap { Data(base64Encoded: $0) }
54
+ let preferImmediate = preferImmediatelyAvailableCredentials?.boolValue
55
+
56
+ let core = PasskeyAssertionCore(
57
+ rpId: rpId, rpName: rpName, userName: userName, userDisplayName: userDisplayName,
58
+ graceTracker: graceTracker
59
+ )
60
+ Task { @MainActor in
61
+ do {
62
+ let derivation = try await core.deriveSeeds(
63
+ salts: saltDatas,
64
+ autoRegister: autoRegister,
65
+ allowCredentials: allowIds,
66
+ preferImmediatelyAvailableCredentials: preferImmediate ?? true
67
+ )
68
+ resolve([
69
+ "seeds": derivation.seeds.map { $0.base64EncodedString() },
70
+ "credentialId": derivation.credentialId.map { $0.base64EncodedString() } ?? NSNull(),
71
+ ])
72
+ } catch let err as PasskeyAssertionError {
73
+ Self.reject(err, reject: reject, defaultMessage: "User cancelled authentication")
74
+ } catch {
75
+ reject("ERR_PASSKEY", error.localizedDescription, nil)
76
+ }
77
+ }
78
+ }
79
+
80
+ /// Create a new passkey with PRF support.
81
+ ///
82
+ /// Only registers the credential, no seed derivation. Triggers exactly
83
+ /// 1 platform prompt. Use for multi-step onboarding flows. Per-call
84
+ /// overrides on userId, userName, userDisplayName fall back to the
85
+ /// constructor values when null.
86
+ @objc
87
+ func createPasskey(
88
+ _ rpId: String,
89
+ rpName: String,
90
+ userName: String,
91
+ userDisplayName: String,
92
+ excludeCredentials: [String],
93
+ registerSalts: [String],
94
+ resolve: @escaping RCTPromiseResolveBlock,
95
+ reject: @escaping RCTPromiseRejectBlock
96
+ ) {
97
+ let excludeIds: [Data] = excludeCredentials.compactMap { Data(base64Encoded: $0) }
98
+
99
+ let core = PasskeyAssertionCore(
100
+ rpId: rpId, rpName: rpName, userName: userName, userDisplayName: userDisplayName,
101
+ graceTracker: graceTracker
102
+ )
103
+ Task { @MainActor in
104
+ do {
105
+ let registration = try await core.register(
106
+ excludeCredentials: excludeIds,
107
+ salts: registerSalts
108
+ )
109
+ let registered = registration.credential
110
+ // Null unless the authenticator evaluated PRF during the
111
+ // create ceremony and returned one output per salt. The JS
112
+ // side then skips the assertion entirely.
113
+ resolve([
114
+ "credentialId": registered.credentialId.base64EncodedString(),
115
+ "userId": registered.userId?.base64EncodedString() as Any?,
116
+ "aaguid": registered.aaguid?.base64EncodedString() as Any?,
117
+ "backupEligible": registered.backupEligible as Any?,
118
+ "seeds": registration.seeds?.map { $0.base64EncodedString() } as Any?,
119
+ ])
120
+ } catch let err as PasskeyAssertionError {
121
+ Self.reject(err, reject: reject, defaultMessage: "User cancelled registration")
122
+ } catch {
123
+ reject("ERR_PASSKEY", error.localizedDescription, nil)
124
+ }
125
+ }
126
+ }
127
+
128
+ /// Check if PRF-capable passkeys are available on this device.
129
+ @objc
130
+ func isSupported(
131
+ _ resolve: @escaping RCTPromiseResolveBlock,
132
+ reject: @escaping RCTPromiseRejectBlock
133
+ ) {
134
+ if #available(iOS 18.0, *) {
135
+ resolve(true)
136
+ } else {
137
+ resolve(false)
138
+ }
139
+ }
140
+
141
+ /// Domain association check. Delegates to the canonical
142
+ /// `PasskeyAssertionCore` which probes Apple's AASA CDN
143
+ /// (`app-site-association.cdn-apple.com`) for the bundle ID's
144
+ /// `webcredentials` listing. Verification-level failures (no
145
+ /// team ID, network errors, malformed JSON) all map to `Skipped`
146
+ ///: the SDK never blocks on this check, it's diagnostic.
147
+ @objc
148
+ func checkDomainAssociation(
149
+ _ rpId: String,
150
+ teamId: String?,
151
+ resolve: @escaping RCTPromiseResolveBlock,
152
+ reject: @escaping RCTPromiseRejectBlock
153
+ ) {
154
+ // Branding fields are unused by the domain check; pass rpId as
155
+ // a placeholder since this is a check-only core.
156
+ let core = PasskeyAssertionCore(
157
+ rpId: rpId, rpName: rpId, userName: rpId, userDisplayName: rpId,
158
+ explicitTeamId: teamId
159
+ )
160
+ Task { @MainActor in
161
+ let outcome = await core.checkDomainAssociation()
162
+ switch outcome {
163
+ case .associated:
164
+ resolve(["kind": "Associated"])
165
+ case .notAssociated(let source, let reason):
166
+ resolve(["kind": "NotAssociated", "source": source, "reason": reason])
167
+ case .skipped(let reason):
168
+ resolve(["kind": "Skipped", "reason": reason])
169
+ }
170
+ }
171
+ }
172
+
173
+ // MARK: - Error mapping
174
+
175
+ private static func reject(
176
+ _ err: PasskeyAssertionError,
177
+ reject: @escaping RCTPromiseRejectBlock,
178
+ defaultMessage: String
179
+ ) {
180
+ switch err {
181
+ case .userCancelled:
182
+ reject("ERR_USER_CANCELLED", defaultMessage, nil)
183
+ case .userTimedOut:
184
+ reject("ERR_USER_TIMED_OUT", "Authenticator timed out", nil)
185
+ case .credentialNotFound(let msg):
186
+ reject("ERR_NO_CREDENTIAL", msg, nil)
187
+ case .credentialAlreadyExists(let msg):
188
+ reject("ERR_CREDENTIAL_ALREADY_EXISTS", msg, nil)
189
+ case .prfNotSupported:
190
+ reject("ERR_PRF_NOT_SUPPORTED", "PRF not supported by authenticator", nil)
191
+ case .prfEvaluationFailed(let msg):
192
+ reject("ERR_PRF_NOT_SUPPORTED", msg, nil)
193
+ case .configuration(let msg):
194
+ reject("ERR_CONFIGURATION", msg, nil)
195
+ case .authenticationFailed(let msg):
196
+ reject("ERR_PASSKEY", msg, nil)
197
+ case .generic(let msg):
198
+ reject("ERR_PASSKEY", msg, nil)
199
+ }
200
+ }
201
+ }