@vanikya/ota-react-native 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.
- package/README.md +223 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +185 -0
- package/android/src/main/java/com/otaupdate/OTAUpdatePackage.kt +16 -0
- package/ios/OTAUpdate.m +61 -0
- package/ios/OTAUpdate.swift +194 -0
- package/lib/commonjs/OTAProvider.js +113 -0
- package/lib/commonjs/OTAProvider.js.map +1 -0
- package/lib/commonjs/hooks/useOTAUpdate.js +272 -0
- package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -0
- package/lib/commonjs/index.js +98 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/utils/api.js +60 -0
- package/lib/commonjs/utils/api.js.map +1 -0
- package/lib/commonjs/utils/storage.js +209 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/utils/verification.js +145 -0
- package/lib/commonjs/utils/verification.js.map +1 -0
- package/lib/module/OTAProvider.js +104 -0
- package/lib/module/OTAProvider.js.map +1 -0
- package/lib/module/hooks/useOTAUpdate.js +266 -0
- package/lib/module/hooks/useOTAUpdate.js.map +1 -0
- package/lib/module/index.js +11 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/utils/api.js +52 -0
- package/lib/module/utils/api.js.map +1 -0
- package/lib/module/utils/storage.js +202 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/utils/verification.js +137 -0
- package/lib/module/utils/verification.js.map +1 -0
- package/lib/typescript/OTAProvider.d.ts +28 -0
- package/lib/typescript/OTAProvider.d.ts.map +1 -0
- package/lib/typescript/hooks/useOTAUpdate.d.ts +35 -0
- package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +12 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/utils/api.d.ts +47 -0
- package/lib/typescript/utils/api.d.ts.map +1 -0
- package/lib/typescript/utils/storage.d.ts +32 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/lib/typescript/utils/verification.d.ts +11 -0
- package/lib/typescript/utils/verification.d.ts.map +1 -0
- package/ota-update.podspec +21 -0
- package/package.json +83 -0
- package/src/OTAProvider.tsx +160 -0
- package/src/hooks/useOTAUpdate.ts +344 -0
- package/src/index.ts +36 -0
- package/src/utils/api.ts +99 -0
- package/src/utils/storage.ts +249 -0
- package/src/utils/verification.ts +167 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { Platform, NativeModules } from 'react-native';
|
|
2
|
+
|
|
3
|
+
// Types
|
|
4
|
+
export interface StoredUpdate {
|
|
5
|
+
releaseId: string;
|
|
6
|
+
version: string;
|
|
7
|
+
bundlePath: string;
|
|
8
|
+
bundleHash: string;
|
|
9
|
+
downloadedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StorageAdapter {
|
|
13
|
+
getDocumentDirectory(): string;
|
|
14
|
+
writeFile(path: string, data: string | ArrayBuffer): Promise<void>;
|
|
15
|
+
readFile(path: string): Promise<string>;
|
|
16
|
+
readFileAsBuffer(path: string): Promise<ArrayBuffer>;
|
|
17
|
+
deleteFile(path: string): Promise<void>;
|
|
18
|
+
exists(path: string): Promise<boolean>;
|
|
19
|
+
makeDirectory(path: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Try to use Expo FileSystem if available
|
|
23
|
+
let ExpoFileSystem: any = null;
|
|
24
|
+
try {
|
|
25
|
+
ExpoFileSystem = require('expo-file-system');
|
|
26
|
+
} catch {
|
|
27
|
+
// Expo not available, will use native module
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Native module for bare React Native
|
|
31
|
+
const OTAUpdateNative = NativeModules.OTAUpdate;
|
|
32
|
+
|
|
33
|
+
// Expo implementation
|
|
34
|
+
class ExpoStorageAdapter implements StorageAdapter {
|
|
35
|
+
getDocumentDirectory(): string {
|
|
36
|
+
return ExpoFileSystem.documentDirectory || '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async writeFile(path: string, data: string | ArrayBuffer): Promise<void> {
|
|
40
|
+
if (data instanceof ArrayBuffer) {
|
|
41
|
+
// Convert ArrayBuffer to base64
|
|
42
|
+
const bytes = new Uint8Array(data);
|
|
43
|
+
let binary = '';
|
|
44
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
45
|
+
binary += String.fromCharCode(bytes[i]);
|
|
46
|
+
}
|
|
47
|
+
const base64 = btoa(binary);
|
|
48
|
+
await ExpoFileSystem.writeAsStringAsync(path, base64, {
|
|
49
|
+
encoding: ExpoFileSystem.EncodingType.Base64,
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
await ExpoFileSystem.writeAsStringAsync(path, data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async readFile(path: string): Promise<string> {
|
|
57
|
+
return ExpoFileSystem.readAsStringAsync(path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async readFileAsBuffer(path: string): Promise<ArrayBuffer> {
|
|
61
|
+
const base64 = await ExpoFileSystem.readAsStringAsync(path, {
|
|
62
|
+
encoding: ExpoFileSystem.EncodingType.Base64,
|
|
63
|
+
});
|
|
64
|
+
const binary = atob(base64);
|
|
65
|
+
const bytes = new Uint8Array(binary.length);
|
|
66
|
+
for (let i = 0; i < binary.length; i++) {
|
|
67
|
+
bytes[i] = binary.charCodeAt(i);
|
|
68
|
+
}
|
|
69
|
+
return bytes.buffer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async deleteFile(path: string): Promise<void> {
|
|
73
|
+
await ExpoFileSystem.deleteAsync(path, { idempotent: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async exists(path: string): Promise<boolean> {
|
|
77
|
+
const info = await ExpoFileSystem.getInfoAsync(path);
|
|
78
|
+
return info.exists;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async makeDirectory(path: string): Promise<void> {
|
|
82
|
+
await ExpoFileSystem.makeDirectoryAsync(path, { intermediates: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Native implementation for bare React Native
|
|
87
|
+
class NativeStorageAdapter implements StorageAdapter {
|
|
88
|
+
getDocumentDirectory(): string {
|
|
89
|
+
if (!OTAUpdateNative) {
|
|
90
|
+
throw new Error('OTAUpdate native module not found. Did you link the library?');
|
|
91
|
+
}
|
|
92
|
+
return OTAUpdateNative.getDocumentDirectory();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async writeFile(path: string, data: string | ArrayBuffer): Promise<void> {
|
|
96
|
+
if (!OTAUpdateNative) {
|
|
97
|
+
throw new Error('OTAUpdate native module not found');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (data instanceof ArrayBuffer) {
|
|
101
|
+
const bytes = new Uint8Array(data);
|
|
102
|
+
let binary = '';
|
|
103
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
104
|
+
binary += String.fromCharCode(bytes[i]);
|
|
105
|
+
}
|
|
106
|
+
const base64 = btoa(binary);
|
|
107
|
+
await OTAUpdateNative.writeFileBase64(path, base64);
|
|
108
|
+
} else {
|
|
109
|
+
await OTAUpdateNative.writeFile(path, data);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async readFile(path: string): Promise<string> {
|
|
114
|
+
if (!OTAUpdateNative) {
|
|
115
|
+
throw new Error('OTAUpdate native module not found');
|
|
116
|
+
}
|
|
117
|
+
return OTAUpdateNative.readFile(path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async readFileAsBuffer(path: string): Promise<ArrayBuffer> {
|
|
121
|
+
if (!OTAUpdateNative) {
|
|
122
|
+
throw new Error('OTAUpdate native module not found');
|
|
123
|
+
}
|
|
124
|
+
const base64: string = await OTAUpdateNative.readFileBase64(path);
|
|
125
|
+
const binary = atob(base64);
|
|
126
|
+
const bytes = new Uint8Array(binary.length);
|
|
127
|
+
for (let i = 0; i < binary.length; i++) {
|
|
128
|
+
bytes[i] = binary.charCodeAt(i);
|
|
129
|
+
}
|
|
130
|
+
return bytes.buffer;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async deleteFile(path: string): Promise<void> {
|
|
134
|
+
if (!OTAUpdateNative) {
|
|
135
|
+
throw new Error('OTAUpdate native module not found');
|
|
136
|
+
}
|
|
137
|
+
await OTAUpdateNative.deleteFile(path);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async exists(path: string): Promise<boolean> {
|
|
141
|
+
if (!OTAUpdateNative) {
|
|
142
|
+
throw new Error('OTAUpdate native module not found');
|
|
143
|
+
}
|
|
144
|
+
return OTAUpdateNative.exists(path);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async makeDirectory(path: string): Promise<void> {
|
|
148
|
+
if (!OTAUpdateNative) {
|
|
149
|
+
throw new Error('OTAUpdate native module not found');
|
|
150
|
+
}
|
|
151
|
+
await OTAUpdateNative.makeDirectory(path);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Factory function to get the appropriate storage adapter
|
|
156
|
+
export function getStorageAdapter(): StorageAdapter {
|
|
157
|
+
if (ExpoFileSystem) {
|
|
158
|
+
return new ExpoStorageAdapter();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (OTAUpdateNative) {
|
|
162
|
+
return new NativeStorageAdapter();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(
|
|
166
|
+
'No storage adapter available. Install expo-file-system or link the OTAUpdate native module.'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update storage manager
|
|
171
|
+
export class UpdateStorage {
|
|
172
|
+
private storage: StorageAdapter;
|
|
173
|
+
private baseDir: string;
|
|
174
|
+
|
|
175
|
+
constructor() {
|
|
176
|
+
this.storage = getStorageAdapter();
|
|
177
|
+
this.baseDir = `${this.storage.getDocumentDirectory()}ota-update/`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async ensureDirectory(): Promise<void> {
|
|
181
|
+
const exists = await this.storage.exists(this.baseDir);
|
|
182
|
+
if (!exists) {
|
|
183
|
+
await this.storage.makeDirectory(this.baseDir);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async saveBundle(releaseId: string, data: ArrayBuffer): Promise<string> {
|
|
188
|
+
await this.ensureDirectory();
|
|
189
|
+
|
|
190
|
+
const bundlePath = `${this.baseDir}${releaseId}.bundle`;
|
|
191
|
+
await this.storage.writeFile(bundlePath, data);
|
|
192
|
+
|
|
193
|
+
return bundlePath;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getBundlePath(releaseId: string): Promise<string | null> {
|
|
197
|
+
const bundlePath = `${this.baseDir}${releaseId}.bundle`;
|
|
198
|
+
const exists = await this.storage.exists(bundlePath);
|
|
199
|
+
return exists ? bundlePath : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async readBundle(releaseId: string): Promise<ArrayBuffer | null> {
|
|
203
|
+
const bundlePath = await this.getBundlePath(releaseId);
|
|
204
|
+
if (!bundlePath) return null;
|
|
205
|
+
|
|
206
|
+
return this.storage.readFileAsBuffer(bundlePath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async deleteBundle(releaseId: string): Promise<void> {
|
|
210
|
+
const bundlePath = `${this.baseDir}${releaseId}.bundle`;
|
|
211
|
+
if (await this.storage.exists(bundlePath)) {
|
|
212
|
+
await this.storage.deleteFile(bundlePath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async saveMetadata(update: StoredUpdate): Promise<void> {
|
|
217
|
+
await this.ensureDirectory();
|
|
218
|
+
|
|
219
|
+
const metadataPath = `${this.baseDir}current.json`;
|
|
220
|
+
await this.storage.writeFile(metadataPath, JSON.stringify(update));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async getMetadata(): Promise<StoredUpdate | null> {
|
|
224
|
+
const metadataPath = `${this.baseDir}current.json`;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (await this.storage.exists(metadataPath)) {
|
|
228
|
+
const content = await this.storage.readFile(metadataPath);
|
|
229
|
+
return JSON.parse(content);
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// Corrupted metadata, return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async clearMetadata(): Promise<void> {
|
|
239
|
+
const metadataPath = `${this.baseDir}current.json`;
|
|
240
|
+
if (await this.storage.exists(metadataPath)) {
|
|
241
|
+
await this.storage.deleteFile(metadataPath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async cleanOldBundles(keepReleaseId: string): Promise<void> {
|
|
246
|
+
// For now, we just keep one bundle at a time
|
|
247
|
+
// In a more advanced implementation, we might keep a few for rollback
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
// Try to use Expo Crypto if available
|
|
4
|
+
let ExpoCrypto: any = null;
|
|
5
|
+
try {
|
|
6
|
+
ExpoCrypto = require('expo-crypto');
|
|
7
|
+
} catch {
|
|
8
|
+
// Expo not available
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const OTAUpdateNative = NativeModules.OTAUpdate;
|
|
12
|
+
|
|
13
|
+
// Convert ArrayBuffer to hex string
|
|
14
|
+
function bufferToHex(buffer: ArrayBuffer): string {
|
|
15
|
+
const bytes = new Uint8Array(buffer);
|
|
16
|
+
return Array.from(bytes)
|
|
17
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
18
|
+
.join('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Convert hex string to Uint8Array
|
|
22
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
23
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
24
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
25
|
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Calculate SHA-256 hash of data
|
|
31
|
+
export async function calculateHash(data: ArrayBuffer): Promise<string> {
|
|
32
|
+
if (ExpoCrypto) {
|
|
33
|
+
// Use Expo Crypto
|
|
34
|
+
const hash = await ExpoCrypto.digestStringAsync(
|
|
35
|
+
ExpoCrypto.CryptoDigestAlgorithm.SHA256,
|
|
36
|
+
bufferToHex(data),
|
|
37
|
+
{ encoding: ExpoCrypto.CryptoEncoding.HEX }
|
|
38
|
+
);
|
|
39
|
+
return 'sha256:' + hash;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (OTAUpdateNative?.calculateSHA256) {
|
|
43
|
+
// Use native module
|
|
44
|
+
const bytes = new Uint8Array(data);
|
|
45
|
+
let binary = '';
|
|
46
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
47
|
+
binary += String.fromCharCode(bytes[i]);
|
|
48
|
+
}
|
|
49
|
+
const base64 = btoa(binary);
|
|
50
|
+
const hash = await OTAUpdateNative.calculateSHA256(base64);
|
|
51
|
+
return 'sha256:' + hash;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback: Use SubtleCrypto (not available in all RN environments)
|
|
55
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
56
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
57
|
+
return 'sha256:' + bufferToHex(hashBuffer);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error('No crypto implementation available');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Verify bundle hash
|
|
64
|
+
export async function verifyBundleHash(
|
|
65
|
+
data: ArrayBuffer,
|
|
66
|
+
expectedHash: string
|
|
67
|
+
): Promise<boolean> {
|
|
68
|
+
const actualHash = await calculateHash(data);
|
|
69
|
+
return actualHash === expectedHash;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Verify Ed25519 signature
|
|
73
|
+
export async function verifySignature(
|
|
74
|
+
data: ArrayBuffer,
|
|
75
|
+
signatureHex: string,
|
|
76
|
+
publicKeyHex: string
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
// Ed25519 verification is complex in JS
|
|
79
|
+
// We rely on native modules or skip if not available
|
|
80
|
+
|
|
81
|
+
if (OTAUpdateNative?.verifySignature) {
|
|
82
|
+
const bytes = new Uint8Array(data);
|
|
83
|
+
let binary = '';
|
|
84
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
85
|
+
binary += String.fromCharCode(bytes[i]);
|
|
86
|
+
}
|
|
87
|
+
const base64 = btoa(binary);
|
|
88
|
+
return OTAUpdateNative.verifySignature(base64, signatureHex, publicKeyHex);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If no native module, we can't verify signature
|
|
92
|
+
// In production, you might want to require this
|
|
93
|
+
if (__DEV__) {
|
|
94
|
+
console.warn(
|
|
95
|
+
'[OTAUpdate] Signature verification skipped: native module not available'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true; // Skip verification if not available
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Full bundle verification
|
|
103
|
+
export interface VerificationResult {
|
|
104
|
+
valid: boolean;
|
|
105
|
+
hashValid: boolean;
|
|
106
|
+
signatureValid: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function verifyBundle(
|
|
111
|
+
data: ArrayBuffer,
|
|
112
|
+
expectedHash: string,
|
|
113
|
+
signature: string | null,
|
|
114
|
+
publicKey: string | null
|
|
115
|
+
): Promise<VerificationResult> {
|
|
116
|
+
// Verify hash
|
|
117
|
+
let hashValid = false;
|
|
118
|
+
try {
|
|
119
|
+
hashValid = await verifyBundleHash(data, expectedHash);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
valid: false,
|
|
123
|
+
hashValid: false,
|
|
124
|
+
signatureValid: false,
|
|
125
|
+
error: `Hash verification failed: ${error}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!hashValid) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
hashValid: false,
|
|
133
|
+
signatureValid: false,
|
|
134
|
+
error: 'Bundle hash mismatch',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Verify signature if both signature and public key are provided
|
|
139
|
+
let signatureValid = true;
|
|
140
|
+
if (signature && publicKey) {
|
|
141
|
+
try {
|
|
142
|
+
signatureValid = await verifySignature(data, signature, publicKey);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
valid: false,
|
|
146
|
+
hashValid: true,
|
|
147
|
+
signatureValid: false,
|
|
148
|
+
error: `Signature verification failed: ${error}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!signatureValid) {
|
|
153
|
+
return {
|
|
154
|
+
valid: false,
|
|
155
|
+
hashValid: true,
|
|
156
|
+
signatureValid: false,
|
|
157
|
+
error: 'Invalid bundle signature',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
valid: true,
|
|
164
|
+
hashValid: true,
|
|
165
|
+
signatureValid,
|
|
166
|
+
};
|
|
167
|
+
}
|