@scalebun/react-native 1.10.6 → 1.11.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 (75) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -24
  2. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +75 -0
  3. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +12 -0
  4. package/dist/scalebun.full.js +653 -223
  5. package/dist/scalebun.slim.js +652 -222
  6. package/ios/Capture/InteractionTracker.swift +8 -4
  7. package/ios/Ota/ScaleBunOtaBridge.mm +6 -0
  8. package/ios/Ota/ScaleBunOtaModule.swift +67 -0
  9. package/lib/commonjs/analytics/EventTracker.js +5 -5
  10. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  11. package/lib/commonjs/core/config/schema.js +34 -1
  12. package/lib/commonjs/core/constants/version.js +7 -2
  13. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -79
  14. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  15. package/lib/commonjs/features/journey/uiState.js +94 -0
  16. package/lib/commonjs/features/ota/crypto/builtinVerifier.js +248 -0
  17. package/lib/commonjs/features/ota/crypto/loadEd25519.js +40 -0
  18. package/lib/commonjs/features/ota/crypto/loadSha512.js +40 -0
  19. package/lib/commonjs/features/ota/crypto/nativeVerifier.js +121 -0
  20. package/lib/commonjs/features/ota/signature.js +87 -27
  21. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  22. package/lib/commonjs/features/session/SessionManager.js +37 -38
  23. package/lib/commonjs/metro/serializerCompose.js +32 -0
  24. package/lib/commonjs/public/ScaleBunFacade.js +74 -11
  25. package/lib/module/analytics/EventTracker.js +5 -5
  26. package/lib/module/analytics/automaticEvents.js +3 -2
  27. package/lib/module/core/config/schema.js +34 -1
  28. package/lib/module/core/constants/version.js +7 -2
  29. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -79
  30. package/lib/module/features/journey/interactionProtocol.js +38 -0
  31. package/lib/module/features/journey/uiState.js +86 -0
  32. package/lib/module/features/ota/crypto/builtinVerifier.js +240 -0
  33. package/lib/module/features/ota/crypto/loadEd25519.js +34 -0
  34. package/lib/module/features/ota/crypto/loadSha512.js +34 -0
  35. package/lib/module/features/ota/crypto/nativeVerifier.js +113 -0
  36. package/lib/module/features/ota/signature.js +87 -27
  37. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  38. package/lib/module/features/session/SessionManager.js +37 -38
  39. package/lib/module/metro/serializerCompose.js +32 -0
  40. package/lib/module/public/ScaleBunFacade.js +74 -11
  41. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  42. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  43. package/lib/typescript/core/config/schema.d.ts +2 -0
  44. package/lib/typescript/core/constants/version.d.ts +7 -2
  45. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  46. package/lib/typescript/features/journey/uiState.d.ts +53 -0
  47. package/lib/typescript/features/ota/OtaTypes.d.ts +50 -0
  48. package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +53 -0
  49. package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +30 -0
  50. package/lib/typescript/features/ota/crypto/loadSha512.d.ts +15 -0
  51. package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +35 -0
  52. package/lib/typescript/features/ota/signature.d.ts +22 -7
  53. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  54. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  55. package/lib/typescript/public/ScaleBunFacade.d.ts +35 -6
  56. package/lib/typescript/specs/NativeScaleBunOta.d.ts +23 -0
  57. package/package.json +19 -3
  58. package/src/analytics/EventTracker.ts +5 -5
  59. package/src/analytics/automaticEvents.ts +4 -0
  60. package/src/core/config/schema.ts +30 -3
  61. package/src/core/constants/version.ts +7 -2
  62. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -75
  63. package/src/features/journey/interactionProtocol.ts +65 -0
  64. package/src/features/journey/uiState.ts +89 -0
  65. package/src/features/ota/OtaTypes.ts +51 -0
  66. package/src/features/ota/crypto/builtinVerifier.ts +257 -0
  67. package/src/features/ota/crypto/loadEd25519.ts +41 -0
  68. package/src/features/ota/crypto/loadSha512.ts +35 -0
  69. package/src/features/ota/crypto/nativeVerifier.ts +117 -0
  70. package/src/features/ota/signature.ts +108 -25
  71. package/src/features/session/JourneyEventPipeline.ts +7 -5
  72. package/src/features/session/SessionManager.ts +75 -38
  73. package/src/metro/serializerCompose.ts +38 -2
  74. package/src/public/ScaleBunFacade.ts +87 -13
  75. package/src/specs/NativeScaleBunOta.ts +24 -0
@@ -9,6 +9,7 @@ import android.view.Window
9
9
  import android.view.WindowManager
10
10
  import java.lang.ref.WeakReference
11
11
  import java.util.WeakHashMap
12
+ import java.util.UUID
12
13
  import com.facebook.react.bridge.Arguments
13
14
  import com.facebook.react.bridge.ReactApplicationContext
14
15
  import com.facebook.react.modules.core.DeviceEventManagerModule
@@ -34,13 +35,10 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
34
35
  private var touchDownX = 0f
