@tetherto/wdk-react-native-secure-storage 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ import {
2
+ getStorageKey,
3
+ withTimeout,
4
+ isKeychainCredentials,
5
+ createStorageKey,
6
+ } from '../utils'
7
+ import { MIN_TIMEOUT_MS, MAX_TIMEOUT_MS } from '../constants'
8
+ import { TimeoutError, ValidationError } from '../errors'
9
+
10
+ // Mock expo-crypto
11
+ jest.mock('expo-crypto', () => ({
12
+ CryptoDigestAlgorithm: {
13
+ SHA256: 'SHA256',
14
+ },
15
+ digestStringAsync: jest.fn(async (_algorithm: string, data: string) => {
16
+ // Simple deterministic hash for testing
17
+ let hash = 0
18
+ for (let i = 0; i < data.length; i++) {
19
+ const char = data.charCodeAt(i)
20
+ hash = ((hash << 5) - hash) + char
21
+ hash = hash & hash
22
+ }
23
+ const hex = Math.abs(hash).toString(16).padStart(8, '0')
24
+ return (hex.repeat(8)).substring(0, 64)
25
+ }),
26
+ }))
27
+
28
+ describe('utils', () => {
29
+
30
+ describe('getStorageKey', () => {
31
+ it('should return base key when identifier is undefined', async () => {
32
+ const storageKey = createStorageKey('wallet_encryption_key')
33
+ const key = await getStorageKey(storageKey, undefined)
34
+ expect(key).toBe('wallet_encryption_key')
35
+ })
36
+
37
+ it('should return base key when identifier is null', async () => {
38
+ const storageKey = createStorageKey('wallet_encryption_key')
39
+ const key = await getStorageKey(storageKey, null as any)
40
+ expect(key).toBe('wallet_encryption_key')
41
+ })
42
+
43
+ it('should generate hashed key for identifier', async () => {
44
+ const storageKey = createStorageKey('wallet_encryption_key')
45
+ const key = await getStorageKey(storageKey, 'user@example.com')
46
+ expect(key).toContain('wallet_encryption_key')
47
+ expect(key).not.toBe('wallet_encryption_key')
48
+ expect(key.length).toBeGreaterThan('wallet_encryption_key'.length)
49
+ })
50
+
51
+ it('should normalize identifier (lowercase and trim)', async () => {
52
+ const storageKey = createStorageKey('wallet_encryption_key')
53
+ const key1 = await getStorageKey(storageKey, 'User@Example.com')
54
+ const key2 = await getStorageKey(storageKey, ' user@example.com ')
55
+ expect(key1).toBe(key2)
56
+ })
57
+
58
+ it('should throw ValidationError for invalid storage key', async () => {
59
+ await expect(
60
+ getStorageKey('invalid_key' as any, 'user@example.com')
61
+ ).rejects.toThrow(ValidationError)
62
+ })
63
+ })
64
+
65
+ describe('withTimeout', () => {
66
+ it('should resolve if promise completes before timeout', async () => {
67
+ const promise = Promise.resolve('success')
68
+ const result = await withTimeout(promise, 1000, 'test')
69
+ expect(result).toBe('success')
70
+ })
71
+
72
+ it('should throw TimeoutError if promise exceeds timeout', async () => {
73
+ const promise = new Promise((resolve) => {
74
+ setTimeout(() => resolve('too late'), 2000)
75
+ })
76
+
77
+ await expect(withTimeout(promise, MIN_TIMEOUT_MS, 'test')).rejects.toThrow(TimeoutError)
78
+ }, 5000)
79
+
80
+ it('should throw ValidationError for negative timeout', async () => {
81
+ const promise = Promise.resolve('success')
82
+ await expect(withTimeout(promise, -1000, 'test')).rejects.toThrow(ValidationError)
83
+ })
84
+
85
+ it('should throw ValidationError for zero timeout', async () => {
86
+ const promise = Promise.resolve('success')
87
+ await expect(withTimeout(promise, 0, 'test')).rejects.toThrow(ValidationError)
88
+ })
89
+
90
+ it('should throw ValidationError for timeout below minimum', async () => {
91
+ const promise = Promise.resolve('success')
92
+ await expect(withTimeout(promise, MIN_TIMEOUT_MS - 1, 'test')).rejects.toThrow(ValidationError)
93
+ })
94
+
95
+ it('should throw ValidationError for timeout above maximum', async () => {
96
+ const promise = Promise.resolve('success')
97
+ await expect(withTimeout(promise, MAX_TIMEOUT_MS + 1, 'test')).rejects.toThrow(ValidationError)
98
+ })
99
+
100
+ it('should throw ValidationError for NaN timeout', async () => {
101
+ const promise = Promise.resolve('success')
102
+ await expect(withTimeout(promise, NaN, 'test')).rejects.toThrow(ValidationError)
103
+ })
104
+
105
+ it('should throw ValidationError for Infinity timeout', async () => {
106
+ const promise = Promise.resolve('success')
107
+ await expect(withTimeout(promise, Infinity, 'test')).rejects.toThrow(ValidationError)
108
+ })
109
+
110
+ it('should accept valid timeout values', async () => {
111
+ const promise = Promise.resolve('success')
112
+ const result = await withTimeout(promise, MIN_TIMEOUT_MS, 'test')
113
+ expect(result).toBe('success')
114
+ })
115
+
116
+ it('should accept maximum timeout value', async () => {
117
+ const promise = Promise.resolve('success')
118
+ const result = await withTimeout(promise, MAX_TIMEOUT_MS, 'test')
119
+ expect(result).toBe('success')
120
+ })
121
+ })
122
+
123
+
124
+ describe('isKeychainCredentials', () => {
125
+ it('should return true for valid credentials', () => {
126
+ const credentials = {
127
+ username: 'test',
128
+ password: 'password123',
129
+ service: 'test-service',
130
+ storage: 'AES_GCM',
131
+ }
132
+ expect(isKeychainCredentials(credentials)).toBe(true)
133
+ })
134
+
135
+ it('should return true for credentials without storage', () => {
136
+ const credentials = {
137
+ username: 'test',
138
+ password: 'password123',
139
+ service: 'test-service',
140
+ }
141
+ expect(isKeychainCredentials(credentials)).toBe(true)
142
+ })
143
+
144
+ it('should return false for false', () => {
145
+ expect(isKeychainCredentials(false)).toBe(false)
146
+ })
147
+
148
+ it('should return false for null', () => {
149
+ expect(isKeychainCredentials(null)).toBe(false)
150
+ })
151
+
152
+ it('should return false for undefined', () => {
153
+ expect(isKeychainCredentials(undefined)).toBe(false)
154
+ })
155
+
156
+ it('should return false for non-object', () => {
157
+ expect(isKeychainCredentials('string')).toBe(false)
158
+ expect(isKeychainCredentials(123)).toBe(false)
159
+ expect(isKeychainCredentials([])).toBe(false)
160
+ })
161
+
162
+ it('should return false for object without password', () => {
163
+ expect(isKeychainCredentials({ username: 'test', service: 'test' })).toBe(false)
164
+ })
165
+
166
+ it('should return false for object with non-string password', () => {
167
+ expect(isKeychainCredentials({ username: 'test', password: 123, service: 'test' })).toBe(false)
168
+ expect(isKeychainCredentials({ username: 'test', password: null, service: 'test' })).toBe(false)
169
+ })
170
+
171
+ it('should return false for object with empty password', () => {
172
+ expect(isKeychainCredentials({ username: 'test', password: '', service: 'test' })).toBe(false)
173
+ })
174
+
175
+ it('should return true for object with non-empty password string', () => {
176
+ expect(isKeychainCredentials({ username: 'test', password: 'a', service: 'test' })).toBe(true)
177
+ expect(isKeychainCredentials({ username: 'test', password: 'valid', service: 'test' })).toBe(true)
178
+ })
179
+ })
180
+
181
+ })
182
+
@@ -0,0 +1,222 @@
1
+ import { validateIdentifier, validateValue, validateTimeout, validateAuthenticationOptions, MAX_IDENTIFIER_LENGTH, MAX_VALUE_LENGTH } from '../validation'
2
+ import { ValidationError } from '../errors'
3
+ import { MIN_TIMEOUT_MS, MAX_TIMEOUT_MS } from '../constants'
4
+
5
+ describe('validation', () => {
6
+ describe('validateIdentifier', () => {
7
+ it('should allow undefined identifier', () => {
8
+ expect(() => validateIdentifier(undefined)).not.toThrow()
9
+ })
10
+
11
+ it('should allow null identifier', () => {
12
+ expect(() => validateIdentifier(null as any)).not.toThrow()
13
+ })
14
+
15
+ it('should allow valid email identifier', () => {
16
+ expect(() => validateIdentifier('user@example.com')).not.toThrow()
17
+ })
18
+
19
+ it('should allow email identifier with plus sign', () => {
20
+ expect(() => validateIdentifier('dario.moceri+3@tether.to')).not.toThrow()
21
+ expect(() => validateIdentifier('user+tag@example.com')).not.toThrow()
22
+ })
23
+
24
+ it('should allow valid alphanumeric identifier', () => {
25
+ expect(() => validateIdentifier('user123')).not.toThrow()
26
+ expect(() => validateIdentifier('user_123')).not.toThrow()
27
+ expect(() => validateIdentifier('user-123')).not.toThrow()
28
+ expect(() => validateIdentifier('user.123')).not.toThrow()
29
+ })
30
+
31
+ it('should throw ValidationError for non-string identifier', () => {
32
+ expect(() => validateIdentifier(123 as any)).toThrow(ValidationError)
33
+ expect(() => validateIdentifier({} as any)).toThrow(ValidationError)
34
+ expect(() => validateIdentifier([] as any)).toThrow(ValidationError)
35
+ })
36
+
37
+ it('should throw ValidationError for empty string', () => {
38
+ expect(() => validateIdentifier('')).toThrow(ValidationError)
39
+ expect(() => validateIdentifier(' ')).toThrow(ValidationError)
40
+ })
41
+
42
+ it('should throw ValidationError for identifier exceeding max length', () => {
43
+ const longIdentifier = 'a'.repeat(MAX_IDENTIFIER_LENGTH + 1)
44
+ expect(() => validateIdentifier(longIdentifier)).toThrow(ValidationError)
45
+ })
46
+
47
+ it('should allow identifier at max length', () => {
48
+ const maxIdentifier = 'a'.repeat(MAX_IDENTIFIER_LENGTH)
49
+ expect(() => validateIdentifier(maxIdentifier)).not.toThrow()
50
+ })
51
+
52
+ it('should throw ValidationError for invalid characters', () => {
53
+ expect(() => validateIdentifier('user@#$%example')).toThrow(ValidationError)
54
+ expect(() => validateIdentifier('user example')).toThrow(ValidationError)
55
+ expect(() => validateIdentifier('user\nexample')).toThrow(ValidationError)
56
+ })
57
+ })
58
+
59
+ describe('validateValue', () => {
60
+ it('should allow valid non-empty string', () => {
61
+ expect(() => validateValue('valid value')).not.toThrow()
62
+ expect(() => validateValue('a')).not.toThrow()
63
+ })
64
+
65
+ it('should throw ValidationError for null', () => {
66
+ expect(() => validateValue(null as any, 'test')).toThrow(ValidationError)
67
+ })
68
+
69
+ it('should throw ValidationError for undefined', () => {
70
+ expect(() => validateValue(undefined as any, 'test')).toThrow(ValidationError)
71
+ })
72
+
73
+ it('should throw ValidationError for non-string', () => {
74
+ expect(() => validateValue(123 as any, 'test')).toThrow(ValidationError)
75
+ expect(() => validateValue({} as any, 'test')).toThrow(ValidationError)
76
+ })
77
+
78
+ it('should throw ValidationError for empty string', () => {
79
+ expect(() => validateValue('', 'test')).toThrow(ValidationError)
80
+ })
81
+
82
+ it('should throw ValidationError for value exceeding max length', () => {
83
+ const longValue = 'a'.repeat(MAX_VALUE_LENGTH + 1)
84
+ expect(() => validateValue(longValue, 'test')).toThrow(ValidationError)
85
+ })
86
+
87
+ it('should allow value at max length', () => {
88
+ const maxValue = 'a'.repeat(MAX_VALUE_LENGTH)
89
+ expect(() => validateValue(maxValue, 'test')).not.toThrow()
90
+ })
91
+
92
+ it('should use custom field name in error message', () => {
93
+ try {
94
+ validateValue('', 'customField')
95
+ } catch (error) {
96
+ expect(error).toBeInstanceOf(ValidationError)
97
+ expect((error as ValidationError).message).toContain('customField')
98
+ }
99
+ })
100
+ })
101
+
102
+ describe('validateTimeout', () => {
103
+ it('should allow undefined timeout', () => {
104
+ expect(() => validateTimeout(undefined)).not.toThrow()
105
+ expect(validateTimeout(undefined)).toBeUndefined()
106
+ })
107
+
108
+ it('should allow valid timeout within range', () => {
109
+ expect(() => validateTimeout(MIN_TIMEOUT_MS)).not.toThrow()
110
+ expect(() => validateTimeout(MAX_TIMEOUT_MS)).not.toThrow()
111
+ expect(() => validateTimeout(30000)).not.toThrow()
112
+ expect(validateTimeout(30000)).toBe(30000)
113
+ })
114
+
115
+ it('should throw ValidationError for non-number', () => {
116
+ expect(() => validateTimeout('1000' as any)).toThrow(ValidationError)
117
+ expect(() => validateTimeout(null as any)).toThrow(ValidationError)
118
+ expect(() => validateTimeout({} as any)).toThrow(ValidationError)
119
+ })
120
+
121
+ it('should throw ValidationError for NaN', () => {
122
+ expect(() => validateTimeout(NaN)).toThrow(ValidationError)
123
+ })
124
+
125
+ it('should throw ValidationError for Infinity', () => {
126
+ expect(() => validateTimeout(Infinity)).toThrow(ValidationError)
127
+ expect(() => validateTimeout(-Infinity)).toThrow(ValidationError)
128
+ })
129
+
130
+ it('should throw ValidationError for timeout too short', () => {
131
+ expect(() => validateTimeout(MIN_TIMEOUT_MS - 1)).toThrow(ValidationError)
132
+ expect(() => validateTimeout(0)).toThrow(ValidationError)
133
+ expect(() => validateTimeout(-1000)).toThrow(ValidationError)
134
+ })
135
+
136
+ it('should throw ValidationError for timeout too long', () => {
137
+ expect(() => validateTimeout(MAX_TIMEOUT_MS + 1)).toThrow(ValidationError)
138
+ })
139
+
140
+ it('should return the timeout value when valid', () => {
141
+ expect(validateTimeout(5000)).toBe(5000)
142
+ expect(validateTimeout(MIN_TIMEOUT_MS)).toBe(MIN_TIMEOUT_MS)
143
+ expect(validateTimeout(MAX_TIMEOUT_MS)).toBe(MAX_TIMEOUT_MS)
144
+ })
145
+ })
146
+
147
+ describe('validateAuthenticationOptions', () => {
148
+ it('should allow undefined options', () => {
149
+ expect(() => validateAuthenticationOptions(undefined)).not.toThrow()
150
+ })
151
+
152
+ it('should allow empty options object', () => {
153
+ expect(() => validateAuthenticationOptions({})).not.toThrow()
154
+ })
155
+
156
+ it('should allow valid promptMessage', () => {
157
+ expect(() => validateAuthenticationOptions({ promptMessage: 'Authenticate' })).not.toThrow()
158
+ expect(() => validateAuthenticationOptions({ promptMessage: 'Please authenticate' })).not.toThrow()
159
+ })
160
+
161
+ it('should throw ValidationError for non-string promptMessage', () => {
162
+ expect(() => validateAuthenticationOptions({ promptMessage: 123 as any })).toThrow(ValidationError)
163
+ expect(() => validateAuthenticationOptions({ promptMessage: null as any })).toThrow(ValidationError)
164
+ expect(() => validateAuthenticationOptions({ promptMessage: {} as any })).toThrow(ValidationError)
165
+ })
166
+
167
+ it('should throw ValidationError for empty promptMessage', () => {
168
+ expect(() => validateAuthenticationOptions({ promptMessage: '' })).toThrow(ValidationError)
169
+ expect(() => validateAuthenticationOptions({ promptMessage: ' ' })).toThrow(ValidationError)
170
+ })
171
+
172
+ it('should allow valid cancelLabel', () => {
173
+ expect(() => validateAuthenticationOptions({ cancelLabel: 'Cancel' })).not.toThrow()
174
+ expect(() => validateAuthenticationOptions({ cancelLabel: 'Abort' })).not.toThrow()
175
+ })
176
+
177
+ it('should throw ValidationError for non-string cancelLabel', () => {
178
+ expect(() => validateAuthenticationOptions({ cancelLabel: 123 as any })).toThrow(ValidationError)
179
+ expect(() => validateAuthenticationOptions({ cancelLabel: null as any })).toThrow(ValidationError)
180
+ })
181
+
182
+ it('should throw ValidationError for empty cancelLabel', () => {
183
+ expect(() => validateAuthenticationOptions({ cancelLabel: '' })).toThrow(ValidationError)
184
+ expect(() => validateAuthenticationOptions({ cancelLabel: ' ' })).toThrow(ValidationError)
185
+ })
186
+
187
+ it('should allow valid disableDeviceFallback', () => {
188
+ expect(() => validateAuthenticationOptions({ disableDeviceFallback: true })).not.toThrow()
189
+ expect(() => validateAuthenticationOptions({ disableDeviceFallback: false })).not.toThrow()
190
+ })
191
+
192
+ it('should throw ValidationError for non-boolean disableDeviceFallback', () => {
193
+ expect(() => validateAuthenticationOptions({ disableDeviceFallback: 'true' as any })).toThrow(ValidationError)
194
+ expect(() => validateAuthenticationOptions({ disableDeviceFallback: 1 as any })).toThrow(ValidationError)
195
+ expect(() => validateAuthenticationOptions({ disableDeviceFallback: null as any })).toThrow(ValidationError)
196
+ })
197
+
198
+ it('should allow all options together', () => {
199
+ expect(() => validateAuthenticationOptions({
200
+ promptMessage: 'Authenticate',
201
+ cancelLabel: 'Cancel',
202
+ disableDeviceFallback: true,
203
+ })).not.toThrow()
204
+ })
205
+
206
+ it('should validate each option independently', () => {
207
+ // Valid promptMessage, invalid cancelLabel
208
+ expect(() => validateAuthenticationOptions({
209
+ promptMessage: 'Authenticate',
210
+ cancelLabel: '',
211
+ })).toThrow(ValidationError)
212
+
213
+ // Valid cancelLabel, invalid promptMessage
214
+ expect(() => validateAuthenticationOptions({
215
+ promptMessage: '',
216
+ cancelLabel: 'Cancel',
217
+ })).toThrow(ValidationError)
218
+ })
219
+ })
220
+ })
221
+
222
+
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Timeout constants for keychain operations
3
+ */
4
+ export const DEFAULT_TIMEOUT_MS = 30000
5
+ export const MIN_TIMEOUT_MS = 1000
6
+ export const MAX_TIMEOUT_MS = 5 * 60 * 1000
7
+
8
+ /**
9
+ * Cache TTL for device authentication availability (5 minutes)
10
+ */
11
+ export const DEVICE_AUTH_CACHE_TTL_MS = 5 * 60 * 1000
12
+
package/src/errors.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Base error class for all secure storage errors
3
+ */
4
+ export class SecureStorageError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public readonly code: string,
8
+ public readonly cause?: Error
9
+ ) {
10
+ super(message)
11
+ this.name = 'SecureStorageError'
12
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
13
+ // Note: Error.captureStackTrace is V8-specific (Node.js, Chrome). In React Native,
14
+ // this will typically work on Android (V8) but may not work on iOS (JavaScriptCore).
15
+ // If unavailable, the standard Error stack trace will be used instead.
16
+ if (Error.captureStackTrace) {
17
+ Error.captureStackTrace(this, this.constructor)
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Error thrown when keychain operations fail
24
+ *
25
+ * Note: This is a base error class for completeness. In practice, use
26
+ * KeychainWriteError or KeychainReadError for more specific error handling.
27
+ */
28
+ export class KeychainError extends SecureStorageError {
29
+ constructor(message: string, cause?: Error) {
30
+ super(message, 'KEYCHAIN_ERROR', cause)
31
+ this.name = 'KeychainError'
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Error thrown when keychain write operations fail
37
+ */
38
+ export class KeychainWriteError extends SecureStorageError {
39
+ constructor(message: string, cause?: Error) {
40
+ super(message, 'KEYCHAIN_WRITE_ERROR', cause)
41
+ this.name = 'KeychainWriteError'
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Error thrown when keychain read operations fail
47
+ */
48
+ export class KeychainReadError extends SecureStorageError {
49
+ constructor(message: string, cause?: Error) {
50
+ super(message, 'KEYCHAIN_READ_ERROR', cause)
51
+ this.name = 'KeychainReadError'
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Error thrown when authentication fails or is required but unavailable
57
+ */
58
+ export class AuthenticationError extends SecureStorageError {
59
+ constructor(message: string, cause?: Error) {
60
+ super(message, 'AUTHENTICATION_ERROR', cause)
61
+ this.name = 'AuthenticationError'
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Error thrown when input validation fails
67
+ */
68
+ export class ValidationError extends SecureStorageError {
69
+ constructor(message: string) {
70
+ super(message, 'VALIDATION_ERROR')
71
+ this.name = 'ValidationError'
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Error thrown when an operation times out
77
+ */
78
+ export class TimeoutError extends SecureStorageError {
79
+ constructor(message: string) {
80
+ super(message, 'TIMEOUT_ERROR')
81
+ this.name = 'TimeoutError'
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Error thrown when device security (PIN/pattern/password/biometrics) is not enabled
87
+ *
88
+ * This error can be used by apps that want to enforce device security as a prerequisite.
89
+ * Whether to require device security is up to the app - the library will still function
90
+ * on devices without authentication configured (data remains encrypted at rest).
91
+ */
92
+ export class DeviceSecurityNotEnabledError extends SecureStorageError {
93
+ constructor(message: string = 'Device security is not enabled. Please set up a PIN, pattern, or password in your device settings to use secure wallet storage.', cause?: Error) {
94
+ super(message, 'DEVICE_SECURITY_NOT_ENABLED', cause)
95
+ this.name = 'DeviceSecurityNotEnabledError'
96
+ }
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @tetherto/wdk-react-native-secure-storage
3
+ *
4
+ * Secure storage abstractions for React Native
5
+ * Provides secure storage for sensitive data (encrypted seeds, keys)
6
+ *
7
+ * **Note on Internal Types:** Some internal types like `StorageKey` are intentionally
8
+ * not exported. These are implementation details that consumers should not use directly.
9
+ * Use the public API methods (setEncryptionKey, getEncryptionKey, etc.) instead.
10
+ */
11
+
12
+ // Main types and factory
13
+ export type {
14
+ SecureStorage,
15
+ SecureStorageOptions,
16
+ AuthenticationOptions,
17
+ SecureStorageItemOptions,
18
+ } from './secureStorage'
19
+ export { createSecureStorage } from './secureStorage'
20
+
21
+ // Error classes
22
+ export {
23
+ SecureStorageError,
24
+ KeychainError,
25
+ KeychainWriteError,
26
+ KeychainReadError,
27
+ AuthenticationError,
28
+ ValidationError,
29
+ TimeoutError,
30
+ DeviceSecurityNotEnabledError,
31
+ } from './errors'
32
+
33
+ // Logger types
34
+ export type { Logger, LogEntry } from './logger'
35
+ export { LogLevel, defaultLogger } from './logger'
36
+
37
+ // Validation constants
38
+ export { MAX_IDENTIFIER_LENGTH, MAX_VALUE_LENGTH } from './validation'
39
+
40
+ // Timeout constants
41
+ export { MIN_TIMEOUT_MS, MAX_TIMEOUT_MS } from './constants'
@@ -0,0 +1,34 @@
1
+ import * as Keychain from 'react-native-keychain'
2
+
3
+ /**
4
+ * Keychain options for setGenericPassword
5
+ */
6
+ export type KeychainOptions = Parameters<typeof Keychain.setGenericPassword>[2]
7
+
8
+ /**
9
+ * Create keychain options with conditional access control
10
+ *
11
+ * @param deviceAuthAvailable - Whether device authentication (biometrics/PIN) is available
12
+ * @param requireAuth - Whether authentication should be required for this operation
13
+ * @param syncable - Whether the value should sync across devices (default: true)
14
+ * @returns Keychain options object with appropriate access control settings
15
+ */
16
+ export function createKeychainOptions(
17
+ deviceAuthAvailable: boolean,
18
+ requireAuth: boolean = true,
19
+ syncable: boolean = true
20
+ ): KeychainOptions {
21
+ const options: KeychainOptions = {
22
+ accessible: syncable
23
+ ? Keychain.ACCESSIBLE.WHEN_UNLOCKED
24
+ : Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
25
+ }
26
+
27
+ if (requireAuth && deviceAuthAvailable) {
28
+ options.accessControl = Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE
29
+ }
30
+
31
+ return options
32
+ }
33
+
34
+