@scalebun/react-native 1.13.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +8 -0
- package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
- package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
- package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
- package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +132 -62
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +80 -1
- package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +6 -8
- package/dist/scalebun.full.js +964 -533
- package/dist/scalebun.slim.js +962 -531
- package/ios/Ota/OtaProtocol.swift +242 -0
- package/ios/Ota/OtaSlotManager.swift +26 -0
- package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
- package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
- package/ios/Ota/ScaleBunOtaModule.swift +125 -53
- package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
- package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
- package/ios/ReplaySdk.swift +65 -2
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/commonjs/core/config/schema.js +19 -12
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
- package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
- package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
- package/lib/commonjs/features/journey/scrollContext.js +146 -0
- package/lib/commonjs/features/journey/targetGeometry.js +164 -0
- package/lib/commonjs/features/journey/touchTarget.js +152 -24
- package/lib/commonjs/features/journey/uiState.js +81 -1
- package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/commonjs/features/network/NetworkFeature.js +39 -5
- package/lib/commonjs/features/network/thirdParty.js +90 -0
- package/lib/commonjs/features/ota/OtaOrchestrator.js +175 -80
- package/lib/commonjs/features/session/SessionManager.js +63 -0
- package/lib/commonjs/public/ScaleBunFacade.js +23 -57
- package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/module/core/config/schema.js +19 -12
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
- package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
- package/lib/module/features/journey/interactionProtocol.js +128 -8
- package/lib/module/features/journey/scrollContext.js +135 -0
- package/lib/module/features/journey/targetGeometry.js +155 -0
- package/lib/module/features/journey/touchTarget.js +151 -24
- package/lib/module/features/journey/uiState.js +78 -1
- package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/module/features/network/NetworkFeature.js +38 -4
- package/lib/module/features/network/thirdParty.js +83 -0
- package/lib/module/features/ota/OtaOrchestrator.js +175 -80
- package/lib/module/features/session/SessionManager.js +63 -0
- package/lib/module/public/ScaleBunFacade.js +23 -57
- package/lib/typescript/core/config/schema.d.ts +2 -3
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
- package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
- package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
- package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
- package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
- package/lib/typescript/features/journey/uiState.d.ts +39 -0
- package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
- package/lib/typescript/features/network/index.d.ts +18 -0
- package/lib/typescript/features/network/thirdParty.d.ts +65 -0
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +12 -8
- package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
- package/lib/typescript/features/session/SessionManager.d.ts +55 -0
- package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
- package/lib/typescript/public/types.d.ts +10 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +32 -20
- package/package.json +3 -13
- package/scalebun-react-native.podspec +4 -0
- package/src/bootstrap/SDKBootstrapper.ts +40 -0
- package/src/core/config/schema.ts +21 -9
- package/src/core/constants/version.ts +1 -1
- package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
- package/src/features/journey/ScaleBunScrollView.tsx +29 -0
- package/src/features/journey/autoInstrumentScroll.ts +155 -0
- package/src/features/journey/interactionProtocol.ts +188 -10
- package/src/features/journey/scrollContext.ts +137 -0
- package/src/features/journey/targetGeometry.ts +176 -0
- package/src/features/journey/touchTarget.ts +237 -26
- package/src/features/journey/uiState.ts +80 -1
- package/src/features/navigation/AutoScreenDetector.ts +65 -5
- package/src/features/network/NetworkFeature.ts +41 -4
- package/src/features/network/index.ts +18 -0
- package/src/features/network/thirdParty.ts +92 -0
- package/src/features/ota/OtaOrchestrator.ts +190 -90
- package/src/features/ota/OtaTypes.ts +23 -1
- package/src/features/session/SessionManager.ts +113 -0
- package/src/public/ScaleBunFacade.ts +23 -69
- package/src/public/types.ts +10 -0
- package/src/specs/NativeScaleBunOta.ts +35 -20
- package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
- package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
- package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
- package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
- package/lib/commonjs/features/ota/signature.js +0 -175
- package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
- package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
- package/lib/module/features/ota/crypto/loadSha512.js +0 -34
- package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
- package/lib/module/features/ota/signature.js +0 -169
- package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
- package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
- package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
- package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
- package/lib/typescript/features/ota/signature.d.ts +0 -81
- package/src/features/ota/crypto/builtinVerifier.ts +0 -257
- package/src/features/ota/crypto/loadEd25519.ts +0 -41
- package/src/features/ota/crypto/loadSha512.ts +0 -35
- package/src/features/ota/crypto/nativeVerifier.ts +0 -117
- package/src/features/ota/signature.ts +0 -206
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import React
|
|
3
3
|
import CommonCrypto
|
|
4
|
-
import CryptoKit
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* React Native bridge for OTA bundle management (`ScaleBunOta`) on iOS.
|
|
@@ -21,6 +20,21 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
21
20
|
|
|
22
21
|
private static let slotManager = OtaSlotManager()
|
|
23
22
|
|
|
23
|
+
/// Trusted RSA public keys embedded in the app bundle (docs §9). Non-empty
|
|
24
|
+
/// once the app ships a `ScaleBunOtaKeys.json` resource — which flips staging
|
|
25
|
+
/// into the native-ENFORCED path (see `stageBundle`).
|
|
26
|
+
private static let keyRegistry = ScaleBunOtaKeyRegistry.fromBundle()
|
|
27
|
+
|
|
28
|
+
/// Device-derived verification context — never taken from JS.
|
|
29
|
+
private static func buildContext() -> ExpectedContext {
|
|
30
|
+
ExpectedContext(
|
|
31
|
+
projectId: keyRegistry.projectId,
|
|
32
|
+
applicationId: Bundle.main.bundleIdentifier ?? "",
|
|
33
|
+
currentSequence: slotManager.highestConfirmedSequence(),
|
|
34
|
+
nowEpochSeconds: Int64(Date().timeIntervalSince1970)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
static func moduleName() -> String! { "ScaleBunOta" }
|
|
25
39
|
|
|
26
40
|
@objc static func requiresMainQueueSetup() -> Bool { false }
|
|
@@ -73,6 +87,17 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
73
87
|
return ScaleBunOtaModule.slotManager.slotStateJSON() as NSString
|
|
74
88
|
}
|
|
75
89
|
|
|
90
|
+
/// JSON array of the trusted key IDs embedded in this app (SDK trust coverage).
|
|
91
|
+
@objc(getTrustedKeyIds)
|
|
92
|
+
func getTrustedKeyIds() -> NSString {
|
|
93
|
+
let ids = ScaleBunOtaModule.keyRegistry.keyIds()
|
|
94
|
+
if let data = try? JSONSerialization.data(withJSONObject: ids),
|
|
95
|
+
let s = String(data: data, encoding: .utf8) {
|
|
96
|
+
return s as NSString
|
|
97
|
+
}
|
|
98
|
+
return "[]"
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
@objc(stageBundle:sha256:resolve:reject:)
|
|
77
102
|
/// Stage a bundle into the `next` slot.
|
|
78
103
|
///
|
|
@@ -85,6 +110,16 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
85
110
|
sha256: NSString,
|
|
86
111
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
87
112
|
reject: @escaping RCTPromiseRejectBlock) {
|
|
113
|
+
// Native-enforced staging (docs §17): once trusted keys are embedded, the
|
|
114
|
+
// ONLY way to stage is verifyAndStageRelease, which proves an RSA signature
|
|
115
|
+
// over the release payload first. SHA-256 integrity alone (what this path
|
|
116
|
+
// checks) is not authenticity, so a hostile JS bundle cannot use it to
|
|
117
|
+
// drive an unauthenticated bundle onto the device.
|
|
118
|
+
if !ScaleBunOtaModule.keyRegistry.isEmpty {
|
|
119
|
+
NSLog("[ScaleBunOta] stageBundle refused: trusted keys embedded — use verifyAndStageRelease")
|
|
120
|
+
resolve(false)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
88
123
|
let source = sourcePath as String
|
|
89
124
|
let meta: [String: Any] = [
|
|
90
125
|
"sha256": sha256 as String,
|
|
@@ -140,6 +175,16 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
140
175
|
expectedSha256: NSString,
|
|
141
176
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
142
177
|
reject: @escaping RCTPromiseRejectBlock) {
|
|
178
|
+
// Same native-enforcement as stageBundle: a bsdiff patch staged by hash
|
|
179
|
+
// alone carries no signature, so once trusted keys are embedded this path
|
|
180
|
+
// is closed. Result-anchored signed patches are a native follow-up; until
|
|
181
|
+
// then the SDK falls back to the verified full download. false = "patch
|
|
182
|
+
// unavailable".
|
|
183
|
+
if !ScaleBunOtaModule.keyRegistry.isEmpty {
|
|
184
|
+
NSLog("[ScaleBunOta] stagePatch disabled while trusted keys are embedded — full verified download")
|
|
185
|
+
resolve(false)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
143
188
|
// Patching is relative to the bundle already on disk. With no OTA bundle
|
|
144
189
|
// installed the source would be the app's built-in bundle, which the
|
|
145
190
|
// server did not diff against — so a full download is the correct path.
|
|
@@ -255,68 +300,95 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
255
300
|
}
|
|
256
301
|
|
|
257
302
|
/**
|
|
258
|
-
* Verify a
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* Contract (mirrors `src/specs/NativeScaleBunOta.ts`): inputs are
|
|
267
|
-
* lowercase hex pre-validated by the JS caller; the message is the RAW 32
|
|
268
|
-
* BYTES of the bundle SHA-256 (the CLI signs digest bytes, not hex text);
|
|
269
|
-
* resolves 'valid' / 'invalid' / 'unavailable'. A wrong signature is
|
|
270
|
-
* 'invalid', never a rejection; anything unexpected is 'unavailable' so
|
|
271
|
-
* machinery failure can never read as a pass.
|
|
303
|
+
* Verify a SB-OTA-RSA-SHA256-V1 release envelope with PLATFORM crypto
|
|
304
|
+
* (`SecKeyVerifySignature`) and the EMBEDDED trusted-key registry — no
|
|
305
|
+
* download, no side effects. Resolves the verdict JSON (`{ ok, code?, keyId?,
|
|
306
|
+
* releaseId?, bundleId?, releaseSequence?, artifactSha256?, artifactSize?,
|
|
307
|
+
* resultArtifactSha256?, fingerprint? }`). A failed check is `ok:false` with a
|
|
308
|
+
* stable `code`, never a rejected promise. Native because the JS verifier
|
|
309
|
+
* lives inside the very bundle it protects, and the trusted key must come from
|
|
310
|
+
* the app bundle, not from JS or the server.
|
|
272
311
|
*
|
|
273
|
-
* ⚪
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* this file still needs one Xcode build before an iOS consumer ships it.
|
|
312
|
+
* ⚪ Not compiled in this workspace (no macOS toolchain). The RSA/SHA-256
|
|
313
|
+
* algorithm + wire format are gated by tools/rsa-ota-parity against the shared
|
|
314
|
+
* fixtures, and ScaleBunOtaVerifierTests is provided to run on a simulator.
|
|
277
315
|
*/
|
|
278
|
-
@objc(
|
|
279
|
-
func
|
|
280
|
-
signatureHex: NSString,
|
|
281
|
-
publicKeyHex: NSString,
|
|
316
|
+
@objc(verifyRelease:resolve:reject:)
|
|
317
|
+
func verifyRelease(_ envelopeJson: NSString,
|
|
282
318
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
283
319
|
reject: @escaping RCTPromiseRejectBlock) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
320
|
+
let verdict = ScaleBunOtaReleaseVerifier.verify(
|
|
321
|
+
envelopeJson: envelopeJson as String,
|
|
322
|
+
expected: ScaleBunOtaModule.buildContext(),
|
|
323
|
+
registry: ScaleBunOtaModule.keyRegistry
|
|
324
|
+
)
|
|
325
|
+
resolve(verdict.toJson())
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Native-ENFORCED install (docs §5, §17): verify the RSA signature against an
|
|
330
|
+
* embedded key, THEN download, enforce the SIGNED size + SHA-256, and stage
|
|
331
|
+
* into the `next` slot. JS never stages bytes that were not proven authentic
|
|
332
|
+
* (once keys are embedded, `stageBundle` refuses). Does not activate —
|
|
333
|
+
* `applyUpdate` promotes per the install mode. Resolves the verdict JSON.
|
|
334
|
+
*/
|
|
335
|
+
@objc(verifyAndStageRelease:resolve:reject:)
|
|
336
|
+
func verifyAndStageRelease(_ envelopeJson: NSString,
|
|
337
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
338
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
339
|
+
let verdict = ScaleBunOtaReleaseVerifier.verify(
|
|
340
|
+
envelopeJson: envelopeJson as String,
|
|
341
|
+
expected: ScaleBunOtaModule.buildContext(),
|
|
342
|
+
registry: ScaleBunOtaModule.keyRegistry
|
|
343
|
+
)
|
|
344
|
+
guard verdict.ok, let payload = verdict.payload else {
|
|
345
|
+
resolve(verdict.toJson())
|
|
290
346
|
return
|
|
291
347
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
348
|
+
|
|
349
|
+
guard let data = (envelopeJson as String).data(using: .utf8),
|
|
350
|
+
let env = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
351
|
+
let url = env["downloadUrl"] as? String, !url.isEmpty else {
|
|
352
|
+
resolve(Verdict.fail(.DOWNLOAD_FAILED, "envelope has no downloadUrl").toJson())
|
|
353
|
+
return
|
|
298
354
|
}
|
|
299
|
-
}
|
|
300
355
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
356
|
+
let temp = FileManager.default.temporaryDirectory
|
|
357
|
+
.appendingPathComponent("scalebun_ota_dl_\(UUID().uuidString)")
|
|
358
|
+
defer { try? FileManager.default.removeItem(at: temp) }
|
|
359
|
+
|
|
360
|
+
guard BundleDownloader.download(urlString: url, destination: temp) else {
|
|
361
|
+
resolve(Verdict.fail(.DOWNLOAD_FAILED, "download failed").toJson())
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
// Size first (cheap) against the SIGNED artifactSize.
|
|
365
|
+
let attrs = try? FileManager.default.attributesOfItem(atPath: temp.path)
|
|
366
|
+
let fileSize = (attrs?[.size] as? NSNumber)?.int64Value ?? -1
|
|
367
|
+
if fileSize != payload.artifactSize {
|
|
368
|
+
resolve(Verdict.fail(.ARTIFACT_SIZE_MISMATCH, "downloaded size mismatch").toJson())
|
|
369
|
+
return
|
|
310
370
|
}
|
|
311
|
-
return out
|
|
312
|
-
}
|
|
313
371
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
372
|
+
// The slot manager streams a SHA-256 and compares against the signed
|
|
373
|
+
// artifactSha256 (== resultArtifactSha256 for a full artifact), so a wrong
|
|
374
|
+
// download cannot be staged. meta carries the sequence used by the
|
|
375
|
+
// anti-downgrade floor once the bundle is confirmed healthy.
|
|
376
|
+
let meta: [String: Any] = [
|
|
377
|
+
"sha256": payload.artifactSha256,
|
|
378
|
+
"releaseSequence": payload.releaseSequence,
|
|
379
|
+
"bundleId": payload.bundleId,
|
|
380
|
+
"releaseId": payload.releaseId,
|
|
381
|
+
"installedAt": Int(Date().timeIntervalSince1970 * 1000)
|
|
382
|
+
]
|
|
383
|
+
let staged = ScaleBunOtaModule.slotManager.stage(
|
|
384
|
+
sourcePath: temp.path,
|
|
385
|
+
expectedSha256: payload.artifactSha256,
|
|
386
|
+
meta: meta
|
|
387
|
+
)
|
|
388
|
+
if !staged {
|
|
389
|
+
resolve(Verdict.fail(.ARTIFACT_HASH_MISMATCH, "staged bytes failed hash check").toJson())
|
|
390
|
+
return
|
|
320
391
|
}
|
|
392
|
+
resolve(verdict.toJson())
|
|
321
393
|
}
|
|
322
394
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SB-OTA-RSA-SHA256-V1 native release verifier for iOS (docs §5) — the mirror of
|
|
6
|
+
* Android's ScaleBunOtaReleaseVerifier. Runs the authoritative order: strict
|
|
7
|
+
* decode → registry-only key resolution → `SecKeyVerifySignature` with
|
|
8
|
+
* `.rsaSignatureMessagePKCS1v15SHA256` over the EXACT signed-payload bytes →
|
|
9
|
+
* parse → cross-check against the device's own identity + the anti-downgrade
|
|
10
|
+
* floor. Fails closed and maps every failure to a stable OtaFailureCode.
|
|
11
|
+
*
|
|
12
|
+
* The message-level algorithm hashes the payload internally, so the payload is
|
|
13
|
+
* passed RAW (never pre-hashed — pre-hashing then using a message-level API would
|
|
14
|
+
* hash twice, PROMPT §11.2).
|
|
15
|
+
*/
|
|
16
|
+
struct ExpectedContext {
|
|
17
|
+
let projectId: String
|
|
18
|
+
let applicationId: String // the app's own bundle identifier
|
|
19
|
+
let currentSequence: Int64 // anti-downgrade floor
|
|
20
|
+
let nowEpochSeconds: Int64
|
|
21
|
+
var platform: String = "ios"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
struct Verdict {
|
|
25
|
+
let ok: Bool
|
|
26
|
+
let code: OtaFailureCode?
|
|
27
|
+
let message: String?
|
|
28
|
+
let payload: SignedRelease?
|
|
29
|
+
let fingerprint: String?
|
|
30
|
+
|
|
31
|
+
static func fail(_ code: OtaFailureCode, _ message: String) -> Verdict {
|
|
32
|
+
Verdict(ok: false, code: code, message: message, payload: nil, fingerprint: nil)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func toJson() -> String {
|
|
36
|
+
var o: [String: Any] = ["ok": ok]
|
|
37
|
+
if let c = code { o["code"] = c.rawValue }
|
|
38
|
+
if let m = message { o["message"] = m }
|
|
39
|
+
if let f = fingerprint { o["fingerprint"] = f }
|
|
40
|
+
if let p = payload {
|
|
41
|
+
o["keyId"] = p.keyId
|
|
42
|
+
o["releaseId"] = p.releaseId
|
|
43
|
+
o["bundleId"] = p.bundleId
|
|
44
|
+
o["releaseSequence"] = p.releaseSequence
|
|
45
|
+
o["artifactSha256"] = p.artifactSha256
|
|
46
|
+
o["artifactSize"] = p.artifactSize
|
|
47
|
+
o["resultArtifactSha256"] = p.resultArtifactSha256
|
|
48
|
+
o["artifactType"] = p.artifactType
|
|
49
|
+
}
|
|
50
|
+
guard let data = try? JSONSerialization.data(withJSONObject: o),
|
|
51
|
+
let s = String(data: data, encoding: .utf8) else { return "{\"ok\":false,\"code\":\"INTERNAL_VERIFICATION_ERROR\"}" }
|
|
52
|
+
return s
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
enum ScaleBunOtaReleaseVerifier {
|
|
57
|
+
|
|
58
|
+
static func verify(envelopeJson: String, expected: ExpectedContext, registry: ScaleBunOtaKeyRegistry) -> Verdict {
|
|
59
|
+
do {
|
|
60
|
+
guard let data = envelopeJson.data(using: .utf8),
|
|
61
|
+
let env = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
62
|
+
return .fail(.INVALID_ENVELOPE, "envelope is not JSON")
|
|
63
|
+
}
|
|
64
|
+
let proto = (env["protocol"] as? String) ?? ""
|
|
65
|
+
if proto != OtaProtocol.signatureAlgorithm { return .fail(.UNSUPPORTED_PROTOCOL, "protocol \(proto)") }
|
|
66
|
+
let signedPayloadB64 = (env["signedPayload"] as? String) ?? ""
|
|
67
|
+
let signatureB64 = (env["signature"] as? String) ?? ""
|
|
68
|
+
let envKeyId = (env["keyId"] as? String) ?? ""
|
|
69
|
+
|
|
70
|
+
let payloadBytes = try OtaProtocol.decodeBase64Strict(signedPayloadB64, OtaProtocol.maxSignedPayloadBytes, "signedPayload")
|
|
71
|
+
let signature = try OtaProtocol.decodeBase64Strict(signatureB64, OtaProtocol.maxSignatureBytes, "signature")
|
|
72
|
+
|
|
73
|
+
let payloadKeyId = try OtaProtocol.peekKeyId(payloadBytes)
|
|
74
|
+
if !envKeyId.isEmpty && envKeyId != payloadKeyId {
|
|
75
|
+
return .fail(.INVALID_ENVELOPE, "envelope keyId disagrees with payload")
|
|
76
|
+
}
|
|
77
|
+
guard let key = registry.get(payloadKeyId) else {
|
|
78
|
+
return .fail(.UNKNOWN_KEY_ID, "no embedded key with id \(payloadKeyId)")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// RSA-SHA256 over the EXACT payload bytes, BEFORE parsing.
|
|
82
|
+
var error: Unmanaged<CFError>?
|
|
83
|
+
let valid = SecKeyVerifySignature(
|
|
84
|
+
key.secKey,
|
|
85
|
+
.rsaSignatureMessagePKCS1v15SHA256,
|
|
86
|
+
Data(payloadBytes) as CFData,
|
|
87
|
+
Data(signature) as CFData,
|
|
88
|
+
&error
|
|
89
|
+
)
|
|
90
|
+
if !valid { return .fail(.INVALID_SIGNATURE, "signature did not verify") }
|
|
91
|
+
|
|
92
|
+
let payload = try OtaProtocol.parse(payloadBytes)
|
|
93
|
+
|
|
94
|
+
if payload.artifactType != "full" { return .fail(.INVALID_SIGNED_PAYLOAD, "signed payload is not a full release") }
|
|
95
|
+
let projectExpected = registry.projectId.isEmpty ? expected.projectId : registry.projectId
|
|
96
|
+
if !projectExpected.isEmpty && payload.projectId != projectExpected { return .fail(.PROJECT_MISMATCH, "projectId") }
|
|
97
|
+
if !expected.applicationId.isEmpty && payload.applicationId != expected.applicationId {
|
|
98
|
+
return .fail(.APPLICATION_MISMATCH, "applicationId")
|
|
99
|
+
}
|
|
100
|
+
if payload.platform != expected.platform { return .fail(.PLATFORM_MISMATCH, "platform") }
|
|
101
|
+
if payload.expiresAtEpochSeconds != 0 && expected.nowEpochSeconds > payload.expiresAtEpochSeconds {
|
|
102
|
+
return .fail(.RELEASE_EXPIRED, "release expired")
|
|
103
|
+
}
|
|
104
|
+
if payload.releaseSequence < expected.currentSequence {
|
|
105
|
+
return .fail(.RELEASE_DOWNGRADE_REJECTED, "sequence below current")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return Verdict(ok: true, code: nil, message: nil, payload: payload, fingerprint: key.fingerprint)
|
|
109
|
+
} catch let e as OtaVerifyError {
|
|
110
|
+
return .fail(e.code, e.message)
|
|
111
|
+
} catch {
|
|
112
|
+
return .fail(.INTERNAL_VERIFICATION_ERROR, "\(error)")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import scalebun_react_native
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* On-device / simulator verification for SB-OTA-RSA-SHA256-V1 (PROMPT §23).
|
|
6
|
+
*
|
|
7
|
+
* Proves the shared committed vector (packages/ota-protocol/fixtures — the SAME
|
|
8
|
+
* bytes the Node codec, backend, JVM harness, and Android test use) verifies with
|
|
9
|
+
* `SecKeyVerifySignature(.rsaSignatureMessagePKCS1v15SHA256)` and the SPKI→PKCS#1
|
|
10
|
+
* import path, and that tampering / unknown keys / downgrades are rejected.
|
|
11
|
+
*
|
|
12
|
+
* ⚪ Not run in this workspace (no macOS toolchain). Add this file to the host
|
|
13
|
+
* app's iOS unit-test target and run: xcodebuild test -scheme <App> -destination
|
|
14
|
+
* 'platform=iOS Simulator,name=iPhone 15'. Constants are copied verbatim from
|
|
15
|
+
* fixtures/vectors/full.json.
|
|
16
|
+
*/
|
|
17
|
+
final class ScaleBunOtaVerifierTests: XCTestCase {
|
|
18
|
+
|
|
19
|
+
private let signedPayloadB64 =
|
|
20
|
+
"U0JPVEEwMDEAAQAAABRTQi1PVEEtUlNBLVNIQTI1Ni1WMQAAAAdTSEEtMjU2AAAAFXRlc3QtcnNhLTMwNzItMjAyNi0wMQAAAA5wcm9qX3Rlc3RfMDAwMQAAABVvcmcuc2NhbGVidW4ubGVnYXJhZ2UAAAANcmVsX2Z1bGxfMDAwMQAAAA5ibmRsX2Z1bGxfMDAwMQAAAAdhbmRyb2lkAAAABGZ1bGwAAAAFNC41LjkAAAAGMC43NC4xAAAAAjI0AAAAAAAAACoAAAAAaVW5AAAAAAAAAAAAAAAAAAAAAWD6bpV7WCc0zBI9NJ6rivyixtOu66jZ7EYSJniR99yQMwAAAAAAAAAA+m6Ve1gnNMwSPTSeq4r8osbTruuo2exGEiZ4kffckDM="
|
|
21
|
+
private let signatureB64 =
|
|
22
|
+
"jlC7GRLWKXku3cbqtODlFt+qn2CEmS3oHOr9QTopCCzEovUF5a2GRT0PQ5k2C5EX7UGGO0Sv6/WhkznKR9NVEZl6ed0KARHAY8zfwTjin3czx9jpwV9bfsLlxEoKNo9bPfIs3mc0VoHCWOp95hQAotG+GqEwYsXqPruZ43U87qWT0t9/a3QboX+07hPO5vI4fDZQuPeo3W1HmXPKbOB7QGSijtKgKwQNuJHz+etsVsQelDsDofRSpYV92XCheUWWHP7kZKFOCcYUXnVwJcdU0Jhej+22vAESRK2G0CXP3orXE4pASaA3y4wkBQgJAJkcTN/ic1/Wm5BLo7KH+ypK1eo0hnrrLNSh6E+St5NvxlTM3bmxwnwHkPMYVBuOT0fUJy+gbD1/1CPvKakL12vizKwWDYuhfcIGkQUTvP2zCxcqWLfmZ7ZGo6n6WXMX63Vz+IQG0tnx1cp6YqJVIgxqjskQ47sacsfS9QeV5LyJhvBQrDSbpqujo9la8dDzZfDN"
|
|
23
|
+
private let keyId = "test-rsa-3072-2026-01"
|
|
24
|
+
private let projectId = "proj_test_0001"
|
|
25
|
+
private let applicationId = "org.scalebun.legarage"
|
|
26
|
+
|
|
27
|
+
private let spkiPem = """
|
|
28
|
+
-----BEGIN PUBLIC KEY-----
|
|
29
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAt3tfFTPE+lkEbFeYi6zE
|
|
30
|
+
+uI+3oJl0HfKtGTh7sr7pSh/uPJq2giF0Uc09ux3Z0i4NlqpOyLmiIrVOgYcWcN+
|
|
31
|
+
/pCl1mh6VmT7nYO3sEhL+HyaIaclCGxvHCJSsaVHH8qQyiCVUypWWKN0L38U4isN
|
|
32
|
+
dJ2x+Wv7iLSZu3oM+ZDyGWCjRLE49jKNl0tfaQX6G1YoyBi1aEu5sWIQ6LjGE4IX
|
|
33
|
+
1AJtOiNoaO9orjVgphA5FDKFFqguhEJrHoSXt3awHaeCYFm/mt+NI6k4sRf59UNx
|
|
34
|
+
hdGh0kVL/6jZncclxueFafOOdfPf+jF3ylTczQhnID7Mg1PdmjXR7cfq7jDdjzfW
|
|
35
|
+
VICJqGt+EhUd92pWrMTa5k3SDin3EetYGJsN7iKLbAl0D6Uy8RfE2mdsF2K6D3EP
|
|
36
|
+
9waHN7vLURCF//TGy84FxPy7vbFgLO4oT07lxaopuGX7vOdONI8qLcFhEAJ9fRVN
|
|
37
|
+
lqPWQGib0iAKE+gLl4CWZen674QZF39fGnOBEz7JsmDfAgMBAAE=
|
|
38
|
+
-----END PUBLIC KEY-----
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
private func registry() throws -> ScaleBunOtaKeyRegistry {
|
|
42
|
+
let json = "{\"projectId\":\"\(projectId)\",\"keys\":[{\"keyId\":\"\(keyId)\",\"publicKey\":\"\(spkiPem.replacingOccurrences(of: "\n", with: "\\n"))\"}]}"
|
|
43
|
+
return try ScaleBunOtaKeyRegistry.fromJson(json)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private func context(_ currentSequence: Int64 = 0) -> ExpectedContext {
|
|
47
|
+
ExpectedContext(projectId: projectId, applicationId: applicationId,
|
|
48
|
+
currentSequence: currentSequence, nowEpochSeconds: 1_767_225_660)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private func envelope(payload: String? = nil, sig: String? = nil) -> String {
|
|
52
|
+
"{\"protocol\":\"\(OtaProtocol.signatureAlgorithm)\",\"keyId\":\"\(keyId)\"," +
|
|
53
|
+
"\"signedPayload\":\"\(payload ?? signedPayloadB64)\",\"signature\":\"\(sig ?? signatureB64)\"}"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testGenuineReleaseVerifies() throws {
|
|
57
|
+
let v = ScaleBunOtaReleaseVerifier.verify(envelopeJson: envelope(), expected: context(), registry: try registry())
|
|
58
|
+
XCTAssertTrue(v.ok, "expected ok, got \(String(describing: v.code))")
|
|
59
|
+
XCTAssertEqual(v.payload?.bundleId, "bndl_full_0001")
|
|
60
|
+
XCTAssertEqual(v.payload?.releaseSequence, 42)
|
|
61
|
+
XCTAssertEqual(v.payload?.platform, "android") // vector is an android full release
|
|
62
|
+
XCTAssertEqual(v.payload?.artifactSha256, v.payload?.resultArtifactSha256)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func testPlatformMismatchRejected() throws {
|
|
66
|
+
// Device context is iOS by default; the vector payload is android.
|
|
67
|
+
var ctx = context(); ctx.platform = "ios"
|
|
68
|
+
let v = ScaleBunOtaReleaseVerifier.verify(envelopeJson: envelope(), expected: ctx, registry: try registry())
|
|
69
|
+
XCTAssertEqual(v.code, .PLATFORM_MISMATCH)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func testTamperedPayloadRejected() throws {
|
|
73
|
+
let bad = String(signedPayloadB64.dropLast(2)) + (signedPayloadB64.hasSuffix("M=") ? "N=" : "M=")
|
|
74
|
+
var ctx = context(); ctx.platform = "android"
|
|
75
|
+
let v = ScaleBunOtaReleaseVerifier.verify(envelopeJson: envelope(payload: bad), expected: ctx, registry: try registry())
|
|
76
|
+
XCTAssertFalse(v.ok)
|
|
77
|
+
XCTAssertTrue(v.code == .INVALID_SIGNATURE || v.code == .INVALID_BASE64)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func testUnknownKeyRejected() throws {
|
|
81
|
+
let empty = try ScaleBunOtaKeyRegistry.fromJson("{\"projectId\":\"\(projectId)\",\"keys\":[]}")
|
|
82
|
+
var ctx = context(); ctx.platform = "android"
|
|
83
|
+
let v = ScaleBunOtaReleaseVerifier.verify(envelopeJson: envelope(), expected: ctx, registry: empty)
|
|
84
|
+
XCTAssertEqual(v.code, .UNKNOWN_KEY_ID)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func testDowngradeRejected() throws {
|
|
88
|
+
var ctx = context(100); ctx.platform = "android"
|
|
89
|
+
let v = ScaleBunOtaReleaseVerifier.verify(envelopeJson: envelope(), expected: ctx, registry: try registry())
|
|
90
|
+
XCTAssertEqual(v.code, .RELEASE_DOWNGRADE_REJECTED)
|
|
91
|
+
}
|
|
92
|
+
}
|
package/ios/ReplaySdk.swift
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import React
|
|
2
3
|
import UIKit
|
|
3
4
|
|
|
4
5
|
/// Main ReplaySdk native module (RCTEventEmitter subclass).
|
|
@@ -32,12 +33,57 @@ class ReplaySdk: RCTEventEmitter {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/// Attach native touch tracking to the key window.
|
|
35
|
-
///
|
|
36
|
+
///
|
|
37
|
+
/// RETRIED, AND RE-ATTACHED ON FOREGROUND — because the previous one-shot version is the most
|
|
38
|
+
/// likely explanation for iOS capturing coordinates on 0.4% of interactions where Android
|
|
39
|
+
/// manages 15.3%.
|
|
40
|
+
///
|
|
41
|
+
/// It ran exactly once, 500 ms after `initialize()`. If `getKeyWindow()` returned nil at that
|
|
42
|
+
/// instant it logged a warning and never tried again, so native touch tracking was off for the
|
|
43
|
+
/// WHOLE app session. A cold start behind a heavy JS bundle, a slow device, or a scene-based
|
|
44
|
+
/// app whose key window is not the one that existed at +500 ms — any of those and no touch is
|
|
45
|
+
/// ever tracked. Losing that race once lost it permanently.
|
|
46
|
+
///
|
|
47
|
+
/// Android never had the problem because it attaches from a lifecycle hook
|
|
48
|
+
/// (`ReplaySdkModule.onHostResume`), so every foreground corrects an attach that was too early.
|
|
49
|
+
/// This mirrors that: bounded retries for the cold-start race, plus a foreground observer that
|
|
50
|
+
/// attaches if a previous attempt never succeeded. `attach` is idempotent —
|
|
51
|
+
/// `guard !isActive else { return }` — so a redundant call costs nothing and cannot install a
|
|
52
|
+
/// second gesture recognizer.
|
|
53
|
+
///
|
|
54
|
+
/// WHAT THIS DOES NOT FIX, stated because that same guard is why: a key window that CHANGES
|
|
55
|
+
/// after a successful attach. `attach` returns early on `isActive` without comparing the
|
|
56
|
+
/// window, so the tracker stays bound to the original one and the foreground observer cannot
|
|
57
|
+
/// help. That needs a detach-and-rebind inside `InteractionTracker`, which is a larger change
|
|
58
|
+
/// than the race this addresses. The cold-start race is the dominant cause — it costs the whole
|
|
59
|
+
/// session every time it is lost — and it is fixed here; the window-change case remains open.
|
|
36
60
|
private func activateNativeTracking() {
|
|
61
|
+
scheduleAttach(attemptsRemaining: 10)
|
|
62
|
+
NotificationCenter.default.addObserver(
|
|
63
|
+
self,
|
|
64
|
+
selector: #selector(reattachNativeTracking),
|
|
65
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
66
|
+
object: nil
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Try to attach, and try again shortly if there is no key window yet.
|
|
71
|
+
///
|
|
72
|
+
/// Ten attempts at 500 ms covers about five seconds of startup, which is far longer than a
|
|
73
|
+
/// window takes to appear and short enough that a genuinely window-less process stops asking.
|
|
74
|
+
/// The last failure is logged at warn so a host that never gets tracking is told, rather than
|
|
75
|
+
/// discovering it as an empty heatmap.
|
|
76
|
+
private func scheduleAttach(attemptsRemaining: Int) {
|
|
37
77
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
38
78
|
guard let self = self else { return }
|
|
39
79
|
guard let window = Self.getKeyWindow() else {
|
|
40
|
-
|
|
80
|
+
if attemptsRemaining > 1 {
|
|
81
|
+
self.scheduleAttach(attemptsRemaining: attemptsRemaining - 1)
|
|
82
|
+
} else {
|
|
83
|
+
ReplayLogger.warn(
|
|
84
|
+
"Native tracking: no key window after repeated attempts — touch capture is off"
|
|
85
|
+
)
|
|
86
|
+
}
|
|
41
87
|
return
|
|
42
88
|
}
|
|
43
89
|
self.interactionTracker.attach(to: window, sendEvent: { [weak self] name, payload in
|
|
@@ -46,6 +92,23 @@ class ReplaySdk: RCTEventEmitter {
|
|
|
46
92
|
}
|
|
47
93
|
}
|
|
48
94
|
|
|
95
|
+
/// Re-attach when the app becomes active, mirroring Android's `onHostResume`.
|
|
96
|
+
@objc private func reattachNativeTracking() {
|
|
97
|
+
guard let window = Self.getKeyWindow() else { return }
|
|
98
|
+
interactionTracker.attach(to: window, sendEvent: { [weak self] name, payload in
|
|
99
|
+
self?.sendEvent(withName: name, body: payload)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Matches what the other two observer-registering modules here do
|
|
104
|
+
/// (`SessionManager`, `ScaleBunProfilerModule`). A native module usually outlives the app's
|
|
105
|
+
/// interesting lifetime and selector observers have been zeroing-weak since iOS 9, so this is
|
|
106
|
+
/// hygiene rather than a crash fix — but being the one file that registers and never removes is
|
|
107
|
+
/// how a convention quietly stops being one.
|
|
108
|
+
deinit {
|
|
109
|
+
NotificationCenter.default.removeObserver(self)
|
|
110
|
+
}
|
|
111
|
+
|
|
49
112
|
private static func getKeyWindow() -> UIWindow? {
|
|
50
113
|
return WindowResolver.keyWindow()
|
|
51
114
|
}
|
|
@@ -80,6 +80,15 @@ class SDKBootstrapper {
|
|
|
80
80
|
// apiBaseUrl is LOCKED: whatever an integrator passed (or the schema default) is
|
|
81
81
|
// discarded here so the SDK can never be repointed away from the ScaleBun cloud.
|
|
82
82
|
// Change DEFAULT_API_BASE_URL at build time to target a different backend.
|
|
83
|
+
// Say so when we discard it. The value was accepted by the schema, silently
|
|
84
|
+
// overwritten here, and the SDK then talked to a different server than the
|
|
85
|
+
// integrator configured — with no log, no warning, and no error. Someone
|
|
86
|
+
// pointing the SDK at a staging or local backend would watch requests arrive
|
|
87
|
+
// at production and have nothing to go on.
|
|
88
|
+
const requestedApiBaseUrl = rawConfig?.apiBaseUrl;
|
|
89
|
+
if (typeof requestedApiBaseUrl === 'string' && requestedApiBaseUrl && requestedApiBaseUrl !== _endpoints.DEFAULT_API_BASE_URL) {
|
|
90
|
+
_internalLogger.logger.warn(`[ScaleBun] Ignoring apiBaseUrl '${requestedApiBaseUrl}' — the endpoint is ` + `locked at build time and this build targets ${_endpoints.DEFAULT_API_BASE_URL}. To ` + `target another backend, change DEFAULT_API_BASE_URL in the SDK source and ` + `rebuild.`);
|
|
91
|
+
}
|
|
83
92
|
this.config.apiBaseUrl = _endpoints.DEFAULT_API_BASE_URL;
|
|
84
93
|
// Explicit logLevel wins; otherwise debug:true implies verbose 'debug'.
|
|
85
94
|
if (this.config.logLevel) {
|
|
@@ -319,6 +328,27 @@ class SDKBootstrapper {
|
|
|
319
328
|
await this.featureRegistry.initializeAll(featureContext);
|
|
320
329
|
|
|
321
330
|
// ── Session Replay Initialization (Core) ───────────────
|
|
331
|
+
/**
|
|
332
|
+
* RN-3 zero-config scroll instrumentation, opt-in.
|
|
333
|
+
*
|
|
334
|
+
* Placed before replay init because it patches a module export and must be in place before the
|
|
335
|
+
* host renders its first scroll view. Reports rather than assumes: the equivalent React
|
|
336
|
+
* Navigation hook once claimed success it had not verified and left a whole pipeline silently
|
|
337
|
+
* empty, so a failure here is said out loud with a reason an integrator can act on.
|
|
338
|
+
*/
|
|
339
|
+
if (this.config.autoInstrumentScrollViews === true) {
|
|
340
|
+
try {
|
|
341
|
+
const {
|
|
342
|
+
autoInstrumentScrollViews
|
|
343
|
+
} = require('../features/journey/autoInstrumentScroll');
|
|
344
|
+
const outcome = autoInstrumentScrollViews();
|
|
345
|
+
if (!outcome.installed) {
|
|
346
|
+
_internalLogger.logger.warn('[Bootstrap] autoInstrumentScrollViews was requested but could not be ' + 'installed: ' + outcome.reason + ' — taps will carry no content depth.');
|
|
347
|
+
} else {
|
|
348
|
+
__DEV__ && _internalLogger.logger.debug('[Bootstrap] ScrollView auto-instrumentation installed');
|
|
349
|
+
}
|
|
350
|
+
} catch {/* no-throw */}
|
|
351
|
+
}
|
|
322
352
|
if (features.replay !== false) {
|
|
323
353
|
try {
|
|
324
354
|
__DEV__ && _internalLogger.logger.debug('[Bootstrap] Initializing unified SessionManager');
|
|
@@ -275,6 +275,22 @@ const SHAPE = {
|
|
|
275
275
|
}
|
|
276
276
|
},
|
|
277
277
|
captureInteractionHeatmap: bool(true),
|
|
278
|
+
/**
|
|
279
|
+
* RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
|
|
280
|
+
* offset, not just ones wrapped in ScaleBunScrollView.
|
|
281
|
+
*
|
|
282
|
+
* DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
|
|
283
|
+
* That flag changes what the SDK records about the host; this one changes what the HOST
|
|
284
|
+
* RENDERS — every scroll view in the app, including ones inside third-party libraries the app
|
|
285
|
+
* did not write, starts routing through an SDK component. The failure mode is not a wrong
|
|
286
|
+
* number in a dashboard, it is the host's UI behaving differently because an analytics SDK
|
|
287
|
+
* replaced a primitive. That is a decision an integrator makes deliberately, not one they
|
|
288
|
+
* discover afterwards.
|
|
289
|
+
*
|
|
290
|
+
* With it off, a plain <ScrollView> yields no content depth and the tap says so
|
|
291
|
+
* (scroll_source: 'none') rather than defaulting its offset to 0.
|
|
292
|
+
*/
|
|
293
|
+
autoInstrumentScrollViews: bool(false),
|
|
278
294
|
// Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
|
|
279
295
|
// mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
|
|
280
296
|
// so capturing it is the host's explicit choice, not a silent default. When true and a
|
|
@@ -291,18 +307,9 @@ const SHAPE = {
|
|
|
291
307
|
kind: 'str',
|
|
292
308
|
opt: true
|
|
293
309
|
},
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
},
|
|
298
|
-
// Rotation support: several pinned keys, any of which may verify a
|
|
299
|
-
// bundle. Ship a build pinning [old, new], re-sign server-side with
|
|
300
|
-
// new, drop old next release — no emergency store submission when a
|
|
301
|
-
// key must be replaced. Merged with publicSigningKey by the facade.
|
|
302
|
-
publicSigningKeys: {
|
|
303
|
-
kind: 'strArr',
|
|
304
|
-
opt: true
|
|
305
|
-
},
|
|
310
|
+
// No signing-key fields: signed releases are verified natively against
|
|
311
|
+
// RSA keys embedded in the app package (SB-OTA-RSA-SHA256-V1). JS must
|
|
312
|
+
// not supply a trusted key.
|
|
306
313
|
mandatoryBlocksUi: bool(false)
|
|
307
314
|
}
|
|
308
315
|
}
|
|
@@ -14,5 +14,5 @@ exports.SDK_VERSION = void 0;
|
|
|
14
14
|
* value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
|
|
15
15
|
* version was introduced to solve.
|
|
16
16
|
*/
|
|
17
|
-
const SDK_VERSION = exports.SDK_VERSION = '
|
|
17
|
+
const SDK_VERSION = exports.SDK_VERSION = '2.0.1';
|
|
18
18
|
//# sourceMappingURL=version.js.map
|