@bota.dev/react-native-app-sdk 2.0.0-beta.13 → 2.0.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +49 -7
  2. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +2 -2
  3. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +3 -0
  4. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +1 -0
  5. package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +37 -3
  6. package/android/src/main/java/dev/bota/sdk/reactnative/upload/ByoBridgeDriver.kt +79 -0
  7. package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +30 -2
  8. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +5 -2
  9. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadStreamingStatus.kt +67 -0
  10. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +23 -0
  11. package/generated/codegen-contract.json +110 -8
  12. package/ios/BotaDeviceSDK.mm +1 -0
  13. package/ios/BotaDeviceSDKAppleBridge.swift +4 -2
  14. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +3 -3
  15. package/ios/BotaDeviceSDKAppleRecordings.swift +1 -0
  16. package/ios/BotaUploadV2Backend.mm +21 -0
  17. package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +52 -2
  18. package/ios/UploadBackend/ByoBridgeDriver.swift +94 -0
  19. package/ios/UploadBackend/UploadModels.swift +6 -1
  20. package/ios/UploadBackend/UploadOperation.swift +41 -3
  21. package/ios/UploadBackend/UploadStreamingStatus.swift +43 -0
  22. package/lib/commonjs/byoStorageBackend.js +144 -0
  23. package/lib/commonjs/byoStorageBackend.js.map +1 -0
  24. package/lib/commonjs/client.js +2 -1
  25. package/lib/commonjs/client.js.map +1 -1
  26. package/lib/commonjs/encryptedUploadV2Backend.js +4 -1
  27. package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -1
  28. package/lib/commonjs/index.js +7 -1
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/managers/DeviceManager.js +6 -0
  31. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  32. package/lib/commonjs/managers/NativeUploadQueue.js +22 -8
  33. package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -1
  34. package/lib/commonjs/managers/uploadRecoveryMetadata.js +4 -0
  35. package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -1
  36. package/lib/commonjs/managers/uploadVerificationPending.js +20 -0
  37. package/lib/commonjs/managers/uploadVerificationPending.js.map +1 -0
  38. package/lib/commonjs/protectedStreamingStatus.js +120 -0
  39. package/lib/commonjs/protectedStreamingStatus.js.map +1 -0
  40. package/lib/commonjs/sdkIdentity.js +1 -1
  41. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
  42. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -1
  43. package/lib/commonjs/uploadRecovery.js +50 -7
  44. package/lib/commonjs/uploadRecovery.js.map +1 -1
  45. package/lib/module/byoStorageBackend.js +140 -0
  46. package/lib/module/byoStorageBackend.js.map +1 -0
  47. package/lib/module/client.js +2 -1
  48. package/lib/module/client.js.map +1 -1
  49. package/lib/module/encryptedUploadV2Backend.js +4 -1
  50. package/lib/module/encryptedUploadV2Backend.js.map +1 -1
  51. package/lib/module/index.js +4 -0
  52. package/lib/module/index.js.map +1 -1
  53. package/lib/module/managers/DeviceManager.js +6 -0
  54. package/lib/module/managers/DeviceManager.js.map +1 -1
  55. package/lib/module/managers/NativeUploadQueue.js +22 -8
  56. package/lib/module/managers/NativeUploadQueue.js.map +1 -1
  57. package/lib/module/managers/uploadRecoveryMetadata.js +4 -0
  58. package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -1
  59. package/lib/module/managers/uploadVerificationPending.js +14 -0
  60. package/lib/module/managers/uploadVerificationPending.js.map +1 -0
  61. package/lib/module/protectedStreamingStatus.js +116 -0
  62. package/lib/module/protectedStreamingStatus.js.map +1 -0
  63. package/lib/module/sdkIdentity.js +1 -1
  64. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
  65. package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -1
  66. package/lib/module/uploadRecovery.js +50 -7
  67. package/lib/module/uploadRecovery.js.map +1 -1
  68. package/lib/typescript/byoStorageBackend.d.ts +36 -0
  69. package/lib/typescript/byoStorageBackend.d.ts.map +1 -0
  70. package/lib/typescript/client.d.ts.map +1 -1
  71. package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -1
  72. package/lib/typescript/index.d.ts +6 -0
  73. package/lib/typescript/index.d.ts.map +1 -1
  74. package/lib/typescript/managers/DeviceManager.d.ts +2 -0
  75. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
  76. package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -1
  77. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -1
  78. package/lib/typescript/managers/uploadVerificationPending.d.ts +6 -0
  79. package/lib/typescript/managers/uploadVerificationPending.d.ts.map +1 -0
  80. package/lib/typescript/models/Recording.d.ts +6 -0
  81. package/lib/typescript/models/Recording.d.ts.map +1 -1
  82. package/lib/typescript/protectedStreamingStatus.d.ts +37 -0
  83. package/lib/typescript/protectedStreamingStatus.d.ts.map +1 -0
  84. package/lib/typescript/sdkIdentity.d.ts +1 -1
  85. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +1 -0
  86. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
  87. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +4 -0
  88. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -1
  89. package/lib/typescript/uploadRecovery.d.ts +1 -1
  90. package/lib/typescript/uploadRecovery.d.ts.map +1 -1
  91. package/package.json +1 -1
  92. package/src/byoStorageBackend.ts +104 -0
  93. package/src/client.ts +2 -0
  94. package/src/encryptedUploadV2Backend.ts +2 -1
  95. package/src/index.ts +11 -0
  96. package/src/managers/DeviceManager.ts +6 -0
  97. package/src/managers/NativeUploadQueue.ts +18 -9
  98. package/src/managers/uploadRecoveryMetadata.ts +2 -0
  99. package/src/managers/uploadVerificationPending.ts +10 -0
  100. package/src/models/Recording.ts +6 -0
  101. package/src/protectedStreamingStatus.ts +137 -0
  102. package/src/sdkIdentity.ts +1 -1
  103. package/src/specs/NativeBotaDeviceSDK.ts +1 -0
  104. package/src/specs/NativeBotaUploadV2Backend.ts +4 -0
  105. package/src/uploadRecovery.ts +42 -8
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@bota.dev/react-native-app-sdk",
4
- "packageVersion": "2.0.0-beta.13",
4
+ "packageVersion": "2.0.0-beta.15",
5
5
  "reactNativeVersion": "0.86.3",