35
36
  private var touchDownY = 0f
36
37
  private var touchDownTs = 0L
37
- private var lastEmitTs = 0L
38
- private val THROTTLE_MS = 150L
39
- // Same-coordinate duplicate window — see handleTouchUp. Wider than THROTTLE_MS on purpose:
40
- // it only ever suppresses a bit-identical re-delivery, never a real second tap.
41
- private val SAME_COORD_WINDOW_MS = 500L
42
- private var lastEmitRawX = Float.NaN
43
- private var lastEmitRawY = Float.NaN
38
+ private var touchDownOccurredAt = 0L
39
+ private var touchInteractionId = ""
40
+ private var lastEmittedInteractionId = ""
41
+ private val processInteractionNonce = UUID.randomUUID().toString().take(8)
44
42
  private val mainHandler = Handler(Looper.getMainLooper())
45
43
 
46
44
  fun attach(activity: Activity) {
@@ -63,6 +61,9 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
63
61
  touchDownX = rawX
64
62
  touchDownY = rawY
65
63
  touchDownTs = ts
64
+ val id = "ixn-a-$processInteractionNonce-$ts"
65
+ if (id != touchInteractionId) touchDownOccurredAt = System.currentTimeMillis()
66
+ touchInteractionId = id
66
67
  },
67
68
  onUp = { rawX, rawY, ts ->
68
69
  handleTouchUp(rawX, rawY, ts, resolveCaptureView(window))
@@ -86,6 +87,9 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
86
87
  touchDownX = rawX
87
88
  touchDownY = rawY
88
89
  touchDownTs = ts
90
+ val id = "ixn-a-$processInteractionNonce-$ts"
91
+ if (id != touchInteractionId) touchDownOccurredAt = System.currentTimeMillis()
92
+ touchInteractionId = id
89
93
  },
90
94
  onUp = { rawX, rawY, ts ->
91
95
  val main = attachedWindow ?: return@DialogTouchObserver
@@ -137,21 +141,11 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
137
141
  ReplayLogger.debug("Native touch up dropped: no usable capture view")
138
142
  return
139
143
  }
140
- val now = System.currentTimeMillis()
141
- if (now - lastEmitTs < THROTTLE_MS) return
142
- // Same-coordinate suppression: the same UP can be re-delivered through more than one
143
- // hooked window callback at gaps just past THROTTLE_MS (measured in production:
144
- // identical sub-pixel coords 158–240ms apart, recorded as 2–3 taps for one press).
145
- // Bit-identical float coordinates within half a second cannot be a second physical
146
- // tap — a real double-tap lands on at least slightly different pixels — so this
147
- // drops only machine duplicates. Distinct-coordinate taps still pass at THROTTLE_MS.
148
- if (rawX == lastEmitRawX && rawY == lastEmitRawY && now - lastEmitTs < SAME_COORD_WINDOW_MS) {
149
- lastEmitTs = now // keep a chained burst suppressed
150
- return
151
- }
152
- lastEmitTs = now
153
- lastEmitRawX = rawX
154
- lastEmitRawY = rawY
144
+ // Window/dialog hooks can re-deliver one physical touch. The ACTION_DOWN identity is stable
145
+ // across those deliveries, unlike coordinates: two legitimate rapid taps may land on the
146
+ // exact same pixel and must both survive.
147
+ if (touchInteractionId.isNotEmpty() && touchInteractionId == lastEmittedInteractionId) return
148
+ lastEmittedInteractionId = touchInteractionId
155
149
 
156
150
  val durationMs = ts - touchDownTs
157
151
  val dx = rawX - touchDownX
@@ -215,6 +209,8 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
215
209
  screenshotW = viewW,
216
210
  screenshotH = viewH,
217
211
  durationMs = durationMs,
212
+ interactionId = touchInteractionId,
213
+ occurredAt = touchDownOccurredAt,
218
214
  onDialogWindow = onDialogWindow
219
215
  )
220
216
  }
@@ -226,11 +222,16 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
226
222
  normalizedEndX: Float, normalizedEndY: Float,
227
223
  screenshotW: Float, screenshotH: Float,
228
224
  durationMs: Long,
225
+ interactionId: String,
226
+ occurredAt: Long,
229
227
  onDialogWindow: Boolean = false
230
228
  ) {
231
229
  try {
232
230
  val params = Arguments.createMap().apply {
233
231
  putString("gestureType", gestureType)
232
+ putString("interactionId", interactionId)
233
+ putInt("interactionProtocol", 1)
234
+ putDouble("occurredAt", occurredAt.toDouble())
234
235
  putDouble("rawX", rawX.toDouble())
235
236
  putDouble("rawY", rawY.toDouble())
236
237
  putDouble("normalizedX", normalizedX.toDouble())
@@ -276,8 +277,8 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
276
277
  fun reset() {
277
278
  lastInteractionMs = 0
278
279
  lastEmitTs = 0
279
- lastEmitRawX = Float.NaN
280
- lastEmitRawY = Float.NaN
280
+ touchInteractionId = ""
281
+ lastEmittedInteractionId = ""
281
282
  touchDownTs = 0
282
283
  }
283
284
  }
@@ -283,6 +283,81 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
283
283
  }
284
284
  }
