@scalebun/react-native 2.0.0 → 2.0.2
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/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/ScaleBunOtaModule.kt +18 -3
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +62 -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 +2 -0
- package/dist/scalebun.full.js +840 -114
- package/dist/scalebun.slim.js +838 -112
- package/ios/ReplaySdk.swift +65 -2
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/commonjs/core/config/schema.js +16 -0
- 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/ota/OtaOrchestrator.js +69 -22
- package/lib/commonjs/features/session/SessionManager.js +63 -0
- package/lib/commonjs/public/ScaleBunFacade.js +18 -16
- package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/module/core/config/schema.js +16 -0
- 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/ota/OtaOrchestrator.js +69 -22
- package/lib/module/features/session/SessionManager.js +63 -0
- package/lib/module/public/ScaleBunFacade.js +18 -16
- package/lib/typescript/core/config/schema.d.ts +2 -0
- 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/ota/OtaOrchestrator.d.ts +7 -0
- package/lib/typescript/features/session/SessionManager.d.ts +55 -0
- package/lib/typescript/public/types.d.ts +10 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +2 -0
- package/package.json +2 -2
- package/src/bootstrap/SDKBootstrapper.ts +40 -0
- package/src/core/config/schema.ts +18 -0
- 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/ota/OtaOrchestrator.ts +76 -22
- package/src/features/session/SessionManager.ts +113 -0
- package/src/public/ScaleBunFacade.ts +18 -16
- package/src/public/types.ts +10 -0
- package/src/specs/NativeScaleBunOta.ts +3 -0
|
@@ -58,6 +58,7 @@ internal object BundleDownloader {
|
|
|
58
58
|
* bar overshoot 100% and walk backwards. Unknown is reported as unknown.
|
|
59
59
|
*/
|
|
60
60
|
fun download(
|
|
61
|
+
context: android.content.Context,
|
|
61
62
|
url: String,
|
|
62
63
|
destination: File,
|
|
63
64
|
onProgress: ((bytesRead: Long, totalBytes: Long) -> Unit)? = null,
|
|
@@ -79,6 +80,15 @@ internal object BundleDownloader {
|
|
|
79
80
|
setRequestProperty("Accept-Encoding", "gzip")
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// Certificate pinning, when the host app ships pins. Checked after the
|
|
84
|
+
// handshake but before a single byte of the body is read, and before any
|
|
85
|
+
// redirect is followed, so an intercepted endpoint cannot serve content or
|
|
86
|
+
// bounce us somewhere else. No pins configured = no behaviour change.
|
|
87
|
+
val https = connection as? javax.net.ssl.HttpsURLConnection
|
|
88
|
+
if (https != null && !TlsPinning.verify(context, https, current.host)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
val code = connection.responseCode
|
|
83
93
|
if (code == HttpURLConnection.HTTP_MOVED_PERM ||
|
|
84
94
|
code == HttpURLConnection.HTTP_MOVED_TEMP ||
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package com.scalebun.rn.ota
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import java.io.File
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Best-effort root detection.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately modest in what it claims. Root detection is a signal, never a
|
|
11
|
+
* boundary: anything running with root can defeat any check made from inside the
|
|
12
|
+
* app, and a determined attacker with a rooted device has already won that
|
|
13
|
+
* argument. What it IS good for is telling an operator that a fleet's telemetry
|
|
14
|
+
* — crash rates, adoption, rollback rates — includes devices whose OTA slot is
|
|
15
|
+
* writable by other processes, and letting a security-sensitive host app decide
|
|
16
|
+
* to withhold updates on those devices.
|
|
17
|
+
*
|
|
18
|
+
* The checks are the cheap, well-understood ones: known su binaries, the
|
|
19
|
+
* test-keys build tag, and the presence of common management packages. No
|
|
20
|
+
* heuristics that produce false positives on ordinary devices, because a false
|
|
21
|
+
* positive here means a paying customer stops receiving updates.
|
|
22
|
+
*/
|
|
23
|
+
object DeviceIntegrity {
|
|
24
|
+
|
|
25
|
+
private val SU_PATHS = arrayOf(
|
|
26
|
+
"/system/bin/su",
|
|
27
|
+
"/system/xbin/su",
|
|
28
|
+
"/sbin/su",
|
|
29
|
+
"/system/su",
|
|
30
|
+
"/vendor/bin/su",
|
|
31
|
+
"/data/local/xbin/su",
|
|
32
|
+
"/data/local/bin/su",
|
|
33
|
+
"/system/sd/xbin/su",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
private val MANAGEMENT_PACKAGES = arrayOf(
|
|
37
|
+
"com.topjohnwu.magisk",
|
|
38
|
+
"eu.chainfire.supersu",
|
|
39
|
+
"com.noshufou.android.su",
|
|
40
|
+
"com.koushikdutta.superuser",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reasons this device looks rooted. Empty list = nothing detected, which is
|
|
45
|
+
* NOT the same as "device is clean" — see the class comment.
|
|
46
|
+
*/
|
|
47
|
+
fun rootIndicators(context: Context): List<String> {
|
|
48
|
+
val found = mutableListOf<String>()
|
|
49
|
+
|
|
50
|
+
if (SU_PATHS.any { runCatching { File(it).exists() }.getOrDefault(false) }) {
|
|
51
|
+
found.add("su_binary_present")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Release builds signed with Android's public test keys are development
|
|
55
|
+
// images, not retail firmware.
|
|
56
|
+
if (Build.TAGS?.contains("test-keys") == true) {
|
|
57
|
+
found.add("test_keys_build")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
val pm = context.packageManager
|
|
61
|
+
for (pkg in MANAGEMENT_PACKAGES) {
|
|
62
|
+
val installed = runCatching {
|
|
63
|
+
@Suppress("DEPRECATION")
|
|
64
|
+
pm.getPackageInfo(pkg, 0)
|
|
65
|
+
true
|
|
66
|
+
}.getOrDefault(false)
|
|
67
|
+
if (installed) {
|
|
68
|
+
found.add("root_manager_installed")
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return found
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -154,6 +154,21 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
154
154
|
return slotManager.slotState().toString()
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Device integrity signals as a JSON array of reason strings.
|
|
159
|
+
*
|
|
160
|
+
* Reported, never enforced here: the SDK does not decide to withhold an
|
|
161
|
+
* update on a rooted device, because that is a product decision a host app
|
|
162
|
+
* makes with knowledge of its own users. Empty array means nothing was
|
|
163
|
+
* detected, which is not proof the device is clean.
|
|
164
|
+
*/
|
|
165
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
166
|
+
override fun getDeviceIntegrity(): String {
|
|
167
|
+
return org.json.JSONArray(
|
|
168
|
+
DeviceIntegrity.rootIndicators(reactApplicationContext),
|
|
169
|
+
).toString()
|
|
170
|
+
}
|
|
171
|
+
|
|
157
172
|
/** JSON array of the trusted key IDs embedded in this app (SDK trust coverage). */
|
|
158
173
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
159
174
|
override fun getTrustedKeyIds(): String {
|
|
@@ -197,7 +212,7 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
197
212
|
if (isRemote) {
|
|
198
213
|
val temp = File(reactApplicationContext.cacheDir, "scalebun_ota_dl_${System.nanoTime()}")
|
|
199
214
|
downloaded = temp
|
|
200
|
-
if (!BundleDownloader.download(sourcePath, temp, ::emitProgress)) {
|
|
215
|
+
if (!BundleDownloader.download(reactApplicationContext, sourcePath, temp, ::emitProgress)) {
|
|
201
216
|
promise.resolve(false)
|
|
202
217
|
return
|
|
203
218
|
}
|
|
@@ -274,7 +289,7 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
274
289
|
|
|
275
290
|
val temp = File(reactApplicationContext.cacheDir, "scalebun_ota_patch_${System.nanoTime()}")
|
|
276
291
|
downloadedPatch = temp
|
|
277
|
-
if (!BundleDownloader.download(patchUrl, temp, ::emitProgress)) {
|
|
292
|
+
if (!BundleDownloader.download(reactApplicationContext, patchUrl, temp, ::emitProgress)) {
|
|
278
293
|
promise.resolve(false)
|
|
279
294
|
return
|
|
280
295
|
}
|
|
@@ -378,7 +393,7 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
378
393
|
|
|
379
394
|
val temp = File(reactApplicationContext.cacheDir, "scalebun_ota_dl_${System.nanoTime()}")
|
|
380
395
|
downloaded = temp
|
|
381
|
-
if (!BundleDownloader.download(url, temp, ::emitProgress)) {
|
|
396
|
+
if (!BundleDownloader.download(reactApplicationContext, url, temp, ::emitProgress)) {
|
|
382
397
|
promise.resolve(Verdict.fail(OtaFailureCode.DOWNLOAD_FAILED, "download failed").toJson())
|
|
383
398
|
return
|
|
384
399
|
}
|
|
@@ -70,7 +70,68 @@ class SlotManager(private val filesDir: File) {
|
|
|
70
70
|
*/
|
|
71
71
|
fun currentBundlePath(): File? {
|
|
72
72
|
val bundle = File(currentSlot, BUNDLE_FILENAME)
|
|
73
|
-
|
|
73
|
+
if (!bundle.exists()) return null
|
|
74
|
+
|
|
75
|
+
// Integrity re-check on execution.
|
|
76
|
+
//
|
|
77
|
+
// The signature and hash are verified when a bundle is STAGED. That
|
|
78
|
+
// protects the download path, but it says nothing about the bytes still
|
|
79
|
+
// being the ones we installed: on a rooted device, anything able to write
|
|
80
|
+
// into app-private storage can replace current/bundle after the fact and
|
|
81
|
+
// the old `exists()` check would happily execute it.
|
|
82
|
+
//
|
|
83
|
+
// meta.json already records the sha256 the slot manager installed, so the
|
|
84
|
+
// comparison costs one hash of the active bundle per process — not per
|
|
85
|
+
// call, hence the cache. On mismatch we refuse the slot and return null,
|
|
86
|
+
// which makes the caller fall back to the bundle compiled into the APK.
|
|
87
|
+
// Failing closed to a known-good baseline is the only safe answer here.
|
|
88
|
+
verifiedBundlePath?.let { return if (it.exists()) it else null }
|
|
89
|
+
if (integrityFailed) return null
|
|
90
|
+
|
|
91
|
+
val expected = readMeta(currentSlot)?.optString("sha256", "").orEmpty()
|
|
92
|
+
if (expected.isEmpty()) {
|
|
93
|
+
// Pre-integrity install, or metadata lost. Nothing to compare against;
|
|
94
|
+
// do not invent a failure for a bundle that may be perfectly good.
|
|
95
|
+
Log.w(TAG, "current/ has no recorded sha256 — skipping integrity re-check")
|
|
96
|
+
verifiedBundlePath = bundle
|
|
97
|
+
return bundle
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
val actual = try {
|
|
101
|
+
sha256(bundle)
|
|
102
|
+
} catch (e: Exception) {
|
|
103
|
+
Log.e(TAG, "Integrity re-check could not hash current/ — refusing the slot", e)
|
|
104
|
+
integrityFailed = true
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!constantTimeEquals(actual, expected)) {
|
|
109
|
+
Log.e(
|
|
110
|
+
TAG,
|
|
111
|
+
"INTEGRITY FAILURE — current/ bundle does not match the sha256 recorded " +
|
|
112
|
+
"at install (expected=${expected.take(12)}… actual=${actual.take(12)}…). " +
|
|
113
|
+
"The bundle was modified after installation. Falling back to the " +
|
|
114
|
+
"binary baseline and clearing the slot.",
|
|
115
|
+
)
|
|
116
|
+
integrityFailed = true
|
|
117
|
+
runCatching { currentSlot.deleteRecursively() }
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
verifiedBundlePath = bundle
|
|
122
|
+
return bundle
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Per-process memo so the active bundle is hashed once, not on every call. */
|
|
126
|
+
private var verifiedBundlePath: File? = null
|
|
127
|
+
private var integrityFailed = false
|
|
128
|
+
|
|
129
|
+
/** Length-independent comparison, so a mismatch leaks nothing through timing. */
|
|
130
|
+
private fun constantTimeEquals(a: String, b: String): Boolean {
|
|
131
|
+
if (a.length != b.length) return false
|
|
132
|
+
var diff = 0
|
|
133
|
+
for (i in a.indices) diff = diff or (a[i].code xor b[i].code)
|
|
134
|
+
return diff == 0
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
/**
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package com.scalebun.rn.ota
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Base64
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import java.security.MessageDigest
|
|
7
|
+
import java.security.cert.X509Certificate
|
|
8
|
+
import javax.net.ssl.HttpsURLConnection
|
|
9
|
+
import org.json.JSONObject
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Optional certificate pinning for the OTA artifact download.
|
|
13
|
+
*
|
|
14
|
+
* Pins are **SPKI SHA-256 hashes**, not certificates. Pinning a certificate
|
|
15
|
+
* breaks on every renewal even when the key is unchanged, which is how pinning
|
|
16
|
+
* earns its reputation for bricking apps; pinning the public key survives
|
|
17
|
+
* renewal and only needs rotating when the key itself rotates.
|
|
18
|
+
*
|
|
19
|
+
* Configuration is a native asset, `scalebun_tls_pins.json`, alongside the
|
|
20
|
+
* trusted-key registry — JavaScript must not be able to introduce or remove a
|
|
21
|
+
* pin, for the same reason it cannot introduce a signing key:
|
|
22
|
+
*
|
|
23
|
+
* { "pins": {
|
|
24
|
+
* "api.scalebun.com": ["sha256/AAAA…", "sha256/BBBB…"]
|
|
25
|
+
* }}
|
|
26
|
+
*
|
|
27
|
+
* **Absent asset = pinning disabled.** That is deliberate. A pin that ships
|
|
28
|
+
* wrong cannot be fixed over the air — it takes a store release — so this is
|
|
29
|
+
* opt-in, and refuses to arm with fewer than two pins for a host: one live, one
|
|
30
|
+
* backup. An operator who pins a single key has built a single point of failure
|
|
31
|
+
* that a routine key rotation will detonate.
|
|
32
|
+
*/
|
|
33
|
+
object TlsPinning {
|
|
34
|
+
|
|
35
|
+
private const val TAG = "ScaleBunOta"
|
|
36
|
+
private const val ASSET = "scalebun_tls_pins.json"
|
|
37
|
+
private const val MIN_PINS = 2
|
|
38
|
+
|
|
39
|
+
private var loaded = false
|
|
40
|
+
private var pinsByHost: Map<String, Set<String>> = emptyMap()
|
|
41
|
+
|
|
42
|
+
@Synchronized
|
|
43
|
+
private fun load(context: Context) {
|
|
44
|
+
if (loaded) return
|
|
45
|
+
loaded = true
|
|
46
|
+
try {
|
|
47
|
+
val json = context.assets.open(ASSET).use {
|
|
48
|
+
it.readBytes().toString(Charsets.UTF_8)
|
|
49
|
+
}
|
|
50
|
+
val root = JSONObject(json).optJSONObject("pins") ?: return
|
|
51
|
+
val map = mutableMapOf<String, Set<String>>()
|
|
52
|
+
for (host in root.keys()) {
|
|
53
|
+
val arr = root.optJSONArray(host) ?: continue
|
|
54
|
+
val set = (0 until arr.length()).mapNotNull { arr.optString(it, null) }.toSet()
|
|
55
|
+
if (set.size < MIN_PINS) {
|
|
56
|
+
Log.e(
|
|
57
|
+
TAG,
|
|
58
|
+
"TLS pinning NOT armed for $host — ${set.size} pin(s) configured, " +
|
|
59
|
+
"at least $MIN_PINS required (a live pin and a backup). " +
|
|
60
|
+
"Shipping one pin means the next key rotation bricks the app.",
|
|
61
|
+
)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
map[host.lowercase()] = set
|
|
65
|
+
}
|
|
66
|
+
pinsByHost = map
|
|
67
|
+
if (map.isNotEmpty()) {
|
|
68
|
+
Log.i(TAG, "TLS pinning armed for: ${map.keys.joinToString(", ")}")
|
|
69
|
+
}
|
|
70
|
+
} catch (_: Exception) {
|
|
71
|
+
// No asset shipped: pinning is simply not in use.
|
|
72
|
+
pinsByHost = emptyMap()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True when this host has pins configured, so callers can log honestly. */
|
|
77
|
+
fun isPinned(context: Context, host: String): Boolean {
|
|
78
|
+
load(context)
|
|
79
|
+
return pinsByHost.containsKey(host.lowercase())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Verify an established HTTPS connection against the pins for its host.
|
|
84
|
+
*
|
|
85
|
+
* Returns true when the connection is acceptable — either because no pins
|
|
86
|
+
* are configured for that host, or because one of the certificates in the
|
|
87
|
+
* presented chain matches a pin. Returns false ONLY when pins exist and none
|
|
88
|
+
* matched, which is a hard failure the caller must treat as a failed
|
|
89
|
+
* download rather than retrying against the same endpoint.
|
|
90
|
+
*/
|
|
91
|
+
fun verify(context: Context, connection: HttpsURLConnection, host: String): Boolean {
|
|
92
|
+
load(context)
|
|
93
|
+
val expected = pinsByHost[host.lowercase()] ?: return true
|
|
94
|
+
|
|
95
|
+
val chain = try {
|
|
96
|
+
connection.serverCertificates
|
|
97
|
+
} catch (e: Exception) {
|
|
98
|
+
Log.e(TAG, "TLS pin check could not read the certificate chain for $host", e)
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (cert in chain) {
|
|
103
|
+
val x509 = cert as? X509Certificate ?: continue
|
|
104
|
+
val spki = x509.publicKey.encoded ?: continue
|
|
105
|
+
val digest = MessageDigest.getInstance("SHA-256").digest(spki)
|
|
106
|
+
val pin = "sha256/" + Base64.encodeToString(digest, Base64.NO_WRAP)
|
|
107
|
+
if (expected.contains(pin)) return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Log.e(
|
|
111
|
+
TAG,
|
|
112
|
+
"TLS PIN MISMATCH for $host — the presented certificate chain matches none " +
|
|
113
|
+
"of the ${expected.size} configured pins. Refusing the download. This is " +
|
|
114
|
+
"either an interception attempt or a rotated key whose pin was never shipped.",
|
|
115
|
+
)
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -18,6 +18,8 @@ abstract class ScaleBunOtaSpec(reactContext: ReactApplicationContext) :
|
|
|
18
18
|
abstract fun getActiveBundle(): String?
|
|
19
19
|
abstract fun getSlotState(): String
|
|
20
20
|
abstract fun getTrustedKeyIds(): String
|
|
21
|
+
|
|
22
|
+
abstract fun getDeviceIntegrity(): String
|
|
21
23
|
abstract fun stageBundle(sourcePath: String, sha256: String, promise: Promise)
|
|
22
24
|
abstract fun stagePatch(
|
|
23
25
|
patchUrl: String,
|