6
6
  "codegenLibrary": "BotaDeviceSDKSpec",
7
7
  "nativeModule": "BotaDeviceSDK",
@@ -1216,6 +1216,13 @@
1216
1216
  "typeAnnotation": {
1217
1217
  "type": "NumberTypeAnnotation"
1218
1218
  }
1219
+ },
1220
+ {
1221
+ "name": "markersRequired",
1222
+ "optional": true,
1223
+ "typeAnnotation": {
1224
+ "type": "BooleanTypeAnnotation"
1225
+ }
1219
1226
  }
1220
1227
  ]
1221
1228
  },
@@ -4143,6 +4150,101 @@
4143
4150
  }
4144
4151
  ],
4145
4152
  "methods": [
4153
+ {
4154
+ "name": "syncByo",
4155
+ "optional": false,
4156
+ "typeAnnotation": {
4157
+ "type": "FunctionTypeAnnotation",
4158
+ "returnTypeAnnotation": {
4159
+ "type": "PromiseTypeAnnotation",
4160
+ "elementType": {
4161
+ "type": "StringTypeAnnotation"
4162
+ }
4163
+ },
4164
+ "params": [
4165
+ {
4166
+ "name": "inputJSON",
4167
+ "optional": false,
4168
+ "typeAnnotation": {
4169
+ "type": "StringTypeAnnotation"
4170
+ }
4171
+ }
4172
+ ]
4173
+ }
4174
+ },
4175
+ {
4176
+ "name": "cancelByo",
4177
+ "optional": false,
4178
+ "typeAnnotation": {
4179
+ "type": "FunctionTypeAnnotation",
4180
+ "returnTypeAnnotation": {
4181
+ "type": "PromiseTypeAnnotation",
4182
+ "elementType": {
4183
+ "type": "VoidTypeAnnotation"
4184
+ }
4185
+ },
4186
+ "params": [
4187
+ {
4188
+ "name": "operationId",
4189
+ "optional": false,
4190
+ "typeAnnotation": {
4191
+ "type": "StringTypeAnnotation"
4192
+ }
4193
+ }
4194
+ ]
4195
+ }
4196
+ },
4197
+ {
4198
+ "name": "readProtectedStreamingStatus",
4199
+ "optional": false,
4200
+ "typeAnnotation": {
4201
+ "type": "FunctionTypeAnnotation",
4202
+ "returnTypeAnnotation": {
4203
+ "type": "PromiseTypeAnnotation",
4204
+ "elementType": {
4205
+ "type": "StringTypeAnnotation"
4206
+ }
4207
+ },
4208
+ "params": [
4209
+ {
4210
+ "name": "requestId",
4211
+ "optional": false,
4212
+ "typeAnnotation": {
4213
+ "type": "StringTypeAnnotation"
4214
+ }
4215
+ },
4216
+ {
4217
+ "name": "inputJSON",
4218
+ "optional": false,
4219
+ "typeAnnotation": {
4220
+ "type": "StringTypeAnnotation"
4221
+ }
4222
+ }
4223
+ ]
4224
+ }
4225
+ },
4226
+ {
4227
+ "name": "cancelProtectedStreamingStatus",
4228
+ "optional": false,
4229
+ "typeAnnotation": {
4230
+ "type": "FunctionTypeAnnotation",
4231
+ "returnTypeAnnotation": {
4232
+ "type": "PromiseTypeAnnotation",
4233
+ "elementType": {
4234
+ "type": "VoidTypeAnnotation"
4235
+ }
4236
+ },
4237
+ "params": [
4238
+ {
4239
+ "name": "requestId",
4240
+ "optional": false,
4241
+ "typeAnnotation": {
4242
+ "type": "StringTypeAnnotation"
4243
+ }
4244
+ }
4245
+ ]
4246
+ }
4247
+ },
4146
4248
  {
4147
4249
  "name": "prepare",
4148
4250
  "optional": false,
@@ -4274,11 +4376,11 @@
4274
4376
  },
4275
4377
  {
4276
4378
  "path": "java/dev/bota/sdk/reactnative/NativeBotaUploadV2BackendSpec.java",
4277
- "sha256": "50844eb0e1ed98848de2819d2a88f00ccc4a550d4bcfdc5db21039a1eb17127f"
4379
+ "sha256": "a6b48b64cb3531d04b204172456115c041025d18db612d836174ced2617d50fe"
4278
4380
  },
4279
4381
  {
4280
4382
  "path": "jni/BotaDeviceSDKSpec-generated.cpp",
4281
- "sha256": "f1a82f3cf03616e6f08655204af85317087cd2b0f633c052f2939eb49448c6b8"
4383
+ "sha256": "c98c66a2a320b56be0f7106e35a867d4664e6c8029836c153443cea4e1dd38ce"
4282
4384
  },
4283
4385
  {
4284
4386
  "path": "jni/BotaDeviceSDKSpec.h",
@@ -4290,23 +4392,23 @@
4290
4392
  },
4291
4393
  {
4292
4394
  "path": "jni/react/renderer/components/BotaDeviceSDKSpec/BotaDeviceSDKSpecJSI.h",
4293
- "sha256": "6c5309bc7b6b6b4b2e62e5bed85a7fab8a9da8c43af9e3904692e50756de9e0f"
4395
+ "sha256": "6c3ab34b33b2e18b2237e389bbca5f813aa679c3ec66e61441ac8fea4a1904a5"
4294
4396
  }
4295
4397
  ],
