@hot-updater/react-native 0.22.1 → 0.23.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 (39) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +17 -12
  2. package/android/src/main/java/com/hotupdater/HotUpdater.kt +1 -1
  3. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +1 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +1 -1
  5. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +346 -0
  6. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +26 -18
  7. package/ios/HotUpdater/Internal/SignatureVerifier.swift +339 -0
  8. package/lib/commonjs/checkForUpdate.js +1 -1
  9. package/lib/commonjs/checkForUpdate.js.map +1 -1
  10. package/lib/commonjs/index.js +16 -1
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  13. package/lib/commonjs/types.js +45 -0
  14. package/lib/commonjs/types.js.map +1 -0
  15. package/lib/module/checkForUpdate.js +1 -1
  16. package/lib/module/checkForUpdate.js.map +1 -1
  17. package/lib/module/index.js +1 -0
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  20. package/lib/module/types.js +40 -0
  21. package/lib/module/types.js.map +1 -0
  22. package/lib/typescript/commonjs/index.d.ts +1 -0
  23. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +7 -2
  25. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/types.d.ts +34 -0
  27. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  28. package/lib/typescript/module/index.d.ts +1 -0
  29. package/lib/typescript/module/index.d.ts.map +1 -1
  30. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +7 -2
  31. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  32. package/lib/typescript/module/types.d.ts +34 -0
  33. package/lib/typescript/module/types.d.ts.map +1 -0
  34. package/package.json +7 -7
  35. package/plugin/build/transformers.js +269 -0
  36. package/src/checkForUpdate.ts +1 -1
  37. package/src/index.ts +5 -0
  38. package/src/specs/NativeHotUpdater.ts +7 -2
  39. package/src/types.ts +63 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgBpE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GACf,4BAA4B,CAS9B"}
@@ -3,6 +3,7 @@ import { updateBundle } from "./native";
3
3
  import { wrap } from "./wrap";
4
4
  export type { HotUpdaterEvent } from "./native";
5
5
  export * from "./store";
6
+ export { extractSignatureFailure, isSignatureVerificationError, type SignatureVerificationFailure, } from "./types";
6
7
  export type { HotUpdaterOptions } from "./wrap";
