@scalebun/react-native 1.13.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/android/build.gradle +8 -0
  2. package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
  4. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
  5. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +115 -60
  6. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
  7. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +18 -0
  8. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +4 -8
  9. package/dist/scalebun.full.js +127 -422
  10. package/dist/scalebun.slim.js +127 -422
  11. package/ios/Ota/OtaProtocol.swift +242 -0
  12. package/ios/Ota/OtaSlotManager.swift +26 -0
  13. package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
  14. package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
  15. package/ios/Ota/ScaleBunOtaModule.swift +125 -53
  16. package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
  17. package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
  18. package/lib/commonjs/core/config/schema.js +3 -12
  19. package/lib/commonjs/core/constants/version.js +1 -1
  20. package/lib/commonjs/features/network/NetworkFeature.js +39 -5
  21. package/lib/commonjs/features/network/thirdParty.js +90 -0
  22. package/lib/commonjs/features/ota/OtaOrchestrator.js +108 -60
  23. package/lib/commonjs/public/ScaleBunFacade.js +5 -41
  24. package/lib/module/core/config/schema.js +3 -12
  25. package/lib/module/core/constants/version.js +1 -1
  26. package/lib/module/features/network/NetworkFeature.js +38 -4
  27. package/lib/module/features/network/thirdParty.js +83 -0
  28. package/lib/module/features/ota/OtaOrchestrator.js +108 -60
  29. package/lib/module/public/ScaleBunFacade.js +5 -41
  30. package/lib/typescript/core/config/schema.d.ts +0 -3
  31. package/lib/typescript/core/constants/version.d.ts +1 -1
  32. package/lib/typescript/features/network/index.d.ts +18 -0
  33. package/lib/typescript/features/network/thirdParty.d.ts +65 -0
  34. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +5 -8
  35. package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
  36. package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
  37. package/lib/typescript/specs/NativeScaleBunOta.d.ts +30 -20
  38. package/package.json +3 -13
  39. package/scalebun-react-native.podspec +4 -0
  40. package/src/core/config/schema.ts +3 -9
  41. package/src/core/constants/version.ts +1 -1
  42. package/src/features/network/NetworkFeature.ts +41 -4
  43. package/src/features/network/index.ts +18 -0
  44. package/src/features/network/thirdParty.ts +92 -0
  45. package/src/features/ota/OtaOrchestrator.ts +116 -70
  46. package/src/features/ota/OtaTypes.ts +23 -1
  47. package/src/public/ScaleBunFacade.ts +5 -53
  48. package/src/specs/NativeScaleBunOta.ts +32 -20
  49. package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
  50. package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
  51. package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
  52. package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
  53. package/lib/commonjs/features/ota/signature.js +0 -175
  54. package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
  55. package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
  56. package/lib/module/features/ota/crypto/loadSha512.js +0 -34
  57. package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
  58. package/lib/module/features/ota/signature.js +0 -169
  59. package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
  60. package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
  61. package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
  62. package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
  63. package/lib/typescript/features/ota/signature.d.ts +0 -81
  64. package/src/features/ota/crypto/builtinVerifier.ts +0 -257
  65. package/src/features/ota/crypto/loadEd25519.ts +0 -41
  66. package/src/features/ota/crypto/loadSha512.ts +0 -35
  67. package/src/features/ota/crypto/nativeVerifier.ts +0 -117
  68. package/src/features/ota/signature.ts +0 -206
@@ -6,7 +6,6 @@ import { otaEventEmitter } from './OtaEventEmitter';
6
6
  import { getDeviceCountry, prefetchDeviceCountry } from './geoCountry';
7
7
  import { deviceTargetingAttributes } from './deviceAttributes';
8
8
  import { resolveInstallationId } from '../../core/id/installationId';
9
- import { verifyBundleSignature } from './signature';
10
9
  import { detectOtaEnvironment } from './environment';
11
10
  import { retryWithBackoff, isRetryableNetworkError, HttpStatusError } from './retry';
12
11
  import { createStorageBackend } from '../../storage/StorageBackend';