4296
4398
  "ios": [
4297
4399
  {
4298
4400
  "path": "BotaDeviceSDKSpec/BotaDeviceSDKSpec-generated.mm",
4299
- "sha256": "8bee25e75f2e73f0b989b65e1c083fb851ba77ca3d4384aecaabb1bcb252fe7a"
4401
+ "sha256": "23bd8488e1e6a59795ec1845d7d6287731e7924e3211655899a58992efa3f6e1"
4300
4402
  },
4301
4403
  {
4302
4404
  "path": "BotaDeviceSDKSpec/BotaDeviceSDKSpec.h",
4303
- "sha256": "6bef81e2a87bb83ddbc3021e69ddbd280dac23a5049af675b738925454b1c22c"
4405
+ "sha256": "221da1ba25df2b6c8bac02a90fccdb701dc3644c0bfd9df31c75c117a2f9b47c"
4304
4406
  },
4305
4407
  {
4306
4408
  "path": "BotaDeviceSDKSpecJSI.h",
4307
- "sha256": "6c5309bc7b6b6b4b2e62e5bed85a7fab8a9da8c43af9e3904692e50756de9e0f"
4409
+ "sha256": "6c3ab34b33b2e18b2237e389bbca5f813aa679c3ec66e61441ac8fea4a1904a5"
4308
4410
  }
4309
4411
  ]
4310
4412
  },
4311
- "contractDigest": "d7f7eef4625f160f168c8e7440128c056f3bc826a33ca61c9fc2e91ecf074c64"
4413
+ "contractDigest": "7b58a71c032bb3c46157b5c52ffc41a655af87186e5a9cd812e8330a93fed844"
4312
4414
  }
@@ -670,6 +670,7 @@ RCT_EXPORT_MODULE(BotaDeviceSDK)
670
670
  durationMs:recording.durationMs()
671
671
  plaintextLength:recording.plaintextLength()
672
672
  storageFormat:recording.storageFormat().has_value() ? @(recording.storageFormat().value()) : nil
673
+ markersRequired:recording.markersRequired().value_or(false)
673
674
  operationID:operationId
674
675
  onProfileRequest:^(NSDictionary *request) {
675
676
  [weakSelf emitOnEncryptedUploadV2ProfileRequested:request];
@@ -909,7 +909,7 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
909
909
  }
910
910
  }
911
911
 
