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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +36 -7
  2. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +3 -0
  3. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +1 -0
  4. package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +37 -3
  5. package/android/src/main/java/dev/bota/sdk/reactnative/upload/ByoBridgeDriver.kt +79 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +30 -2
  7. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +5 -2
  8. package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadStreamingStatus.kt +67 -0
  9. package/generated/codegen-contract.json +110 -8
  10. package/ios/BotaDeviceSDK.mm +1 -0
  11. package/ios/BotaDeviceSDKAppleBridge.swift +4 -2
  12. package/ios/BotaDeviceSDKAppleRecordings.swift +1 -0
  13. package/ios/BotaUploadV2Backend.mm +21 -0
  14. package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +52 -2
  15. package/ios/UploadBackend/ByoBridgeDriver.swift +94 -0
  16. package/ios/UploadBackend/UploadModels.swift +6 -1
  17. package/ios/UploadBackend/UploadOperation.swift +41 -3
  18. package/ios/UploadBackend/UploadStreamingStatus.swift +43 -0
  19. package/lib/commonjs/byoStorageBackend.js +144 -0
  20. package/lib/commonjs/byoStorageBackend.js.map +1 -0
  21. package/lib/commonjs/client.js +2 -1
  22. package/lib/commonjs/client.js.map +1 -1
  23. package/lib/commonjs/encryptedUploadV2Backend.js +4 -1
  24. package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -1
  25. package/lib/commonjs/index.js +7 -1
  26. package/lib/commonjs/index.js.map +1 -1
  27. package/lib/commonjs/managers/DeviceManager.js +6 -0
  28. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  29. package/lib/commonjs/protectedStreamingStatus.js +120 -0
  30. package/lib/commonjs/protectedStreamingStatus.js.map +1 -0
  31. package/lib/commonjs/sdkIdentity.js +1 -1
  32. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
  33. package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -1
  34. package/lib/module/byoStorageBackend.js +140 -0
  35. package/lib/module/byoStorageBackend.js.map +1 -0
  36. package/lib/module/client.js +2 -1
  37. package/lib/module/client.js.map +1 -1
  38. package/lib/module/encryptedUploadV2Backend.js +4 -1
  39. package/lib/module/encryptedUploadV2Backend.js.map +1 -1
  40. package/lib/module/index.js +4 -0
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/managers/DeviceManager.js +6 -0
  43. package/lib/module/managers/DeviceManager.js.map +1 -1
  44. package/lib/module/protectedStreamingStatus.js +116 -0
  45. package/lib/module/protectedStreamingStatus.js.map +1 -0
  46. package/lib/module/sdkIdentity.js +1 -1
  47. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
  48. package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -1
  49. package/lib/typescript/byoStorageBackend.d.ts +36 -0
  50. package/lib/typescript/byoStorageBackend.d.ts.map +1 -0
  51. package/lib/typescript/client.d.ts.map +1 -1
  52. package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -1
  53. package/lib/typescript/index.d.ts +6 -0
  54. package/lib/typescript/index.d.ts.map +1 -1
  55. package/lib/typescript/managers/DeviceManager.d.ts +2 -0
  56. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
  57. package/lib/typescript/protectedStreamingStatus.d.ts +37 -0
  58. package/lib/typescript/protectedStreamingStatus.d.ts.map +1 -0
  59. package/lib/typescript/sdkIdentity.d.ts +1 -1
  60. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +1 -0
  61. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
  62. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +4 -0
  63. package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/byoStorageBackend.ts +104 -0
  66. package/src/client.ts +2 -0
  67. package/src/encryptedUploadV2Backend.ts +2 -1
  68. package/src/index.ts +11 -0
  69. package/src/managers/DeviceManager.ts +6 -0
  70. package/src/protectedStreamingStatus.ts +137 -0
  71. package/src/sdkIdentity.ts +1 -1
  72. package/src/specs/NativeBotaDeviceSDK.ts +1 -0
  73. package/src/specs/NativeBotaUploadV2Backend.ts +4 -0
