react-native-update 10.53.1 → 10.54.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 (105) hide show
  1. package/README-CN.md +31 -31
  2. package/README.md +31 -31
  3. package/android/jni/HDiffPatch/.travis.yml +33 -0
  4. package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  5. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  6. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  7. package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  8. package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  9. package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  10. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  11. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  12. package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  13. package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  14. package/android/jni/lzma/DOC/7zC.txt +187 -0
  15. package/android/jni/lzma/DOC/7zFormat.txt +469 -0
  16. package/android/jni/lzma/DOC/Methods.txt +173 -0
  17. package/android/jni/lzma/DOC/installer.txt +166 -0
  18. package/android/jni/lzma/DOC/lzma-history.txt +446 -0
  19. package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
  20. package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
  21. package/android/jni/lzma/DOC/lzma.txt +328 -0
  22. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  23. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  24. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  25. package/android/jni/lzma/bin/7zdec.exe +0 -0
  26. package/android/jni/lzma/bin/7zr.exe +0 -0
  27. package/android/jni/lzma/bin/installer/config.txt +5 -0
  28. package/android/jni/lzma/bin/installer/cr.bat +5 -0
  29. package/android/jni/lzma/bin/lzma.exe +0 -0
  30. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  31. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  32. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  33. package/android/lib/x86/librnupdate.so +0 -0
  34. package/android/lib/x86_64/librnupdate.so +0 -0
  35. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +44 -0
  36. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
  37. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
  38. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
  39. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
  40. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
  41. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
  42. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
  43. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
  44. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -0
  45. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  46. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  47. package/cpp/patch_core/archive_limits.h +38 -0
  48. package/cpp/patch_core/install_record.h +36 -0
  49. package/cpp/update_flow_core/flow_json.h +1 -0
  50. package/cpp/update_flow_core/update_flow_core.cpp +41 -7
  51. package/cpp/update_flow_core/update_flow_core.h +9 -0
  52. package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
  53. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
  54. package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
  55. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
  56. package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
  57. package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
  58. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
  59. package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
  60. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  61. package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
  62. package/harmony/pushy.har +0 -0
  63. package/ios/RCTPushy/RCTPushy.mm +384 -25
  64. package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
  65. package/package.json +25 -2
  66. package/src/NativePushy.ts +13 -0
  67. package/src/client.ts +160 -43
  68. package/src/core.ts +17 -3
  69. package/src/error.ts +3 -0
  70. package/src/index.ts +8 -0
  71. package/src/locales/en.ts +3 -0
  72. package/src/locales/zh.ts +2 -0
  73. package/src/metadata.ts +166 -0
  74. package/src/provider.tsx +52 -17
  75. package/src/type.ts +22 -0
  76. package/src/updateFlowCore.ts +95 -4
  77. package/src/utils.ts +31 -19
  78. package/.gitmodules +0 -6
  79. package/BUNDLEHASH_MIGRATION.md +0 -355
  80. package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
  81. package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
  82. package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
  83. package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
  84. package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
  85. package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
  86. package/biome.json +0 -59
  87. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  88. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  89. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  90. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  91. package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
  92. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  93. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  94. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  95. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  96. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  97. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  98. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  99. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  100. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  101. package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
  102. package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
  103. package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
  104. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
  105. package/scripts/check-expo-version.js +0 -92
package/src/provider.tsx CHANGED
@@ -343,13 +343,36 @@ export const UpdateProvider = ({
343
343
  if (!assertWeb()) {
344
344
  return;
345
345
  }
346
- const { checkStrategy, autoMarkSuccess } = options;
346
+ const { checkStrategy, autoMarkSuccess, autoMarkSuccessDelayMs } = options;
347
347
  let markSuccessTimer: ReturnType<typeof setTimeout> | undefined;
348
348
  if (autoMarkSuccess) {
349
+ const delay =
350
+ typeof autoMarkSuccessDelayMs === 'number' &&
351
+ autoMarkSuccessDelayMs >= 0
352
+ ? autoMarkSuccessDelayMs
353
+ : 1000;
349
354
  markSuccessTimer = setTimeout(() => {
350
- // Failures are reported and surfaced via the onError subscription.
351
- Promise.resolve(markSuccess()).catch(noop);
352
- }, 1000);
355
+ // The health check is read when the timer fires, not when it was
356
+ // armed, so a late setOptions still applies.
357
+ const { healthCheck } = options;
358
+ (async () => {
359
+ if (healthCheck) {
360
+ let healthy = false;
361
+ try {
362
+ healthy = (await healthCheck()) !== false;
363
+ } catch (e: any) {
364
+ log('healthCheck threw, not marking success:', e?.message || e);
365
+ return;
366
+ }
367
+ if (!healthy) {
368
+ log('healthCheck returned false, not marking success');
369
+ return;
370
+ }
371
+ }
372
+ // Failures are reported and surfaced via the onError subscription.
373
+ await markSuccess();
374
+ })().catch(noop);
375
+ }, delay);
353
376
  }