912
- @objc(syncEncryptedRecordingV2WithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:generation:ciphertextLength:ciphertextSHA256:startedAtMs:durationMs:plaintextLength:storageFormat:operationID:onProfileRequest:onProgress:completion:)
912
+ @objc(syncEncryptedRecordingV2WithID:serialNumber:deviceType:firmwareVersion:hardwareRevision:isProvisioned:connectionState:mtu:recordingUUID:generation:ciphertextLength:ciphertextSHA256:startedAtMs:durationMs:plaintextLength:storageFormat:markersRequired:operationID:onProfileRequest:onProgress:completion:)
913
913
  public func syncEncryptedRecordingV2(
914
914
  id: String,
915
915
  serialNumber: String,
@@ -927,6 +927,7 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
927
927
  durationMs: String?,
928
928
  plaintextLength: String?,
929
929
  storageFormat: NSNumber?,
930
+ markersRequired: Bool,
930
931
  operationID: String,
931
932
  onProfileRequest: @escaping @Sendable ([String: Any]) -> Void,
932
933
  onProgress: @escaping @Sendable ([String: Any]) -> Void,
@@ -964,7 +965,8 @@ public final class BotaDeviceSDKAppleBridge: NSObject, @unchecked Sendable {
964
965
  startedAtMs: try Self.catalogDecimal(startedAtMs),
965
966
  durationMs: try Self.catalogDecimal(durationMs),
966
967
  plaintextLength: try Self.catalogDecimal(plaintextLength),
967
- storageFormat: format
968
+ storageFormat: format,
969
+ markersRequired: markersRequired
968
970
  ),
969
971
  operationID: operationID,
970
972
  onProfileRequest: onProfileRequest,
@@ -179,7 +179,7 @@ actor BotaDeviceSDKAppleRecordingUploads {
179
179
  guard let tasks = try JSONSerialization.jsonObject(with: Data(serialized.utf8)) as? [[String: Any]] else {
180
180
  throw UploadError.invalidJournal
181
181
  }
182
- let fields: Set<String> = ["id", "recordingId", "deviceId", "recordingUuid", "localPath", "status", "retryCount", "createdAt", "updatedAt", "recoveryScope", "fileSizeBytes", "nextAttemptAt", "contentType", "contentSha256", "relayUpload"]
182
+ let fields: Set<String> = ["id", "recordingId", "deviceId", "recordingUuid", "localPath", "status", "retryCount", "createdAt", "updatedAt", "recoveryScope", "fileSizeBytes", "nextAttemptAt", "contentType", "contentSha256", "relayUpload", "completionPending"]
183
183
  var ids = Set<String>()
184
184
  return try tasks.map { task in
185
185
  guard let id = task["id"] as? String, !id.isEmpty, ids.insert(id).inserted,
@@ -195,8 +195,8 @@ actor BotaDeviceSDKAppleRecordingUploads {
195
195
  for key in ["fileSizeBytes", "nextAttemptAt"] where task[key] != nil {
196
196
  guard validInteger(task[key]) else { throw UploadError.invalidJournal }
197
197
  }
198
- if let relay = task["relayUpload"] {
199
- guard let number = relay as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() else { throw UploadError.invalidJournal }
198
+ for key in ["relayUpload", "completionPending"] where task[key] != nil {
199
+ guard let number = task[key] as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() else { throw UploadError.invalidJournal }
200
200
  }
201
201
  let dates = ISO8601DateFormatter()
202
202
  let fractionalDates = ISO8601DateFormatter()
@@ -646,6 +646,7 @@ actor BotaDeviceSDKAppleRecordings {
646
646
  "durationMs": String(recording.durationMs),
647
647
  "plaintextLength": String(recording.plaintextLength),
648
648
  "storageFormat": Int(recording.storageFormat),
649
+ "markersRequired": recording.markersRequired,
649
650
  ]
650
651
  }
651
652
 
@@ -16,6 +16,15 @@ RCT_EXPORT_MODULE(BotaUploadV2Backend)
16
16
  if ((self = [super init])) { _backend = [BotaUploadV2BackendBridge new]; }
17
17
  return self;
18
18
  }
19
+ - (void)readProtectedStreamingStatus:(NSString *)requestId inputJSON:(NSString *)inputJSON resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
20
+ [_backend readProtectedStreamingStatus:requestId inputJSON:inputJSON completion:^(NSString *result, NSError *error) {
21
+ if (error) { reject(@"BOTA_STREAM_STATUS_FAILED", @"BOTA_STREAM_STATUS_FAILED", nil); }
22
+ else { resolve(result); }
23
+ }];
24
+ }
25
+ - (void)cancelProtectedStreamingStatus:(NSString *)requestId resolve:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPromiseRejectBlock)reject {
26
+ [_backend cancelProtectedStreamingStatus:requestId completion:^{ resolve(nil); }];
27
+ }
19
28
  - (void)prepare:(NSString *)inputJSON resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
20
29
  __weak BotaUploadV2Backend *weakSelf = self;
21
30
  [_backend prepare:inputJSON credentialsRequested:^(NSString *requestId, NSString *operationId) {
@@ -25,6 +34,18 @@ RCT_EXPORT_MODULE(BotaUploadV2Backend)
25
34
  else { resolve(result); }
26
35
  }];
27
36
  }
37
+ - (void)syncByo:(NSString *)inputJSON resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
38
+ __weak BotaUploadV2Backend *weakSelf = self;
39
+ [_backend syncByo:inputJSON credentialsRequested:^(NSString *requestId, NSString *operationId) {
40
+ [weakSelf emitOnCredentialsRequested:@{@"requestId": requestId, @"operationId": operationId}];
41
+ } completion:^(NSString *result, NSError *error) {
42
+ if (error) { reject(error.userInfo[@"byoCode"] ?: @"byo_upload_failed", error.localizedDescription, nil); }
43
+ else { resolve(result); }
44
+ }];
45
+ }
46
+ - (void)cancelByo:(NSString *)operationId resolve:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPromiseRejectBlock)reject {
47
+ [_backend cancelByo:operationId completion:^{ resolve(nil); }];
48
+ }
28
49
  - (void)cancel:(NSString *)operationId resolve:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPromiseRejectBlock)reject {
29
50
  [_backend cancel:operationId completion:^{ resolve(nil); }];
30
51
  }
@@ -4,9 +4,28 @@ import Foundation
4
4
  @objc(BotaUploadV2BackendBridge)
