@bota.dev/react-native-app-sdk 2.0.0-beta.1 → 2.0.0-beta.11

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 (152) hide show
  1. package/README.md +115 -4
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +22 -0
  4. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDiagnostics.kt +53 -0
  5. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +16 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +144 -14
  7. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +150 -29
  8. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +27 -0
  9. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +86 -3
  10. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +9 -0
  11. package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +66 -0
  12. package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +400 -0
  13. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadHttp.kt +158 -0
  14. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadJournal.kt +112 -0
  15. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +171 -0
  16. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +35 -0
  17. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +33 -0
  18. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +135 -6
  19. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +236 -10
  20. package/generated/codegen-contract.json +751 -32
  21. package/ios/BotaDeviceSDK.mm +114 -0
  22. package/ios/BotaDeviceSDKAppleBridge.swift +141 -4
  23. package/ios/BotaDeviceSDKAppleDevices.swift +11 -0
  24. package/ios/BotaDeviceSDKAppleDiagnostics.swift +40 -0
  25. package/ios/BotaDeviceSDKAppleLogs.swift +18 -0
  26. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +138 -6
  27. package/ios/BotaDeviceSDKAppleRecordings.swift +113 -5
  28. package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +40 -0
  29. package/ios/BotaUploadV2Backend.mm +49 -0
  30. package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +98 -0
  31. package/ios/UploadBackend/UploadCoordinator.swift +84 -0
  32. package/ios/UploadBackend/UploadHTTP.swift +82 -0
  33. package/ios/UploadBackend/UploadJournal.swift +187 -0
  34. package/ios/UploadBackend/UploadModels.swift +273 -0
  35. package/ios/UploadBackend/UploadOperation.swift +391 -0
  36. package/lib/commonjs/BotaClient.js +16 -2
  37. package/lib/commonjs/BotaClient.js.map +1 -1
  38. package/lib/commonjs/ble/constants.js +5 -1
  39. package/lib/commonjs/ble/constants.js.map +1 -1
  40. package/lib/commonjs/ble/deviceDiagnostics.js +240 -0
  41. package/lib/commonjs/ble/deviceDiagnostics.js.map +1 -0
  42. package/lib/commonjs/client.js +183 -28
  43. package/lib/commonjs/client.js.map +1 -1
  44. package/lib/commonjs/clientPresence.js +26 -0
  45. package/lib/commonjs/clientPresence.js.map +1 -0
  46. package/lib/commonjs/encryptedUploadV2Backend.js +200 -0
  47. package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -0
  48. package/lib/commonjs/index.js +34 -0
  49. package/lib/commonjs/index.js.map +1 -1
  50. package/lib/commonjs/managers/DeviceManager.js +23 -0
  51. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  52. package/lib/commonjs/managers/NativeUploadQueue.js +386 -0
  53. package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -0
  54. package/lib/commonjs/managers/RecordingManager.js +83 -102
  55. package/lib/commonjs/managers/RecordingManager.js.map +1 -1
  56. package/lib/commonjs/managers/uploadRecoveryMetadata.js +83 -0
  57. package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -0
  58. package/lib/commonjs/sdkIdentity.js +10 -0
  59. package/lib/commonjs/sdkIdentity.js.map +1 -0
  60. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
  61. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js +11 -0
  62. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -0
  63. package/lib/commonjs/uploadRecovery.js +68 -0
  64. package/lib/commonjs/uploadRecovery.js.map +1 -0
  65. package/lib/module/BotaClient.js +16 -2
  66. package/lib/module/BotaClient.js.map +1 -1
  67. package/lib/module/ble/constants.js +4 -0
  68. package/lib/module/ble/constants.js.map +1 -1
  69. package/lib/module/ble/deviceDiagnostics.js +234 -0
  70. package/lib/module/ble/deviceDiagnostics.js.map +1 -0
  71. package/lib/module/client.js +183 -28
  72. package/lib/module/client.js.map +1 -1
  73. package/lib/module/clientPresence.js +22 -0
  74. package/lib/module/clientPresence.js.map +1 -0
  75. package/lib/module/encryptedUploadV2Backend.js +196 -0
  76. package/lib/module/encryptedUploadV2Backend.js.map +1 -0
  77. package/lib/module/index.js +7 -0
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/managers/DeviceManager.js +23 -0
  80. package/lib/module/managers/DeviceManager.js.map +1 -1
  81. package/lib/module/managers/NativeUploadQueue.js +380 -0
  82. package/lib/module/managers/NativeUploadQueue.js.map +1 -0
  83. package/lib/module/managers/RecordingManager.js +83 -102
  84. package/lib/module/managers/RecordingManager.js.map +1 -1
  85. package/lib/module/managers/uploadRecoveryMetadata.js +77 -0
  86. package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -0
  87. package/lib/module/sdkIdentity.js +6 -0
  88. package/lib/module/sdkIdentity.js.map +1 -0
  89. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
  90. package/lib/module/specs/NativeBotaUploadV2Backend.js +9 -0
  91. package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -0
  92. package/lib/module/uploadRecovery.js +64 -0
  93. package/lib/module/uploadRecovery.js.map +1 -0
  94. package/lib/typescript/BotaClient.d.ts +2 -0
  95. package/lib/typescript/BotaClient.d.ts.map +1 -1
  96. package/lib/typescript/ble/constants.d.ts +4 -0
  97. package/lib/typescript/ble/constants.d.ts.map +1 -1
  98. package/lib/typescript/ble/deviceDiagnostics.d.ts +10 -0
  99. package/lib/typescript/ble/deviceDiagnostics.d.ts.map +1 -0
  100. package/lib/typescript/client.d.ts +24 -3
  101. package/lib/typescript/client.d.ts.map +1 -1
  102. package/lib/typescript/clientPresence.d.ts +26 -0
  103. package/lib/typescript/clientPresence.d.ts.map +1 -0
  104. package/lib/typescript/encryptedUploadV2Backend.d.ts +41 -0
  105. package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -0
  106. package/lib/typescript/index.d.ts +13 -4
  107. package/lib/typescript/index.d.ts.map +1 -1
  108. package/lib/typescript/managers/DeviceManager.d.ts +5 -1
  109. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
  110. package/lib/typescript/managers/NativeUploadQueue.d.ts +41 -0
  111. package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -0
  112. package/lib/typescript/managers/RecordingManager.d.ts +3 -8
  113. package/lib/typescript/managers/RecordingManager.d.ts.map +1 -1
  114. package/lib/typescript/managers/types.d.ts +6 -1
  115. package/lib/typescript/managers/types.d.ts.map +1 -1
  116. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts +6 -0
  117. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -0
  118. package/lib/typescript/models/Device.d.ts +42 -0
  119. package/lib/typescript/models/Device.d.ts.map +1 -1
  120. package/lib/typescript/models/Recording.d.ts +47 -0
  121. package/lib/typescript/models/Recording.d.ts.map +1 -1
  122. package/lib/typescript/models/Status.d.ts +5 -1
  123. package/lib/typescript/models/Status.d.ts.map +1 -1
  124. package/lib/typescript/sdkIdentity.d.ts +3 -0
  125. package/lib/typescript/sdkIdentity.d.ts.map +1 -0
  126. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +70 -0
  127. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
  128. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +18 -0
  129. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -0
  130. package/lib/typescript/uploadRecovery.d.ts +27 -0
  131. package/lib/typescript/uploadRecovery.d.ts.map +1 -0
  132. package/package.json +1 -1
  133. package/scripts/bota_device_sdk_spm_workaround.rb +26 -15
  134. package/src/BotaClient.ts +17 -3
  135. package/src/ble/constants.ts +5 -0
  136. package/src/ble/deviceDiagnostics.ts +238 -0
  137. package/src/client.ts +189 -18
  138. package/src/clientPresence.ts +33 -0
  139. package/src/encryptedUploadV2Backend.ts +171 -0
  140. package/src/index.ts +24 -0
  141. package/src/managers/DeviceManager.ts +23 -0
  142. package/src/managers/NativeUploadQueue.ts +330 -0
  143. package/src/managers/RecordingManager.ts +83 -116
  144. package/src/managers/types.ts +7 -1
  145. package/src/managers/uploadRecoveryMetadata.ts +59 -0
  146. package/src/models/Device.ts +43 -0
  147. package/src/models/Recording.ts +52 -0
  148. package/src/models/Status.ts +5 -1
  149. package/src/sdkIdentity.ts +3 -0
  150. package/src/specs/NativeBotaDeviceSDK.ts +66 -0
  151. package/src/specs/NativeBotaUploadV2Backend.ts +19 -0
  152. package/src/uploadRecovery.ts +66 -0
