@bota.dev/react-native-app-sdk 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/BotaDeviceSDK.podspec +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/android/build.gradle +76 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +172 -0
  7. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycle.kt +100 -0
  8. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +82 -0
  9. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidModels.kt +476 -0
  10. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTA.kt +58 -0
  11. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +180 -0
  12. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +751 -0
  13. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurity.kt +509 -0
  14. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFi.kt +125 -0
  15. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +46 -0
  16. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +723 -0
  17. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +33 -0
  18. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +211 -0
  19. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycleTest.kt +132 -0
  20. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +57 -0
  21. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTATest.kt +75 -0
  22. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +147 -0
  23. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +355 -0
  24. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurityTest.kt +523 -0
  25. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFiTest.kt +100 -0
  26. package/generated/codegen-contract.json +3593 -0
  27. package/ios/BotaDeviceSDK.mm +1300 -0
  28. package/ios/BotaDeviceSDKAppleBridge.swift +2260 -0
  29. package/ios/BotaDeviceSDKAppleDevices.swift +191 -0
  30. package/ios/BotaDeviceSDKAppleLifecycle.swift +167 -0
  31. package/ios/BotaDeviceSDKAppleLogs.swift +79 -0
  32. package/ios/BotaDeviceSDKAppleOTA.swift +73 -0
  33. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +178 -0
  34. package/ios/BotaDeviceSDKAppleRecordings.swift +670 -0
  35. package/ios/BotaDeviceSDKAppleSecurity.swift +586 -0
  36. package/ios/BotaDeviceSDKAppleWiFi.swift +128 -0
  37. package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +99 -0
  38. package/lib/commonjs/BotaClient.js +194 -0
  39. package/lib/commonjs/BotaClient.js.map +1 -0
  40. package/lib/commonjs/ble/constants.js +10 -0
  41. package/lib/commonjs/ble/constants.js.map +1 -0
  42. package/lib/commonjs/ble/deviceLogs.js +50 -0
  43. package/lib/commonjs/ble/deviceLogs.js.map +1 -0
  44. package/lib/commonjs/cache/DeviceStateCache.js +6 -0
  45. package/lib/commonjs/cache/DeviceStateCache.js.map +1 -0
  46. package/lib/commonjs/client.js +746 -0
  47. package/lib/commonjs/client.js.map +1 -0
  48. package/lib/commonjs/compatibility/runtime.js +44 -0
  49. package/lib/commonjs/compatibility/runtime.js.map +1 -0
  50. package/lib/commonjs/index.js +123 -0
  51. package/lib/commonjs/index.js.map +1 -0
  52. package/lib/commonjs/managers/DeviceManager.js +647 -0
  53. package/lib/commonjs/managers/DeviceManager.js.map +1 -0
  54. package/lib/commonjs/managers/OTAManager.js +140 -0
  55. package/lib/commonjs/managers/OTAManager.js.map +1 -0
  56. package/lib/commonjs/managers/RecordingManager.js +444 -0
  57. package/lib/commonjs/managers/RecordingManager.js.map +1 -0
  58. package/lib/commonjs/managers/StreamingSession.js +175 -0
  59. package/lib/commonjs/managers/StreamingSession.js.map +1 -0
  60. package/lib/commonjs/managers/types.js +6 -0
  61. package/lib/commonjs/managers/types.js.map +1 -0
  62. package/lib/commonjs/models/Device.js +2 -0
  63. package/lib/commonjs/models/Device.js.map +1 -0
  64. package/lib/commonjs/models/Recording.js +2 -0
  65. package/lib/commonjs/models/Recording.js.map +1 -0
  66. package/lib/commonjs/models/Status.js +6 -0
  67. package/lib/commonjs/models/Status.js.map +1 -0
  68. package/lib/commonjs/package.json +1 -0
  69. package/lib/commonjs/specs/NativeBotaDeviceSDK.js +9 -0
  70. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -0
  71. package/lib/commonjs/sync/syncStatus.js +178 -0
  72. package/lib/commonjs/sync/syncStatus.js.map +1 -0
  73. package/lib/commonjs/utils/errors.js +194 -0
  74. package/lib/commonjs/utils/errors.js.map +1 -0
  75. package/lib/commonjs/utils/logger.js +84 -0
  76. package/lib/commonjs/utils/logger.js.map +1 -0
  77. package/lib/module/BotaClient.js +189 -0
  78. package/lib/module/BotaClient.js.map +1 -0
  79. package/lib/module/ble/constants.js +6 -0
  80. package/lib/module/ble/constants.js.map +1 -0
  81. package/lib/module/ble/deviceLogs.js +45 -0
  82. package/lib/module/ble/deviceLogs.js.map +1 -0
  83. package/lib/module/cache/DeviceStateCache.js +4 -0
  84. package/lib/module/cache/DeviceStateCache.js.map +1 -0
  85. package/lib/module/client.js +740 -0
  86. package/lib/module/client.js.map +1 -0
  87. package/lib/module/compatibility/runtime.js +35 -0
  88. package/lib/module/compatibility/runtime.js.map +1 -0
  89. package/lib/module/index.js +17 -0
  90. package/lib/module/index.js.map +1 -0
  91. package/lib/module/managers/DeviceManager.js +641 -0
  92. package/lib/module/managers/DeviceManager.js.map +1 -0
  93. package/lib/module/managers/OTAManager.js +134 -0
  94. package/lib/module/managers/OTAManager.js.map +1 -0
  95. package/lib/module/managers/RecordingManager.js +438 -0
  96. package/lib/module/managers/RecordingManager.js.map +1 -0
  97. package/lib/module/managers/StreamingSession.js +168 -0
  98. package/lib/module/managers/StreamingSession.js.map +1 -0
  99. package/lib/module/managers/types.js +4 -0
  100. package/lib/module/managers/types.js.map +1 -0
  101. package/lib/module/models/Device.js +2 -0
  102. package/lib/module/models/Device.js.map +1 -0
  103. package/lib/module/models/Recording.js +2 -0
  104. package/lib/module/models/Recording.js.map +1 -0
  105. package/lib/module/models/Status.js +4 -0
  106. package/lib/module/models/Status.js.map +1 -0
  107. package/lib/module/specs/NativeBotaDeviceSDK.js +5 -0
  108. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -0
  109. package/lib/module/sync/syncStatus.js +174 -0
  110. package/lib/module/sync/syncStatus.js.map +1 -0
  111. package/lib/module/utils/errors.js +183 -0
  112. package/lib/module/utils/errors.js.map +1 -0
  113. package/lib/module/utils/logger.js +80 -0
  114. package/lib/module/utils/logger.js.map +1 -0
  115. package/lib/typescript/BotaClient.d.ts +40 -0
  116. package/lib/typescript/BotaClient.d.ts.map +1 -0
  117. package/lib/typescript/ble/constants.d.ts +4 -0
  118. package/lib/typescript/ble/constants.d.ts.map +1 -0
  119. package/lib/typescript/ble/deviceLogs.d.ts +10 -0
  120. package/lib/typescript/ble/deviceLogs.d.ts.map +1 -0
  121. package/lib/typescript/cache/DeviceStateCache.d.ts +14 -0
  122. package/lib/typescript/cache/DeviceStateCache.d.ts.map +1 -0
  123. package/lib/typescript/client.d.ts +208 -0
  124. package/lib/typescript/client.d.ts.map +1 -0
  125. package/lib/typescript/compatibility/runtime.d.ts +11 -0
  126. package/lib/typescript/compatibility/runtime.d.ts.map +1 -0
  127. package/lib/typescript/index.d.ts +19 -0
  128. package/lib/typescript/index.d.ts.map +1 -0
  129. package/lib/typescript/managers/DeviceManager.d.ts +117 -0
  130. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -0
  131. package/lib/typescript/managers/OTAManager.d.ts +25 -0
  132. package/lib/typescript/managers/OTAManager.d.ts.map +1 -0
  133. package/lib/typescript/managers/RecordingManager.d.ts +54 -0
  134. package/lib/typescript/managers/RecordingManager.d.ts.map +1 -0
  135. package/lib/typescript/managers/StreamingSession.d.ts +35 -0
  136. package/lib/typescript/managers/StreamingSession.d.ts.map +1 -0
  137. package/lib/typescript/managers/types.d.ts +19 -0
  138. package/lib/typescript/managers/types.d.ts.map +1 -0
  139. package/lib/typescript/models/Device.d.ts +392 -0
  140. package/lib/typescript/models/Device.d.ts.map +1 -0
  141. package/lib/typescript/models/Recording.d.ts +259 -0
  142. package/lib/typescript/models/Recording.d.ts.map +1 -0
  143. package/lib/typescript/models/Status.d.ts +106 -0
  144. package/lib/typescript/models/Status.d.ts.map +1 -0
  145. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +393 -0
  146. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -0
  147. package/lib/typescript/sync/syncStatus.d.ts +123 -0
  148. package/lib/typescript/sync/syncStatus.d.ts.map +1 -0
  149. package/lib/typescript/utils/errors.d.ts +83 -0
  150. package/lib/typescript/utils/errors.d.ts.map +1 -0
  151. package/lib/typescript/utils/logger.d.ts +36 -0
  152. package/lib/typescript/utils/logger.d.ts.map +1 -0
  153. package/package.json +100 -0
  154. package/scripts/bota_device_sdk_spm_workaround.rb +53 -0
  155. package/src/BotaClient.ts +231 -0
  156. package/src/ble/constants.ts +3 -0
  157. package/src/ble/deviceLogs.ts +61 -0
  158. package/src/cache/DeviceStateCache.ts +20 -0
  159. package/src/client.ts +1410 -0
  160. package/src/compatibility/runtime.ts +55 -0
  161. package/src/index.ts +146 -0
  162. package/src/managers/DeviceManager.ts +927 -0
  163. package/src/managers/OTAManager.ts +195 -0
  164. package/src/managers/RecordingManager.ts +578 -0
  165. package/src/managers/StreamingSession.ts +212 -0
  166. package/src/managers/types.ts +36 -0
  167. package/src/models/Device.ts +484 -0
  168. package/src/models/Recording.ts +281 -0
  169. package/src/models/Status.ts +128 -0
  170. package/src/specs/NativeBotaDeviceSDK.ts +542 -0
  171. package/src/sync/syncStatus.ts +243 -0
  172. package/src/utils/errors.ts +327 -0
  173. package/src/utils/logger.ts +122 -0