5
5
  public final class BotaUploadV2BackendBridge: NSObject, @unchecked Sendable {
6
6
  @MainActor private var coordinator: UploadCoordinator?
7
+ @MainActor private var byo: ByoBridgeDriver?
7
8
  @MainActor private var destroyed = false
9
+ @MainActor private var statusJobs: [String: Task<Void, Never>] = [:]
8
10
  @MainActor private var credentialsRequested: (@Sendable (String, String) -> Void)?
9
11
 
12
+ @objc(readProtectedStreamingStatus:inputJSON:completion:)
13
+ public func readProtectedStreamingStatus(_ id: String, inputJSON: String,
14
+ completion: @escaping @Sendable (String?, NSError?) -> Void) {
15
+ Task { @MainActor in
16
+ guard !destroyed, statusJobs[id] == nil else { completion(nil, UploadFailure.cancelled as NSError); return }
17
+ statusJobs[id] = Task { @MainActor in
18
+ defer { statusJobs[id] = nil }
19
+ do { let result = try await UploadStreamingStatus().read(inputJSON); completion(result, nil) }
20
+ catch { completion(nil, UploadFailure.safe(error) as NSError) }
21
+ }
22
+ }
23
+ }
24
+ @objc(cancelProtectedStreamingStatus:completion:)
25
+ public func cancelProtectedStreamingStatus(_ id: String, completion: @escaping @Sendable () -> Void) {
26
+ Task { @MainActor in statusJobs[id]?.cancel(); completion() }
27
+ }
28
+
10
29
  @objc(prepare:credentialsRequested:completion:)
11
30
  public func prepare(_ json: String,
12
31
  credentialsRequested: @escaping @Sendable (String, String) -> Void,
@@ -23,6 +42,26 @@ public final class BotaUploadV2BackendBridge: NSObject, @unchecked Sendable {
23
42
  }
24
43
  }
25
44
 
45
+ @objc(syncByo:credentialsRequested:completion:)
46
+ public func syncByo(_ json: String, credentialsRequested: @escaping @Sendable (String, String) -> Void,
47
+ completion: @escaping @Sendable (String?, NSError?) -> Void) {
48
+ Task { @MainActor in
49
+ do {
50
+ guard !destroyed else { throw UploadFailure.cancelled }
51
+ self.credentialsRequested = credentialsRequested
52
+ completion(try await byoService().sync(json), nil)
53
+ } catch { completion(nil, (error is ByoStorageError ? error : UploadFailure.safe(error)) as NSError) }
54
+ }
55
+ }
56
+ @objc(cancelByo:completion:)
57
+ public func cancelByo(_ id: String, completion: @escaping @Sendable () -> Void) {
58
+ Task { @MainActor in await byoService().cancel(id); completion() }
59
+ }
60
+ @MainActor private func byoService() -> ByoBridgeDriver {
61
+ if let byo { return byo }
62
+ let value = ByoBridgeDriver(credentials: service().credentials); byo = value; return value
63
+ }
64
+
26
65
  @objc(cancel:completion:)
27
66
  public func cancel(_ id: String, completion: @escaping @Sendable () -> Void) {
28
67
  Task { @MainActor in coordinator?.cancel(id); completion() }
@@ -45,7 +84,11 @@ public final class BotaUploadV2BackendBridge: NSObject, @unchecked Sendable {
45
84
  Task { @MainActor in coordinator?.credentials.reject(requestId: id); completion() }
46
85
  }
47
86
  @objc public func invalidate() {
48
- Task { @MainActor in destroyed = true; coordinator?.cancelAll(); credentialsRequested = nil }
87
+ Task { @MainActor in
88
+ destroyed = true; coordinator?.cancelAll(); credentialsRequested = nil
89
+ statusJobs.values.forEach { $0.cancel() }; statusJobs.removeAll()
90
+ await byo?.stop()
91
+ }
49
92
  }
50
93
 
51
94
  @MainActor private func service() -> UploadCoordinator {
@@ -69,6 +112,12 @@ public final class BotaUploadV2BackendBridge: NSObject, @unchecked Sendable {
69
112
  default: throw UploadFailure.invalidDocument
70
113
  }
71
114
  guard let sessionId = UUID(uuidString: prepared.session.session_id) else { throw UploadFailure.invalidDocument }
115
+ let submitMarkers: EncryptedUploadV2Material.MarkerSubmitter?
116
+ if operation.input.recording.markersRequired == true {
117
+ submitMarkers = { documents, evidence in
118
+ try await operation.submitMarkers(documents, evidence: Self.evidence(evidence))
119
+ }
120
+ } else { submitMarkers = nil }
72
121
  let material = EncryptedUploadV2Material(
73
122
  materialID: UUID().uuidString.lowercased(), recordingID: operation.input.recording.uuid,
74
123
  uploadSessionID: sessionId, ownerRevision: prepared.session.owner_revision, policy: policy,
@@ -83,7 +132,8 @@ public final class BotaUploadV2BackendBridge: NSObject, @unchecked Sendable {
83
132
  return EncryptedUploadV2ContextExchange(challenge: context.challenge, exchangeProof: context.exchangeProof)
84
133
  },
85
134
  shouldUploadCiphertext: { try await operation.shouldUpload(Self.evidence($0)) },
86
- reconcileStaging: { try await operation.reconcileStaging($0, evidence: Self.evidence($1)) }
135
+ reconcileStaging: { try await operation.reconcileStaging($0, evidence: Self.evidence($1)) },
136
+ submitMarkers: submitMarkers
87
137
  )
88
138
  return BotaDeviceSDKEncryptedUploadV2Materials.register(material)
89
139
  }, unregister: { id in _ = BotaDeviceSDKEncryptedUploadV2Materials.remove(id) })
@@ -0,0 +1,94 @@
1
+ import BotaAppSDK
2
+ import Foundation
3
+
4
+ /// Metadata-only RN boundary. SDK native code owns documents and recording bytes.
5
+ @MainActor final class ByoBridgeDriver {
6
+ private let credentials: UploadCredentials
7
+ private var tasks: [String: Task<String, Error>] = [:]
8
+ private var cancelled: Set<String> = []
9
+ private var stopped = false
10
+ init(credentials: UploadCredentials) { self.credentials = credentials }
11
+ private struct Input: Decodable {
12
+ let operationId: String
13
+ let device: Device
14
+ let recording: Recording?
15
+ let mode: String
16
+ let scope: Scope
17
+ struct Recording: Decodable { let uuid: String; let generation: UInt32 }
18
+ struct Scope: Decodable {
19
+ let baseUrl, scopeKey, organizationId, projectId, environment, deviceId: String
20
+ let bindingGeneration: UInt32
21
+ }
22
+ struct Device: Decodable {
23
+ let id, serialNumber, deviceType, firmwareVersion, connectionState: String
24
+ let hardwareRevision: String?
25
+ let isProvisioned: Bool
26
+ let mtu: Int
27
+ func native() throws -> ConnectedDevice {
28
+ let type: DeviceType
29
+ switch deviceType {
30
+ case "bota_pin": type = .botaPin
31
+ case "bota_pin_4g": type = .botaPin4G
32
+ case "bota_note": type = .botaNote
33
+ default: throw UploadFailure.invalidInput
34
+ }
35
+ guard connectionState == "connected", (23...517).contains(mtu),
36
+ !id.isEmpty, !serialNumber.isEmpty else { throw UploadFailure.invalidInput }
37
+ return ConnectedDevice(id: id, serialNumber: serialNumber, deviceType: type, firmwareVersion: firmwareVersion,
38
+ hardwareRevision: hardwareRevision, isProvisioned: isProvisioned, connectionState: .connected, mtu: mtu)
39
+ }
40
+ }
41
+ }
42
+ private func check(_ id: String) throws {
43
+ try Task.checkCancellation()
44
+ guard !stopped, !cancelled.contains(id), tasks[id] != nil else { throw UploadFailure.cancelled }
45
+ }
46
+ func sync(_ json: String) async throws -> String {
47
+ guard json.utf8.count <= 8192 else { throw UploadFailure.invalidInput }
48
+ let input = try JSONDecoder().decode(Input.self, from: Data(json.utf8))
49
+ let id = input.operationId
50
+ guard id.hasPrefix("byo-"), id.utf8.count <= 64, !stopped, !cancelled.contains(id), tasks[id] == nil,
51
+ let url = URL(string: input.scope.baseUrl) else { throw UploadFailure.invalidInput }
52
+ let device = try input.device.native()
53
+ let task = Task {
54
+ try self.check(id)
55
+ let options = ByoStorageOptions(baseURL: url, scopeKey: input.scope.scopeKey,
56
+ organizationID: input.scope.organizationId, projectID: input.scope.projectId, deviceID: input.scope.deviceId,
57
+ environment: input.scope.environment, bindingGeneration: input.scope.bindingGeneration,
58
+ stateDirectory: FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
59
+ .appendingPathComponent("BotaSDKByo", isDirectory: true),
60
+ accessToken: { try await self.credentials.request(operationId: id) },
61
+ validateScope: { try await self.check(id) })
62
+ let bytes: Data
63
+ if input.mode == "list" {
64
+ let recordings = try await BotaDeviceClient.shared.recordings.listByoRecordings(device, options: options)
65
+ try self.check(id)
66
+ bytes = try JSONSerialization.data(withJSONObject: recordings.map { ["uuid": $0.uuid, "generation": $0.generation] as [String: Any] })
67
+ } else {
68
+ guard input.mode == "stream" || (input.mode == "sync" && input.recording != nil) else { throw UploadFailure.invalidInput }
69
+ let selected = input.recording.map { ByoRecording(uuid: $0.uuid, generation: $0.generation) }
70
+ let result: ByoUploadResult
71
+ if input.mode == "stream" { result = try await BotaDeviceClient.shared.recordings.streamByoRecording(device, recording: selected, options: options) }
72
+ else { result = try await BotaDeviceClient.shared.recordings.syncByoRecording(device, recording: selected!, options: options) }
73
+ try self.check(id)
74
+ bytes = try JSONSerialization.data(withJSONObject: ["recordingId": result.recordingID, "uploadSessionId": result.uploadSessionID])
75
+ }
76
+ return String(decoding: bytes, as: UTF8.self)
77
+ }
78
+ tasks[id] = task
79
+ defer { tasks[id] = nil; cancelled.remove(id); credentials.cancel(operationId: id) }
80
+ return try await task.value
81
+ }
82
+ func cancel(_ id: String) async {
83
+ guard id.hasPrefix("byo-"), id.utf8.count <= 64 else { return }
84
+ if cancelled.count >= 128 { stopped = true }
85
+ else { cancelled.insert(id) }
86
+ let task = tasks[id]; task?.cancel(); credentials.cancel(operationId: id); _ = await task?.result
87
+ }
88
+ func stop() async {
89
+ stopped = true
90
+ let current = tasks
91
+ for (id, task) in current { task.cancel(); credentials.cancel(operationId: id) }
92
+ for task in current.values { _ = await task.result }
93
+ }
94
+ }
@@ -101,6 +101,7 @@ struct UploadInput: Codable, Sendable {
101
101
  var durationMs: String
102
102
  var plaintextLength: String
103
103
  var storageFormat: UInt8
104
+ var markersRequired: Bool? = nil
104
105
  }
105
106
  struct Capability: Codable, Sendable { var rawValueHex: String; var flags: UInt32 }
106
107
  struct Checkpoint: Codable, Sendable {
@@ -187,7 +188,9 @@ struct UploadInput: Codable, Sendable {
187
188
  encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
188
189
  var identityScope = scope
189
190
  if !includeTransport { identityScope.nativeDeviceId = "" }
190
- return UploadValidation.hash(try encoder.encode(Identity(scope: identityScope, recording: recording)))
191
+ var identityRecording = recording
192
+ if identityRecording.markersRequired == false { identityRecording.markersRequired = nil }
193
+ return UploadValidation.hash(try encoder.encode(Identity(scope: identityScope, recording: identityRecording)))
191
194
  }
192
195
 
193
196
  func validateCheckpoint(_ entry: UploadJournalEntry?) throws {
@@ -242,6 +245,8 @@ struct UploadSession: Decodable, Sendable {
242
245
  let plaintext_length: UInt64?
243
246
  let completion_receipt_base64: String?
244
247
  let completion_receipt_sha256: String?
248
+ let marker_completion_receipt_base64: String?
249
+ let marker_completion_receipt_sha256: String?
245
250
 
246
251
  func validatePointer() throws {
247
252
  guard profile == "encrypted_upload_v2", UploadValidation.uuid(session_id), owner_revision > 0,
@@ -178,7 +178,7 @@ struct UploadContext: Sendable {
178
178
  } else {
179
179
  let nonce = try await nonce()
180
180
  let body: [String: Any] = [
181
- "device_id": input.scope.deviceId, "end_user_id": input.scope.endUserId, "binding_generation": input.scope.bindingGeneration,
181
+ "device_id": input.scope.deviceId, "binding_generation": input.scope.bindingGeneration,
182
182
  "recording_uuid": input.recording.uuid, "recording_generation": input.recording.generation,
183
183
  "storage_format": "bota_enc_v2", "channel": "ble",
184
184
  "capabilities_base64": try UploadValidation.bytes(input.capability.rawValueHex).base64EncodedString(),
@@ -201,7 +201,28 @@ struct UploadContext: Sendable {
201
201
  try selected.validatePointer()
202
202
  _ = try UploadValidation.date(selected.expires_at)
203
203
  guard UploadValidation.hex(selected.authorization_sha256, bytes: 32) else { throw UploadFailure.invalidDocument }
204
- let authorization = try UploadValidation.document(selected.authorization_base64, hash: selected.authorization_sha256, size: 408)
204
+ var authorization = try UploadValidation.document(selected.authorization_base64, hash: selected.authorization_sha256, size: 408)
205
+ if input.recording.markersRequired == true {
206
+ struct Admission: Decodable {
207
+ let profile: String
208
+ let context_base64: String
209
+ let authorization_base64: String
210
+ let authorization_sha256: String
211
+ }
212
+ let path = try sessionPath() + "/markers/authorization"
213
+ let admitted: Data
214
+ do {
215
+ admitted = try await retry { try await request(path + "?owner_revision=\(selected.owner_revision)") }
216
+ } catch UploadFailure.http(404) where ["", "created", "staging", "staged"].contains(selected.state ?? "") {
217
+ admitted = try await retry { try await request(path, method: "POST", body: [
218
+ "owner_revision": selected.owner_revision, "request_uuid": selected.session_id,
219
+ ]) }
220
+ }
221
+ let admission: Admission = try decode(admitted)
222
+ guard admission.profile == "recording-markers/1" else { throw UploadFailure.invalidDocument }
223
+ authorization.append(try UploadValidation.document(admission.context_base64, size: 176))
224
+ authorization.append(try UploadValidation.document(admission.authorization_base64, hash: admission.authorization_sha256, size: 280))
225
+ }
205
226
  session = selected
206
227
  return UploadPrepared(session: selected, authorization: authorization, recordingId: initial.recordingId)
207
228
  }
@@ -313,6 +334,18 @@ struct UploadContext: Sendable {
313
334
  needsManifest = false
314
335
  }
315
336
 
337
+ func submitMarkers(_ documents: [Data], evidence: UploadEvidence) async throws {
338
+ try verify(evidence)
339
+ guard input.recording.markersRequired == true, (2...4097).contains(documents.count),
340
+ documents.first?.count == 200, documents.dropFirst().allSatisfy({ (216...402).contains($0.count) }),
341
+ let revision = entry?.ownerRevision else { throw UploadFailure.invalidDocument }
342
+ let path = try sessionPath() + "/markers/batch"
343
+ _ = try await retry { try await request(path, method: "POST", body: [
344
+ "owner_revision": revision, "seal_base64": documents[0].base64EncodedString(),
345
+ "pages_base64": documents.dropFirst().map { $0.base64EncodedString() },
346
+ ]) }
347
+ }
348
+
316
349
  func finalize(_ evidence: UploadEvidence) async throws {
317
350
  try verify(evidence)
318
351
  guard let entry else { throw UploadFailure.state }
@@ -325,7 +358,12 @@ struct UploadContext: Sendable {
325
358
  let hash = status.completion_receipt_sha256, UploadValidation.hex(hash, bytes: 32) else {
326
359
  throw UploadFailure.invalidDocument
327
360
  }
328
- receipt = try UploadValidation.document(status.completion_receipt_base64, hash: hash, size: 336)
361
+ var completed = try UploadValidation.document(status.completion_receipt_base64, hash: hash, size: 336)
362
+ if input.recording.markersRequired == true {
363
+ guard let markerHash = status.marker_completion_receipt_sha256 else { throw UploadFailure.invalidDocument }
364
+ completed.append(try UploadValidation.document(status.marker_completion_receipt_base64, hash: markerHash, size: 296))
365
+ }
366
+ receipt = completed
329
367
  return
330
368
  }
331
369
  try await wait(2000)
@@ -0,0 +1,43 @@
1
+ import Foundation
2
+ import CoreFoundation
3
+
4
+ /** Scalar-only read boundary; all HTTP runs through the no-redirect native client. */
5
+ @MainActor final class UploadStreamingStatus {
6
+ private let http: any UploadHTTP
7
+ init(http: any UploadHTTP = UploadURLSessionHTTP()) { self.http = http }
8
+ func read(_ input: String) async throws -> String {
9
+ guard let object = try JSONSerialization.jsonObject(with: Data(input.utf8)) as? [String: Any],
10
+ let address = object["url"] as? String, let token = object["token"] as? String,
11
+ let org = object["organizationId"] as? String,
12
+ UploadValidation.matches(address, "https://[A-Za-z0-9.-]+(?::[0-9]+)?(?:/[A-Za-z0-9_-]+)+/recordings/rec_[A-Za-z0-9_-]+/streaming-status\\?session_id=[0-9a-f-]{36}"),
13
+ let url = URL(string: address), (1...16384).contains(token.utf8.count),
14
+ token.utf8.allSatisfy({ (33...126).contains($0) }),
15
+ org.isEmpty || UploadValidation.matches(org, "[A-Za-z0-9_-]{1,128}") else { throw UploadFailure.invalidInput }
16
+ try Task.checkCancellation()
17
+ var request = URLRequest(url: url); request.httpMethod = "GET"
18
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
19
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
20
+ if !org.isEmpty { request.setValue(org, forHTTPHeaderField: "X-Organization-Id") }
21
+ let response = try await http.send(request)
22
+ try Task.checkCancellation()
23
+ guard response.status == 200 else { throw UploadFailure.http(response.status) }
24
+ guard response.body.count <= 16384 else { throw UploadFailure.invalidDocument }
25
+ return try Self.scalarStatus(response.body)
26
+ }
27
+ static func scalarStatus(_ body: Data) throws -> String {
28
+ let strings: Set<String> = ["profile", "recording_id", "session_id", "writer_epoch", "revision", "state"]
29
+ let numbers: Set<String> = ["recording_generation", "received_count", "contiguous_sequence"]
30
+ guard let object = try JSONSerialization.jsonObject(with: body) as? [String: Any],
31
+ Set(object.keys) == strings.union(numbers).union(["expected_count", "authorization_expired"]),
32
+ strings.allSatisfy({ (object[$0] as? String).map { $0.utf8.count <= 160 } == true }),
33
+ numbers.allSatisfy({ isNumber(object[$0]) }),
34
+ object["expected_count"] is NSNull || isNumber(object["expected_count"]),
35
+ let expired = object["authorization_expired"] as? NSNumber,
36
+ CFGetTypeID(expired) == CFBooleanGetTypeID() else { throw UploadFailure.invalidDocument }
37
+ return String(decoding: try JSONSerialization.data(withJSONObject: object), as: UTF8.self)
38
+ }
39
+ private static func isNumber(_ value: Any?) -> Bool {
40
+ guard let number = value as? NSNumber else { return false }
41
+ return CFGetTypeID(number) != CFBooleanGetTypeID()
42
+ }
43
+ }