@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
package/src/client.ts ADDED
@@ -0,0 +1,1410 @@
1
+ import type {
2
+ NativeCapabilities,
3
+ NativeConnectedDevice,
4
+ NativeConfiguration,
5
+ NativeDeviceStatus,
6
+ NativeDeviceConnectionSettings,
7
+ NativeDeviceRecording,
8
+ NativeDiscoveredDevice,
9
+ NativeEncryptedUploadV2Capability,
10
+ NativeEncryptedUploadV2Checkpoint,
11
+ NativeEncryptedUploadV2Progress,
12
+ NativeEncryptedUploadV2Recording,
13
+ NativeFactoryResetCompletion,
14
+ NativeFactoryResetGrantRequest,
15
+ NativeFirmwareUpdateProgress,
16
+ NativeRecordingControlResult,
17
+ NativeRecordingState,
18
+ NativeRecordingTransferProgress,
19
+ NativeRecordingUploadRequest,
20
+ NativeStreamingChunkDestinationRequest,
21
+ NativeStreamingFinalizeRequest,
22
+ NativeStreamingStartRequest,
23
+ NativeStreamingUploadDestination,
24
+ NativeProvisioningMaterialRequest,
25
+ NativeUploadOwnershipResult,
26
+ NativeWiFiConfigResult,
27
+ NativeWiFiStatusInfo,
28
+ NativeDeviceWiFiScanResult,
29
+ Spec,
30
+ } from './specs/NativeBotaDeviceSDK';
31
+ import type {
32
+ ConnectedDevice,
33
+ ConnectionType,
34
+ ConnectionState,
35
+ DeviceConnectionSettings,
36
+ DeviceState,
37
+ DeviceStatus,
38
+ DeviceType,
39
+ DeviceLogEvent,
40
+ DiscoveredDevice,
41
+ PairingState,
42
+ ReconnectOptions,
43
+ ScanOptions,
44
+ LteStatus,
45
+ WiFiConfigGrant,
46
+ WiFiConfigResult,
47
+ WiFiCredentials,
48
+ WiFiStatus,
49
+ WiFiStatusInfo,
50
+ BleFactoryResetResultPersister,
51
+ DeviceWiFiScanResult,
52
+ Environment,
53
+ RecordingState,
54
+ WifiStatus,
55
+ } from './models/Device';
56
+ import { reportCompatibilityDisconnection } from './compatibility/runtime';
57
+ import type {
58
+ AudioCodec,
59
+ DeviceRecording,
60
+ UploadTask,
61
+ } from './models/Recording';
62
+
63
+ export type BotaLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
64
+
65
+ export type BotaDeviceSDKConfiguration = {
66
+ applicationSupportDirectory?: string;
67
+ logLevel?: BotaLogLevel;
68
+ };
69
+
70
+ export type BotaDeviceSDKState =
71
+ | 'uninitialized'
72
+ | 'initializing'
73
+ | 'ready'
74
+ | 'error';
75
+
76
+ export type BotaDeviceSDKCapabilities = NativeCapabilities;
77
+
78
+ export type BotaEventSubscription = {
79
+ remove(): void;
80
+ };
81
+
82
+ export type BotaAsyncEventSubscription = {
83
+ remove(): Promise<void>;
84
+ };
85
+
86
+ export type BotaProvisioningMaterialRequest = Omit<
87
+ NativeProvisioningMaterialRequest,
88
+ 'requestId'
89
+ >;
90
+
91
+ export type BotaProvisioningMaterial = {
92
+ apiEndpoint: string;
93
+ deviceToken: string;
94
+ mtu: number;
95
+ };
96
+
97
+ export type BotaProvisioningMaterialProvider = (
98
+ request: BotaProvisioningMaterialRequest
99
+ ) => Promise<BotaProvisioningMaterial>;
100
+
101
+ export type BotaDeviceSDKProvisioningClient = {
102
+ provision(
103
+ device: ConnectedDevice,
104
+ provider: BotaProvisioningMaterialProvider
105
+ ): Promise<void>;
106
+ deprovision(
107
+ device: ConnectedDevice,
108
+ grantBlob: string
109
+ ): Promise<BotaDeprovisionResult>;
110
+ readConnectionSettings(device: ConnectedDevice): Promise<DeviceConnectionSettings>;
111
+ writeConnectionSettings(
112
+ device: ConnectedDevice,
113
+ settings: DeviceConnectionSettings
114
+ ): Promise<void>;
115
+ };
116
+
117
+ export type BotaDeprovisionResult = {
118
+ success: boolean;
119
+ error?: string;
120
+ };
121
+
122
+ export type BotaDeviceSDKControlClient = {
123
+ isProvisioned(device: ConnectedDevice): Promise<boolean>;
124
+ readPublicKey(device: ConnectedDevice): Promise<string | null>;
125
+ readAuthNonce(device: ConnectedDevice): Promise<string | null>;
126
+ setApiEndpoint(
127
+ device: ConnectedDevice,
128
+ environment: Environment
129
+ ): Promise<void>;
130
+ deliverCertificate(
131
+ device: ConnectedDevice,
132
+ certificatePem: string,
133
+ privateKeyPem: string
134
+ ): Promise<void>;
135
+ deliverBackendPublicKey(
136
+ device: ConnectedDevice,
137
+ publicKey: Uint8Array
138
+ ): Promise<void>;
139
+ writeGrant(device: ConnectedDevice, grantBlob: string): Promise<void>;
140
+ syncTime(device: ConnectedDevice): Promise<void>;
141
+ requestStartRecording(
142
+ device: ConnectedDevice,
143
+ grantBlob: string
144
+ ): Promise<BotaRecordingControlResult>;
145
+ requestStopRecording(
146
+ device: ConnectedDevice,
147
+ grantBlob: string
148
+ ): Promise<BotaRecordingControlResult>;
149
+ readRecordingState(device: ConnectedDevice): Promise<RecordingState>;
150
+ subscribeToRecordingState(
151
+ device: ConnectedDevice,
152
+ onState: (state: RecordingState) => void
153
+ ): Promise<BotaAsyncEventSubscription>;
154
+ };
155
+
156
+ export type BotaRecordingControlResult = {
157
+ success: boolean;
158
+ error?: string;
159
+ };
160
+
161
+ export type BotaFactoryResetGrantRequest = Omit<
162
+ NativeFactoryResetGrantRequest,
163
+ 'requestId'
164
+ >;
165
+
166
+ export type BotaFactoryResetGrantProvider = (
167
+ request: BotaFactoryResetGrantRequest
168
+ ) => Promise<string>;
169
+
170
+ export type BotaFactoryResetOptions = {
171
+ commandId: string;
172
+ bindingGeneration: number;
173
+ };
174
+
175
+ export type BotaFactoryResetCompletion = NativeFactoryResetCompletion;
176
+
177
+ export type BotaDeviceSDKFactoryResetClient = {
178
+ factoryReset(
179
+ device: ConnectedDevice,
180
+ options: BotaFactoryResetOptions,
181
+ provider: BotaFactoryResetGrantProvider,
182
+ persistResult?: BleFactoryResetResultPersister
183
+ ): Promise<BotaFactoryResetCompletion>;
184
+ resumePendingFactoryReset(
185
+ device: ConnectedDevice,
186
+ currentBindingGeneration: number,
187
+ persistResult?: BleFactoryResetResultPersister
188
+ ): Promise<BotaFactoryResetCompletion | null>;
189
+ };
190
+
191
+ export type BotaRecordingTransferProgress = {
192
+ completedBytes: number;
193
+ totalBytes: number;
194
+ };
195
+
196
+ export type BotaEncryptedUploadV2Recording = NativeEncryptedUploadV2Recording;
197
+
198
+ export type BotaEncryptedUploadV2Capability = NativeEncryptedUploadV2Capability;
199
+
200
+ export type BotaEncryptedUploadV2Checkpoint = NativeEncryptedUploadV2Checkpoint;
201
+
202
+ export type BotaEncryptedUploadV2ProviderContext = {
203
+ recording: BotaEncryptedUploadV2Recording;
204
+ capability: BotaEncryptedUploadV2Capability;
205
+ checkpoint?: BotaEncryptedUploadV2Checkpoint;
206
+ };
207
+
208
+ export type BotaEncryptedUploadV2SecurityPolicy =
209
+ | 'legacy_allowed'
210
+ | 'v2_preferred'
211
+ | 'v2_required';
212
+
213
+ export type BotaEncryptedUploadV2ProfileDecision = {
214
+ profile: 'encrypted_upload_v2';
215
+ uploadSessionId: string;
216
+ ownerRevision: number;
217
+ securityPolicy: BotaEncryptedUploadV2SecurityPolicy;
218
+ materialRegistrationId: string;
219
+ };
220
+
221
+ export type BotaEncryptedUploadV2ProfileProvider = (
222
+ context: BotaEncryptedUploadV2ProviderContext
223
+ ) => Promise<BotaEncryptedUploadV2ProfileDecision>;
224
+
225
+ export type BotaEncryptedUploadV2ProgressPhase =
226
+ | 'profile_requested'
227
+ | 'transferring'
228
+ | 'completed'
229
+ | 'failed';
230
+
231
+ export type BotaEncryptedUploadV2Progress = {
232
+ recordingUuid: string;
233
+ phase: BotaEncryptedUploadV2ProgressPhase;
234
+ completedBytes: string;
235
+ totalBytes: string;
236
+ checkpointRevision?: number;
237
+ errorCode?: string;
238
+ retryable?: boolean;
239
+ protocolStatus?: number;
240
+ };
241
+
242
+ export type BotaUploadOwnershipRequest = {
243
+ recordingUuid: string;
244
+ uploadId: string;
245
+ destinationId: string;
246
+ };
247
+
248
+ export type BotaUploadOwnershipResult =
249
+ | { kind: 'device_upload_completed' }
250
+ | { kind: 'device_upload_preserved'; uploadId: string }
251
+ | {
252
+ kind: 'bluetooth_fallback';
253
+ recordingUuid: string;
254
+ uploadId: string;
255
+ destinationId: string;
256
+ };
257
+
258
+ export type BotaFirmwareImage = {
259
+ version: string;
260
+ sizeBytes: number;
261
+ crc32: number;
262
+ url: string;
263
+ };
264
+
265
+ export type BotaFirmwareUpdatePhase =
266
+ | 'downloading'
267
+ | 'awaiting_device'
268
+ | 'transferring'
269
+ | 'verifying'
270
+ | 'rebooting'
271
+ | 'reconnecting'
272
+ | 'complete';
273
+
274
+ export type BotaFirmwareUpdateProgress = {
275
+ phase: BotaFirmwareUpdatePhase;
276
+ completedBytes: number;
277
+ totalBytes: number;
278
+ };
279
+
280
+ export type BotaDeviceSDKOTAClient = {
281
+ updateFirmware(
282
+ device: ConnectedDevice,
283
+ image: BotaFirmwareImage,
284
+ onProgress?: (progress: BotaFirmwareUpdateProgress) => void
285
+ ): Promise<void>;
286
+ cancelFirmwareUpdate(): Promise<void>;
287
+ };
288
+
289
+ export type BotaDeviceSDKLogClient = {
290
+ subscribe(
291
+ device: ConnectedDevice,
292
+ onLine: (line: DeviceLogEvent) => void
293
+ ): Promise<BotaAsyncEventSubscription>;
294
+ };
295
+
296
+ export type BotaDeviceSDKWiFiClient = {
297
+ configure(
298
+ device: ConnectedDevice,
299
+ credentials: WiFiCredentials,
300
+ grant: WiFiConfigGrant
301
+ ): Promise<WiFiConfigResult>;
302
+ disconnect(device: ConnectedDevice): Promise<WiFiConfigResult>;
303
+ readStatus(device: ConnectedDevice): Promise<WiFiStatusInfo>;
304
+ subscribeToStatus(
305
+ device: ConnectedDevice,
306
+ onStatus: (status: WiFiStatusInfo) => void
307
+ ): Promise<BotaAsyncEventSubscription>;
308
+ scanNetworks(device: ConnectedDevice): Promise<DeviceWiFiScanResult>;
309
+ };
310
+
311
+ export type BotaDeviceSDKRecordingClient = {
312
+ listRecordings(device: ConnectedDevice): Promise<DeviceRecording[]>;
313
+ syncRecording(
314
+ device: ConnectedDevice,
315
+ recording: DeviceRecording,
316
+ onProgress?: (progress: BotaRecordingTransferProgress) => void,
317
+ sinkId?: string
318
+ ): Promise<{
319
+ localPath: string;
320
+ e2eEncrypted: boolean;
321
+ contentSha256?: string;
322
+ }>;
323
+ syncEncryptedRecordingV2(
324
+ device: ConnectedDevice,
325
+ recording: BotaEncryptedUploadV2Recording,
326
+ provider: BotaEncryptedUploadV2ProfileProvider,
327
+ onProgress?: (progress: BotaEncryptedUploadV2Progress) => void
328
+ ): Promise<void>;
329
+ confirmRecording(
330
+ device: ConnectedDevice,
331
+ recordingUuid: string
332
+ ): Promise<void>;
333
+ uploadRecordingFile(
334
+ task: UploadTask,
335
+ onProgress?: (progress: BotaRecordingTransferProgress) => void
336
+ ): Promise<void>;
337
+ cancelRecordingUpload(taskId: string): Promise<void>;
338
+ loadUploadQueue(): Promise<UploadTask[]>;
339
+ saveUploadQueue(tasks: UploadTask[]): Promise<void>;
340
+ destroyCompatibilityOperations(): Promise<void>;
341
+ observeUploadOwnership(
342
+ device: ConnectedDevice,
343
+ request: BotaUploadOwnershipRequest,
344
+ onProgress?: (progress: BotaRecordingTransferProgress) => void
345
+ ): Promise<BotaUploadOwnershipResult>;
346
+ };
347
+
348
+ export type BotaStreamingProgress = {
349
+ state: string;
350
+ bytesReceived: number;
351
+ chunksUploaded: number;
352
+ };
353
+
354
+ export type BotaStreamingChunkRequest = {
355
+ sequence: number;
356
+ encrypted: boolean;
357
+ };
358
+
359
+ export type BotaStreamingFinalizeRequest = {
360
+ totalChunks: number;
361
+ durationMs: number;
362
+ fileSizeBytes: number;
363
+ encrypted: boolean;
364
+ };
365
+
366
+ export type BotaStreamingUploadDestination = NativeStreamingUploadDestination;
367
+
368
+ export type BotaDeviceSDKStreamingClient = {
369
+ startStreaming(
370
+ device: ConnectedDevice,
371
+ request: NativeStreamingStartRequest,
372
+ handlers: {
373
+ onProgress(progress: BotaStreamingProgress): void;
374
+ resolveChunkDestination(
375
+ request: BotaStreamingChunkRequest
376
+ ): Promise<BotaStreamingUploadDestination>;
377
+ finalize(request: BotaStreamingFinalizeRequest): Promise<void>;
378
+ }
379
+ ): Promise<{ totalBytes: number }>;
380
+ abortStreaming(sessionId: string): Promise<void>;
381
+ };
382
+
383
+ export type BotaDeviceSDKDeviceClient = {
384
+ startScan(
385
+ options: ScanOptions | undefined,
386
+ onDevice: (device: DiscoveredDevice) => void
387
+ ): Promise<BotaEventSubscription>;
388
+ stopScan(): Promise<void>;
389
+ connect(device: DiscoveredDevice): Promise<ConnectedDevice>;
390
+ reconnect(
391
+ serialNumber: string,
392
+ options?: ReconnectOptions
393
+ ): Promise<ConnectedDevice>;
394
+ disconnect(): Promise<void>;
395
+ readStatus(): Promise<DeviceStatus>;
396
+ subscribeToStatus(
397
+ onStatus: (status: DeviceStatus) => void
398
+ ): Promise<BotaAsyncEventSubscription>;
399
+ };
400
+
401
+ export class BotaNativeModuleError extends Error {
402
+ readonly code = 'native_module_unavailable';
403
+
404
+ constructor() {
405
+ super(
406
+ 'BotaDeviceSDK native module is unavailable. Rebuild the native application after installing the SDK.'
407
+ );
408
+ this.name = 'BotaNativeModuleError';
409
+ }
410
+ }
411
+
412
+ export type BotaDeviceSDKClient = {
413
+ readonly controls: BotaDeviceSDKControlClient;
414
+ readonly devices: BotaDeviceSDKDeviceClient;
415
+ readonly factoryReset: BotaDeviceSDKFactoryResetClient;
416
+ readonly logs: BotaDeviceSDKLogClient;
417
+ readonly ota: BotaDeviceSDKOTAClient;
418
+ readonly provisioning: BotaDeviceSDKProvisioningClient;
419
+ readonly recordings: BotaDeviceSDKRecordingClient;
420
+ readonly streaming: BotaDeviceSDKStreamingClient;
421
+ readonly wifi: BotaDeviceSDKWiFiClient;
422
+ configure(configuration?: BotaDeviceSDKConfiguration): Promise<void>;
423
+ destroy(): Promise<void>;
424
+ getCapabilities(): Promise<BotaDeviceSDKCapabilities>;
425
+ getState(): Promise<BotaDeviceSDKState>;
426
+ };
427
+
428
+ const mapDiscoveredDevice = (
429
+ device: NativeDiscoveredDevice
430
+ ): DiscoveredDevice => ({
431
+ id: device.id,
432
+ name: device.name ?? '',
433
+ deviceType: (device.deviceType ?? 'bota_pin') as DeviceType,
434
+ firmwareVersion: device.firmwareVersion ?? '',
435
+ macAddress: device.macAddress ?? null,
436
+ pairingState: (device.pairingState ?? 'unpaired') as PairingState,
437
+ rssi: device.rssi,
438
+ discoveredAt: new Date(device.discoveredAtMs),
439
+ });
440
+
441
+ const toNativeDiscoveredDevice = (
442
+ device: DiscoveredDevice
443
+ ): NativeDiscoveredDevice => ({
444
+ id: device.id,
445
+ name: device.name,
446
+ deviceType: device.deviceType,
447
+ firmwareVersion: device.firmwareVersion,
448
+ ...(device.macAddress ? { macAddress: device.macAddress } : {}),
449
+ pairingState: device.pairingState,
450
+ rssi: device.rssi,
451
+ discoveredAtMs: device.discoveredAt.getTime(),
452
+ });
453
+
454
+ const mapConnectedDevice = (
455
+ device: NativeConnectedDevice
456
+ ): ConnectedDevice => ({
457
+ ...device,
458
+ deviceType: device.deviceType as DeviceType,
459
+ connectionState: device.connectionState as ConnectionState,
460
+ });
461
+
462
+ const toNativeConnectedDevice = (
463
+ device: ConnectedDevice
464
+ ): NativeConnectedDevice => ({
465
+ id: device.id,
466
+ serialNumber: device.serialNumber,
467
+ deviceType: device.deviceType,
468
+ firmwareVersion: device.firmwareVersion,
469
+ ...(device.hardwareRevision === undefined
470
+ ? {}
471
+ : { hardwareRevision: device.hardwareRevision }),
472
+ isProvisioned: device.isProvisioned,
473
+ connectionState: device.connectionState,
474
+ mtu: device.mtu,
475
+ });
476
+
477
+ const errorMessage = (error: unknown): string =>
478
+ error instanceof Error ? error.message : String(error);
479
+
480
+ const createOpaqueId = (): string =>
481
+ 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (value) => {
482
+ const random = Math.floor(Math.random() * 16);
483
+ const nibble = value === 'x' ? random : (random & 0x3) | 0x8;
484
+ return nibble.toString(16);
485
+ });
486
+
487
+ const bytesToHex = (value: Uint8Array): string =>
488
+ Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
489
+
490
+ const toNativeConnectionSettings = (settings: DeviceConnectionSettings) => ({
491
+ enabledConnections: settings.enabled_connections,
492
+ heartbeatEnabledConnections:
493
+ settings.heartbeat_enabled_connections ?? { wifi: true, cellular: true },
494
+ uploadNetworkPreference: settings.upload_network_preference,
495
+ powerManagement: {
496
+ wifiIdleTimeoutSeconds:
497
+ settings.power_management?.wifi_idle_timeout_seconds ?? 180,
498
+ cellularIdleTimeoutSeconds:
499
+ settings.power_management?.cellular_idle_timeout_seconds ?? 180,
500
+ },
501
+ streamingEnabled: settings.streaming_enabled ?? true,
502
+ streamingFlushIntervalSeconds:
503
+ settings.streaming_flush_interval_seconds ?? 60,
504
+ });
505
+
506
+ const isConnectionType = (value: string): value is ConnectionType =>
507
+ value === 'wifi' || value === 'ble' || value === 'cellular';
508
+
509
+ const mapNativeConnectionSettings = (
510
+ settings: NativeDeviceConnectionSettings
511
+ ): DeviceConnectionSettings => ({
512
+ enabled_connections: settings.enabledConnections,
513
+ heartbeat_enabled_connections: settings.heartbeatEnabledConnections,
514
+ upload_network_preference: settings.uploadNetworkPreference.filter(isConnectionType),
515
+ power_management: {
516
+ wifi_idle_timeout_seconds: settings.powerManagement.wifiIdleTimeoutSeconds,
517
+ cellular_idle_timeout_seconds:
518
+ settings.powerManagement.cellularIdleTimeoutSeconds,
519
+ },
520
+ streaming_enabled: settings.streamingEnabled,
521
+ streaming_flush_interval_seconds: settings.streamingFlushIntervalSeconds,
522
+ });
523
+
524
+ const matchesScanOptions = (
525
+ device: DiscoveredDevice,
526
+ options: ScanOptions | undefined
527
+ ): boolean => {
528
+ if (options?.deviceTypes && !options.deviceTypes.includes(device.deviceType)) {
529
+ return false;
530
+ }
531
+ if (options?.pairingState && device.pairingState !== options.pairingState) {
532
+ return false;
533
+ }
534
+ if (options?.minRssi !== undefined && device.rssi < options.minRssi) {
535
+ return false;
536
+ }
537
+ return true;
538
+ };
539
+
540
+ const mapDeviceStatus = (status: NativeDeviceStatus): DeviceStatus => ({
541
+ batteryLevel: status.batteryLevel,
542
+ ...(status.batteryMv === undefined ? {} : { batteryMv: status.batteryMv }),
543
+ storageTotalMb: status.storageTotalMb,
544
+ storageUsedMb: status.storageUsedMb,
545
+ state: status.state as DeviceState,
546
+ pendingRecordings: status.pendingRecordings,
547
+ lastTimeSyncAt:
548
+ status.lastTimeSyncAtMs === undefined
549
+ ? null
550
+ : new Date(status.lastTimeSyncAtMs),
551
+ signalStrength: status.signalStrength,
552
+ flags: status.flags,
553
+ timestamp: status.timestamp,
554
+ lteStatus: status.lteStatus as LteStatus,
555
+ ...(status.lteSignalQuality === undefined
556
+ ? {}
557
+ : { lteSignalQuality: status.lteSignalQuality }),
558
+ ...(status.wifiStatus === undefined
559
+ ? {}
560
+ : { wifiStatus: status.wifiStatus as WifiStatus }),
561
+ ...(status.modemInfo === undefined ? {} : { modemInfo: status.modemInfo }),
562
+ });
563
+
564
+ const mapRecording = (recording: NativeDeviceRecording): DeviceRecording => ({
565
+ uuid: recording.uuid,
566
+ startedAt: new Date(recording.startedAtMs),
567
+ durationMs: recording.durationMs,
568
+ fileSizeBytes: recording.fileSize,
569
+ codec: recording.codec as AudioCodec,
570
+ isEncrypted: recording.isEncrypted,
571
+ });
572
+
573
+ const toNativeRecording = (recording: DeviceRecording): NativeDeviceRecording => ({
574
+ uuid: recording.uuid,
575
+ startedAtMs: recording.startedAt.getTime(),
576
+ durationMs: recording.durationMs,
577
+ fileSize: recording.fileSizeBytes,
578
+ codec: recording.codec,
579
+ isEncrypted: recording.isEncrypted ?? false,
580
+ });
581
+
582
+ const mapRecordingProgress = (
583
+ progress: NativeRecordingTransferProgress
584
+ ): BotaRecordingTransferProgress => ({
585
+ completedBytes: progress.completedUnits,
586
+ totalBytes: progress.totalUnits,
587
+ });
588
+
589
+ const mapEncryptedUploadV2Progress = (
590
+ progress: NativeEncryptedUploadV2Progress
591
+ ): BotaEncryptedUploadV2Progress => ({
592
+ recordingUuid: progress.recordingUuid,
593
+ phase: progress.phase as BotaEncryptedUploadV2ProgressPhase,
594
+ completedBytes: progress.completedBytes,
595
+ totalBytes: progress.totalBytes,
596
+ ...(progress.checkpointRevision === undefined
597
+ ? {}
598
+ : { checkpointRevision: progress.checkpointRevision }),
599
+ ...(progress.errorCode === undefined ? {} : { errorCode: progress.errorCode }),
600
+ ...(progress.retryable === undefined ? {} : { retryable: progress.retryable }),
601
+ ...(progress.protocolStatus === undefined
602
+ ? {}
603
+ : { protocolStatus: progress.protocolStatus }),
604
+ });
605
+
606
+ const mapFirmwareProgress = (
607
+ progress: NativeFirmwareUpdateProgress
608
+ ): BotaFirmwareUpdateProgress => ({
609
+ phase: progress.phase as BotaFirmwareUpdatePhase,
610
+ completedBytes: progress.completedUnits,
611
+ totalBytes: progress.totalUnits,
612
+ });
613
+
614
+ const isWiFiStatus = (value: string): value is WiFiStatus =>
615
+ value === 'idle' ||
616
+ value === 'connecting' ||
617
+ value === 'connected' ||
618
+ value === 'failed' ||
619
+ value === 'disconnected';
620
+
621
+ const mapWiFiStatus = (status: NativeWiFiStatusInfo): WiFiStatusInfo => ({
622
+ status: isWiFiStatus(status.status) ? status.status : 'idle',
623
+ ...(status.signalStrength === undefined
624
+ ? {}
625
+ : { signalStrength: status.signalStrength }),
626
+ ...(status.ssid === undefined ? {} : { ssid: status.ssid }),
627
+ ...(status.lastError === undefined ? {} : { lastError: status.lastError }),
628
+ });
629
+
630
+ const mapRecordingControlResult = (
631
+ result: NativeRecordingControlResult
632
+ ): BotaRecordingControlResult => ({
633
+ success: result.success,
634
+ ...(result.error === undefined ? {} : { error: result.error }),
635
+ });
636
+
637
+ const mapRecordingState = (state: NativeRecordingState): RecordingState => ({
638
+ active: state.active,
639
+ ...(state.recordingId === undefined
640
+ ? {}
641
+ : { recordingId: state.recordingId }),
642
+ initiatedBy: state.initiatedBy === 'remote' ? 'remote' : 'local',
643
+ });
644
+
645
+ const mapWiFiConfigResult = (
646
+ result: NativeWiFiConfigResult
647
+ ): WiFiConfigResult => {
648
+ if (result.success) return { success: true };
649
+ switch (result.error) {
650
+ case 'invalid_grant':
651
+ case 'grant_expired':
652
+ case 'decryption_error':
653
+ case 'storage_error':
654
+ return { success: false, error: result.error };
655
+ default:
656
+ return { success: false, error: 'unknown' };
657
+ }
658
+ };
659
+
660
+ const mapWiFiScanResult = (
661
+ result: NativeDeviceWiFiScanResult
662
+ ): DeviceWiFiScanResult => ({
663
+ networks: result.networks.map((network) => ({ ...network })),
664
+ currentSsid: result.currentSsid ?? null,
665
+ });
666
+
667
+ const requiredUploadOwnershipField = (
668
+ value: string | undefined,
669
+ field: string
670
+ ): string => {
671
+ if (value === undefined) {
672
+ throw new Error(`native upload ownership result is missing ${field}`);
673
+ }
674
+ return value;
675
+ };
676
+
677
+ const mapUploadOwnershipResult = (
678
+ result: NativeUploadOwnershipResult
679
+ ): BotaUploadOwnershipResult => {
680
+ switch (result.kind) {
681
+ case 'device_upload_completed':
682
+ return { kind: 'device_upload_completed' };
683
+ case 'device_upload_preserved':
684
+ return {
685
+ kind: 'device_upload_preserved',
686
+ uploadId: requiredUploadOwnershipField(result.uploadId, 'uploadId'),
687
+ };
688
+ case 'bluetooth_fallback':
689
+ return {
690
+ kind: 'bluetooth_fallback',
691
+ recordingUuid: requiredUploadOwnershipField(
692
+ result.recordingUuid,
693
+ 'recordingUuid'
694
+ ),
695
+ uploadId: requiredUploadOwnershipField(result.uploadId, 'uploadId'),
696
+ destinationId: requiredUploadOwnershipField(
697
+ result.destinationId,
698
+ 'destinationId'
699
+ ),
700
+ };
701
+ default:
702
+ throw new Error(`unsupported native upload ownership result: ${result.kind}`);
703
+ }
704
+ };
705
+
706
+ const toNativeRecordingUploadRequest = (
707
+ task: UploadTask
708
+ ): NativeRecordingUploadRequest => ({
709
+ taskId: task.id,
710
+ recordingId: task.recordingId,
711
+ deviceId: task.deviceId,
712
+ localPath: task.localPath,
713
+ uploadUrl: task.uploadUrl,
714
+ ...(task.uploadToken ? { uploadToken: task.uploadToken } : {}),
715
+ ...(task.completeUrl ? { completeUrl: task.completeUrl } : {}),
716
+ ...(task.contentType ? { contentType: task.contentType } : {}),
717
+ ...(task.contentSha256 ? { contentSha256: task.contentSha256 } : {}),
718
+ ...(task.relay
719
+ ? {
720
+ relayUrl: task.relay.url,
721
+ relayBearerToken: task.relay.bearerToken,
722
+ }
723
+ : {}),
724
+ });
725
+
726
+ const parseUploadQueue = (serialized: string): UploadTask[] => {
727
+ const value: unknown = JSON.parse(serialized || '[]');
728
+ if (!Array.isArray(value)) return [];
729
+ return value.map((task) => {
730
+ const candidate = task as UploadTask & {
731
+ createdAt: string | Date;
732
+ updatedAt: string | Date;
733
+ };
734
+ return {
735
+ ...candidate,
736
+ createdAt: new Date(candidate.createdAt),
737
+ updatedAt: new Date(candidate.updatedAt),
738
+ };
739
+ });
740
+ };
741
+
742
+ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKClient => {
743
+ const requireNativeModule = (): Spec => {
744
+ if (!nativeModule) throw new BotaNativeModuleError();
745
+ return nativeModule;
746
+ };
747
+
748
+ const devices: BotaDeviceSDKDeviceClient = {
749
+ async startScan(options, onDevice) {
750
+ const module = requireNativeModule();
751
+ const subscription = module.onDeviceDiscovered((device) => {
752
+ const discovered = mapDiscoveredDevice(device);
753
+ if (matchesScanOptions(discovered, options)) onDevice(discovered);
754
+ });
755
+ try {
756
+ await module.startScan(
757
+ options?.timeout ?? 30_000,
758
+ options?.allowDuplicates ?? false
759
+ );
760
+ } catch (error) {
761
+ subscription.remove();
762
+ throw error;
763
+ }
764
+ return subscription;
765
+ },
766
+
767
+ async stopScan() {
768
+ await requireNativeModule().stopScan();
769
+ },
770
+
771
+ async connect(device) {
772
+ return mapConnectedDevice(
773
+ await requireNativeModule().connectSelected(
774
+ toNativeDiscoveredDevice(device)
775
+ )
776
+ );
777
+ },
778
+
779
+ async reconnect(serialNumber, options = {}) {
780
+ return mapConnectedDevice(
781
+ await requireNativeModule().reconnect(serialNumber, {
782
+ scanTimeoutMs: options.scanTimeout ?? 10_000,
783
+ connectionTimeoutMs: 10_000,
784
+ })
785
+ );
786
+ },
787
+
788
+ async disconnect() {
789
+ await requireNativeModule().disconnect();
790
+ },
791
+
792
+ async readStatus() {
793
+ return mapDeviceStatus(await requireNativeModule().readStatus());
794
+ },
795
+
796
+ async subscribeToStatus(onStatus) {
797
+ const module = requireNativeModule();
798
+ const eventSubscription = module.onDeviceStatusUpdated((status) => {
799
+ onStatus(mapDeviceStatus(status));
800
+ });
801
+ try {
802
+ await module.startStatusUpdates();
803
+ } catch (error) {
804
+ eventSubscription.remove();
805
+ throw error;
806
+ }
807
+ let removed = false;
808
+ return {
809
+ async remove() {
810
+ if (removed) return;
811
+ removed = true;
812
+ eventSubscription.remove();
813
+ await module.stopStatusUpdates();
814
+ },
815
+ };
816
+ },
817
+ };
818
+
819
+ const provisioning: BotaDeviceSDKProvisioningClient = {
820
+ async provision(device, provider) {
821
+ const module = requireNativeModule();
822
+ const subscription = module.onProvisioningMaterialRequested((request) => {
823
+ void (async () => {
824
+ try {
825
+ const material = await provider({
826
+ serialNumber: request.serialNumber,
827
+ nonce: request.nonce,
828
+ devicePublicKey: request.devicePublicKey,
829
+ });
830
+ await module.resolveProvisioningMaterial(request.requestId, material);
831
+ } catch (error) {
832
+ await module.rejectApplicationMaterial(
833
+ request.requestId,
834
+ errorMessage(error)
835
+ );
836
+ }
837
+ })().catch(() => {});
838
+ });
839
+ try {
840
+ await module.provision(toNativeConnectedDevice(device));
841
+ } finally {
842
+ subscription.remove();
843
+ }
844
+ },
845
+
846
+ async deprovision(device, grantBlob) {
847
+ const result = await requireNativeModule().deprovision(
848
+ toNativeConnectedDevice(device),
849
+ grantBlob
850
+ );
851
+ return result.error === undefined
852
+ ? { success: result.success }
853
+ : { success: result.success, error: result.error };
854
+ },
855
+
856
+ async readConnectionSettings(device) {
857
+ return mapNativeConnectionSettings(
858
+ await requireNativeModule().readConnectionSettings(
859
+ toNativeConnectedDevice(device)
860
+ )
861
+ );
862
+ },
863
+
864
+ async writeConnectionSettings(device, settings) {
865
+ await requireNativeModule().writeConnectionSettings(
866
+ toNativeConnectedDevice(device),
867
+ toNativeConnectionSettings(settings)
868
+ );
869
+ },
870
+ };
871
+
872
+ const controls: BotaDeviceSDKControlClient = {
873
+ async isProvisioned(device) {
874
+ return requireNativeModule().isProvisioned(toNativeConnectedDevice(device));
875
+ },
876
+
877
+ async readPublicKey(device) {
878
+ return requireNativeModule().readPublicKey(toNativeConnectedDevice(device));
879
+ },
880
+
881
+ async readAuthNonce(device) {
882
+ return requireNativeModule().readAuthNonce(toNativeConnectedDevice(device));
883
+ },
884
+
885
+ async setApiEndpoint(device, environment) {
886
+ await requireNativeModule().setApiEndpoint(
887
+ toNativeConnectedDevice(device),
888
+ environment
889
+ );
890
+ },
891
+
892
+ async deliverCertificate(device, certificatePem, privateKeyPem) {
893
+ await requireNativeModule().deliverCertificate(
894
+ toNativeConnectedDevice(device),
895
+ certificatePem,
896
+ privateKeyPem
897
+ );
898
+ },
899
+
900
+ async deliverBackendPublicKey(device, publicKey) {
901
+ await requireNativeModule().deliverBackendPublicKey(
902
+ toNativeConnectedDevice(device),
903
+ bytesToHex(publicKey)
904
+ );
905
+ },
906
+
907
+ async writeGrant(device, grantBlob) {
908
+ await requireNativeModule().writeGrant(
909
+ toNativeConnectedDevice(device),
910
+ grantBlob
911
+ );
912
+ },
913
+
914
+ async syncTime(device) {
915
+ await requireNativeModule().syncTime(toNativeConnectedDevice(device));
916
+ },
917
+
918
+ async requestStartRecording(device, grantBlob) {
919
+ return mapRecordingControlResult(
920
+ await requireNativeModule().requestStartRecording(
921
+ toNativeConnectedDevice(device),
922
+ grantBlob
923
+ )
924
+ );
925
+ },
926
+
927
+ async requestStopRecording(device, grantBlob) {
928
+ return mapRecordingControlResult(
929
+ await requireNativeModule().requestStopRecording(
930
+ toNativeConnectedDevice(device),
931
+ grantBlob
932
+ )
933
+ );
934
+ },
935
+
936
+ async readRecordingState(device) {
937
+ return mapRecordingState(
938
+ await requireNativeModule().readRecordingState(
939
+ toNativeConnectedDevice(device)
940
+ )
941
+ );
942
+ },
943
+
944
+ async subscribeToRecordingState(device, onState) {
945
+ const module = requireNativeModule();
946
+ const eventSubscription = module.onRecordingStateUpdated((state) => {
947
+ onState(mapRecordingState(state));
948
+ });
949
+ try {
950
+ await module.startRecordingStateUpdates(toNativeConnectedDevice(device));
951
+ } catch (error) {
952
+ eventSubscription.remove();
953
+ throw error;
954
+ }
955
+ let removed = false;
956
+ return {
957
+ async remove() {
958
+ if (removed) return;
959
+ removed = true;
960
+ eventSubscription.remove();
961
+ await module.stopRecordingStateUpdates();
962
+ },
963
+ };
964
+ },
965
+ };
966
+
967
+ const factoryReset: BotaDeviceSDKFactoryResetClient = {
968
+ async factoryReset(device, options, provider, persistResult) {
969
+ const module = requireNativeModule();
970
+ const grantSubscription = module.onFactoryResetGrantRequested((request) => {
971
+ void (async () => {
972
+ try {
973
+ const grantBlob = await provider({
974
+ serialNumber: request.serialNumber,
975
+ nonce: request.nonce,
976
+ commandId: request.commandId,
977
+ bindingGeneration: request.bindingGeneration,
978
+ });
979
+ await module.resolveFactoryResetGrant(request.requestId, grantBlob);
980
+ } catch (error) {
981
+ await module.rejectApplicationMaterial(
982
+ request.requestId,
983
+ errorMessage(error)
984
+ );
985
+ }
986
+ })().catch(() => {});
987
+ });
988
+ const persistenceSubscription = persistResult
989
+ ? module.onFactoryResetResultPersistenceRequested((request) => {
990
+ void (async () => {
991
+ try {
992
+ await persistResult({
993
+ success: true,
994
+ localRecordingsDeleted: request.localRecordingsDeleted,
995
+ });
996
+ await module.resolveFactoryResetResultPersistence(request.requestId);
997
+ } catch (error) {
998
+ await module.rejectApplicationMaterial(
999
+ request.requestId,
1000
+ errorMessage(error)
1001
+ );
1002
+ }
1003
+ })().catch(() => {});
1004
+ })
1005
+ : null;
1006
+ try {
1007
+ return await module.factoryReset(
1008
+ toNativeConnectedDevice(device),
1009
+ options.commandId,
1010
+ options.bindingGeneration,
1011
+ persistResult !== undefined
1012
+ );
1013
+ } finally {
1014
+ grantSubscription.remove();
1015
+ persistenceSubscription?.remove();
1016
+ }
1017
+ },
1018
+
1019
+ async resumePendingFactoryReset(device, currentBindingGeneration, persistResult) {
1020
+ const module = requireNativeModule();
1021
+ const subscription = persistResult
1022
+ ? module.onFactoryResetResultPersistenceRequested((request) => {
1023
+ void (async () => {
1024
+ try {
1025
+ await persistResult({
1026
+ success: true,
1027
+ localRecordingsDeleted: request.localRecordingsDeleted,
1028
+ });
1029
+ await module.resolveFactoryResetResultPersistence(request.requestId);
1030
+ } catch (error) {
1031
+ await module.rejectApplicationMaterial(
1032
+ request.requestId,
1033
+ errorMessage(error)
1034
+ );
1035
+ }
1036
+ })().catch(() => {});
1037
+ })
1038
+ : null;
1039
+ try {
1040
+ return await module.resumePendingFactoryReset(
1041
+ toNativeConnectedDevice(device),
1042
+ currentBindingGeneration,
1043
+ persistResult !== undefined
1044
+ );
1045
+ } finally {
1046
+ subscription?.remove();
1047
+ }
1048
+ },
1049
+ };
1050
+
1051
+ const recordings: BotaDeviceSDKRecordingClient = {
1052
+ async listRecordings(device) {
1053
+ const values = await requireNativeModule().listRecordings(
1054
+ toNativeConnectedDevice(device)
1055
+ );
1056
+ return values.map(mapRecording);
1057
+ },
1058
+
1059
+ async syncRecording(device, recording, onProgress, sinkId) {
1060
+ const module = requireNativeModule();
1061
+ const subscription = module.onRecordingTransferProgress((progress) => {
1062
+ onProgress?.(mapRecordingProgress(progress));
1063
+ });
1064
+ try {
1065
+ return await module.syncRecording(
1066
+ toNativeConnectedDevice(device),
1067
+ toNativeRecording(recording),
1068
+ sinkId ?? createOpaqueId()
1069
+ );
1070
+ } finally {
1071
+ subscription.remove();
1072
+ }
1073
+ },
1074
+
1075
+ async syncEncryptedRecordingV2(device, recording, provider, onProgress) {
1076
+ const module = requireNativeModule();
1077
+ const operationId = createOpaqueId();
1078
+ const profileSubscription = module.onEncryptedUploadV2ProfileRequested(
1079
+ (request) => {
1080
+ if (request.operationId !== operationId) return;
1081
+ void (async () => {
1082
+ try {
1083
+ const decision = await provider({
1084
+ recording: request.recording,
1085
+ capability: request.capability,
1086
+ ...(request.checkpoint === undefined
1087
+ ? {}
1088
+ : { checkpoint: request.checkpoint }),
1089
+ });
1090
+ if (decision.profile !== 'encrypted_upload_v2') {
1091
+ throw new Error('encrypted upload v2 requires an explicit v2 profile');
1092
+ }
1093
+ await module.resolveEncryptedUploadV2Profile(
1094
+ request.requestId,
1095
+ decision
1096
+ );
1097
+ } catch (error) {
1098
+ await module.rejectEncryptedUploadV2Profile(
1099
+ request.requestId,
1100
+ 'application_material_rejected'
1101
+ );
1102
+ }
1103
+ })().catch(() => {});
1104
+ }
1105
+ );
1106
+ const progressSubscription = module.onEncryptedUploadV2Progress(
1107
+ (progress) => {
1108
+ if (progress.operationId !== operationId) return;
1109
+ onProgress?.(mapEncryptedUploadV2Progress(progress));
1110
+ }
1111
+ );
1112
+ try {
1113
+ await module.syncEncryptedRecordingV2(
1114
+ toNativeConnectedDevice(device),
1115
+ recording,
1116
+ operationId
1117
+ );
1118
+ } finally {
1119
+ profileSubscription.remove();
1120
+ progressSubscription.remove();
1121
+ }
1122
+ },
1123
+
1124
+ async uploadRecordingFile(task, onProgress) {
1125
+ const module = requireNativeModule();
1126
+ const subscription = module.onRecordingUploadProgress((progress) => {
1127
+ if (progress.taskId !== task.id) return;
1128
+ onProgress?.({
1129
+ completedBytes: progress.completedBytes,
1130
+ totalBytes: progress.totalBytes,
1131
+ });
1132
+ });
1133
+ try {
1134
+ await module.uploadRecordingFile(toNativeRecordingUploadRequest(task));
1135
+ } finally {
1136
+ subscription.remove();
1137
+ }
1138
+ },
1139
+
1140
+ async confirmRecording(device, recordingUuid) {
1141
+ await requireNativeModule().confirmRecording(
1142
+ toNativeConnectedDevice(device),
1143
+ recordingUuid
1144
+ );
1145
+ },
1146
+
1147
+ async cancelRecordingUpload(taskId) {
1148
+ await requireNativeModule().cancelRecordingUpload(taskId);
1149
+ },
1150
+
1151
+ async loadUploadQueue() {
1152
+ return parseUploadQueue(
1153
+ await requireNativeModule().loadCompatibilityUploadQueue()
1154
+ );
1155
+ },
1156
+
1157
+ async saveUploadQueue(tasks) {
1158
+ await requireNativeModule().saveCompatibilityUploadQueue(
1159
+ JSON.stringify(tasks)
1160
+ );
1161
+ },
1162
+
1163
+ async destroyCompatibilityOperations() {
1164
+ await requireNativeModule().stopAllRecordingOperations();
1165
+ },
1166
+
1167
+ async observeUploadOwnership(device, request, onProgress) {
1168
+ const module = requireNativeModule();
1169
+ const subscription = module.onUploadOwnershipProgress((progress) => {
1170
+ onProgress?.(mapRecordingProgress(progress));
1171
+ });
1172
+ try {
1173
+ return mapUploadOwnershipResult(
1174
+ await module.observeUploadOwnership(
1175
+ toNativeConnectedDevice(device),
1176
+ request
1177
+ )
1178
+ );
1179
+ } finally {
1180
+ subscription.remove();
1181
+ }
1182
+ },
1183
+ };
1184
+
1185
+ const ota: BotaDeviceSDKOTAClient = {
1186
+ async updateFirmware(device, image, onProgress) {
1187
+ const module = requireNativeModule();
1188
+ const subscription = module.onFirmwareUpdateProgress((progress) => {
1189
+ onProgress?.(mapFirmwareProgress(progress));
1190
+ });
1191
+ try {
1192
+ await module.updateFirmware(toNativeConnectedDevice(device), {
1193
+ version: image.version,
1194
+ sizeUnits: image.sizeBytes,
1195
+ crc32: image.crc32,
1196
+ url: image.url,
1197
+ });
1198
+ } finally {
1199
+ subscription.remove();
1200
+ }
1201
+ },
1202
+
1203
+ async cancelFirmwareUpdate() {
1204
+ await requireNativeModule().cancelFirmwareUpdate();
1205
+ },
1206
+ };
1207
+
1208
+ const streaming: BotaDeviceSDKStreamingClient = {
1209
+ async startStreaming(device, request, handlers) {
1210
+ const module = requireNativeModule();
1211
+ const progressSubscription = module.onStreamingProgress((progress) => {
1212
+ if (progress.sessionId !== request.sessionId) return;
1213
+ handlers.onProgress({
1214
+ state: progress.state,
1215
+ bytesReceived: progress.bytesReceived,
1216
+ chunksUploaded: progress.chunksUploaded,
1217
+ });
1218
+ });
1219
+ const destinationSubscription = module.onStreamingChunkDestinationRequested(
1220
+ (nativeRequest: NativeStreamingChunkDestinationRequest) => {
1221
+ if (nativeRequest.sessionId !== request.sessionId) return;
1222
+ void handlers
1223
+ .resolveChunkDestination({
1224
+ sequence: nativeRequest.sequence,
1225
+ encrypted: nativeRequest.encrypted,
1226
+ })
1227
+ .then(
1228
+ (destination) =>
1229
+ module.resolveStreamingChunkDestination(
1230
+ nativeRequest.requestId,
1231
+ destination
1232
+ ),
1233
+ (error) =>
1234
+ module.rejectStreamingChunkDestination(
1235
+ nativeRequest.requestId,
1236
+ errorMessage(error)
1237
+ )
1238
+ )
1239
+ .catch(() => {});
1240
+ }
1241
+ );
1242
+ const finalizeSubscription = module.onStreamingFinalizeRequested(
1243
+ (nativeRequest: NativeStreamingFinalizeRequest) => {
1244
+ if (nativeRequest.sessionId !== request.sessionId) return;
1245
+ void handlers
1246
+ .finalize({
1247
+ totalChunks: nativeRequest.totalChunks,
1248
+ durationMs: nativeRequest.durationMs,
1249
+ fileSizeBytes: nativeRequest.fileSizeBytes,
1250
+ encrypted: nativeRequest.encrypted,
1251
+ })
1252
+ .then(
1253
+ () => module.resolveStreamingFinalize(nativeRequest.requestId),
1254
+ (error) =>
1255
+ module.rejectStreamingFinalize(
1256
+ nativeRequest.requestId,
1257
+ errorMessage(error)
1258
+ )
1259
+ )
1260
+ .catch(() => {});
1261
+ }
1262
+ );
1263
+ try {
1264
+ return await module.startStreaming(
1265
+ toNativeConnectedDevice(device),
1266
+ request
1267
+ );
1268
+ } finally {
1269
+ progressSubscription.remove();
1270
+ destinationSubscription.remove();
1271
+ finalizeSubscription.remove();
1272
+ }
1273
+ },
1274
+
1275
+ async abortStreaming(sessionId) {
1276
+ await requireNativeModule().abortStreaming(sessionId);
1277
+ },
1278
+ };
1279
+
1280
+ const logs: BotaDeviceSDKLogClient = {
1281
+ async subscribe(device, onLine) {
1282
+ const module = requireNativeModule();
1283
+ const eventSubscription = module.onDeviceLog((line) => {
1284
+ onLine({
1285
+ level: 'debug',
1286
+ message: line.message,
1287
+ isBacklog: line.isBacklog,
1288
+ });
1289
+ });
1290
+ try {
1291
+ await module.startDeviceLogs(toNativeConnectedDevice(device));
1292
+ } catch (error) {
1293
+ eventSubscription.remove();
1294
+ throw error;
1295
+ }
1296
+ let removed = false;
1297
+ return {
1298
+ async remove() {
1299
+ if (removed) return;
1300
+ removed = true;
1301
+ eventSubscription.remove();
1302
+ await module.stopDeviceLogs();
1303
+ },
1304
+ };
1305
+ },
1306
+ };
1307
+
1308
+ const wifi: BotaDeviceSDKWiFiClient = {
1309
+ async configure(device, credentials, grant) {
1310
+ return mapWiFiConfigResult(
1311
+ await requireNativeModule().configureWiFi(
1312
+ toNativeConnectedDevice(device),
1313
+ credentials.ssid,
1314
+ credentials.password,
1315
+ grant.grantBlob
1316
+ )
1317
+ );
1318
+ },
1319
+
1320
+ async disconnect(device) {
1321
+ return mapWiFiConfigResult(
1322
+ await requireNativeModule().disconnectWiFi(
1323
+ toNativeConnectedDevice(device)
1324
+ )
1325
+ );
1326
+ },
1327
+
1328
+ async readStatus(device) {
1329
+ return mapWiFiStatus(
1330
+ await requireNativeModule().readWiFiStatus(
1331
+ toNativeConnectedDevice(device)
1332
+ )
1333
+ );
1334
+ },
1335
+
1336
+ async subscribeToStatus(device, onStatus) {
1337
+ const module = requireNativeModule();
1338
+ const eventSubscription = module.onWiFiStatusUpdated((status) => {
1339
+ onStatus(mapWiFiStatus(status));
1340
+ });
1341
+ try {
1342
+ await module.startWiFiStatusUpdates(toNativeConnectedDevice(device));
1343
+ } catch (error) {
1344
+ eventSubscription.remove();
1345
+ throw error;
1346
+ }
1347
+ let removed = false;
1348
+ return {
1349
+ async remove() {
1350
+ if (removed) return;
1351
+ removed = true;
1352
+ eventSubscription.remove();
1353
+ await module.stopWiFiStatusUpdates();
1354
+ },
1355
+ };
1356
+ },
1357
+
1358
+ async scanNetworks(device) {
1359
+ return mapWiFiScanResult(
1360
+ await requireNativeModule().scanWiFiNetworks(
1361
+ toNativeConnectedDevice(device)
1362
+ )
1363
+ );
1364
+ },
1365
+ };
1366
+
1367
+ const client: BotaDeviceSDKClient = {
1368
+ controls,
1369
+ devices,
1370
+ factoryReset,
1371
+ logs,
1372
+ ota,
1373
+ provisioning,
1374
+ recordings,
1375
+ streaming,
1376
+ wifi,
1377
+
1378
+ async configure(configuration = {}) {
1379
+ const nativeConfiguration: NativeConfiguration = {
1380
+ logLevel: configuration.logLevel ?? 'warn',
1381
+ ...(configuration.applicationSupportDirectory
1382
+ ? {
1383
+ applicationSupportDirectory:
1384
+ configuration.applicationSupportDirectory,
1385
+ }
1386
+ : {}),
1387
+ };
1388
+ await requireNativeModule().configure(nativeConfiguration);
1389
+ },
1390
+
1391
+ async destroy() {
1392
+ await requireNativeModule().destroy();
1393
+ },
1394
+
1395
+ async getCapabilities() {
1396
+ return requireNativeModule().getCapabilities();
1397
+ },
1398
+
1399
+ async getState() {
1400
+ return (await requireNativeModule().getState()) as BotaDeviceSDKState;
1401
+ },
1402
+ };
1403
+ nativeModule?.onDeviceDisconnected?.((event) => {
1404
+ reportCompatibilityDisconnection(
1405
+ client,
1406
+ event.error ? new Error(event.error) : undefined
1407
+ );
1408
+ });
1409
+ return client;
1410
+ };