@@ -0,0 +1,670 @@
1
+ import BotaAppSDK
2
+ import Foundation
3
+
4
+ struct BotaDeviceSDKAppleRecordingFile: Equatable, Sendable {
5
+ let localPath: String
6
+ let isE2EEncrypted: Bool
7
+ let contentSHA256Hex: String?
8
+ }
9
+
10
+ protocol BotaDeviceSDKAppleRecordingClient: Sendable {
11
+ func listRecordings(_ device: ConnectedDevice) async throws -> [DeviceRecording]
12
+ func syncRecording(
13
+ _ device: ConnectedDevice,
14
+ recording: DeviceRecording,
15
+ sinkID: String
16
+ ) async throws -> AsyncThrowingStream<RecordingSyncEvent, Error>
17
+ func transferMetadata(sinkID: String) async -> RecordingTransferMetadata?
18
+ func syncEncryptedRecordingV2(
19
+ _ device: ConnectedDevice,
20
+ recording: EncryptedUploadV2Recording,
21
+ provider: @escaping EncryptedUploadV2ProfileProvider
22
+ ) async throws
23
+ func confirmRecording(_ device: ConnectedDevice, recordingUUID: String) async throws
24
+ func observeUploadOwnership(
25
+ _ device: ConnectedDevice,
26
+ recordingUUID: String,
27
+ uploadID: String,
28
+ destinationID: String
29
+ ) async throws -> AsyncThrowingStream<UploadOwnershipEvent, Error>
30
+ func streamRecording(
31
+ _ device: ConnectedDevice,
32
+ recordingUUID: String,
33
+ sinkID: String,
34
+ chunkSizeBytes: Int,
35
+ flushIntervalMilliseconds: UInt64,
36
+ destinationProvider: @escaping StreamingChunkDestinationProvider,
37
+ finalize: @escaping StreamingFinalizeHandler
38
+ ) async throws -> AsyncThrowingStream<StreamingRecordingEvent, Error>
39
+ func cancelCurrentOperation() async throws
40
+ }
41
+
42
+ struct BotaDeviceSDKSharedAppleRecordingClient: BotaDeviceSDKAppleRecordingClient {
43
+ private let recordings: RecordingManager
44
+
45
+ init(client: BotaDeviceClient = .shared) {
46
+ recordings = client.recordings
47
+ }
48
+
49
+ func listRecordings(_ device: ConnectedDevice) async throws -> [DeviceRecording] {
50
+ try await recordings.listRecordings(device)
51
+ }
52
+
53
+ func syncRecording(
54
+ _ device: ConnectedDevice,
55
+ recording: DeviceRecording,
56
+ sinkID: String
57
+ ) async throws -> AsyncThrowingStream<RecordingSyncEvent, Error> {
58
+ try await recordings.syncRecording(
59
+ device,
60
+ recording: recording,
61
+ sinkID: sinkID,
62
+ confirmOnCompletion: false
63
+ )
64
+ }
65
+
66
+ func transferMetadata(sinkID: String) async -> RecordingTransferMetadata? {
67
+ await recordings.transferMetadata(sinkID: sinkID)
68
+ }
69
+
70
+ func syncEncryptedRecordingV2(
71
+ _ device: ConnectedDevice,
72
+ recording: EncryptedUploadV2Recording,
73
+ provider: @escaping EncryptedUploadV2ProfileProvider
74
+ ) async throws {
75
+ try await recordings.syncEncryptedRecordingV2(
76
+ device,
77
+ recording: recording,
78
+ provider: provider
79
+ )
80
+ }
81
+
82
+ func confirmRecording(_ device: ConnectedDevice, recordingUUID: String) async throws {
83
+ try await recordings.confirmRecording(device, recordingUUID: recordingUUID)
84
+ }
85
+
86
+ func observeUploadOwnership(
87
+ _ device: ConnectedDevice,
88
+ recordingUUID: String,
89
+ uploadID: String,
90
+ destinationID: String
91
+ ) async throws -> AsyncThrowingStream<UploadOwnershipEvent, Error> {
92
+ try await recordings.observeUploadOwnership(
93
+ device,
94
+ recordingUUID: recordingUUID,
95
+ uploadID: uploadID,
96
+ destinationID: destinationID
97
+ )
98
+ }
99
+
100
+ func streamRecording(
101
+ _ device: ConnectedDevice,
102
+ recordingUUID: String,
103
+ sinkID: String,
104
+ chunkSizeBytes: Int,
105
+ flushIntervalMilliseconds: UInt64,
106
+ destinationProvider: @escaping StreamingChunkDestinationProvider,
107
+ finalize: @escaping StreamingFinalizeHandler
108
+ ) async throws -> AsyncThrowingStream<StreamingRecordingEvent, Error> {
109
+ try await recordings.streamRecording(
110
+ device,
111
+ recordingUUID: recordingUUID,
112
+ sinkID: sinkID,
113
+ chunkSizeBytes: chunkSizeBytes,
114
+ flushIntervalMilliseconds: flushIntervalMilliseconds,
115
+ destinationProvider: destinationProvider,
116
+ finalize: finalize
117
+ )
118
+ }
119
+
120
+ func cancelCurrentOperation() async throws {
121
+ try await recordings.cancelCurrentOperation()
122
+ }
123
+ }
124
+
125
+ actor BotaDeviceSDKAppleRecordings {
126
+ private enum RecordingError: LocalizedError {
127
+ case missingNativeFile
128
+ case missingUploadOwnershipResult
129
+ case invalidEncryptedUploadV2Selection
130
+ case encryptedUploadV2MaterialRejected
131
+ case missingEncryptedUploadV2Request
132
+ case invalidStreamingDestination
133
+ case requestRejected(String)
134
+
135
+ var errorDescription: String? {
136
+ switch self {
137
+ case .missingNativeFile:
138
+ "recording transfer completed without a native file"
139
+ case .missingUploadOwnershipResult:
140
+ "upload ownership completed without a result"
141
+ case .invalidEncryptedUploadV2Selection:
142
+ "encrypted upload v2 requires an explicit matching profile"
143
+ case .encryptedUploadV2MaterialRejected:
144
+ "encrypted upload v2 material was rejected"
145
+ case .missingEncryptedUploadV2Request:
146
+ "encrypted upload v2 profile request is no longer pending"
147
+ case .invalidStreamingDestination:
148
+ "streaming upload destination is invalid"
149
+ case let .requestRejected(message):
150
+ message
151
+ }
152
+ }
153
+ }
154
+
155
+ private let client: any BotaDeviceSDKAppleRecordingClient
156
+ private struct EncryptedUploadV2Request {
157
+ let recording: EncryptedUploadV2Recording
158
+ let continuation: CheckedContinuation<EncryptedUploadV2Material, Error>
159
+ }
160
+ private var encryptedUploadV2Requests: [String: EncryptedUploadV2Request] = [:]
161
+ private var destinationRequests: [
162
+ String: CheckedContinuation<StreamingUploadDestination, Error>
163
+ ] = [:]
164
+ private var finalizeRequests: [String: CheckedContinuation<Void, Error>] = [:]
165
+ private var activeStreamingSessionID: String?
166
+
167
+ init(
168
+ client: any BotaDeviceSDKAppleRecordingClient =
169
+ BotaDeviceSDKSharedAppleRecordingClient()
170
+ ) {
171
+ self.client = client
172
+ }
173
+
174
+ func listRecordings(_ device: ConnectedDevice) async throws -> [DeviceRecording] {
175
+ try await client.listRecordings(device)
176
+ }
177
+
178
+ func syncRecording(
179
+ _ device: ConnectedDevice,
180
+ recording: DeviceRecording,
181
+ sinkID: String,
182
+ onProgress: @escaping @Sendable (RecordingTransferProgress) -> Void
183
+ ) async throws -> BotaDeviceSDKAppleRecordingFile {
184
+ let events = try await client.syncRecording(
185
+ device,
186
+ recording: recording,
187
+ sinkID: sinkID
188
+ )
189
+ var path: String?
190
+ for try await event in events {
191
+ switch event {
192
+ case let .progress(progress):
193
+ onProgress(progress)
194
+ case let .completed(url):
195
+ path = url.path
196
+ }
197
+ }
198
+ guard let path else { throw RecordingError.missingNativeFile }
199
+ let metadata = await client.transferMetadata(sinkID: sinkID)
200
+ return BotaDeviceSDKAppleRecordingFile(
201
+ localPath: path,
202
+ isE2EEncrypted: metadata?.isE2EEncrypted ?? false,
203
+ contentSHA256Hex: metadata?.contentSHA256Hex
204
+ )
205
+ }
206
+
207
+ func syncEncryptedRecordingV2(
208
+ _ device: ConnectedDevice,
209
+ recording: EncryptedUploadV2Recording,
210
+ operationID: String,
211
+ onProfileRequest: @escaping @Sendable ([String: Any]) -> Void,
212
+ onProgress: @escaping @Sendable ([String: Any]) -> Void
213
+ ) async throws {
214
+ do {
215
+ try await client.syncEncryptedRecordingV2(
216
+ device,
217
+ recording: recording
218
+ ) { context in
219
+ let completedBytes = String(context.checkpoint?.nextCiphertextOffset ?? 0)
220
+ let checkpointRevision = context.checkpoint?.revision
221
+ onProgress(Self.encryptedUploadV2Progress(
222
+ operationID: operationID,
223
+ recordingUUID: recording.uuid,
224
+ phase: "profile_requested",
225
+ completedBytes: completedBytes,
226
+ totalBytes: String(recording.ciphertextLength),
227
+ checkpointRevision: checkpointRevision
228
+ ))
229
+ let material = try await self.requestEncryptedUploadV2Profile(
230
+ operationID: operationID,
231
+ context: context,
232
+ onRequest: onProfileRequest
233
+ )
234
+ onProgress(Self.encryptedUploadV2Progress(
235
+ operationID: operationID,
236
+ recordingUUID: recording.uuid,
237
+ phase: "transferring",
238
+ completedBytes: completedBytes,
239
+ totalBytes: String(recording.ciphertextLength),
240
+ checkpointRevision: checkpointRevision
241
+ ))
242
+ return material
243
+ }
244
+ onProgress(Self.encryptedUploadV2Progress(
245
+ operationID: operationID,
246
+ recordingUUID: recording.uuid,
247
+ phase: "completed",
248
+ completedBytes: String(recording.ciphertextLength),
249
+ totalBytes: String(recording.ciphertextLength)
250
+ ))
251
+ } catch {
252
+ let failure = Self.encryptedUploadV2Failure(error)
253
+ onProgress(Self.encryptedUploadV2Progress(
254
+ operationID: operationID,
255
+ recordingUUID: recording.uuid,
256
+ phase: "failed",
257
+ completedBytes: "0",
258
+ totalBytes: String(recording.ciphertextLength),
259
+ errorCode: failure.code,
260
+ retryable: failure.retryable,
261
+ protocolStatus: failure.protocolStatus
262
+ ))
263
+ throw error
264
+ }
265
+ }
266
+
267
+ func resolveEncryptedUploadV2Profile(
268
+ requestID: String,
269
+ profile: String,
270
+ uploadSessionID: String,
271
+ ownerRevision: UInt32,
272
+ securityPolicy: String,
273
+ materialRegistrationID: String
274
+ ) throws {
275
+ guard let request = encryptedUploadV2Requests.removeValue(forKey: requestID) else {
276
+ throw RecordingError.missingEncryptedUploadV2Request
277
+ }
278
+ do {
279
+ guard profile == "encrypted_upload_v2",
280
+ let sessionID = UUID(uuidString: uploadSessionID)
281
+ else {
282
+ throw RecordingError.invalidEncryptedUploadV2Selection
283
+ }
284
+ let material = try BotaDeviceSDKEncryptedUploadV2Materials.consume(
285
+ materialRegistrationID,
286
+ recording: request.recording,
287
+ uploadSessionID: sessionID,
288
+ ownerRevision: ownerRevision,
289
+ securityPolicy: securityPolicy
290
+ )
291
+ request.continuation.resume(returning: material)
292
+ } catch {
293
+ request.continuation.resume(throwing: error)
294
+ throw error
295
+ }
296
+ }
297
+
298
+ func rejectEncryptedUploadV2Profile(requestID: String, errorCode: String) throws {
299
+ guard let request = encryptedUploadV2Requests.removeValue(forKey: requestID) else {
300
+ throw RecordingError.missingEncryptedUploadV2Request
301
+ }
302
+ guard errorCode == "application_material_rejected" else {
303
+ request.continuation.resume(throwing: RecordingError.invalidEncryptedUploadV2Selection)
304
+ throw RecordingError.invalidEncryptedUploadV2Selection
305
+ }
306
+ request.continuation.resume(throwing: RecordingError.encryptedUploadV2MaterialRejected)
307
+ }
308
+
309
+ func observeUploadOwnership(
310
+ _ device: ConnectedDevice,
311
+ recordingUUID: String,
312
+ uploadID: String,
313
+ destinationID: String,
314
+ onProgress: @escaping @Sendable (RecordingTransferProgress) -> Void
315
+ ) async throws -> UploadOwnershipResult {
316
+ let events = try await client.observeUploadOwnership(
317
+ device,
318
+ recordingUUID: recordingUUID,
319
+ uploadID: uploadID,
320
+ destinationID: destinationID
321
+ )
322
+ var result: UploadOwnershipResult?
323
+ for try await event in events {
324
+ switch event {
325
+ case let .progress(progress):
326
+ onProgress(progress)
327
+ case let .result(value):
328
+ result = value
329
+ }
330
+ }
331
+ guard let result else { throw RecordingError.missingUploadOwnershipResult }
332
+ return result
333
+ }
334
+
335
+ func confirmRecording(_ device: ConnectedDevice, recordingUUID: String) async throws {
336
+ try await client.confirmRecording(device, recordingUUID: recordingUUID)
337
+ }
338
+
339
+ func streamRecording(
340
+ _ device: ConnectedDevice,
341
+ recordingUUID: String,
342
+ sessionID: String,
343
+ chunkSizeBytes: Int,
344
+ flushIntervalMilliseconds: UInt64,
345
+ onProgress: @escaping @Sendable ([String: Any]) -> Void,
346
+ onDestinationRequest: @escaping @Sendable ([String: Any]) -> Void,
347
+ onFinalizeRequest: @escaping @Sendable ([String: Any]) -> Void
348
+ ) async throws -> UInt64 {
349
+ activeStreamingSessionID = sessionID
350
+ var bytesReceived: UInt64 = 0
351
+ var chunksUploaded: UInt32 = 0
352
+ onProgress(Self.progress(
353
+ sessionID: sessionID,
354
+ state: "streaming",
355
+ bytesReceived: bytesReceived,
356
+ chunksUploaded: chunksUploaded
357
+ ))
358
+ defer { activeStreamingSessionID = nil }
359
+ let events = try await client.streamRecording(
360
+ device,
361
+ recordingUUID: recordingUUID,
362
+ sinkID: sessionID,
363
+ chunkSizeBytes: chunkSizeBytes,
364
+ flushIntervalMilliseconds: flushIntervalMilliseconds,
365
+ destinationProvider: { request in
366
+ try await self.requestDestination(
367
+ sessionID: sessionID,
368
+ request: request,
369
+ onRequest: onDestinationRequest
370
+ )
371
+ },
372
+ finalize: { metadata in
373
+ try await self.requestFinalize(
374
+ sessionID: sessionID,
375
+ metadata: metadata,
376
+ onRequest: onFinalizeRequest
377
+ )
378
+ }
379
+ )
380
+ for try await event in events {
381
+ switch event {
382
+ case let .paused(completedBytes):
383
+ bytesReceived = completedBytes
384
+ onProgress(Self.progress(
385
+ sessionID: sessionID,
386
+ state: "paused",
387
+ bytesReceived: bytesReceived,
388
+ chunksUploaded: chunksUploaded
389
+ ))
390
+ case .resumed:
391
+ onProgress(Self.progress(
392
+ sessionID: sessionID,
393
+ state: "streaming",
394
+ bytesReceived: bytesReceived,
395
+ chunksUploaded: chunksUploaded
396
+ ))
397
+ case let .completed(totalBytes, uploaded, _):
398
+ bytesReceived = totalBytes
399
+ chunksUploaded = uploaded
400
+ onProgress(Self.progress(
401
+ sessionID: sessionID,
402
+ state: "completing",
403
+ bytesReceived: bytesReceived,
404
+ chunksUploaded: chunksUploaded
405
+ ))
406
+ }
407
+ }
408
+ return bytesReceived
409
+ }
410
+
411
+ func resolveStreamingDestination(
412
+ requestID: String,
413
+ url: String,
414
+ method: String,
415
+ contentType: String,
416
+ bearerToken: String?
417
+ ) {
418
+ guard let continuation = destinationRequests.removeValue(forKey: requestID) else { return }
419
+ guard let url = URL(string: url),
420
+ let method = StreamingUploadMethod(rawValue: method)
421
+ else {
422
+ continuation.resume(throwing: RecordingError.invalidStreamingDestination)
423
+ return
424
+ }
425
+ continuation.resume(returning: .init(
426
+ url: url,
427
+ method: method,
428
+ contentType: contentType,
429
+ bearerToken: bearerToken
430
+ ))
431
+ }
432
+
433
+ func rejectStreamingDestination(requestID: String, message: String) {
434
+ destinationRequests.removeValue(forKey: requestID)?.resume(
435
+ throwing: RecordingError.requestRejected(message)
436
+ )
437
+ }
438
+
439
+ func resolveStreamingFinalize(requestID: String) {
440
+ finalizeRequests.removeValue(forKey: requestID)?.resume()
441
+ }
442
+
443
+ func rejectStreamingFinalize(requestID: String, message: String) {
444
+ finalizeRequests.removeValue(forKey: requestID)?.resume(
445
+ throwing: RecordingError.requestRejected(message)
446
+ )
447
+ }
448
+
449
+ func abortStreaming(sessionID: String) async {
450
+ guard activeStreamingSessionID == sessionID else { return }
451
+ rejectPendingRequests(message: "streaming session was aborted")
452
+ try? await client.cancelCurrentOperation()
453
+ }
454
+
455
+ func cancelAll() async {
456
+ rejectPendingRequests(message: "recording operations were cancelled")
457
+ try? await client.cancelCurrentOperation()
458
+ }
459
+
460
+ private func requestEncryptedUploadV2Profile(
461
+ operationID: String,
462
+ context: EncryptedUploadV2ProviderContext,
463
+ onRequest: @escaping @Sendable ([String: Any]) -> Void
464
+ ) async throws -> EncryptedUploadV2Material {
465
+ let requestID = UUID().uuidString.lowercased()
466
+ return try await withCheckedThrowingContinuation { continuation in
467
+ encryptedUploadV2Requests[requestID] = .init(
468
+ recording: context.recording,
469
+ continuation: continuation
470
+ )
471
+ var request: [String: Any] = [
472
+ "requestId": requestID,
473
+ "operationId": operationID,
474
+ "recording": Self.encryptedUploadV2Recording(context.recording),
475
+ "capability": Self.encryptedUploadV2Capability(context.capability),
476
+ ]
477
+ if let checkpoint = context.checkpoint {
478
+ request["checkpoint"] = Self.encryptedUploadV2Checkpoint(checkpoint)
479
+ }
480
+ onRequest(request)
481
+ }
482
+ }
483
+
484
+ private func requestDestination(
485
+ sessionID: String,
486
+ request: StreamingChunkRequest,
487
+ onRequest: @escaping @Sendable ([String: Any]) -> Void
488
+ ) async throws -> StreamingUploadDestination {
489
+ let requestID = UUID().uuidString
490
+ return try await withCheckedThrowingContinuation { continuation in
491
+ destinationRequests[requestID] = continuation
492
+ onRequest([
493
+ "requestId": requestID,
494
+ "sessionId": sessionID,
495
+ "sequence": request.sequence,
496
+ "encrypted": request.isEncrypted,
497
+ ])
498
+ }
499
+ }
500
+
501
+ private func requestFinalize(
502
+ sessionID: String,
503
+ metadata: StreamingFinalizeMetadata,
504
+ onRequest: @escaping @Sendable ([String: Any]) -> Void
505
+ ) async throws {
506
+ let requestID = UUID().uuidString
507
+ try await withCheckedThrowingContinuation { continuation in
508
+ finalizeRequests[requestID] = continuation
509
+ onRequest([
510
+ "requestId": requestID,
511
+ "sessionId": sessionID,
512
+ "totalChunks": metadata.totalChunks,
513
+ "durationMs": metadata.durationMilliseconds,
514
+ "fileSizeBytes": metadata.fileSizeBytes,
515
+ "encrypted": metadata.isEncrypted,
516
+ ])
517
+ }
518
+ }
519
+
520
+ private func rejectPendingRequests(message: String) {
521
+ let error = RecordingError.requestRejected(message)
522
+ let encryptedUploadV2 = encryptedUploadV2Requests.values
523
+ let destinations = destinationRequests.values
524
+ let finalizations = finalizeRequests.values
525
+ destinationRequests.removeAll()
526
+ finalizeRequests.removeAll()
527
+ encryptedUploadV2Requests.removeAll()
528
+ encryptedUploadV2.forEach { $0.continuation.resume(throwing: error) }
529
+ destinations.forEach { $0.resume(throwing: error) }
530
+ finalizations.forEach { $0.resume(throwing: error) }
531
+ }
532
+
533
+ private static func encryptedUploadV2Recording(
534
+ _ recording: EncryptedUploadV2Recording
535
+ ) -> [String: Any] {
536
+ [
537
+ "uuid": recording.uuid,
538
+ "generation": recording.generation,
539
+ "ciphertextLength": String(recording.ciphertextLength),
540
+ "ciphertextSha256": recording.ciphertextSHA256.hexString,
541
+ ]
542
+ }
543
+
544
+ private static func encryptedUploadV2Capability(
545
+ _ snapshot: EncryptedUploadV2CapabilitySnapshot
546
+ ) -> [String: Any] {
547
+ let values = snapshot.capabilities
548
+ return [
549
+ "encodingVersion": Int(snapshot.rawValue.first ?? 0),
550
+ "transferProfileVersion": Int(snapshot.rawValue.dropFirst().first ?? 0),
551
+ "rawValueHex": snapshot.rawValue.hexString,
552
+ "sha256Hex": snapshot.sha256.hexString,
553
+ "flags": values.flags,
554
+ "maximumSignedBlobBytes": values.maximumSignedBlobBytes,
555
+ "maximumManifestBytes": values.maximumManifestBytes,
556
+ "maximumDataPayloadBytes": values.maximumDataPayloadBytes,
557
+ "maximumWindowPackets": values.maximumWindowPackets,
558
+ "durableCheckpointIntervalBlocks": values.durableCheckpointIntervalBlocks,
559
+ "maximumMissingSequences": values.maximumMissingSequences,
560
+ ]
561
+ }
562
+
563
+ private static func encryptedUploadV2Checkpoint(
564
+ _ checkpoint: EncryptedUploadV2Checkpoint
565
+ ) -> [String: Any] {
566
+ var value: [String: Any] = [
567
+ "version": 1,
568
+ "uploadSessionId": checkpoint.uploadSessionID.uuidString.lowercased(),
569
+ "ownerRevision": checkpoint.ownerRevision,
570
+ "revision": checkpoint.revision,
571
+ "nextCiphertextOffset": String(checkpoint.nextCiphertextOffset),
572
+ "prefixSha256": checkpoint.prefixSHA256.hexString,
573
+ "transportSessionId": String(checkpoint.transportSessionID),
574
+ "sinkRegistrationId": checkpoint.sinkID,
575
+ "windowPackets": checkpoint.windowPackets,
576
+ "dataPayloadBytes": checkpoint.dataPayloadBytes,
577
+ ]
578
+ if let sequence = checkpoint.highestContiguousSequence {
579
+ value["highestContiguousSequence"] = sequence
580
+ }
581
+ return value
582
+ }
583
+
584
+ private static func encryptedUploadV2Progress(
585
+ operationID: String,
586
+ recordingUUID: String,
587
+ phase: String,
588
+ completedBytes: String,
589
+ totalBytes: String,
590
+ checkpointRevision: UInt32? = nil,
591
+ errorCode: String? = nil,
592
+ retryable: Bool? = nil,
593
+ protocolStatus: UInt16? = nil
594
+ ) -> [String: Any] {
595
+ var value: [String: Any] = [
596
+ "operationId": operationID,
597
+ "recordingUuid": recordingUUID,
598
+ "phase": phase,
599
+ "completedBytes": completedBytes,
600
+ "totalBytes": totalBytes,
601
+ ]
602
+ if let checkpointRevision { value["checkpointRevision"] = checkpointRevision }
603
+ if let errorCode { value["errorCode"] = errorCode }
604
+ if let retryable { value["retryable"] = retryable }
605
+ if let protocolStatus { value["protocolStatus"] = protocolStatus }
606
+ return value
607
+ }
608
+
609
+ private static func encryptedUploadV2Failure(
610
+ _ error: Error
611
+ ) -> (code: String, retryable: Bool, protocolStatus: UInt16?) {
612
+ guard let sdkError = error as? BotaSDKError else {
613
+ return (
614
+ error is CancellationError ? "cancelled" : "application_material_rejected",
615
+ false,
616
+ nil
617
+ )
618
+ }
619
+ return (sdkError.code.stableName, sdkError.retryable, sdkError.protocolStatus)
620
+ }
621
+
622
+ private static func progress(
623
+ sessionID: String,
624
+ state: String,
625
+ bytesReceived: UInt64,
626
+ chunksUploaded: UInt32
627
+ ) -> [String: Any] {
628
+ [
629
+ "sessionId": sessionID,
630
+ "state": state,
631
+ "bytesReceived": bytesReceived,
632
+ "chunksUploaded": chunksUploaded,
633
+ ]
634
+ }
635
+ }
636
+
637
+ private extension Data {
638
+ var hexString: String {
639
+ map { String(format: "%02x", $0) }.joined()
640
+ }
641
+ }
642
+
643
+ private extension BotaSDKErrorCode {
644
+ var stableName: String {
645
+ switch self {
646
+ case .invalidInput: "invalid_input"
647
+ case .truncatedPacket: "truncated_packet"
648
+ case .unknownPacket: "unknown_packet"
649
+ case .payloadTooLarge: "payload_too_large"
650
+ case .unsupportedCapability: "unsupported_capability"
651
+ case .unsupportedOperation: "unsupported_operation"
652
+ case .featureUnavailable: "feature_unavailable"
653
+ case .operationInProgress: "operation_in_progress"
654
+ case .unexpectedEvent: "unexpected_event"
655
+ case .deviceNotFound: "device_not_found"
656
+ case .identityMismatch: "identity_mismatch"
657
+ case .connectionFailed: "connection_failed"
658
+ case .persistenceFailed: "persistence_failed"
659
+ case .notConnected: "not_connected"
660
+ case .timeout: "timeout"
661
+ case .cancelled: "cancelled"
662
+ case .protocolRejected: "protocol_rejected"
663
+ case .integrityFailed: "integrity_failed"
664
+ case .uploadOwnershipUnknown: "upload_ownership_unknown"
665
+ case .downloadFailed: "download_failed"
666
+ case .internal: "internal"
667
+ case .unknown: "unknown"
668
+ }
669
+ }
670
+ }