@hot-updater/react-native 0.23.0 → 0.24.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/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +393 -49
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +204 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +48 -36
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +134 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +168 -95
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +15 -3
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +17 -12
- package/android/src/newarch/HotUpdaterModule.kt +88 -23
- package/android/src/oldarch/HotUpdaterModule.kt +89 -22
- package/android/src/oldarch/HotUpdaterSpec.kt +6 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +401 -77
- package/ios/HotUpdater/Internal/BundleMetadata.swift +177 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +213 -47
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +96 -25
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +35 -29
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +2 -2
- package/ios/HotUpdater/Public/HotUpdater.h +8 -2
- package/lib/commonjs/checkForUpdate.js +31 -28
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/error.js +45 -1
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchUpdateInfo.js +7 -45
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
- package/lib/commonjs/index.js +237 -208
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +103 -3
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/wrap.js +39 -1
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/checkForUpdate.js +32 -26
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/error.js +45 -0
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchUpdateInfo.js +7 -45
- package/lib/module/fetchUpdateInfo.js.map +1 -1
- package/lib/module/index.js +238 -203
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +87 -2
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/wrap.js +40 -2
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +11 -13
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +120 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +35 -41
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +58 -2
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +76 -5
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +11 -13
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +120 -0
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +35 -41
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +58 -2
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +76 -5
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +8 -7
- package/plugin/build/withHotUpdater.js +55 -4
- package/src/checkForUpdate.ts +51 -40
- package/src/error.ts +153 -0
- package/src/fetchUpdateInfo.ts +10 -58
- package/src/index.ts +283 -206
- package/src/native.ts +88 -2
- package/src/specs/NativeHotUpdater.ts +63 -0
- package/src/wrap.tsx +131 -9
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +0 -52
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +0 -24
- package/lib/commonjs/runUpdateProcess.js +0 -69
- package/lib/commonjs/runUpdateProcess.js.map +0 -1
- package/lib/module/runUpdateProcess.js +0 -64
- package/lib/module/runUpdateProcess.js.map +0 -1
- package/lib/typescript/commonjs/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +0 -1
- package/lib/typescript/module/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/module/runUpdateProcess.d.ts.map +0 -1
- package/src/runUpdateProcess.ts +0 -80
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { type CheckForUpdateOptions } from "./checkForUpdate";
|
|
3
2
|
import type { HotUpdaterError } from "./error";
|
|
4
|
-
import type
|
|
3
|
+
import { type NotifyAppReadyResult } from "./native";
|
|
4
|
+
export interface RunUpdateProcessResponse {
|
|
5
|
+
status: "ROLLBACK" | "UPDATE" | "UP_TO_DATE";
|
|
6
|
+
shouldForceUpdate: boolean;
|
|
7
|
+
message: string | null;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
5
10
|
type UpdateStatus = "CHECK_FOR_UPDATE" | "UPDATING" | "UPDATE_PROCESS_COMPLETED";
|
|
6
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Common options shared between auto and manual update modes
|
|
13
|
+
*/
|
|
14
|
+
interface CommonHotUpdaterOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Base URL for update server
|
|
17
|
+
* @example "https://update.example.com"
|
|
18
|
+
*/
|
|
19
|
+
baseURL: string;
|
|
20
|
+
/**
|
|
21
|
+
* Custom request headers for update checks
|
|
22
|
+
*/
|
|
23
|
+
requestHeaders?: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Request timeout in milliseconds
|
|
26
|
+
* @default 5000
|
|
27
|
+
*/
|
|
28
|
+
requestTimeout?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Callback invoked when the app is ready and bundle verification completes.
|
|
31
|
+
* Provides information about bundle promotion, recovery from crashes, or stable state.
|
|
32
|
+
*
|
|
33
|
+
* @param result - Bundle state information
|
|
34
|
+
* @param result.status - Current bundle state:
|
|
35
|
+
* - "PROMOTED": Staging bundle was promoted to stable (new update applied)
|
|
36
|
+
* - "RECOVERED": App recovered from a crash, rollback occurred
|
|
37
|
+
* - "STABLE": No changes, bundle is stable
|
|
38
|
+
* @param result.crashedBundleId - Present only when status is "RECOVERED"
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* HotUpdater.wrap({
|
|
43
|
+
* baseURL: "https://api.example.com",
|
|
44
|
+
* updateMode: "manual",
|
|
45
|
+
* onNotifyAppReady: ({ status, crashedBundleId }) => {
|
|
46
|
+
* if (status === "RECOVERED") {
|
|
47
|
+
* analytics.track('bundle_rollback', { crashedBundleId });
|
|
48
|
+
* } else if (status === "PROMOTED") {
|
|
49
|
+
* analytics.track('bundle_promoted');
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* })(App);
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
56
|
+
}
|
|
57
|
+
export interface AutoUpdateOptions extends CommonHotUpdaterOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Update strategy
|
|
60
|
+
* - "fingerprint": Use fingerprint hash to check for updates
|
|
61
|
+
* - "appVersion": Use app version to check for updates
|
|
62
|
+
*/
|
|
63
|
+
updateStrategy: "fingerprint" | "appVersion";
|
|
64
|
+
/**
|
|
65
|
+
* Update mode
|
|
66
|
+
* - "auto": Automatically check and download updates
|
|
67
|
+
*/
|
|
68
|
+
updateMode: "auto";
|
|
69
|
+
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
7
70
|
/**
|
|
8
71
|
* Component to show while downloading a new bundle update.
|
|
9
72
|
*
|
|
@@ -14,7 +77,8 @@ export interface HotUpdaterOptions extends CheckForUpdateOptions {
|
|
|
14
77
|
*
|
|
15
78
|
* ```tsx
|
|
16
79
|
* HotUpdater.wrap({
|
|
17
|
-
*
|
|
80
|
+
* baseURL: "<update-server-url>",
|
|
81
|
+
* updateStrategy: "appVersion",
|
|
18
82
|
* fallbackComponent: ({ progress = 0 }) => (
|
|
19
83
|
* <View style={styles.container}>
|
|
20
84
|
* <Text style={styles.text}>Updating... {progress}%</Text>
|
|
@@ -30,7 +94,6 @@ export interface HotUpdaterOptions extends CheckForUpdateOptions {
|
|
|
30
94
|
progress: number;
|
|
31
95
|
message: string | null;
|
|
32
96
|
}>;
|
|
33
|
-
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
34
97
|
onProgress?: (progress: number) => void;
|
|
35
98
|
/**
|
|
36
99
|
* When a force update exists, the app will automatically reload.
|
|
@@ -46,6 +109,14 @@ export interface HotUpdaterOptions extends CheckForUpdateOptions {
|
|
|
46
109
|
*/
|
|
47
110
|
onUpdateProcessCompleted?: (response: RunUpdateProcessResponse) => void;
|
|
48
111
|
}
|
|
112
|
+
export interface ManualUpdateOptions extends CommonHotUpdaterOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Update mode
|
|
115
|
+
* - "manual": Only notify app ready, user manually calls checkForUpdate()
|
|
116
|
+
*/
|
|
117
|
+
updateMode: "manual";
|
|
118
|
+
}
|
|
119
|
+
export type HotUpdaterOptions = AutoUpdateOptions | ManualUpdateOptions;
|
|
49
120
|
export declare function wrap<P extends React.JSX.IntrinsicAttributes = object>(options: HotUpdaterOptions): (WrappedComponent: React.ComponentType<P>) => React.ComponentType<P>;
|
|
50
121
|
export {};
|
|
51
122
|
//# sourceMappingURL=wrap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap.d.ts","sourceRoot":"","sources":["../../../src/wrap.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"wrap.d.ts","sourceRoot":"","sources":["../../../src/wrap.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,UAAU,CAAC;AAGlB,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,UAAU,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC7C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,KAAK,YAAY,GACb,kBAAkB,GAClB,UAAU,GACV,0BAA0B,CAAC;AAE/B;;GAEG;AACH,UAAU,uBAAuB;IAC/B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;CAC3D;AAED,MAAM,WAAW,iBAAkB,SAAQ,uBAAuB;IAChE;;;;OAIG;IACH,cAAc,EAAE,aAAa,GAAG,YAAY,CAAC;IAE7C;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,GAAG,OAAO,KAAK,IAAI,CAAC;IAE7D;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,0BAA0B,CAAC,CAAC;QAC1D,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC,CAAC;IAEH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,IAAI,CAAC;CACzE;AAED,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IAClE;;;OAGG;IACH,UAAU,EAAE,QAAQ,CAAC;CACtB;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAExE,wBAAgB,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,mBAAmB,GAAG,MAAM,EACnE,OAAO,EAAE,iBAAiB,GACzB,CAAC,gBAAgB,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAwItE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -111,22 +111,23 @@
|
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@react-native-community/cli": "18.0.0",
|
|
114
|
+
"@types/node": "^22.10.5",
|
|
114
115
|
"@types/react": "19.1.3",
|
|
115
116
|
"@types/use-sync-external-store": "^0.0.6",
|
|
116
117
|
"del-cli": "^6.0.0",
|
|
117
118
|
"expo": "^50.0.0",
|
|
118
|
-
"react": "19.1.
|
|
119
|
+
"react": "19.1.2",
|
|
119
120
|
"react-native": "0.79.1",
|
|
120
121
|
"react-native-builder-bob": "^0.40.10",
|
|
121
122
|
"typescript": "^5.8.3",
|
|
122
|
-
"hot-updater": "0.
|
|
123
|
+
"hot-updater": "0.24.0"
|
|
123
124
|
},
|
|
124
125
|
"dependencies": {
|
|
125
126
|
"use-sync-external-store": "1.5.0",
|
|
126
|
-
"@hot-updater/cli-tools": "0.
|
|
127
|
-
"@hot-updater/
|
|
128
|
-
"@hot-updater/
|
|
129
|
-
"@hot-updater/core": "0.
|
|
127
|
+
"@hot-updater/cli-tools": "0.24.0",
|
|
128
|
+
"@hot-updater/js": "0.24.0",
|
|
129
|
+
"@hot-updater/core": "0.24.0",
|
|
130
|
+
"@hot-updater/plugin-core": "0.24.0"
|
|
130
131
|
},
|
|
131
132
|
"scripts": {
|
|
132
133
|
"build": "bob build && tsc -p plugin/tsconfig.build.json",
|
|
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
var cli_tools_1 = require("@hot-updater/cli-tools");
|
|
43
43
|
var config_plugins_1 = require("expo/config-plugins");
|
|
44
44
|
var hot_updater_1 = require("hot-updater");
|
|
45
|
+
var path_1 = __importDefault(require("path"));
|
|
45
46
|
var package_json_1 = __importDefault(require("../../package.json"));
|
|
46
47
|
var transformers_1 = require("./transformers");
|
|
47
48
|
var fingerprintCache = null;
|
|
@@ -62,6 +63,36 @@ var getFingerprint = function () { return __awaiter(void 0, void 0, void 0, func
|
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
65
|
}); };
|
|
66
|
+
/**
|
|
67
|
+
* Extract public key from private key in signing config
|
|
68
|
+
*/
|
|
69
|
+
var getPublicKeyFromConfig = function (signingConfig) { return __awaiter(void 0, void 0, void 0, function () {
|
|
70
|
+
var privateKeyPath, privateKeyPEM, publicKeyPEM, error_1;
|
|
71
|
+
return __generator(this, function (_a) {
|
|
72
|
+
switch (_a.label) {
|
|
73
|
+
case 0:
|
|
74
|
+
if (!(signingConfig === null || signingConfig === void 0 ? void 0 : signingConfig.enabled) || !(signingConfig === null || signingConfig === void 0 ? void 0 : signingConfig.privateKeyPath)) {
|
|
75
|
+
return [2 /*return*/, null];
|
|
76
|
+
}
|
|
77
|
+
_a.label = 1;
|
|
78
|
+
case 1:
|
|
79
|
+
_a.trys.push([1, 3, , 4]);
|
|
80
|
+
privateKeyPath = path_1.default.isAbsolute(signingConfig.privateKeyPath)
|
|
81
|
+
? signingConfig.privateKeyPath
|
|
82
|
+
: path_1.default.resolve(process.cwd(), signingConfig.privateKeyPath);
|
|
83
|
+
return [4 /*yield*/, (0, hot_updater_1.loadPrivateKey)(privateKeyPath)];
|
|
84
|
+
case 2:
|
|
85
|
+
privateKeyPEM = _a.sent();
|
|
86
|
+
publicKeyPEM = (0, hot_updater_1.getPublicKeyFromPrivate)(privateKeyPEM);
|
|
87
|
+
return [2 /*return*/, publicKeyPEM.trim()];
|
|
88
|
+
case 3:
|
|
89
|
+
error_1 = _a.sent();
|
|
90
|
+
throw new Error("[hot-updater] Failed to extract public key: ".concat(error_1 instanceof Error ? error_1.message : String(error_1), "\n") +
|
|
91
|
+
"Run 'npx hot-updater keys generate' to create signing keys");
|
|
92
|
+
case 4: return [2 /*return*/];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}); };
|
|
65
96
|
/**
|
|
66
97
|
* Native code modifications - should only run once
|
|
67
98
|
*/
|
|
@@ -91,7 +122,7 @@ var withHotUpdaterConfigAsync = function (props) { return function (config) {
|
|
|
91
122
|
var modifiedConfig = config;
|
|
92
123
|
// === iOS: Add channel and fingerprint to Info.plist ===
|
|
93
124
|
modifiedConfig = (0, config_plugins_1.withInfoPlist)(modifiedConfig, function (cfg) { return __awaiter(void 0, void 0, void 0, function () {
|
|
94
|
-
var fingerprintHash, config, fingerprint;
|
|
125
|
+
var fingerprintHash, config, fingerprint, publicKey;
|
|
95
126
|
return __generator(this, function (_a) {
|
|
96
127
|
switch (_a.label) {
|
|
97
128
|
case 0:
|
|
@@ -105,18 +136,23 @@ var withHotUpdaterConfigAsync = function (props) { return function (config) {
|
|
|
105
136
|
fingerprint = _a.sent();
|
|
106
137
|
fingerprintHash = fingerprint.ios.hash;
|
|
107
138
|
_a.label = 3;
|
|
108
|
-
case 3:
|
|
139
|
+
case 3: return [4 /*yield*/, getPublicKeyFromConfig(config.signing)];
|
|
140
|
+
case 4:
|
|
141
|
+
publicKey = _a.sent();
|
|
109
142
|
cfg.modResults.HOT_UPDATER_CHANNEL = channel;
|
|
110
143
|
if (fingerprintHash) {
|
|
111
144
|
cfg.modResults.HOT_UPDATER_FINGERPRINT_HASH = fingerprintHash;
|
|
112
145
|
}
|
|
146
|
+
if (publicKey) {
|
|
147
|
+
cfg.modResults.HOT_UPDATER_PUBLIC_KEY = publicKey;
|
|
148
|
+
}
|
|
113
149
|
return [2 /*return*/, cfg];
|
|
114
150
|
}
|
|
115
151
|
});
|
|
116
152
|
}); });
|
|
117
153
|
// === Android: Add channel and fingerprint to strings.xml ===
|
|
118
154
|
modifiedConfig = (0, config_plugins_1.withStringsXml)(modifiedConfig, function (cfg) { return __awaiter(void 0, void 0, void 0, function () {
|
|
119
|
-
var fingerprintHash, config, fingerprint;
|
|
155
|
+
var fingerprintHash, config, fingerprint, publicKey;
|
|
120
156
|
return __generator(this, function (_a) {
|
|
121
157
|
switch (_a.label) {
|
|
122
158
|
case 0:
|
|
@@ -130,7 +166,9 @@ var withHotUpdaterConfigAsync = function (props) { return function (config) {
|
|
|
130
166
|
fingerprint = _a.sent();
|
|
131
167
|
fingerprintHash = fingerprint.android.hash;
|
|
132
168
|
_a.label = 3;
|
|
133
|
-
case 3:
|
|
169
|
+
case 3: return [4 /*yield*/, getPublicKeyFromConfig(config.signing)];
|
|
170
|
+
case 4:
|
|
171
|
+
publicKey = _a.sent();
|
|
134
172
|
// Ensure resources object exists
|
|
135
173
|
if (!cfg.modResults.resources) {
|
|
136
174
|
cfg.modResults.resources = {};
|
|
@@ -163,6 +201,19 @@ var withHotUpdaterConfigAsync = function (props) { return function (config) {
|
|
|
163
201
|
_: fingerprintHash,
|
|
164
202
|
});
|
|
165
203
|
}
|
|
204
|
+
if (publicKey) {
|
|
205
|
+
// Remove existing hot_updater_public_key entry if it exists
|
|
206
|
+
cfg.modResults.resources.string =
|
|
207
|
+
cfg.modResults.resources.string.filter(function (item) { return !(item.$ && item.$.name === "hot_updater_public_key"); });
|
|
208
|
+
// Add the new hot_updater_public_key entry
|
|
209
|
+
cfg.modResults.resources.string.push({
|
|
210
|
+
$: {
|
|
211
|
+
name: "hot_updater_public_key",
|
|
212
|
+
moduleConfig: "true",
|
|
213
|
+
},
|
|
214
|
+
_: publicKey,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
166
217
|
return [2 /*return*/, cfg];
|
|
167
218
|
}
|
|
168
219
|
});
|
package/src/checkForUpdate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AppUpdateInfo, UpdateBundleParams } from "@hot-updater/core";
|
|
2
2
|
import { Platform } from "react-native";
|
|
3
3
|
import { HotUpdaterError } from "./error";
|
|
4
|
-
import { fetchUpdateInfo
|
|
4
|
+
import { fetchUpdateInfo } from "./fetchUpdateInfo";
|
|
5
5
|
import {
|
|
6
6
|
getAppVersion,
|
|
7
7
|
getBundleId,
|
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
} from "./native";
|
|
13
13
|
|
|
14
14
|
export interface CheckForUpdateOptions {
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Update strategy
|
|
17
|
+
* - "fingerprint": Use fingerprint hash to check for updates
|
|
18
|
+
* - "appVersion": Use app version to check for updates
|
|
19
|
+
* - Can override the strategy set in HotUpdater.wrap()
|
|
20
|
+
*/
|
|
21
|
+
updateStrategy: "appVersion" | "fingerprint";
|
|
22
|
+
|
|
16
23
|
requestHeaders?: Record<string, string>;
|
|
17
24
|
onError?: (error: Error) => void;
|
|
18
25
|
/**
|
|
@@ -30,8 +37,32 @@ export type CheckForUpdateResult = AppUpdateInfo & {
|
|
|
30
37
|
updateBundle: () => Promise<boolean>;
|
|
31
38
|
};
|
|
32
39
|
|
|
40
|
+
// Internal type that includes baseURL for use within index.ts
|
|
41
|
+
export interface InternalCheckForUpdateOptions extends CheckForUpdateOptions {
|
|
42
|
+
baseURL: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Internal function to build update URL (not exported)
|
|
46
|
+
function buildUpdateUrl(
|
|
47
|
+
baseURL: string,
|
|
48
|
+
updateStrategy: "appVersion" | "fingerprint",
|
|
49
|
+
params: UpdateBundleParams,
|
|
50
|
+
): string {
|
|
51
|
+
switch (updateStrategy) {
|
|
52
|
+
case "fingerprint": {
|
|
53
|
+
if (!params.fingerprintHash) {
|
|
54
|
+
throw new HotUpdaterError("Fingerprint hash is required");
|
|
55
|
+
}
|
|
56
|
+
return `${baseURL}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
57
|
+
}
|
|
58
|
+
case "appVersion": {
|
|
59
|
+
return `${baseURL}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
export async function checkForUpdate(
|
|
34
|
-
options:
|
|
65
|
+
options: InternalCheckForUpdateOptions,
|
|
35
66
|
): Promise<CheckForUpdateResult | null> {
|
|
36
67
|
if (__DEV__) {
|
|
37
68
|
return null;
|
|
@@ -44,6 +75,13 @@ export async function checkForUpdate(
|
|
|
44
75
|
return null;
|
|
45
76
|
}
|
|
46
77
|
|
|
78
|
+
if (!options.baseURL || !options.updateStrategy) {
|
|
79
|
+
options.onError?.(
|
|
80
|
+
new HotUpdaterError("'baseURL' and 'updateStrategy' are required"),
|
|
81
|
+
);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
const currentAppVersion = getAppVersion();
|
|
48
86
|
const platform = Platform.OS as "ios" | "android";
|
|
49
87
|
const currentBundleId = getBundleId();
|
|
@@ -57,16 +95,17 @@ export async function checkForUpdate(
|
|
|
57
95
|
|
|
58
96
|
const fingerprintHash = getFingerprintHash();
|
|
59
97
|
|
|
98
|
+
const url = buildUpdateUrl(options.baseURL, options.updateStrategy, {
|
|
99
|
+
platform,
|
|
100
|
+
appVersion: currentAppVersion,
|
|
101
|
+
fingerprintHash: fingerprintHash ?? null,
|
|
102
|
+
channel,
|
|
103
|
+
minBundleId,
|
|
104
|
+
bundleId: currentBundleId,
|
|
105
|
+
});
|
|
106
|
+
|
|
60
107
|
return fetchUpdateInfo({
|
|
61
|
-
|
|
62
|
-
params: {
|
|
63
|
-
bundleId: currentBundleId,
|
|
64
|
-
appVersion: currentAppVersion,
|
|
65
|
-
platform,
|
|
66
|
-
minBundleId,
|
|
67
|
-
channel,
|
|
68
|
-
fingerprintHash,
|
|
69
|
-
},
|
|
108
|
+
url,
|
|
70
109
|
requestHeaders: options.requestHeaders,
|
|
71
110
|
onError: options.onError,
|
|
72
111
|
requestTimeout: options.requestTimeout,
|
|
@@ -88,31 +127,3 @@ export async function checkForUpdate(
|
|
|
88
127
|
};
|
|
89
128
|
});
|
|
90
129
|
}
|
|
91
|
-
|
|
92
|
-
export interface GetUpdateSourceOptions {
|
|
93
|
-
/**
|
|
94
|
-
* The update strategy to use.
|
|
95
|
-
* @description
|
|
96
|
-
* - "fingerprint": Use the fingerprint hash to check for updates.
|
|
97
|
-
* - "appVersion": Use the target app version to check for updates.
|
|
98
|
-
*/
|
|
99
|
-
updateStrategy: "appVersion" | "fingerprint";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const getUpdateSource =
|
|
103
|
-
(baseUrl: string, options: GetUpdateSourceOptions) =>
|
|
104
|
-
(params: UpdateBundleParams) => {
|
|
105
|
-
switch (options.updateStrategy) {
|
|
106
|
-
case "fingerprint": {
|
|
107
|
-
if (!params.fingerprintHash) {
|
|
108
|
-
throw new HotUpdaterError("Fingerprint hash is required");
|
|
109
|
-
}
|
|
110
|
-
return `${baseUrl}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
111
|
-
}
|
|
112
|
-
case "appVersion": {
|
|
113
|
-
return `${baseUrl}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
114
|
-
}
|
|
115
|
-
default:
|
|
116
|
-
return baseUrl;
|
|
117
|
-
}
|
|
118
|
-
};
|
package/src/error.ts
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot Updater Error Codes
|
|
3
|
+
*
|
|
4
|
+
* This file defines all possible error codes that can be thrown by the native
|
|
5
|
+
* updateBundle function. These error codes are shared across iOS and Android
|
|
6
|
+
* implementations to ensure consistent error handling.
|
|
7
|
+
*
|
|
8
|
+
* Error Classification:
|
|
9
|
+
* - Parameter Validation: Invalid or missing function parameters
|
|
10
|
+
* - Bundle Storage: Errors during download, extraction, and storage
|
|
11
|
+
* - Signature Verification: Cryptographic verification failures (collapsed to a single public code)
|
|
12
|
+
* - Internal: Platform-specific or unexpected errors
|
|
13
|
+
*
|
|
14
|
+
* Retryability:
|
|
15
|
+
* - Retryable: DOWNLOAD_FAILED, INCOMPLETE_DOWNLOAD
|
|
16
|
+
* - Non-retryable: Most validation and verification errors
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export enum HotUpdaterErrorCode {
|
|
20
|
+
// ==================== Parameter Validation Errors ====================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bundle ID is missing or empty.
|
|
24
|
+
* Thrown when bundleId parameter is null, undefined, or empty string.
|
|
25
|
+
* @retryable false
|
|
26
|
+
*/
|
|
27
|
+
MISSING_BUNDLE_ID = "MISSING_BUNDLE_ID",
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* File URL is invalid or malformed.
|
|
31
|
+
* Thrown when fileUrl parameter cannot be parsed as a valid URL.
|
|
32
|
+
* @retryable false
|
|
33
|
+
*/
|
|
34
|
+
INVALID_FILE_URL = "INVALID_FILE_URL",
|
|
35
|
+
|
|
36
|
+
// ==================== Bundle Storage Errors ====================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Failed to create required directory for bundle storage.
|
|
40
|
+
* Thrown when bundle directory creation fails due to permissions or disk errors.
|
|
41
|
+
* @retryable false - Usually indicates permissions or filesystem corruption
|
|
42
|
+
*/
|
|
43
|
+
DIRECTORY_CREATION_FAILED = "DIRECTORY_CREATION_FAILED",
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bundle download failed.
|
|
47
|
+
* Covers network errors, HTTP errors (4xx/5xx), timeouts, and connection issues.
|
|
48
|
+
* Check error message for specific cause (network, HTTP status code, etc.).
|
|
49
|
+
* @retryable true - Network issues are often transient
|
|
50
|
+
*/
|
|
51
|
+
DOWNLOAD_FAILED = "DOWNLOAD_FAILED",
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Download incomplete - received size doesn't match expected size.
|
|
55
|
+
* Thrown when downloaded file size doesn't match Content-Length header.
|
|
56
|
+
* Error message includes both expected and actual byte counts.
|
|
57
|
+
* @retryable true - Download may succeed on retry
|
|
58
|
+
*/
|
|
59
|
+
INCOMPLETE_DOWNLOAD = "INCOMPLETE_DOWNLOAD",
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Bundle archive format is invalid or corrupted.
|
|
63
|
+
* Thrown when ZIP file has wrong magic bytes, invalid structure, or unsupported format.
|
|
64
|
+
* Also thrown for path traversal attempts during extraction.
|
|
65
|
+
* @retryable false - Indicates corrupted or malicious bundle
|
|
66
|
+
*/
|
|
67
|
+
EXTRACTION_FORMAT_ERROR = "EXTRACTION_FORMAT_ERROR",
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Bundle missing required platform files.
|
|
71
|
+
* Thrown when extracted bundle doesn't contain index.android.bundle (Android)
|
|
72
|
+
* or main.jsbundle (iOS).
|
|
73
|
+
* @retryable false - Indicates incorrectly built bundle
|
|
74
|
+
*/
|
|
75
|
+
INVALID_BUNDLE = "INVALID_BUNDLE",
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Insufficient disk space for bundle download and extraction.
|
|
79
|
+
* Thrown when available disk space is less than required (file size * 2).
|
|
80
|
+
* Error message includes required and available bytes.
|
|
81
|
+
* @retryable false - User must free up disk space
|
|
82
|
+
*/
|
|
83
|
+
INSUFFICIENT_DISK_SPACE = "INSUFFICIENT_DISK_SPACE",
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Bundle signature verification failed (general).
|
|
87
|
+
* Thrown when cryptographic signature verification fails.
|
|
88
|
+
* All signature/hash sub-errors are collapsed into this public code.
|
|
89
|
+
* @retryable false - Indicates tampered or incorrectly signed bundle
|
|
90
|
+
*/
|
|
91
|
+
SIGNATURE_VERIFICATION_FAILED = "SIGNATURE_VERIFICATION_FAILED",
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Failed to move bundle to final location.
|
|
95
|
+
* Thrown when atomic move from temp directory to final directory fails.
|
|
96
|
+
* iOS: Thrown if move operation fails.
|
|
97
|
+
* Android: Thrown if rename, move, AND copy all fail.
|
|
98
|
+
* @retryable false - Usually indicates filesystem corruption or permissions
|
|
99
|
+
*/
|
|
100
|
+
MOVE_OPERATION_FAILED = "MOVE_OPERATION_FAILED",
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bundle is in crashed history and cannot be applied.
|
|
104
|
+
* Thrown when attempting to install a bundle that previously caused a crash.
|
|
105
|
+
* Use HotUpdater.clearCrashHistory() to allow retrying this bundle.
|
|
106
|
+
* @retryable false - Bundle was marked as crashed for safety
|
|
107
|
+
*/
|
|
108
|
+
BUNDLE_IN_CRASHED_HISTORY = "BUNDLE_IN_CRASHED_HISTORY",
|
|
109
|
+
|
|
110
|
+
// ==================== Signature Verification Errors ====================
|
|
111
|
+
// (Collapsed into SIGNATURE_VERIFICATION_FAILED)
|
|
112
|
+
|
|
113
|
+
// ==================== Internal Errors ====================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Internal error: self deallocated during update (iOS only).
|
|
117
|
+
* Thrown when the native object is deallocated mid-operation.
|
|
118
|
+
* iOS-specific due to manual memory management (ARC).
|
|
119
|
+
* Not applicable to Android (uses garbage collection).
|
|
120
|
+
* @platform iOS
|
|
121
|
+
* @retryable false - Memory management issue
|
|
122
|
+
*/
|
|
123
|
+
SELF_DEALLOCATED = "SELF_DEALLOCATED",
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* An unknown or unexpected error occurred.
|
|
127
|
+
* Catch-all for errors that don't fit other categories.
|
|
128
|
+
* Check error message for details.
|
|
129
|
+
* @retryable unknown - Depends on underlying cause
|
|
130
|
+
*/
|
|
131
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Type guard to check if an error is a HotUpdaterError
|
|
136
|
+
*/
|
|
137
|
+
export function isHotUpdaterError(
|
|
138
|
+
error: unknown,
|
|
139
|
+
): error is { code: HotUpdaterErrorCode; message: string } {
|
|
140
|
+
return (
|
|
141
|
+
typeof error === "object" &&
|
|
142
|
+
error !== null &&
|
|
143
|
+
"code" in error &&
|
|
144
|
+
typeof error.code === "string" &&
|
|
145
|
+
Object.values(HotUpdaterErrorCode).includes(
|
|
146
|
+
error.code as HotUpdaterErrorCode,
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Base error class for Hot Updater
|
|
153
|
+
*/
|
|
1
154
|
export class HotUpdaterError extends Error {
|
|
2
155
|
constructor(message: string) {
|
|
3
156
|
super(message);
|
package/src/fetchUpdateInfo.ts
CHANGED
|
@@ -1,76 +1,28 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AppUpdateInfo,
|
|
3
|
-
UpdateBundleParams,
|
|
4
|
-
UpdateStrategy,
|
|
5
|
-
} from "@hot-updater/core";
|
|
6
|
-
|
|
7
|
-
export type UpdateSource =
|
|
8
|
-
| string
|
|
9
|
-
| ((params: UpdateBundleParams) => Promise<AppUpdateInfo | null>)
|
|
10
|
-
| ((params: UpdateBundleParams) => string);
|
|
11
|
-
|
|
12
|
-
function buildRequestHeaders(
|
|
13
|
-
params: UpdateBundleParams,
|
|
14
|
-
requestHeaders?: Record<string, string>,
|
|
15
|
-
): Record<string, string> {
|
|
16
|
-
const updateStrategy: UpdateStrategy = params.fingerprintHash
|
|
17
|
-
? "fingerprint"
|
|
18
|
-
: "appVersion";
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
"x-app-platform": params.platform,
|
|
23
|
-
"x-bundle-id": params.bundleId,
|
|
24
|
-
...(updateStrategy === "fingerprint"
|
|
25
|
-
? { "x-fingerprint-hash": params.fingerprintHash! }
|
|
26
|
-
: { "x-app-version": params.appVersion }),
|
|
27
|
-
...(params.minBundleId && { "x-min-bundle-id": params.minBundleId }),
|
|
28
|
-
...(params.channel && { "x-channel": params.channel }),
|
|
29
|
-
...requestHeaders,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function resolveSource(
|
|
34
|
-
source: UpdateSource,
|
|
35
|
-
params: UpdateBundleParams,
|
|
36
|
-
): Promise<{ url: string } | { info: AppUpdateInfo | null }> {
|
|
37
|
-
if (typeof source !== "function") {
|
|
38
|
-
return { url: source };
|
|
39
|
-
}
|
|
40
|
-
const result = source(params);
|
|
41
|
-
if (typeof result === "string") {
|
|
42
|
-
return { url: result };
|
|
43
|
-
}
|
|
44
|
-
return { info: await result };
|
|
45
|
-
}
|
|
1
|
+
import type { AppUpdateInfo } from "@hot-updater/core";
|
|
46
2
|
|
|
47
3
|
export const fetchUpdateInfo = async ({
|
|
48
|
-
|
|
49
|
-
params,
|
|
4
|
+
url,
|
|
50
5
|
requestHeaders,
|
|
51
6
|
onError,
|
|
52
7
|
requestTimeout = 5000,
|
|
53
8
|
}: {
|
|
54
|
-
|
|
55
|
-
params: UpdateBundleParams;
|
|
9
|
+
url: string;
|
|
56
10
|
requestHeaders?: Record<string, string>;
|
|
57
11
|
onError?: (error: Error) => void;
|
|
58
12
|
requestTimeout?: number;
|
|
59
13
|
}): Promise<AppUpdateInfo | null> => {
|
|
60
14
|
try {
|
|
61
|
-
const resolvedSource = await resolveSource(source, params);
|
|
62
|
-
if ("info" in resolvedSource) {
|
|
63
|
-
return resolvedSource.info;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
15
|
const controller = new AbortController();
|
|
67
16
|
const timeoutId = setTimeout(() => {
|
|
68
17
|
controller.abort();
|
|
69
18
|
}, requestTimeout);
|
|
70
19
|
|
|
71
|
-
const headers =
|
|
20
|
+
const headers = {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...requestHeaders,
|
|
23
|
+
};
|
|
72
24
|
|
|
73
|
-
const response = await fetch(
|
|
25
|
+
const response = await fetch(url, {
|
|
74
26
|
signal: controller.signal,
|
|
75
27
|
headers,
|
|
76
28
|
});
|
|
@@ -80,8 +32,8 @@ export const fetchUpdateInfo = async ({
|
|
|
80
32
|
throw new Error(response.statusText);
|
|
81
33
|
}
|
|
82
34
|
return response.json();
|
|
83
|
-
} catch (error:
|
|
84
|
-
if (error.name === "AbortError") {
|
|
35
|
+
} catch (error: unknown) {
|
|
36
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
85
37
|
onError?.(new Error("Request timed out"));
|
|
86
38
|
} else {
|
|
87
39
|
onError?.(error as Error);
|