7
8
  export declare const HotUpdater: {
8
9
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAQL,YAAY,EACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,cAAc,SAAS,CAAC;AACxB,YAAY,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAQhD,eAAO,MAAM,UAAU;IACrB;;;;;;;;;;;;;;;;;;;;;OAqBG;;IAEH;;OAEG;;IAEH;;;;;;;;;;;;;;;;OAgBG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;;;;;;;;;;;OAYG;;IAEH;;;;;;;;;;;;;;;;OAgBG;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;;IAEH;;;;;;;;;;OAUG;;CAEJ,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAQL,YAAY,EACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,4BAA4B,GAClC,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAQhD,eAAO,MAAM,UAAU;IACrB;;;;;;;;;;;;;;;;;;;;;OAqBG;;IAEH;;OAEG;;IAEH;;;;;;;;;;;;;;;;OAgBG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;IAEH;;;;;;;;;;;;OAYG;;IAEH;;;;;;;;;;;;;;;;OAgBG;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;;IAEH;;;;;;;;;;OAUG;;CAEJ,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
@@ -3,8 +3,13 @@ export interface UpdateBundleParams {
3
3
  bundleId: string;
4
4
  fileUrl: string | null;
5
5
  /**
6
- * SHA256 hash of the bundle file for integrity verification.
7
- * If provided, the native layer will verify the downloaded file's hash.
6
+ * File hash for integrity/signature verification.
7
+ *
8
+ * Format depends on signing configuration:
9
+ * - Signed: `sig:<base64_signature>` - Native will verify signature (and implicitly hash)
10
+ * - Unsigned: `<hex_hash>` - Native will verify SHA256 hash only
11
+ *
12
+ * Native determines verification mode by checking for "sig:" prefix.
8
13
  */
9
14
  fileHash: string | null;
10
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NativeHotUpdater.d.ts","sourceRoot":"","sources":["../../../../src/specs/NativeHotUpdater.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,IAAK,SAAQ,WAAW;IAEvC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAG3D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,MAAM;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,OAAO,EAAE,MAAM,CAAC;QAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,CAAC;CACH;;AAED,wBAAoE"}
1
+ {"version":3,"file":"NativeHotUpdater.d.ts","sourceRoot":"","sources":["../../../../src/specs/NativeHotUpdater.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,IAAK,SAAQ,WAAW;IAEvC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAG3D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,MAAM;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,OAAO,EAAE,MAAM,CAAC;QAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,CAAC;CACH;;AAED,wBAAoE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Information about a signature verification failure.
3
+ * This is a security-critical event that indicates the bundle
4
+ * may have been tampered with or the public key is misconfigured.
5
+ */
6
+ export interface SignatureVerificationFailure {
7
+ /**
8
+ * The bundle ID that failed verification.
9
+ */
10
+ bundleId: string;
11
+ /**
12
+ * Human-readable error message from the native layer.
13
+ */
14
+ message: string;
15
+ /**
16
+ * The underlying error object.
17
+ */
18
+ error: Error;
19
+ }
20
+ /**
21
+ * Checks if an error is a signature verification failure.
22
+ * Matches error messages from both iOS and Android native implementations.
23
+ *
24
+ * **IMPORTANT**: This function relies on specific error message patterns from native code.
25
+ * If you change the error messages in the native implementations, update these patterns:
26
+ * - iOS: `ios/HotUpdater/Internal/SignatureVerifier.swift` (SignatureVerificationError)
27
+ * - Android: `android/src/main/java/com/hotupdater/SignatureVerifier.kt` (SignatureVerificationException)
28
+ */
29
+ export declare function isSignatureVerificationError(error: unknown): boolean;
30
+ /**
31
+ * Extracts signature verification failure details from an error.
32
+ */
33
+ export declare function extractSignatureFailure(error: unknown, bundleId: string): SignatureVerificationFailure;
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgBpE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GACf,4BAA4B,CAS9B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -27,7 +27,7 @@
27
27
  "ios",
28
28
  "cpp",
29
29
  "app.plugin.js",
30
- "plugin/build/withHotUpdater.js",
30
+ "plugin/build",
31
31
  "*.podspec",
32
32
  "react-native.config.js",
33
33
  "!ios/build",
@@ -119,14 +119,14 @@
119
119
  "react-native": "0.79.1",
120
120
  "react-native-builder-bob": "^0.40.10",
121
121
  "typescript": "^5.8.3",
122
- "hot-updater": "0.22.1"
122
+ "hot-updater": "0.23.0"
123
123
  },
124
124
  "dependencies": {
125
125
  "use-sync-external-store": "1.5.0",
126
- "@hot-updater/cli-tools": "0.22.1",
127
- "@hot-updater/js": "0.22.1",
128
- "@hot-updater/core": "0.22.1",
129
- "@hot-updater/plugin-core": "0.22.1"
126
+ "@hot-updater/cli-tools": "0.23.0",
127
+ "@hot-updater/plugin-core": "0.23.0",
128
+ "@hot-updater/js": "0.23.0",
129
+ "@hot-updater/core": "0.23.0"
130
130
  },
131
131
  "scripts": {
132
132
  "build": "bob build && tsc -p plugin/tsconfig.build.json",
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * Pure transformation functions for HotUpdater code injection
4
+ * These utilities handle code transformations for different React Native patterns
5
+ */
6
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
7
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
8
+ if (ar || !(i in from)) {
9
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
10
+ ar[i] = from[i];
11
+ }
12
+ }
13
+ return to.concat(ar || Array.prototype.slice.call(from));
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.transformAndroid = transformAndroid;
17
+ exports.transformIOS = transformIOS;
18
+ /**
19
+ * Helper to add lines if they don't exist, anchored by a specific string.
20
+ */
21
+ function addLinesOnce(contents, anchor, linesToAdd) {
22
+ if (linesToAdd.every(function (line) { return contents.includes(line); })) {
23
+ // All lines already exist, do nothing
24
+ return contents;
25
+ }
26
+ // Check if the anchor exists
27
+ if (!contents.includes(anchor)) {
28
+ // Anchor not found, cannot add lines reliably.
29
+ return contents;
30
+ }
31
+ // Add lines after the anchor
32
+ return contents.replace(anchor, "".concat(anchor, "\n").concat(linesToAdd.join("\n")));
33
+ }
34
+ /**
35
+ * Android: handle getDefaultReactHost pattern (RN 0.82+ style).
36
+ * Adds jsBundleFilePath parameter to the call.
37
+ */
38
+ function transformAndroidReactHost(contents) {
39
+ var kotlinImport = "import com.hotupdater.HotUpdater";
40
+ var kotlinImportAnchor = "import com.facebook.react.ReactApplication";
41
+ var kotlinMethodCheck = "HotUpdater.getJSBundleFile(applicationContext)";
42
+ // Quick pattern detection: only touch files using getDefaultReactHost
43
+ // with the new RN 0.82+ parameter style.
44
+ if (!contents.includes("getDefaultReactHost(") ||
45
+ !contents.includes("packageList =")) {
46
+ return contents;
47
+ }
48
+ // 1. Ensure HotUpdater import exists (idempotent via addLinesOnce)
49
+ var result = addLinesOnce(contents, kotlinImportAnchor, [kotlinImport]);
50
+ // 2. If jsBundleFilePath is already wired to HotUpdater, do nothing
51
+ if (result.includes(kotlinMethodCheck) &&
52
+ result.includes("jsBundleFilePath")) {
53
+ return result;
54
+ }
55
+ var lines = result.split("\n");
56
+ var callIndex = lines.findIndex(function (line) {
57
+ return line.includes("getDefaultReactHost(");
58
+ });
59
+ if (callIndex === -1) {
60
+ return result;
61
+ }
62
+ // Determine the indentation used for parameters (e.g. " ")
63
+ var paramIndent = "";
64
+ for (var i = callIndex + 1; i < lines.length; i += 1) {
65
+ var line = lines[i];
66
+ var trimmed = line.trim();
67
+ if (trimmed.length === 0) {
68
+ continue;
69
+ }
70
+ if (trimmed.startsWith(")")) {
71
+ // No parameters detected, give up safely.
72
+ return result;
73
+ }
74
+ var indentMatch = line.match(/^(\s*)/);
75
+ paramIndent = indentMatch ? indentMatch[1] : "";
76
+ break;
77
+ }
78
+ if (!paramIndent) {
79
+ return result;
80
+ }
81
+ // Find the closing line of the call (a line that is just ")" with indentation).
82
+ var closingIndex = -1;
83
+ for (var i = callIndex + 1; i < lines.length; i += 1) {
84
+ if (lines[i].trim() === ")") {
85
+ closingIndex = i;
86
+ break;
87
+ }
88
+ }
89
+ if (closingIndex === -1) {
90
+ return result;
91
+ }
92
+ // Avoid inserting twice if jsBundleFilePath already added somewhere in the call.
93
+ for (var i = callIndex; i < closingIndex; i += 1) {
94
+ if (lines[i].includes("jsBundleFilePath")) {
95
+ return result;
96
+ }
97
+ }
98
+ var jsBundleLine = "".concat(paramIndent, "jsBundleFilePath = HotUpdater.getJSBundleFile(applicationContext),");
99
+ lines.splice(closingIndex, 0, jsBundleLine);
100
+ return lines.join("\n");
101
+ }
102
+ /**
103
+ * Android: DefaultReactNativeHost pattern (RN 0.81 / Expo 54).
104
+ * Adds getJSBundleFile() override to the host.
105
+ */
106
+ function transformAndroidDefaultHost(contents) {
107
+ var _a;
108
+ var kotlinImport = "import com.hotupdater.HotUpdater";
109
+ var kotlinImportAnchor = "import com.facebook.react.ReactApplication";
110
+ var kotlinReactNativeHostAnchor = "object : DefaultReactNativeHost(this) {";
111
+ var kotlinMethodCheck = "HotUpdater.getJSBundleFile(applicationContext)";
112
+ var kotlinExistingMethodRegex = /^\s*override fun getJSBundleFile\(\): String\?\s*\{[\s\S]*?^\s*\}/gm;
113
+ var kotlinHermesAnchor = "override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED";
114
+ var kotlinNewArchAnchor = "override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED";
115
+ // Check if this is the old pattern with DefaultReactNativeHost
116
+ if (!contents.includes(kotlinReactNativeHostAnchor)) {
117
+ return contents;
118
+ }
119
+ // 1. Add import if missing
120
+ var result = addLinesOnce(contents, kotlinImportAnchor, [kotlinImport]);
121
+ // 2. Add/Replace getJSBundleFile method if needed
122
+ if (!result.includes(kotlinMethodCheck)) {
123
+ // Remove potentially existing (different) override first
124
+ result = result.replace(kotlinExistingMethodRegex, "");
125
+ var lines_1 = result.split("\n");
126
+ var findLineIndex = function (needle) {
127
+ for (var i = 0; i < lines_1.length; i += 1) {
128
+ if (lines_1[i].includes(needle)) {
129
+ return i;
130
+ }
131
+ }
132
+ return -1;
133
+ };
134
+ // Prefer inserting after Hermes line, then after new architecture line
135
+ var anchorIndex = findLineIndex(kotlinHermesAnchor);
136
+ if (anchorIndex === -1) {
137
+ anchorIndex = findLineIndex(kotlinNewArchAnchor);
138
+ }
139
+ if (anchorIndex !== -1) {
140
+ var indentMatch = lines_1[anchorIndex].match(/^\s*/);
141
+ var indent = indentMatch ? indentMatch[0] : "";
142
+ var objectLine = lines_1.find(function (line) {
143
+ return line.includes("object : DefaultReactNativeHost");
144
+ });
145
+ var indentSize = 2;
146
+ if (objectLine) {
147
+ var objectIndent = (((_a = objectLine.match(/^\s*/)) === null || _a === void 0 ? void 0 : _a[0]) || "").length;
148
+ var propertyIndent = indent.length;
149
+ var diff = propertyIndent - objectIndent;
150
+ if (diff > 0) {
151
+ indentSize = diff;
152
+ }
153
+ }
154
+ var spaces = indentSize === 2 ? " " : " ";
155
+ var bodyIndent = indent + spaces;
156
+ var methodLines = [
157
+ "", // blank line
158
+ "".concat(indent, "override fun getJSBundleFile(): String? {"),
159
+ "".concat(bodyIndent, "return HotUpdater.getJSBundleFile(applicationContext)"),
160
+ "".concat(indent, "}"),
161
+ ];
162
+ var insertIndex = anchorIndex + 1;
163
+ lines_1.splice.apply(lines_1, __spreadArray([insertIndex, 0], methodLines, false));
164
+ result = lines_1.join("\n");
165
+ }
166
+ else {
167
+ // Fallback: insert before the closing brace of the object block
168
+ var hostStartIndex = lines_1.findIndex(function (line) {
169
+ return line.includes("object : DefaultReactNativeHost");
170
+ });
171
+ if (hostStartIndex === -1) {
172
+ throw new Error("[transformAndroidDefaultHost] Could not find DefaultReactNativeHost block.");
173
+ }
174
+ var hostEndIndex = -1;
175
+ for (var i = lines_1.length - 1; i > hostStartIndex; i -= 1) {
176
+ if (lines_1[i].trim() === "}") {
177
+ hostEndIndex = i;
178
+ break;
179
+ }
180
+ }
181
+ if (hostEndIndex === -1) {
182
+ throw new Error("[transformAndroidDefaultHost] Could not find end of DefaultReactNativeHost block.");
183
+ }
184
+ var indentMatch = lines_1[hostEndIndex].match(/^\s*/);
185
+ var indent = indentMatch ? indentMatch[0] : "";
186
+ var bodyIndent = "".concat(indent, " ");
187
+ var methodLines = [
188
+ "".concat(indent, "override fun getJSBundleFile(): String? {"),
189
+ "".concat(bodyIndent, "return HotUpdater.getJSBundleFile(applicationContext)"),
190
+ "".concat(indent, "}"),
191
+ ];
192
+ lines_1.splice.apply(lines_1, __spreadArray([hostEndIndex, 0], methodLines, false));
193
+ result = lines_1.join("\n");
194
+ }
195
+ }
196
+ return result;
197
+ }
198
+ /**
199
+ * Public Android transformer that applies all Android-specific transforms.
200
+ */
201
+ function transformAndroid(contents) {
202
+ var result = contents;
203
+ result = transformAndroidReactHost(result);
204
+ result = transformAndroidDefaultHost(result);
205
+ return result;
206
+ }
207
+ /**
208
+ * iOS: Objective-C AppDelegate transformation.
209
+ * Replaces NSBundle-based bundleURL with HotUpdater bundleURL.
210
+ */
211
+ function transformIOSObjC(contents) {
212
+ var iosImport = "#import <HotUpdater/HotUpdater.h>";
213
+ var iosBundleUrl = "[HotUpdater bundleURL]";
214
+ var iosOriginalBundleUrlRegex = /\[\[NSBundle mainBundle\] URLForResource:@"main" withExtension:@"jsbundle"\]/g;
215
+ var iosAppDelegateHeader = '#import "AppDelegate.h"';
216
+ // Check if it's likely Obj-C
217
+ if (!contents.includes(iosAppDelegateHeader)) {
218
+ return contents;
219
+ }
220
+ var result = contents;
221
+ // 1. Ensure HotUpdater import is present
222
+ if (!result.includes(iosImport)) {
223
+ result = addLinesOnce(result, iosAppDelegateHeader, [iosImport]);
224
+ }
225
+ // 2. Swap NSBundle-based URL with HotUpdater bundleURL, but only once
226
+ if (!result.includes(iosBundleUrl) &&
227
+ iosOriginalBundleUrlRegex.test(result)) {
228
+ result = result.replace(iosOriginalBundleUrlRegex, iosBundleUrl);
229
+ }
230
+ return result;
231
+ }
232
+ /**
233
+ * iOS: Swift / Expo AppDelegate transformation.
234
+ * Replaces Bundle.main.url-based bundleURL with HotUpdater.bundleURL().
235
+ */
236
+ function transformIOSSwift(contents) {
237
+ var swiftImport = "import HotUpdater";
238
+ var swiftBundleUrl = "HotUpdater.bundleURL()";
239
+ var swiftOriginalBundleUrlRegex = /Bundle\.main\.url\(forResource: "?main"?, withExtension: "jsbundle"\)/g;
240
+ // Check if it's likely Swift AppDelegate code
241
+ if (!contents.includes("import ")) {
242
+ return contents;
243
+ }
244
+ // 1. Add import if missing - find the last import statement and add after it
245
+ var result = contents;
246
+ if (!result.includes(swiftImport)) {
247
+ // Find the last import statement
248
+ var lastImportMatch = result.match(/^import .*$/gm);
249
+ if (lastImportMatch) {
250
+ var lastImport = lastImportMatch[lastImportMatch.length - 1];
251
+ result = result.replace(lastImport, "".concat(lastImport, "\n").concat(swiftImport));
252
+ }
253
+ }
254
+ // 2. Replace bundleURL provider if the original exists and hasn't been replaced
255
+ if (!result.includes(swiftBundleUrl) &&
256
+ swiftOriginalBundleUrlRegex.test(result)) {
257
+ result = result.replace(swiftOriginalBundleUrlRegex, swiftBundleUrl);
258
+ }
259
+ return result;
260
+ }
261
+ /**
262
+ * Public iOS transformer that applies both Objective-C and Swift transforms.
263
+ */
264
+ function transformIOS(contents) {
265
+ var result = contents;
266
+ result = transformIOSObjC(result);
267
+ result = transformIOSSwift(result);
268
+ return result;
269
+ }
@@ -81,7 +81,7 @@ export async function checkForUpdate(
81
81
  return updateBundle({
82
82
  bundleId: updateInfo.id,
83
83
  fileUrl: updateInfo.fileUrl,
84
- fileHash: updateInfo?.fileHash ?? null,
84
+ fileHash: updateInfo.fileHash,
85
85
  status: updateInfo.status,
86
86
  });
87
87
  },
package/src/index.ts CHANGED
@@ -15,6 +15,11 @@ import { wrap } from "./wrap";
15
15
 
16
16
  export type { HotUpdaterEvent } from "./native";
17
17
  export * from "./store";
18
+ export {
19
+ extractSignatureFailure,
20
+ isSignatureVerificationError,
21
+ type SignatureVerificationFailure,
22
+ } from "./types";
18
23
  export type { HotUpdaterOptions } from "./wrap";
19
24
 
20
25
  addListener("onProgress", ({ progress }) => {
@@ -5,8 +5,13 @@ export interface UpdateBundleParams {
5
5
  bundleId: string;
6
6
  fileUrl: string | null;
7
7
  /**
8
- * SHA256 hash of the bundle file for integrity verification.
9
- * If provided, the native layer will verify the downloaded file's hash.
8
+ * File hash for integrity/signature verification.
9
+ *
10
+ * Format depends on signing configuration:
11
+ * - Signed: `sig:<base64_signature>` - Native will verify signature (and implicitly hash)
12
+ * - Unsigned: `<hex_hash>` - Native will verify SHA256 hash only
13
+ *
14
+ * Native determines verification mode by checking for "sig:" prefix.
10
15
  */
11
16
  fileHash: string | null;
12
17
  }
package/src/types.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Information about a signature verification failure.
3
+ * This is a security-critical event that indicates the bundle
4
+ * may have been tampered with or the public key is misconfigured.
5
+ */
6
+ export interface SignatureVerificationFailure {
7
+ /**
8
+ * The bundle ID that failed verification.
9
+ */
10
+ bundleId: string;
11
+ /**
12
+ * Human-readable error message from the native layer.
13
+ */
14
+ message: string;
15
+ /**
16
+ * The underlying error object.
17
+ */
18
+ error: Error;
19
+ }
20
+
21
+ /**
22
+ * Checks if an error is a signature verification failure.
23
+ * Matches error messages from both iOS and Android native implementations.
24
+ *
25
+ * **IMPORTANT**: This function relies on specific error message patterns from native code.
26
+ * If you change the error messages in the native implementations, update these patterns:
27
+ * - iOS: `ios/HotUpdater/Internal/SignatureVerifier.swift` (SignatureVerificationError)
28
+ * - Android: `android/src/main/java/com/hotupdater/SignatureVerifier.kt` (SignatureVerificationException)
29
+ */
30
+ export function isSignatureVerificationError(error: unknown): boolean {
31
+ if (!(error instanceof Error)) {
32
+ return false;
33
+ }
34
+
35
+ const message = error.message.toLowerCase();
36
+
37
+ // Match iOS SignatureVerificationError messages
38
+ // Match Android SignatureVerificationException messages
39
+ return (
40
+ message.includes("signature verification") ||
41
+ message.includes("public key not configured") ||
42
+ message.includes("public key format is invalid") ||
43
+ message.includes("signature format is invalid") ||
44
+ message.includes("bundle may be corrupted or tampered")
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Extracts signature verification failure details from an error.
50
+ */
51
+ export function extractSignatureFailure(
52
+ error: unknown,
53
+ bundleId: string,
54
+ ): SignatureVerificationFailure {
55
+ const normalizedError =
56
+ error instanceof Error ? error : new Error(String(error));
57
+
58
+ return {
59
+ bundleId,
60
+ message: normalizedError.message,
61
+ error: normalizedError,
62
+ };
63
+ }