@blazium/ton-connect-mobile 1.0.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/LICENSE +22 -0
- package/README.md +271 -0
- package/dist/adapters/expo.d.ts +25 -0
- package/dist/adapters/expo.js +145 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/react-native.d.ts +25 -0
- package/dist/adapters/react-native.js +133 -0
- package/dist/adapters/web.d.ts +27 -0
- package/dist/adapters/web.js +147 -0
- package/dist/core/crypto.d.ts +28 -0
- package/dist/core/crypto.js +183 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +21 -0
- package/dist/core/protocol.d.ts +50 -0
- package/dist/core/protocol.js +260 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +502 -0
- package/dist/types/index.d.ts +192 -0
- package/dist/types/index.js +6 -0
- package/package.json +57 -0
- package/src/adapters/expo.ts +160 -0
- package/src/adapters/index.ts +8 -0
- package/src/adapters/react-native.ts +148 -0
- package/src/adapters/web.ts +176 -0
- package/src/core/crypto.ts +238 -0
- package/src/core/index.ts +7 -0
- package/src/core/protocol.ts +330 -0
- package/src/index.d.ts +19 -0
- package/src/index.ts +578 -0
- package/src/types/index.ts +206 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blazium/ton-connect-mobile",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready TON Connect Mobile SDK for React Native and Expo. Implements the real TonConnect protocol for mobile applications using deep links and callbacks.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/blaziumdev/ton-connect-mobile.git"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"example": "cd example && npm start"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ton",
|
|
18
|
+
"tonconnect",
|
|
19
|
+
"react-native",
|
|
20
|
+
"expo",
|
|
21
|
+
"blockchain",
|
|
22
|
+
"wallet",
|
|
23
|
+
"mobile",
|
|
24
|
+
"sdk"
|
|
25
|
+
],
|
|
26
|
+
"author": "BlaziumDev",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"tweetnacl": "^1.0.3",
|
|
30
|
+
"tweetnacl-util": "^0.15.1",
|
|
31
|
+
"react-native-get-random-values": "^1.9.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react-native": "*"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"expo-linking": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"expo-crypto": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"@react-native-async-storage/async-storage": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^18.2.0",
|
|
49
|
+
"@types/react-native": "^0.72.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"src"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo platform adapter
|
|
3
|
+
* Handles deep linking, storage, and crypto for Expo environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Type declarations for runtime globals
|
|
7
|
+
declare const require: {
|
|
8
|
+
(id: string): any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
import { PlatformAdapter } from '../types';
|
|
12
|
+
|
|
13
|
+
// Dynamic imports to handle optional dependencies
|
|
14
|
+
let Linking: any;
|
|
15
|
+
let Crypto: any;
|
|
16
|
+
let AsyncStorage: any;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
20
|
+
Linking = require('expo-linking');
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
22
|
+
Crypto = require('expo-crypto');
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
24
|
+
AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
25
|
+
} catch {
|
|
26
|
+
// Dependencies not available
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Expo platform adapter implementation
|
|
31
|
+
*/
|
|
32
|
+
export class ExpoAdapter implements PlatformAdapter {
|
|
33
|
+
private urlListeners: Array<(url: string) => void> = [];
|
|
34
|
+
private subscription: { remove: () => void } | null = null;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
// Set up URL listener
|
|
38
|
+
this.setupURLListener();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private setupURLListener(): void {
|
|
42
|
+
if (!Linking) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Listen for deep links when app is already open
|
|
46
|
+
this.subscription = Linking.addEventListener('url', (event: { url: string }) => {
|
|
47
|
+
this.urlListeners.forEach((listener) => listener(event.url));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async openURL(url: string): Promise<boolean> {
|
|
52
|
+
if (!Linking) {
|
|
53
|
+
throw new Error('expo-linking is not available');
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const canOpen = await Linking.canOpenURL(url);
|
|
57
|
+
if (!canOpen) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
await Linking.openURL(url);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getInitialURL(): Promise<string | null> {
|
|
68
|
+
if (!Linking) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return await Linking.getInitialURL();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
addURLListener(callback: (url: string) => void): () => void {
|
|
79
|
+
this.urlListeners.push(callback);
|
|
80
|
+
|
|
81
|
+
// Return unsubscribe function
|
|
82
|
+
return () => {
|
|
83
|
+
const index = this.urlListeners.indexOf(callback);
|
|
84
|
+
if (index > -1) {
|
|
85
|
+
this.urlListeners.splice(index, 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
91
|
+
if (!AsyncStorage) {
|
|
92
|
+
throw new Error('@react-native-async-storage/async-storage is not available');
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await AsyncStorage.setItem(key, value);
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
throw new Error(`Failed to set storage item: ${error?.message || error}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getItem(key: string): Promise<string | null> {
|
|
102
|
+
if (!AsyncStorage) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return await AsyncStorage.getItem(key);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async removeItem(key: string): Promise<void> {
|
|
113
|
+
if (!AsyncStorage) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await AsyncStorage.removeItem(key);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Ignore errors on remove
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async randomBytes(length: number): Promise<Uint8Array> {
|
|
124
|
+
if (Crypto) {
|
|
125
|
+
try {
|
|
126
|
+
// Use expo-crypto for random bytes
|
|
127
|
+
const randomString = await Crypto.getRandomBytesAsync(length);
|
|
128
|
+
return randomString;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Fall through to crypto.getRandomValues check
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// HIGH FIX: Try crypto.getRandomValues as fallback
|
|
135
|
+
// eslint-disable-next-line no-undef
|
|
136
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).crypto && (globalThis as any).crypto.getRandomValues) {
|
|
137
|
+
const bytes = new Uint8Array(length);
|
|
138
|
+
(globalThis as any).crypto.getRandomValues(bytes);
|
|
139
|
+
return bytes;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// HIGH FIX: Throw error instead of using insecure Math.random()
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Cryptographically secure random number generation not available. ' +
|
|
145
|
+
'Please ensure expo-crypto is installed or use react-native-get-random-values'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Cleanup resources
|
|
151
|
+
*/
|
|
152
|
+
destroy(): void {
|
|
153
|
+
if (this.subscription) {
|
|
154
|
+
this.subscription.remove();
|
|
155
|
+
this.subscription = null;
|
|
156
|
+
}
|
|
157
|
+
this.urlListeners = [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native platform adapter
|
|
3
|
+
* Handles deep linking and storage for React Native CLI environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Type declarations for runtime globals
|
|
7
|
+
declare const require: {
|
|
8
|
+
(id: string): any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
import { PlatformAdapter } from '../types';
|
|
12
|
+
|
|
13
|
+
// Dynamic imports to handle optional dependencies
|
|
14
|
+
let Linking: any;
|
|
15
|
+
let AsyncStorage: any;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
19
|
+
const RN = require('react-native');
|
|
20
|
+
Linking = RN.Linking;
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
22
|
+
AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
23
|
+
} catch {
|
|
24
|
+
// Dependencies not available
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* React Native platform adapter implementation
|
|
29
|
+
*/
|
|
30
|
+
export class ReactNativeAdapter implements PlatformAdapter {
|
|
31
|
+
private urlListeners: Array<(url: string) => void> = [];
|
|
32
|
+
private subscription: { remove: () => void } | null = null;
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
// Set up URL listener
|
|
36
|
+
this.setupURLListener();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private setupURLListener(): void {
|
|
40
|
+
if (!Linking) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Listen for deep links when app is already open
|
|
44
|
+
this.subscription = Linking.addEventListener('url', (event: { url: string }) => {
|
|
45
|
+
this.urlListeners.forEach((listener) => listener(event.url));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async openURL(url: string): Promise<boolean> {
|
|
50
|
+
if (!Linking) {
|
|
51
|
+
throw new Error('react-native Linking is not available');
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const canOpen = await Linking.canOpenURL(url);
|
|
55
|
+
if (!canOpen) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
await Linking.openURL(url);
|
|
59
|
+
return true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getInitialURL(): Promise<string | null> {
|
|
66
|
+
if (!Linking) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return await Linking.getInitialURL();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addURLListener(callback: (url: string) => void): () => void {
|
|
77
|
+
this.urlListeners.push(callback);
|
|
78
|
+
|
|
79
|
+
// Return unsubscribe function
|
|
80
|
+
return () => {
|
|
81
|
+
const index = this.urlListeners.indexOf(callback);
|
|
82
|
+
if (index > -1) {
|
|
83
|
+
this.urlListeners.splice(index, 1);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
89
|
+
if (!AsyncStorage) {
|
|
90
|
+
throw new Error('@react-native-async-storage/async-storage is not available');
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await AsyncStorage.setItem(key, value);
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
throw new Error(`Failed to set storage item: ${error?.message || error}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getItem(key: string): Promise<string | null> {
|
|
100
|
+
if (!AsyncStorage) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return await AsyncStorage.getItem(key);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async removeItem(key: string): Promise<void> {
|
|
111
|
+
if (!AsyncStorage) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await AsyncStorage.removeItem(key);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// Ignore errors on remove
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async randomBytes(length: number): Promise<Uint8Array> {
|
|
122
|
+
// HIGH FIX: Use crypto.getRandomValues if available (with polyfill)
|
|
123
|
+
// eslint-disable-next-line no-undef
|
|
124
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).crypto && (globalThis as any).crypto.getRandomValues) {
|
|
125
|
+
const bytes = new Uint8Array(length);
|
|
126
|
+
(globalThis as any).crypto.getRandomValues(bytes);
|
|
127
|
+
return bytes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// HIGH FIX: Throw error instead of using insecure Math.random()
|
|
131
|
+
throw new Error(
|
|
132
|
+
'Cryptographically secure random number generation not available. ' +
|
|
133
|
+
'Please install react-native-get-random-values: npm install react-native-get-random-values'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Cleanup resources
|
|
139
|
+
*/
|
|
140
|
+
destroy(): void {
|
|
141
|
+
if (this.subscription) {
|
|
142
|
+
this.subscription.remove();
|
|
143
|
+
this.subscription = null;
|
|
144
|
+
}
|
|
145
|
+
this.urlListeners = [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web platform adapter
|
|
3
|
+
* Handles deep linking and storage for web environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Type declarations for browser APIs
|
|
7
|
+
declare const window: {
|
|
8
|
+
location: {
|
|
9
|
+
href: string;
|
|
10
|
+
};
|
|
11
|
+
open(url: string, target?: string): any;
|
|
12
|
+
addEventListener(type: string, listener: () => void): void;
|
|
13
|
+
removeEventListener(type: string, listener: () => void): void;
|
|
14
|
+
localStorage: {
|
|
15
|
+
setItem(key: string, value: string): void;
|
|
16
|
+
getItem(key: string): string | null;
|
|
17
|
+
removeItem(key: string): void;
|
|
18
|
+
};
|
|
19
|
+
crypto: {
|
|
20
|
+
getRandomValues(array: Uint8Array): Uint8Array;
|
|
21
|
+
};
|
|
22
|
+
} | undefined;
|
|
23
|
+
|
|
24
|
+
import { PlatformAdapter } from '../types';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Web platform adapter implementation
|
|
28
|
+
* Uses browser APIs for deep linking and localStorage for storage
|
|
29
|
+
*/
|
|
30
|
+
export class WebAdapter implements PlatformAdapter {
|
|
31
|
+
private urlListeners: Array<(url: string) => void> = [];
|
|
32
|
+
private isListening = false;
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
// Set up URL listener
|
|
36
|
+
this.setupURLListener();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private setupURLListener(): void {
|
|
40
|
+
// Listen for hash changes (web deep links)
|
|
41
|
+
if (typeof window !== 'undefined') {
|
|
42
|
+
window.addEventListener('hashchange', () => {
|
|
43
|
+
this.handleURLChange();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Also check on popstate (browser back/forward)
|
|
47
|
+
window.addEventListener('popstate', () => {
|
|
48
|
+
this.handleURLChange();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.isListening = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private handleURLChange(): void {
|
|
56
|
+
if (typeof window === 'undefined') return;
|
|
57
|
+
|
|
58
|
+
const url = window.location.href;
|
|
59
|
+
this.urlListeners.forEach((listener) => {
|
|
60
|
+
try {
|
|
61
|
+
listener(url);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Ignore errors in listeners
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async openURL(url: string): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
if (typeof window !== 'undefined') {
|
|
71
|
+
// For web, we can use window.open or window.location
|
|
72
|
+
// For deep links to mobile wallets, we'll use window.location
|
|
73
|
+
if (url.startsWith('tonconnect://')) {
|
|
74
|
+
// Try to open in new window/tab, fallback to current window
|
|
75
|
+
const opened = window.open(url, '_blank');
|
|
76
|
+
if (!opened) {
|
|
77
|
+
// Popup blocked, try current window
|
|
78
|
+
window.location.href = url;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
} else {
|
|
82
|
+
window.location.href = url;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getInitialURL(): Promise<string | null> {
|
|
93
|
+
if (typeof window !== 'undefined') {
|
|
94
|
+
return window.location.href;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
addURLListener(callback: (url: string) => void): () => void {
|
|
100
|
+
this.urlListeners.push(callback);
|
|
101
|
+
|
|
102
|
+
// Immediately check current URL
|
|
103
|
+
if (typeof window !== 'undefined') {
|
|
104
|
+
try {
|
|
105
|
+
callback(window.location.href);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Ignore errors
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Return unsubscribe function
|
|
112
|
+
return () => {
|
|
113
|
+
const index = this.urlListeners.indexOf(callback);
|
|
114
|
+
if (index > -1) {
|
|
115
|
+
this.urlListeners.splice(index, 1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
121
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
122
|
+
throw new Error('localStorage is not available');
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
window.localStorage.setItem(key, value);
|
|
126
|
+
} catch (error: any) {
|
|
127
|
+
throw new Error(`Failed to set storage item: ${error?.message || error}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getItem(key: string): Promise<string | null> {
|
|
132
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return window.localStorage.getItem(key);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async removeItem(key: string): Promise<void> {
|
|
143
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
window.localStorage.removeItem(key);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Ignore errors on remove
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async randomBytes(length: number): Promise<Uint8Array> {
|
|
154
|
+
// Use crypto.getRandomValues (available in modern browsers)
|
|
155
|
+
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
|
|
156
|
+
const bytes = new Uint8Array(length);
|
|
157
|
+
window.crypto.getRandomValues(bytes);
|
|
158
|
+
return bytes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fallback: should not happen in modern browsers
|
|
162
|
+
throw new Error(
|
|
163
|
+
'Cryptographically secure random number generation not available. ' +
|
|
164
|
+
'Please use a modern browser with crypto.getRandomValues support.'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cleanup resources
|
|
170
|
+
*/
|
|
171
|
+
destroy(): void {
|
|
172
|
+
this.urlListeners = [];
|
|
173
|
+
this.isListening = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|