@authon/react-native-wallet 0.1.0 → 0.5.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/CHANGELOG.md +24 -0
- package/README.md +58 -4
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +16 -8
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/appleSignIn.d.ts +26 -0
- package/build/appleSignIn.d.ts.map +1 -0
- package/build/appleSignIn.js +76 -0
- package/build/appleSignIn.js.map +1 -0
- package/build/client.d.ts +18 -2
- package/build/client.d.ts.map +1 -1
- package/build/client.js +88 -6
- package/build/client.js.map +1 -1
- package/build/errors.d.ts +8 -1
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +7 -1
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/native.d.ts +11 -1
- package/build/native.d.ts.map +1 -1
- package/build/native.js.map +1 -1
- package/build/types.d.ts +47 -1
- package/build/types.d.ts.map +1 -1
- package/ios/AuthonReactNativeWallet.podspec +9 -3
- package/ios/AuthonWalletModule.swift +72 -3
- package/ios/vendor/AuthonAppleSignIn/AppleSignIn.swift +285 -0
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
- package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
- package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
- package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
- package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
- package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
- package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
- package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
- package/ios/vendor/CKeccak/README.md +6 -3
- package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -0
- package/ios/vendor/CMonocypher/LICENCE.md +173 -0
- package/ios/vendor/CMonocypher/README.md +27 -0
- package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
- package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
- package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
- package/ios/vendor/CMonocypher/monocypher.c +2956 -0
- package/ios/vendor/GENERATED.md +1 -1
- package/package.json +3 -2
- package/src/appleSignIn.ts +94 -0
- package/src/client.ts +90 -6
- package/src/errors.ts +7 -1
- package/src/index.ts +7 -0
- package/src/native.ts +6 -2
- package/src/types.ts +52 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonAppleSignIn/AppleSignIn.swift. Do not edit: change the source and run the script.
|
|
2
|
+
import AuthenticationServices
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import Foundation
|
|
5
|
+
import Security
|
|
6
|
+
#if canImport(UIKit)
|
|
7
|
+
import UIKit
|
|
8
|
+
#elseif canImport(AppKit)
|
|
9
|
+
import AppKit
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
/// Error of the native Apple sign-in. `code` is a sign-in code shared by every Authon SDK
|
|
13
|
+
/// (docs/wallet/NATIVE.md section 10): OAUTH_CANCELLED (show nothing), OAUTH_START_FAILED,
|
|
14
|
+
/// OAUTH_REDIRECT_NOT_ALLOWED (app not registered or approved), OAUTH_PROVIDER_ERROR,
|
|
15
|
+
/// OAUTH_STATE_MISMATCH, OAUTH_EXCHANGE_FAILED, ACCOUNT_LINK_REQUIRED, NETWORK_ERROR,
|
|
16
|
+
/// RATE_LIMITED, API_ERROR.
|
|
17
|
+
public struct AppleSignInError: Error, Equatable, LocalizedError, Sendable {
|
|
18
|
+
public let code: String
|
|
19
|
+
public let message: String
|
|
20
|
+
/// The server's request ID when the error came from an API response.
|
|
21
|
+
public let diagnosticId: String?
|
|
22
|
+
/// The server's own `code`, when it sent one (e.g. APP_NOT_APPROVED).
|
|
23
|
+
public let serverCode: String?
|
|
24
|
+
public let statusCode: Int
|
|
25
|
+
/// Whether the same call can succeed again. False for Apple ID sheet failures: showing the
|
|
26
|
+
/// same sheet again does not help (e.g. no Apple account on the device).
|
|
27
|
+
public let retryable: Bool
|
|
28
|
+
|
|
29
|
+
public var errorDescription: String? { message }
|
|
30
|
+
|
|
31
|
+
init(_ code: String, _ message: String, diagnosticId: String? = nil, serverCode: String? = nil, statusCode: Int = 0, retryable: Bool? = nil) {
|
|
32
|
+
self.code = code
|
|
33
|
+
self.message = message
|
|
34
|
+
self.diagnosticId = diagnosticId
|
|
35
|
+
self.serverCode = serverCode
|
|
36
|
+
self.statusCode = statusCode
|
|
37
|
+
self.retryable = retryable ?? ["OAUTH_START_FAILED", "OAUTH_PROVIDER_ERROR", "OAUTH_STATE_MISMATCH", "OAUTH_EXCHANGE_FAILED", "NETWORK_ERROR", "RATE_LIMITED", "API_ERROR"].contains(code)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// What the Apple ID sheet returned. Credentials only travel to the Authon API.
|
|
42
|
+
public struct AppleIDResult: Sendable {
|
|
43
|
+
public var identityToken: Data?
|
|
44
|
+
public var authorizationCode: Data?
|
|
45
|
+
public var fullName: PersonNameComponents?
|
|
46
|
+
|
|
47
|
+
public init(identityToken: Data?, authorizationCode: Data?, fullName: PersonNameComponents?) {
|
|
48
|
+
self.identityToken = identityToken
|
|
49
|
+
self.authorizationCode = authorizationCode
|
|
50
|
+
self.fullName = fullName
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Shows the Apple ID sheet for a nonce (already hashed: lowercase hex SHA-256 of the server nonce).
|
|
55
|
+
public protocol AppleIDAuthorizing: Sendable {
|
|
56
|
+
func authorize(hashedNonce: String) async throws -> AppleIDResult
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Native Sign in with Apple against Authon's nonce-bound flow:
|
|
60
|
+
/// `POST /v1/auth/oauth/native/apple/start` → Apple ID sheet → `POST /v1/auth/oauth/native/apple/complete`.
|
|
61
|
+
/// There is no browser and no web fallback. An attempt is used once: after any failure, start again.
|
|
62
|
+
/// The identity token, authorization code, nonce and verifier are never logged or returned.
|
|
63
|
+
public final class AuthonAppleSignIn: @unchecked Sendable {
|
|
64
|
+
private let base: String
|
|
65
|
+
private let publishableKey: String
|
|
66
|
+
private let appId: String?
|
|
67
|
+
private let authorizer: AppleIDAuthorizing
|
|
68
|
+
private let session: URLSession
|
|
69
|
+
|
|
70
|
+
/// - Parameters:
|
|
71
|
+
/// - appId: `TEAMID.bundleId` as registered in the Authon console; detected from the default
|
|
72
|
+
/// keychain access group when nil.
|
|
73
|
+
/// - anchor: The window for the Apple ID sheet (the key window when nil).
|
|
74
|
+
public convenience init(publishableKey: String, apiURL: String = "https://api.authon.dev", appId: String? = nil,
|
|
75
|
+
anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
|
|
76
|
+
self.init(publishableKey: publishableKey, apiURL: apiURL, appId: appId, authorizer: SystemAppleIDAuthorizer(anchor: anchor), session: .shared)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public init(publishableKey: String, apiURL: String, appId: String?, authorizer: AppleIDAuthorizing, session: URLSession = .shared) {
|
|
80
|
+
var b = apiURL
|
|
81
|
+
while b.hasSuffix("/") { b.removeLast() }
|
|
82
|
+
base = b
|
|
83
|
+
self.publishableKey = publishableKey
|
|
84
|
+
self.appId = appId
|
|
85
|
+
self.authorizer = authorizer
|
|
86
|
+
self.session = session
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Runs the sign-in and returns the API's JSON result: a session
|
|
90
|
+
/// (`accessToken`, `refreshToken`, `expiresIn`, `user`) or an MFA challenge (`mfaRequired`, `mfaToken`).
|
|
91
|
+
public func signIn() async throws -> [String: Any] {
|
|
92
|
+
guard let appId = appId ?? Self.detectAppIdentifier() else {
|
|
93
|
+
throw AppleSignInError("OAUTH_REDIRECT_NOT_ALLOWED", "appId (TEAMID.bundleId) could not be determined")
|
|
94
|
+
}
|
|
95
|
+
var verifierBytes = [UInt8](repeating: 0, count: 32)
|
|
96
|
+
guard SecRandomCopyBytes(kSecRandomDefault, verifierBytes.count, &verifierBytes) == errSecSuccess else {
|
|
97
|
+
throw AppleSignInError("OAUTH_START_FAILED", "random source unavailable")
|
|
98
|
+
}
|
|
99
|
+
let verifier = Self.base64url(Data(verifierBytes))
|
|
100
|
+
let challenge = Self.base64url(Data(SHA256.hash(data: Data(verifier.utf8))))
|
|
101
|
+
|
|
102
|
+
let start: [String: Any]
|
|
103
|
+
do {
|
|
104
|
+
start = try await post("/v1/auth/oauth/native/apple/start", ["appId": appId, "codeChallenge": challenge])
|
|
105
|
+
} catch let e as AppleSignInError where e.code == "API_ERROR" {
|
|
106
|
+
throw AppleSignInError("OAUTH_START_FAILED", e.message, diagnosticId: e.diagnosticId, serverCode: e.serverCode, statusCode: e.statusCode)
|
|
107
|
+
}
|
|
108
|
+
guard let attemptId = start["attemptId"] as? String, let nonce = start["nonce"] as? String, !nonce.isEmpty else {
|
|
109
|
+
throw AppleSignInError("OAUTH_START_FAILED", "Unexpected response from the server")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let apple = try await authorizer.authorize(hashedNonce: Self.hashNonce(nonce))
|
|
113
|
+
guard let tokenData = apple.identityToken, let identityToken = String(data: tokenData, encoding: .utf8),
|
|
114
|
+
let codeData = apple.authorizationCode, let authorizationCode = String(data: codeData, encoding: .utf8) else {
|
|
115
|
+
throw AppleSignInError("OAUTH_PROVIDER_ERROR", "Apple returned no identity token or authorization code", retryable: false)
|
|
116
|
+
}
|
|
117
|
+
var body: [String: Any] = ["attemptId": attemptId, "codeVerifier": verifier, "identityToken": identityToken, "authorizationCode": authorizationCode]
|
|
118
|
+
if let name = apple.fullName.map({ PersonNameComponentsFormatter.localizedString(from: $0, style: .default) })?
|
|
119
|
+
.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty {
|
|
120
|
+
body["displayName"] = name
|
|
121
|
+
}
|
|
122
|
+
// Single use: no retry, whatever the outcome.
|
|
123
|
+
return try await post("/v1/auth/oauth/native/apple/complete", body)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Lowercase hex SHA-256 of the server nonce (what ASAuthorizationAppleIDRequest.nonce carries).
|
|
127
|
+
public static func hashNonce(_ nonce: String) -> String {
|
|
128
|
+
SHA256.hash(data: Data(nonce.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static func base64url(_ data: Data) -> String {
|
|
132
|
+
data.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// MARK: - HTTP
|
|
136
|
+
|
|
137
|
+
private func post(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
|
|
138
|
+
guard let url = URL(string: base + path) else { throw AppleSignInError("API_ERROR", "Invalid URL") }
|
|
139
|
+
var req = URLRequest(url: url)
|
|
140
|
+
req.httpMethod = "POST"
|
|
141
|
+
req.setValue("application/json", forHTTPHeaderField: "content-type")
|
|
142
|
+
req.setValue("application/json", forHTTPHeaderField: "accept")
|
|
143
|
+
req.setValue(publishableKey, forHTTPHeaderField: "x-api-key")
|
|
144
|
+
req.httpShouldHandleCookies = false
|
|
145
|
+
req.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
146
|
+
let data: Data
|
|
147
|
+
let response: URLResponse
|
|
148
|
+
do {
|
|
149
|
+
(data, response) = try await session.data(for: req)
|
|
150
|
+
} catch {
|
|
151
|
+
throw AppleSignInError("NETWORK_ERROR", "The server could not be reached")
|
|
152
|
+
}
|
|
153
|
+
let http = response as? HTTPURLResponse
|
|
154
|
+
let status = http?.statusCode ?? 0
|
|
155
|
+
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
|
|
156
|
+
guard (200..<300).contains(status) else {
|
|
157
|
+
let serverCode = json?["code"] as? String
|
|
158
|
+
let id = (json?["requestId"] as? String) ?? http?.value(forHTTPHeaderField: "X-Request-Id")
|
|
159
|
+
let message = (json?["message"] as? String) ?? HTTPURLResponse.localizedString(forStatusCode: status)
|
|
160
|
+
throw AppleSignInError(Self.code(status: status, serverCode: serverCode), message, diagnosticId: id, serverCode: serverCode, statusCode: status)
|
|
161
|
+
}
|
|
162
|
+
guard let json else { throw AppleSignInError("API_ERROR", "Unexpected response from the server", statusCode: status) }
|
|
163
|
+
return json
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static func code(status: Int, serverCode: String?) -> String {
|
|
167
|
+
switch serverCode {
|
|
168
|
+
case "OAUTH_STATE_INVALID": return "OAUTH_STATE_MISMATCH"
|
|
169
|
+
case "OAUTH_EXCHANGE_FAILED": return "OAUTH_EXCHANGE_FAILED"
|
|
170
|
+
case "OAUTH_REDIRECT_NOT_ALLOWED", "APP_NOT_REGISTERED", "APP_NOT_APPROVED": return "OAUTH_REDIRECT_NOT_ALLOWED"
|
|
171
|
+
case "EXPLICIT_IDENTITY_LINK_REQUIRED": return "ACCOUNT_LINK_REQUIRED"
|
|
172
|
+
case "RATE_LIMITED": return "RATE_LIMITED"
|
|
173
|
+
default: return status == 429 ? "RATE_LIMITED" : "API_ERROR"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// `TEAMID.bundleId` from the app's default keychain access group.
|
|
178
|
+
static func detectAppIdentifier() -> String? {
|
|
179
|
+
var q: [String: Any] = [
|
|
180
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
181
|
+
kSecAttrService as String: "dev.authon.apple-sign-in.app-id",
|
|
182
|
+
kSecAttrAccount as String: "probe",
|
|
183
|
+
kSecReturnAttributes as String: true,
|
|
184
|
+
]
|
|
185
|
+
#if os(macOS)
|
|
186
|
+
q[kSecUseDataProtectionKeychain as String] = true
|
|
187
|
+
#endif
|
|
188
|
+
var out: AnyObject?
|
|
189
|
+
var status = SecItemCopyMatching(q as CFDictionary, &out)
|
|
190
|
+
if status == errSecItemNotFound {
|
|
191
|
+
var add = q
|
|
192
|
+
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
193
|
+
status = SecItemAdd(add as CFDictionary, &out)
|
|
194
|
+
}
|
|
195
|
+
guard status == errSecSuccess, let attrs = out as? [String: Any], let group = attrs[kSecAttrAccessGroup as String] as? String,
|
|
196
|
+
group.contains(".") else { return nil }
|
|
197
|
+
return group
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The system Apple ID sheet (ASAuthorizationController), shown immediately.
|
|
202
|
+
public final class SystemAppleIDAuthorizer: AppleIDAuthorizing, @unchecked Sendable {
|
|
203
|
+
private let anchor: (@MainActor () -> ASPresentationAnchor?)?
|
|
204
|
+
|
|
205
|
+
public init(anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
|
|
206
|
+
self.anchor = anchor
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@MainActor
|
|
210
|
+
public func authorize(hashedNonce: String) async throws -> AppleIDResult {
|
|
211
|
+
let request = ASAuthorizationAppleIDProvider().createRequest()
|
|
212
|
+
request.requestedScopes = [.fullName, .email]
|
|
213
|
+
request.nonce = hashedNonce
|
|
214
|
+
let delegate = AppleIDDelegate(anchor: anchor?() ?? Self.keyWindow())
|
|
215
|
+
let credential = try await delegate.run(request)
|
|
216
|
+
return AppleIDResult(identityToken: credential.identityToken, authorizationCode: credential.authorizationCode, fullName: credential.fullName)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@MainActor
|
|
220
|
+
static func keyWindow() -> ASPresentationAnchor {
|
|
221
|
+
#if canImport(UIKit)
|
|
222
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
223
|
+
return scenes.flatMap(\.windows).first(where: \.isKeyWindow) ?? scenes.first?.windows.first ?? ASPresentationAnchor()
|
|
224
|
+
#else
|
|
225
|
+
return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
|
|
226
|
+
#endif
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// ASAuthorizationError → sign-in code. Only `.canceled` is a cancel; everything else (including
|
|
230
|
+
/// 1000 `.unknown` after dismissing "sign in to your Apple account") is a provider error that
|
|
231
|
+
/// retrying the same sheet will not fix.
|
|
232
|
+
static func map(_ error: Error) -> AppleSignInError {
|
|
233
|
+
if let e = error as? ASAuthorizationError, e.code == .canceled {
|
|
234
|
+
return AppleSignInError("OAUTH_CANCELLED", "Sign in with Apple was cancelled")
|
|
235
|
+
}
|
|
236
|
+
return AppleSignInError("OAUTH_PROVIDER_ERROR", "Sign in with Apple failed (\((error as NSError).code))", retryable: false)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@MainActor
|
|
241
|
+
private final class AppleIDDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
|
|
242
|
+
private let anchor: ASPresentationAnchor
|
|
243
|
+
private var continuation: CheckedContinuation<ASAuthorizationAppleIDCredential, Error>?
|
|
244
|
+
private var retain: AppleIDDelegate?
|
|
245
|
+
|
|
246
|
+
init(anchor: ASPresentationAnchor) {
|
|
247
|
+
self.anchor = anchor
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func run(_ request: ASAuthorizationRequest) async throws -> ASAuthorizationAppleIDCredential {
|
|
251
|
+
try await withCheckedThrowingContinuation { cont in
|
|
252
|
+
continuation = cont
|
|
253
|
+
retain = self
|
|
254
|
+
let controller = ASAuthorizationController(authorizationRequests: [request])
|
|
255
|
+
controller.delegate = self
|
|
256
|
+
controller.presentationContextProvider = self
|
|
257
|
+
controller.performRequests()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
nonisolated func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
|
262
|
+
MainActor.assumeIsolated { anchor }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
|
266
|
+
MainActor.assumeIsolated {
|
|
267
|
+
if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
|
|
268
|
+
finish(.success(credential))
|
|
269
|
+
} else {
|
|
270
|
+
finish(.failure(AppleSignInError("OAUTH_PROVIDER_ERROR", "Unexpected Apple credential type", retryable: false)))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
|
276
|
+
let mapped = SystemAppleIDAuthorizer.map(error)
|
|
277
|
+
MainActor.assumeIsolated { finish(.failure(mapped)) }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func finish(_ result: Result<ASAuthorizationAppleIDCredential, Error>) {
|
|
281
|
+
continuation?.resume(with: result)
|
|
282
|
+
continuation = nil
|
|
283
|
+
retain = nil
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -61,11 +61,16 @@ public final class AuthonWallet: @unchecked Sendable {
|
|
|
61
61
|
/// App registration and wallet settings from the API (cached).
|
|
62
62
|
public func appConfig() async throws -> WalletAppConfig {
|
|
63
63
|
if let c = lock.withLock({ cachedConfig }) { return c }
|
|
64
|
+
return try await refreshAppConfig()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Fetches app-config again (used by capabilities()); a successful answer replaces the cache.
|
|
68
|
+
func refreshAppConfig() async throws -> WalletAppConfig {
|
|
64
69
|
guard let appId = appId ?? Self.detectAppIdentifier() else {
|
|
65
70
|
throw WalletError(.originNotAllowed, "appId (TEAMID.bundleId) could not be determined")
|
|
66
71
|
}
|
|
67
72
|
let cfg = try await api.appConfig(platform: "ios", appId: appId)
|
|
68
|
-
lock.withLock { cachedConfig = cfg }
|
|
73
|
+
if cfg.projectFound { lock.withLock { cachedConfig = cfg } }
|
|
69
74
|
return cfg
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -135,9 +140,10 @@ public final class AuthonWallet: @unchecked Sendable {
|
|
|
135
140
|
try await run { ops, s in try PublicWallet(try await ops.create(s)) }
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
/// Addresses of accounts start..<start+count (m/44'/60'/0'/0/n
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
/// Addresses of accounts start..<start+count on `chain` (EVM: m/44'/60'/0'/0/n; other paths in
|
|
144
|
+
/// `WalletChain.path`). A wallet from before HD wallets has only EVM account 0 (ACCOUNT_UNAVAILABLE).
|
|
145
|
+
public func accounts(start: Int = 0, count: Int = 1, chain: WalletChain = .evm) async throws -> [WalletAccount] {
|
|
146
|
+
try await run { ops, s in try await ops.accounts(s, start: start, count: count, chain: chain) }
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
/// EIP-191 personal_sign. `account` > 0 must come with the address from `accounts()`.
|
|
@@ -166,6 +172,20 @@ public final class AuthonWallet: @unchecked Sendable {
|
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
/// Signs a transaction the app built for another chain (EVM uses `signTransaction`: UNSUPPORTED_TYPE).
|
|
176
|
+
/// - Solana: `["message": <serialized Message, base64>, "cluster": "devnet"]` (cluster optional,
|
|
177
|
+
/// only labels the approval) → `["signature": <base58>]`.
|
|
178
|
+
/// - TRON: `["rawDataHex": <raw_data protobuf, hex>]` → `["txID": <hex>, "signature": <r||s||v hex>]`.
|
|
179
|
+
/// - Bitcoin / testnet: `["psbt": <base64>]` → `["psbt": <signed PSBT base64>, "txHex": <raw tx>]`
|
|
180
|
+
/// (`txHex` only when every input was the account's).
|
|
181
|
+
/// - XRP: `["txBlobHex": <encodeForSigning output>]` → `["signature": <DER, uppercase hex>]`.
|
|
182
|
+
/// `address` is the expected address of `account` on that chain (from `accounts(chain:)`).
|
|
183
|
+
public func signChainTransaction(chain: WalletChain, account: Int = 0, address: String, payload: [String: Any]) async throws -> [String: String] {
|
|
184
|
+
try await run { ops, s in
|
|
185
|
+
try await ops.signChainTransaction(s, chain: chain, account: account, address: address, payload: try JSONValue.from(payload))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
/// Creates the passkey backup (iOS 18 / macOS 15 for PRF).
|
|
170
190
|
public func protect() async throws -> ProtectResult {
|
|
171
191
|
try await run(passkeys: true) { ops, s in try await ops.protect(s) }
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Capabilities.swift. Do not edit: change the source and run the script.
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import Foundation
|
|
4
|
+
import LocalAuthentication
|
|
5
|
+
import Security
|
|
6
|
+
|
|
7
|
+
/// Pre-sign-in check (docs/wallet/NATIVE.md section 9): whether this app and device can use the
|
|
8
|
+
/// wallet. Needs no session, opens no login or passkey UI and never creates a wallet.
|
|
9
|
+
public struct WalletCapabilities: Equatable, Sendable {
|
|
10
|
+
public enum Status: String, Sendable {
|
|
11
|
+
case available, unavailable, unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public struct Check: Equatable, Sendable {
|
|
15
|
+
public var status: Status
|
|
16
|
+
/// Section 9 reason code; nil when available.
|
|
17
|
+
public var reason: String?
|
|
18
|
+
|
|
19
|
+
public static let available = Check(status: .available, reason: nil)
|
|
20
|
+
static func unavailable(_ reason: String) -> Check { Check(status: .unavailable, reason: reason) }
|
|
21
|
+
static func unknown(_ reason: String) -> Check { Check(status: .unknown, reason: reason) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// unavailable if any check is unavailable, else unknown if any is unknown, else available.
|
|
25
|
+
public var ready: Status
|
|
26
|
+
/// App registration and approval (GET /v1/wallet/app-config).
|
|
27
|
+
public var app: Check
|
|
28
|
+
/// The embedded wallet is enabled for the project.
|
|
29
|
+
public var wallet: Check
|
|
30
|
+
/// Hardware-protected key storage (Secure Enclave on a real device).
|
|
31
|
+
public var secureStorage: Check
|
|
32
|
+
/// A device passcode or biometrics is set.
|
|
33
|
+
public var deviceLock: Check
|
|
34
|
+
/// Passkey PRF backup and restore (static OS support only).
|
|
35
|
+
public var passkeyBackup: Check
|
|
36
|
+
/// `X-Request-Id` of the app-config response.
|
|
37
|
+
public var diagnosticId: String?
|
|
38
|
+
|
|
39
|
+
init(app: Check, wallet: Check, secureStorage: Check, deviceLock: Check, passkeyBackup: Check, diagnosticId: String?) {
|
|
40
|
+
self.app = app
|
|
41
|
+
self.wallet = wallet
|
|
42
|
+
self.secureStorage = secureStorage
|
|
43
|
+
self.deviceLock = deviceLock
|
|
44
|
+
self.passkeyBackup = passkeyBackup
|
|
45
|
+
self.diagnosticId = diagnosticId
|
|
46
|
+
let all = [app, wallet, secureStorage, deviceLock, passkeyBackup].map(\.status)
|
|
47
|
+
ready = all.contains(.unavailable) ? .unavailable : all.contains(.unknown) ? .unknown : .available
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Device checks behind capabilities(); replaceable in tests.
|
|
52
|
+
struct CapabilityProbes: Sendable {
|
|
53
|
+
var secureStorage: @Sendable () -> WalletCapabilities.Check
|
|
54
|
+
var deviceLock: @Sendable () -> WalletCapabilities.Check
|
|
55
|
+
var passkeyBackup: @Sendable () -> WalletCapabilities.Check
|
|
56
|
+
|
|
57
|
+
static let device = CapabilityProbes(
|
|
58
|
+
secureStorage: { probeSecureEnclave() },
|
|
59
|
+
deviceLock: { probeDeviceLock() },
|
|
60
|
+
passkeyBackup: { probePasskeyBackup() }
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/// The simulator never counts as hardware protection, even where it emulates an enclave.
|
|
64
|
+
/// On a device a throwaway enclave key is created and released (never stored).
|
|
65
|
+
static func probeSecureEnclave() -> WalletCapabilities.Check {
|
|
66
|
+
#if targetEnvironment(simulator)
|
|
67
|
+
return .unknown("SIMULATOR")
|
|
68
|
+
#else
|
|
69
|
+
guard SecureEnclave.isAvailable else { return .unavailable("HARDWARE_KEYSTORE_UNAVAILABLE") }
|
|
70
|
+
var error: Unmanaged<CFError>?
|
|
71
|
+
guard let access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, &error) else {
|
|
72
|
+
return .unknown("CHECK_FAILED")
|
|
73
|
+
}
|
|
74
|
+
let attrs: [String: Any] = [
|
|
75
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
76
|
+
kSecAttrKeySizeInBits as String: 256,
|
|
77
|
+
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
|
|
78
|
+
kSecPrivateKeyAttrs as String: [
|
|
79
|
+
kSecAttrIsPermanent as String: false,
|
|
80
|
+
kSecAttrAccessControl as String: access,
|
|
81
|
+
] as [String: Any],
|
|
82
|
+
]
|
|
83
|
+
guard SecKeyCreateRandomKey(attrs as CFDictionary, &error) != nil else { return .unknown("CHECK_FAILED") }
|
|
84
|
+
return .available
|
|
85
|
+
#endif
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static func probeDeviceLock() -> WalletCapabilities.Check {
|
|
89
|
+
let context = LAContext()
|
|
90
|
+
var error: NSError?
|
|
91
|
+
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { return .available }
|
|
92
|
+
if let code = error.map({ LAError.Code(rawValue: $0.code) }), code == .passcodeNotSet {
|
|
93
|
+
return .unavailable("DEVICE_LOCK_NOT_SET")
|
|
94
|
+
}
|
|
95
|
+
return .unknown("CHECK_FAILED")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static func probePasskeyBackup() -> WalletCapabilities.Check {
|
|
99
|
+
if #available(iOS 18.0, macOS 15.0, *) {
|
|
100
|
+
// The API exists; whether the user's passkey provider evaluates PRF shows on the first protect().
|
|
101
|
+
return .unknown("PRF_PROVIDER_UNVERIFIED")
|
|
102
|
+
}
|
|
103
|
+
return .unavailable("OS_UNSUPPORTED")
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
extension AuthonWallet {
|
|
108
|
+
/// See `WalletCapabilities`. Never throws: failures become `unknown` checks.
|
|
109
|
+
/// `allowSoftwareKeys` does not affect the answer.
|
|
110
|
+
public func capabilities() async -> WalletCapabilities {
|
|
111
|
+
await capabilities(probes: .device)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func capabilities(probes: CapabilityProbes) async -> WalletCapabilities {
|
|
115
|
+
let app: WalletCapabilities.Check
|
|
116
|
+
let wallet: WalletCapabilities.Check
|
|
117
|
+
var diagnosticId: String?
|
|
118
|
+
do {
|
|
119
|
+
let cfg = try await refreshAppConfig()
|
|
120
|
+
diagnosticId = cfg.diagnosticId
|
|
121
|
+
if !cfg.projectFound {
|
|
122
|
+
app = .unavailable("APP_NOT_REGISTERED")
|
|
123
|
+
wallet = .unknown("API_ERROR")
|
|
124
|
+
} else {
|
|
125
|
+
app = !cfg.allowed ? .unavailable("APP_NOT_REGISTERED") : cfg.approved ? .available : .unknown("APP_NOT_APPROVED")
|
|
126
|
+
wallet = cfg.enabled ? .available : .unavailable("WALLET_DISABLED")
|
|
127
|
+
}
|
|
128
|
+
} catch let e as WalletError where e.isNetworkFailure {
|
|
129
|
+
app = .unknown("NETWORK_ERROR")
|
|
130
|
+
wallet = .unknown("NETWORK_ERROR")
|
|
131
|
+
} catch let e as WalletError where e.code == .originNotAllowed {
|
|
132
|
+
// The app ID could not be determined, so no registration can match it.
|
|
133
|
+
app = .unavailable("APP_NOT_REGISTERED")
|
|
134
|
+
wallet = .unknown("API_ERROR")
|
|
135
|
+
} catch {
|
|
136
|
+
diagnosticId = (error as? WalletError)?.diagnosticId
|
|
137
|
+
app = .unknown("API_ERROR")
|
|
138
|
+
wallet = .unknown("API_ERROR")
|
|
139
|
+
}
|
|
140
|
+
return WalletCapabilities(
|
|
141
|
+
app: app,
|
|
142
|
+
wallet: wallet,
|
|
143
|
+
secureStorage: probes.secureStorage(),
|
|
144
|
+
deviceLock: probes.deviceLock(),
|
|
145
|
+
passkeyBackup: probes.passkeyBackup(),
|
|
146
|
+
diagnosticId: diagnosticId
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|