@authon/react-native-wallet 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +221 -2
  2. package/android/build.gradle +40 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +181 -0
  6. package/android/src/vendor/GENERATED.md +4 -0
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +191 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +82 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +232 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +353 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +58 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +449 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +71 -0
  28. package/build/authonContext.d.ts +19 -0
  29. package/build/authonContext.d.ts.map +1 -0
  30. package/build/authonContext.js +20 -0
  31. package/build/authonContext.js.map +1 -0
  32. package/build/client.d.ts +76 -0
  33. package/build/client.d.ts.map +1 -0
  34. package/build/client.js +349 -0
  35. package/build/client.js.map +1 -0
  36. package/build/errors.d.ts +18 -0
  37. package/build/errors.d.ts.map +1 -0
  38. package/build/errors.js +9 -0
  39. package/build/errors.js.map +1 -0
  40. package/build/index.d.ts +7 -0
  41. package/build/index.d.ts.map +1 -0
  42. package/build/index.js +4 -0
  43. package/build/index.js.map +1 -0
  44. package/build/native.d.ts +39 -0
  45. package/build/native.d.ts.map +1 -0
  46. package/build/native.js +12 -0
  47. package/build/native.js.map +1 -0
  48. package/build/types.d.ts +121 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +3 -0
  51. package/build/types.js.map +1 -0
  52. package/build/useAuthonWallet.d.ts +25 -0
  53. package/build/useAuthonWallet.d.ts.map +1 -0
  54. package/build/useAuthonWallet.js +77 -0
  55. package/build/useAuthonWallet.js.map +1 -0
  56. package/expo-module.config.json +9 -0
  57. package/ios/AuthonReactNativeWallet.podspec +37 -0
  58. package/ios/AuthonWalletModule.swift +190 -0
  59. package/ios/vendor/AuthonWallet/AuthonWallet.swift +212 -0
  60. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  61. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  62. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +104 -0
  63. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  64. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  65. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  66. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  67. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  68. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  69. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  70. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  71. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  72. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  73. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  74. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  75. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  76. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  77. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  78. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +350 -0
  79. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +555 -0
  80. package/ios/vendor/AuthonWallet/WalletError.swift +54 -0
  81. package/ios/vendor/CKeccak/LICENSE +22 -0
  82. package/ios/vendor/CKeccak/README.md +27 -0
  83. package/ios/vendor/CKeccak/byte_order.h +59 -0
  84. package/ios/vendor/CKeccak/include/ckeccak.h +9 -0
  85. package/ios/vendor/CKeccak/memzero.c +75 -0
  86. package/ios/vendor/CKeccak/memzero.h +8 -0
  87. package/ios/vendor/CKeccak/options.h +99 -0
  88. package/ios/vendor/CKeccak/sha3.c +413 -0
  89. package/ios/vendor/CKeccak/sha3.h +89 -0
  90. package/ios/vendor/CSecp256k1/COPYING +19 -0
  91. package/ios/vendor/CSecp256k1/README.md +35 -0
  92. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  93. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  94. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  95. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  96. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  97. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  98. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  99. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  100. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  101. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  102. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  103. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  104. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  105. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  106. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  107. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  108. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  109. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  110. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  111. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  112. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  113. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  114. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  115. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  116. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  117. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  118. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  119. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  120. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  121. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  122. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  123. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  124. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  125. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  126. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  127. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  128. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  129. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  130. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  131. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  132. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  133. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  134. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  135. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  136. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  137. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  138. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  139. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  140. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  141. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  142. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  143. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  144. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  145. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  146. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  147. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  148. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  149. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  150. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  151. package/ios/vendor/GENERATED.md +4 -0
  152. package/package.json +65 -3
  153. package/src/authonContext.ts +30 -0
  154. package/src/client.ts +375 -0
  155. package/src/errors.ts +52 -0
  156. package/src/index.ts +21 -0
  157. package/src/native.ts +50 -0
  158. package/src/types.ts +120 -0
  159. package/src/useAuthonWallet.ts +103 -0
