react-native-bspatch 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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/android/build.gradle +100 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +139 -0
  6. package/android/src/main/java/vn/reactnativebspatch/BsPatchPackage.kt +16 -0
  7. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +227 -0
  8. package/cpp/CMakeLists.txt +43 -0
  9. package/cpp/bsdiff/bspatch.c +194 -0
  10. package/cpp/bsdiff/bspatch.h +41 -0
  11. package/cpp/bsdiff/bspatch_wrapper.c +150 -0
  12. package/cpp/bsdiff/bspatch_wrapper.h +6 -0
  13. package/cpp/bspatch_jni.cpp +41 -0
  14. package/cpp/bzip2/LICENSE +42 -0
  15. package/cpp/bzip2/blocksort.c +1094 -0
  16. package/cpp/bzip2/bzlib.c +1572 -0
  17. package/cpp/bzip2/bzlib.h +282 -0
  18. package/cpp/bzip2/bzlib_private.h +509 -0
  19. package/cpp/bzip2/compress.c +672 -0
  20. package/cpp/bzip2/crctable.c +104 -0
  21. package/cpp/bzip2/decompress.c +652 -0
  22. package/cpp/bzip2/huffman.c +205 -0
  23. package/cpp/bzip2/randtable.c +84 -0
  24. package/ios/OtaBundleResolver.swift +241 -0
  25. package/ios/RNBsPatch.h +8 -0
  26. package/ios/RNBsPatch.mm +198 -0
  27. package/lib/commonjs/NativeBsPatch.js +9 -0
  28. package/lib/commonjs/NativeBsPatch.js.map +1 -0
  29. package/lib/commonjs/crypto-js.d.js +2 -0
  30. package/lib/commonjs/crypto-js.d.js.map +1 -0
  31. package/lib/commonjs/index.js +90 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/ota/OtaProvider.js +306 -0
  34. package/lib/commonjs/ota/OtaProvider.js.map +1 -0
  35. package/lib/commonjs/ota/otaAssetResolver.js +145 -0
  36. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -0
  37. package/lib/commonjs/ota/otaCore.js +736 -0
  38. package/lib/commonjs/ota/otaCore.js.map +1 -0
  39. package/lib/commonjs/ota/otaLogger.js +64 -0
  40. package/lib/commonjs/ota/otaLogger.js.map +1 -0
  41. package/lib/commonjs/ota/otaMetadata.js +138 -0
  42. package/lib/commonjs/ota/otaMetadata.js.map +1 -0
  43. package/lib/commonjs/ota/storage/CustomStorage.js +140 -0
  44. package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -0
  45. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +96 -0
  46. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -0
  47. package/lib/commonjs/ota/types.js +39 -0
  48. package/lib/commonjs/ota/types.js.map +1 -0
  49. package/lib/commonjs/package.json +1 -0
  50. package/lib/module/NativeBsPatch.js +5 -0
  51. package/lib/module/NativeBsPatch.js.map +1 -0
  52. package/lib/module/crypto-js.d.js +2 -0
  53. package/lib/module/crypto-js.d.js.map +1 -0
  54. package/lib/module/index.js +23 -0
  55. package/lib/module/index.js.map +1 -0
  56. package/lib/module/ota/OtaProvider.js +299 -0
  57. package/lib/module/ota/OtaProvider.js.map +1 -0
  58. package/lib/module/ota/otaAssetResolver.js +138 -0
  59. package/lib/module/ota/otaAssetResolver.js.map +1 -0
  60. package/lib/module/ota/otaCore.js +725 -0
  61. package/lib/module/ota/otaCore.js.map +1 -0
  62. package/lib/module/ota/otaLogger.js +59 -0
  63. package/lib/module/ota/otaLogger.js.map +1 -0
  64. package/lib/module/ota/otaMetadata.js +127 -0
  65. package/lib/module/ota/otaMetadata.js.map +1 -0
  66. package/lib/module/ota/storage/CustomStorage.js +134 -0
  67. package/lib/module/ota/storage/CustomStorage.js.map +1 -0
  68. package/lib/module/ota/storage/GitHubReleaseStorage.js +90 -0
  69. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -0
  70. package/lib/module/ota/types.js +38 -0
  71. package/lib/module/ota/types.js.map +1 -0
  72. package/lib/typescript/NativeBsPatch.d.ts +39 -0
  73. package/lib/typescript/NativeBsPatch.d.ts.map +1 -0
  74. package/lib/typescript/index.d.ts +15 -0
  75. package/lib/typescript/index.d.ts.map +1 -0
  76. package/lib/typescript/ota/OtaProvider.d.ts +10 -0
  77. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -0
  78. package/lib/typescript/ota/otaAssetResolver.d.ts +15 -0
  79. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -0
  80. package/lib/typescript/ota/otaCore.d.ts +109 -0
  81. package/lib/typescript/ota/otaCore.d.ts.map +1 -0
  82. package/lib/typescript/ota/otaLogger.d.ts +44 -0
  83. package/lib/typescript/ota/otaLogger.d.ts.map +1 -0
  84. package/lib/typescript/ota/otaMetadata.d.ts +46 -0
  85. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -0
  86. package/lib/typescript/ota/storage/CustomStorage.d.ts +12 -0
  87. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -0
  88. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +20 -0
  89. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -0
  90. package/lib/typescript/ota/types.d.ts +191 -0
  91. package/lib/typescript/ota/types.d.ts.map +1 -0
  92. package/package.json +89 -0
  93. package/react-native-bspatch.podspec +46 -0
  94. package/src/NativeBsPatch.ts +52 -0
  95. package/src/crypto-js.d.ts +1 -0
  96. package/src/index.ts +29 -0
  97. package/src/ota/OtaProvider.tsx +399 -0
  98. package/src/ota/otaAssetResolver.ts +182 -0
  99. package/src/ota/otaCore.ts +963 -0
  100. package/src/ota/otaLogger.ts +72 -0
  101. package/src/ota/otaMetadata.ts +149 -0
  102. package/src/ota/storage/CustomStorage.ts +143 -0
  103. package/src/ota/storage/GitHubReleaseStorage.ts +98 -0
  104. package/src/ota/types.ts +211 -0
  105. package/tools/bsdiff +0 -0
  106. package/tools/bsdiff.c +445 -0
  107. package/tools/bsdiff.h +45 -0
  108. package/tools/build_ota.sh +457 -0
  109. package/tools/setup_project.js +273 -0
  110. package/tools/update_registry.js +145 -0
  111. package/tools/upload_ota_custom.sh +138 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * OTA Structured Event Logger