@@ -0,0 +1,238 @@
1
+ import { Buffer } from 'buffer';
2
+
3
+ import {
4
+ DEVICE_DIAGNOSTICS_EVT_END,
5
+ DEVICE_DIAGNOSTICS_EVT_DETAIL,
6
+ DEVICE_DIAGNOSTICS_EVT_META,
7
+ DEVICE_DIAGNOSTICS_EVT_SIGNATURE,
8
+ } from './constants';
9
+ import type {
10
+ DeviceDiagnosticEvent,
11
+ DeviceDiagnosticEventType,
12
+ DeviceDiagnosticReasonCode,
13
+ DeviceDiagnosticsBatch,
14
+ } from '../models/Device';
15
+
16
+ const EVENT_TYPES: Record<number, DeviceDiagnosticEventType> = {
17
+ 1: 'reset', 2: 'watchdog', 3: 'hard_fault', 4: 'storage',
18
+ 5: 'power', 6: 'radio', 7: 'security',
19
+ };
20
+ const REASONS: Record<number, DeviceDiagnosticReasonCode> = {
21
+ 1: 'watchdog_timeout', 2: 'cpu_illegal_instruction',
22
+ 3: 'cpu_misaligned_access', 4: 'cpu_stack_overflow',
23
+ 5: 'cpu_usage_fault', 6: 'memory_protection_fault',
24
+ 7: 'invalid_register_read', 8: 'invalid_register_write',
25
+ 9: 'audio_subsystem_fault', 10: 'wireless_subsystem_fault',
26
+ 11: 'flash_mmu_fault', 12: 'low_voltage_reset',
27
+ 255: 'unknown_cpu_exception',
28
+ };
29
+ const SUBSYSTEMS: Record<number, string> = {
30
+ 0: 'system', 1: 'audio', 2: 'wireless', 3: 'storage',
31
+ 4: 'power', 5: 'security',
32
+ };
33
+ const STATES: Record<number, string> = {
34
+ 0: 'boot', 1: 'deep_standby', 2: 'recording', 3: 'upload_preparing',
35
+ 4: 'uploading', 5: 'error_recovery', 6: 'full_sleep',
36
+ };
37
+ const BREADCRUMBS: Record<number, string> = {
38
+ 1: 'boot', 2: 'standby_entered', 3: 'recording_started',
39
+ 4: 'upload_preparing', 5: 'upload_started', 6: 'error_recovery',
40
+ 7: 'full_sleep',
41
+ };
42
+ const DETAIL_WIRE_SIZE = 176;
43
+
44
+ function u64LeHex(bytes: Uint8Array): string {
45
+ return Buffer.from(bytes).reverse().toString('hex').padStart(16, '0');
46
+ }
47
+
48
+ export function diagnosticEventIdCommand(eventId: string): Buffer {
49
+ if (!/^[0-9a-f]{16}$/.test(eventId)) {
50
+ throw new Error('Diagnostic event_id must be 16 lowercase hex characters');
51
+ }
52
+ return Buffer.concat([
53
+ Buffer.from([0x11]),
54
+ Buffer.from(eventId, 'hex').reverse(),
55
+ ]);
56
+ }
57
+
58
+ type PartialEvent = Omit<DeviceDiagnosticEvent, 'signature' | 'firmware_build_id' |
59
+ 'subsystem' | 'state_before_event'> & {
60
+ signature?: string;
61
+ firmware_build_id?: string;
62
+ subsystem?: string;
63
+ state_before_event?: string;
64
+ };
65
+
66
+ interface DetailAssembly {
67
+ buffer: Buffer;
68
+ received: number;
69
+ }
70
+
71
+ function normalizedAddress(value: number): string | undefined {
72
+ if (value === 0) return undefined;
73
+ const region = (value & 0x80000000) !== 0
74
+ ? 'sdram'
75
+ : (value & 0x40000000) !== 0 ? 'ram' : 'rom';
76
+ const offset = (value & 0x3fffffff).toString(16).padStart(8, '0');
77
+ return `${region}:${offset}`;
78
+ }
79
+
80
+ function fixedHex32(value: number): string {
81
+ return value.toString(16).padStart(8, '0');
82
+ }
83
+
84
+ function decodeDetail(bytes: Buffer): Pick<DeviceDiagnosticEvent,
85
+ 'firmware_build_id' | 'subsystem' | 'state_before_event' | 'report'> {
86
+ if (bytes.length !== DETAIL_WIRE_SIZE || bytes[0] !== 1) {
87
+ throw new Error('Unsupported diagnostic detail payload');
88
+ }
89
+ const hasReport = (bytes[1] & 1) !== 0;
90
+ const firmwareBuildId = Buffer.from(bytes.subarray(2, 14)).toString('ascii');
91
+ if (!/^[0-9a-f]{12}$/.test(firmwareBuildId)) {
92
+ throw new Error('Invalid diagnostic firmware build id');
93
+ }
94
+ const common = {
95
+ firmware_build_id: firmwareBuildId,
96
+ subsystem: SUBSYSTEMS[bytes[14]] ?? 'system',
97
+ state_before_event: STATES[bytes[15]] ?? 'unknown',
98
+ };
99
+ if (!hasReport) return common;
100
+
101
+ const pcCount = Math.min(bytes[17], 6);
102
+ const breadcrumbCount = Math.min(bytes[18], 8);
103
+ const runtimeMask = bytes[19];
104
+ const pcTrace: string[] = [];
105
+ for (let index = 0; index < pcCount; index++) {
106
+ const address = normalizedAddress(bytes.readUInt32LE(48 + index * 4));
107
+ if (address) pcTrace.push(address);
108
+ }
109
+ const task = Buffer.from(bytes.subarray(80, 96)).toString('ascii').replace(/\0.*$/, '');
110
+ const breadcrumbs = [];
111
+ for (let index = 0; index < breadcrumbCount; index++) {
112
+ const offset = 96 + index * 10;
113
+ breadcrumbs.push({
114
+ delta_ms: bytes.readInt32LE(offset),
115
+ code: BREADCRUMBS[bytes.readUInt16LE(offset + 4)] ?? 'state_changed',
116
+ arg0: bytes.readInt32LE(offset + 6),
117
+ });
118
+ }
119
+ const reti = normalizedAddress(bytes.readUInt32LE(40));
120
+ const rets = normalizedAddress(bytes.readUInt32LE(44));
121
+ return {
122
+ ...common,
123
+ report: {
124
+ fault: {
125
+ cpu_id: bytes[16],
126
+ cpu_emu: fixedHex32(bytes.readUInt32LE(20)),
127
+ core_emu: fixedHex32(bytes.readUInt32LE(24)),
128
+ hsb_emu: fixedHex32(bytes.readUInt32LE(28)),
129
+ audio_emu: fixedHex32(bytes.readUInt32LE(32)),
130
+ wireless_emu: fixedHex32(bytes.readUInt32LE(36)),
131
+ },
132
+ execution: {
133
+ ...(task ? { task } : {}),
134
+ ...(reti ? { reti } : {}),
135
+ ...(rets ? { rets } : {}),
136
+ pc_trace: pcTrace,
137
+ },
138
+ ...((runtimeMask & 3) !== 0 ? {
139
+ runtime: {
140
+ ...((runtimeMask & 1) !== 0
141
+ ? { heap_free_bytes: bytes.readUInt32LE(72) } : {}),
142
+ ...((runtimeMask & 2) !== 0
143
+ ? { task_stack_remaining_bytes: bytes.readUInt32LE(76) } : {}),
144
+ },
145
+ } : {}),
146
+ ...(breadcrumbs.length ? { breadcrumbs } : {}),
147
+ },
148
+ };
149
+ }
150
+
151
+ export class DeviceDiagnosticsDecoder {
152
+ private events = new Map<number, PartialEvent>();
153
+ private details = new Map<number, DetailAssembly>();
154
+
155
+ push(packet: Buffer): DeviceDiagnosticsBatch | null {
156
+ if (packet.length === 17 && packet[0] === DEVICE_DIAGNOSTICS_EVT_META) {
157
+ const type = EVENT_TYPES[packet[2]];
158
+ if (!type) return null;
159
+ this.events.set(packet[1], {
160
+ event_id: u64LeHex(packet.subarray(9, 17)),
161
+ event_type: type,
162
+ reason_code: REASONS[packet.readUInt16LE(3)] ?? 'unknown_cpu_exception',
163
+ uptime_ms: packet.readUInt32LE(5),
164
+ });
165
+ return null;
166
+ }
167
+ if (packet.length === 10 && packet[0] === DEVICE_DIAGNOSTICS_EVT_SIGNATURE) {
168
+ const event = this.events.get(packet[1]);
169
+ if (event) event.signature = u64LeHex(packet.subarray(2, 10));
170
+ return null;
171
+ }
172
+ if (packet.length >= 6 && packet.length <= 20 &&
173
+ packet[0] === DEVICE_DIAGNOSTICS_EVT_DETAIL) {
174
+ const index = packet[1];
175
+ const offset = packet.readUInt16LE(2);
176
+ const total = packet.readUInt16LE(4);
177
+ if (total !== DETAIL_WIRE_SIZE || offset + packet.length - 6 > total) {
178
+ this.reset();
179
+ throw new Error('Invalid diagnostic detail chunk');
180
+ }
181
+ let detail = this.details.get(index);
182
+ if (offset === 0) {
183
+ if (detail) {
184
+ this.reset();
185
+ throw new Error(`Duplicate diagnostic detail at index ${index}`);
186
+ }
187
+ detail = { buffer: Buffer.alloc(total), received: 0 };
188
+ this.details.set(index, detail);
189
+ }
190
+ if (!detail || detail.received !== offset) {
191
+ this.reset();
192
+ throw new Error(`Out-of-order diagnostic detail at index ${index}`);
193
+ }
194
+ packet.copy(detail.buffer, offset, 6);
195
+ detail.received += packet.length - 6;
196
+ if (detail.received === total) {
197
+ const event = this.events.get(index);
198
+ if (!event) {
199
+ this.reset();
200
+ throw new Error(`Diagnostic detail without metadata at index ${index}`);
201
+ }
202
+ try {
203
+ Object.assign(event, decodeDetail(detail.buffer));
204
+ } catch (error) {
205
+ this.reset();
206
+ throw error;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ if (packet.length === 2 && packet[0] === DEVICE_DIAGNOSTICS_EVT_END) {
212
+ const expected = packet[1];
213
+ const events: DeviceDiagnosticEvent[] = [];
214
+ for (let index = 0; index < expected; index++) {
215
+ const event = this.events.get(index);
216
+ if (!event?.signature || !event.firmware_build_id ||
217
+ !event.subsystem || !event.state_before_event ||
218
+ this.details.get(index)?.received !== DETAIL_WIRE_SIZE) {
219
+ this.reset();
220
+ throw new Error(`Incomplete diagnostic event at index ${index}`);
221
+ }
222
+ events.push(event as DeviceDiagnosticEvent);
223
+ }
224
+ const batch: DeviceDiagnosticsBatch = {
225
+ schema_version: 1,
226
+ events,
227
+ };
228
+ this.reset();
229
+ return batch;
230
+ }
231
+ return null;
232
+ }
233
+
234
+ reset(): void {
235
+ this.events.clear();
236
+ this.details.clear();
237
+ }
238
+ }
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ NativeClientContext,
2
3
  NativeCapabilities,
3
4
  NativeConnectedDevice,
4
5
  NativeConfiguration,
@@ -10,6 +11,7 @@ import type {
10
11
  NativeEncryptedUploadV2Checkpoint,
11
12
  NativeEncryptedUploadV2Progress,
12
13
  NativeEncryptedUploadV2Recording,
14
+ NativePendingRecording,
13
15
  NativeFactoryResetCompletion,
14
16
  NativeFactoryResetGrantRequest,
15
17
  NativeFirmwareUpdateProgress,
@@ -28,6 +30,12 @@ import type {
28
30
  NativeDeviceWiFiScanResult,
29
31
  Spec,
30
32
  } from './specs/NativeBotaDeviceSDK';
33
+ import { SDK_PACKAGE, SDK_VERSION } from './sdkIdentity';
34
+
35
+ export type BotaClientPresenceContext = NativeClientContext;
36
+ export type BotaClientPresence = {
37
+ nextReport(deviceId: string): Promise<BotaClientPresenceContext | null>;
38
+ };
31
39
  import type {
32
40
  ConnectedDevice,
33
41
  ConnectionType,
@@ -37,6 +45,9 @@ import type {
37
45
  DeviceStatus,
38
46
  DeviceType,
39
47
  DeviceLogEvent,
48
+ DeviceDiagnosticEventType,
49
+ DeviceDiagnosticReasonCode,
50
+ DeviceDiagnosticsBatch,
40
51
  DiscoveredDevice,
41
52
  PairingState,
42
53
  ReconnectOptions,
@@ -195,11 +206,24 @@ export type BotaRecordingTransferProgress = {
195
206
 
196
207
  export type BotaEncryptedUploadV2Recording = NativeEncryptedUploadV2Recording;
197
208
 
209
+ export type BotaEncryptedUploadV2PendingRecording = Omit<BotaEncryptedUploadV2Recording, 'durationMs'> & {
210
+ storageFormat: 3;
211
+ startedAt: Date;
212
+ durationMs: number;
213
+ plaintextLength: string;
214
+ };
215
+
216
+ export type BotaEncryptedUploadV2SyncOptions = {
217
+ signal?: AbortSignal;
218
+ operationId?: string;
219
+ };
220
+
198
221
  export type BotaEncryptedUploadV2Capability = NativeEncryptedUploadV2Capability;
199
222
 
200
223
  export type BotaEncryptedUploadV2Checkpoint = NativeEncryptedUploadV2Checkpoint;
201
224
 
202
225
  export type BotaEncryptedUploadV2ProviderContext = {
226
+ operationId: string;
203
227
  recording: BotaEncryptedUploadV2Recording;
204
228
  capability: BotaEncryptedUploadV2Capability;
205
229
  checkpoint?: BotaEncryptedUploadV2Checkpoint;
@@ -287,6 +311,8 @@ export type BotaDeviceSDKOTAClient = {
287
311
  };
288
312
 
289
313
  export type BotaDeviceSDKLogClient = {
314
+ readDiagnosticEvents(device: ConnectedDevice): Promise<DeviceDiagnosticsBatch>;
315
+ acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: string[]): Promise<void>;
290
316
  subscribe(
291
317
  device: ConnectedDevice,
292
318
  onLine: (line: DeviceLogEvent) => void
@@ -310,6 +336,7 @@ export type BotaDeviceSDKWiFiClient = {
310
336
 
311
337
  export type BotaDeviceSDKRecordingClient = {
312
338
  listRecordings(device: ConnectedDevice): Promise<DeviceRecording[]>;
339
+ listPendingRecordings(device: ConnectedDevice): Promise<Array<DeviceRecording | BotaEncryptedUploadV2PendingRecording>>;
313
340
  syncRecording(
314
341
  device: ConnectedDevice,
315
342
  recording: DeviceRecording,
@@ -319,12 +346,14 @@ export type BotaDeviceSDKRecordingClient = {
319
346
  localPath: string;
320
347
  e2eEncrypted: boolean;
321
348
  contentSha256?: string;
349
+ fileSizeBytes: number;
322
350
  }>;
323
351
  syncEncryptedRecordingV2(
324
352
  device: ConnectedDevice,
325
353
  recording: BotaEncryptedUploadV2Recording,
326
354
  provider: BotaEncryptedUploadV2ProfileProvider,
327
- onProgress?: (progress: BotaEncryptedUploadV2Progress) => void
355
+ onProgress?: (progress: BotaEncryptedUploadV2Progress) => void,
356
+ options?: BotaEncryptedUploadV2SyncOptions
328
357
  ): Promise<void>;
329
358
  confirmRecording(
330
359
  device: ConnectedDevice,
@@ -335,6 +364,7 @@ export type BotaDeviceSDKRecordingClient = {
335
364
  onProgress?: (progress: BotaRecordingTransferProgress) => void
336
365
  ): Promise<void>;
337
366
  cancelRecordingUpload(taskId: string): Promise<void>;
367
+ releaseRecordingFile(taskId: string, localPath: string): Promise<void>;
338
368
  loadUploadQueue(): Promise<UploadTask[]>;
339
369
  saveUploadQueue(tasks: UploadTask[]): Promise<void>;
340
370
  destroyCompatibilityOperations(): Promise<void>;
@@ -410,6 +440,7 @@ export class BotaNativeModuleError extends Error {
410
440
  }
411
441
 
412
442
  export type BotaDeviceSDKClient = {
443
+ readonly clientPresence: BotaClientPresence;
413
444
  readonly controls: BotaDeviceSDKControlClient;
414
445
  readonly devices: BotaDeviceSDKDeviceClient;
415
446
  readonly factoryReset: BotaDeviceSDKFactoryResetClient;
@@ -425,6 +456,13 @@ export type BotaDeviceSDKClient = {
425
456
  getState(): Promise<BotaDeviceSDKState>;
426
457
  };
427
458
 
459
+ const formatAdvertisedMac = (value: string | undefined): string | null => {
460
+ if (!value || !/^(?:[0-9a-f]{12}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})$/i.test(value)) {
461
+ return null;
462
+ }
463
+ return value.replace(/:/g, '').toUpperCase().match(/.{2}/g)!.join(':');
464
+ };
465
+
428
466
  const mapDiscoveredDevice = (
429
467
  device: NativeDiscoveredDevice
430
468
  ): DiscoveredDevice => ({
@@ -432,7 +470,7 @@ const mapDiscoveredDevice = (
432
470
  name: device.name ?? '',
433
471
  deviceType: (device.deviceType ?? 'bota_pin') as DeviceType,
434
472
  firmwareVersion: device.firmwareVersion ?? '',
435
- macAddress: device.macAddress ?? null,
473
+ macAddress: formatAdvertisedMac(device.macAddress),
436
474
  pairingState: (device.pairingState ?? 'unpaired') as PairingState,
437
475
  rssi: device.rssi,
438
476
  discoveredAt: new Date(device.discoveredAtMs),
@@ -484,6 +522,48 @@ const createOpaqueId = (): string =>
484
522
  return nibble.toString(16);
485
523
  });
486
524
 
525
+ const catalogInteger = (value: string | undefined, maximum = BigInt(Number.MAX_SAFE_INTEGER)): number => {
526
+ if (typeof value !== 'string' || !/^(0|[1-9][0-9]{0,19})$/.test(value) || BigInt(value) > maximum) {
527
+ throw new Error('invalid native recording catalog metadata');
528
+ }
529
+ return Number(value);
530
+ };
531
+
532
+ const mapPendingRecording = (value: NativePendingRecording): DeviceRecording | BotaEncryptedUploadV2PendingRecording => {
533
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
534
+ if (value.profile === 'legacy' && value.legacy && !value.encrypted) {
535
+ const recording = value.legacy;
536
+ if (typeof recording.uuid !== 'string' || !uuidPattern.test(recording.uuid) ||
537
+ !Number.isSafeInteger(recording.startedAtMs) || recording.startedAtMs < 0 || recording.startedAtMs > 8640000000000000 ||
538
+ !Number.isSafeInteger(recording.durationMs) || recording.durationMs < 0 ||
539
+ !Number.isSafeInteger(recording.fileSize) || recording.fileSize < 0 ||
540
+ typeof recording.codec !== 'string' || typeof recording.isEncrypted !== 'boolean') {
541
+ throw new Error('invalid native recording catalog metadata');
542
+ }
543
+ return mapRecording(recording);
544
+ }
545
+ if (value.profile !== 'encrypted_upload_v2' || !value.encrypted || value.legacy) {
546
+ throw new Error('invalid native recording catalog profile');
547
+ }
548
+ const recording = value.encrypted;
549
+ const plaintextLength = recording.plaintextLength;
550
+ if (typeof recording.uuid !== 'string' || !uuidPattern.test(recording.uuid) ||
551
+ recording.storageFormat !== 3 || typeof plaintextLength !== 'string' ||
552
+ !/^(0|[1-9][0-9]{0,19})$/.test(plaintextLength) || BigInt(plaintextLength) > 0xffffffffffffffffn ||
553
+ typeof recording.ciphertextLength !== 'string' || !/^[1-9][0-9]{0,19}$/.test(recording.ciphertextLength) || BigInt(recording.ciphertextLength) > 0xffffffffffffffffn ||
554
+ typeof recording.ciphertextSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(recording.ciphertextSha256) ||
555
+ !Number.isInteger(recording.generation) || recording.generation < 0 || recording.generation > 0xffffffff) {
556
+ throw new Error('invalid native recording catalog identity');
557
+ }
558
+ return {
559
+ uuid: recording.uuid, generation: recording.generation,
560
+ ciphertextLength: recording.ciphertextLength, ciphertextSha256: recording.ciphertextSha256,
561
+ storageFormat: 3, plaintextLength, startedAtMs: recording.startedAtMs,
562
+ startedAt: new Date(catalogInteger(recording.startedAtMs, 8640000000000000n)),
563
+ durationMs: catalogInteger(recording.durationMs),
564
+ };
565
+ };
566
+
487
567
  const bytesToHex = (value: Uint8Array): string =>
488
568
  Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
489
569
 
@@ -710,6 +790,7 @@ const toNativeRecordingUploadRequest = (
710
790
  recordingId: task.recordingId,
711
791
  deviceId: task.deviceId,
712
792
  localPath: task.localPath,
793
+ ...(task.fileSizeBytes === undefined ? {} : { fileSizeBytes: task.fileSizeBytes }),
713
794
  uploadUrl: task.uploadUrl,
714
795
  ...(task.uploadToken ? { uploadToken: task.uploadToken } : {}),
715
796
  ...(task.completeUrl ? { completeUrl: task.completeUrl } : {}),
@@ -724,9 +805,13 @@ const toNativeRecordingUploadRequest = (
724
805
  });
725
806
 
726
807
  const parseUploadQueue = (serialized: string): UploadTask[] => {
727
- const value: unknown = JSON.parse(serialized || '[]');
728
- if (!Array.isArray(value)) return [];
808
+ const value: unknown = JSON.parse(serialized);
809
+ if (!Array.isArray(value)) throw new Error('Invalid upload recovery journal');
729
810
  return value.map((task) => {
811
+ if (!task || typeof task !== 'object' || Array.isArray(task) ||
812
+ typeof task.createdAt !== 'string' || typeof task.updatedAt !== 'string') {
813
+ throw new Error('Invalid upload recovery journal');
814
+ }
730
815
  const candidate = task as UploadTask & {
731
816
  createdAt: string | Date;
732
817
  updatedAt: string | Date;
@@ -740,6 +825,8 @@ const parseUploadQueue = (serialized: string): UploadTask[] => {
740
825
  };
741
826
 
742
827
  export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKClient => {
828
+ let presenceStopped = false;
829
+ let presenceGeneration = 0;
743
830
  const requireNativeModule = (): Spec => {
744
831
  if (!nativeModule) throw new BotaNativeModuleError();
745
832
  return nativeModule;
@@ -1056,6 +1143,11 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1056
1143
  return values.map(mapRecording);
1057
1144
  },
1058
1145
 
1146
+ async listPendingRecordings(device) {
1147
+ const values = await requireNativeModule().listPendingRecordings(toNativeConnectedDevice(device));
1148
+ return values.map(mapPendingRecording);
1149
+ },
1150
+
1059
1151
  async syncRecording(device, recording, onProgress, sinkId) {
1060
1152
  const module = requireNativeModule();
1061
1153
  const subscription = module.onRecordingTransferProgress((progress) => {
@@ -1072,21 +1164,48 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1072
1164
  }
1073
1165
  },
1074
1166
 
1075
- async syncEncryptedRecordingV2(device, recording, provider, onProgress) {
1167
+ async syncEncryptedRecordingV2(device, recording, provider, onProgress, options = {}) {
1168
+ if (options.signal?.aborted) throw new Error('encrypted upload v2 cancelled');
1076
1169
  const module = requireNativeModule();
1077
- const operationId = createOpaqueId();
1078
- const profileSubscription = module.onEncryptedUploadV2ProfileRequested(
1170
+ const operationId = options.operationId ?? createOpaqueId();
1171
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(operationId)) {
1172
+ throw new Error('invalid encrypted upload v2 operation ID');
1173
+ }
1174
+ let active = true;
1175
+ let nativeStarted = false;
1176
+ let cancellation: Promise<void> | undefined;
1177
+ let cancellationFailed = false;
1178
+ const cancel = () => {
1179
+ if (nativeStarted && !cancellation) {
1180
+ try {
1181
+ cancellation = module.cancelEncryptedRecordingV2(operationId).catch(() => { cancellationFailed = true; });
1182
+ } catch {
1183
+ cancellationFailed = true;
1184
+ }
1185
+ }
1186
+ };
1187
+ let profileSubscription: BotaEventSubscription | undefined;
1188
+ let progressSubscription: BotaEventSubscription | undefined;
1189
+ try {
1190
+ options.signal?.addEventListener('abort', cancel, { once: true });
1191
+ profileSubscription = module.onEncryptedUploadV2ProfileRequested(
1079
1192
  (request) => {
1080
- if (request.operationId !== operationId) return;
1193
+ if (!active || options.signal?.aborted || request.operationId !== operationId) return;
1081
1194
  void (async () => {
1195
+ let decision: BotaEncryptedUploadV2ProfileDecision | undefined;
1082
1196
  try {
1083
- const decision = await provider({
1197
+ decision = await provider({
1198
+ operationId,
1084
1199
  recording: request.recording,
1085
1200
  capability: request.capability,
1086
1201
  ...(request.checkpoint === undefined
1087
1202
  ? {}
1088
1203
  : { checkpoint: request.checkpoint }),
1089
1204
  });
1205
+ if (!active || options.signal?.aborted) {
1206
+ await module.releaseEncryptedUploadV2Material(decision.materialRegistrationId);
1207
+ return;
1208
+ }
1090
1209
  if (decision.profile !== 'encrypted_upload_v2') {
1091
1210
  throw new Error('encrypted upload v2 requires an explicit v2 profile');
1092
1211
  }
@@ -1095,29 +1214,40 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1095
1214
  decision
1096
1215
  );
1097
1216
  } catch (error) {
1098
- await module.rejectEncryptedUploadV2Profile(
1099
- request.requestId,
1100
- 'application_material_rejected'
1101
- );
1217
+ if (decision) await module.releaseEncryptedUploadV2Material(decision.materialRegistrationId).catch(() => {});
1218
+ if (active && !options.signal?.aborted) {
1219
+ await module.rejectEncryptedUploadV2Profile(request.requestId, 'application_material_rejected');
1220
+ }
1102
1221
  }
1103
1222
  })().catch(() => {});
1104
1223
  }
1105
1224
  );
1106
- const progressSubscription = module.onEncryptedUploadV2Progress(
1225
+ progressSubscription = module.onEncryptedUploadV2Progress(
1107
1226
  (progress) => {
1108
- if (progress.operationId !== operationId) return;
1227
+ if (!active || options.signal?.aborted || progress.operationId !== operationId) return;
1109
1228
  onProgress?.(mapEncryptedUploadV2Progress(progress));
1110
1229
  }
1111
1230
  );
1112
- try {
1231
+ if (options.signal?.aborted) throw new Error('encrypted upload v2 cancelled');
1232
+ nativeStarted = true;
1113
1233
  await module.syncEncryptedRecordingV2(
1114
1234
  toNativeConnectedDevice(device),
1115
1235
  recording,
1116
1236
  operationId
1117
1237
  );
1118
1238
  } finally {
1119
- profileSubscription.remove();
1120
- progressSubscription.remove();
1239
+ active = false;
1240
+ let cleanupFailed = false;
1241
+ for (const remove of [
1242
+ () => options.signal?.removeEventListener('abort', cancel),
1243
+ () => profileSubscription?.remove(),
1244
+ () => progressSubscription?.remove(),
1245
+ ]) {
1246
+ try { remove(); } catch { cleanupFailed = true; }
1247
+ }
1248
+ await cancellation;
1249
+ if (cancellationFailed) throw new Error('encrypted upload v2 cancellation failed');
1250
+ if (cleanupFailed) throw new Error('encrypted upload v2 listener cleanup failed');
1121
1251
  }
1122
1252
  },
1123
1253
 
@@ -1148,6 +1278,10 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1148
1278
  await requireNativeModule().cancelRecordingUpload(taskId);
1149
1279
  },
1150
1280
 
1281
+ async releaseRecordingFile(taskId, localPath) {
1282
+ await requireNativeModule().releaseRecordingFile(taskId, localPath);
1283
+ },
1284
+
1151
1285
  async loadUploadQueue() {
1152
1286
  return parseUploadQueue(
1153
1287
  await requireNativeModule().loadCompatibilityUploadQueue()
@@ -1278,6 +1412,30 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1278
1412
  };
1279
1413
 
1280
1414
  const logs: BotaDeviceSDKLogClient = {
1415
+ async readDiagnosticEvents(device) {
1416
+ const batch = await requireNativeModule().readDiagnosticEvents(toNativeConnectedDevice(device));
1417
+ if (batch.schema_version !== 1) throw new Error('Unsupported diagnostic batch schema');
1418
+ return {
1419
+ schema_version: 1,
1420
+ events: batch.events.map(({ report, ...event }) => ({
1421
+ ...event,
1422
+ event_type: event.event_type as DeviceDiagnosticEventType,
1423
+ reason_code: event.reason_code as DeviceDiagnosticReasonCode,
1424
+ ...(report ? { report: {
1425
+ fault: report.fault,
1426
+ execution: { ...report.execution, pc_trace: [...report.execution.pc_trace] },
1427
+ ...(report.runtime ? { runtime: report.runtime } : {}),
1428
+ ...(report.breadcrumbs ? { breadcrumbs: [...report.breadcrumbs] } : {}),
1429
+ } } : {}),
1430
+ })),
1431
+ };
1432
+ },
1433
+ async acknowledgeDiagnosticEvents(device, acceptedEventIds) {
1434
+ if (acceptedEventIds.some((id) => !/^[0-9a-f]{16}$/.test(id))) {
1435
+ throw new Error('Diagnostic event_id must be 16 lowercase hex characters');
1436
+ }
1437
+ await requireNativeModule().acknowledgeDiagnosticEvents(toNativeConnectedDevice(device), [...acceptedEventIds]);
1438
+ },
1281
1439
  async subscribe(device, onLine) {
1282
1440
  const module = requireNativeModule();
1283
1441
  const eventSubscription = module.onDeviceLog((line) => {
@@ -1365,6 +1523,15 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1365
1523
  };
1366
1524
 
1367
1525
  const client: BotaDeviceSDKClient = {
1526
+ clientPresence: {
1527
+ async nextReport(deviceId) {
1528
+ if (presenceStopped) return null;
1529
+ const generation = presenceGeneration;
1530
+ const report = await requireNativeModule().nextClientPresence(deviceId);
1531
+ if (presenceStopped || generation !== presenceGeneration) return null;
1532
+ return report === null ? null : { ...report, sdkPackage: SDK_PACKAGE, sdkVersion: SDK_VERSION };
1533
+ },
1534
+ },
1368
1535
  controls,
1369
1536
  devices,
1370
1537
  factoryReset,
@@ -1376,6 +1543,7 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1376
1543
  wifi,
1377
1544
 
1378
1545
  async configure(configuration = {}) {
1546
+ const generation = presenceGeneration;
1379
1547
  const nativeConfiguration: NativeConfiguration = {
1380
1548
  logLevel: configuration.logLevel ?? 'warn',
1381
1549
  ...(configuration.applicationSupportDirectory
@@ -1386,9 +1554,12 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1386
1554
  : {}),
1387
1555
  };
1388
1556
  await requireNativeModule().configure(nativeConfiguration);
1557
+ if (generation === presenceGeneration) presenceStopped = false;
1389
1558
  },
1390
1559
 
1391
1560
  async destroy() {
1561
+ presenceStopped = true;
1562
+ presenceGeneration++;
1392
1563
  await requireNativeModule().destroy();
1393
1564
  },
1394
1565
 
@@ -0,0 +1,33 @@
1
+ /** Optional diagnostics, not device attestation or command authority. */
2
+ export interface SdkClientContext {
3
+ schema_version: 1;
4
+ session_id: string;
5
+ sequence: number;
6
+ platform: 'ios' | 'android';
7
+ sdk_package: string;
8
+ sdk_version: string;
9
+ }
10
+
11
+ export interface ClientPresence {
12
+ nextReport(deviceId: string): Promise<SdkClientContext | null>;
13
+ }
14
+
15
+ /** Read fresh BLE status only while the same verified connection and host scope survive.
16
+ * The caller owns authentication, binding generation, scheduling and HTTP.
17
+ * Unavailable presence or a changed scope/session skips reporting without BLE writes.
18
+ */
19
+ export async function readClientPresenceObservation<T>(options: {
20
+ presence: ClientPresence;
21
+ deviceId: string;
22
+ isCurrent(): boolean;
23
+ readStatus(): Promise<T>;
24
+ }): Promise<{ status: T; clientContext: SdkClientContext } | null> {
25
+ if (!options.isCurrent()) return null;
26
+ const before = await options.presence.nextReport(options.deviceId);
27
+ if (!before || !options.isCurrent()) return null;
28
+ const status = await options.readStatus();
29
+ if (!options.isCurrent()) return null;
30
+ const after = await options.presence.nextReport(options.deviceId);
31
+ if (!after || !options.isCurrent() || before.session_id !== after.session_id) return null;
32
+ return { status, clientContext: after };
33
+ }