@qntx/wallet-native 0.5.2 → 0.7.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.
@@ -59,7 +59,7 @@ Pod::Spec.new do |s|
59
59
 
60
60
  # The kernel's backends.spark binds the qs_* C ABI; the prebuilt
61
61
  # QntxSpark.xcframework ships inside the SparkNative pod.
62
- s.frameworks = "LocalAuthentication", "Security"
62
+ s.frameworks = "LocalAuthentication", "Security", "AuthenticationServices", "UIKit"
63
63
 
64
64
  s.dependency 'NostrNative'
65
65
  s.dependency 'SparkNative'
@@ -14,20 +14,10 @@ def reactNativeArchitectures() {
14
14
  return value ? value.split(",") : ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"]
15
15
  }
16
16
 
17
- def isNewArchitectureEnabled() {
18
- return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
- }
20
-
21
17
  apply plugin: "com.android.library"
22
18
  apply plugin: 'org.jetbrains.kotlin.android'
23
19
  apply from: '../nitrogen/generated/android/WalletNative+autolinking.gradle'
24
20
 
25
- if (isNewArchitectureEnabled()) {
26
- apply plugin: "com.facebook.react"
27
-
28
-
29
- }
30
-
31
21
  def getExtOrDefault(name) {
32
22
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["WalletNative_" + name]
33
23
  }
@@ -133,23 +123,12 @@ android {
133
123
  targetCompatibility JavaVersion.VERSION_1_8
134
124
  }
135
125
 
136
- sourceSets {
137
- main {
138
- if (isNewArchitectureEnabled()) {
139
- java.srcDirs += [
140
- "${project.buildDir}/generated/source/codegen/java"
141
- ]
142
- }
143
- }
144
- }
145
126
  }
146
127
 