@@ -0,0 +1,25 @@
1
+ import { AuthonWalletClient } from './client';
2
+ import { AuthonWalletError } from './errors';
3
+ import type { AuthonWalletOptions, EmbeddedWallet, TokenGetter } from './types';
4
+ export interface UseAuthonWalletOptions extends Omit<AuthonWalletOptions, 'getToken'> {
5
+ /** Defaults to the session of the surrounding `<AuthonProvider>` from @authon/react-native. */
6
+ getToken?: TokenGetter;
7
+ }
8
+ export interface UseAuthonWalletResult {
9
+ /** The client; call ensure(), signMessage(), protect() etc. on it. */
10
+ wallet: AuthonWalletClient;
11
+ /** The signed-in user's wallet, null when signed out or before it is created. */
12
+ current: EmbeddedWallet | null;
13
+ /** False until the first status check finished (or while signed out and the auth SDK is loading). */
14
+ isLoaded: boolean;
15
+ error: AuthonWalletError | null;
16
+ /** Reloads `current`. */
17
+ refresh: () => Promise<void>;
18
+ }
19
+ /**
20
+ * Wallet of the signed-in user. Inside `<AuthonProvider>` (@authon/react-native) the session token
21
+ * comes from the provider and the wallet follows sign-in and sign-out; elsewhere pass `getToken`.
22
+ * The client is recreated only when publishableKey, apiUrl or appId change.
23
+ */
24
+ export declare function useAuthonWallet(options: UseAuthonWalletOptions): UseAuthonWalletResult;
25
+ //# sourceMappingURL=useAuthonWallet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAuthonWallet.d.ts","sourceRoot":"","sources":["../src/useAuthonWallet.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEhF,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC;IACnF,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,MAAM,EAAE,kBAAkB,CAAC;IAC3B,iFAAiF;IACjF,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,qGAAqG;IACrG,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAChC,yBAAyB;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CA0EtF"}
@@ -0,0 +1,77 @@
1
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { getAuthonContext } from './authonContext';
3
+ import { AuthonWalletClient } from './client';
4
+ import { AuthonWalletError } from './errors';
5
+ /**
6
+ * Wallet of the signed-in user. Inside `<AuthonProvider>` (@authon/react-native) the session token
7
+ * comes from the provider and the wallet follows sign-in and sign-out; elsewhere pass `getToken`.
8
+ * The client is recreated only when publishableKey, apiUrl or appId change.
9
+ */
10
+ export function useAuthonWallet(options) {
11
+ const session = useContext(getAuthonContext());
12
+ const latest = useRef({ options, session });
13
+ latest.current = { options, session };
14
+ if (!options.getToken && !session) {
15
+ throw new AuthonWalletError('NOT_CONFIGURED', 'useAuthonWallet needs getToken, or must be used inside <AuthonProvider> from @authon/react-native');
16
+ }
17
+ const appIdKey = typeof options.appId === 'string' ? options.appId : JSON.stringify(options.appId ?? null);
18
+ const testingKey = JSON.stringify(options.testing ?? null);
19
+ const wallet = useMemo(() => new AuthonWalletClient({
20
+ ...latest.current.options,
21
+ getToken: async () => {
22
+ const { options: o, session: s } = latest.current;
23
+ if (o.getToken)
24
+ return o.getToken();
25
+ if (!s?.isSignedIn)
26
+ return null;
27
+ const token = s.getToken?.();
28
+ if (token)
29
+ return token;
30
+ return (await s.client?.refreshToken?.())?.accessToken ?? null;
31
+ },
32
+ }),
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ [options.publishableKey, options.apiUrl, appIdKey, options.requireDeviceAuthentication, testingKey]);
35
+ // dispose() only resets the client, so this is safe under StrictMode's double effects.
36
+ useEffect(() => () => wallet.dispose(), [wallet]);
37
+ const [current, setCurrent] = useState(null);
38
+ const [isLoaded, setLoaded] = useState(false);
39
+ const [error, setError] = useState(null);
40
+ useEffect(() => wallet.onChange(setCurrent), [wallet]);
41
+ const signedIn = options.getToken ? true : session?.isSignedIn === true;
42
+ const sessionUser = options.getToken ? null : session?.userId ?? null;
43
+ const authLoaded = options.getToken ? true : session?.isLoaded !== false;
44
+ const lastUser = useRef(undefined);
45
+ const refresh = useCallback(async () => {
46
+ try {
47
+ setCurrent(await wallet.get());
48
+ setError(null);
49
+ }
50
+ catch (e) {
51
+ setError(e instanceof AuthonWalletError ? e : new AuthonWalletError('UNKNOWN', String(e)));
52
+ }
53
+ finally {
54
+ setLoaded(true);
55
+ }
56
+ }, [wallet]);
57
+ useEffect(() => {
58
+ if (!authLoaded)
59
+ return;
60
+ if (!signedIn) {
61
+ wallet.dispose();
62
+ lastUser.current = undefined;
63
+ setCurrent(null);
64
+ setError(null);
65
+ setLoaded(true);
66
+ return;
67
+ }
68
+ // Another user signed in: drop the previous user's cached accounts and in-flight calls.
69
+ if (lastUser.current !== undefined && lastUser.current !== sessionUser)
70
+ wallet.dispose();
71
+ lastUser.current = sessionUser;
72
+ setLoaded(false);
73
+ void refresh();
74
+ }, [authLoaded, signedIn, sessionUser, refresh, wallet]);
75
+ return { wallet, current, isLoaded, error, refresh };
76
+ }
77
+ //# sourceMappingURL=useAuthonWallet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAuthonWallet.js","sourceRoot":"","sources":["../src/useAuthonWallet.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["AuthonWalletModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["dev.authon.reactnative.wallet.AuthonWalletModule"]
8
+ }
9
+ }
@@ -0,0 +1,37 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'AuthonReactNativeWallet'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = { :type => 'MIT' }
10
+ s.author = 'Authon'
11
+ s.homepage = 'https://authon.dev'
12
+ # AuthonWallet needs iOS 16 (the Swift package's minimum); PRF passkeys need iOS 18 at run time.
13
+ s.platforms = { :ios => '16.0' }
14
+ s.source = { :git => 'https://github.com/mikusnuz/authon-sdk.git', :tag => "react-native-wallet@#{s.version}" }
15
+ s.static_framework = true
16
+ s.swift_version = '5.9'
17
+ s.dependency 'ExpoModulesCore'
18
+ s.frameworks = 'Security', 'CryptoKit', 'LocalAuthentication', 'AuthenticationServices', 'SwiftUI'
19
+
20
+ # vendor/ is generated from swift/Sources by scripts/sync-native-wallet.mjs.
21
+ # CSecp256k1: compile only build/*.c (they include src/*.c); CKeccak: sha3.c and memzero.c.
22
+ s.source_files = [
23
+ '*.swift',
24
+ 'vendor/AuthonWallet/**/*.swift',
25
+ 'vendor/CSecp256k1/build/*.c',
26
+ 'vendor/CSecp256k1/include/*.h',
27
+ 'vendor/CKeccak/sha3.c',
28
+ 'vendor/CKeccak/memzero.c',
29
+ 'vendor/CKeccak/include/ckeccak.h',
30
+ ]
31
+ s.public_header_files = ['vendor/CSecp256k1/include/*.h', 'vendor/CKeccak/include/ckeccak.h']
32
+ s.preserve_paths = ['vendor/CSecp256k1/src/**/*', 'vendor/CKeccak/*.h']
33
+ s.pod_target_xcconfig = {
34
+ 'DEFINES_MODULE' => 'YES',
35
+ 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) ENABLE_MODULE_RECOVERY=1',
36
+ }
37
+ end
@@ -0,0 +1,190 @@
1
+ import ExpoModulesCore
2
+ import Foundation
3
+
4
+ /// Bridges the AuthonWallet core (vendor/AuthonWallet, generated from swift/Sources) to JS.
5
+ ///
6
+ /// Only public data crosses the bridge: addresses, signatures, status and devices. The device
7
+ /// share, the company share, the key and passkey PRF output stay inside the core. Every
8
+ /// function resolves an envelope `{ ok: true, value }` or `{ ok: false, code, message }` with
9
+ /// the web wallet's error codes, so JS never parses native exception text.
10
+ public final class AuthonWalletModule: Module {
11
+ private let lock = NSLock()
12
+ private var wallets: [String: AuthonWallet] = [:]
13
+
14
+ public func definition() -> ModuleDefinition {
15
+ Name("AuthonWallet")
16
+
17
+ AsyncFunction("configure") { (id: String, config: [String: Any]) -> [String: Any] in
18
+ self.envelope { try self.configure(id, config); return NSNull() }
19
+ }
20
+
21
+ AsyncFunction("release") { (id: String) -> [String: Any] in
22
+ self.lock.withLock { self.wallets[id] = nil }
23
+ return ["ok": true, "value": NSNull()]
24
+ }
25
+
26
+ AsyncFunction("appConfig") { (id: String) async -> [String: Any] in
27
+ await self.call(id, token: nil) { try Self.appConfig(await $0.appConfig()) }
28
+ }
29
+
30
+ AsyncFunction("status") { (id: String, token: String) async -> [String: Any] in
31
+ await self.call(id, token: token) { try Self.wallet(await $0.status()) }
32
+ }
33
+
34
+ AsyncFunction("create") { (id: String, token: String) async -> [String: Any] in
35
+ await self.call(id, token: token) { try Self.wallet(await $0.create()) }
36
+ }
37
+
38
+ AsyncFunction("accounts") { (id: String, token: String, start: Int, count: Int) async -> [String: Any] in
39
+ await self.call(id, token: token) { w in
40
+ try await w.accounts(start: start, count: count).map { ["index": $0.index, "path": $0.path, "address": $0.address] }
41
+ }
42
+ }
43
+
44
+ AsyncFunction("signMessage") { (id: String, token: String, kind: String, message: String, account: Int, address: String?) async -> [String: Any] in
45
+ await self.call(id, token: token) { w in
46
+ let payload: WalletMessage = kind == "raw" ? .raw(message) : .text(message)
47
+ return try await w.signMessage(payload, account: account, address: address)
48
+ }
49
+ }
50
+
51
+ AsyncFunction("signTypedData") { (id: String, token: String, typedDataJson: String, account: Int, address: String?) async -> [String: Any] in
52
+ await self.call(id, token: token) { try await $0.signTypedData(typedDataJson, account: account, address: address) }
53
+ }
54
+
55
+ AsyncFunction("signTransaction") { (id: String, token: String, transactionJson: String, account: Int, address: String?) async -> [String: Any] in
56
+ await self.call(id, token: token) { try await $0.signTransaction(transactionJson, account: account, address: address) }
57
+ }
58
+
59
+ AsyncFunction("protect") { (id: String, token: String) async -> [String: Any] in
60
+ await self.call(id, token: token) { ["shareVersion": try await $0.protect().shareVersion] }
61
+ }
62
+
63
+ AsyncFunction("upgradeBackup") { (id: String, token: String) async -> [String: Any] in
64
+ await self.call(id, token: token) { ["shareVersion": try await $0.upgradeBackup().shareVersion] }
65
+ }
66
+
67
+ AsyncFunction("restore") { (id: String, token: String, reset: Bool) async -> [String: Any] in
68
+ await self.call(id, token: token) { w in
69
+ let r = try await w.restore(mode: reset ? .reset : .add)
70
+ return ["shareVersion": r.shareVersion, "backupUpgradeRecommended": r.backupUpgradeRecommended]
71
+ }
72
+ }
73
+
74
+ AsyncFunction("revokeDevice") { (id: String, token: String, deviceId: String) async -> [String: Any] in
75
+ await self.call(id, token: token) { try await $0.revokeDevice(deviceId); return NSNull() }
76
+ }
77
+ }
78
+
79
+ // MARK: - Instances
80
+
81
+ private static var isDebugBuild: Bool {
82
+ #if DEBUG
83
+ return true
84
+ #else
85
+ return false
86
+ #endif
87
+ }
88
+
89
+ private func configure(_ id: String, _ config: [String: Any]) throws {
90
+ guard let publishableKey = config["publishableKey"] as? String, !publishableKey.isEmpty else {
91
+ throw WalletError(.invalidRequest, "publishableKey required")
92
+ }
93
+ let apiUrl = (config["apiUrl"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? "https://api.authon.dev"
94
+ let appId = (config["appId"] as? String).flatMap { $0.isEmpty ? nil : $0 }
95
+ let testing = config["testing"] as? [String: Any] ?? [:]
96
+ let softwareKeys = testing["allowSoftwareKeys"] as? Bool ?? false
97
+ let autoApprove = testing["autoApprove"] as? Bool ?? false
98
+ if (softwareKeys || autoApprove) && !Self.isDebugBuild {
99
+ throw WalletError(.invalidRequest, "testing options are only available in debug builds")
100
+ }
101
+ let wallet = AuthonWallet(
102
+ publishableKey: publishableKey,
103
+ apiURL: apiUrl,
104
+ sessionProvider: { CallToken.value ?? "" },
105
+ appId: appId,
106
+ approver: autoApprove ? TestAutoApprover() : SheetWalletApprover(),
107
+ requireDeviceAuthentication: config["requireDeviceAuthentication"] as? Bool ?? false,
108
+ allowSoftwareKeys: softwareKeys
109
+ )
110
+ lock.withLock { wallets[id] = wallet }
111
+ }
112
+
113
+ private func call(_ id: String, token: String?, _ body: @escaping (AuthonWallet) async throws -> Any) async -> [String: Any] {
114
+ guard let wallet = lock.withLock({ wallets[id] }) else {
115
+ return Self.failure(WalletError(.invalidRequest, "wallet not configured"))
116
+ }
117
+ do {
118
+ // The token is the Authon session token for this call, not a wallet secret.
119
+ let value = try await CallToken.$value.withValue(token) { try await body(wallet) }
120
+ return ["ok": true, "value": value]
121
+ } catch {
122
+ return Self.failure(error)
123
+ }
124
+ }
125
+
126
+ private func envelope(_ body: () throws -> Any) -> [String: Any] {
127
+ do {
128
+ return ["ok": true, "value": try body()]
129
+ } catch {
130
+ return Self.failure(error)
131
+ }
132
+ }
133
+
134
+ private static func failure(_ error: Error) -> [String: Any] {
135
+ let e: WalletError
136
+ if let w = error as? WalletError {
137
+ e = w
138
+ } else if error is CancellationError {
139
+ e = WalletError(.userCancelled)
140
+ } else {
141
+ e = WalletError(.internal, "unexpected error")
142
+ }
143
+ return ["ok": false, "code": e.code.rawValue, "message": e.message]
144
+ }
145
+
146
+ // MARK: - Public data only
147
+
148
+ private static func orNull(_ value: Any?) -> Any { value ?? NSNull() }
149
+
150
+ private static func wallet(_ w: PublicWallet) -> [String: Any] {
151
+ [
152
+ "address": w.address,
153
+ "protected": w.isProtected,
154
+ "needsRestore": w.needsRestore,
155
+ "backupUpgradeRecommended": w.backupUpgradeRecommended,
156
+ "devices": w.devices.map {
157
+ [
158
+ "id": $0.id,
159
+ "label": orNull($0.label),
160
+ "createdAt": orNull($0.createdAt),
161
+ "lastUsedAt": orNull($0.lastUsedAt),
162
+ "current": $0.current,
163
+ ] as [String: Any]
164
+ },
165
+ ]
166
+ }
167
+
168
+ private static func appConfig(_ c: WalletAppConfig) -> [String: Any] {
169
+ [
170
+ "projectId": orNull(c.projectId),
171
+ "allowed": c.allowed,
172
+ "approved": c.approved,
173
+ "enabled": c.enabled,
174
+ "chainIds": c.chainIds,
175
+ "confirmSigning": c.confirmSigning,
176
+ "rpId": c.rpId,
177
+ "minFrameProtocol": orNull(c.minFrameProtocol),
178
+ ]
179
+ }
180
+ }
181
+
182
+ private enum CallToken {
183
+ @TaskLocal static var value: String?
184
+ }
185
+
186
+ /// Debug builds only (checked in configure): approves every request without UI, so automated
187
+ /// smoke tests on the simulator can sign. Never used unless the app passes `testing.autoApprove`.
188
+ private struct TestAutoApprover: WalletApprover {
189
+ func approveSignature(_ summary: SignSummary) async throws {}
190
+ }
@@ -0,0 +1,212 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/AuthonWallet.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+ import Security
4
+
5
+ /// The Authon embedded wallet on iOS and macOS. Same wallet, shares and API as the web
6
+ /// frame: the device keeps only its share D; every signature fetches the company share A
7
+ /// with a device-key proof, combines them in memory, signs and wipes the key.
8
+ ///
9
+ /// This module does not depend on the `Authon` auth module; `AuthonWalletAuthon` adds an
10
+ /// initializer that takes an `Authon` instance.
11
+ public final class AuthonWallet: @unchecked Sendable {
12
+ public typealias SessionProvider = @Sendable () async throws -> String
13
+
14
+ private let api: WalletAPIClient
15
+ private let sessionProvider: SessionProvider
16
+ private let appId: String?
17
+ private let customStore: DeviceStore?
18
+ private let customPasskeys: PasskeyProvider?
19
+ private let approver: WalletApprover?
20
+ private let deviceAuthenticator: DeviceAuthenticator?
21
+ private let allowSoftwareKeys: Bool
22
+ private let lock = NSLock()
23
+ private var cachedConfig: WalletAppConfig?
24
+ private var cachedStore: DeviceStore?
25
+
26
+ /// - Parameters:
27
+ /// - sessionProvider: Returns the current Authon session token (refreshed if needed).
28
+ /// - appId: `TEAMID.bundleId` as registered in the Authon console. Detected from the
29
+ /// app's keychain access group when nil.
30
+ /// - approver: Shown before every signature when the project requires confirmation
31
+ /// (server setting). Pass your own to replace the default sheet.
32
+ /// - requireDeviceAuthentication: Ask for Face ID / Touch ID / passcode before each signature.
33
+ /// - deviceStore: Defaults to the Secure Enclave store.
34
+ /// - allowSoftwareKeys: Use software device keys when there is no Secure Enclave (simulator,
35
+ /// tests). Refused in release builds on devices. Never enable it for production.
36
+ public init(
37
+ publishableKey: String,
38
+ apiURL: String = "https://api.authon.dev",
39
+ sessionProvider: @escaping SessionProvider,
40
+ appId: String? = nil,
41
+ approver: WalletApprover? = SheetWalletApprover(),
42
+ requireDeviceAuthentication: Bool = false,
43
+ deviceAuthenticator: DeviceAuthenticator? = nil,
44
+ deviceStore: DeviceStore? = nil,
45
+ passkeys: PasskeyProvider? = nil,
46
+ allowSoftwareKeys: Bool = false,
47
+ urlSession: URLSession = .shared
48
+ ) {
49
+ api = WalletAPIClient(apiURL: apiURL, publishableKey: publishableKey, session: urlSession)
50
+ self.sessionProvider = sessionProvider
51
+ self.appId = appId
52
+ self.approver = approver
53
+ self.deviceAuthenticator = deviceAuthenticator ?? (requireDeviceAuthentication ? LocalDeviceAuthenticator() : nil)
54
+ customStore = deviceStore
55
+ customPasskeys = passkeys
56
+ self.allowSoftwareKeys = allowSoftwareKeys
57
+ }
58
+
59
+ // MARK: - Context
60
+
61
+ /// App registration and wallet settings from the API (cached).
62
+ public func appConfig() async throws -> WalletAppConfig {
63
+ if let c = lock.withLock({ cachedConfig }) { return c }
64
+ guard let appId = appId ?? Self.detectAppIdentifier() else {
65
+ throw WalletError(.originNotAllowed, "appId (TEAMID.bundleId) could not be determined")
66
+ }
67
+ let cfg = try await api.appConfig(platform: "ios", appId: appId)
68
+ lock.withLock { cachedConfig = cfg }
69
+ return cfg
70
+ }
71
+
72
+ private func store() throws -> DeviceStore {
73
+ if let s = customStore { return s }
74
+ return try lock.withLock {
75
+ if let s = cachedStore { return s }
76
+ let s: DeviceStore
77
+ if let se = try? SecureEnclaveDeviceStore() {
78
+ s = se
79
+ } else if allowSoftwareKeys {
80
+ s = try SoftwareDeviceStore(persistent: true)
81
+ } else {
82
+ throw WalletError(.storageUnavailable, "this device has no Secure Enclave")
83
+ }
84
+ cachedStore = s
85
+ return s
86
+ }
87
+ }
88
+
89
+ private func context(passkeys needed: Bool = false) async throws -> (WalletOps, String) {
90
+ // Like the frame, a request without a session is refused before anything else.
91
+ let session = try await sessionProvider()
92
+ guard !session.isEmpty else { throw WalletError(.unauthorized, "token required") }
93
+ let cfg = try await appConfig()
94
+ guard cfg.allowed else { throw WalletError(.originNotAllowed, "this app is not registered for the project") }
95
+ if let min = cfg.minFrameProtocol, min > WalletAPIClient.frameProtocol { throw WalletError(.frameOutdated) }
96
+ if needed && !cfg.approved {
97
+ throw WalletError(.originNotAllowed, "the app registration is not approved yet, so wallet passkeys are unavailable")
98
+ }
99
+ let ctx = WalletContext(
100
+ api: api,
101
+ store: try store(),
102
+ passkeys: customPasskeys ?? PlatformPasskeyProvider(relyingPartyIdentifier: cfg.rpId),
103
+ projectId: cfg.projectId ?? api.publishableKey,
104
+ confirmSigning: cfg.confirmSigning,
105
+ backupVersion: cfg.prfSaltVersion,
106
+ approver: approver,
107
+ deviceAuthenticator: deviceAuthenticator
108
+ )
109
+ return (WalletOps(ctx: ctx), session)
110
+ }
111
+
112
+ private func run<T>(passkeys: Bool = false, _ body: (WalletOps, String) async throws -> T) async throws -> T {
113
+ do {
114
+ let (ops, session) = try await context(passkeys: passkeys)
115
+ return try await body(ops, session)
116
+ } catch {
117
+ throw WalletError.wrap(error)
118
+ }
119
+ }
120
+
121
+ // MARK: - Operations
122
+
123
+ /// The wallet of the signed-in user. Throws NOT_FOUND when there is none yet.
124
+ public func status() async throws -> PublicWallet {
125
+ try await run { ops, s in try PublicWallet(try await ops.status(s)) }
126
+ }
127
+
128
+ /// Full status including a pending (unconfirmed) wallet; does not throw NOT_FOUND.
129
+ public func detailedStatus() async throws -> WalletStatus {
130
+ try await run { ops, s in try await ops.status(s) }
131
+ }
132
+
133
+ /// Creates the wallet (two-phase), or returns the existing one.
134
+ public func create() async throws -> PublicWallet {
135
+ try await run { ops, s in try PublicWallet(try await ops.create(s)) }
136
+ }
137
+
138
+ /// Addresses of accounts start..<start+count (m/44'/60'/0'/0/n).
139
+ public func accounts(start: Int = 0, count: Int = 1) async throws -> [WalletAccount] {
140
+ try await run { ops, s in try await ops.accounts(s, start: start, count: count) }
141
+ }
142
+
143
+ /// EIP-191 personal_sign. `account` > 0 must come with the address from `accounts()`.
144
+ public func signMessage(_ message: WalletMessage, account: Int = 0, address: String? = nil) async throws -> String {
145
+ try await run { ops, s in try await ops.signMessage(s, message, account: try SignAccount.parse(index: account, address: address)) }
146
+ }
147
+
148
+ public func signMessage(_ text: String, account: Int = 0, address: String? = nil) async throws -> String {
149
+ try await signMessage(.text(text), account: account, address: address)
150
+ }
151
+
152
+ /// EIP-712 v4. `typedData` is a JSON string, a `[String: Any]` dictionary or a JSONValue.
153
+ public func signTypedData(_ typedData: Any, account: Int = 0, address: String? = nil) async throws -> String {
154
+ try await run { ops, s in
155
+ let value: JSONValue = try (typedData as? String).map { try JSONValue.parse($0) } ?? JSONValue.from(typedData)
156
+ return try await ops.signTypedData(s, value, account: try SignAccount.parse(index: account, address: address))
157
+ }
158
+ }
159
+
160
+ /// Signs a legacy, EIP-2930 or EIP-1559 transaction and returns the serialized signed
161
+ /// transaction. Numeric fields may be numbers, decimal strings or 0x hex strings.
162
+ public func signTransaction(_ transaction: Any, account: Int = 0, address: String? = nil) async throws -> String {
163
+ try await run { ops, s in
164
+ let value: JSONValue = try (transaction as? String).map { try JSONValue.parse($0) } ?? JSONValue.from(transaction)
165
+ return try await ops.signTransaction(s, value, account: try SignAccount.parse(index: account, address: address))
166
+ }
167
+ }
168
+
169
+ /// Creates the passkey backup (iOS 18 / macOS 15 for PRF).
170
+ public func protect() async throws -> ProtectResult {
171
+ try await run(passkeys: true) { ops, s in try await ops.protect(s) }
172
+ }
173
+
174
+ /// Replaces a legacy backup with a verifiable one, from a device that holds the wallet.
175
+ public func upgradeBackup() async throws -> ProtectResult {
176
+ try await run(passkeys: true) { ops, s in try await ops.upgradeBackup(s) }
177
+ }
178
+
179
+ /// Restores the wallet on this device with the backup passkey.
180
+ public func restore(mode: RestoreMode = .add) async throws -> RestoreResult {
181
+ try await run(passkeys: true) { ops, s in try await ops.restore(s, mode: mode) }
182
+ }
183
+
184
+ public func revokeDevice(_ deviceId: String) async throws {
185
+ try await run { ops, s in try await ops.revokeDevice(s, deviceId: deviceId) }
186
+ }
187
+
188
+ // MARK: - App identifier
189
+
190
+ /// `TEAMID.bundleId` from the default keychain access group (the app identifier prefix).
191
+ static func detectAppIdentifier() -> String? {
192
+ var q: [String: Any] = [
193
+ kSecClass as String: kSecClassGenericPassword,
194
+ kSecAttrService as String: "dev.authon.wallet.app-id",
195
+ kSecAttrAccount as String: "probe",
196
+ kSecReturnAttributes as String: true,
197
+ ]
198
+ #if os(macOS)
199
+ q[kSecUseDataProtectionKeychain as String] = true
200
+ #endif
201
+ var out: AnyObject?
202
+ var status = SecItemCopyMatching(q as CFDictionary, &out)
203
+ if status == errSecItemNotFound {
204
+ var add = q
205
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
206
+ status = SecItemAdd(add as CFDictionary, &out)
207
+ }
208
+ guard status == errSecSuccess, let attrs = out as? [String: Any], let group = attrs[kSecAttrAccessGroup as String] as? String,
209
+ group.contains(".") else { return nil }
210
+ return group
211
+ }
212
+ }