@bota.dev/react-native-app-sdk 2.0.0-beta.14 → 2.0.0-beta.15
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/README.md +36 -7
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +3 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +37 -3
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/ByoBridgeDriver.kt +79 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +30 -2
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +5 -2
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadStreamingStatus.kt +67 -0
- package/generated/codegen-contract.json +110 -8
- package/ios/BotaDeviceSDK.mm +1 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +4 -2
- package/ios/BotaDeviceSDKAppleRecordings.swift +1 -0
- package/ios/BotaUploadV2Backend.mm +21 -0
- package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +52 -2
- package/ios/UploadBackend/ByoBridgeDriver.swift +94 -0
- package/ios/UploadBackend/UploadModels.swift +6 -1
- package/ios/UploadBackend/UploadOperation.swift +41 -3
- package/ios/UploadBackend/UploadStreamingStatus.swift +43 -0
- package/lib/commonjs/byoStorageBackend.js +144 -0
- package/lib/commonjs/byoStorageBackend.js.map +1 -0
- package/lib/commonjs/client.js +2 -1
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/encryptedUploadV2Backend.js +4 -1
- package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -1
- package/lib/commonjs/index.js +7 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +6 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/commonjs/protectedStreamingStatus.js +120 -0
- package/lib/commonjs/protectedStreamingStatus.js.map +1 -0
- package/lib/commonjs/sdkIdentity.js +1 -1
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -1
- package/lib/module/byoStorageBackend.js +140 -0
- package/lib/module/byoStorageBackend.js.map +1 -0
- package/lib/module/client.js +2 -1
- package/lib/module/client.js.map +1 -1
- package/lib/module/encryptedUploadV2Backend.js +4 -1
- package/lib/module/encryptedUploadV2Backend.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +6 -0
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/module/protectedStreamingStatus.js +116 -0
- package/lib/module/protectedStreamingStatus.js.map +1 -0
- package/lib/module/sdkIdentity.js +1 -1
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -1
- package/lib/typescript/byoStorageBackend.d.ts +36 -0
- package/lib/typescript/byoStorageBackend.d.ts.map +1 -0
- package/lib/typescript/client.d.ts.map +1 -1
- package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/managers/DeviceManager.d.ts +2 -0
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/protectedStreamingStatus.d.ts +37 -0
- package/lib/typescript/protectedStreamingStatus.d.ts.map +1 -0
- package/lib/typescript/sdkIdentity.d.ts +1 -1
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +1 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
- package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +4 -0
- package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/byoStorageBackend.ts +104 -0
- package/src/client.ts +2 -0
- package/src/encryptedUploadV2Backend.ts +2 -1
- package/src/index.ts +11 -0
- package/src/managers/DeviceManager.ts +6 -0
- package/src/protectedStreamingStatus.ts +137 -0
- package/src/sdkIdentity.ts +1 -1
- package/src/specs/NativeBotaDeviceSDK.ts +1 -0
- package/src/specs/NativeBotaUploadV2Backend.ts +4 -0
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# Bota SDK for React Native
|
|
2
2
|
|
|
3
|
-
Source candidate `2.0.0-beta.
|
|
4
|
-
It
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Source candidate `2.0.0-beta.15` synchronizes the five-platform SDK family.
|
|
4
|
+
It includes Apple single-ATT transfer bounds and local connection ownership,
|
|
5
|
+
awaitable React Native reconnect pause, and the reviewed main OTA, recording
|
|
6
|
+
marker, protected-status and BYO storage changes. Beta.14 remains the published
|
|
7
|
+
baseline until this candidate completes its gates.
|
|
7
8
|
Native applications require a new binary.
|
|
9
|
+
The managed Apple backend also matches the strict session-create request without
|
|
10
|
+
an EndUser override; recording creation and native scope fences retain EndUser.
|
|
11
|
+
See [request correction](../../docs/parity/v2-managed-backend.md#october-12-apple-session-request-correction).
|
|
8
12
|
|
|
9
13
|
Bluetooth device SDK for Bota Pin and Bota Note applications. The 2.x candidate
|
|
10
14
|
preserves the public TypeScript API of `@bota.dev/react-native-sdk@0.0.65`
|
|
@@ -29,9 +33,9 @@ adopting this source API; published-package availability is a separate gate.
|
|
|
29
33
|
|
|
30
34
|
## Install
|
|
31
35
|
|
|
32
|
-
Published beta.
|
|
36
|
+
Published beta.14 remains available. Use the beta.15 pin below only after
|
|
33
37
|
publication is verified; see the
|
|
34
|
-
[preflight](../../release/evidence/2.0.0-beta.
|
|
38
|
+
[preflight](../../release/evidence/2.0.0-beta.15-preflight.md).
|
|
35
39
|
Rebuild native applications and reconnect explicitly after connection loss.
|
|
36
40
|
Physical completed-resume acceptance and wider GATT recovery remain separate.
|
|
37
41
|
Remove `@bota.dev/react-native-sdk` before adding the replacement; do not
|
|
@@ -39,7 +43,7 @@ co-install both. Standalone maintenance ended September 29, 2026; follow the
|
|
|
39
43
|
[retirement and migration notice](../../docs/migrations/react-native-sdk-sunset.md).
|
|
40
44
|
|
|
41
45
|
```bash
|
|
42
|
-
npm install --save-exact @bota.dev/react-native-app-sdk@2.0.0-beta.
|
|
46
|
+
npm install --save-exact @bota.dev/react-native-app-sdk@2.0.0-beta.15
|
|
43
47
|
npx pod-install
|
|
44
48
|
```
|
|
45
49
|
|
|
@@ -84,6 +88,14 @@ await BotaClient.waitForBluetooth();
|
|
|
84
88
|
The `BotaClient.devices`, `BotaClient.recordings`, and `BotaClient.ota`
|
|
85
89
|
managers become available after configuration completes.
|
|
86
90
|
|
|
91
|
+
Source-only workspace coordination adds
|
|
92
|
+
`await BotaClient.devices.pauseAutoReconnect()`. Fence new reconnect work and
|
|
93
|
+
retry rearming first, then inspect fresh device activity and disconnect idle
|
|
94
|
+
connections before changing scope. This method waits for submitted reconnects;
|
|
95
|
+
it does not itself disconnect. Existing `disableAutoReconnect(): void` remains
|
|
96
|
+
unchanged. Published beta.14 does not include the addition; see
|
|
97
|
+
[verification and delivery limits](../../docs/parity/workspace-reconnect-quiescence.md).
|
|
98
|
+
|
|
87
99
|
Discovery exposes the manufacturer-advertised `macAddress` as uppercase,
|
|
88
100
|
colon-separated bytes (`AA:BB:CC:DD:EE:FF`), preserving the legacy React Native
|
|
89
101
|
registration contract. Compact native addresses are formatted at the RN
|
|
@@ -183,6 +195,23 @@ separate physical-device and application rollout gates.
|
|
|
183
195
|
|
|
184
196
|
## Documentation
|
|
185
197
|
|
|
198
|
+
Unpublished source also exports `createProtectedStreamingStatusBackend` for
|
|
199
|
+
read-only protected STREAMING metadata. Configure a scoped HTTPS `baseUrl`,
|
|
200
|
+
optional `organizationId`, lifetime `signal` and fresh application
|
|
201
|
+
`getAccessToken`; call `getStatus({ recordingId, sessionId,
|
|
202
|
+
recordingGeneration, writerEpoch })`. The exact session identity is checked;
|
|
203
|
+
`revision` remains a decimal string and may be `"0"`. Cancel the lifetime and
|
|
204
|
+
call `dispose()` on workspace or account changes. A matching rebuilt native
|
|
205
|
+
binary is required; published beta.14 lacks these methods. `sealed` means
|
|
206
|
+
marker metadata only, never audio completion or permission to delete. This
|
|
207
|
+
reader does not start protected capture or relay signed documents.
|
|
208
|
+
See `docs/parity/protected-streaming-status.md` in the source repository.
|
|
209
|
+
|
|
210
|
+
未发布源码新增上述只读状态接口,必须使用匹配的原生二进制;已发布 beta.14 不包含
|
|
211
|
+
这些方法。查询校验精确会话身份,revision 保持字符串且允许 `"0"`;账号或工作空间
|
|
212
|
+
变化时取消并释放。`sealed` 仅指标记元数据,不代表音频完成或允许删除;本接口不启动
|
|
213
|
+
protected 采集,也不转发签名文档。
|
|
214
|
+
|
|
186
215
|
See [docs.bota.dev](https://docs.bota.dev) for pairing, provisioning,
|
|
187
216
|
recording transfer, WiFi, OTA, and device-management guides.
|
|
188
217
|
|
|
@@ -106,6 +106,7 @@ internal data class BotaEncryptedUploadV2Recording(
|
|
|
106
106
|
val durationMs: String,
|
|
107
107
|
val plaintextLength: String,
|
|
108
108
|
val storageFormat: Int,
|
|
109
|
+
val markersRequired: Boolean = false,
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
internal data class BotaEncryptedUploadV2Capability(
|
|
@@ -225,6 +226,7 @@ internal fun BotaEncryptedUploadV2Recording.toWritableMap(): WritableMap = Argum
|
|
|
225
226
|
putString("durationMs", durationMs)
|
|
226
227
|
putString("plaintextLength", plaintextLength)
|
|
227
228
|
putInt("storageFormat", storageFormat)
|
|
229
|
+
putBoolean("markersRequired", markersRequired)
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
private fun BotaEncryptedUploadV2Capability.toWritableMap(): WritableMap = Arguments.createMap().apply {
|
|
@@ -779,6 +781,7 @@ internal fun EncryptedUploadV2Recording.toBridgeValue(): BotaEncryptedUploadV2Re
|
|
|
779
781
|
durationMs.toString(),
|
|
780
782
|
plaintextLength.toString(),
|
|
781
783
|
storageFormat.toInt(),
|
|
784
|
+
markersRequired,
|
|
782
785
|
)
|
|
783
786
|
|
|
784
787
|
private fun EncryptedUploadV2CapabilitySnapshot.toBridgeValue(): BotaEncryptedUploadV2Capability {
|
|
@@ -786,6 +786,7 @@ private fun ReadableMap.toEncryptedUploadV2Recording(): EncryptedUploadV2Recordi
|
|
|
786
786
|
if (hasKey("storageFormat") && !isNull("storageFormat")) {
|
|
787
787
|
getDouble("storageFormat").toUnsignedInt().also { require(it <= 255u) }.toUByte()
|
|
788
788
|
} else 3u,
|
|
789
|
+
hasKey("markersRequired") && !isNull("markersRequired") && getBoolean("markersRequired"),
|
|
789
790
|
)
|
|
790
791
|
}
|
|
791
792
|
|
|
@@ -13,6 +13,8 @@ import kotlinx.coroutines.CoroutineScope
|
|
|
13
13
|
import kotlinx.coroutines.Dispatchers
|
|
14
14
|
import kotlinx.coroutines.SupervisorJob
|
|
15
15
|
import kotlinx.coroutines.launch
|
|
16
|
+
import kotlinx.coroutines.Job
|
|
17
|
+
import kotlinx.coroutines.CoroutineStart
|
|
16
18
|
import org.json.JSONObject
|
|
17
19
|
|
|
18
20
|
@ReactModule(name = BotaUploadV2BackendModule.NAME)
|
|
@@ -24,6 +26,11 @@ internal class BotaUploadV2BackendModule(context: ReactApplicationContext) : Nat
|
|
|
24
26
|
putString("requestId", requestId); putString("operationId", operationId)
|
|
25
27
|
})
|
|
26
28
|
}
|
|
29
|
+
private val byo = ByoBridgeDriver(File(context.noBackupFilesDir, "BotaSDKByo")) { requestId, operationId ->
|
|
30
|
+
emitOnCredentialsRequested(Arguments.createMap().apply {
|
|
31
|
+
putString("requestId", requestId); putString("operationId", operationId)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
27
34
|
private val upload = NativeUpload(
|
|
28
35
|
UploadJournal(File(context.noBackupFilesDir, "BotaSDKUploadV2"), object : JournalDurability {
|
|
29
36
|
override fun syncDirectory(directory: File) {
|
|
@@ -35,31 +42,58 @@ internal class BotaUploadV2BackendModule(context: ReactApplicationContext) : Nat
|
|
|
35
42
|
}
|
|
36
43
|
}), BackendUploadHttp(), credentials::request, BotaDeviceSDKEncryptedUploadV2Materials::readAuthNonce,
|
|
37
44
|
)
|
|
45
|
+
private val statusReader = UploadStreamingStatus()
|
|
46
|
+
private val statusJobs = mutableMapOf<String, Job>()
|
|
38
47
|
@Volatile private var destroyed = false
|
|
39
48
|
|
|
40
49
|
private fun invoke(promise: Promise, block: suspend () -> Any?) {
|
|
41
50
|
work.launch {
|
|
42
51
|
try { demand(!destroyed, "CANCELLED"); promise.resolve(block()) }
|
|
43
52
|
catch (error: Exception) {
|
|
44
|
-
val code = (error as? UploadFailure)?.code ?: "BOTA_UPLOAD_NATIVE"
|
|
53
|
+
val code = (error as? dev.bota.sdk.ByoStorageException)?.message ?: (error as? UploadFailure)?.code ?: "BOTA_UPLOAD_NATIVE"
|
|
45
54
|
promise.reject(code, code, null)
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
}
|
|
58
|
+
override fun syncByo(inputJSON: String, promise: Promise) = invoke(promise) { byo.sync(inputJSON) }
|
|
59
|
+
override fun cancelByo(operationId: String, promise: Promise) = invoke(promise) { byo.cancel(operationId); null }
|
|
60
|
+
override fun readProtectedStreamingStatus(requestId: String, inputJSON: String, promise: Promise) {
|
|
61
|
+
val job = work.launch(start = CoroutineStart.LAZY) {
|
|
62
|
+
try { demand(!destroyed, "CANCELLED"); promise.resolve(statusReader.read(inputJSON)) }
|
|
63
|
+
catch (_: Exception) { promise.reject("BOTA_STREAM_STATUS_FAILED", "BOTA_STREAM_STATUS_FAILED", null) }
|
|
64
|
+
}
|
|
65
|
+
job.invokeOnCompletion { error ->
|
|
66
|
+
synchronized(statusJobs) { if (statusJobs[requestId] === job) statusJobs.remove(requestId) }
|
|
67
|
+
if (error != null) promise.reject("BOTA_STREAM_STATUS_FAILED", "BOTA_STREAM_STATUS_FAILED", null)
|
|
68
|
+
}
|
|
69
|
+
synchronized(statusJobs) {
|
|
70
|
+
if (destroyed || statusJobs.containsKey(requestId)) {
|
|
71
|
+
job.cancel(); return
|
|
72
|
+
}
|
|
73
|
+
statusJobs[requestId] = job
|
|
74
|
+
}
|
|
75
|
+
job.start()
|
|
76
|
+
}
|
|
77
|
+
override fun cancelProtectedStreamingStatus(requestId: String, promise: Promise) {
|
|
78
|
+
synchronized(statusJobs) { statusJobs[requestId]?.cancel() }
|
|
79
|
+
promise.resolve(null)
|
|
80
|
+
}
|
|
49
81
|
override fun prepare(inputJSON: String, promise: Promise) = invoke(promise) {
|
|
50
82
|
JSONObject(upload.prepare(inputJSON)).toString()
|
|
51
83
|
}
|
|
52
84
|
override fun cancel(operationId: String, promise: Promise) = invoke(promise) { upload.cancel(operationId); null }
|
|
53
85
|
override fun complete(operationId: String, promise: Promise) = invoke(promise) { upload.complete(operationId); null }
|
|
54
86
|
override fun resolveCredentials(requestId: String, token: String, promise: Promise) = invoke(promise) {
|
|
55
|
-
credentials.resolve(requestId, token); null
|
|
87
|
+
credentials.resolve(requestId, token); byo.resolve(requestId, token); null
|
|
56
88
|
}
|
|
57
89
|
override fun rejectCredentials(requestId: String, promise: Promise) = invoke(promise) {
|
|
58
|
-
credentials.reject(requestId); null
|
|
90
|
+
credentials.reject(requestId); byo.reject(requestId); null
|
|
59
91
|
}
|
|
60
92
|
override fun invalidate() {
|
|
61
93
|
destroyed = true
|
|
62
94
|
upload.cancelAll()
|
|
95
|
+
work.launch { byo.stop() }
|
|
96
|
+
synchronized(statusJobs) { statusJobs.values.toList().forEach { it.cancel() }; statusJobs.clear() }
|
|
63
97
|
// Dispatched identity-creating requests must drain into their original journal.
|
|
64
98
|
super.invalidate()
|
|
65
99
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package dev.bota.sdk.reactnative.upload
|
|
2
|
+
|
|
3
|
+
import dev.bota.sdk.*
|
|
4
|
+
import dev.bota.sdk.model.*
|
|
5
|
+
import java.io.File
|
|
6
|
+
import java.util.UUID
|
|
7
|
+
import kotlinx.coroutines.*
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
|
|
10
|
+
/** RN passes only identity metadata; all signed material and audio remain native. */
|
|
11
|
+
internal class ByoBridgeDriver(private val root: File,private val emit: (String,String)->Unit) {
|
|
12
|
+
private val lock=Any()
|
|
13
|
+
private val tasks=mutableMapOf<String,Job>()
|
|
14
|
+
private val cancelled=mutableSetOf<String>()
|
|
15
|
+
private var stopped=false
|
|
16
|
+
private data class Pending(val owner: String,val value: CompletableDeferred<String>)
|
|
17
|
+
private val pending=mutableMapOf<String,Pending>()
|
|
18
|
+
private fun check(id: String)=synchronized(lock) {demand(!stopped && id !in cancelled && tasks.containsKey(id),"CANCELLED")}
|
|
19
|
+
suspend fun sync(json: String): String=coroutineScope {
|
|
20
|
+
demand(json.toByteArray().size<=8192,"INVALID_INPUT")
|
|
21
|
+
val input=JSONObject(json);val id=input.getString("operationId")
|
|
22
|
+
val job=currentCoroutineContext()[Job]!!
|
|
23
|
+
synchronized(lock) {
|
|
24
|
+
demand(id.startsWith("byo-") && id.length<=64 && !stopped && id !in cancelled && !tasks.containsKey(id),"INVALID_INPUT")
|
|
25
|
+
tasks[id]=job
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
val value=input.getJSONObject("device");val scope=input.getJSONObject("scope");val recording=input.optJSONObject("recording")
|
|
29
|
+
val type=when(value.getString("deviceType")) {"bota_pin"->DeviceType.BotaPin;"bota_pin_4g"->DeviceType.BotaPin4G;"bota_note"->DeviceType.BotaNote;else->fail("INVALID_INPUT")}
|
|
30
|
+
demand(value.getString("connectionState")=="connected" && value.getInt("mtu") in 23..517,"INVALID_INPUT")
|
|
31
|
+
val device=ConnectedDevice(value.getString("id"),value.getString("serialNumber"),type,value.getString("firmwareVersion"),
|
|
32
|
+
if(value.isNull("hardwareRevision")) null else value.optString("hardwareRevision"),
|
|
33
|
+
value.getBoolean("isProvisioned"),ConnectionState.Connected,value.getInt("mtu"))
|
|
34
|
+
val binding=scope.getLong("bindingGeneration");val generation=recording?.getLong("generation") ?: 0
|
|
35
|
+
demand(binding in 1..0xffffffffL && generation in 0..0xffffffffL,"INVALID_INPUT")
|
|
36
|
+
val options=ByoStorageOptions(scope.getString("baseUrl"),scope.getString("scopeKey"),scope.getString("organizationId"),
|
|
37
|
+
scope.getString("projectId"),scope.getString("deviceId"),scope.getString("environment"),binding.toUInt(),root,
|
|
38
|
+
{token(id)},{currentCoroutineContext().ensureActive();check(id)})
|
|
39
|
+
if(input.getString("mode")=="list") {
|
|
40
|
+
val values=BotaDeviceClient.shared.recordings.listByoRecordings(device,options);check(id)
|
|
41
|
+
org.json.JSONArray(values.map {JSONObject().put("uuid",it.uuid).put("generation",it.generation.toLong())}).toString()
|
|
42
|
+
} else {
|
|
43
|
+
demand(input.getString("mode")=="stream" || (input.getString("mode")=="sync" && recording!=null),"INVALID_INPUT")
|
|
44
|
+
val selected=recording?.let {ByoRecording(it.getString("uuid"),generation.toUInt())}
|
|
45
|
+
val result=if(input.getString("mode")=="stream") BotaDeviceClient.shared.recordings.streamByoRecording(device,selected,options)
|
|
46
|
+
else BotaDeviceClient.shared.recordings.syncByoRecording(device,requireNotNull(selected),options)
|
|
47
|
+
check(id)
|
|
48
|
+
JSONObject().put("recordingId",result.recordingId).put("uploadSessionId",result.uploadSessionId).toString()
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
synchronized(lock) {tasks.remove(id);cancelled.remove(id)
|
|
52
|
+
pending.filterValues {it.owner==id}.keys.toList().forEach {pending.remove(it)?.value?.cancel()}}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
private suspend fun token(id: String): String {
|
|
56
|
+
check(id);val request=UUID.randomUUID().toString();val value=CompletableDeferred<String>()
|
|
57
|
+
synchronized(lock) {check(id);pending[request]=Pending(id,value)}
|
|
58
|
+
try {emit(request,id);return withTimeout(30_000) {value.await()}.also {check(id)}}
|
|
59
|
+
finally {synchronized(lock) {pending.remove(request)}}
|
|
60
|
+
}
|
|
61
|
+
fun resolve(request: String,token: String) {
|
|
62
|
+
val entry=synchronized(lock) {pending.remove(request)} ?: return
|
|
63
|
+
if(token.isNotEmpty() && token.length<=16384 && token.all {it.code in 33..126}) entry.value.complete(token)
|
|
64
|
+
else entry.value.completeExceptionally(UploadFailure("BOTA_UPLOAD_CREDENTIALS"))
|
|
65
|
+
}
|
|
66
|
+
fun reject(request: String) {synchronized(lock) {pending.remove(request)}?.value?.completeExceptionally(UploadFailure("BOTA_UPLOAD_CREDENTIALS"))}
|
|
67
|
+
suspend fun cancel(id: String) {
|
|
68
|
+
val task=synchronized(lock) {
|
|
69
|
+
if(!id.startsWith("byo-") || id.length>64) return
|
|
70
|
+
if(cancelled.size>=128) stopped=true else cancelled.add(id)
|
|
71
|
+
tasks[id]
|
|
72
|
+
}
|
|
73
|
+
task?.cancelAndJoin()
|
|
74
|
+
}
|
|
75
|
+
suspend fun stop() {
|
|
76
|
+
val current=synchronized(lock) {stopped=true;tasks.values.toList()}
|
|
77
|
+
current.forEach {it.cancel()};current.forEach {it.join()}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -257,13 +257,29 @@ internal class NativeUpload(
|
|
|
257
257
|
else -> fail("INVALID_DOCUMENT")
|
|
258
258
|
}
|
|
259
259
|
timestamp(selected.string("expires_at"))
|
|
260
|
-
|
|
260
|
+
var authorization = opaque(selected.string("authorization_base64"), 408, selected.string("authorization_sha256"))
|
|
261
|
+
if (input.recording.markersRequired) {
|
|
262
|
+
val path = sessionPath() + "/markers/authorization"
|
|
263
|
+
val admission = try {
|
|
264
|
+
retry { request(path + "?owner_revision=" + pointer!!.ownerRevision) }
|
|
265
|
+
} catch (error: UploadFailure) {
|
|
266
|
+
if (error.code != "BOTA_UPLOAD_HTTP_404" || selected.optString("state") !in setOf("", "created", "staging", "staged")) throw error
|
|
267
|
+
retry { request(path, JSONObject().put("owner_revision", pointer!!.ownerRevision)
|
|
268
|
+
.put("request_uuid", pointer!!.sessionId)) }
|
|
269
|
+
}
|
|
270
|
+
demand(admission.string("profile") == "recording-markers/1", "INVALID_DOCUMENT")
|
|
271
|
+
authorization += opaque(admission.string("context_base64"), 176)
|
|
272
|
+
authorization += opaque(admission.string("authorization_base64"), 280, admission.string("authorization_sha256"))
|
|
273
|
+
}
|
|
261
274
|
val material = EncryptedUploadV2Material(
|
|
262
275
|
materialId = UUID.randomUUID().toString(), recordingId = input.recording.uuid,
|
|
263
276
|
uploadSessionId = UUID.fromString(pointer!!.sessionId), ownerRevision = pointer!!.ownerRevision!!.toUInt(),
|
|
264
277
|
policy = nativePolicy, authorization = authorization,
|
|
265
278
|
stagingRequest = { evidence -> callbacks.withLock { staging(evidence) } },
|
|
266
279
|
submitManifest = { bytes, evidence -> callbacks.withLock { manifest(bytes, evidence) } },
|
|
280
|
+
submitMarkers = if (input.recording.markersRequired) { documents, evidence ->
|
|
281
|
+
callbacks.withLock { markers(documents, evidence) }
|
|
282
|
+
} else null,
|
|
267
283
|
finalize = { evidence -> callbacks.withLock { finalize(evidence) } },
|
|
268
284
|
completionReceipt = { evidence -> callbacks.withLock {
|
|
269
285
|
verifyEvidence(evidence)
|
|
@@ -349,6 +365,15 @@ internal class NativeUpload(
|
|
|
349
365
|
uploadCiphertext = false
|
|
350
366
|
sendManifest = false
|
|
351
367
|
}
|
|
368
|
+
private suspend fun markers(documents: List<ByteArray>, evidence: EncryptedUploadV2TransferEvidence) {
|
|
369
|
+
verifyEvidence(evidence)
|
|
370
|
+
demand(input.recording.markersRequired && documents.size in 2..4097 && documents[0].size == 200 &&
|
|
371
|
+
documents.drop(1).all { it.size in 216..402 }, "INVALID_DOCUMENT")
|
|
372
|
+
val pages = org.json.JSONArray()
|
|
373
|
+
documents.drop(1).forEach { pages.put(encoded(it)) }
|
|
374
|
+
retry { request(sessionPath() + "/markers/batch", JSONObject().put("owner_revision", pointer!!.ownerRevision)
|
|
375
|
+
.put("seal_base64", encoded(documents[0])).put("pages_base64", pages)) }
|
|
376
|
+
}
|
|
352
377
|
private suspend fun finalize(evidence: EncryptedUploadV2TransferEvidence) {
|
|
353
378
|
verifyEvidence(evidence)
|
|
354
379
|
for (poll in 0 until 60) {
|
|
@@ -356,7 +381,10 @@ internal class NativeUpload(
|
|
|
356
381
|
applyStatus(status)
|
|
357
382
|
if (status.string("state") == "published") {
|
|
358
383
|
demand(status.integer("plaintext_length") == input.recording.plaintextLength, "INVALID_DOCUMENT")
|
|
359
|
-
|
|
384
|
+
val audio = opaque(status.string("completion_receipt_base64"), 336, status.string("completion_receipt_sha256"))
|
|
385
|
+
receipt = if (input.recording.markersRequired) audio + opaque(
|
|
386
|
+
(status.opt("marker_completion_receipt_base64") as? String ?: fail("INVALID_DOCUMENT")), 296,
|
|
387
|
+
(status.opt("marker_completion_receipt_sha256") as? String ?: fail("INVALID_DOCUMENT"))) else audio
|
|
360
388
|
return
|
|
361
389
|
}
|
|
362
390
|
wait(2000, operation)
|
|
@@ -78,15 +78,18 @@ internal data class UploadScope(
|
|
|
78
78
|
internal data class UploadRecording(
|
|
79
79
|
val uuid: String, val generation: Long, val ciphertextLength: Long, val ciphertextSha256: String,
|
|
80
80
|
val startedAtMs: Long, val durationMs: Long, val plaintextLength: Long, val storageFormat: Long,
|
|
81
|
+
val markersRequired: Boolean = false,
|
|
81
82
|
) {
|
|
82
83
|
fun identity(): List<Any> = listOf(uuid, generation, ciphertextLength.toString(), ciphertextSha256,
|
|
83
|
-
startedAtMs.toString(), durationMs.toString(), plaintextLength.toString(), storageFormat)
|
|
84
|
+
startedAtMs.toString(), durationMs.toString(), plaintextLength.toString(), storageFormat) +
|
|
85
|
+
if (markersRequired) listOf(true) else emptyList()
|
|
84
86
|
|
|
85
87
|
companion object {
|
|
86
88
|
fun parse(json: JSONObject): UploadRecording = UploadRecording(uuid(json.string("uuid")),
|
|
87
89
|
json.integer("generation", 1, 4294967295), json.decimal("ciphertextLength", 1),
|
|
88
90
|
hashString(json.string("ciphertextSha256")), json.decimal("startedAtMs"), json.decimal("durationMs"),
|
|
89
|
-
json.decimal("plaintextLength"), json.integer("storageFormat", 3, 3)
|
|
91
|
+
json.decimal("plaintextLength"), json.integer("storageFormat", 3, 3),
|
|
92
|
+
if (json.has("markersRequired")) (json.get("markersRequired") as? Boolean ?: fail("INVALID_DOCUMENT")) else false).also {
|
|
90
93
|
demand(it.startedAtMs + it.durationMs <= 8640000000000000L)
|
|
91
94
|
}
|
|
92
95
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package dev.bota.sdk.reactnative.upload
|
|
2
|
+
|
|
3
|
+
import java.io.IOException
|
|
4
|
+
import java.util.concurrent.TimeUnit
|
|
5
|
+
import kotlin.coroutines.resume
|
|
6
|
+
import kotlin.coroutines.resumeWithException
|
|
7
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
8
|
+
import okhttp3.Call
|
|
9
|
+
import okhttp3.Callback
|
|
10
|
+
import okhttp3.OkHttpClient
|
|
11
|
+
import okhttp3.Request
|
|
12
|
+
import okhttp3.Response
|
|
13
|
+
import org.json.JSONObject
|
|
14
|
+
|
|
15
|
+
/** Read-only native HTTP boundary. Never sends opaque server fields over RN. */
|
|
16
|
+
internal class UploadStreamingStatus(private val client: OkHttpClient = OkHttpClient.Builder()
|
|
17
|
+
.followRedirects(false).followSslRedirects(false).retryOnConnectionFailure(false)
|
|
18
|
+
.connectTimeout(20, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).callTimeout(45, TimeUnit.SECONDS).build()) {
|
|
19
|
+
suspend fun read(input: String): String {
|
|
20
|
+
val json = sanitized("INVALID_INPUT") { JSONObject(input) }
|
|
21
|
+
val url = json.optString("url")
|
|
22
|
+
val token = json.optString("token")
|
|
23
|
+
val org = json.optString("organizationId")
|
|
24
|
+
demand(url.matches(Regex("https://[A-Za-z0-9.-]+(?::[0-9]+)?(?:/[A-Za-z0-9_-]+)+/recordings/rec_[A-Za-z0-9_-]+/streaming-status\\?session_id=[0-9a-f-]{36}")), "INVALID_INPUT")
|
|
25
|
+
demand(token.length in 1..16384 && token.all { it.code in 33..126 } &&
|
|
26
|
+
(org.isEmpty() || org.matches(Regex("[A-Za-z0-9_-]{1,128}"))), "INVALID_INPUT")
|
|
27
|
+
val request = Request.Builder().url(url).get().header("Authorization", "Bearer $token")
|
|
28
|
+
.header("Accept", "application/json").apply { if (org.isNotEmpty()) header("X-Organization-Id", org) }.build()
|
|
29
|
+
val call = client.newCall(request)
|
|
30
|
+
return suspendCancellableCoroutine { continuation ->
|
|
31
|
+
continuation.invokeOnCancellation { call.cancel() }
|
|
32
|
+
call.enqueue(object : Callback {
|
|
33
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
34
|
+
if (continuation.isActive) continuation.resumeWithException(UploadFailure("BOTA_UPLOAD_TRANSPORT"))
|
|
35
|
+
}
|
|
36
|
+
override fun onResponse(call: Call, response: Response) {
|
|
37
|
+
val result = runCatching {
|
|
38
|
+
response.use {
|
|
39
|
+
demand(response.code == 200 && response.request.url == request.url, "HTTP_${response.code}")
|
|
40
|
+
val body = response.body ?: fail("INVALID_DOCUMENT")
|
|
41
|
+
demand(body.contentLength() <= 16384, "INVALID_DOCUMENT")
|
|
42
|
+
val source = body.source(); source.request(16385)
|
|
43
|
+
demand(source.buffer.size <= 16384, "INVALID_DOCUMENT")
|
|
44
|
+
scalarStatus(source.readUtf8())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (continuation.isActive) result.fold({ continuation.resume(it) },
|
|
48
|
+
{ continuation.resumeWithException(if (it is UploadFailure) it else UploadFailure("BOTA_UPLOAD_INVALID_DOCUMENT")) })
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
companion object {
|
|
55
|
+
private val stringFields = setOf("profile", "recording_id", "session_id", "writer_epoch", "revision", "state")
|
|
56
|
+
private val numberFields = setOf("recording_generation", "received_count", "contiguous_sequence")
|
|
57
|
+
fun scalarStatus(body: String): String {
|
|
58
|
+
val json = sanitized("INVALID_DOCUMENT") { JSONObject(body) }
|
|
59
|
+
demand(json.keys().asSequence().toSet() == stringFields + numberFields + setOf("expected_count", "authorization_expired"), "INVALID_DOCUMENT")
|
|
60
|
+
stringFields.forEach { demand(json.get(it) is String && (json.get(it) as String).length <= 160, "INVALID_DOCUMENT") }
|
|
61
|
+
numberFields.forEach { demand(json.get(it) is Number, "INVALID_DOCUMENT") }
|
|
62
|
+
demand(json.isNull("expected_count") || json.get("expected_count") is Number, "INVALID_DOCUMENT")
|
|
63
|
+
demand(json.get("authorization_expired") is Boolean, "INVALID_DOCUMENT")
|
|
64
|
+
return json.toString()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@bota.dev/react-native-app-sdk",
|
|
4
|
-
"packageVersion": "2.0.0-beta.
|
|
4
|
+
"packageVersion": "2.0.0-beta.15",
|
|
5
5
|
"reactNativeVersion": "0.86.3",
|
|
6
6
|
"codegenLibrary": "BotaDeviceSDKSpec",
|
|
7
7
|
"nativeModule": "BotaDeviceSDK",
|
|
@@ -1216,6 +1216,13 @@
|
|
|
1216
1216
|
"typeAnnotation": {
|
|
1217
1217
|
"type": "NumberTypeAnnotation"
|
|
1218
1218
|
}
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
"name": "markersRequired",
|
|
1222
|
+
"optional": true,
|
|
1223
|
+
"typeAnnotation": {
|
|
1224
|
+
"type": "BooleanTypeAnnotation"
|
|
1225
|
+
}
|
|
1219
1226
|
}
|
|
1220
1227
|
]
|
|
1221
1228
|
},
|
|
@@ -4143,6 +4150,101 @@
|
|
|
4143
4150
|
}
|
|
4144
4151
|
],
|
|
4145
4152
|
"methods": [
|
|
4153
|
+
{
|
|
4154
|
+
"name": "syncByo",
|
|
4155
|
+
"optional": false,
|
|
4156
|
+
"typeAnnotation": {
|
|
4157
|
+
"type": "FunctionTypeAnnotation",
|
|
4158
|
+
"returnTypeAnnotation": {
|
|
4159
|
+
"type": "PromiseTypeAnnotation",
|
|
4160
|
+
"elementType": {
|
|
4161
|
+
"type": "StringTypeAnnotation"
|
|
4162
|
+
}
|
|
4163
|
+
},
|
|
4164
|
+
"params": [
|
|
4165
|
+
{
|
|
4166
|
+
"name": "inputJSON",
|
|
4167
|
+
"optional": false,
|
|
4168
|
+
"typeAnnotation": {
|
|
4169
|
+
"type": "StringTypeAnnotation"
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
]
|
|
4173
|
+
}
|
|
4174
|
+
},
|
|
4175
|
+
{
|
|
4176
|
+
"name": "cancelByo",
|
|
4177
|
+
"optional": false,
|
|
4178
|
+
"typeAnnotation": {
|
|
4179
|
+
"type": "FunctionTypeAnnotation",
|
|
4180
|
+
"returnTypeAnnotation": {
|
|
4181
|
+
"type": "PromiseTypeAnnotation",
|
|
4182
|
+
"elementType": {
|
|
4183
|
+
"type": "VoidTypeAnnotation"
|
|
4184
|
+
}
|
|
4185
|
+
},
|
|
4186
|
+
"params": [
|
|
4187
|
+
{
|
|
4188
|
+
"name": "operationId",
|
|
4189
|
+
"optional": false,
|
|
4190
|
+
"typeAnnotation": {
|
|
4191
|
+
"type": "StringTypeAnnotation"
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
]
|
|
4195
|
+
}
|
|
4196
|
+
},
|
|
4197
|
+
{
|
|
4198
|
+
"name": "readProtectedStreamingStatus",
|
|
4199
|
+
"optional": false,
|
|
4200
|
+
"typeAnnotation": {
|
|
4201
|
+
"type": "FunctionTypeAnnotation",
|
|
4202
|
+
"returnTypeAnnotation": {
|
|
4203
|
+
"type": "PromiseTypeAnnotation",
|
|
4204
|
+
"elementType": {
|
|
4205
|
+
"type": "StringTypeAnnotation"
|
|
4206
|
+
}
|
|
4207
|
+
},
|
|
4208
|
+
"params": [
|
|
4209
|
+
{
|
|
4210
|
+
"name": "requestId",
|
|
4211
|
+
"optional": false,
|
|
4212
|
+
"typeAnnotation": {
|
|
4213
|
+
"type": "StringTypeAnnotation"
|
|
4214
|
+
}
|
|
4215
|
+
},
|
|
4216
|
+
{
|
|
4217
|
+
"name": "inputJSON",
|
|
4218
|
+
"optional": false,
|
|
4219
|
+
"typeAnnotation": {
|
|
4220
|
+
"type": "StringTypeAnnotation"
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
]
|
|
4224
|
+
}
|
|
4225
|
+
},
|
|
4226
|
+
{
|
|
4227
|
+
"name": "cancelProtectedStreamingStatus",
|
|
4228
|
+
"optional": false,
|
|
4229
|
+
"typeAnnotation": {
|
|
4230
|
+
"type": "FunctionTypeAnnotation",
|
|
4231
|
+
"returnTypeAnnotation": {
|
|
4232
|
+
"type": "PromiseTypeAnnotation",
|
|
4233
|
+
"elementType": {
|
|
4234
|
+
"type": "VoidTypeAnnotation"
|
|
4235
|
+
}
|
|
4236
|
+
},
|
|
4237
|
+
"params": [
|
|
4238
|
+
{
|
|
4239
|
+
"name": "requestId",
|
|
4240
|
+
"optional": false,
|
|
4241
|
+
"typeAnnotation": {
|
|
4242
|
+
"type": "StringTypeAnnotation"
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
]
|
|
4246
|
+
}
|
|
4247
|
+
},
|
|
4146
4248
|
{
|
|
4147
4249
|
"name": "prepare",
|
|
4148
4250
|
"optional": false,
|
|
@@ -4274,11 +4376,11 @@
|
|
|
4274
4376
|
},
|
|
4275
4377
|
{
|
|
4276
4378
|
"path": "java/dev/bota/sdk/reactnative/NativeBotaUploadV2BackendSpec.java",
|
|
4277
|
-
"sha256": "
|
|
4379
|
+
"sha256": "a6b48b64cb3531d04b204172456115c041025d18db612d836174ced2617d50fe"
|
|
4278
4380
|
},
|
|
4279
4381
|
{
|
|
4280
4382
|
"path": "jni/BotaDeviceSDKSpec-generated.cpp",
|
|
4281
|
-
"sha256": "
|
|
4383
|
+
"sha256": "c98c66a2a320b56be0f7106e35a867d4664e6c8029836c153443cea4e1dd38ce"
|
|
4282
4384
|
},
|
|
4283
4385
|
{
|
|
4284
4386
|
"path": "jni/BotaDeviceSDKSpec.h",
|
|
@@ -4290,23 +4392,23 @@
|
|
|
4290
4392
|
},
|
|
4291
4393
|
{
|
|
4292
4394
|
"path": "jni/react/renderer/components/BotaDeviceSDKSpec/BotaDeviceSDKSpecJSI.h",
|
|
4293
|
-
"sha256": "
|
|
4395
|
+
"sha256": "6c3ab34b33b2e18b2237e389bbca5f813aa679c3ec66e61441ac8fea4a1904a5"
|
|
4294
4396
|
}
|
|
4295
4397
|
],
|
|
4296
4398
|
"ios": [
|
|
4297
4399
|
{
|
|
4298
4400
|
"path": "BotaDeviceSDKSpec/BotaDeviceSDKSpec-generated.mm",
|
|
4299
|
-
"sha256": "
|
|
4401
|
+
"sha256": "23bd8488e1e6a59795ec1845d7d6287731e7924e3211655899a58992efa3f6e1"
|
|
4300
4402
|
},
|
|
4301
4403
|
{
|
|
4302
4404
|
"path": "BotaDeviceSDKSpec/BotaDeviceSDKSpec.h",
|
|
4303
|
-
"sha256": "
|
|
4405
|
+
"sha256": "221da1ba25df2b6c8bac02a90fccdb701dc3644c0bfd9df31c75c117a2f9b47c"
|
|
4304
4406
|
},
|
|
4305
4407
|
{
|
|
4306
4408
|
"path": "BotaDeviceSDKSpecJSI.h",
|
|
4307
|
-
"sha256": "
|
|
4409
|
+
"sha256": "6c3ab34b33b2e18b2237e389bbca5f813aa679c3ec66e61441ac8fea4a1904a5"
|
|
4308
4410
|
}
|
|
4309
4411
|
]
|
|
4310
4412
|
},
|
|
4311
|
-
"contractDigest": "
|
|
4413
|
+
"contractDigest": "7b58a71c032bb3c46157b5c52ffc41a655af87186e5a9cd812e8330a93fed844"
|
|
4312
4414
|
}
|
package/ios/BotaDeviceSDK.mm
CHANGED
|
@@ -670,6 +670,7 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
|
|
|
670
670
|
durationMs:recording.durationMs()
|
|
671
671
|
plaintextLength:recording.plaintextLength()
|
|
672
672
|
storageFormat:recording.storageFormat().has_value() ? @(recording.storageFormat().value()) : nil
|
|
673
|
+
markersRequired:recording.markersRequired().value_or(false)
|
|
673
674
|
operationID:operationId
|
|
674
675
|
onProfileRequest:^(NSDictionary *request) {
|
|
675
676
|
[weakSelf emitOnEncryptedUploadV2ProfileRequested:request];
|
|
@@ -909,7 +909,7 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
@objc(syncEncryptedRecordingV2WithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:generation:ciphertextLength:ciphertextSHA256:startedAtMs:durationMs:plaintextLength:storageFormat:operationID:onProfileRequest:onProgress:completion:)
|
|
912
|
+
@objc(syncEncryptedRecordingV2WithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:generation:ciphertextLength:ciphertextSHA256:startedAtMs:durationMs:plaintextLength:storageFormat:markersRequired:operationID:onProfileRequest:onProgress:completion:)
|
|
913
913
|
public func syncEncryptedRecordingV2(
|
|
914
914
|
id: String,
|
|
915
915
|
serialNumber: String,
|
|
@@ -927,6 +927,7 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
927
927
|
durationMs: String?,
|
|
928
928
|
plaintextLength: String?,
|
|
929
929
|
storageFormat: NSNumber?,
|
|
930
|
+
markersRequired: Bool,
|
|
930
931
|
operationID: String,
|
|
931
932
|
onProfileRequest: @escaping @Sendable ([String: Any]) -> Void,
|
|
932
933
|
onProgress: @escaping @Sendable ([String: Any]) -> Void,
|
|
@@ -964,7 +965,8 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
|
|
|
964
965
|
startedAtMs: try Self.catalogDecimal(startedAtMs),
|
|
965
966
|
durationMs: try Self.catalogDecimal(durationMs),
|
|
966
967
|
plaintextLength: try Self.catalogDecimal(plaintextLength),
|
|
967
|
-
storageFormat: format
|
|
968
|
+
storageFormat: format,
|
|
969
|
+
markersRequired: markersRequired
|
|
968
970
|
),
|
|
969
971
|
operationID: operationID,
|
|
970
972
|
onProfileRequest: onProfileRequest,
|
|
@@ -646,6 +646,7 @@ actor BotaDeviceSDKAppleRecordings {
|
|
|
646
646
|
"durationMs": String(recording.durationMs),
|
|
647
647
|
"plaintextLength": String(recording.plaintextLength),
|
|
648
648
|
"storageFormat": Int(recording.storageFormat),
|
|
649
|
+
"markersRequired": recording.markersRequired,
|
|
649
650
|
]
|
|
650
651
|
}
|
|
651
652
|
|