react-native-bspatch 4.0.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -30
- package/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +86 -6
- package/android/src/main/res/values/bspatch_public_key.xml +5 -0
- package/cpp/bsdiff/bspatch_wrapper.c +34 -0
- package/cpp/bsdiff/bspatch_wrapper.h +2 -0
- package/cpp/bspatch_jni.cpp +18 -0
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +9 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +124 -32
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/eligibility.js +40 -0
- package/lib/commonjs/ota/eligibility.js.map +1 -0
- package/lib/commonjs/ota/entrySignature.js +23 -0
- package/lib/commonjs/ota/entrySignature.js.map +1 -0
- package/lib/commonjs/ota/installId.js +27 -0
- package/lib/commonjs/ota/installId.js.map +1 -0
- package/lib/commonjs/ota/otaCore.js +96 -21
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/storage/{GitHubRawStorage.js → R2Storage.js} +20 -15
- package/lib/commonjs/ota/storage/R2Storage.js.map +1 -0
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +124 -32
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/eligibility.js +34 -0
- package/lib/module/ota/eligibility.js.map +1 -0
- package/lib/module/ota/entrySignature.js +17 -0
- package/lib/module/ota/entrySignature.js.map +1 -0
- package/lib/module/ota/installId.js +22 -0
- package/lib/module/ota/installId.js.map +1 -0
- package/lib/module/ota/otaCore.js +96 -21
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/storage/{GitHubRawStorage.js → R2Storage.js} +18 -14
- package/lib/module/ota/storage/R2Storage.js.map +1 -0
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +28 -2
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/eligibility.d.ts +19 -0
- package/lib/typescript/ota/eligibility.d.ts.map +1 -0
- package/lib/typescript/ota/entrySignature.d.ts +9 -0
- package/lib/typescript/ota/entrySignature.d.ts.map +1 -0
- package/lib/typescript/ota/installId.d.ts +3 -0
- package/lib/typescript/ota/installId.d.ts.map +1 -0
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/storage/{GitHubRawStorage.d.ts → R2Storage.d.ts} +4 -3
- package/lib/typescript/ota/storage/R2Storage.d.ts.map +1 -0
- package/lib/typescript/ota/types.d.ts +54 -5
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/react-native-bspatch.podspec +3 -1
- package/src/NativeBsPatch.ts +34 -2
- package/src/index.ts +5 -1
- package/src/ota/OtaProvider.tsx +128 -43
- package/src/ota/eligibility.ts +48 -0
- package/src/ota/entrySignature.ts +31 -0
- package/src/ota/installId.ts +18 -0
- package/src/ota/otaCore.ts +102 -21
- package/src/ota/storage/{GitHubRawStorage.ts → R2Storage.ts} +17 -13
- package/src/ota/types.ts +51 -5
- package/tools/build_ota.sh +343 -116
- package/tools/doctor.js +130 -0
- package/tools/ensure_signing_key.js +216 -0
- package/tools/fingerprint.js +111 -0
- package/tools/ios_fingerprint.sh +9 -0
- package/tools/ota_admin.js +131 -0
- package/tools/setup_project.js +41 -15
- package/tools/update_registry.js +111 -11
- package/tools/upload_ota.sh +85 -184
- package/tools/verify_publish.js +81 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"R2Storage.d.ts","sourceRoot":"","sources":["../../../../src/ota/storage/R2Storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAmBxE,uFAAuF;AACvF,eAAO,MAAM,SAAS,GAClB,KAAK,MAAM,EACX,WAAW,MAAM,EACjB,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACjC,OAAO,CAAC,GAAG,CAYb,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAKlD,CAAC;AAEF,qBAAa,iBAAkB,YAAW,kBAAkB;IAClD,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAiCrD,gBAAgB,CAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,GAClB,OAAO,CAAC,MAAM,CAAC;IAeZ,WAAW,CACb,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAClB,OAAO,CAAC,MAAM,CAAC;CAWrB"}
|
|
@@ -43,16 +43,38 @@ export type OtaPatch = {
|
|
|
43
43
|
minAppVersion?: string;
|
|
44
44
|
/** Full bundle file: used when there is no OTA bundle to patch from, or the patch fails */
|
|
45
45
|
fullBundleUrl?: string;
|
|
46
|
+
/** Size of the full bundle in bytes: download progress when it arrives Content-Encoded */
|
|
47
|
+
bundleSize?: number;
|
|
46
48
|
/** RSA/ECDSA SHA-256 signature (base64) of the patch file */
|
|
47
49
|
signature?: string;
|
|
48
50
|
/** RSA/ECDSA SHA-256 signature (base64) of the resulting bundle */
|
|
49
51
|
bundleSignature?: string;
|
|
50
52
|
/** URL to the asset manifest JSON for this version */
|
|
51
53
|
assetManifestUrl?: string;
|
|
54
|
+
/** SHA-256 of the asset manifest file (covered by entrySignature) */
|
|
55
|
+
assetManifestHash?: string;
|
|
56
|
+
/** Signature (base64) of entryPayload(entry): required when a public key is set */
|
|
57
|
+
entrySignature?: string;
|
|
52
58
|
/** Target platform for this patch */
|
|
53
59
|
platform?: string;
|
|
54
60
|
/** Flavor/channel for this patch */
|
|
55
61
|
flavor?: string;
|
|
62
|
+
/** How fullBundleUrl is compressed ('gzip' | 'bz2'); absent = raw .hbc */
|
|
63
|
+
fullBundleCompression?: string;
|
|
64
|
+
/** Native runtime fingerprint the bundle was built against: installs only on a matching binary */
|
|
65
|
+
runtimeFingerprint?: string;
|
|
66
|
+
/** Hermes bytecode version of the bundle: installs only on a runtime that reads it */
|
|
67
|
+
hermesBytecodeVersion?: number;
|
|
68
|
+
/** Client features this entry needs; a client missing one skips the entry */
|
|
69
|
+
requires?: string[];
|
|
70
|
+
/** Kill switch: no client installs this version */
|
|
71
|
+
disabled?: boolean;
|
|
72
|
+
/** Staged rollout: share of installs (0-100) that get this version, by a stable per-install bucket */
|
|
73
|
+
rolloutPercent?: number;
|
|
74
|
+
/** QA: only these install IDs (useOta().installId) get this version */
|
|
75
|
+
allowInstallIds?: string[];
|
|
76
|
+
/** Apply as soon as it's ready instead of on the next launch (BACKGROUND_DOWNLOAD) */
|
|
77
|
+
mandatory?: boolean;
|
|
56
78
|
};
|
|
57
79
|
/**
|
|
58
80
|
* A single content-addressed asset from the OTA asset store.
|
|
@@ -89,6 +111,8 @@ export type OtaMetrics = {
|
|
|
89
111
|
export type OtaNetworkStrategy = {
|
|
90
112
|
retry?: number;
|
|
91
113
|
timeoutMs?: number;
|
|
114
|
+
/** Automatic downloads only on Wi-Fi/Ethernet (manual applyUpdate always downloads) */
|
|
115
|
+
wifiOnly?: boolean;
|
|
92
116
|
};
|
|
93
117
|
export type OtaConfig = {
|
|
94
118
|
/** Current app versionName (e.g. "1.0.0"). If omitted, auto-detected from native app. */
|
|
@@ -106,8 +130,22 @@ export type OtaConfig = {
|
|
|
106
130
|
/** Telemetry: structured event callback for analytics */
|
|
107
131
|
onEvent?: OtaEventCallback;
|
|
108
132
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
133
|
+
* POSTs every OTA event as JSON to `url` (fire-and-forget), with installId, platform,
|
|
134
|
+
* appVersion, appBuild, runtimeFingerprint, flavor and the running OTA version attached.
|
|
135
|
+
*/
|
|
136
|
+
telemetry?: {
|
|
137
|
+
url: string;
|
|
138
|
+
headers?: Record<string, string>;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Minimum time between automatic update checks (app start / foreground / checkUpdate()).
|
|
142
|
+
* checkUpdate({ force: true }) ignores it. Default 10 minutes.
|
|
143
|
+
*/
|
|
144
|
+
minCheckIntervalMs?: number;
|
|
145
|
+
/**
|
|
146
|
+
* SPKI public key(s), PEM or base64 (comma-separated for several), for RSA/ECDSA
|
|
147
|
+
* signature verification. Defaults to the keys `patch` embedded in the app.
|
|
148
|
+
* When there is a key, unsigned or wrongly signed updates are rejected.
|
|
111
149
|
*/
|
|
112
150
|
publicKey?: string;
|
|
113
151
|
/**
|
|
@@ -116,12 +154,14 @@ export type OtaConfig = {
|
|
|
116
154
|
* Read-only and public: never put upload credentials here, they ship inside the app.
|
|
117
155
|
*/
|
|
118
156
|
customServer?: {
|
|
119
|
-
/** e.g. https://
|
|
157
|
+
/** R2 public URL, e.g. https://pub-<id>.r2.dev or a custom domain */
|
|
120
158
|
baseUrl: string;
|
|
121
159
|
channel: string;
|
|
122
160
|
baseFolder?: string;
|
|
161
|
+
/** Extra headers on every request (registry, bundles, assets), e.g. auth for a private server */
|
|
162
|
+
headers?: Record<string, string>;
|
|
123
163
|
};
|
|
124
|
-
/** Storage provider instance (optional, defaults to
|
|
164
|
+
/** Storage provider instance (optional, defaults to R2StorageProvider) */
|
|
125
165
|
storage?: OtaStorageProvider;
|
|
126
166
|
};
|
|
127
167
|
export declare enum OtaStatus {
|
|
@@ -146,10 +186,19 @@ export type OtaState = {
|
|
|
146
186
|
metrics: OtaMetrics | null;
|
|
147
187
|
/** True if a background update has been downloaded and prepared silently */
|
|
148
188
|
isUpdateReady: boolean;
|
|
189
|
+
/** The ready update is marked mandatory: it reloads as soon as the app is in the foreground */
|
|
190
|
+
isMandatory: boolean;
|
|
191
|
+
/** Stable random ID of this install (rollout bucket, QA allow-lists, telemetry) */
|
|
192
|
+
installId: string;
|
|
149
193
|
applyUpdate: () => Promise<void>;
|
|
194
|
+
/** Installs and reloads `version` from the last checked manifest (full bundle, or a patch from the current version) */
|
|
195
|
+
applyVersion: (version: number) => Promise<void>;
|
|
150
196
|
resetToFactory: () => Promise<void>;
|
|
151
197
|
markAsHealthy: () => Promise<void>;
|
|
152
|
-
|
|
198
|
+
/** Checks now unless the last check was under minCheckIntervalMs ago (force: always) */
|
|
199
|
+
checkUpdate: (options?: {
|
|
200
|
+
force?: boolean;
|
|
201
|
+
}) => Promise<void>;
|
|
153
202
|
/** Reload the React Native JS bundle immediately to apply downloaded update */
|
|
154
203
|
reloadBundle: () => void;
|
|
155
204
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/ota/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,kBAAkB;IAC/B;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtD;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvE;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9E;AAED,oBAAY,aAAa;IACrB,MAAM,WAAW;IACjB,YAAY,iBAAiB;IAC7B,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;CAC9C;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/ota/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,kBAAkB;IAC/B;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtD;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvE;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9E;AAED,oBAAY,aAAa;IACrB,MAAM,WAAW;IACjB,YAAY,iBAAiB;IAC7B,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;CAC9C;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kGAAkG;IAClG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sFAAsF;IACtF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAGpB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sGAAsG;IACtG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACnB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,4FAA4F;IAC5F,YAAY,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,oGAAoG;IACpG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACpB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,gCAAgC;IAChC,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAE7B,yDAAyD;IACzD,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IAC9D;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,YAAY,CAAC,EAAE;QACX,qEAAqE;QACrE,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,iGAAiG;QACjG,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACpC,CAAC;IAEF,0EAA0E;IAC1E,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAChC,CAAC;AAEF,oBAAY,SAAS;IACjB,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,gBAAgB,qBAAqB;IACrC,UAAU,eAAe;IACzB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;IACrB,cAAc,mBAAmB;IACjC,OAAO,YAAY;IACnB,KAAK,UAAU;CAClB;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,+FAA+F;IAC/F,WAAW,EAAE,OAAO,CAAC;IACrB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,uHAAuH;IACvH,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,wFAAwF;IACxF,WAAW,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,+EAA+E;IAC/E,YAAY,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-bspatch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Premium OTA Update Solution for React Native using TurboModules and bsdiff",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"bump-version": "node ./tools/bump_version.js",
|
|
63
63
|
"generate-keys": "node ./tools/generate_keys.js",
|
|
64
64
|
"typecheck": "tsc --noEmit",
|
|
65
|
-
"prepare": "bob build"
|
|
65
|
+
"prepare": "bob build",
|
|
66
|
+
"test": "node tools/__tests__/entry_signature.test.js && node tools/__tests__/signing_key.test.js && node tools/__tests__/eligibility.test.js"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@types/react": "^19.2.0",
|
|
@@ -18,7 +18,9 @@ Pod::Spec.new do |s|
|
|
|
18
18
|
s.exclude_files = "cpp/bspatch_jni.cpp", "cpp/bzip2/**"
|
|
19
19
|
s.public_header_files = 'ios/**/*.h'
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# bz2: bspatch + decompressFile, z: decompressFile gzip, Network: getNetworkType
|
|
22
|
+
s.libraries = 'bz2', 'z'
|
|
23
|
+
s.frameworks = 'Network'
|
|
22
24
|
|
|
23
25
|
s.pod_target_xcconfig = {
|
|
24
26
|
'OTHER_CFLAGS' => '$(inherited) -O3 -flto'
|
package/src/NativeBsPatch.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type DownloadProgress = {
|
|
|
5
5
|
/** destPath passed to downloadFile */
|
|
6
6
|
path: string;
|
|
7
7
|
received: number;
|
|
8
|
-
/** -1 when
|
|
8
|
+
/** -1 when unknown: no Content-Length, or a Content-Encoding (received counts decoded bytes) */
|
|
9
9
|
total: number;
|
|
10
10
|
};
|
|
11
11
|
|
|
@@ -60,13 +60,45 @@ export interface Spec extends TurboModule {
|
|
|
60
60
|
/** Directory the native resolver reads `ota/` from (filesDir on Android, Documents on iOS) */
|
|
61
61
|
getDocumentDir(): string;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Public keys the CLI embedded in the app, comma-separated base64 SPKI (primary first,
|
|
65
|
+
* then the offline backup): Info.plist `BsPatchPublicKey` / Android string
|
|
66
|
+
* `bspatch_public_key`. '' when none.
|
|
67
|
+
*/
|
|
68
|
+
getEmbeddedPublicKeys(): string;
|
|
69
|
+
|
|
70
|
+
/** Build number: versionCode (Android) / CFBundleVersion (iOS) */
|
|
71
|
+
getAppBuild(): string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Native runtime fingerprint stamped into this binary at build time (Info.plist
|
|
75
|
+
* `BsPatchRuntimeFingerprint` / Android resValue `bspatch_runtime_fingerprint`);
|
|
76
|
+
* '' when the build hook didn't run. An OTA only installs on a matching binary.
|
|
77
|
+
*/
|
|
78
|
+
getRuntimeFingerprint(): string;
|
|
79
|
+
|
|
80
|
+
/** Free bytes on the volume holding getDocumentDir() */
|
|
81
|
+
getFreeDiskSpace(): Promise<number>;
|
|
82
|
+
|
|
83
|
+
/** Current connection: 'wifi' | 'cellular' | 'ethernet' | 'none' | 'unknown' */
|
|
84
|
+
getNetworkType(): Promise<string>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Decompresses srcPath into destPath (replacing it). format: 'gzip' | 'bz2'.
|
|
88
|
+
* Rejects on unknown format or corrupt input; destPath is removed on failure.
|
|
89
|
+
*/
|
|
90
|
+
decompressFile(srcPath: string, destPath: string, format: string): Promise<void>;
|
|
91
|
+
|
|
63
92
|
/**
|
|
64
93
|
* Streams `url` to `destPath` (via `destPath.part`, renamed only on a 2xx response,
|
|
65
94
|
* replacing any existing file). Rejects on network error or non-2xx status.
|
|
95
|
+
* A failed download of the same destPath is resumed (HTTP Range) on the next call
|
|
96
|
+
* when the server supports it; otherwise it restarts from zero.
|
|
66
97
|
* @param timeoutMs Idle timeout: no bytes for this long fails the download
|
|
98
|
+
* @param headersJson Extra request headers as a JSON object string ('{}' for none)
|
|
67
99
|
* @returns Size of the downloaded file in bytes
|
|
68
100
|
*/
|
|
69
|
-
downloadFile(url: string, destPath: string, timeoutMs: number): Promise<number>;
|
|
101
|
+
downloadFile(url: string, destPath: string, timeoutMs: number, headersJson: string): Promise<number>;
|
|
70
102
|
|
|
71
103
|
/** Lowercase hex SHA-256 of a file */
|
|
72
104
|
hashFile(path: string): Promise<string>;
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,11 @@ export * from './ota/otaLogger';
|
|
|
20
20
|
export * from './ota/OtaProvider';
|
|
21
21
|
export * from './ota/types';
|
|
22
22
|
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
R2StorageProvider,
|
|
25
|
+
/** @deprecated Renamed to R2StorageProvider */
|
|
26
|
+
R2StorageProvider as GitHubRawStorageProvider,
|
|
27
|
+
} from './ota/storage/R2Storage';
|
|
24
28
|
|
|
25
29
|
export default {
|
|
26
30
|
applyPatch,
|
package/src/ota/OtaProvider.tsx
CHANGED
|
@@ -17,8 +17,10 @@ import {
|
|
|
17
17
|
View,
|
|
18
18
|
} from 'react-native';
|
|
19
19
|
import NativeBsPatch from '../NativeBsPatch';
|
|
20
|
+
import { ineligibleReason } from './eligibility';
|
|
21
|
+
import { getInstallId } from './installId';
|
|
20
22
|
import { applyOtaPatchInternal, fetchOtaManifest, selectUpdate, validateOtaConfig } from './otaCore';
|
|
21
|
-
import { createOtaLogger, OtaEvent } from './otaLogger';
|
|
23
|
+
import { createOtaLogger, OtaEvent, type OtaLogEntry } from './otaLogger';
|
|
22
24
|
import { DOCUMENT_DIR, loadMetadata, markHealthy, OTA_DIR } from './otaMetadata';
|
|
23
25
|
import {
|
|
24
26
|
OtaConfig,
|
|
@@ -31,6 +33,23 @@ import {
|
|
|
31
33
|
|
|
32
34
|
const OtaContext = createContext<OtaState | undefined>(undefined);
|
|
33
35
|
|
|
36
|
+
const DEFAULT_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
/** Sync native getter, '' if it throws */
|
|
39
|
+
const nativeString = (get: () => string): string => {
|
|
40
|
+
try {
|
|
41
|
+
return get() ?? '';
|
|
42
|
+
} catch {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Bytecode version the running Hermes reads (undefined off Hermes) */
|
|
48
|
+
const hermesBytecodeVersion = (): number | undefined => {
|
|
49
|
+
const v = (globalThis as any).HermesInternal?.getRuntimeProperties?.()?.['Bytecode Version'];
|
|
50
|
+
return v ? Number(v) : undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
34
53
|
export function OtaProvider({
|
|
35
54
|
children,
|
|
36
55
|
config,
|
|
@@ -58,14 +77,18 @@ export function OtaProvider({
|
|
|
58
77
|
const [patches, setPatches] = useState<OtaPatch[]>([]);
|
|
59
78
|
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
|
60
79
|
const [isUpdateReady, setIsUpdateReady] = useState<boolean>(false);
|
|
80
|
+
const [isMandatory, setIsMandatory] = useState<boolean>(false);
|
|
81
|
+
const [installId, setInstallId] = useState<string>('');
|
|
61
82
|
|
|
62
83
|
const appState = useRef(AppState.currentState);
|
|
63
84
|
const isCheckingRef = useRef(false);
|
|
64
|
-
const pendingReloadRef = useRef(false);
|
|
65
85
|
const bootstrapDone = useRef(false);
|
|
66
86
|
/** Version the native resolver loaded for this JS session */
|
|
67
87
|
const runningVersionRef = useRef(config.baseAppVersion);
|
|
68
88
|
const progressRef = useRef(0);
|
|
89
|
+
const lastCheckRef = useRef(0);
|
|
90
|
+
const appVersionRef = useRef(config.appVersion ?? '');
|
|
91
|
+
const installIdRef = useRef('');
|
|
69
92
|
const isDebugUpdateEnabled = !__DEV__ || config.allowDebugUpdates === true;
|
|
70
93
|
|
|
71
94
|
// No fallbacks: a missing env value disables OTA instead of silently pointing elsewhere.
|
|
@@ -86,21 +109,46 @@ export function OtaProvider({
|
|
|
86
109
|
setProgress(nextProgress);
|
|
87
110
|
}, []);
|
|
88
111
|
|
|
112
|
+
/** config.onEvent, plus a fire-and-forget POST to config.telemetry.url */
|
|
113
|
+
const onEvent = useCallback((entry: OtaLogEntry) => {
|
|
114
|
+
config.onEvent?.(entry);
|
|
115
|
+
const telemetry = config.telemetry;
|
|
116
|
+
if (!telemetry) return;
|
|
117
|
+
fetch(telemetry.url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json', ...telemetry.headers },
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
...entry,
|
|
122
|
+
installId: installIdRef.current,
|
|
123
|
+
platform: Platform.OS,
|
|
124
|
+
appVersion: appVersionRef.current,
|
|
125
|
+
appBuild: nativeString(() => NativeBsPatch.getAppBuild()),
|
|
126
|
+
runtimeFingerprint: nativeString(() => NativeBsPatch.getRuntimeFingerprint()),
|
|
127
|
+
flavor: config.customServer?.channel,
|
|
128
|
+
otaVersion: runningVersionRef.current,
|
|
129
|
+
}),
|
|
130
|
+
}).catch(() => {});
|
|
131
|
+
}, [config]);
|
|
132
|
+
|
|
89
133
|
const getEffectiveConfig = useCallback(async (): Promise<OtaConfig> => {
|
|
90
|
-
|
|
134
|
+
// The keys `patch` embedded in the app (primary + backup), unless the app passes its own
|
|
135
|
+
const publicKey = config.publicKey || nativeString(() => NativeBsPatch.getEmbeddedPublicKeys()) || undefined;
|
|
136
|
+
const base = { ...config, publicKey, onEvent };
|
|
137
|
+
if (config.appVersion) return base;
|
|
91
138
|
try {
|
|
92
139
|
const nativeVer = await NativeBsPatch.getAppVersion();
|
|
93
140
|
if (!nativeVer) {
|
|
94
141
|
throw new Error('Native appVersion is empty');
|
|
95
142
|
}
|
|
96
|
-
|
|
143
|
+
appVersionRef.current = nativeVer;
|
|
144
|
+
return { ...base, appVersion: nativeVer };
|
|
97
145
|
} catch (e) {
|
|
98
146
|
throw new Error(
|
|
99
147
|
`[OtaProvider] Failed to auto-detect appVersion from native: ${(e as Error).message}. ` +
|
|
100
148
|
`Please set versionName (Android) / CFBundleShortVersionString (iOS), or provide config.appVersion explicitly.`
|
|
101
149
|
);
|
|
102
150
|
}
|
|
103
|
-
}, [config]);
|
|
151
|
+
}, [config, onEvent]);
|
|
104
152
|
|
|
105
153
|
/**
|
|
106
154
|
* Core apply update logic — separated so it can be mapped to button clicks
|
|
@@ -150,21 +198,20 @@ export function OtaProvider({
|
|
|
150
198
|
/**
|
|
151
199
|
* Exposed method for manual trigger
|
|
152
200
|
*/
|
|
153
|
-
const
|
|
201
|
+
const applyVersion = useCallback(async (targetVersion: number) => {
|
|
154
202
|
if (!isDebugUpdateEnabled) {
|
|
155
203
|
setPendingCount(0);
|
|
156
204
|
setStatus(OtaStatus.UP_TO_DATE);
|
|
157
205
|
setMessage('OTA updates are disabled while running a debug build.');
|
|
158
206
|
return;
|
|
159
207
|
}
|
|
160
|
-
await executeApplyUpdate(patches, currentVersion,
|
|
161
|
-
}, [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
latestVersion,
|
|
166
|
-
|
|
167
|
-
]);
|
|
208
|
+
await executeApplyUpdate(patches, currentVersion, targetVersion);
|
|
209
|
+
}, [currentVersion, executeApplyUpdate, isDebugUpdateEnabled, patches]);
|
|
210
|
+
|
|
211
|
+
const applyUpdate = useCallback(
|
|
212
|
+
() => applyVersion(latestVersion),
|
|
213
|
+
[applyVersion, latestVersion],
|
|
214
|
+
);
|
|
168
215
|
|
|
169
216
|
const downloadBackgroundUpdate = useCallback(
|
|
170
217
|
async (
|
|
@@ -183,10 +230,15 @@ export function OtaProvider({
|
|
|
183
230
|
(s: OtaStatus, msg: string, m?: OtaMetrics) => {
|
|
184
231
|
if (s === OtaStatus.SUCCESS) {
|
|
185
232
|
setIsUpdateReady(true);
|
|
186
|
-
pendingReloadRef.current = true;
|
|
187
233
|
setStatus(OtaStatus.UP_TO_DATE);
|
|
188
234
|
setMessage(`Update V${targetV} ready for next launch`);
|
|
189
235
|
if (m) setMetrics(m);
|
|
236
|
+
if (directPatch.mandatory) {
|
|
237
|
+
setIsMandatory(true);
|
|
238
|
+
// Only in the foreground: a reload in the background is rolled back (see the
|
|
239
|
+
// AppState effect); the effect reloads once the app comes back
|
|
240
|
+
if (AppState.currentState === 'active') NativeBsPatch.reloadBundle();
|
|
241
|
+
}
|
|
190
242
|
}
|
|
191
243
|
},
|
|
192
244
|
() => {}, // silent: background doesn't spam progress
|
|
@@ -204,8 +256,9 @@ export function OtaProvider({
|
|
|
204
256
|
/**
|
|
205
257
|
* Check for updates from the manifest.
|
|
206
258
|
* Uses a ref to guard against concurrent calls (avoids stale closure).
|
|
259
|
+
* Automatic calls are throttled by minCheckIntervalMs; { force: true } always checks.
|
|
207
260
|
*/
|
|
208
|
-
const checkUpdates = useCallback(async () => {
|
|
261
|
+
const checkUpdates = useCallback(async (options?: { force?: boolean }) => {
|
|
209
262
|
if (isCheckingRef.current || configError) return;
|
|
210
263
|
if (!isDebugUpdateEnabled) {
|
|
211
264
|
setPendingCount(0);
|
|
@@ -214,6 +267,9 @@ export function OtaProvider({
|
|
|
214
267
|
setMessage('OTA updates are disabled while running a debug build.');
|
|
215
268
|
return;
|
|
216
269
|
}
|
|
270
|
+
const minInterval = config.minCheckIntervalMs ?? DEFAULT_MIN_CHECK_INTERVAL_MS;
|
|
271
|
+
if (!options?.force && Date.now() - lastCheckRef.current < minInterval) return;
|
|
272
|
+
lastCheckRef.current = Date.now();
|
|
217
273
|
isCheckingRef.current = true;
|
|
218
274
|
setIsChecking(true);
|
|
219
275
|
setStatus(OtaStatus.CHECKING);
|
|
@@ -226,14 +282,22 @@ export function OtaProvider({
|
|
|
226
282
|
const metadata = await loadMetadata(config.baseAppVersion);
|
|
227
283
|
const localV = metadata.currentVersion;
|
|
228
284
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
285
|
+
// Storage providers return every platform: keep what this install may take (platform,
|
|
286
|
+
// native fingerprint, Hermes, rollout/QA lists, kill switch, versions that crashed here)
|
|
287
|
+
const context = {
|
|
288
|
+
platform: Platform.OS,
|
|
289
|
+
failedVersions: metadata.failedVersions ?? [],
|
|
290
|
+
runtimeFingerprint: nativeString(() => NativeBsPatch.getRuntimeFingerprint()),
|
|
291
|
+
hermesBytecodeVersion: hermesBytecodeVersion(),
|
|
292
|
+
installId: await getInstallId(),
|
|
293
|
+
};
|
|
294
|
+
const platformPatches = remotePatches.filter(p => {
|
|
295
|
+
const reason = ineligibleReason(p, context);
|
|
296
|
+
if (reason && __DEV__ && p.toVersion > localV) {
|
|
297
|
+
console.log(`[OtaProvider] Skipping V${p.toVersion} (${p.id}): ${reason}`);
|
|
298
|
+
}
|
|
299
|
+
return !reason;
|
|
300
|
+
});
|
|
237
301
|
|
|
238
302
|
const maxV =
|
|
239
303
|
platformPatches.length > 0
|
|
@@ -250,7 +314,12 @@ export function OtaProvider({
|
|
|
250
314
|
setStatus(OtaStatus.UPDATE_AVAILABLE);
|
|
251
315
|
|
|
252
316
|
// Trigger behaviors based on mode
|
|
253
|
-
|
|
317
|
+
const network = config.network?.wifiOnly
|
|
318
|
+
? await NativeBsPatch.getNetworkType().catch(() => 'unknown')
|
|
319
|
+
: 'wifi';
|
|
320
|
+
if (network !== 'wifi' && network !== 'ethernet') {
|
|
321
|
+
setMessage(`Update available: V${localV} → V${maxV} (waiting for Wi-Fi)`);
|
|
322
|
+
} else if (config.updateMode === OtaUpdateMode.BACKGROUND_DOWNLOAD) {
|
|
254
323
|
downloadBackgroundUpdate(platformPatches, localV, maxV);
|
|
255
324
|
} else if (
|
|
256
325
|
config.updateMode === OtaUpdateMode.ON_APP_START ||
|
|
@@ -286,6 +355,11 @@ export function OtaProvider({
|
|
|
286
355
|
*/
|
|
287
356
|
useEffect(() => {
|
|
288
357
|
const bootstrap = async () => {
|
|
358
|
+
getInstallId().then(id => {
|
|
359
|
+
installIdRef.current = id;
|
|
360
|
+
setInstallId(id);
|
|
361
|
+
}).catch(() => {});
|
|
362
|
+
|
|
289
363
|
// Clean up any pending trash directories from previous resets (non-blocking)
|
|
290
364
|
NativeBsPatch.readDir(DOCUMENT_DIR).then(dirs => {
|
|
291
365
|
for (const dir of dirs) {
|
|
@@ -323,34 +397,39 @@ export function OtaProvider({
|
|
|
323
397
|
}
|
|
324
398
|
};
|
|
325
399
|
|
|
326
|
-
bootstrap
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
400
|
+
// OTA must never take the app down: if bootstrap fails, render the app with OTA idle
|
|
401
|
+
bootstrap()
|
|
402
|
+
.catch(error => {
|
|
403
|
+
console.warn('[OtaProvider] Bootstrap failed, OTA disabled for this session:', error);
|
|
404
|
+
setMessage(`OTA disabled: ${(error as Error).message}`);
|
|
405
|
+
setStatus(OtaStatus.ERROR);
|
|
406
|
+
})
|
|
407
|
+
.finally(() => setIsInitialized(true));
|
|
331
408
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
332
409
|
}, []);
|
|
333
410
|
|
|
334
411
|
/**
|
|
335
|
-
* AppState listener for ON_APP_FOREGROUND mode and
|
|
412
|
+
* AppState listener for ON_APP_FOREGROUND mode, and for a mandatory update that got
|
|
413
|
+
* ready while in the background.
|
|
414
|
+
* A downloaded update is not reloaded on background: iOS suspends JS before the
|
|
415
|
+
* first render, so the native resolver counts the launch as failed and rolls it back.
|
|
416
|
+
* metadata already points at the new version, so the next cold start applies it
|
|
336
417
|
*/
|
|
418
|
+
const isMandatoryRef = useRef(false);
|
|
419
|
+
isMandatoryRef.current = isMandatory && isUpdateReady;
|
|
337
420
|
useEffect(() => {
|
|
338
421
|
const subscription = AppState.addEventListener(
|
|
339
422
|
'change',
|
|
340
423
|
(nextAppState: AppStateStatus) => {
|
|
341
|
-
// When going into background: if update is downloaded and ready, reload bundle silently
|
|
342
|
-
// Only 'background', not 'inactive' (iOS fires it for Control Center, calls,
|
|
343
|
-
// Face ID). Any previous state: iOS goes active → inactive → background.
|
|
344
|
-
if (nextAppState === 'background' && pendingReloadRef.current) {
|
|
345
|
-
pendingReloadRef.current = false;
|
|
346
|
-
NativeBsPatch.reloadBundle();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
424
|
if (
|
|
350
425
|
appState.current.match(/inactive|background/) &&
|
|
351
426
|
nextAppState === 'active' &&
|
|
352
427
|
bootstrapDone.current
|
|
353
428
|
) {
|
|
429
|
+
if (isMandatoryRef.current) {
|
|
430
|
+
NativeBsPatch.reloadBundle();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
354
433
|
const mode = config.updateMode ?? OtaUpdateMode.MANUAL;
|
|
355
434
|
if (mode === OtaUpdateMode.ON_APP_FOREGROUND) {
|
|
356
435
|
checkUpdates();
|
|
@@ -366,14 +445,14 @@ export function OtaProvider({
|
|
|
366
445
|
}, [config.updateMode, checkUpdates]);
|
|
367
446
|
|
|
368
447
|
const markAsHealthy = useCallback(async () => {
|
|
369
|
-
const log = createOtaLogger(
|
|
448
|
+
const log = createOtaLogger(onEvent);
|
|
370
449
|
log.emit(OtaEvent.MARK_HEALTHY);
|
|
371
450
|
const appVersion = await NativeBsPatch.getAppVersion().catch(() => config.appVersion);
|
|
372
451
|
await markHealthy(config.baseAppVersion, runningVersionRef.current, appVersion);
|
|
373
|
-
}, [config]);
|
|
452
|
+
}, [config, onEvent]);
|
|
374
453
|
|
|
375
454
|
const resetToFactory = useCallback(async () => {
|
|
376
|
-
const log = createOtaLogger(
|
|
455
|
+
const log = createOtaLogger(onEvent);
|
|
377
456
|
try {
|
|
378
457
|
setStatus(OtaStatus.APPLYING);
|
|
379
458
|
setMessage('Executing system wipe...');
|
|
@@ -389,7 +468,7 @@ export function OtaProvider({
|
|
|
389
468
|
setStatus(OtaStatus.ERROR);
|
|
390
469
|
setMessage(`Reset failed: ${(error as Error).message}`);
|
|
391
470
|
}
|
|
392
|
-
}, [
|
|
471
|
+
}, [onEvent]);
|
|
393
472
|
|
|
394
473
|
// Memoized: consumers re-render only when something they can see changes
|
|
395
474
|
const value = useMemo<OtaState>(() => ({
|
|
@@ -402,7 +481,10 @@ export function OtaProvider({
|
|
|
402
481
|
isChecking,
|
|
403
482
|
metrics,
|
|
404
483
|
isUpdateReady,
|
|
484
|
+
isMandatory,
|
|
485
|
+
installId,
|
|
405
486
|
applyUpdate,
|
|
487
|
+
applyVersion,
|
|
406
488
|
resetToFactory,
|
|
407
489
|
markAsHealthy,
|
|
408
490
|
checkUpdate: checkUpdates,
|
|
@@ -417,7 +499,10 @@ export function OtaProvider({
|
|
|
417
499
|
isChecking,
|
|
418
500
|
metrics,
|
|
419
501
|
isUpdateReady,
|
|
502
|
+
isMandatory,
|
|
503
|
+
installId,
|
|
420
504
|
applyUpdate,
|
|
505
|
+
applyVersion,
|
|
421
506
|
resetToFactory,
|
|
422
507
|
markAsHealthy,
|
|
423
508
|
checkUpdates,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { OtaPatch } from './types';
|
|
2
|
+
|
|
3
|
+
/** Entry `requires` values this client understands */
|
|
4
|
+
export const SUPPORTED_FEATURES = ['gzip', 'bz2'];
|
|
5
|
+
|
|
6
|
+
export type EligibilityContext = {
|
|
7
|
+
/** Platform.OS */
|
|
8
|
+
platform: string;
|
|
9
|
+
/** Versions that crashed on boot here and were rolled back */
|
|
10
|
+
failedVersions: number[];
|
|
11
|
+
/** Fingerprint stamped into this binary ('' when the build hook didn't run) */
|
|
12
|
+
runtimeFingerprint: string;
|
|
13
|
+
/** Bytecode version of the running Hermes (undefined off Hermes) */
|
|
14
|
+
hermesBytecodeVersion?: number;
|
|
15
|
+
installId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** FNV-1a: stable 0-99 bucket for an install and version (a different slice of installs per version) */
|
|
19
|
+
export const rolloutBucket = (installId: string, version: number): number => {
|
|
20
|
+
let h = 0x811c9dc5;
|
|
21
|
+
for (const c of `${installId}:${version}`) {
|
|
22
|
+
h ^= c.charCodeAt(0);
|
|
23
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
24
|
+
}
|
|
25
|
+
return h % 100;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Why this client must not install the entry, or null when it may */
|
|
29
|
+
export const ineligibleReason = (e: OtaPatch, ctx: EligibilityContext): string | null => {
|
|
30
|
+
if (e.platform && e.platform.toLowerCase() !== ctx.platform) return 'other platform';
|
|
31
|
+
if (ctx.failedVersions.includes(e.toVersion)) return 'crashed on boot here before';
|
|
32
|
+
if (e.disabled) return 'disabled';
|
|
33
|
+
const missing = (e.requires ?? []).filter(f => !SUPPORTED_FEATURES.includes(f));
|
|
34
|
+
if (missing.length) return `needs a newer client (${missing.join(', ')})`;
|
|
35
|
+
if (e.runtimeFingerprint && e.runtimeFingerprint !== ctx.runtimeFingerprint) {
|
|
36
|
+
return ctx.runtimeFingerprint
|
|
37
|
+
? 'built for different native code (needs a store build)'
|
|
38
|
+
: 'this binary has no runtime fingerprint (run `react-native-bspatch setup` and rebuild)';
|
|
39
|
+
}
|
|
40
|
+
if (e.hermesBytecodeVersion && ctx.hermesBytecodeVersion && e.hermesBytecodeVersion !== ctx.hermesBytecodeVersion) {
|
|
41
|
+
return `Hermes bytecode ${e.hermesBytecodeVersion}, runtime reads ${ctx.hermesBytecodeVersion}`;
|
|
42
|
+
}
|
|
43
|
+
if (e.allowInstallIds?.length && !e.allowInstallIds.includes(ctx.installId)) return 'not in the QA list';
|
|
44
|
+
if (e.rolloutPercent !== undefined && rolloutBucket(ctx.installId, e.toVersion) >= e.rolloutPercent) {
|
|
45
|
+
return `outside the ${e.rolloutPercent}% rollout`;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { OtaPatch } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What `entrySignature` signs: the fields that decide which bundle a client installs.
|
|
5
|
+
* Must stay byte-for-byte equal to entryPayload in tools/update_registry.js.
|
|
6
|
+
*/
|
|
7
|
+
export const entryPayload = (p: OtaPatch): string =>
|
|
8
|
+
[
|
|
9
|
+
'bspatch-entry-v2',
|
|
10
|
+
p.appVersion,
|
|
11
|
+
p.flavor ?? '',
|
|
12
|
+
p.platform ?? '',
|
|
13
|
+
p.fromVersion,
|
|
14
|
+
p.toVersion,
|
|
15
|
+
p.bundleHash ?? '',
|
|
16
|
+
p.assetManifestHash ?? '',
|
|
17
|
+
p.minAppVersion ?? '',
|
|
18
|
+
p.runtimeFingerprint ?? '',
|
|
19
|
+
p.hermesBytecodeVersion ?? '',
|
|
20
|
+
].join('\n');
|
|
21
|
+
|
|
22
|
+
/** Public keys from config or the embedded list: PEM blocks, or base64 SPKI separated by commas/whitespace */
|
|
23
|
+
export const parsePublicKeys = (keys: string | undefined): string[] => {
|
|
24
|
+
if (!keys) return [];
|
|
25
|
+
if (keys.includes('-----BEGIN')) {
|
|
26
|
+
return (keys.match(/-----BEGIN PUBLIC KEY-----[\s\S]*?-----END PUBLIC KEY-----/g) ?? []).map(block =>
|
|
27
|
+
block.replace(/-----[A-Z ]+-----|\s+/g, ''),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return keys.split(/[\s,]+/).filter(Boolean);
|
|
31
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import NativeBsPatch from '../NativeBsPatch';
|
|
2
|
+
import { DOCUMENT_DIR } from './otaMetadata';
|
|
3
|
+
|
|
4
|
+
// Outside ota/: resetToFactory and native rollbacks keep it
|
|
5
|
+
const INSTALL_ID_PATH = `${DOCUMENT_DIR}/bspatch-install-id`;
|
|
6
|
+
let installId: Promise<string> | undefined;
|
|
7
|
+
|
|
8
|
+
/** Random ID made on first use and kept for the life of the install (rollout bucket, QA lists, telemetry) */
|
|
9
|
+
export const getInstallId = (): Promise<string> =>
|
|
10
|
+
(installId ??= (async () => {
|
|
11
|
+
try {
|
|
12
|
+
const saved = (await NativeBsPatch.readFile(INSTALL_ID_PATH)).trim();
|
|
13
|
+
if (/^[0-9a-f]{32}$/.test(saved)) return saved;
|
|
14
|
+
} catch {}
|
|
15
|
+
const id = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
16
|
+
await NativeBsPatch.writeFile(INSTALL_ID_PATH, id).catch(() => {});
|
|
17
|
+
return id;
|
|
18
|
+
})());
|