3
+ *
4
+ * Provides a centralized logging system for all OTA operations.
5
+ * Events can be forwarded to external analytics via the `onEvent` callback in OtaConfig.
6
+ */
7
+
8
+ import NativeBsPatch from '../NativeBsPatch';
9
+
10
+ export enum OtaEvent {
11
+ CHECK_UPDATE = 'check_update',
12
+ DOWNLOAD_START = 'download_start',
13
+ DOWNLOAD_SUCCESS = 'download_success',
14
+ DOWNLOAD_FAIL = 'download_fail',
15
+ PATCH_START = 'patch_start',
16
+ PATCH_SUCCESS = 'patch_success',
17
+ PATCH_FAIL = 'patch_fail',
18
+ HASH_VERIFY_START = 'hash_verify_start',
19
+ HASH_VERIFY_SUCCESS = 'hash_verify_success',
20
+ HASH_MISMATCH = 'hash_mismatch',
21
+ ROLLBACK = 'rollback',
22
+ FALLBACK_FULL_BUNDLE = 'fallback_full_bundle',
23
+ MARK_HEALTHY = 'mark_healthy',
24
+ RESET_FACTORY = 'reset_factory',
25
+ VERSION_INCOMPATIBLE = 'version_incompatible',
26
+ SIGNATURE_INVALID = 'signature_invalid',
27
+ DECOMPRESS_START = 'decompress_start',
28
+ DECOMPRESS_SUCCESS = 'decompress_success',
29
+ }
30
+
31
+ export type OtaLogEntry = {
32
+ event: OtaEvent;
33
+ timestamp: number;
34
+ version?: number;
35
+ message?: string;
36
+ durationMs?: number;
37
+ sizeKB?: number;
38
+ error?: string;
39
+ metadata?: Record<string, unknown>;
40
+ };
41
+
42
+ export type OtaEventCallback = (entry: OtaLogEntry) => void;
43
+
44
+ /**
45
+ * Creates an OTA logger instance bound to an optional external callback.
46
+ */
47
+ export const createOtaLogger = (onEvent?: OtaEventCallback) => {
48
+ const emit = (
49
+ event: OtaEvent,
50
+ details?: Omit<OtaLogEntry, 'event' | 'timestamp'>,
51
+ ) => {
52
+ const entry: OtaLogEntry = {
53
+ event,
54
+ timestamp: Date.now(),
55
+ ...details,
56
+ };
57
+
58
+ // Always log to console for debugging
59
+ const logStr = `[OTA:${event}] ${JSON.stringify(entry)}`;
60
+ console.log(logStr);
61
+
62
+ // Also log to native for physical device debugging (visible in Xcode)
63
+ if (NativeBsPatch && NativeBsPatch.logNative) {
64
+ NativeBsPatch.logNative(logStr);
65
+ }
66
+
67
+ // Forward to external analytics if configured
68
+ onEvent?.(entry);
69
+ };
70
+
71
+ return { emit };
72
+ };
@@ -0,0 +1,149 @@
1
+ import { Platform } from 'react-native';
2
+ import ReactNativeBlobUtil from 'react-native-blob-util';
3
+ import { OtaMetadata } from './types';
4
+
5
+ const OTA_DIR = `${ReactNativeBlobUtil.fs.dirs.DocumentDir}/ota`;
6
+ const METADATA_PATH = `${OTA_DIR}/metadata.json`;
7
+
8
+ /** Promise-based lock to prevent concurrent metadata writes (double-tap guard). */
9
+ let saveLock: Promise<void> = Promise.resolve();
10
+
11
+ /**
12
+ * Ensures the OTA root directory exists.
13
+ */
14
+ export const ensureOtaDir = async (): Promise<void> => {
15
+ if (!(await ReactNativeBlobUtil.fs.exists(OTA_DIR))) {
16
+ await ReactNativeBlobUtil.fs.mkdir(OTA_DIR);
17
+ }
18
+ };
19
+
20
+ /**
21
+ * Loads the current OTA metadata from the local filesystem.
22
+ *
23
+ * @param defaultVersion - The version to return if no metadata exists (usually baseAppVersion).
24
+ * @returns The persisted metadata or a default if loading fails.
25
+ */
26
+ export const loadMetadata = async (
27
+ defaultVersion: number,
28
+ ): Promise<OtaMetadata> => {
29
+ try {
30
+ if (await ReactNativeBlobUtil.fs.exists(METADATA_PATH)) {
31
+ const content = await ReactNativeBlobUtil.fs.readFile(
32
+ METADATA_PATH,
33
+ 'utf8',
34
+ );
35
+ return JSON.parse(content);
36
+ }
37
+ } catch (e) {
38
+ console.warn('[otaMetadata] Failed to load metadata:', e);
39
+ }
40
+
41
+ return {
42
+ currentVersion: defaultVersion,
43
+ previousVersions: [],
44
+ rollbackCount: 0,
45
+ lastLaunchSuccessful: true,
46
+ };
47
+ };
48
+
49
+ /**
50
+ * Persists OTA metadata to the local filesystem.
51
+ *
52
+ * @param metadata - The metadata object to save.
53
+ */
54
+ export const saveMetadata = async (metadata: OtaMetadata): Promise<void> => {
55
+ // Wait for any in-flight save to complete before starting our own
56
+ const prevLock = saveLock;
57
+ let settleFn: (() => void) | undefined;
58
+ saveLock = new Promise<void>(resolve => { settleFn = resolve; });
59
+
60
+ try {
61
+ await prevLock;
62
+ await ensureOtaDir();
63
+ await ReactNativeBlobUtil.fs.writeFile(
64
+ METADATA_PATH,
65
+ JSON.stringify(metadata, null, 2),
66
+ 'utf8',
67
+ );
68
+ } catch (e) {
69
+ console.error('[otaMetadata] Failed to save metadata:', e);
70
+ throw new Error(`Failed to save metadata: ${(e as Error).message}`);
71
+ } finally {
72
+ settleFn!();
73
+ }
74
+ };
75
+
76
+ /** Maximum number of previous versions kept in history for rollback. */
77
+ export const MAX_VERSION_HISTORY = 3;
78
+
79
+ /**
80
+ * Rotates OTA versions for auditing and rollback capabilities.
81
+ * Pushes the current version to the history and limits history to MAX_VERSION_HISTORY.
82
+ *
83
+ * @param metadata - Current metadata object.
84
+ * @param newVersion - The newly applied OTA version.
85
+ * @returns Updated metadata object.
86
+ */
87
+ export const registerNewVersion = (
88
+ metadata: OtaMetadata,
89
+ newVersion: number,
90
+ ): OtaMetadata => {
91
+ const versions = [
92
+ metadata.currentVersion,
93
+ ...(metadata.previousVersions || []),
94
+ ];
95
+ // Keep internal history unique and capped
96
+ const uniquePrevious = Array.from(new Set(versions))
97
+ .filter(v => v !== newVersion)
98
+ .slice(0, MAX_VERSION_HISTORY);
99
+
100
+ return {
101
+ ...metadata,
102
+ currentVersion: newVersion,
103
+ previousVersions: uniquePrevious,
104
+ rollbackCount: 0,
105
+ lastLaunchSuccessful: true, // Native layer will set to false on next boot until JS marks healthy
106
+ };
107
+ };
108
+
109
+ /**
110
+ * Marks the current session as healthy, indicating a successful boot.
111
+ * This prevents the native bridge from rolling back the version on the next launch.
112
+ *
113
+ * @param defaultVersion - Base version for metadata lookup fallback.
114
+ * @returns The updated metadata.
115
+ */
116
+ export const markHealthy = async (
117
+ defaultVersion: number,
118
+ ): Promise<OtaMetadata> => {
119
+ const metadata = await loadMetadata(defaultVersion);
120
+ const updated: OtaMetadata = {
121
+ ...metadata,
122
+ lastLaunchSuccessful: true,
123
+ rollbackCount: 0,
124
+ };
125
+ await saveMetadata(updated);
126
+ return updated;
127
+ };
128
+
129
+ /**
130
+ * Resolves the absolute path to the JS bundle for a specific version.
131
+ *
132
+ * @param version - The OTA version number.
133
+ * @param baseVersion - The app's base version (bundled).
134
+ * @returns Absolute path to the bundle or null if it refers to the base bundle.
135
+ */
136
+ export const getBundlePath = (
137
+ version: number,
138
+ baseVersion: number,
139
+ ): string | null => {
140
+ if (version === baseVersion) {
141
+ return null;
142
+ }
143
+ const filename = Platform.select({
144
+ android: 'index.android.bundle',
145
+ ios: 'main.jsbundle',
146
+ default: 'index.android.bundle',
147
+ });
148
+ return `${OTA_DIR}/v${version}/${filename}`;
149
+ };
@@ -0,0 +1,143 @@
1
+ import { Platform } from 'react-native';
2
+ import ReactNativeBlobUtil from 'react-native-blob-util';
3
+ import type { OtaPatch, OtaConfig, OtaStorageProvider } from '../types';
4
+
5
+ interface TokenCache {
6
+ token: string;
7
+ expiresAt: number;
8
+ }
9
+
10
+ /** Cache tokens per server endpoint so switching configs invalidates stale tokens. */
11
+ const tokenCache = new Map<string, TokenCache>();
12
+
13
+ /**
14
+ * Implementation for the custom MinIO-based storage server.
15
+ */
16
+ export class CustomStorageProvider implements OtaStorageProvider {
17
+ private getEndpoint(config: OtaConfig, key: 'login' | 'genDownloadLink' | 'downloadFile', fallback: string): string {
18
+ return config.customServer?.endpoints?.[key] ?? fallback;
19
+ }
20
+
21
+ private async getAccessToken(config: OtaConfig): Promise<string> {
22
+ if (!config.customServer) {
23
+ throw new Error('customServer configuration missing');
24
+ }
25
+
26
+ const { baseUrl, username, password } = config.customServer;
27
+ const cacheKey = `${baseUrl}|${username}`;
28
+ const now = Date.now();
29
+
30
+ const cached = tokenCache.get(cacheKey);
31
+ if (cached && now < cached.expiresAt - 30000) {
32
+ return cached.token;
33
+ }
34
+
35
+ const loginPath = this.getEndpoint(config, 'login', 'public/authen/login');
36
+ const loginUrl = baseUrl.endsWith('/') ? `${baseUrl}${loginPath}` : `${baseUrl}/${loginPath}`;
37
+
38
+ try {
39
+ const res = await ReactNativeBlobUtil.fetch(
40
+ 'POST',
41
+ loginUrl,
42
+ { 'Content-Type': 'application/json; charset=utf-8' },
43
+ JSON.stringify({ username, password })
44
+ );
45
+
46
+ const json = JSON.parse(res.data);
47
+ const token = json.data?.accessTokenInfo?.accessToken;
48
+
49
+ if (!token) throw new Error('Login failed: token not found');
50
+
51
+ tokenCache.set(cacheKey, { token, expiresAt: now + 3600000 }); // 1 hour
52
+ return token;
53
+ } catch (error) {
54
+ throw new Error(`Authentication failed: ${(error as Error).message}`);
55
+ }
56
+ }
57
+
58
+ async fetchManifest(config: OtaConfig): Promise<OtaPatch[]> {
59
+ if (!config.customServer) return config.bundledPatches || [];
60
+
61
+ const { baseUrl, channel, baseFolder = 'bspatch' } = config.customServer;
62
+ const registryPath = `${baseFolder}/ota_registry.json`;
63
+
64
+ try {
65
+ const token = await this.getAccessToken(config);
66
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
67
+ const genLinkPath = this.getEndpoint(config, 'genDownloadLink', 'api/minio/gen-download-link');
68
+ const genLinkUrl = `${normalizedBaseUrl}/${genLinkPath}?pathFile=${encodeURIComponent(registryPath)}`;
69
+
70
+ const linkRes = await ReactNativeBlobUtil.fetch('GET', genLinkUrl, {
71
+ Authorization: `Bearer ${token}`,
72
+ });
73
+ const linkJson = JSON.parse(linkRes.data);
74
+ const registryUrl = linkJson.data;
75
+ if (!registryUrl) throw new Error('Failed to generate registry download link');
76
+
77
+ const registryRes = await ReactNativeBlobUtil.fetch('GET', registryUrl);
78
+ if (registryRes.respInfo.status !== 200) {
79
+ throw new Error(`Registry fetch failed with status ${registryRes.respInfo.status}`);
80
+ }
81
+
82
+ const registry = JSON.parse(registryRes.data);
83
+ const appGroup = registry?.apps?.[config.appVersion];
84
+ const flavorGroup = appGroup?.flavors?.[channel.toLowerCase()];
85
+
86
+ if (!flavorGroup || !flavorGroup.patches?.length) {
87
+ return config.bundledPatches || [];
88
+ }
89
+
90
+ return (flavorGroup.patches || []).filter((p: OtaPatch) => p.platform === Platform.OS);
91
+ } catch (error) {
92
+ console.warn('[CustomStorage] Manifest fetch error:', (error as Error).message);
93
+ return config.bundledPatches || [];
94
+ }
95
+ }
96
+
97
+ async getUpdateFileUrl(filename: string, config: OtaConfig): Promise<string> {
98
+ if (!config.customServer) throw new Error('customServer config missing');
99
+
100
+ const { baseUrl, baseFolder = 'bspatch', useMinio } = config.customServer;
101
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
102
+ const channel = config.customServer.channel.toLowerCase();
103
+ const remotePath = `${baseFolder}/${config.appVersion}/${channel}/${filename}`;
104
+
105
+ if (useMinio) {
106
+ const token = await this.getAccessToken(config);
107
+ const genLinkPath = this.getEndpoint(config, 'genDownloadLink', 'api/minio/gen-download-link');
108
+ const genLinkUrl = `${normalizedBaseUrl}/${genLinkPath}?pathFile=${encodeURIComponent(remotePath)}`;
109
+ const res = await ReactNativeBlobUtil.fetch('GET', genLinkUrl, {
110
+ Authorization: `Bearer ${token}`,
111
+ });
112
+ const json = JSON.parse(res.data);
113
+ if (!json.data) throw new Error(`Failed to generate link for: ${filename}`);
114
+ return json.data;
115
+ }
116
+
117
+ const downloadPath = this.getEndpoint(config, 'downloadFile', 'files/downloadStreamFile');
118
+ return `${normalizedBaseUrl}/${downloadPath}?filepath=${encodeURIComponent(remotePath)}`;
119
+ }
120
+
121
+ async getAssetUrl(hash: string, ext: string, config: OtaConfig): Promise<string> {
122
+ if (!config.customServer) return '';
123
+
124
+ const { baseUrl, baseFolder = 'bspatch', useMinio } = config.customServer;
125
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
126
+ const filename = `${hash}.${ext}`;
127
+ const remotePath = `${baseFolder}/assets/${filename}`;
128
+
129
+ if (useMinio) {
130
+ const token = await this.getAccessToken(config);
131
+ const genLinkPath = this.getEndpoint(config, 'genDownloadLink', 'api/minio/gen-download-link');
132
+ const genLinkUrl = `${normalizedBaseUrl}/${genLinkPath}?pathFile=${encodeURIComponent(remotePath)}`;
133
+ const res = await ReactNativeBlobUtil.fetch('GET', genLinkUrl, {
134
+ Authorization: `Bearer ${token}`,
135
+ });
136
+ const json = JSON.parse(res.data);
137
+ return json.data || '';
138
+ }
139
+
140
+ const downloadPath = this.getEndpoint(config, 'downloadFile', 'files/downloadStreamFile');
141
+ return `${normalizedBaseUrl}/${downloadPath}?filepath=${encodeURIComponent(remotePath)}`;
142
+ }
143
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * GitHub Releases Storage Provider
3
+ *
4
+ * Reads the OTA registry from a GitHub Release asset and resolves download URLs
5
+ * directly from the release. Supports both public and token-authenticated repos.
6
+ *
7
+ * Registry format:
8
+ * - Expected at: `https://raw.githubusercontent.com/{owner}/{repo}/{tag}/{registryPath}`
9
+ * - Asset download: `https://github.com/{owner}/{repo}/releases/download/{tag}/{filename}`
10
+ */
11
+
12
+ import { Platform } from 'react-native';
13
+ import ReactNativeBlobUtil from 'react-native-blob-util';
14
+ import type { OtaPatch, OtaConfig, OtaStorageProvider } from '../types';
15
+
16
+ export class GitHubReleaseStorageProvider implements OtaStorageProvider {
17
+ private getRawRegistryUrl(config: OtaConfig): string {
18
+ const { repo, tag, registryPath = 'ota_registry.json' } = config.githubRelease!;
19
+ return `https://raw.githubusercontent.com/${repo}/${tag}/${registryPath}`;
20
+ }
21
+
22
+ private getAssetDownloadUrl(config: OtaConfig, filename: string): string {
23
+ const { repo, tag } = config.githubRelease!;
24
+ return `https://github.com/${repo}/releases/download/${tag}/${filename}`;
25
+ }
26
+
27
+ private authHeaders(token?: string): Record<string, string> {
28
+ if (!token) return {};
29
+ return { Authorization: `Bearer ${token}` };
30
+ }
31
+
32
+ async fetchManifest(config: OtaConfig): Promise<OtaPatch[]> {
33
+ if (!config.githubRelease) return config.bundledPatches || [];
34
+
35
+ const { token } = config.githubRelease;
36
+ const url = this.getRawRegistryUrl(config);
37
+
38
+ try {
39
+ const res = await ReactNativeBlobUtil.fetch(
40
+ 'GET',
41
+ url,
42
+ this.authHeaders(token),
43
+ );
44
+
45
+ if (res.respInfo.status !== 200) {
46
+ throw new Error(`Registry fetch failed with status ${res.respInfo.status}`);
47
+ }
48
+
49
+ const registry =
50
+ typeof res.data === 'string'
51
+ ? JSON.parse(res.data)
52
+ : JSON.parse(JSON.stringify(res.data));
53
+
54
+ // Support both flat array (legacy) and structured format
55
+ if (Array.isArray(registry)) {
56
+ return registry.filter(
57
+ (p: OtaPatch) => !p.platform || p.platform === Platform.OS,
58
+ );
59
+ }
60
+
61
+ // Structured: apps["1.0.0"].channels["production"].patches[]
62
+ const appGroup = registry?.apps?.[config.appVersion];
63
+ const channelGroup = appGroup?.channels?.[config.githubRelease.repo] // use repo as channel fallback
64
+ ?? appGroup?.channels?.production
65
+ ?? appGroup?.flavors?.production;
66
+
67
+ if (!channelGroup?.patches?.length) {
68
+ return config.bundledPatches || [];
69
+ }
70
+
71
+ return channelGroup.patches.filter(
72
+ (p: OtaPatch) => !p.platform || p.platform === Platform.OS,
73
+ );
74
+ } catch (error) {
75
+ console.warn('[GitHubStorage] Manifest fetch error:', (error as Error).message);
76
+ return config.bundledPatches || [];
77
+ }
78
+ }
79
+
80
+ async getUpdateFileUrl(filename: string, config: OtaConfig): Promise<string> {
81
+ if (!config.githubRelease) throw new Error('githubRelease config missing');
82
+ return this.getAssetDownloadUrl(config, filename);
83
+ }
84
+
85
+ async getAssetUrl(hash: string, ext: string, config: OtaConfig): Promise<string> {
86
+ if (!config.githubRelease) return '';
87
+
88
+ const { assetBaseUrl, tag, repo } = config.githubRelease;
89
+
90
+ // If a custom CDN/base is provided, use it
91
+ if (assetBaseUrl) {
92
+ return `${assetBaseUrl}/${hash}.${ext}`;
93
+ }
94
+
95
+ // Fallback: serve from GitHub raw with optional token
96
+ return `https://raw.githubusercontent.com/${repo}/${tag}/assets/${hash}.${ext}`;
97
+ }
98
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * OTA Data Types
3
+ */
4
+
5
+ import type { OtaEventCallback } from './otaLogger';
6
+
7
+ export interface OtaStorageProvider {
8
+ /**
9
+ * Fetch the list of available patches for the current configuration.
10
+ */
11
+ fetchManifest(config: OtaConfig): Promise<OtaPatch[]>;
12
+
13
+ /**
14
+ * Get a signed download URL for a version-specific update file (patch or full bundle).
15
+ */
16
+ getUpdateFileUrl(filename: string, config: OtaConfig): Promise<string>;
17
+
18
+ /**
19
+ * Get a signed download URL for a content-addressed asset.
20
+ */
21
+ getAssetUrl(hash: string, ext: string, config: OtaConfig): Promise<string>;
22
+ }
23
+
24
+ export enum OtaUpdateMode {
25
+ MANUAL = 'MANUAL',
26
+ ON_APP_START = 'ON_APP_START',
27
+ ON_APP_FOREGROUND = 'ON_APP_FOREGROUND',
28
+ BACKGROUND_DOWNLOAD = 'BACKGROUND_DOWNLOAD',
29
+ }
30
+
31
+ export enum OtaStorageType {
32
+ /** Custom storage server (primary) */
33
+ CUSTOM = 'CUSTOM',
34
+ /** GitHub Releases — public or token-authenticated */
35
+ GITHUB = 'GITHUB',
36
+ }
37
+
38
+ export type OtaPatch = {
39
+ /** Unique ID: `v{from}-to-v{to}` */
40
+ id: string;
41
+ /** Human-readable label shown in UI */
42
+ label: string;
43
+ /** App version this patch belongs to */
44
+ appVersion: string;
45
+ /** Release tag (legacy, keep for type compatibility) */
46
+ releaseTag?: string;
47
+ /** Filename on the server (e.g. "v1-to-v3.patch") */
48
+ filename: string;
49
+ /** Version the app must be at to apply this patch */
50
+ fromVersion: number;
51
+ /** Version this patch upgrades to */
52
+ toVersion: number;
53
+ /** Integrity hashes (optional for legacy support) */
54
+ baseHash?: string;
55
+ /** Hash of the bundle after patching */
56
+ bundleHash?: string;
57
+ /** Minimum native app version required (semver) */
58
+ minAppVersion?: string;
59
+ /** URL for full bundle download (fallback when patch fails) */
60
+ fullBundleUrl?: string;
61
+ /** Whether the patch/bundle is gzip compressed */
62
+ compressed?: boolean;
63
+ /** HMAC-SHA256 signature for security verification */
64
+ signature?: string;
65
+ /** Whether this update is mandatory */
66
+ mandatory?: boolean;
67
+ /** URL to the asset manifest JSON for this version */
68
+ assetManifestUrl?: string;
69
+ /** Target platform for this patch */
70
+ platform?: string;
71
+ };
72
+
73
+ /**
74
+ * A single content-addressed asset from the OTA asset store.
75
+ */
76
+ export type OtaAsset = {
77
+ /** SHA-256 hash of the file (used as filename: `{hash}.{ext}`) */
78
+ hash: string;
79
+ /** File extension without dot (e.g. "jpg", "png") */
80
+ ext: string;
81
+ /** Original relative path as Metro knows it (e.g. "drawable-mdpi/assets_images_foo.jpg") */
82
+ originalPath: string;
83
+ };
84
+
85
+ export type OtaRemotePatch = {
86
+ version: number;
87
+ minAppVersion: string;
88
+ patchUrl: string;
89
+ bundleHash: string;
90
+ baseHash: string;
91
+ mandatory: boolean;
92
+ };
93
+
94
+ export type OtaMetadata = {
95
+ currentVersion: number;
96
+ previousVersions: number[];
97
+ rollbackCount: number;
98
+ lastLaunchSuccessful: boolean;
99
+ };
100
+
101
+ export type OtaMetrics = {
102
+ patchSizeKB: number;
103
+ downloadMs: number;
104
+ applyMs: number;
105
+ totalMs: number;
106
+ fromCache: boolean;
107
+ };
108
+
109
+ export type OtaNetworkStrategy = {
110
+ retry?: number;
111
+ timeoutMs?: number;
112
+ wifiOnly?: boolean;
113
+ };
114
+
115
+ export type OtaConfig = {
116
+ /** Current app versionName (e.g. "1.0.0") */
117
+ appVersion: string;
118
+ /** Initial version baked into the APK (usually 1) */
119
+ baseAppVersion: number;
120
+ /** Default patches if manifest fetch fails */
121
+ bundledPatches?: OtaPatch[];
122
+
123
+ /** Enterprise configurations */
124
+ updateMode?: OtaUpdateMode;
125
+ maxRollback?: number;
126
+ network?: OtaNetworkStrategy;
127
+
128
+ /** Telemetry: structured event callback for analytics */
129
+ onEvent?: OtaEventCallback;
130
+ /** Public key for signature verification (base64) */
131
+ publicKey?: string;
132
+ /** Base URL for the shared hashed asset store (e.g. a GitHub release URL) */
133
+ assetBaseUrl?: string;
134
+
135
+ /** Storage strategy */
136
+ storageType?: OtaStorageType;
137
+
138
+ /** GitHub Releases configuration (used when storageType === GITHUB) */
139
+ githubRelease?: {
140
+ /** Owner / repo in the format "owner/repo" */
141
+ repo: string;
142
+ /** Tag name of the release to fetch updates from */
143
+ tag: string;
144
+ /**
145
+ * Path inside the release where ota_registry.json lives.
146
+ * If your release contains the file at root, set to "ota_registry.json".
147
+ * If it's inside a folder "ota/", set to "ota/ota_registry.json".
148
+ */
149
+ registryPath?: string;
150
+ /**
151
+ * Optional GitHub personal access token for private repos or higher rate limits.
152
+ * For public repos this is optional.
153
+ */
154
+ token?: string;
155
+ /**
156
+ * Asset base URL for content-addressed assets.
157
+ * Supports CDN URLs (jsDelivr, GitHub raw) or a custom base.
158
+ * Example: "https://cdn.jsdelivr.net/gh/owner/repo@tag/assets"
159
+ */
160
+ assetBaseUrl?: string;
161
+ };
162
+
163
+ /** Custom storage server configuration */
164
+ customServer?: {
165
+ baseUrl: string;
166
+ username: string;
167
+ password: string;
168
+ channel: string;
169
+ baseFolder?: string;
170
+ useMinio?: boolean;
171
+ /** Override default API endpoints for non-standard servers */
172
+ endpoints?: {
173
+ /** Auth login endpoint (default: /public/authen/login) */
174
+ login?: string;
175
+ /** Generate download link endpoint (default: /api/minio/gen-download-link) */
176
+ genDownloadLink?: string;
177
+ /** Direct file download endpoint (default: /files/downloadStreamFile) */
178
+ downloadFile?: string;
179
+ };
180
+ };
181
+
182
+ /** Storage provider instance (optional, defaults to CustomStorageProvider) */
183
+ storage?: OtaStorageProvider;
184
+ };
185
+
186
+ export enum OtaStatus {
187
+ IDLE = 'IDLE',
188
+ CHECKING = 'CHECKING',
189
+ UPDATE_AVAILABLE = 'UPDATE_AVAILABLE',
190
+ UP_TO_DATE = 'UP_TO_DATE',
191
+ DOWNLOADING = 'DOWNLOADING',
192
+ APPLYING = 'APPLYING',
193
+ SYNCING_ASSETS = 'SYNCING_ASSETS',
194
+ SUCCESS = 'SUCCESS',
195
+ ERROR = 'ERROR',
196
+ }
197
+
198
+ export type OtaState = {
199
+ status: OtaStatus;
200
+ message: string;
201
+ progress: number; // 0-100
202
+ currentVersion: number;
203
+ latestVersion: number;
204
+ pendingCount: number;
205
+ isChecking: boolean;
206
+ metrics: OtaMetrics | null;
207
+ applyUpdate: () => Promise<void>;
208
+ resetToFactory: () => Promise<void>;
209
+ markAsHealthy: () => Promise<void>;
210
+ checkUpdate: () => Promise<void>;
211
+ };
package/tools/bsdiff ADDED
Binary file