@@ -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
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createManagedByoStorageBackend = createManagedByoStorageBackend;
7
+ /** @internal Shared implementation for the production factory and host tests. */
8
+ function createManagedByoStorageBackend(native, options) {
9
+ const config = {
10
+ ...options
11
+ };
12
+ const url = new URL(config.baseUrl);
13
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !config.scopeKey || !config.organizationId || !config.projectId || !config.signal || !['development', 'gamma', 'production'].includes(config.environment)) throw new Error('Invalid BYO configuration');
14
+ let disposed = false;
15
+ const attempts = new Set();
16
+ const perform = async (device, recording, target, live = false) => {
17
+ if (!native?.syncByo || !native.cancelByo) throw new Error('BYO requires the matching native SDK binary');
18
+ device = {
19
+ ...device
20
+ };
21
+ recording = recording && {
22
+ ...recording
23
+ };
24
+ target = {
25
+ ...target
26
+ };
27
+ if (recording !== null && (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(recording.uuid) || !Number.isSafeInteger(recording.generation) || recording.generation < 0 || recording.generation > 0xffffffff) || !Number.isSafeInteger(target.bindingGeneration) || target.bindingGeneration < 1 || target.bindingGeneration > 0xffffffff) {
28
+ throw new Error('Invalid BYO recording identity');
29
+ }
30
+ const operationId = 'byo-' + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, key => {
31
+ const value = Math.floor(Math.random() * 16);
32
+ return (key === 'x' ? value : value & 3 | 8).toString(16);
33
+ });
34
+ const controller = new AbortController();
35
+ let cancelTask;
36
+ let settle;
37
+ const settled = new Promise(resolve => {
38
+ settle = resolve;
39
+ });
40
+ const check = () => {
41
+ if (disposed || controller.signal.aborted || config.signal.aborted || target.signal?.aborted) throw new Error('BYO upload cancelled');
42
+ };
43
+ const cancel = () => {
44
+ controller.abort();
45
+ return cancelTask ??= Promise.resolve().then(() => native.cancelByo(operationId));
46
+ };
47
+ const abort = () => {
48
+ void cancel().catch(() => {});
49
+ };
50
+ const attempt = {
51
+ cancel,
52
+ settled
53
+ };
54
+ attempts.add(attempt);
55
+ let subscription;
56
+ let completed = false;
57
+ try {
58
+ config.signal.addEventListener('abort', abort, {
59
+ once: true
60
+ });
61
+ target.signal?.addEventListener('abort', abort, {
62
+ once: true
63
+ });
64
+ check();
65
+ subscription = native.onCredentialsRequested(event => {
66
+ if (event.operationId !== operationId) return;
67
+ void (async () => {
68
+ try {
69
+ check();
70
+ const token = await config.getAccessToken(controller.signal);
71
+ check();
72
+ await native.resolveCredentials(event.requestId, token);
73
+ } catch {
74
+ await native.rejectCredentials(event.requestId).catch(() => {});
75
+ }
76
+ })();
77
+ });
78
+ const value = JSON.parse(await native.syncByo(JSON.stringify({
79
+ operationId,
80
+ mode: live ? 'stream' : recording === null ? 'list' : 'sync',
81
+ device,
82
+ recording,
83
+ scope: {
84
+ baseUrl: url.href,
85
+ scopeKey: config.scopeKey,
86
+ organizationId: config.organizationId,
87
+ projectId: config.projectId,
88
+ environment: config.environment,
89
+ deviceId: target.deviceId,
90
+ bindingGeneration: target.bindingGeneration
91
+ }
92
+ })));
93
+ check();
94
+ if (recording === null && !live) {
95
+ if (!Array.isArray(value) || value.length > 4096 || value.some(item => !item || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(item.uuid) || !Number.isSafeInteger(item.generation) || item.generation < 0 || item.generation > 0xffffffff)) throw new Error('Invalid BYO native catalog');
96
+ completed = true;
97
+ return value.map(item => ({
98
+ uuid: item.uuid,
99
+ generation: item.generation
100
+ }));
101
+ }
102
+ const result = value;
103
+ if (!result || !/^rec_[A-Za-z0-9_-]+$/.test(result.recordingId) || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(result.uploadSessionId)) throw new Error('Invalid BYO native result');
104
+ completed = true;
105
+ return {
106
+ recordingId: result.recordingId,
107
+ uploadSessionId: result.uploadSessionId
108
+ };
109
+ } finally {
110
+ try {
111
+ if (!completed) await cancel();
112
+ } finally {
113
+ attempts.delete(attempt);
114
+ config.signal.removeEventListener('abort', abort);
115
+ target.signal?.removeEventListener('abort', abort);
116
+ subscription?.remove();
117
+ settle();
118
+ }
119
+ }
120
+ };
121
+ return {
122
+ async sync(device, recording, target) {
123
+ return await perform(device, recording, target);
124
+ },
125
+ async stream(device, recording, target) {
126
+ return await perform(device, recording, target, true);
127
+ },
128
+ async list(device, target) {
129
+ return await perform(device, null, target);
130
+ },
131
+ async dispose() {
132
+ disposed = true;
133
+ const results = await Promise.allSettled([...attempts].map(async attempt => {
134
+ try {
135
+ await attempt.cancel();
136
+ } finally {
137
+ await attempt.settled;
138
+ }
139
+ }));
140
+ if (results.some(result => result.status === 'rejected')) throw new Error('BYO cancellation failed');
141
+ }
142
+ };
143
+ }
144
+ //# sourceMappingURL=byoStorageBackend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createManagedByoStorageBackend","native","options","config","url","URL","baseUrl","protocol","username","password","search","hash","scopeKey","organizationId","projectId","signal","includes","environment","Error","disposed","attempts","Set","perform","device","recording","target","live","syncByo","cancelByo","test","uuid","Number","isSafeInteger","generation","bindingGeneration","operationId","replace","key","value","Math","floor","random","toString","controller","AbortController","cancelTask","settle","settled","Promise","resolve","check","aborted","cancel","abort","then","catch","attempt","add","subscription","completed","addEventListener","once","onCredentialsRequested","event","token","getAccessToken","resolveCredentials","requestId","rejectCredentials","JSON","parse","stringify","mode","scope","href","deviceId","Array","isArray","length","some","item","map","result","recordingId","uploadSessionId","delete","removeEventListener","remove","sync","stream","list","dispose","results","allSettled","status"],"sourceRoot":"../../src","sources":["byoStorageBackend.ts"],"mappings":"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","ignoreList":[]}
@@ -86,7 +86,7 @@ const mapPendingRecording = value => {
86
86
  }
