@scalebun/react-native 1.4.0 → 1.5.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.
- package/android/src/main/java/com/scalebun/rn/ota/BsPatch.kt +182 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +88 -13
- package/android/src/oldarch/java/com/scalebun/rn/crash/ScaleBunCrashSpec.kt +16 -0
- package/bin/scalebun.js +55 -3
- package/dist/scalebun.full.js +129 -97
- package/dist/scalebun.full.js.map +1 -1
- package/dist/scalebun.slim.js +129 -97
- package/dist/scalebun.slim.js.map +1 -1
- package/ios/Ota/BsPatch.swift +180 -0
- package/ios/Ota/OtaSlotManager.swift +59 -2
- package/ios/Ota/ScaleBunOtaModule.swift +89 -10
- package/lib/commonjs/analytics/EventTracker.js +21 -1
- package/lib/commonjs/analytics/EventTracker.js.map +1 -1
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/core/id/installationId.js +55 -0
- package/lib/commonjs/core/id/installationId.js.map +1 -0
- package/lib/commonjs/debug/bootstrap.js +12 -0
- package/lib/commonjs/debug/bootstrap.js.map +1 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +2 -30
- package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/commonjs/features/session/BackendSessionAdapter.js +8 -0
- package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/module/analytics/EventTracker.js +21 -1
- package/lib/module/analytics/EventTracker.js.map +1 -1
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/core/id/installationId.js +50 -0
- package/lib/module/core/id/installationId.js.map +1 -0
- package/lib/module/debug/bootstrap.js +12 -0
- package/lib/module/debug/bootstrap.js.map +1 -1
- package/lib/module/features/ota/OtaOrchestrator.js +1 -29
- package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/module/features/session/BackendSessionAdapter.js +8 -0
- package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/typescript/analytics/EventTracker.d.ts.map +1 -1
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/core/id/installationId.d.ts +20 -0
- package/lib/typescript/core/id/installationId.d.ts.map +1 -0
- package/lib/typescript/debug/bootstrap.d.ts +18 -0
- package/lib/typescript/debug/bootstrap.d.ts.map +1 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/analytics/EventTracker.ts +21 -1
- package/src/core/constants/version.ts +1 -1
- package/src/core/id/installationId.ts +52 -0
- package/src/debug/bootstrap.ts +36 -0
- package/src/features/ota/OtaOrchestrator.ts +1 -30
- package/src/features/session/BackendSessionAdapter.ts +8 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
package com.scalebun.rn.ota
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayOutputStream
|
|
4
|
+
import java.util.zip.Inflater
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* bspatch — reconstructs a bundle from a previous bundle plus a delta.
|
|
8
|
+
*
|
|
9
|
+
* WHAT WAS WRONG. `stagePatch` resolved `false` unconditionally on both
|
|
10
|
+
* platforms, so the whole delta pipeline was decorative: the server generated
|
|
11
|
+
* patches, the check response advertised them, the orchestrator asked for
|
|
12
|
+
* them, and the device always fell back to a full download. Nobody ever
|
|
13
|
+
* downloaded less than the entire bundle.
|
|
14
|
+
*
|
|
15
|
+
* FORMAT (produced by the backend's `bsdiff.ts`; all integers little-endian):
|
|
16
|
+
*
|
|
17
|
+
* 0 8 magic "SBDIFF01"
|
|
18
|
+
* 8 4 target size — the exact number of bytes this must produce
|
|
19
|
+
* 12 4 deflated control-block length
|
|
20
|
+
* 16 4 inflated control-block length
|
|
21
|
+
* 20 4 deflated diff-block length
|
|
22
|
+
* 24 4 inflated diff-block length
|
|
23
|
+
* 28 4 deflated extra-block length
|
|
24
|
+
* 32 4 inflated extra-block length
|
|
25
|
+
* 36 . deflate(control) ++ deflate(diff) ++ deflate(extra)
|
|
26
|
+
*
|
|
27
|
+
* Control is a run of (addLen, extraLen, seek) int32 triples. `addLen` bytes
|
|
28
|
+
* are read from the diff block and added, bytewise mod 256, to the source at
|
|
29
|
+
* the current read cursor; `extraLen` bytes are copied verbatim from the extra
|
|
30
|
+
* block; then the source cursor moves by the signed `seek`.
|
|
31
|
+
*
|
|
32
|
+
* WHY RAW DEFLATE. Android has `java.util.zip` and iOS has the Compression
|
|
33
|
+
* framework; neither ships zstd. On real Hermes bundles zstd-19 beat deflate-9
|
|
34
|
+
* by 0.3 percentage points (95.79% vs 95.51%) — not worth a native dependency
|
|
35
|
+
* on two platforms forever. See the backend module for the measurements.
|
|
36
|
+
*
|
|
37
|
+
* EVERY FIELD HERE IS UNTRUSTED. The patch arrives over the network, so a
|
|
38
|
+
* malformed or hostile one must fail cleanly rather than index out of bounds.
|
|
39
|
+
* Each run is bounds-checked against all four buffers before it is used, and
|
|
40
|
+
* the caller still verifies the SHA-256 of the result before anything is
|
|
41
|
+
* promoted — this routine producing the wrong bytes can never install them.
|
|
42
|
+
*/
|
|
43
|
+
internal object BsPatch {
|
|
44
|
+
|
|
45
|
+
private const val MAGIC = "SBDIFF01"
|
|
46
|
+
private const val HEADER_BYTES = 36
|
|
47
|
+
|
|
48
|
+
/** Refuse absurd allocations from a corrupt header (bundles are single-digit MB). */
|
|
49
|
+
private const val MAX_TARGET_BYTES = 256 * 1024 * 1024
|
|
50
|
+
|
|
51
|
+
class PatchFormatException(message: String) : Exception(message)
|
|
52
|
+
|
|
53
|
+
private fun u32(b: ByteArray, off: Int): Long =
|
|
54
|
+
(b[off].toLong() and 0xff) or
|
|
55
|
+
((b[off + 1].toLong() and 0xff) shl 8) or
|
|
56
|
+
((b[off + 2].toLong() and 0xff) shl 16) or
|
|
57
|
+
((b[off + 3].toLong() and 0xff) shl 24)
|
|
58
|
+
|
|
59
|
+
private fun i32(b: ByteArray, off: Int): Int =
|
|
60
|
+
(b[off].toInt() and 0xff) or
|
|
61
|
+
((b[off + 1].toInt() and 0xff) shl 8) or
|
|
62
|
+
((b[off + 2].toInt() and 0xff) shl 16) or
|
|
63
|
+
((b[off + 3].toInt() and 0xff) shl 24)
|
|
64
|
+
|
|
65
|
+
/** Raw DEFLATE (no zlib header) — matches Node's `deflateRawSync`. */
|
|
66
|
+
private fun inflateRaw(data: ByteArray, offset: Int, length: Int): ByteArray {
|
|
67
|
+
val inflater = Inflater(true)
|
|
68
|
+
try {
|
|
69
|
+
inflater.setInput(data, offset, length)
|
|
70
|
+
val out = ByteArrayOutputStream(maxOf(length * 4, 1024))
|
|
71
|
+
val buf = ByteArray(64 * 1024)
|
|
72
|
+
while (true) {
|
|
73
|
+
val n = inflater.inflate(buf)
|
|
74
|
+
if (n > 0) {
|
|
75
|
+
out.write(buf, 0, n)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
// ORDER MATTERS. A block that legitimately decodes to zero bytes
|
|
79
|
+
// — an empty extra block, which happens whenever a patch is one
|
|
80
|
+
// unbroken run — also reports needsInput() once the stream is
|
|
81
|
+
// consumed. Testing needsInput() first therefore rejected valid
|
|
82
|
+
// patches as truncated; `finished()` has to be asked first.
|
|
83
|
+
if (inflater.finished()) break
|
|
84
|
+
if (inflater.needsInput() || inflater.needsDictionary()) {
|
|
85
|
+
throw PatchFormatException("compressed block is truncated")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out.toByteArray()
|
|
89
|
+
} finally {
|
|
90
|
+
inflater.end()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Apply [patch] to [source], returning the reconstructed bundle.
|
|
96
|
+
*
|
|
97
|
+
* @throws PatchFormatException if the patch is malformed, truncated, or
|
|
98
|
+
* inconsistent with [source]. Callers treat this as "fall back to a full
|
|
99
|
+
* download", never as a fatal error.
|
|
100
|
+
*/
|
|
101
|
+
fun apply(source: ByteArray, patch: ByteArray): ByteArray {
|
|
102
|
+
if (patch.size < HEADER_BYTES) throw PatchFormatException("patch too short")
|
|
103
|
+
val magic = String(patch, 0, 8, Charsets.ISO_8859_1)
|
|
104
|
+
if (magic != MAGIC) throw PatchFormatException("bad patch magic '$magic'")
|
|
105
|
+
|
|
106
|
+
val targetSizeL = u32(patch, 8)
|
|
107
|
+
val cLen = u32(patch, 12)
|
|
108
|
+
val cRaw = u32(patch, 16)
|
|
109
|
+
val dLen = u32(patch, 20)
|
|
110
|
+
val dRaw = u32(patch, 24)
|
|
111
|
+
val eLen = u32(patch, 28)
|
|
112
|
+
val eRaw = u32(patch, 32)
|
|
113
|
+
|
|
114
|
+
if (targetSizeL > MAX_TARGET_BYTES) {
|
|
115
|
+
throw PatchFormatException("declared target size $targetSizeL is implausible")
|
|
116
|
+
}
|
|
117
|
+
if (cRaw > MAX_TARGET_BYTES || dRaw > MAX_TARGET_BYTES || eRaw > MAX_TARGET_BYTES) {
|
|
118
|
+
throw PatchFormatException("declared block size is implausible")
|
|
119
|
+
}
|
|
120
|
+
if (HEADER_BYTES + cLen + dLen + eLen != patch.size.toLong()) {
|
|
121
|
+
throw PatchFormatException("block lengths do not match patch size")
|
|
122
|
+
}
|
|
123
|
+
val targetSize = targetSizeL.toInt()
|
|
124
|
+
|
|
125
|
+
var off = HEADER_BYTES
|
|
126
|
+
val control = inflateRaw(patch, off, cLen.toInt()); off += cLen.toInt()
|
|
127
|
+
val diff = inflateRaw(patch, off, dLen.toInt()); off += dLen.toInt()
|
|
128
|
+
val extra = inflateRaw(patch, off, eLen.toInt())
|
|
129
|
+
|
|
130
|
+
// A block that inflates to a size other than the declared one is
|
|
131
|
+
// corrupt; iOS sizes its buffers from these numbers, so Android holds
|
|
132
|
+
// the format to the same contract rather than trusting the stream.
|
|
133
|
+
if (control.size.toLong() != cRaw) throw PatchFormatException("control block size mismatch")
|
|
134
|
+
if (diff.size.toLong() != dRaw) throw PatchFormatException("diff block size mismatch")
|
|
135
|
+
if (extra.size.toLong() != eRaw) throw PatchFormatException("extra block size mismatch")
|
|
136
|
+
|
|
137
|
+
if (control.size % 12 != 0) {
|
|
138
|
+
throw PatchFormatException("control block is not a whole number of triples")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
val out = ByteArray(targetSize)
|
|
142
|
+
var newpos = 0
|
|
143
|
+
var oldpos = 0
|
|
144
|
+
var dpos = 0
|
|
145
|
+
var epos = 0
|
|
146
|
+
|
|
147
|
+
var c = 0
|
|
148
|
+
while (c < control.size) {
|
|
149
|
+
val addLen = i32(control, c)
|
|
150
|
+
val extraLen = i32(control, c + 4)
|
|
151
|
+
val seek = i32(control, c + 8)
|
|
152
|
+
c += 12
|
|
153
|
+
|
|
154
|
+
if (addLen < 0 || extraLen < 0) throw PatchFormatException("negative run length")
|
|
155
|
+
if (newpos + addLen > targetSize) throw PatchFormatException("add run overruns target")
|
|
156
|
+
if (dpos + addLen > diff.size) throw PatchFormatException("add run overruns diff block")
|
|
157
|
+
if (oldpos < 0 || oldpos + addLen > source.size) {
|
|
158
|
+
throw PatchFormatException("add run overruns source")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (i in 0 until addLen) {
|
|
162
|
+
out[newpos + i] = ((diff[dpos + i].toInt() + source[oldpos + i].toInt()) and 0xff).toByte()
|
|
163
|
+
}
|
|
164
|
+
newpos += addLen
|
|
165
|
+
oldpos += addLen
|
|
166
|
+
dpos += addLen
|
|
167
|
+
|
|
168
|
+
if (newpos + extraLen > targetSize) throw PatchFormatException("extra run overruns target")
|
|
169
|
+
if (epos + extraLen > extra.size) throw PatchFormatException("extra run overruns extra block")
|
|
170
|
+
System.arraycopy(extra, epos, out, newpos, extraLen)
|
|
171
|
+
newpos += extraLen
|
|
172
|
+
epos += extraLen
|
|
173
|
+
|
|
174
|
+
oldpos += seek
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (newpos != targetSize) {
|
|
178
|
+
throw PatchFormatException("patch produced $newpos bytes, declared $targetSize")
|
|
179
|
+
}
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -191,20 +191,21 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
* Binary-patch staging (S7-NAT-1)
|
|
194
|
+
* Binary-patch staging (S7-NAT-1).
|
|
195
195
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
196
|
+
* Downloads a delta, reconstructs the full bundle from the currently
|
|
197
|
+
* installed one, and hands the result to the ordinary staging path — so a
|
|
198
|
+
* patched bundle passes exactly the same SHA-256 and slot-promotion checks
|
|
199
|
+
* as a fully downloaded one. [expectedSha256] is the hash of the COMPLETE
|
|
200
|
+
* bundle, which is what makes a bad patch unable to install: reconstruct
|
|
201
|
+
* the wrong bytes and staging rejects them.
|
|
202
202
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
203
|
+
* Resolves `false` — never rejects — for every "cannot patch" case, because
|
|
204
|
+
* `src/specs/NativeScaleBunOta.ts` defines false as "patching unavailable,
|
|
205
|
+
* fall back to a full download". The update still lands, just larger. The
|
|
206
|
+
* cases are: no OTA bundle installed yet (nothing to patch against — the
|
|
207
|
+
* first install is always full), a patch that does not match its own
|
|
208
|
+
* advertised hash, and a malformed patch.
|
|
208
209
|
*/
|
|
209
210
|
@ReactMethod
|
|
210
211
|
override fun stagePatch(
|
|
@@ -213,7 +214,81 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
213
214
|
expectedSha256: String,
|
|
214
215
|
promise: Promise,
|
|
215
216
|
) {
|
|
216
|
-
|
|
217
|
+
var downloadedPatch: File? = null
|
|
218
|
+
var rebuilt: File? = null
|
|
219
|
+
try {
|
|
220
|
+
// Patching is relative to the bundle already on disk. With no OTA
|
|
221
|
+
// bundle installed the source would be the APK's built-in asset,
|
|
222
|
+
// which the server did not diff against — so there is nothing valid
|
|
223
|
+
// to apply and a full download is the correct path.
|
|
224
|
+
val currentBundle = slotManager.currentBundlePath()
|
|
225
|
+
if (currentBundle == null || !currentBundle.exists()) {
|
|
226
|
+
android.util.Log.i(NAME, "stagePatch: no current OTA bundle to patch — full download")
|
|
227
|
+
promise.resolve(false)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
val temp = File(reactApplicationContext.cacheDir, "scalebun_ota_patch_${System.nanoTime()}")
|
|
232
|
+
downloadedPatch = temp
|
|
233
|
+
if (!BundleDownloader.download(patchUrl, temp, ::emitProgress)) {
|
|
234
|
+
promise.resolve(false)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
val patchBytes = temp.readBytes()
|
|
239
|
+
|
|
240
|
+
// Verify the patch against its own hash before parsing it. This is
|
|
241
|
+
// cheap and keeps obviously-corrupt input away from the parser.
|
|
242
|
+
val actualPatchSha = sha256Hex(patchBytes)
|
|
243
|
+
if (!actualPatchSha.equals(patchSha256, ignoreCase = true)) {
|
|
244
|
+
android.util.Log.w(NAME, "stagePatch: patch hash mismatch — full download")
|
|
245
|
+
promise.resolve(false)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
val sourceBytes = currentBundle.readBytes()
|
|
250
|
+
val rebuiltBytes = try {
|
|
251
|
+
BsPatch.apply(sourceBytes, patchBytes)
|
|
252
|
+
} catch (e: BsPatch.PatchFormatException) {
|
|
253
|
+
android.util.Log.w(NAME, "stagePatch: ${e.message} — full download")
|
|
254
|
+
promise.resolve(false)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
val out = File(reactApplicationContext.cacheDir, "scalebun_ota_rebuilt_${System.nanoTime()}")
|
|
259
|
+
rebuilt = out
|
|
260
|
+
out.writeBytes(rebuiltBytes)
|
|
261
|
+
|
|
262
|
+
val meta = JSONObject()
|
|
263
|
+
meta.put("sha256", expectedSha256)
|
|
264
|
+
meta.put("installedAt", System.currentTimeMillis())
|
|
265
|
+
meta.put("patched", true)
|
|
266
|
+
|
|
267
|
+
// SlotManager re-hashes and compares against the full-bundle hash;
|
|
268
|
+
// a mis-applied patch cannot get past this.
|
|
269
|
+
val success = slotManager.stage(out, expectedSha256, meta)
|
|
270
|
+
if (!success) {
|
|
271
|
+
android.util.Log.w(NAME, "stagePatch: rebuilt bundle failed verification — full download")
|
|
272
|
+
}
|
|
273
|
+
promise.resolve(success)
|
|
274
|
+
} catch (e: Exception) {
|
|
275
|
+
// Any unexpected failure is still just "no patch" — the caller has
|
|
276
|
+
// a working full-download path and a rejected promise would turn a
|
|
277
|
+
// recoverable situation into a failed update.
|
|
278
|
+
android.util.Log.w(NAME, "stagePatch: ${e.message} — full download")
|
|
279
|
+
promise.resolve(false)
|
|
280
|
+
} finally {
|
|
281
|
+
downloadedPatch?.delete()
|
|
282
|
+
rebuilt?.delete()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private fun sha256Hex(bytes: ByteArray): String {
|
|
287
|
+
val digest = java.security.MessageDigest.getInstance("SHA-256")
|
|
288
|
+
val hash = digest.digest(bytes)
|
|
289
|
+
val sb = StringBuilder(hash.size * 2)
|
|
290
|
+
for (b in hash) sb.append(String.format("%02x", b))
|
|
291
|
+
return sb.toString()
|
|
217
292
|
}
|
|
218
293
|
|
|
219
294
|
@ReactMethod
|
|
@@ -18,4 +18,20 @@ abstract class ScaleBunCrashSpec(reactContext: ReactApplicationContext) :
|
|
|
18
18
|
abstract fun install(promise: Promise)
|
|
19
19
|
abstract fun crashNow(promise: Promise)
|
|
20
20
|
abstract fun drainPendingCrash(promise: Promise)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* These two were added to the JS spec and to [ScaleBunCrashModule] without
|
|
24
|
+
* being added here, and the omission is invisible on the New Architecture:
|
|
25
|
+
* there the base is GENERATED from the JS spec, so it grew the methods
|
|
26
|
+
* automatically. On the Old Architecture this file IS the base and it has
|
|
27
|
+
* to be edited by hand — so `ScaleBunCrashModule` was overriding two
|
|
28
|
+
* methods that did not exist, and the SDK failed to compile for every
|
|
29
|
+
* old-arch consumer with "'setActiveSessionId' overrides nothing".
|
|
30
|
+
*
|
|
31
|
+
* Any change to `src/specs/NativeScaleBunCrash.ts` has to be mirrored here.
|
|
32
|
+
* That is the cost of the two-base design described above; the alternative
|
|
33
|
+
* is that old-arch apps silently stop building.
|
|
34
|
+
*/
|
|
35
|
+
abstract fun setActiveSessionId(sessionId: String?, promise: Promise)
|
|
36
|
+
abstract fun clearPendingCrash(crashId: String?, promise: Promise)
|
|
21
37
|
}
|
package/bin/scalebun.js
CHANGED
|
@@ -630,12 +630,64 @@ function main() {
|
|
|
630
630
|
console.log(' npx scalebun init ios --check Verify hooks exist (exit 1 if missing)');
|
|
631
631
|
console.log(' npx scalebun init ios --dry-run Preview changes without writing');
|
|
632
632
|
console.log('');
|
|
633
|
+
console.log('OTA commands (login, quickstart, ota publish, releases, rollout…)');
|
|
634
|
+
console.log('ship with this package (via the bundled @scalebun/cli) and are');
|
|
635
|
+
console.log('forwarded automatically — no extra install needed.');
|
|
636
|
+
console.log('');
|
|
633
637
|
break;
|
|
634
638
|
default:
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
639
|
+
// TWO PACKAGES, ONE COMMAND NAME.
|
|
640
|
+
//
|
|
641
|
+
// The OTA commands (login, ota publish, releases, rollout, quickstart…)
|
|
642
|
+
// live in @scalebun/cli, which declares the SAME `scalebun` bin as this
|
|
643
|
+
// package. Every doc and generated CI pipeline says `npx scalebun ota
|
|
644
|
+
// publish`, so whichever package won the bin race decided whether that
|
|
645
|
+
// worked — and when this one won, a new user's very first command died
|
|
646
|
+
// with "Unknown command: ota" and no way to find out why.
|
|
647
|
+
//
|
|
648
|
+
// Delegate instead of guessing: if @scalebun/cli is installed, run it.
|
|
649
|
+
// If not, say exactly what to install rather than what went wrong.
|
|
650
|
+
delegateToOtaCli(argv);
|
|
638
651
|
}
|
|
639
652
|
}
|
|
640
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Hand the invocation to @scalebun/cli, preserving argv and exit code.
|
|
656
|
+
* Falls back to an actionable install instruction when it is not present.
|
|
657
|
+
*/
|
|
658
|
+
function delegateToOtaCli(argv) {
|
|
659
|
+
let cliEntry = null;
|
|
660
|
+
try {
|
|
661
|
+
cliEntry = require.resolve('@scalebun/cli/lib/index.js', {
|
|
662
|
+
paths: [process.cwd(), __dirname],
|
|
663
|
+
});
|
|
664
|
+
} catch {
|
|
665
|
+
cliEntry = null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (!cliEntry) {
|
|
669
|
+
const cmd = argv[0];
|
|
670
|
+
console.error('');
|
|
671
|
+
console.error(`"${cmd}" is an OTA command, provided by @scalebun/cli.`);
|
|
672
|
+
console.error('');
|
|
673
|
+
console.error('@scalebun/cli ships as a dependency of @scalebun/react-native, so it');
|
|
674
|
+
console.error('should already be present. It could not be resolved — your install may');
|
|
675
|
+
console.error('be incomplete or the dependency was pruned.');
|
|
676
|
+
console.error('');
|
|
677
|
+
console.error('Reinstall dependencies, then re-run:');
|
|
678
|
+
console.error(' npm install');
|
|
679
|
+
console.error(` npx scalebun ${argv.join(' ')}`);
|
|
680
|
+
console.error('');
|
|
681
|
+
console.error('(Or install it explicitly: npm i -D @scalebun/cli)');
|
|
682
|
+
console.error('');
|
|
683
|
+
process.exit(2);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const { spawnSync } = require('child_process');
|
|
687
|
+
const res = spawnSync(process.execPath, [cliEntry, ...argv], {
|
|
688
|
+
stdio: 'inherit',
|
|
689
|
+
});
|
|
690
|
+
process.exit(res.status ?? 1);
|
|
691
|
+
}
|
|
692
|
+
|
|
641
693
|
main();
|
package/dist/scalebun.full.js
CHANGED
|
@@ -489,7 +489,7 @@ var SDK_VERSION;
|
|
|
489
489
|
var init_version = __esm({
|
|
490
490
|
"lib/module/core/constants/version.js"() {
|
|
491
491
|
"use strict";
|
|
492
|
-
SDK_VERSION = "1.
|
|
492
|
+
SDK_VERSION = "1.5.0";
|
|
493
493
|
}
|
|
494
494
|
});
|
|
495
495
|
|
|
@@ -10041,6 +10041,10 @@ function enableDebug(config) {
|
|
|
10041
10041
|
__DEV__ && logger.debug("Debug already enabled, skipping");
|
|
10042
10042
|
return;
|
|
10043
10043
|
}
|
|
10044
|
+
if (!__DEV__ && !config.allowInRelease) {
|
|
10045
|
+
logger.warn("[ScaleBun] Debug connection refused in a release build. The debug transport is unencrypted (ws://) and carries captured traffic and the shared secret in cleartext, so it is development-only by default. If you genuinely need it in a release build, set `allowInRelease: true` \u2014 and do not ship that to users.");
|
|
10046
|
+
return;
|
|
10047
|
+
}
|
|
10044
10048
|
if (!isDevToolsBundled()) {
|
|
10045
10049
|
logger.warn("[ScaleBun] debugConnection was configured, but the desktop-debugger tooling is not in this bundle. It is excluded by default (the ScaleBun desktop app is not yet released); opt in with `withScaleBun(config, { features: { devTools: true } })` in metro.config.js. All other SDK behaviour is unaffected.");
|
|
10046
10050
|
return;
|
|
@@ -10550,89 +10554,6 @@ var init_nativeCrashBridge = __esm({
|
|
|
10550
10554
|
}
|
|
10551
10555
|
});
|
|
10552
10556
|
|
|
10553
|
-
// lib/module/analytics/batching.js
|
|
10554
|
-
function chunkEvents(events, size = MAX_BATCH_EVENTS) {
|
|
10555
|
-
const cap = Math.max(1, Math.min(size, MAX_BATCH_EVENTS));
|
|
10556
|
-
if (events.length <= cap) return events.length ? [events.slice()] : [];
|
|
10557
|
-
const out = [];
|
|
10558
|
-
for (let i = 0; i < events.length; i += cap) {
|
|
10559
|
-
out.push(events.slice(i, i + cap));
|
|
10560
|
-
}
|
|
10561
|
-
return out;
|
|
10562
|
-
}
|
|
10563
|
-
function clampBatchSize(configured, fallback) {
|
|
10564
|
-
const n = typeof configured === "number" && configured > 0 ? configured : fallback;
|
|
10565
|
-
return Math.max(1, Math.min(n, MAX_BATCH_EVENTS));
|
|
10566
|
-
}
|
|
10567
|
-
function batchIdempotencyKey(eventIds) {
|
|
10568
|
-
let h = 2166136261;
|
|
10569
|
-
const joined = eventIds.join("");
|
|
10570
|
-
for (let i = 0; i < joined.length; i++) {
|
|
10571
|
-
h ^= joined.charCodeAt(i);
|
|
10572
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
10573
|
-
}
|
|
10574
|
-
return `sdk-${eventIds.length}-${h.toString(16).padStart(8, "0")}`;
|
|
10575
|
-
}
|
|
10576
|
-
var MAX_BATCH_EVENTS;
|
|
10577
|
-
var init_batching = __esm({
|
|
10578
|
-
"lib/module/analytics/batching.js"() {
|
|
10579
|
-
"use strict";
|
|
10580
|
-
MAX_BATCH_EVENTS = 500;
|
|
10581
|
-
}
|
|
10582
|
-
});
|
|
10583
|
-
|
|
10584
|
-
// lib/module/analytics/automaticEvents.js
|
|
10585
|
-
function compactProperties(properties) {
|
|
10586
|
-
const out = {
|
|
10587
|
-
capture_source: "automatic"
|
|
10588
|
-
};
|
|
10589
|
-
for (const [key, value] of Object.entries(properties ?? {})) {
|
|
10590
|
-
if (value !== void 0) out[key] = value;
|
|
10591
|
-
}
|
|
10592
|
-
return out;
|
|
10593
|
-
}
|
|
10594
|
-
function emitAutomaticEvent(name, properties) {
|
|
10595
|
-
const event = {
|
|
10596
|
-
name,
|
|
10597
|
-
properties: compactProperties(properties)
|
|
10598
|
-
};
|
|
10599
|
-
if (listeners.size === 0) {
|
|
10600
|
-
pending3.push(event);
|
|
10601
|
-
if (pending3.length > MAX_PENDING2) pending3.shift();
|
|
10602
|
-
return;
|
|
10603
|
-
}
|
|
10604
|
-
for (const listener of listeners) {
|
|
10605
|
-
try {
|
|
10606
|
-
listener(event);
|
|
10607
|
-
} catch {
|
|
10608
|
-
}
|
|
10609
|
-
}
|
|
10610
|
-
}
|
|
10611
|
-
function subscribeAutomaticEvents(listener) {
|
|
10612
|
-
listeners.add(listener);
|
|
10613
|
-
if (pending3.length > 0) {
|
|
10614
|
-
const buffered = pending3.splice(0, pending3.length);
|
|
10615
|
-
for (const event of buffered) {
|
|
10616
|
-
try {
|
|
10617
|
-
listener(event);
|
|
10618
|
-
} catch {
|
|
10619
|
-
}
|
|
10620
|
-
}
|
|
10621
|
-
}
|
|
10622
|
-
return () => {
|
|
10623
|
-
listeners.delete(listener);
|
|
10624
|
-
};
|
|
10625
|
-
}
|
|
10626
|
-
var listeners, pending3, MAX_PENDING2;
|
|
10627
|
-
var init_automaticEvents = __esm({
|
|
10628
|
-
"lib/module/analytics/automaticEvents.js"() {
|
|
10629
|
-
"use strict";
|
|
10630
|
-
listeners = /* @__PURE__ */ new Set();
|
|
10631
|
-
pending3 = [];
|
|
10632
|
-
MAX_PENDING2 = 50;
|
|
10633
|
-
}
|
|
10634
|
-
});
|
|
10635
|
-
|
|
10636
10557
|
// lib/module/crypto/hmacSha256.js
|
|
10637
10558
|
function utf8(str3) {
|
|
10638
10559
|
const out = [];
|
|
@@ -10749,6 +10670,89 @@ var init_hmacSha256 = __esm({
|
|
|
10749
10670
|
}
|
|
10750
10671
|
});
|
|
10751
10672
|
|
|
10673
|
+
// lib/module/analytics/batching.js
|
|
10674
|
+
function chunkEvents(events, size = MAX_BATCH_EVENTS) {
|
|
10675
|
+
const cap = Math.max(1, Math.min(size, MAX_BATCH_EVENTS));
|
|
10676
|
+
if (events.length <= cap) return events.length ? [events.slice()] : [];
|
|
10677
|
+
const out = [];
|
|
10678
|
+
for (let i = 0; i < events.length; i += cap) {
|
|
10679
|
+
out.push(events.slice(i, i + cap));
|
|
10680
|
+
}
|
|
10681
|
+
return out;
|
|
10682
|
+
}
|
|
10683
|
+
function clampBatchSize(configured, fallback) {
|
|
10684
|
+
const n = typeof configured === "number" && configured > 0 ? configured : fallback;
|
|
10685
|
+
return Math.max(1, Math.min(n, MAX_BATCH_EVENTS));
|
|
10686
|
+
}
|
|
10687
|
+
function batchIdempotencyKey(eventIds) {
|
|
10688
|
+
let h = 2166136261;
|
|
10689
|
+
const joined = eventIds.join("");
|
|
10690
|
+
for (let i = 0; i < joined.length; i++) {
|
|
10691
|
+
h ^= joined.charCodeAt(i);
|
|
10692
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
10693
|
+
}
|
|
10694
|
+
return `sdk-${eventIds.length}-${h.toString(16).padStart(8, "0")}`;
|
|
10695
|
+
}
|
|
10696
|
+
var MAX_BATCH_EVENTS;
|
|
10697
|
+
var init_batching = __esm({
|
|
10698
|
+
"lib/module/analytics/batching.js"() {
|
|
10699
|
+
"use strict";
|
|
10700
|
+
MAX_BATCH_EVENTS = 500;
|
|
10701
|
+
}
|
|
10702
|
+
});
|
|
10703
|
+
|
|
10704
|
+
// lib/module/analytics/automaticEvents.js
|
|
10705
|
+
function compactProperties(properties) {
|
|
10706
|
+
const out = {
|
|
10707
|
+
capture_source: "automatic"
|
|
10708
|
+
};
|
|
10709
|
+
for (const [key, value] of Object.entries(properties ?? {})) {
|
|
10710
|
+
if (value !== void 0) out[key] = value;
|
|
10711
|
+
}
|
|
10712
|
+
return out;
|
|
10713
|
+
}
|
|
10714
|
+
function emitAutomaticEvent(name, properties) {
|
|
10715
|
+
const event = {
|
|
10716
|
+
name,
|
|
10717
|
+
properties: compactProperties(properties)
|
|
10718
|
+
};
|
|
10719
|
+
if (listeners.size === 0) {
|
|
10720
|
+
pending3.push(event);
|
|
10721
|
+
if (pending3.length > MAX_PENDING2) pending3.shift();
|
|
10722
|
+
return;
|
|
10723
|
+
}
|
|
10724
|
+
for (const listener of listeners) {
|
|
10725
|
+
try {
|
|
10726
|
+
listener(event);
|
|
10727
|
+
} catch {
|
|
10728
|
+
}
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10731
|
+
function subscribeAutomaticEvents(listener) {
|
|
10732
|
+
listeners.add(listener);
|
|
10733
|
+
if (pending3.length > 0) {
|
|
10734
|
+
const buffered = pending3.splice(0, pending3.length);
|
|
10735
|
+
for (const event of buffered) {
|
|
10736
|
+
try {
|
|
10737
|
+
listener(event);
|
|
10738
|
+
} catch {
|
|
10739
|
+
}
|
|
10740
|
+
}
|
|
10741
|
+
}
|
|
10742
|
+
return () => {
|
|
10743
|
+
listeners.delete(listener);
|
|
10744
|
+
};
|
|
10745
|
+
}
|
|
10746
|
+
var listeners, pending3, MAX_PENDING2;
|
|
10747
|
+
var init_automaticEvents = __esm({
|
|
10748
|
+
"lib/module/analytics/automaticEvents.js"() {
|
|
10749
|
+
"use strict";
|
|
10750
|
+
listeners = /* @__PURE__ */ new Set();
|
|
10751
|
+
pending3 = [];
|
|
10752
|
+
MAX_PENDING2 = 50;
|
|
10753
|
+
}
|
|
10754
|
+
});
|
|
10755
|
+
|
|
10752
10756
|
// lib/module/analytics/EventTracker.js
|
|
10753
10757
|
function uuid() {
|
|
10754
10758
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
@@ -11114,6 +11118,38 @@ var init_EventTracker = __esm({
|
|
|
11114
11118
|
}
|
|
11115
11119
|
});
|
|
11116
11120
|
|
|
11121
|
+
// lib/module/core/id/installationId.js
|
|
11122
|
+
function resolveInstallationId() {
|
|
11123
|
+
try {
|
|
11124
|
+
const storage3 = createStorageBackend();
|
|
11125
|
+
const existing = storage3.get(K_INSTALLATION_ID);
|
|
11126
|
+
if (existing) return existing;
|
|
11127
|
+
const id = `inst-${randomSegment()}${randomSegment()}`;
|
|
11128
|
+
storage3.set(K_INSTALLATION_ID, id);
|
|
11129
|
+
return id;
|
|
11130
|
+
} catch {
|
|
11131
|
+
return `inst-ephemeral-${randomSegment()}`;
|
|
11132
|
+
}
|
|
11133
|
+
}
|
|
11134
|
+
function randomSegment() {
|
|
11135
|
+
try {
|
|
11136
|
+
const g = globalThis;
|
|
11137
|
+
if (g.crypto?.getRandomValues) {
|
|
11138
|
+
const bytes = g.crypto.getRandomValues(new Uint8Array(6));
|
|
11139
|
+
return Array.from(bytes).map((b) => b.toString(36).padStart(2, "0")).join("").slice(0, 8);
|
|
11140
|
+
}
|
|
11141
|
+
} catch {
|
|
11142
|
+
}
|
|
11143
|
+
return Math.random().toString(36).slice(2, 10).padEnd(8, "0");
|
|
11144
|
+
}
|
|
11145
|
+
var init_installationId = __esm({
|
|
11146
|
+
"lib/module/core/id/installationId.js"() {
|
|
11147
|
+
"use strict";
|
|
11148
|
+
init_StorageBackend();
|
|
11149
|
+
init_EventTracker();
|
|
11150
|
+
}
|
|
11151
|
+
});
|
|
11152
|
+
|
|
11117
11153
|
// lib/module/features/engage/engageSignals.js
|
|
11118
11154
|
var EngageSignalBus, engageSignals;
|
|
11119
11155
|
var init_engageSignals = __esm({
|
|
@@ -11573,18 +11609,6 @@ function subscribeNativeProgress(onTick) {
|
|
|
11573
11609
|
};
|
|
11574
11610
|
}
|
|
11575
11611
|
}
|
|
11576
|
-
function resolveInstallationId() {
|
|
11577
|
-
try {
|
|
11578
|
-
const storage3 = createStorageBackend();
|
|
11579
|
-
const existing = storage3.get(K_INSTALLATION_ID);
|
|
11580
|
-
if (existing) return existing;
|
|
11581
|
-
const id = `inst-${Math.random().toString(36).slice(2, 10)}${Math.random().toString(36).slice(2, 10)}`;
|
|
11582
|
-
storage3.set(K_INSTALLATION_ID, id);
|
|
11583
|
-
return id;
|
|
11584
|
-
} catch {
|
|
11585
|
-
return `inst-ephemeral-${Math.random().toString(36).slice(2, 10)}`;
|
|
11586
|
-
}
|
|
11587
|
-
}
|
|
11588
11612
|
async function deliverOtaEvents(params) {
|
|
11589
11613
|
const events = otaEventEmitter.flush();
|
|
11590
11614
|
if (!events.length) return;
|
|
@@ -11634,7 +11658,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
11634
11658
|
init_OtaEventEmitter();
|
|
11635
11659
|
init_geoCountry();
|
|
11636
11660
|
init_deviceAttributes();
|
|
11637
|
-
|
|
11661
|
+
init_installationId();
|
|
11638
11662
|
init_signature();
|
|
11639
11663
|
init_environment();
|
|
11640
11664
|
init_retry();
|
|
@@ -15374,6 +15398,7 @@ function decodeManual(base64) {
|
|
|
15374
15398
|
}
|
|
15375
15399
|
|
|
15376
15400
|
// lib/module/features/session/BackendSessionAdapter.js
|
|
15401
|
+
init_installationId();
|
|
15377
15402
|
init_version();
|
|
15378
15403
|
init_bridgeAdapter();
|
|
15379
15404
|
init_nativeModule();
|
|
@@ -15509,6 +15534,13 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
|
|
|
15509
15534
|
const payload = {
|
|
15510
15535
|
sessionId: session.sessionId,
|
|
15511
15536
|
deviceId: session.deviceId,
|
|
15537
|
+
// The canonical per-install id — the SAME value OTA events are
|
|
15538
|
+
// reported under, and a DIFFERENT value from deviceId. The
|
|
15539
|
+
// backend stores it on the Device so release health can join a
|
|
15540
|
+
// bundle's installs to that device's sessions. Without it, crash
|
|
15541
|
+
// impact for a release resolves to "no data" for every device
|
|
15542
|
+
// that never registered for push.
|
|
15543
|
+
installationId: resolveInstallationId(),
|
|
15512
15544
|
startedAt: session.startedAt,
|
|
15513
15545
|
// Link the recording row to the always-on analytics row so the
|
|
15514
15546
|
// dashboard can join them. track() events land on the analytics
|