@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.
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@tetherto/wdk-react-native-secure-storage",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Secure storage abstractions for React Native - provides secure storage for sensitive data (encrypted seeds, keys) using react-native-keychain",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "react-native": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "react-native": "./src/index.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/tetherto/wdk-react-native-secure-storage.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/tetherto/wdk-react-native-secure-storage/issues"
21
+ },
22
+ "homepage": "https://github.com/tetherto/wdk-react-native-secure-storage#readme",
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "jest",
27
+ "test:watch": "jest --watch",
28
+ "test:coverage": "jest --coverage",
29
+ "lint": "eslint src --ext .ts",
30
+ "lint:fix": "eslint src --ext .ts --fix",
31
+ "format": "prettier --write \"src/**/*.ts\"",
32
+ "format:check": "prettier --check \"src/**/*.ts\"",
33
+ "prepublishOnly": "npm run build && npm run test",
34
+ "prepack": "npm run build",
35
+ "prepare": "npm run build"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "README.md",
41
+ "LICENSE",
42
+ "tsconfig.json"
43
+ ],
44
+ "keywords": [
45
+ "wallet",
46
+ "react-native",
47
+ "secure-storage",
48
+ "react-native-keychain",
49
+ "keychain"
50
+ ],
51
+ "author": "Tetherto",
52
+ "license": "Apache-2.0",
53
+ "dependencies": {
54
+ "expo-crypto": "^15.0.8",
55
+ "expo-local-authentication": "~17.0.8",
56
+ "react-native-keychain": "^10.0.0"
57
+ },
58
+ "peerDependencies": {
59
+ "react-native": ">=0.70.0"
60
+ },
61
+ "devDependencies": {
62
+ "@jest/test-sequencer": "^30.2.0",
63
+ "@types/jest": "^29.5.0",
64
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
65
+ "@typescript-eslint/parser": "^6.0.0",
66
+ "eslint": "^8.50.0",
67
+ "jest": "^29.5.0",
68
+ "prettier": "^3.0.0",
69
+ "ts-jest": "^29.1.0",
70
+ "typescript": "^5.3.3"
71
+ }
72
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Mock for expo-crypto
3
+ * Used in tests to simulate cryptographic operations
4
+ */
5
+
6
+ export enum CryptoDigestAlgorithm {
7
+ SHA256 = 'SHA256',
8
+ SHA384 = 'SHA384',
9
+ SHA512 = 'SHA512',
10
+ }
11
+
12
+ /**
13
+ * Mock digestStringAsync function
14
+ * Returns a deterministic hash based on input string
15
+ * For testing purposes, uses a simple hash to ensure consistency
16
+ */
17
+ export async function digestStringAsync(
18
+ _algorithm: CryptoDigestAlgorithm,
19
+ data: string
20
+ ): Promise<string> {
21
+ // Simple deterministic hash for testing (not cryptographically secure, but consistent)
22
+ let hash = 0
23
+ for (let i = 0; i < data.length; i++) {
24
+ const char = data.charCodeAt(i)
25
+ hash = ((hash << 5) - hash) + char
26
+ hash = hash & hash // Convert to 32-bit integer
27
+ }
28
+
29
+ // Convert to hex string (64 chars for SHA-256)
30
+ const hex = Math.abs(hash).toString(16).padStart(8, '0')
31
+ // Repeat to get 64 characters (SHA-256 length)
32
+ return (hex.repeat(8)).substring(0, 64)
33
+ }
34
+
35
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Mock for expo-local-authentication
3
+ * Used in tests to simulate authentication operations
4
+ */
5
+
6
+ let mockIsEnrolled = true
7
+ let mockHasHardware = true
8
+ let mockAuthenticateResult = { success: true }
9
+
10
+ export function isEnrolledAsync(): Promise<boolean> {
11
+ return Promise.resolve(mockIsEnrolled)
12
+ }
13
+
14
+ export function hasHardwareAsync(): Promise<boolean> {
15
+ return Promise.resolve(mockHasHardware)
16
+ }
17
+
18
+ export function authenticateAsync(options?: {
19
+ promptMessage?: string
20
+ cancelLabel?: string
21
+ disableDeviceFallback?: boolean
22
+ }): Promise<{ success: boolean }> {
23
+ return Promise.resolve(mockAuthenticateResult)
24
+ }
25
+
26
+ // Helpers for tests to control mock behavior
27
+ export function __setMockIsEnrolled(value: boolean): void {
28
+ mockIsEnrolled = value
29
+ }
30
+
31
+ export function __setMockHasHardware(value: boolean): void {
32
+ mockHasHardware = value
33
+ }
34
+
35
+ export function __setMockAuthenticateResult(value: { success: boolean }): void {
36
+ mockAuthenticateResult = value
37
+ }
38
+
39
+
40
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Mock for react-native-keychain
3
+ * Used in tests to simulate keychain operations
4
+ */
5
+
6
+ export const ACCESSIBLE = {
7
+ WHEN_UNLOCKED: 'WHEN_UNLOCKED',
8
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY',
9
+ }
10
+
11
+ export const ACCESS_CONTROL = {
12
+ BIOMETRY_ANY_OR_DEVICE_PASSCODE: 'BIOMETRY_ANY_OR_DEVICE_PASSCODE',
13
+ }
14
+
15
+ let mockStorage: Map<string, { username: string; password: string }> = new Map()
16
+
17
+ export function setGenericPassword(
18
+ username: string,
19
+ password: string,
20
+ options?: { service?: string }
21
+ ): Promise<{ service: string; storage: string } | false> {
22
+ const service = options?.service || 'default'
23
+ mockStorage.set(service, { username, password })
24
+ return Promise.resolve({ service, storage: 'keychain' })
25
+ }
26
+
27
+ export function getGenericPassword(options?: { service?: string }): Promise<
28
+ | {
29
+ service: string
30
+ username: string
31
+ password: string
32
+ storage: string
33
+ }
34
+ | false
35
+ > {
36
+ const service = options?.service || 'default'
37
+ const stored = mockStorage.get(service)
38
+ if (stored) {
39
+ return Promise.resolve({
40
+ service,
41
+ username: stored.username,
42
+ password: stored.password,
43
+ storage: 'keychain',
44
+ })
45
+ }
46
+ return Promise.resolve(false)
47
+ }
48
+
49
+ export function resetGenericPassword(options?: { service?: string }): Promise<boolean> {
50
+ const service = options?.service || 'default'
51
+ mockStorage.delete(service)
52
+ return Promise.resolve(true)
53
+ }
54
+
55
+ // Helper for tests to reset mock storage
56
+ export function __resetMockStorage(): void {
57
+ mockStorage.clear()
58
+ }
59
+
60
+
@@ -0,0 +1,78 @@
1
+ import {
2
+ SecureStorageError,
3
+ KeychainError,
4
+ KeychainWriteError,
5
+ KeychainReadError,
6
+ AuthenticationError,
7
+ ValidationError,
8
+ TimeoutError,
9
+ } from '../errors'
10
+
11
+ describe('errors', () => {
12
+ describe('SecureStorageError', () => {
13
+ it('should create error with message and code', () => {
14
+ const error = new SecureStorageError('Test error', 'TEST_CODE')
15
+ expect(error.message).toBe('Test error')
16
+ expect(error.code).toBe('TEST_CODE')
17
+ expect(error.name).toBe('SecureStorageError')
18
+ })
19
+
20
+ it('should include cause error', () => {
21
+ const cause = new Error('Original error')
22
+ const error = new SecureStorageError('Test error', 'TEST_CODE', cause)
23
+ expect(error.cause).toBe(cause)
24
+ })
25
+ })
26
+
27
+ describe('KeychainError', () => {
28
+ it('should create keychain error', () => {
29
+ const error = new KeychainError('Keychain failed')
30
+ expect(error.message).toBe('Keychain failed')
31
+ expect(error.code).toBe('KEYCHAIN_ERROR')
32
+ expect(error.name).toBe('KeychainError')
33
+ })
34
+ })
35
+
36
+ describe('KeychainWriteError', () => {
37
+ it('should create write error', () => {
38
+ const error = new KeychainWriteError('Write failed')
39
+ expect(error.code).toBe('KEYCHAIN_WRITE_ERROR')
40
+ expect(error.name).toBe('KeychainWriteError')
41
+ })
42
+ })
43
+
44
+ describe('KeychainReadError', () => {
45
+ it('should create read error', () => {
46
+ const error = new KeychainReadError('Read failed')
47
+ expect(error.code).toBe('KEYCHAIN_READ_ERROR')
48
+ expect(error.name).toBe('KeychainReadError')
49
+ })
50
+ })
51
+
52
+ describe('AuthenticationError', () => {
53
+ it('should create authentication error', () => {
54
+ const error = new AuthenticationError('Auth failed')
55
+ expect(error.code).toBe('AUTHENTICATION_ERROR')
56
+ expect(error.name).toBe('AuthenticationError')
57
+ })
58
+ })
59
+
60
+ describe('ValidationError', () => {
61
+ it('should create validation error', () => {
62
+ const error = new ValidationError('Invalid input')
63
+ expect(error.code).toBe('VALIDATION_ERROR')
64
+ expect(error.name).toBe('ValidationError')
65
+ })
66
+ })
67
+
68
+ describe('TimeoutError', () => {
69
+ it('should create timeout error', () => {
70
+ const error = new TimeoutError('Operation timed out')
71
+ expect(error.code).toBe('TIMEOUT_ERROR')
72
+ expect(error.name).toBe('TimeoutError')
73
+ })
74
+ })
75
+ })
76
+
77
+
78
+
@@ -0,0 +1,131 @@
1
+ import { defaultLogger, LogLevel } from '../logger'
2
+
3
+ describe('logger', () => {
4
+ let consoleErrorSpy: jest.SpyInstance
5
+ let consoleWarnSpy: jest.SpyInstance
6
+ let consoleLogSpy: jest.SpyInstance
7
+
8
+ beforeEach(() => {
9
+ // Spy on console methods
10
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
11
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation()
12
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
13
+ })
14
+
15
+ afterEach(() => {
16
+ // Restore console methods
17
+ consoleErrorSpy.mockRestore()
18
+ consoleWarnSpy.mockRestore()
19
+ consoleLogSpy.mockRestore()
20
+ // Reset logger level to default (ERROR)
21
+ defaultLogger.setLevel(LogLevel.ERROR)
22
+ })
23
+
24
+ describe('defaultLogger', () => {
25
+ it('should set log level', () => {
26
+ defaultLogger.setLevel(LogLevel.DEBUG)
27
+ expect(() => defaultLogger.setLevel(LogLevel.INFO)).not.toThrow()
28
+ })
29
+
30
+ it('should log error messages', () => {
31
+ defaultLogger.setLevel(LogLevel.ERROR)
32
+ const error = new Error('Test error')
33
+ defaultLogger.error('Error message', error, { context: 'test' })
34
+
35
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
36
+ const logCall = consoleErrorSpy.mock.calls[0][0]
37
+ expect(logCall).toContain('Error message')
38
+ expect(JSON.parse(logCall)).toMatchObject({
39
+ level: LogLevel.ERROR,
40
+ message: 'Error message',
41
+ })
42
+ })
43
+
44
+ it('should log warn messages when level is WARN or lower', () => {
45
+ defaultLogger.setLevel(LogLevel.WARN)
46
+ defaultLogger.warn('Warning message', { context: 'test' })
47
+
48
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
49
+ const logCall = consoleWarnSpy.mock.calls[0][0]
50
+ expect(JSON.parse(logCall)).toMatchObject({
51
+ level: LogLevel.WARN,
52
+ message: 'Warning message',
53
+ })
54
+ })
55
+
56
+ it('should log info messages when level is INFO or lower', () => {
57
+ defaultLogger.setLevel(LogLevel.INFO)
58
+ defaultLogger.info('Info message', { context: 'test' })
59
+
60
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
61
+ const logCall = consoleLogSpy.mock.calls[0][0]
62
+ expect(JSON.parse(logCall)).toMatchObject({
63
+ level: LogLevel.INFO,
64
+ message: 'Info message',
65
+ })
66
+ })
67
+
68
+ it('should log debug messages when level is DEBUG', () => {
69
+ defaultLogger.setLevel(LogLevel.DEBUG)
70
+ defaultLogger.debug('Debug message', { context: 'test' })
71
+
72
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
73
+ const logCall = consoleLogSpy.mock.calls[0][0]
74
+ expect(JSON.parse(logCall)).toMatchObject({
75
+ level: LogLevel.DEBUG,
76
+ message: 'Debug message',
77
+ })
78
+ })
79
+
80
+ it('should not log messages below minimum level', () => {
81
+ defaultLogger.setLevel(LogLevel.ERROR)
82
+
83
+ defaultLogger.debug('Debug message')
84
+ defaultLogger.info('Info message')
85
+ defaultLogger.warn('Warning message')
86
+
87
+ expect(consoleLogSpy).not.toHaveBeenCalled()
88
+ expect(consoleWarnSpy).not.toHaveBeenCalled()
89
+
90
+ // Error should still be logged
91
+ defaultLogger.error('Error message')
92
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
93
+ })
94
+
95
+ it('should include context in log entries', () => {
96
+ defaultLogger.setLevel(LogLevel.DEBUG)
97
+ defaultLogger.info('Test message', { key: 'value', number: 123 })
98
+
99
+ const logCall = consoleLogSpy.mock.calls[0][0]
100
+ const entry = JSON.parse(logCall)
101
+ expect(entry.context).toEqual({ key: 'value', number: 123 })
102
+ })
103
+
104
+ it('should include error in log entries', () => {
105
+ defaultLogger.setLevel(LogLevel.ERROR)
106
+ const error = new Error('Test error')
107
+ error.stack = 'Error stack trace'
108
+
109
+ defaultLogger.error('Error message', error)
110
+
111
+ const logCall = consoleErrorSpy.mock.calls[0][0]
112
+ const entry = JSON.parse(logCall)
113
+ expect(entry.error).toBeDefined()
114
+ })
115
+
116
+ it('should include timestamp in log entries', () => {
117
+ defaultLogger.setLevel(LogLevel.DEBUG)
118
+ const beforeTime = Date.now()
119
+
120
+ defaultLogger.info('Test message')
121
+
122
+ const logCall = consoleLogSpy.mock.calls[0][0]
123
+ const entry = JSON.parse(logCall)
124
+ const afterTime = Date.now()
125
+
126
+ expect(entry.timestamp).toBeGreaterThanOrEqual(beforeTime)
127
+ expect(entry.timestamp).toBeLessThanOrEqual(afterTime)
128
+ })
129
+ })
130
+ })
131
+