87
87
  const recording = value.encrypted;
88
88
  const plaintextLength = recording.plaintextLength;
89
- if (typeof recording.uuid !== 'string' || !uuidPattern.test(recording.uuid) || recording.storageFormat !== 3 || typeof plaintextLength !== 'string' || !/^(0|[1-9][0-9]{0,19})$/.test(plaintextLength) || BigInt(plaintextLength) > 0xffffffffffffffffn || typeof recording.ciphertextLength !== 'string' || !/^[1-9][0-9]{0,19}$/.test(recording.ciphertextLength) || BigInt(recording.ciphertextLength) > 0xffffffffffffffffn || typeof recording.ciphertextSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(recording.ciphertextSha256) || !Number.isInteger(recording.generation) || recording.generation < 0 || recording.generation > 0xffffffff) {
89
+ if (typeof recording.uuid !== 'string' || !uuidPattern.test(recording.uuid) || recording.storageFormat !== 3 || typeof plaintextLength !== 'string' || recording.markersRequired !== undefined && typeof recording.markersRequired !== 'boolean' || !/^(0|[1-9][0-9]{0,19})$/.test(plaintextLength) || BigInt(plaintextLength) > 0xffffffffffffffffn || typeof recording.ciphertextLength !== 'string' || !/^[1-9][0-9]{0,19}$/.test(recording.ciphertextLength) || BigInt(recording.ciphertextLength) > 0xffffffffffffffffn || typeof recording.ciphertextSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(recording.ciphertextSha256) || !Number.isInteger(recording.generation) || recording.generation < 0 || recording.generation > 0xffffffff) {
90
90
  throw new Error('invalid native recording catalog identity');
91
91
  }
92
92
  return {
@@ -97,6 +97,7 @@ const mapPendingRecording = value => {
97
97
  storageFormat: 3,
98
98
  plaintextLength,
99
99
  startedAtMs: recording.startedAtMs,
100
+ markersRequired: recording.markersRequired ?? false,
100
101
  startedAt: new Date(catalogInteger(recording.startedAtMs, 8640000000000000n)),
101
102
  durationMs: catalogInteger(recording.durationMs)
102
103
  };