@@ -149,9 +148,10 @@ export class OtaOrchestrator {
149
148
  /**
150
149
  * Initialize the orchestrator. Checks for boot-guard recovery state.
151
150
  *
152
- * `signature` is optional: an app that has not adopted signing keeps working
153
- * on SHA-256 integrity alone. Once a public key IS configured, a bundle that
154
- * cannot be verified is refused rather than installed (OTA-03).
151
+ * Signature enforcement is no longer a JS concern: signed releases are
152
+ * verified and staged in NATIVE code against RSA keys embedded in the app
153
+ * package (SB-OTA-RSA-SHA256-V1), and unsigned releases keep working on
154
+ * SHA-256 integrity alone. There is nothing for the host to configure here.
155
155
  */
156
156
  init(config) {
157
157
  noThrow(() => {
@@ -164,7 +164,6 @@ export class OtaOrchestrator {
164
164
  // them. Config is still refreshed so late signature setup works.
165
165
  if (this.initialized) {
166
166
  this.bootGuardConfig = config ?? this.bootGuardConfig;
167
- this.signatureConfig = config?.signature ?? this.signatureConfig;
168
167
  return;
169
168
  }
170
169
  if (!NativeScaleBunOta) {
@@ -199,7 +198,6 @@ export class OtaOrchestrator {
199
198
  this.initialized = true;
200
199
  this.enabled = true;
201
200
  this.bootGuardConfig = config ?? {};
202
- this.signatureConfig = config?.signature;
203
201
  __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? 'unknown'}` + `${this.environment.bridgeless ? ', bridgeless' : ''}` + `${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? '?'}` : ''})`);
204
202
 
205
203
  // ONE read of the slot state, shared by both boot-time consumers.
@@ -479,12 +477,27 @@ export class OtaOrchestrator {
479
477
  // OTA has no bundle for a non-mobile target; claiming Android there would install the wrong one.
480
478
  const mobileOS = resolveMobileOS();
481
479
  if (!mobileOS) throw new Error('OTA is not supported on this platform');
480
+ // SDK trust coverage (CICDPAGE §12): report which trusted key IDs are
481
+ // embedded in this binary so the dashboard can measure key-rotation
482
+ // adoption. Telemetry only — the server never trusts a key a device claims.
483
+ // Defensive: optional native method, never let it break a check.
484
+ let trustedKeyIds;
485
+ try {
486
+ const raw = NativeScaleBunOta?.getTrustedKeyIds?.();
487
+ if (raw) {
488
+ const arr = JSON.parse(raw);
489
+ if (Array.isArray(arr) && arr.length) trustedKeyIds = arr.slice(0, 16);
490
+ }
491
+ } catch {
492
+ /* trust reporting is best-effort — never break the update check */
493
+ }
482
494
  const payload = {
483
495
  appVersion: requestParams.appVersion,
484
496
  platform: mobileOS,
485
497
  installationId: requestParams.installationId,
486
498
  currentBundleId: this.currentBundle?.id,
487
499
  currentBundleHash: this.currentBundle?.sha256,
500
+ trustedKeyIds,
488
501
  channelName: requestParams.channelName,
489
502
  // Host-supplied country wins; otherwise the cached edge-Worker lookup
490
503
  // fills in (see geoCountry.ts). Undefined when neither knows — the
@@ -618,25 +631,12 @@ export class OtaOrchestrator {
618
631
  version: bundle.version
619
632
  });
620
633
 
621
- // ── VERIFY AUTHENTICITY (OTA-03) ─────────────────────────────────────
622
- // Before anything touches the disk. SHA-256 proves the bytes arrived
623
- // intact; only the signature proves they came from you. Checking after
624
- // staging would mean writing unverified code to the slot directory first.
625
- const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
626
- if (!signatureOutcome.ok) {
627
- otaEventEmitter.emitSimple('APPLY_FAILED', bundle.id, {
628
- releaseId: bundle.releaseId,
629
- error: `Signature check failed: ${signatureOutcome.reason}`,
630
- version: bundle.version
631
- });
632
- return {
633
- status: 'ERROR',
634
- error: `Bundle signature check failed (${signatureOutcome.reason})`,
635
- bundle
636
- };
637
- }
638
-
639
634
  // ── DOWNLOAD ─────────────────────────────────────────────────────────
635
+ // Authenticity is enforced NATIVELY now (see the envelope branch below):
636
+ // SHA-256 proves the bytes arrived intact, and for signed releases the
637
+ // RSA signature — checked in native code against keys embedded in the app
638
+ // package — proves they came from you. JS neither pins keys nor verifies
639
+ // signatures.
640
640
  otaEventEmitter.emitSimple('DOWNLOAD_STARTED', bundle.id, {
641
641
  releaseId: bundle.releaseId,
642
642
  version: bundle.version
@@ -690,46 +690,94 @@ export class OtaOrchestrator {
690
690
  }
691
691
  });
692
692
  let staged = false;
693
-
694
- // Sprint 7: Try patch first if offered, fall back to full bundle
695
- if (bundle.patch && NativeScaleBunOta.stagePatch) {
696
- __DEV__ && logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ` + `${(bundle.size / 1024).toFixed(0)} KB full) — trying patch…`);
693
+ if (bundle.envelope) {
694
+ // ── NATIVE-ENFORCED VERIFY + STAGE (SB-OTA-RSA-SHA256-V1) ───────────
695
+ // Hand the whole envelope to native code, which verifies the RSA
696
+ // signature against the keys EMBEDDED in the app package, then downloads
697
+ // the artifact, enforces the SIGNED size + SHA-256, and stages it into
698
+ // the `next` slot — all outside JS. JavaScript never stages bytes it has
699
+ // not proven authentic. No patch path here: a signed offer is always a
700
+ // full verified download (native patch is a documented follow-up).
701
+ const envelopeJson = JSON.stringify({
702
+ ...bundle.envelope,
703
+ downloadUrl: bundle.url
704
+ });
705
+ let verdict;
697
706
  try {
698
- staged = await NativeScaleBunOta.stagePatch(bundle.patch.url, bundle.patch.sha256, bundle.sha256 // expected full-bundle hash after patching
699
- );
700
- if (staged) {
701
- patchUsed = true;
702
- __DEV__ && logger.debug('[OTA] Patch applied successfully');
703
- } else {
704
- logger.warn('[OTA] Patch staging returned false — falling back to full bundle');
707
+ const verdictJson = await NativeScaleBunOta.verifyAndStageRelease(envelopeJson);
708
+ verdict = JSON.parse(verdictJson);
709
+ } catch (err) {
710
+ // A rejected promise (or an unparseable verdict) is machinery failure,
711
+ // never a valid result — treat it as a hard failure.
712
+ verdict = {
713
+ ok: false,
714
+ code: 'verify_stage_threw',
715
+ message: err?.message ?? String(err)
716
+ };
717
+ }
718
+ if (!verdict.ok) {
719
+ unsubscribeProgress();
720
+ postProgress(0, 'FAILED');
721
+ logger.error(`[OTA] Native verify/stage refused the release: ${verdict.code ?? 'unknown'}` + `${verdict.message ? ` — ${verdict.message}` : ''}`);
722
+ otaEventEmitter.emitSimple('APPLY_FAILED', bundle.id, {
723
+ releaseId: bundle.releaseId,
724
+ // `error` is mapped onto the wire `errorCode` by deliverOtaEvents —
725
+ // carry the stable native verdict code there.
726
+ error: verdict.code ?? 'verify_and_stage_release_failed',
727
+ version: bundle.version
728
+ });
729
+ return {
730
+ status: 'ERROR',
731
+ error: `Release verification failed (${verdict.code ?? 'unknown'})`,
732
+ bundle
733
+ };
734
+ }
735
+ staged = true;
736
+ } else {
737
+ // ── LEGACY / UNSIGNED (SHA-256 integrity only) ─────────────────────
738
+ // No envelope means the app embeds no trusted keys and the release is
739
+ // unsigned: keep the pre-RSA behaviour so those apps still update.
740
+
741
+ // Sprint 7: Try patch first if offered, fall back to full bundle
742
+ if (bundle.patch && NativeScaleBunOta.stagePatch) {
743
+ __DEV__ && logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ` + `${(bundle.size / 1024).toFixed(0)} KB full) — trying patch…`);
744
+ try {
745
+ staged = await NativeScaleBunOta.stagePatch(bundle.patch.url, bundle.patch.sha256, bundle.sha256 // expected full-bundle hash after patching
746
+ );
747
+ if (staged) {
748
+ patchUsed = true;
749
+ __DEV__ && logger.debug('[OTA] Patch applied successfully');
750
+ } else {
751
+ logger.warn('[OTA] Patch staging returned false — falling back to full bundle');
752
+ }
753
+ } catch (patchErr) {
754
+ logger.warn(`[OTA] Patch staging failed: ${patchErr.message} — falling back to full bundle`);
705
755
  }
706
- } catch (patchErr) {
707
- logger.warn(`[OTA] Patch staging failed: ${patchErr.message} — falling back to full bundle`);
708
756
  }
709
- }
710
757
 
711
- // Full-bundle fallback (or primary path when no patch is available).
712
- // Retried with backoff: on a mobile network a transient drop mid-download
713
- // is the common case, and previously one failure meant waiting for the
714
- // next cold start — often hours away, sometimes never.
715
- if (!staged) {
716
- try {
717
- staged = await retryWithBackoff(async () => {
718
- const ok = await NativeScaleBunOta.stageBundle(bundle.url, bundle.sha256);
719
- if (!ok) {
720
- // Native returns a bare boolean, so a hash mismatch and a
721
- // dropped connection are indistinguishable here. Retrying a
722
- // genuine mismatch a couple of times is cheap; not retrying a
723
- // dropped connection costs the whole update.
724
- throw new Error('stageBundle returned false');
725
- }
726
- return ok;
727
- }, {
728
- label: `download v${bundle.version}`,
729
- attempts: 3
730
- });
731
- } catch {
732
- staged = false;
758
+ // Full-bundle fallback (or primary path when no patch is available).
759
+ // Retried with backoff: on a mobile network a transient drop mid-download
760
+ // is the common case, and previously one failure meant waiting for the
761
+ // next cold start — often hours away, sometimes never.
762
+ if (!staged) {
763
+ try {
764
+ staged = await retryWithBackoff(async () => {
765
+ const ok = await NativeScaleBunOta.stageBundle(bundle.url, bundle.sha256);
766
+ if (!ok) {
767
+ // Native returns a bare boolean, so a hash mismatch and a
768
+ // dropped connection are indistinguishable here. Retrying a
769
+ // genuine mismatch a couple of times is cheap; not retrying a
770
+ // dropped connection costs the whole update.
771
+ throw new Error('stageBundle returned false');
772
+ }
773
+ return ok;
774
+ }, {
775
+ label: `download v${bundle.version}`,
776
+ attempts: 3
777
+ });
778
+ } catch {
779
+ staged = false;
780
+ }
733
781
  }
734
782
  }
735
783
  unsubscribeProgress();
@@ -209,17 +209,11 @@ class ScaleBunFacade {
209
209
  /**
210
210
  * Boot the OTA orchestrator when the init config asks for it.
211
211
  *
212
- * `publicSigningKey` / `publicSigningKeys` are honoured here so signature
213
- * enforcement is reachable from configuration alone. The list form exists
214
- * for key ROTATION: a build pinning [old, new] keeps verifying while the
215
- * server moves to the new key, so replacing a key never needs an emergency
216
- * store release. Both fields merge (deduplicated) into one pinned set.
217
- *
218
- * Verification comes from `ota.verifySignature` when supplied, else the
219
- * built-in @noble-based verifier (optional peers). Pinning keys with
220
- * NEITHER available is fail-CLOSED by design (signature.ts) — an update
221
- * that cannot be verified is not installed. We say that out loud rather
222
- * than letting the app discover it as a silent no-update condition.
212
+ * Signature enforcement is no longer configured from JS. Signed releases are
213
+ * verified and staged in NATIVE code against RSA keys embedded in the app
214
+ * package (SB-OTA-RSA-SHA256-V1); unsigned releases keep working on SHA-256
215
+ * integrity. There are no signing-key or `verifySignature` options to pass
216
+ * through the host embeds its trusted keys natively.
223
217
  */
224
218
  _maybeStartOta(rawConfig) {
225
219
  const ota = rawConfig?.ota;
@@ -231,42 +225,12 @@ class ScaleBunFacade {
231
225
  const {
232
226
  otaOrchestrator
233
227
  } = require('../features/ota/OtaOrchestrator');
234
- const rawSingle = ota.publicSigningKey;
235
- const rawList = ota.publicSigningKeys;
236
- const publicKeys = Array.from(new Set([...(typeof rawSingle === 'string' && rawSingle ? [rawSingle] : []), ...(Array.isArray(rawList) ? rawList : [])].filter(k => typeof k === 'string' && k.trim().length > 0)));
237
- // Signing was REQUESTED if a usable key exists, or the host passed a
238
- // keys array at all — an empty one included. `publicSigningKeys: []`
239
- // is a broken attempt to enable signing, and it must flow through to
240
- // signature.ts's fail-closed handling, not silently disable signing.
241
- const signingRequested = publicKeys.length > 0 || Array.isArray(rawList);
242
- const verifier = ota.verifySignature;
243
- if (signingRequested && typeof verifier !== 'function') {
244
- // A host verifier is optional since the built-in one landed — the
245
- // old unconditional warning here told every correctly-configured
246
- // app that its updates would be rejected. Warn only when neither
247
- // verifier can actually be resolved.
248
- // eslint-disable-next-line @typescript-eslint/no-var-requires
249
- const {
250
- getBuiltinVerifier
251
- } = require('../features/ota/crypto/builtinVerifier');
252
- if (!getBuiltinVerifier()) {
253
- logger.warn('[ScaleBun] A signing key is pinned but no signature verifier is available. ' + 'Signature checking is fail-closed: updates will be REJECTED until one exists. ' + 'Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further ' + 'code needed), or supply `ota.verifySignature`.');
254
- }
255
- }
256
228
  // `healthyAfterMs` is the field BootGuardConfig actually declares.
257
229
  // This passed `healthyTimeoutMs`, which nothing reads, so a host that
258
230
  // tuned the boot-guard window silently got the 10s default instead.
259
231
  // Both spellings are accepted so the older one keeps working.
260
232
  const healthyAfterMs = typeof ota.healthyAfterMs === 'number' ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === 'number' ? ota.healthyTimeoutMs : undefined;
261
233
  otaOrchestrator.init({
262
- ...(signingRequested ? {
263
- signature: {
264
- // Single key stays a plain string — the shape every
265
- // existing consumer and test already handles.
266
- publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
267
- verifier
268
- }
269
- } : {}),
270
234
  ...(healthyAfterMs !== undefined ? {
271
235
  healthyAfterMs
272
236
  } : {})
@@ -92,9 +92,6 @@ export interface ScaleBunConfig {
92
92
  enabled: boolean;
93
93
  checkOnForeground: boolean;
94
94
  channelOverride?: string;
95
- publicSigningKey?: string;
96
- /** Additional pinned keys for rotation; unioned with publicSigningKey. */
97
- publicSigningKeys?: string[];
98
95
  mandatoryBlocksUi: boolean;
99
96
  };
100
97
  }
@@ -8,5 +8,5 @@
8
8
  * value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
9
9
  * version was introduced to solve.
10
10
  */
11
- export declare const SDK_VERSION = "1.13.0";
11
+ export declare const SDK_VERSION = "2.0.0";
12
12
  //# sourceMappingURL=version.d.ts.map
@@ -13,6 +13,24 @@ export interface NetworkMonitoringOptions {
13
13
  redactNetwork?: boolean;
14
14
  /** SDK backend base URL — used to filter out SDK's own upload calls from capture */
15
15
  sdkBaseUrl?: string;
16
+ /**
17
+ * Never capture requests whose URL matches (substring or RegExp).
18
+ *
19
+ * Parity with the web SDK, which has had this since F-NET. Mobile had no way to exclude a
20
+ * URL at all, so an app that wanted one endpoint out of its replays had no answer.
21
+ */
22
+ denyUrls?: (string | RegExp)[];
23
+ /**
24
+ * Keep known telemetry beacons: analytics collectors, ad pixels, IP-enrichment calls.
25
+ *
26
+ * Default false — they are dropped before capture. They are not the app, they cost the
27
+ * device real battery and cellular data to upload, and a beacon that fails counts against
28
+ * the SESSION HEALTH score. Matching is by hostname only, never by path, so an app route
29
+ * called /collect is never touched. See features/network/thirdParty.ts.
30
+ */
31
+ captureThirdParty?: boolean;
32
+ /** Replace the built-in beacon host list wholesale. Comma-joined hostname suffixes. */
33
+ thirdPartyHosts?: string;
16
34
  }
17
35
  export declare function installNetworkMonitoring(onEvent: (data: any) => void, options?: NetworkMonitoringOptions): void;
18
36
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * THIRD-PARTY BEACON HOSTS — the one list, and the one matcher, for every layer that drops them.
3
+ *
4
+ * WHAT THIS IS FOR
5
+ * A session on an instrumented site spends most of its network tab on traffic the app did not
6
+ * meaningfully make: analytics collectors, ad pixels, IP-enrichment calls, tag managers. It is
7
+ * captured, uploaded, stored and rendered, and none of it answers a question anyone asks a session
8
+ * replay. One real sign-in screen was 27 requests, most of them beacons.
9
+ *
10
+ * It is not only clutter. Session health scores `failedRequests` and `slowRequests`, so a tracker
11
+ * killed by an ad blocker (a transport error) or one that is simply slow moves a number that is
12
+ * supposed to describe the APP. That is the part that makes this a correctness fix rather than a
13
+ * tidy-up.
14
+ *
15
+ * WHY HOSTS ONLY, NEVER PATHS
16
+ * The obvious rules from looking at a network tab are `/collect`, `/p`, `/api/v2/pixel`. Every one
17
+ * of them is a path that a customer's own API is allowed to have, and dropping a real API call is
18
+ * far worse than keeping a beacon: the noise is visible and annoying, the missing request is
19
+ * invisible and misleading. So matching is on the HOSTNAME, with a dot boundary, and a request to
20
+ * `/collect` on the app's own origin is kept — correctly.
21
+ *
22
+ * WHY IT IS A SUFFIX LIST AND NOT A REGEX
23
+ * Two reasons, both practical. It is applied inside an SDK whose eager bundle is measured in bytes,
24
+ * and a comma-joined string of suffixes compresses to almost nothing next to an equivalent pattern
25
+ * source. And it has to be COPIED, byte for byte, into the React Native SDK and into the backend's
26
+ * ingest path — a list with no syntax is a list that cannot drift in meaning between three
27
+ * languages' worth of tooling. `third-party:parity` hashes all three copies.
28
+ *
29
+ * ZERO IMPORTS, DELIBERATELY. That is what makes the byte-identical copy possible. If an edit here
30
+ * ever needs an import, the parity gate is the thing that will say so — keep the module pure rather
31
+ * than weakening the gate.
32
+ *
33
+ * WHAT IS DELIBERATELY NOT ON THE LIST
34
+ * The rule is: a host whose ONLY purpose is to receive telemetry. Everything else stays, because a
35
+ * dropped request is invisible and a kept one is merely noisy.
36
+ *
37
+ * - Feature-flag and experiment services (LaunchDarkly, Optimizely, Split). Their RESPONSES
38
+ * change what the app does, so a failed flag fetch is an application incident and the single
39
+ * most useful request in the session.
40
+ * - Chat and support widgets (Intercom, Zendesk, Drift). A broken widget is a broken feature the
41
+ * user can see.
42
+ * - `facebook.com` as a whole. `graph.facebook.com` is an API real apps call; only the pixel
43
+ * delivery hosts are listed.
44
+ * - CDNs of any kind, even ones that mostly serve tags: they also serve application assets, and
45
+ * dropping them would hide a real asset failure.
46
+ *
47
+ * When in doubt, leave it out. `denyUrls` stays the per-app escape hatch in both SDKs, and it can
48
+ * do what this list will not.
49
+ */
50
+ /**
51
+ * Known telemetry, advertising and enrichment endpoints, as hostname suffixes.
52
+ *
53
+ * One comma-joined string rather than an array: identical meaning, materially smaller once gzipped,
54
+ * and it keeps the copy in three repos a single line to compare by eye.
55
+ */
56
+ export declare const THIRD_PARTY_BEACON_HOSTS: string;
57
+ /**
58
+ * True when `url` is addressed to a known beacon host.
59
+ *
60
+ * Deliberately total: an unparseable or relative URL is NOT third party. A relative URL is
61
+ * same-origin by definition — the app's own call — and guessing on garbage input would drop real
62
+ * requests for no benefit.
63
+ */
64
+ export declare function isThirdPartyBeacon(url: string, hosts?: string): boolean;
65
+ //# sourceMappingURL=thirdParty.d.ts.map
@@ -1,5 +1,4 @@
1
1
  import type { OtaCheckResponse, OtaBundlePayload } from './OtaTypes';
2
- import { type SignatureConfig } from './signature';
3
2
  export interface SyncResult {
4
3
  status: 'UP_TO_DATE' | 'UPDATE_INSTALLED' | 'ROLLED_BACK' | 'ERROR';
5
4
  bundle?: OtaBundlePayload;
@@ -37,7 +36,6 @@ export declare class OtaOrchestrator {
37
36
  private isRestartRequiredState;
38
37
  private healthyTimer;
39
38
  private bootGuardConfig;
40
- private signatureConfig;
41
39
  private environment;
42
40
  /** One-time boot checks have run in this process (init is idempotent). */
43
41
  private initialized;
@@ -45,13 +43,12 @@ export declare class OtaOrchestrator {
45
43
  /**
46
44
  * Initialize the orchestrator. Checks for boot-guard recovery state.
47
45
  *
48
- * `signature` is optional: an app that has not adopted signing keeps working
49
- * on SHA-256 integrity alone. Once a public key IS configured, a bundle that
50
- * cannot be verified is refused rather than installed (OTA-03).
46
+ * Signature enforcement is no longer a JS concern: signed releases are
47
+ * verified and staged in NATIVE code against RSA keys embedded in the app
48
+ * package (SB-OTA-RSA-SHA256-V1), and unsigned releases keep working on
49
+ * SHA-256 integrity alone. There is nothing for the host to configure here.
51
50
  */
52
- init(config?: BootGuardConfig & {
53
- signature?: SignatureConfig;
54
- }): void;
51
+ init(config?: BootGuardConfig): void;
55
52
  /**
56
53
  * Read the active slot back into `currentBundle` so the next check reports
57
54
  * what this device is genuinely running.
@@ -51,6 +51,11 @@ export interface OtaCheckRequest {
51
51
  installationId: string;
52
52
  currentBundleId?: string;
53
53
  currentBundleHash?: string;
54
+ /**
55
+ * Trusted key IDs embedded in this app binary, for SDK trust coverage
56
+ * (CICDPAGE §12). Telemetry only — never a source of server-side trust.
57
+ */
58
+ trustedKeyIds?: string[];
54
59
  /** Channel to pull from. Omitted means `default`. */
55
60
  channelName?: string;
56
61
  /** ISO 3166-1 alpha-2. The server fills this from the edge when absent. */
@@ -93,6 +98,23 @@ export interface OtaBundlePayload {
93
98
  size: number;
94
99
  sha256: string;
95
100
  signature?: string;
101
+ /**
102
+ * SB-OTA-RSA-SHA256-V1 release envelope. Present on signed releases; carries
103
+ * the protocol id, the id of the trusted RSA key the release was signed with,
104
+ * the canonical signed payload, and its signature. When present, the SDK hands
105
+ * the whole envelope to the native module (`verifyAndStageRelease`), which
106
+ * verifies the signature against the RSA public keys EMBEDDED in the app
107
+ * package and then downloads + integrity-checks + stages the artifact — JS
108
+ * never stages bytes it has not proven authentic, and never pins or verifies a
109
+ * key itself. Absent on legacy/unsigned releases, which fall back to
110
+ * `stageBundle` on SHA-256 integrity alone.
111
+ */
112
+ envelope?: {
113
+ protocol: string;
114
+ keyId: string;
115
+ signedPayload: string;
116
+ signature: string;
117
+ };
96
118
  isMandatory: boolean;
97
119
  /**
98
120
  * How to activate the bundle after install:
@@ -69,17 +69,11 @@ declare class ScaleBunFacade {
69
69
  /**
70
70
  * Boot the OTA orchestrator when the init config asks for it.
71
71
  *
72
- * `publicSigningKey` / `publicSigningKeys` are honoured here so signature
73
- * enforcement is reachable from configuration alone. The list form exists
74
- * for key ROTATION: a build pinning [old, new] keeps verifying while the
75
- * server moves to the new key, so replacing a key never needs an emergency
76
- * store release. Both fields merge (deduplicated) into one pinned set.
77
- *
78
- * Verification comes from `ota.verifySignature` when supplied, else the
79
- * built-in @noble-based verifier (optional peers). Pinning keys with
80
- * NEITHER available is fail-CLOSED by design (signature.ts) — an update
81
- * that cannot be verified is not installed. We say that out loud rather
82
- * than letting the app discover it as a silent no-update condition.
72
+ * Signature enforcement is no longer configured from JS. Signed releases are
73
+ * verified and staged in NATIVE code against RSA keys embedded in the app
74
+ * package (SB-OTA-RSA-SHA256-V1); unsigned releases keep working on SHA-256
75
+ * integrity. There are no signing-key or `verifySignature` options to pass
76
+ * through the host embeds its trusted keys natively.
83
77
  */
84
78
  private _maybeStartOta;
85
79
  /** Guards against overlapping config-driven OTA checks. */
@@ -22,6 +22,14 @@ export interface Spec extends TurboModule {
22
22
  * the bundle URL at launch.
23
23
  */
24
24
  getActiveBundle(): string | null;
25
+ /**
26
+ * JSON array (string) of the trusted key IDs embedded in the app package, e.g.
27
+ * `["production-2026-01"]`. Reported on OTA check-in for SDK trust coverage
28
+ * (CICDPAGE §12) so the dashboard can measure key-rotation adoption. This is
29
+ * telemetry ONLY — the server never trusts a key because a device claims it.
30
+ * Empty array when no keys are embedded.
31
+ */
32
+ getTrustedKeyIds(): string;
25
33
  /**
26
34
  * Returns JSON with current slot state:
27
35
  * { current: { bundleId, version, sha256, installedAt } | null,
@@ -77,28 +85,30 @@ export interface Spec extends TurboModule {
77
85
  */
78
86
  restartApp(): void;
79
87
  /**
80
- * Verify a detached ed25519 signature in NATIVE code.
81
- *
82
- * Exists because the JS verifier runs inside the very bundle it protects
83
- * an attacker who lands one malicious bundle can neuter a JS check for every
84
- * update after it. Platform crypto is outside the bundle's reach, and it also
85
- * removes the runtime dependency on the optional @noble peers wherever the OS
86
- * provides ed25519 (iOS 13+ CryptoKit everywhere; Android API 33+).
88
+ * Verify a SB-OTA-RSA-SHA256-V1 release envelope in NATIVE code, with no side
89
+ * effects. `envelopeJson` is the JSON `{ protocol, keyId, signedPayload,
90
+ * signature, downloadUrl? }` from the check response. Verification uses the
91
+ * trusted RSA public keys EMBEDDED in the app package (never a key from JS or
92
+ * the server) and platform `SHA256withRSA` (present on every supported API
93
+ * level no API-33 floor).
87
94
  *
88
- * All three arguments are lowercase hex, pre-validated by the JS caller:
89
- * the 32-byte bundle SHA-256 (the signature is over its RAW BYTES, matching
90
- * the CLI's signing contract), the 64-byte signature, the 32-byte public key.
91
- *
92
- * Resolves a verdict STRING, not a boolean, because "the signature is wrong"
93
- * and "this OS cannot check" must never collapse into one value:
94
- * 'valid' — signature verifies under the key
95
- * 'invalid' — it does not; the caller must refuse the bundle
96
- * 'unavailable' this OS level has no ed25519 (Android < 33); the caller
97
- * falls back to the JS verifier
98
- * A rejected promise is machinery failure and the caller treats it as
99
- * 'unavailable' never as 'valid'.
95
+ * Resolves a verdict JSON string: `{ ok: boolean, code?: string, message?,
96
+ * keyId?, releaseId?, bundleId?, releaseSequence?, artifactSha256?,
97
+ * artifactSize?, resultArtifactSha256?, fingerprint? }`. A failed check is
98
+ * `ok:false` with a stable `code`; a rejected promise is machinery failure and
99
+ * the caller must treat it as a failure, never as valid.
100
+ */
101
+ verifyRelease(envelopeJson: string): Promise<string>;
102
+ /**
103
+ * Native-ENFORCED install: verify the RSA signature (as `verifyRelease`), then
104
+ * download the artifact, enforce the SIGNED size + SHA-256, and stage it into
105
+ * the `next` slot all natively. JavaScript cannot stage bytes that were not
106
+ * proven authentic (once trusted keys are embedded, the legacy `stageBundle`
107
+ * path refuses). Does not activate; call `applyUpdate` to promote per the
108
+ * install mode. Resolves the same verdict JSON as `verifyRelease` (`ok:true`
109
+ * once staged).
100
110
  */
101
- verifyEd25519(messageHex: string, signatureHex: string, publicKeyHex: string): Promise<string>;
111
+ verifyAndStageRelease(envelopeJson: string): Promise<string>;
102
112
  }
103
113
  /**
104
114
  * Resolved via `get` (not `getEnforcing`) so the SDK degrades gracefully when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalebun/react-native",
3
- "version": "1.13.0",
3
+ "version": "2.0.0",
4
4
  "description": "React Native SDK for ScaleBun",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -70,8 +70,6 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/preset-env": "^8.0.2",
73
- "@noble/ed25519": "^3.1.0",
74
- "@noble/hashes": "^2.3.0",
75
73
  "@react-native-community/eslint-config": "^3.2.0",
76
74
  "@types/jest": "^29.5.12",
77
75
  "@types/node": "^25.3.3",
@@ -96,9 +94,7 @@
96
94
  "expo-router": ">=1.0.0",
97
95
  "react": ">=18.0.0",
98
96
  "react-native": ">=0.74.0",
99
- "react-native-view-shot": ">=3.0.0",
100
- "@noble/ed25519": ">=2.0.0",
101
- "@noble/hashes": ">=2.0.0"
97
+ "react-native-view-shot": ">=3.0.0"
102
98
  },
103
99
  "peerDependenciesMeta": {
104
100
  "react-native-view-shot": {
@@ -109,19 +105,13 @@
109
105
  },
110
106
  "expo-router": {
111
107
  "optional": true
112
- },
113
- "@noble/ed25519": {
114
- "optional": true
115
- },
116
- "@noble/hashes": {
117
- "optional": true
118
108
  }
119
109
  },
120
110
  "dependencies": {
121
111
  "@babel/runtime": "^7.25.0",
122
112
  "@jridgewell/sourcemap-codec": "1.5.5",
123
113
  "@jridgewell/trace-mapping": "0.3.31",
124
- "@scalebun/cli": "^1.13.0"
114
+ "@scalebun/cli": "^2.0.0"
125
115
  },
126
116
  "codegenConfig": {
127
117
  "name": "ScaleBunSpec",
@@ -13,6 +13,10 @@ Pod::Spec.new do |s|
13
13
  # Include .mm (ObjC++) for the New Architecture TurboModule providers added in
14
14
  # Phase 2, alongside the existing .h/.m/.swift sources.
15
15
  s.source_files = "ios/**/*.{h,m,mm,swift}"
16
+ # XCTest files ship with the SDK source but must NOT compile into the pod (they
17
+ # import XCTest and belong in a host app's test target). See the run
18
+ # instructions in ScaleBunOtaVerifierTests.swift (PROMPT §23).
19
+ s.exclude_files = "ios/**/*Tests.swift"
16
20
 
17
21
  # Define a clang module so the generated Swift interop header
18
22
  # (`scalebun_react_native-Swift.h`) is emitted and importable from a host