285
285
 
286
+ /**
287
+ * Verify a detached ed25519 signature with PLATFORM crypto.
288
+ *
289
+ * Why native: the JS verifier lives inside the very bundle it protects, so
290
+ * one malicious bundle can neuter it for every update after. Platform
291
+ * crypto sits outside the bundle's reach. It also drops the runtime need
292
+ * for the optional @noble peers on Android 13+.
293
+ *
294
+ * Contract (mirrors the JS spec doc):
295
+ * - inputs are lowercase hex, pre-validated by the JS caller
296
+ * - the message is the RAW 32 BYTES of the bundle SHA-256 — the CLI
297
+ * signs the digest bytes, not their hex text
298
+ * - resolves 'valid' / 'invalid' / 'unavailable'; never rejects for a
299
+ * wrong signature, and any unexpected throw resolves 'unavailable' so
300
+ * machinery failure is distinguishable from a forged bundle
301
+ *
302
+ * Availability: `Signature.getInstance("Ed25519")` ships in Android's
303
+ * conscrypt from API 33. Below that the JCA lookup throws
304
+ * NoSuchAlgorithmException — reported as 'unavailable', and JS falls back
305
+ * to its own verifier. Checked by attempting the lookup rather than by
306
+ * SDK_INT, because the algorithm's presence is the fact that matters.
307
+ */
308
+ @ReactMethod
309
+ override fun verifyEd25519(
310
+ messageHex: String,
311
+ signatureHex: String,
312
+ publicKeyHex: String,
313
+ promise: Promise,
314
+ ) {
315
+ try {
316
+ val message = hexToBytes(messageHex)
317
+ val signature = hexToBytes(signatureHex)
318
+ val rawKey = hexToBytes(publicKeyHex)
319
+ if (message == null || signature == null || rawKey == null || rawKey.size != 32) {
320
+ // Malformed input from our own caller is not a forgery verdict.
321
+ promise.resolve("unavailable")
322
+ return
323
+ }
324
+
325
+ // JCA takes X.509 SubjectPublicKeyInfo, not raw key bytes: the
326
+ // 12-byte ed25519 SPKI prefix (OID 1.3.101.112) + the raw 32.
327
+ val spkiPrefix = byteArrayOf(
328
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
329
+ )
330
+ val verifier = try {
331
+ java.security.Signature.getInstance("Ed25519")
332
+ } catch (_: java.security.NoSuchAlgorithmException) {
333
+ promise.resolve("unavailable") // API < 33
334
+ return
335
+ }
336
+ val key = java.security.KeyFactory.getInstance("Ed25519")
337
+ .generatePublic(java.security.spec.X509EncodedKeySpec(spkiPrefix + rawKey))
338
+
339
+ verifier.initVerify(key)
340
+ verifier.update(message)
341
+ promise.resolve(if (verifier.verify(signature)) "valid" else "invalid")
342
+ } catch (_: Throwable) {
343
+ // Fail toward "cannot check", never toward "checked out fine".
344
+ promise.resolve("unavailable")
345
+ }
346
+ }
347
+
348
+ /** Hex → bytes; null for odd length or a non-hex character. */
349
+ private fun hexToBytes(hex: String): ByteArray? {
350
+ if (hex.isEmpty() || hex.length % 2 != 0) return null
351
+ val out = ByteArray(hex.length / 2)
352
+ for (i in out.indices) {
353
+ val hi = Character.digit(hex[i * 2], 16)
354
+ val lo = Character.digit(hex[i * 2 + 1], 16)
355
+ if (hi < 0 || lo < 0) return null
356
+ out[i] = ((hi shl 4) or lo).toByte()
357
+ }
358
+ return out
359
+ }
360
+
286
361
  private fun sha256Hex(bytes: ByteArray): String {
287
362
  val digest = java.security.MessageDigest.getInstance("SHA-256")
288
363
  val hash = digest.digest(bytes)
@@ -28,4 +28,16 @@ abstract class ScaleBunOtaSpec(reactContext: ReactApplicationContext) :
28
28
  abstract fun markHealthy()
29
29
  abstract fun revertToPrevious(promise: Promise)
30
30
  abstract fun restartApp()
31
+
32
+ // Hand-maintained: this file does NOT regenerate from the JS spec. A method
33
+ // added to NativeScaleBunOta.ts that is missing here compiles green on New
34
+ // Arch and breaks EVERY old-arch consumer's build (the ScaleBunCrashModule
35
+ // incident). Non-null Strings to match the codegen base's signature, so the
36
+ // one concrete override compiles against either architecture.
37
+ abstract fun verifyEd25519(
38
+ messageHex: String,
39
+ signatureHex: String,
40
+ publicKeyHex: String,
41
+ promise: Promise,
42
+ )
31
43
  }