147
128
  dependencies {
148
- // For > 0.71, this is replaced by `com.facebook.react:react-android:$version` by the React
149
- // gradle plugin — it supplies the fbjni/ReactAndroid prefab packages the generated CMake
150
- // autolinking file finds with find_package.
151
- //noinspection GradleDynamicVersion
152
- implementation "com.facebook.react:react-native:+"
129
+ // The app's React Native Gradle plugin pins the version; the prefab packages (fbjni, ReactAndroid)
130
+ // are what the generated CMake autolinking file finds with find_package.
131
+ implementation "com.facebook.react:react-android"
153
132
 
154
133
  // Hermes engine prefab — the kernel runtime links hermesvm/libhermes
155
134
  implementation "com.facebook.react:hermes-android"
@@ -7,10 +7,11 @@ namespace qntx {
7
7
 
8
8
  // The platform passkey authenticator behind `__qntx.passkey` (W6). The kernel
9
9
  // builds the WebAuthn request JSON and parses the response JSON itself; the
10
- // platform only runs the system ceremony. Android: Credential Manager. iOS:
11
- // not yet available (W6c needs an associated domain) — every call completes
12
- // `unavailable`. Each function has exactly one strong definition per platform
13
- // (PasskeyAndroid.cpp, PasskeyIos.mm); host tests link a fake.
10
+ // platform only runs the system ceremony. Android: Credential Manager. iOS
11
+ // (W6c): ASAuthorization platform passkeys with the PRF extension on iOS 18+,
12
+ // unavailable below or without the associated domain. Each function has
13
+ // exactly one strong definition per platform (PasskeyAndroid.cpp,
14
+ // PasskeyIos.mm); host tests link a fake.
14
15
 
15
16
  // The binding maps each status to the kernel's error code: cancelled →
16
17
  // "cancelled", unavailable → "factor-unavailable", failed → "unavailable".
@@ -1,20 +1,487 @@
1
- // iOS passkey authenticator: not available yet. Native passkeys need the
2
- // app's associated domain (webcredentials), which the current free signing
3
- // team cannot provision; W6c implements ASAuthorization with PRF once a paid
4
- // team exists. Until then every ceremony reports `unavailable`, which the
5
- // kernel surfaces as factor-unavailable (no passkey wallet on this platform).
1
+ // iOS passkey authenticator (W6c): ASAuthorization platform passkeys with the
2
+ // PRF extension (iOS 18+). The kernel builds the WebAuthn request JSON and
3
+ // parses the response JSON itself; this file only reads the fields the system
4
+ // ceremony needs, runs one ASAuthorizationController at a time and repacks the
5
+ // credential into the browser `toJSON()` shape the kernel parses. A ceremony
6
+ // requested while the scene is still activating — e.g. right after a system
7
+ // Face ID prompt resigns it — waits for UISceneDidActivateNotification instead
8
+ // of failing before any sheet appears. PRF outputs are encoded straight into
9
+ // the response JSON — no copies are kept and they are never logged.
6
10
  #include "../host/Passkey.hpp"
7
11
 
12
+ #import <AuthenticationServices/AuthenticationServices.h>
13
+ #import <Foundation/Foundation.h>
14
+ #import <UIKit/UIKit.h>
15
+ #import <os/log.h>
16
+
17
+ #include <mutex>
18
+ #include <string>
19
+ #include <utility>
20
+
21
+ @class QntxPasskeySession;
22
+
23
+ namespace {
24
+
25
+ os_log_t passkeyLog() {
26
+ static os_log_t log = os_log_create("org.qntx.wallet", "passkey");
27
+ return log;
28
+ }
29
+
30
+ // The session — and the controller it owns once the ceremony actually runs —
31
+ // stays alive only while a call is in flight (controller.delegate is weak).
32
+ // It is touched on the main thread only; the mutex pairs the one-in-flight
33
+ // check with the clear on completion. A parked session (scene not yet active)
34
+ // has no controller yet, so the busy check looks at the session.
35
+ std::mutex g_mutex;
36
+ QntxPasskeySession* g_session = nil;
37
+
38
+ // Strict base64url decode (padding restored); nil on any invalid character.
39
+ NSData* _Nullable base64urlDecode(NSString* _Nullable value) {
40
+ if (value == nil) {
41
+ return nil;
42
+ }
43
+ NSMutableString* standard = [NSMutableString stringWithCapacity:value.length + 3];
44
+ for (NSUInteger i = 0; i < value.length; i += 1) {
45
+ const unichar c = [value characterAtIndex:i];
46
+ if (c == '-') {
47
+ [standard appendString:@"+"];
48
+ } else if (c == '_') {
49
+ [standard appendString:@"/"];
50
+ } else if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
51
+ [standard appendFormat:@"%C", c];
52
+ } else {
53
+ return nil;
54
+ }
55
+ }
56
+ if (standard.length % 4 == 1) {
57
+ return nil;
58
+ }
59
+ while (standard.length % 4 != 0) {
60
+ [standard appendString:@"="];
61
+ }
62
+ return [[NSData alloc] initWithBase64EncodedString:standard options:0];
63
+ }
64
+
65
+ NSString* base64urlEncode(NSData* data) {
66
+ NSString* standard = [data base64EncodedStringWithOptions:0];
67
+ standard = [standard stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
68
+ standard = [standard stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
69
+ while ([standard hasSuffix:@"="]) {
70
+ standard = [standard substringToIndex:standard.length - 1];
71
+ }
72
+ return standard;
73
+ }
74
+
75
+ NSDictionary<NSString*, id>* _Nullable dictOf(_Nullable id value) {
76
+ return [value isKindOfClass:NSDictionary.class] ? (NSDictionary<NSString*, id>*)value : nil;
77
+ }
78
+
79
+ NSString* _Nullable stringOf(_Nullable id value) {
80
+ return [value isKindOfClass:NSString.class] ? (NSString*)value : nil;
81
+ }
82
+
83
+ NSArray* _Nullable arrayOf(_Nullable id value) {
84
+ return [value isKindOfClass:NSArray.class] ? (NSArray*)value : nil;
85
+ }
86
+
87
+ NSDictionary<NSString*, id>* _Nullable jsonOf(const std::string& json) {
88
+ NSData* data = [NSData dataWithBytes:json.data() length:json.size()];
89
+ return dictOf([NSJSONSerialization JSONObjectWithData:data options:0 error:nil]);
90
+ }
91
+
92
+ // The extensions.prf.eval block the kernel always sends; nil when missing or
93
+ // malformed (eval.second is optional).
94
+ ASAuthorizationPublicKeyCredentialPRFAssertionInputValues* _Nullable prfEval(
95
+ NSDictionary<NSString*, id>* request) API_AVAILABLE(ios(18.0)) {
96
+ NSDictionary* eval = dictOf(dictOf(dictOf(request[@"extensions"])[@"prf"])[@"eval"]);
97
+ NSData* first = base64urlDecode(stringOf(eval[@"first"]));
98
+ if (first == nil) {
99
+ return nil;
100
+ }
101
+ NSData* second = nil;
102
+ if (eval[@"second"] != nil) {
103
+ second = base64urlDecode(stringOf(eval[@"second"]));
104
+ if (second == nil) {
105
+ return nil;
106
+ }
107
+ }
108
+ return [[ASAuthorizationPublicKeyCredentialPRFAssertionInputValues alloc]
109
+ initWithSaltInput1:first
110
+ saltInput2:second];
111
+ }
112
+
113
+ ASPresentationAnchor _Nullable activeKeyWindow() {
114
+ for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
115
+ if ([scene isKindOfClass:UIWindowScene.class] &&
116
+ scene.activationState == UISceneActivationStateForegroundActive) {
117
+ UIWindow* window = ((UIWindowScene*)scene).keyWindow;
118
+ if (window != nil) {
119
+ return window;
120
+ }
121
+ }
122
+ }
123
+ return nil;
124
+ }
125
+
126
+ // A window scene on its way to the foreground — e.g. right after a system
127
+ // Face ID prompt resigns the app's scene. Once it activates, its key window
128
+ // can anchor the passkey sheet.
129
+ BOOL hasInactiveWindowScene() {
130
+ for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
131
+ if ([scene isKindOfClass:UIWindowScene.class] &&
132
+ scene.activationState == UISceneActivationStateForegroundInactive &&
133
+ ((UIWindowScene*)scene).keyWindow != nil) {
134
+ return YES;
135
+ }
136
+ }
137
+ return NO;
138
+ }
139
+
140
+ std::string jsonString(NSDictionary<NSString*, id>* json) {
141
+ NSData* data = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
142
+ if (data == nil) {
143
+ return {};
144
+ }
145
+ return std::string(static_cast<const char*>(data.bytes), data.length);
146
+ }
147
+
148
+ } // namespace
149
+
150
+ API_AVAILABLE(ios(18.0))
151
+ @interface QntxPasskeySession : NSObject <ASAuthorizationControllerDelegate,
152
+ ASAuthorizationControllerPresentationContextProviding>
153
+ - (instancetype)initWithDone:(qntx::PasskeyDone)done
154
+ request:(ASAuthorizationRequest*)request;
155
+ // Runs the ceremony now, or parks it until the window scene activates.
156
+ - (void)start;
157
+ // Cancels a parked wait or the running ceremony; completes through finish:.
158
+ - (void)cancelCeremony;
159
+ - (void)finish:(qntx::PasskeyResult)result;
160
+ @end
161
+
162
+ @implementation QntxPasskeySession {
163
+ qntx::PasskeyDone _done;
164
+ ASAuthorizationRequest* _request;
165
+ ASPresentationAnchor _anchor;
166
+ ASAuthorizationController* _controller;
167
+ NSObject* _observer;
168
+ }
169
+
170
+ - (instancetype)initWithDone:(qntx::PasskeyDone)done
171
+ request:(ASAuthorizationRequest*)request {
172
+ self = [super init];
173
+ if (self != nil) {
174
+ _done = std::move(done);
175
+ _request = request;
176
+ }
177
+ return self;
178
+ }
179
+
180
+ // A ceremony started while the scene is still activating fails before any
181
+ // sheet appears — like Keystore.kt's whenResumed, park on an inactive window
182
+ // scene and run it at UISceneDidActivateNotification. A scene that is fully
183
+ // absent still reports `unavailable`.
184
+ - (void)start {
185
+ ASPresentationAnchor anchor = activeKeyWindow();
186
+ if (anchor != nil) {
187
+ [self performWithAnchor:anchor];
188
+ return;
189
+ }
190
+ if (hasInactiveWindowScene()) {
191
+ _observer = [NSNotificationCenter.defaultCenter
192
+ addObserverForName:UISceneDidActivateNotification
193
+ object:nil
194
+ queue:NSOperationQueue.mainQueue
195
+ usingBlock:^(NSNotification*) {
196
+ [self sceneActivated];
197
+ }];
198
+ return;
199
+ }
200
+ [self finish:qntx::PasskeyResult{qntx::PasskeyStatus::unavailable, {}}];
201
+ }
202
+
203
+ - (void)sceneActivated {
204
+ [NSNotificationCenter.defaultCenter removeObserver:_observer];
205
+ _observer = nil;
206
+ ASPresentationAnchor anchor = activeKeyWindow();
207
+ if (anchor == nil) {
208
+ [self finish:qntx::PasskeyResult{qntx::PasskeyStatus::unavailable, {}}];
209
+ return;
210
+ }
211
+ [self performWithAnchor:anchor];
212
+ }
213
+
214
+ - (void)performWithAnchor:(ASPresentationAnchor)anchor {
215
+ _anchor = anchor;
216
+ _controller =
217
+ [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[ _request ]];
218
+ _controller.delegate = self;
219
+ _controller.presentationContextProvider = self;
220
+ [_controller performRequests];
221
+ }
222
+
223
+ - (void)cancelCeremony {
224
+ if (_observer != nil) {
225
+ [NSNotificationCenter.defaultCenter removeObserver:_observer];
226
+ _observer = nil;
227
+ [self finish:qntx::PasskeyResult{qntx::PasskeyStatus::cancelled, {}}];
228
+ return;
229
+ }
230
+ [_controller cancel];
231
+ }
232
+
233
+ // Runs exactly once per session; clears the in-flight session before
234
+ // invoking the kernel completion.
235
+ - (void)finish:(qntx::PasskeyResult)result {
236
+ qntx::PasskeyDone done;
237
+ {
238
+ std::lock_guard<std::mutex> lock(g_mutex);
239
+ if (g_session != self) {
240
+ return;
241
+ }
242
+ g_session = nil;
243
+ done = std::move(_done);
244
+ _done = nullptr;
245
+ }
246
+ if (_observer != nil) {
247
+ [NSNotificationCenter.defaultCenter removeObserver:_observer];
248
+ _observer = nil;
249
+ }
250
+ done(std::move(result));
251
+ }
252
+
253
+ - (ASPresentationAnchor)presentationAnchorForAuthorizationController:
254
+ (ASAuthorizationController*)controller {
255
+ return _anchor;
256
+ }
257
+
258
+ - (void)authorizationController:(ASAuthorizationController*)controller
259
+ didCompleteWithAuthorization:(ASAuthorization*)authorization {
260
+ NSMutableDictionary<NSString*, id>* extensions = [NSMutableDictionary new];
261
+ NSMutableDictionary<NSString*, id>* response = [NSMutableDictionary new];
262
+ std::string credentialId;
263
+ if ([authorization.credential
264
+ isKindOfClass:ASAuthorizationPlatformPublicKeyCredentialRegistration.class]) {
265
+ auto* credential = (ASAuthorizationPlatformPublicKeyCredentialRegistration*)
266
+ authorization.credential;
267
+ NSString* id64 = base64urlEncode(credential.credentialID);
268
+ credentialId = std::string(id64.UTF8String);
269
+ response[@"clientDataJSON"] = base64urlEncode(credential.rawClientDataJSON);
270
+ if (credential.rawAttestationObject != nil) {
271
+ response[@"attestationObject"] = base64urlEncode(credential.rawAttestationObject);
272
+ }
273
+ if (credential.prf != nil) {
274
+ NSMutableDictionary<NSString*, id>* prf =
275
+ [@{ @"enabled" : @(credential.prf.isSupported) } mutableCopy];
276
+ if (credential.prf.first != nil) {
277
+ NSMutableDictionary<NSString*, id>* results =
278
+ [@{ @"first" : base64urlEncode(credential.prf.first) } mutableCopy];
279
+ if (credential.prf.second != nil) {
280
+ results[@"second"] = base64urlEncode(credential.prf.second);
281
+ }
282
+ prf[@"results"] = results;
283
+ }
284
+ extensions[@"prf"] = prf;
285
+ }
286
+ } else if ([authorization.credential
287
+ isKindOfClass:ASAuthorizationPlatformPublicKeyCredentialAssertion.class]) {
288
+ auto* credential = (ASAuthorizationPlatformPublicKeyCredentialAssertion*)
289
+ authorization.credential;
290
+ NSString* id64 = base64urlEncode(credential.credentialID);
291
+ credentialId = std::string(id64.UTF8String);
292
+ response[@"clientDataJSON"] = base64urlEncode(credential.rawClientDataJSON);
293
+ response[@"authenticatorData"] = base64urlEncode(credential.rawAuthenticatorData);
294
+ response[@"signature"] = base64urlEncode(credential.signature);
295
+ response[@"userHandle"] = base64urlEncode(credential.userID);
296
+ if (credential.prf != nil) {
297
+ NSMutableDictionary<NSString*, id>* results =
298
+ [@{ @"first" : base64urlEncode(credential.prf.first) } mutableCopy];
299
+ if (credential.prf.second != nil) {
300
+ results[@"second"] = base64urlEncode(credential.prf.second);
301
+ }
302
+ extensions[@"prf"] = @{ @"results" : results };
303
+ }
304
+ } else {
305
+ [self finish:qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}}];
306
+ return;
307
+ }
308
+ std::string json = jsonString(@{
309
+ @"id" : @(credentialId.c_str()),
310
+ @"rawId" : @(credentialId.c_str()),
311
+ @"type" : @"public-key",
312
+ @"authenticatorAttachment" : @"platform",
313
+ @"response" : response,
314
+ @"clientExtensionResults" : extensions,
315
+ });
316
+ if (json.empty()) {
317
+ [self finish:qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}}];
318
+ return;
319
+ }
320
+ [self finish:qntx::PasskeyResult{qntx::PasskeyStatus::ok, std::move(json)}];
321
+ }
322
+
323
+ - (void)authorizationController:(ASAuthorizationController*)controller
324
+ didCompleteWithError:(NSError*)error {
325
+ qntx::PasskeyStatus status = qntx::PasskeyStatus::failed;
326
+ if ([error.domain isEqualToString:ASAuthorizationErrorDomain]) {
327
+ switch (error.code) {
328
+ case ASAuthorizationErrorCanceled:
329
+ status = qntx::PasskeyStatus::cancelled;
330
+ break;
331
+ case ASAuthorizationErrorNotHandled:
332
+ case ASAuthorizationErrorFailed:
333
+ case ASAuthorizationErrorNotInteractive:
334
+ status = qntx::PasskeyStatus::unavailable;
335
+ break;
336
+ default:
337
+ // 1010 = ASAuthorizationErrorDeviceNotConfiguredForPasskeyCreation,
338
+ // compared raw so the iOS 19 symbol needs no availability guard.
339
+ status = error.code == 1010 ? qntx::PasskeyStatus::unavailable
340
+ : qntx::PasskeyStatus::failed;
341
+ }
342
+ }
343
+ // Domain and code only — userInfo/localizedDescription can name the account.
344
+ os_log_error(
345
+ passkeyLog(),
346
+ "passkey ceremony failed: %{public}@/%ld",
347
+ error.domain,
348
+ static_cast<long>(error.code));
349
+ [self finish:qntx::PasskeyResult{status, {}}];
350
+ }
351
+
352
+ @end
353
+
354
+ namespace {
355
+
356
+ ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest* _Nullable registrationRequest(
357
+ NSDictionary<NSString*, id>* request) API_AVAILABLE(ios(18.0)) {
358
+ NSDictionary* rp = dictOf(request[@"rp"]);
359
+ NSDictionary* user = dictOf(request[@"user"]);
360
+ NSString* rpId = stringOf(rp[@"id"]);
361
+ NSString* name = stringOf(user[@"name"]);
362
+ NSData* challenge = base64urlDecode(stringOf(request[@"challenge"]));
363
+ NSData* userId = base64urlDecode(stringOf(user[@"id"]));
364
+ ASAuthorizationPublicKeyCredentialPRFAssertionInputValues* eval = prfEval(request);
365
+ if (rpId == nil || name == nil || challenge == nil || userId == nil || eval == nil) {
366
+ return nil;
367
+ }
368
+ auto* provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc]
369
+ initWithRelyingPartyIdentifier:rpId];
370
+ auto* platform = [provider createCredentialRegistrationRequestWithChallenge:challenge
371
+ name:name
372
+ userID:userId];
373
+ platform.userVerificationPreference =
374
+ ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
375
+ platform.attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKindNone;
376
+ platform.prf = [[ASAuthorizationPublicKeyCredentialPRFRegistrationInput alloc]
377
+ initWithInputValues:eval];
378
+ return platform;
379
+ }
380
+
381
+ ASAuthorizationPlatformPublicKeyCredentialAssertionRequest* _Nullable assertionRequest(
382
+ NSDictionary<NSString*, id>* request) API_AVAILABLE(ios(18.0)) {
383
+ NSString* rpId = stringOf(request[@"rpId"]);
384
+ NSData* challenge = base64urlDecode(stringOf(request[@"challenge"]));
385
+ ASAuthorizationPublicKeyCredentialPRFAssertionInputValues* eval = prfEval(request);
386
+ if (rpId == nil || challenge == nil || eval == nil) {
387
+ return nil;
388
+ }
389
+ auto* provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc]
390
+ initWithRelyingPartyIdentifier:rpId];
391
+ auto* platform = [provider createCredentialAssertionRequestWithChallenge:challenge];
392
+ if (request[@"allowCredentials"] != nil) {
393
+ NSArray* allow = arrayOf(request[@"allowCredentials"]);
394
+ if (allow == nil) {
395
+ return nil;
396
+ }
397
+ NSMutableArray<ASAuthorizationPlatformPublicKeyCredentialDescriptor*>* descriptors =
398
+ [NSMutableArray arrayWithCapacity:allow.count];
399
+ for (id item in allow) {
400
+ NSData* credentialId = base64urlDecode(stringOf(dictOf(item)[@"id"]));
401
+ if (credentialId == nil) {
402
+ return nil;
403
+ }
404
+ [descriptors addObject:[[ASAuthorizationPlatformPublicKeyCredentialDescriptor alloc]
405
+ initWithCredentialID:credentialId]];
406
+ }
407
+ platform.allowedCredentials = descriptors;
408
+ }
409
+ platform.userVerificationPreference =
410
+ ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
411
+ platform.prf = [[ASAuthorizationPublicKeyCredentialPRFAssertionInput alloc]
412
+ initWithInputValues:eval
413
+ perCredentialInputValues:nil];
414
+ return platform;
415
+ }
416
+
417
+ // Claims the single in-flight slot and runs the ceremony (main thread). A nil
418
+ // request means the JSON was malformed, a busy slot a second concurrent call.
419
+ // AuthenticationServices may deliver the delegate callback synchronously, so
420
+ // the mutex never spans a call into it — or into `done`.
421
+ void run(ASAuthorizationRequest* _Nullable request, qntx::PasskeyDone done)
422
+ API_AVAILABLE(ios(18.0)) {
423
+ if (request == nil) {
424
+ done(qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}});
425
+ return;
426
+ }
427
+ QntxPasskeySession* session;
428
+ {
429
+ std::lock_guard<std::mutex> lock(g_mutex);
430
+ if (g_session == nil) {
431
+ session = g_session = [[QntxPasskeySession alloc] initWithDone:std::move(done)
432
+ request:request];
433
+ } else {
434
+ session = nil;
435
+ }
436
+ }
437
+ if (session == nil) {
438
+ done(qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}});
439
+ return;
440
+ }
441
+ [session start];
442
+ }
443
+
444
+ } // namespace
445
+
8
446
  namespace qntx {
9
447
 
10
- void passkeyCreate(std::string, PasskeyDone done) {
11
- done(PasskeyResult{PasskeyStatus::unavailable, {}});
448
+ void passkeyCreate(std::string requestJson, PasskeyDone done) {
449
+ auto json = std::make_shared<std::string>(std::move(requestJson));
450
+ auto finish = std::make_shared<PasskeyDone>(std::move(done));
451
+ dispatch_async(dispatch_get_main_queue(), ^{
452
+ if (@available(iOS 18.0, *)) {
453
+ run(registrationRequest(jsonOf(*json)), std::move(*finish));
454
+ } else {
455
+ (*finish)(PasskeyResult{PasskeyStatus::unavailable, {}});
456
+ }
457
+ });
12
458
  }
13
459
 
14
- void passkeyGet(std::string, PasskeyDone done) {
15
- done(PasskeyResult{PasskeyStatus::unavailable, {}});
460
+ void passkeyGet(std::string requestJson, PasskeyDone done) {
461
+ auto json = std::make_shared<std::string>(std::move(requestJson));
462
+ auto finish = std::make_shared<PasskeyDone>(std::move(done));
463
+ dispatch_async(dispatch_get_main_queue(), ^{
464
+ if (@available(iOS 18.0, *)) {
465
+ run(assertionRequest(jsonOf(*json)), std::move(*finish));
466
+ } else {
467
+ (*finish)(PasskeyResult{PasskeyStatus::unavailable, {}});
468
+ }
469
+ });
16
470
  }
17
471
 
18
- void passkeyCancel() {}
472
+ void passkeyCancel() {
473
+ dispatch_async(dispatch_get_main_queue(), ^{
474
+ if (@available(iOS 18.0, *)) {
475
+ QntxPasskeySession* session;
476
+ {
477
+ std::lock_guard<std::mutex> lock(g_mutex);
478
+ session = g_session;
479
+ }
480
+ // A parked wait completes `cancelled`; a running ceremony is cancelled —
481
+ // both answer through finish:, so this runs off the lock.
482
+ [session cancelCeremony];
483
+ }
484
+ });
485
+ }
19
486
 
20
487
  } // namespace qntx
package/dist/index.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { AutoLockPolicy, PasskeyConfig, Wallet } from "@qntx/wallet";
2
2
  import { MainPort } from "@qntx/wallet/host";
3
3
  import { ProtocolValue } from "@qntx/wallet/protocol";
4
- //#region src/host.d.ts
5
4
  type KernelExitReason = "locked" | "crashed" | "integrity" | "background";
6
5
  /**
7
6
  * Structural mirror of the Nitro `KernelHost` spec (W2b registers the real hybrid object; tests
@@ -15,8 +14,6 @@ type KernelHostLike = {
15
14
  readonly lock: () => void;
16
15
  };
17
16
  export declare function spawnFromHost(host: KernelHostLike, backgroundLockSeconds: number): MainPort;
18
- //#endregion
19
- //#region src/cbor.d.ts
20
17
  export declare const FRAME_MAX_BYTES: number;
21
18
  type CborErrorCode = "oversize" | "truncated" | "malformed" | "float" | "unsafe-int" | "bigint" | "undefined" | "tag" | "non-string-key" | "duplicate-key" | "indefinite" | "bad-type";
22
19
  export declare class CborError extends Error {
@@ -27,8 +24,6 @@ export declare class CborError extends Error {
27
24
  export declare function encodeFrame(value: ProtocolValue): ArrayBuffer;
28
25
  /** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */
29
26
  export declare function decodeFrame(frame: ArrayBuffer | Uint8Array): ProtocolValue;
30
- //#endregion
31
- //#region src/index.d.ts
32
27
  export type NativeWalletOptions = {
33
28
  readonly passkey: PasskeyConfig;
34
29
  readonly autoLock: AutoLockPolicy;
@@ -39,6 +34,5 @@ export type NativeWalletOptions = {
39
34
  readonly host?: KernelHostLike;
40
35
  };
41
36
  export declare function createNativeWallet(options: NativeWalletOptions): Promise<Wallet>;
42
- //#endregion
43
37
  export type { CborErrorCode, KernelExitReason, KernelHostLike };
44
38
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { WalletError } from "@qntx/wallet";
2
2
  import { connectKernel } from "@qntx/wallet/host";
3
3
  import { decode, encode } from "cborg";
4
- //#region src/cbor.ts
4
+ // src/cbor.ts
5
5
  /**
6
6
  * CBOR framing for the kernel channel. `ProtocolValue` travels main-side ↔ kernel-side as CBOR; the
7
7
  * C++ layer moves bytes only. Decode is deliberately strict: floats, unsafe integers, undefined,
@@ -83,8 +83,7 @@ function decodeFrame(frame) {
83
83
  throw classify(error);
84
84
  }
85
85
  }
86
- //#endregion
87
- //#region src/host.ts
86
+ // src/host.ts
88
87
  function spawnFromHost(host, backgroundLockSeconds) {
89
88
  const exitHandlers = /* @__PURE__ */ new Set();
90
89
  let exited = false;
@@ -135,8 +134,7 @@ function spawnFromHost(host, backgroundLockSeconds) {
135
134
  }
136
135
  };
137
136
  }
138
- //#endregion
139
- //#region src/index.ts
137
+ // src/index.ts
140
138
  /**
141
139
  * `@qntx/wallet-native`: the RN host entry. `createNativeWallet` adapts a `KernelHost` (Nitro
142
140
  * hybrid from W2b, or any object with the same shape) to the wallet `connectKernel` entry — the
@@ -164,7 +162,6 @@ async function createNativeWallet(options) {
164
162
  setTimer
165
163
  });
166
164
  }
167
- //#endregion
168
165
  export { CborError, FRAME_MAX_BYTES, createNativeWallet, decodeFrame, encodeFrame, spawnFromHost };
169
166
 
170
167
  //# sourceMappingURL=index.mjs.map