354
377
  if (checkStrategy === 'both' || checkStrategy === 'onAppResume') {
355
378
  stateListener.current = AppState.addEventListener(
@@ -396,30 +419,42 @@ export const UpdateProvider = ({
396
419
  const parseTestPayload = useCallback(
397
420
  (payload: UpdateTestPayload) => {
398
421
  if (payload?.type?.startsWith('__rnPushy')) {
422
+ if (options.testChannel === false) {
423
+ // Production builds may refuse scanned codes that would pull an
424
+ // arbitrary published version.
425
+ log('test channel disabled, ignoring payload', payload.type);
426
+ return false;
427
+ }
399
428
  if (payload.type === '__rnPushyVersionHash') {
400
- // Wrap the logger only for the duration of this test check; wrapping
401
- // on any other __rnPushy* payload would never be undone (the finally
402
- // below is the only restore point).
403
- const logger = options.logger || (() => {});
404
- options.logger = ({ type, data }) => {
405
- logger({ type, data });
406
- Alert.alert(type, JSON.stringify(data));
407
- };
408
429
  const toHash = payload.data;
430
+ if (typeof toHash !== 'string' || !toHash) {
431
+ log('test payload without a target hash ignored');
432
+ return false;
433
+ }
409
434
  sharedState.toHash = toHash;
435
+ // No global logger swap for the duration of the check (it leaked
436
+ // into concurrent checks): the tester gets one alert with the
437
+ // outcome instead of one per logged event.
410
438
  checkUpdate({ extra: { toHash } })
411
439
  .then(() => {
412
- if (updateInfoRef.current?.upToDate) {
440
+ const info = updateInfoRef.current;
441
+ if (info?.upToDate) {
413
442
  Alert.alert(
414
443
  client.t('alert_info'),
415
444
  client.t('alert_no_update_wait')
416
445
  );
446
+ } else if (info?.update) {
447
+ Alert.alert(
448
+ client.t('alert_info'),
449
+ JSON.stringify({
450
+ hash: info.hash,
451
+ name: info.name,
452
+ description: info.description,
453
+ })
454
+ );
417
455
  }
418
456
  })
419
- .catch(noop)
420
- .finally(() => {
421
- options.logger = logger;
422
- });
457
+ .catch(noop);
423
458
  }
424
459
  return true;
425
460
  }
package/src/type.ts CHANGED
@@ -162,6 +162,28 @@ export interface ClientOptions {
162
162
  | null;
163
163
  checkStrategy?: 'onAppStart' | 'onAppResume' | 'both' | null;
164
164
  autoMarkSuccess?: boolean;
165
+ /**
166
+ * Delay before the Provider marks the running update as successful when
167
+ * `autoMarkSuccess` is on. Default 1000ms. Apps whose critical modules load
168
+ * after the first frame (navigation, remote config) should raise it — a
169
+ * version that crashes 3s after mount would otherwise already be marked
170
+ * good and never rolled back. For full control use `autoMarkSuccess: false`
171
+ * and call `markSuccess()` once the app is really ready.
172
+ */
173
+ autoMarkSuccessDelayMs?: number;
174
+ /**
175
+ * Consulted when the auto-mark timer fires: return false (or throw) to
176
+ * skip marking this launch — the next launch's crash protection then still
177
+ * applies. Call `markSuccess()` yourself later once the app is healthy.
178
+ */
179
+ healthCheck?: () => boolean | Promise<boolean>;
180
+ /**
181
+ * Whether the Provider honours test-channel payloads (`parseTestQrCode`,
182
+ * `__rnPushyVersionHash` deep links / QR codes) that target a specific
183
+ * version hash. Default true. Set to false in production builds that must
184
+ * not let a scanned code pull an arbitrary published version.
185
+ */
186
+ testChannel?: boolean;
165
187
  dismissErrorAfter?: number;
166
188
  debug?: boolean;
167
189
  throwError?: boolean;
@@ -172,22 +172,113 @@ export function buildCheckRequestBody({
172
172
  isDev,
173
173
  extra,
174
174
  }: CheckRequestInput): Record<string, any> {
175
+ // Caller extras are spread FIRST so they can add fields (channel, custom
176
+ // filters, the debug-channel toHash) but never override the SDK identity
177
+ // fields the server keys its decision on: a stray `packageVersion` or
178
+ // `hash` in extra would otherwise make the server hand out a package for
179
+ // the wrong binary.
175
180
  const body: Record<string, any> = {
181
+ ...extra,
176
182
  packageVersion,
177
183
  hash: currentVersion,
178
184
  buildTime,
179
185
  cInfo,
180
- // 可消费的 diff 轨道版本(2 = hdiffv2 轨道),服务端据此门控下发
181
- ...(supportedDiffVersion ? { diffV: supportedDiffVersion } : {}),
182
- ...(bundleHash ? { bundleHash } : {}),
183
- ...extra,
184
186
  };
187
+ // 可消费的 diff 轨道版本(2 = hdiffv2 轨道),服务端据此门控下发
188
+ if (supportedDiffVersion) {
189
+ body.diffV = supportedDiffVersion;
190
+ } else {
191
+ delete body.diffV;
192
+ }
193
+ if (bundleHash) {
194
+ body.bundleHash = bundleHash;
195
+ } else {
196
+ delete body.bundleHash;
197
+ }
185
198
  if (isDev) {
186
199
  delete body.buildTime;
187
200
  }
201
+ // Transitional dual form: the legacy root-level spread above is what
202
+ // servers before 2026-08-30 read; the nested copy is what new servers read
203
+ // first (extra.toHash wins there) and lets caller keys stop sharing the
204
+ // SDK's namespace once the root spread is retired.
205
+ if (extra) {
206
+ // Match JSON.stringify semantics: undefined values do not exist on the
207
+ // wire, so they must not create a nested copy either (the native check
208
+ // cache compares serialized requests).
209
+ const defined: Record<string, any> = {};
210
+ for (const [key, value] of Object.entries(extra)) {
211
+ if (value !== undefined) {
212
+ defined[key] = value;
213
+ }
214
+ }
215
+ if (Object.keys(defined).length > 0) {
216
+ body.extra = defined;
217
+ }
218
+ }
188
219
  return body;
189
220
  }
190
221
 
222
+ export interface CheckFingerprintInput {
223
+ appKey?: string;
224
+ endpoints?: string[];
225
+ queryUrls?: string[];
226
+ uuid?: string;
227
+ /** The exact serialized request body (identity fields + extra). */
228
+ body: string;
229
+ }
230
+
231
+ /**
232
+ * Identity of one update check. Two checks may share an in-flight response
233
+ * only when every input that could change the server's answer is identical.
234
+ */
235
+ export function buildCheckFingerprint({
236
+ appKey,
237
+ endpoints,
238
+ queryUrls,
239
+ uuid,
240
+ body,
241
+ }: CheckFingerprintInput): string {
242
+ return JSON.stringify([
243
+ appKey ?? '',
244
+ endpoints ?? [],
245
+ queryUrls ?? [],
246
+ uuid ?? '',
247
+ body,
248
+ ]);
249
+ }
250
+
251
+ /**
252
+ * Whether a failed download attempt is worth retrying on another mirror of
253
+ * the same artifact. Transport failures (DNS, connect, timeout, truncated
254
+ * body, 5xx) and corrupt bytes are; a patch that downloaded fine but could
255
+ * not be applied is not — every mirror serves the same bytes, so the next
256
+ * strategy is the only way forward.
257
+ */
258
+ export function isMirrorRetryableCode(code?: string): boolean {
259
+ return code !== 'PATCH_FAILED';
260
+ }
261
+
262
+ /**
263
+ * Schema gate for a checkUpdate response (mirrors C++ IsValidCheckResult): a
264
+ * JSON object carrying at least one verdict field of the expected type —
265
+ * `upToDate` / `update` / `expired` (boolean) or `paused` (string). A 200
266
+ * with `{"error": ...}`, an array or an HTML page that happened to parse is
267
+ * a failed endpoint, not a verdict, and must not stop the endpoint fallback.
268
+ */
269
+ export function isValidCheckResult(value: unknown): value is CheckResult {
270
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
271
+ return false;
272
+ }
273
+ const root = value as Record<string, unknown>;
274
+ return (
275
+ typeof root.upToDate === 'boolean' ||
276
+ typeof root.update === 'boolean' ||
277
+ typeof root.expired === 'boolean' ||
278
+ typeof root.paused === 'string'
279
+ );
280
+ }
281
+
191
282
  export interface UpdateIdentity {
192
283
  packageVersion: string;
193
284
  currentVersion?: string;
package/src/utils.ts CHANGED
@@ -66,6 +66,13 @@ const ping = isWeb
66
66
  DEFAULT_FETCH_TIMEOUT_MS
67
67
  );
68
68
  if (status === 200) {
69
+ if (isProtocolDowngrade(url, finalUrl)) {
70
+ // The probe was redirected from https to plaintext http. Never
71
+ // let a downgraded final URL win the race: the winner is what
72
+ // the real artifact download uses.
73
+ log('ping rejected: https redirected to http', url, finalUrl);
74
+ throw Error(i18n.t('error_ping_failed'));
75
+ }
69
76
  return finalUrl;
70
77
  }
71
78
  log('ping failed', url, status, statusText);
@@ -166,29 +173,34 @@ export const fetchWithTimeout = (
166
173
  });
167
174
  };
168
175
 
169
- const isIdempotentRequest = (params: Parameters<typeof fetch>[1]) => {
170
- const method = params?.method?.toUpperCase() ?? 'GET';
171
- return method === 'GET' || method === 'HEAD';
172
- };
176
+ /**
177
+ * True when `finalUrl` (after redirects) is plaintext http while the request
178
+ * started on https. An https endpoint or artifact URL must never be served
179
+ * over http, redirected or otherwise: the update package is the supply chain
180
+ * boundary, and TLS is the only thing authenticating it.
181
+ */
182
+ export const isProtocolDowngrade = (
183
+ requestedUrl: string,
184
+ finalUrl?: string | null
185
+ ): boolean =>
186
+ !!finalUrl && /^https:/i.test(requestedUrl) && /^http:/i.test(finalUrl);
173
187
 
174
188
  export const enhancedFetch = async (
175
189
  url: string,
176
- params: Parameters<typeof fetch>[1],
177
- isRetry = false
190
+ params: Parameters<typeof fetch>[1]
178
191
  ): Promise<Response> => {
179
- return fetch(url, params).catch((e) => {
192
+ // No https -> http retry here, ever. Earlier versions replayed failed
193
+ // idempotent https requests over plaintext http as a "network
194
+ // compatibility" fallback; that silently moved endpoint discovery and
195
+ // package downloads outside TLS. Self-hosted plaintext deployments must
196
+ // configure explicit http:// URLs instead.
197
+ const response = await fetch(url, params).catch((e) => {
180
198
  log('fetch error', url, e);
181
- if (
182
- isRetry ||
183
- (params as any)?.signal?.aborted ||
184
- !url.startsWith('https:') ||
185
- // Never replay non-idempotent requests (e.g. the checkUpdate POST)
186
- // over plaintext http: the server may have processed the original.
187
- !isIdempotentRequest(params)
188
- ) {
189
- throw e;
190
- }
191
- log('trying fallback to http');
192
- return enhancedFetch(url.replace(/^https:/, 'http:'), params, true);
199
+ throw e;
193
200
  });
201
+ if (isProtocolDowngrade(url, response?.url)) {
202
+ log('fetch rejected: https redirected to http', url, response.url);
203
+ throw Error(i18n.t('error_insecure_redirect'));
204
+ }
205
+ return response;
194
206
  };
package/.gitmodules DELETED
@@ -1,6 +0,0 @@
1
- [submodule "android/jni/lzma"]
2
- path = android/jni/lzma
3
- url = https://github.com/sisong/lzma.git
4
- [submodule "android/jni/HDiffPatch"]
5
- path = android/jni/HDiffPatch
6
- url = https://github.com/sisong/HDiffPatch.git