@bota.dev/web-app-sdk 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +517 -0
  3. package/dist/capabilities.d.ts +8 -0
  4. package/dist/capabilities.js +21 -0
  5. package/dist/client.d.ts +39 -0
  6. package/dist/client.js +153 -0
  7. package/dist/controlManager.d.ts +38 -0
  8. package/dist/controlManager.js +344 -0
  9. package/dist/core.d.ts +698 -0
  10. package/dist/core.js +13 -0
  11. package/dist/deviceManager.d.ts +52 -0
  12. package/dist/deviceManager.js +491 -0
  13. package/dist/encryptedUploadV2Host.d.ts +239 -0
  14. package/dist/encryptedUploadV2Host.js +2136 -0
  15. package/dist/errors.d.ts +24 -0
  16. package/dist/errors.js +230 -0
  17. package/dist/gatt.d.ts +39 -0
  18. package/dist/gatt.js +57 -0
  19. package/dist/generated/bota_device_sdk_core.d.ts +202 -0
  20. package/dist/generated/bota_device_sdk_core.js +1025 -0
  21. package/dist/generated/bota_device_sdk_core_bg.wasm +0 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.js +2 -0
  24. package/dist/indexedDbWorkflowStore.d.ts +52 -0
  25. package/dist/indexedDbWorkflowStore.js +763 -0
  26. package/dist/logManager.d.ts +24 -0
  27. package/dist/logManager.js +205 -0
  28. package/dist/models.d.ts +217 -0
  29. package/dist/models.js +1 -0
  30. package/dist/opfsBlobStore.d.ts +12 -0
  31. package/dist/opfsBlobStore.js +250 -0
  32. package/dist/otaManager.d.ts +49 -0
  33. package/dist/otaManager.js +951 -0
  34. package/dist/providerCancellation.d.ts +2 -0
  35. package/dist/providerCancellation.js +23 -0
  36. package/dist/providers.d.ts +138 -0
  37. package/dist/providers.js +1 -0
  38. package/dist/provisioningManager.d.ts +48 -0
  39. package/dist/provisioningManager.js +753 -0
  40. package/dist/recordingManager.d.ts +53 -0
  41. package/dist/recordingManager.js +1397 -0
  42. package/dist/storage.d.ts +103 -0
  43. package/dist/storage.js +60 -0
  44. package/dist/transport.d.ts +33 -0
  45. package/dist/transport.js +8 -0
  46. package/dist/wasmCore.d.ts +3 -0
  47. package/dist/wasmCore.js +1651 -0
  48. package/dist/webBluetoothTransport.d.ts +23 -0
  49. package/dist/webBluetoothTransport.js +369 -0
  50. package/dist/wifiManager.d.ts +54 -0
  51. package/dist/wifiManager.js +528 -0
  52. package/dist/workflowRuntime.d.ts +115 -0
  53. package/dist/workflowRuntime.js +1508 -0
  54. package/package.json +46 -0
@@ -0,0 +1,2136 @@
1
+ import { BotaSDKError } from "./errors.js";
2
+ import { BOTA_STORAGE_SERVICE, RECORDING_LIST_V2_CHARACTERISTIC, RECORDING_TRANSFER_V2_CHARACTERISTIC, TRANSFER_CONTROL_V2_CHARACTERISTIC, TRANSFER_SIGNED_BLOB_V2_CHARACTERISTIC, } from "./gatt.js";
3
+ import { awaitProviderCall } from "./providerCancellation.js";
4
+ import { BrowserStorageError, } from "./storage.js";
5
+ import { BrowserTransportError, } from "./transport.js";
6
+ const MAXIMUM_FRAME_BYTES = 512;
7
+ const MAXIMUM_QUEUED_BYTES = 1024 * 1024;
8
+ const MANIFEST_LENGTH = 580;
9
+ const EMPTY_SHA256_HEX = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
10
+ const SIGNED_RESULT_TIMEOUT_MS = 10_000;
11
+ const BLE_CLEANUP_TIMEOUT_MS = 1_000;
12
+ let signedBlobWriteIdValue = null;
13
+ function nextSignedBlobWriteId() {
14
+ if (signedBlobWriteIdValue === null) {
15
+ const seed = crypto.getRandomValues(new Uint32Array(1))[0] ?? 1;
16
+ signedBlobWriteIdValue = seed === 0 ? 1 : seed;
17
+ }
18
+ const value = signedBlobWriteIdValue;
19
+ signedBlobWriteIdValue = value === 0xffff_ffff ? 1 : value + 1;
20
+ return value;
21
+ }
22
+ export class EncryptedUploadV2SignedBlobWriter {
23
+ core;
24
+ transport;
25
+ device;
26
+ onOwnershipUncertain;
27
+ resultTimeoutMs;
28
+ cleanupTimeoutMs;
29
+ writes = new SerializedGattWrites();
30
+ active = false;
31
+ operation = null;
32
+ cancelled = false;
33
+ constructor(core, transport, device, onOwnershipUncertain = () => undefined, timing = {}) {
34
+ this.core = core;
35
+ this.transport = transport;
36
+ this.device = device;
37
+ this.onOwnershipUncertain = onOwnershipUncertain;
38
+ this.resultTimeoutMs = positiveTimeout(timing.resultTimeoutMs, SIGNED_RESULT_TIMEOUT_MS);
39
+ this.cleanupTimeoutMs = positiveTimeout(timing.cleanupTimeoutMs, BLE_CLEANUP_TIMEOUT_MS);
40
+ }
41
+ send(blobKind, writeId, value, maximumBlobBytes, signal, terminalCallback = () => undefined) {
42
+ if (this.active)
43
+ return Promise.reject(operationInProgress());
44
+ if (this.cancelled
45
+ || signal?.aborted
46
+ || !isUint32(writeId)
47
+ || writeId === 0
48
+ || value.byteLength === 0
49
+ || value.byteLength > maximumBlobBytes
50
+ || value.byteLength > 0xffff) {
51
+ terminalCallback();
52
+ return Promise.reject(this.cancelled || signal?.aborted
53
+ ? cancelled()
54
+ : protocolFailure());
55
+ }
56
+ this.active = true;
57
+ const operation = {
58
+ abort: new AbortController(),
59
+ terminal: deferred(),
60
+ terminalCallback,
61
+ terminalSettled: false,
62
+ poisoned: false,
63
+ };
64
+ this.operation = operation;
65
+ return this.runSend(operation, blobKind, writeId, value, signal);
66
+ }
67
+ async runSend(operation, blobKind, writeId, value, signal) {
68
+ const activeSignal = signal
69
+ ? AbortSignal.any([signal, operation.abort.signal])
70
+ : operation.abort.signal;
71
+ let subscription = null;
72
+ let began = false;
73
+ let terminalResultReceived = false;
74
+ let resultWindowOpen = false;
75
+ let primary = null;
76
+ const result = deferred();
77
+ let unmatchedResults = 0;
78
+ let unmatchedBytes = 0;
79
+ try {
80
+ subscription = await this.transport.subscribe(this.device, BOTA_STORAGE_SERVICE, TRANSFER_SIGNED_BLOB_V2_CHARACTERISTIC, ({ characteristicUuid, value: notification }) => {
81
+ if (characteristicUuid.toLowerCase()
82
+ !== TRANSFER_SIGNED_BLOB_V2_CHARACTERISTIC)
83
+ return;
84
+ if (!resultWindowOpen)
85
+ return;
86
+ try {
87
+ const decoded = this.core.decodeEncryptedUploadV2SignedBlobResult(notification);
88
+ if (decoded.blobKind === blobKind && decoded.writeId === writeId) {
89
+ terminalResultReceived = true;
90
+ if (decoded.result === 0)
91
+ result.resolve(undefined);
92
+ else
93
+ result.reject(new BotaSDKError('protocol_error', 'transfer_recording', { protocolStatus: decoded.result }));
94
+ return;
95
+ }
96
+ unmatchedResults += 1;
97
+ unmatchedBytes += notification.byteLength;
98
+ if (unmatchedResults > 64 || unmatchedBytes > 32 * 1024) {
99
+ result.reject(protocolFailure());
100
+ }
101
+ }
102
+ catch (error) {
103
+ result.reject(protocolFailure(error));
104
+ }
105
+ });
106
+ throwIfCancelled(this.cancelled, activeSignal);
107
+ const frameLimit = Math.min(this.transport.maximumWriteValueLength, MAXIMUM_FRAME_BYTES);
108
+ if (frameLimit < 64)
109
+ throw protocolFailure();
110
+ const digest = sha256(this.core, value);
111
+ began = true;
112
+ await this.write(this.core.encodeEncryptedUploadV2SignedBlob({
113
+ kind: 'begin',
114
+ blobKind,
115
+ writeId,
116
+ totalLength: value.byteLength,
117
+ sha256: digest,
118
+ }), frameLimit);
119
+ for (let offset = 0; offset < value.byteLength;) {
120
+ throwIfCancelled(this.cancelled, activeSignal);
121
+ const length = this.largestChunk(blobKind, writeId, offset, value, frameLimit);
122
+ await this.write(this.core.encodeEncryptedUploadV2SignedBlob({
123
+ kind: 'data',
124
+ blobKind,
125
+ writeId,
126
+ offset,
127
+ data: value.slice(offset, offset + length),
128
+ }), frameLimit);
129
+ offset += length;
130
+ }
131
+ await this.write(this.core.encodeEncryptedUploadV2SignedBlob({
132
+ kind: 'commit',
133
+ blobKind,
134
+ writeId,
135
+ }), frameLimit, () => {
136
+ resultWindowOpen = true;
137
+ });
138
+ await rejectAfter(abortable(result.promise, activeSignal), this.resultTimeoutMs, signedResultTimeout);
139
+ }
140
+ catch (error) {
141
+ primary = error;
142
+ }
143
+ const cleanup = (async () => {
144
+ let cleanupError = null;
145
+ if (primary !== null && began && !terminalResultReceived) {
146
+ try {
147
+ const frameLimit = Math.min(this.transport.maximumWriteValueLength, MAXIMUM_FRAME_BYTES);
148
+ await this.write(this.core.encodeEncryptedUploadV2SignedBlob({
149
+ kind: 'abort',
150
+ blobKind,
151
+ writeId,
152
+ }), frameLimit);
153
+ }
154
+ catch (error) {
155
+ cleanupError = error;
156
+ }
157
+ }
158
+ try {
159
+ await subscription?.remove();
160
+ }
161
+ catch (error) {
162
+ cleanupError ??= error;
163
+ }
164
+ if (cleanupError !== null)
165
+ throw cleanupError;
166
+ })();
167
+ const cleanupSettlement = settle(cleanup);
168
+ const bounded = await boundedSettlement(cleanupSettlement, this.cleanupTimeoutMs);
169
+ if (bounded === null) {
170
+ this.poison(operation);
171
+ void cleanupSettlement.then((settlement) => {
172
+ this.finishOperation(operation);
173
+ if (settlement.error !== null)
174
+ this.poison(operation);
175
+ });
176
+ throw ownershipUnknown();
177
+ }
178
+ this.finishOperation(operation);
179
+ if (bounded.error !== null) {
180
+ this.poison(operation);
181
+ throw ownershipUnknown(bounded.error);
182
+ }
183
+ if (primary !== null)
184
+ throw normalizeHostError(primary);
185
+ }
186
+ async cancel() {
187
+ this.cancelled = true;
188
+ const operation = this.operation;
189
+ if (!operation)
190
+ return;
191
+ operation.abort.abort();
192
+ const terminal = settle(operation.terminal.promise);
193
+ const bounded = await boundedSettlement(terminal, this.cleanupTimeoutMs);
194
+ if (bounded === null) {
195
+ this.poison(operation);
196
+ throw ownershipUnknown();
197
+ }
198
+ if (bounded.error !== null)
199
+ throw ownershipUnknown(bounded.error);
200
+ }
201
+ finishOperation(operation) {
202
+ if (operation.terminalSettled)
203
+ return;
204
+ operation.terminalSettled = true;
205
+ try {
206
+ operation.terminalCallback();
207
+ }
208
+ finally {
209
+ operation.terminal.resolve(undefined);
210
+ if (this.operation === operation)
211
+ this.operation = null;
212
+ this.active = false;
213
+ }
214
+ }
215
+ poison(operation) {
216
+ if (operation.poisoned)
217
+ return;
218
+ operation.poisoned = true;
219
+ this.onOwnershipUncertain();
220
+ }
221
+ largestChunk(blobKind, writeId, offset, value, frameLimit) {
222
+ let low = 1;
223
+ let high = Math.min(value.byteLength - offset, 0xffff - offset);
224
+ let best = 0;
225
+ while (low <= high) {
226
+ const middle = Math.floor((low + high) / 2);
227
+ const frame = this.core.encodeEncryptedUploadV2SignedBlob({
228
+ kind: 'data',
229
+ blobKind,
230
+ writeId,
231
+ offset,
232
+ data: value.slice(offset, offset + middle),
233
+ });
234
+ if (frame.byteLength <= frameLimit) {
235
+ best = middle;
236
+ low = middle + 1;
237
+ }
238
+ else {
239
+ high = middle - 1;
240
+ }
241
+ }
242
+ if (best === 0)
243
+ throw protocolFailure();
244
+ return best;
245
+ }
246
+ async write(frame, frameLimit, onInitiated) {
247
+ if (frame.byteLength > frameLimit)
248
+ throw protocolFailure();
249
+ await this.writes.run(async () => {
250
+ onInitiated?.();
251
+ await this.transport.write(this.device, BOTA_STORAGE_SERVICE, TRANSFER_SIGNED_BLOB_V2_CHARACTERISTIC, frame, true);
252
+ });
253
+ }
254
+ }
255
+ export class EncryptedUploadV2TransferControl {
256
+ core;
257
+ transport;
258
+ device;
259
+ onOwnershipUncertain;
260
+ cleanupTimeoutMs;
261
+ writes = new SerializedGattWrites();
262
+ active = false;
263
+ session = null;
264
+ constructor(core, transport, device, onOwnershipUncertain = () => undefined, timing = {}) {
265
+ this.core = core;
266
+ this.transport = transport;
267
+ this.device = device;
268
+ this.onOwnershipUncertain = onOwnershipUncertain;
269
+ this.cleanupTimeoutMs = positiveTimeout(timing.cleanupTimeoutMs, BLE_CLEANUP_TIMEOUT_MS);
270
+ }
271
+ async open(request, checkpoint, signal) {
272
+ if (this.session)
273
+ throw operationInProgress();
274
+ validateTransferRequest(request);
275
+ throwIfCancelled(false, signal);
276
+ const queue = new BoundedFrameQueue();
277
+ const control = deferred();
278
+ const session = {
279
+ request: copyTransferRequest(request),
280
+ subscription: null,
281
+ queue,
282
+ phase: 'awaiting_control',
283
+ pendingNotifications: [],
284
+ pendingNotificationBytes: 0,
285
+ repairSequences: null,
286
+ confirmationAttempted: false,
287
+ confirmationSucceeded: false,
288
+ abort: new AbortController(),
289
+ opening: true,
290
+ writeAttempted: false,
291
+ abortRequested: false,
292
+ abortReason: 0x00ff,
293
+ releasePromise: null,
294
+ terminal: deferred(),
295
+ poisoned: false,
296
+ };
297
+ this.session = session;
298
+ void session.terminal.promise.catch(() => undefined);
299
+ const activeSignal = signal
300
+ ? AbortSignal.any([signal, session.abort.signal])
301
+ : session.abort.signal;
302
+ try {
303
+ session.subscription = await this.transport.subscribe(this.device, BOTA_STORAGE_SERVICE, RECORDING_TRANSFER_V2_CHARACTERISTIC, ({ characteristicUuid, value }) => {
304
+ if (characteristicUuid.toLowerCase()
305
+ !== RECORDING_TRANSFER_V2_CHARACTERISTIC)
306
+ return;
307
+ try {
308
+ const frame = this.core.decodeEncryptedUploadV2Transfer(value);
309
+ if (frame.transportSessionId !== request.transportSessionId) {
310
+ throw identityFailure();
311
+ }
312
+ if (session.phase === 'awaiting_control') {
313
+ session.phase = 'control_pending';
314
+ control.resolve(frame);
315
+ return;
316
+ }
317
+ if (session.phase === 'control_pending') {
318
+ if (session.pendingNotificationBytes + value.byteLength
319
+ > MAXIMUM_QUEUED_BYTES)
320
+ throw protocolFailure();
321
+ session.pendingNotificationBytes += value.byteLength;
322
+ session.pendingNotifications.push({
323
+ frame,
324
+ rawLength: value.byteLength,
325
+ });
326
+ return;
327
+ }
328
+ this.acceptTransferFrame(session, frame, value.byteLength);
329
+ }
330
+ catch (error) {
331
+ const normalized = normalizeHostError(error);
332
+ control.reject(normalized);
333
+ queue.fail(normalized);
334
+ }
335
+ });
336
+ throwIfCancelled(false, activeSignal);
337
+ const frame = checkpoint
338
+ ? this.core.encodeEncryptedUploadV2Transfer({
339
+ kind: 'resume_request',
340
+ flags: 0,
341
+ transportSessionId: request.transportSessionId,
342
+ uploadSessionUuid: request.uploadSessionUuid,
343
+ recordingUuid: request.recordingUuid,
344
+ recordingGeneration: request.recordingGeneration,
345
+ checkpointRevision: checkpoint.revision,
346
+ nextCiphertextOffset: checkpoint.nextCiphertextOffset,
347
+ prefixSha256: checkpoint.prefixSha256,
348
+ windowPackets: request.windowPackets,
349
+ dataPayloadBytes: request.dataPayloadBytes,
350
+ })
351
+ : this.core.encodeEncryptedUploadV2Transfer({
352
+ kind: 'start',
353
+ flags: 0,
354
+ transportSessionId: request.transportSessionId,
355
+ uploadSessionUuid: request.uploadSessionUuid,
356
+ recordingUuid: request.recordingUuid,
357
+ recordingGeneration: request.recordingGeneration,
358
+ authorizationSha256: request.authorizationSha256,
359
+ checkpointRevision: 0,
360
+ nextCiphertextOffset: 0n,
361
+ prefixSha256: initialEncryptedUploadV2Checkpoint().prefixSha256,
362
+ windowPackets: request.windowPackets,
363
+ dataPayloadBytes: request.dataPayloadBytes,
364
+ });
365
+ session.phase = 'awaiting_control';
366
+ session.writeAttempted = true;
367
+ await this.writeControl(frame);
368
+ const response = await abortable(control.promise, activeSignal);
369
+ if (response.kind === 'resume_reject') {
370
+ if (!checkpoint
371
+ || response.reason === 0
372
+ || response.checkpointRevision !== checkpoint.revision
373
+ || response.nextCiphertextOffset !== checkpoint.nextCiphertextOffset
374
+ || !equalBytes(response.prefixSha256, checkpoint.prefixSha256))
375
+ throw identityFailure();
376
+ session.opening = false;
377
+ await this.releaseSession(session, false);
378
+ return { kind: 'resume_rejected' };
379
+ }
380
+ if (response.kind === 'error') {
381
+ const expectedType = checkpoint ? 0x22 : 0x20;
382
+ if (response.result === 0 || response.failedMessageType !== expectedType) {
383
+ throw identityFailure();
384
+ }
385
+ const rejection = new BotaSDKError('protocol_error', 'transfer_recording', { protocolStatus: response.result });
386
+ session.opening = false;
387
+ await this.releaseSession(session, false);
388
+ throw rejection;
389
+ }
390
+ if (checkpoint) {
391
+ if (response.kind !== 'resume_accept')
392
+ throw identityFailure();
393
+ validateResumeAcknowledgement(request, checkpoint, response);
394
+ }
395
+ else {
396
+ if (response.kind !== 'start_ack')
397
+ throw identityFailure();
398
+ validateStartAcknowledgement(request, response);
399
+ }
400
+ session.phase = checkpoint?.nextCiphertextOffset
401
+ === request.expectedCiphertextLength
402
+ ? 'manifest'
403
+ : 'window';
404
+ for (const pending of session.pendingNotifications.splice(0)) {
405
+ this.acceptTransferFrame(session, pending.frame, pending.rawLength);
406
+ }
407
+ session.pendingNotificationBytes = 0;
408
+ session.opening = false;
409
+ return { kind: 'opened', notifications: queue };
410
+ }
411
+ catch (error) {
412
+ try {
413
+ await this.releaseSession(session, session.writeAttempted);
414
+ }
415
+ catch (cleanupError) {
416
+ throw normalizeHostError(cleanupError);
417
+ }
418
+ throw normalizeHostError(error);
419
+ }
420
+ }
421
+ async sendActiveFrame(frame, continuation) {
422
+ const session = this.requireSession(frame.transportSessionId);
423
+ if (session.phase !== 'paused')
424
+ throw protocolFailure();
425
+ session.phase = continuation.kind;
426
+ session.repairSequences = continuation.kind === 'repair'
427
+ ? new Set(continuation.missingSequences)
428
+ : null;
429
+ try {
430
+ await this.writeControl(this.core.encodeEncryptedUploadV2Transfer(frame));
431
+ }
432
+ catch (error) {
433
+ this.onOwnershipUncertain();
434
+ session.queue.fail(ownershipUnknown(error));
435
+ throw ownershipUnknown(error);
436
+ }
437
+ }
438
+ async confirm(frame, writeSucceeded) {
439
+ const session = this.requireSession(frame.transportSessionId);
440
+ if (session.phase !== 'paused' && session.phase !== 'terminal') {
441
+ throw protocolFailure();
442
+ }
443
+ session.phase = 'confirming';
444
+ session.confirmationAttempted = true;
445
+ try {
446
+ await this.writeControl(this.core.encodeEncryptedUploadV2Transfer(frame));
447
+ session.confirmationSucceeded = true;
448
+ await writeSucceeded();
449
+ session.phase = 'confirmed';
450
+ await this.releaseSession(session, false, true);
451
+ }
452
+ catch (error) {
453
+ this.onOwnershipUncertain();
454
+ throw ownershipUnknown(error);
455
+ }
456
+ }
457
+ async confirmationAttemptedOrClaimCancellation() {
458
+ const session = this.session;
459
+ if (!session)
460
+ return false;
461
+ if (session.confirmationAttempted
462
+ || session.phase === 'confirming'
463
+ || session.phase === 'confirmed')
464
+ return true;
465
+ session.abortRequested = true;
466
+ return false;
467
+ }
468
+ async abort(reason = 0x00ff) {
469
+ const session = this.session;
470
+ if (!session)
471
+ return;
472
+ if (session.phase === 'confirming' || session.phase === 'confirmed') {
473
+ throw ownershipUnknown();
474
+ }
475
+ session.abortRequested = true;
476
+ session.abortReason = reason;
477
+ session.abort.abort();
478
+ if (!session.opening) {
479
+ await this.releaseSession(session, true, false, reason);
480
+ return;
481
+ }
482
+ const bounded = await boundedSettlement(settle(session.terminal.promise), this.cleanupTimeoutMs);
483
+ if (bounded === null) {
484
+ this.poison(session);
485
+ throw ownershipUnknown();
486
+ }
487
+ if (bounded.error !== null)
488
+ throw ownershipUnknown(bounded.error);
489
+ }
490
+ async cancel() {
491
+ await this.abort();
492
+ }
493
+ acceptTransferFrame(session, frame, rawLength) {
494
+ if (frame.kind === 'error') {
495
+ session.phase = 'terminal';
496
+ session.queue.push(frame, rawLength);
497
+ return;
498
+ }
499
+ switch (session.phase) {
500
+ case 'window':
501
+ if (frame.kind !== 'data' && frame.kind !== 'window_end') {
502
+ throw protocolFailure();
503
+ }
504
+ if (frame.kind === 'window_end')
505
+ session.phase = 'paused';
506
+ break;
507
+ case 'manifest':
508
+ if (frame.kind !== 'manifest_chunk' && frame.kind !== 'eof') {
509
+ throw protocolFailure();
510
+ }
511
+ if (frame.kind === 'eof')
512
+ session.phase = 'terminal';
513
+ break;
514
+ case 'repair':
515
+ if (frame.kind === 'data') {
516
+ if (!session.repairSequences?.delete(frame.sequence)) {
517
+ throw protocolFailure();
518
+ }
519
+ }
520
+ else if (frame.kind === 'window_end') {
521
+ if (session.repairSequences && session.repairSequences.size > 0) {
522
+ throw protocolFailure();
523
+ }
524
+ session.phase = 'paused';
525
+ }
526
+ else {
527
+ throw protocolFailure();
528
+ }
529
+ break;
530
+ default:
531
+ throw protocolFailure();
532
+ }
533
+ session.queue.push(frame, rawLength);
534
+ }
535
+ requireSession(transportSessionId) {
536
+ const session = this.session;
537
+ if (!session || session.request.transportSessionId !== transportSessionId) {
538
+ throw identityFailure();
539
+ }
540
+ return session;
541
+ }
542
+ async releaseSession(session, abort, confirmed = false, reason = 0x00ff) {
543
+ if (!confirmed
544
+ && (session.phase === 'confirming' || session.phase === 'confirmed'))
545
+ throw ownershipUnknown();
546
+ session.abortRequested ||= abort;
547
+ if (abort)
548
+ session.abortReason = reason;
549
+ session.opening = false;
550
+ if (!session.releasePromise) {
551
+ session.phase = 'cleaning';
552
+ session.releasePromise = (async () => {
553
+ let failure = null;
554
+ if (session.abortRequested && session.writeAttempted) {
555
+ try {
556
+ await this.writeControl(this.core.encodeEncryptedUploadV2Transfer({
557
+ kind: 'abort',
558
+ flags: 0,
559
+ transportSessionId: session.request.transportSessionId,
560
+ reason: session.abortReason,
561
+ }));
562
+ }
563
+ catch (error) {
564
+ failure = error;
565
+ }
566
+ }
567
+ try {
568
+ await session.subscription?.remove();
569
+ }
570
+ catch (error) {
571
+ failure ??= error;
572
+ }
573
+ session.queue.close();
574
+ if (failure === null) {
575
+ session.terminal.resolve(undefined);
576
+ if (this.session === session)
577
+ this.session = null;
578
+ return;
579
+ }
580
+ session.terminal.reject(failure);
581
+ throw failure;
582
+ })();
583
+ }
584
+ const bounded = await boundedSettlement(settle(session.releasePromise), this.cleanupTimeoutMs);
585
+ if (bounded === null) {
586
+ this.poison(session);
587
+ throw ownershipUnknown();
588
+ }
589
+ if (bounded.error !== null) {
590
+ this.poison(session);
591
+ throw ownershipUnknown(bounded.error);
592
+ }
593
+ }
594
+ poison(session) {
595
+ if (session.poisoned)
596
+ return;
597
+ session.poisoned = true;
598
+ this.onOwnershipUncertain();
599
+ }
600
+ async list(transportSessionId, signal) {
601
+ if (this.active)
602
+ throw operationInProgress();
603
+ if (transportSessionId <= 0n || signal?.aborted) {
604
+ throw signal?.aborted ? cancelled() : protocolFailure();
605
+ }
606
+ this.active = true;
607
+ let listSubscription = null;
608
+ let errorSubscription = null;
609
+ const completed = deferred();
610
+ const entries = [];
611
+ const digest = this.core.createIntegrityHasher();
612
+ let queuedBytes = 0;
613
+ let result = null;
614
+ let primary = null;
615
+ try {
616
+ listSubscription = await this.transport.subscribe(this.device, BOTA_STORAGE_SERVICE, RECORDING_LIST_V2_CHARACTERISTIC, ({ characteristicUuid, value }) => {
617
+ if (characteristicUuid.toLowerCase()
618
+ !== RECORDING_LIST_V2_CHARACTERISTIC)
619
+ return;
620
+ try {
621
+ queuedBytes += value.byteLength;
622
+ if (queuedBytes > MAXIMUM_QUEUED_BYTES)
623
+ throw protocolFailure();
624
+ const frame = this.core.decodeEncryptedUploadV2Transfer(value);
625
+ if (frame.transportSessionId !== transportSessionId) {
626
+ throw identityFailure();
627
+ }
628
+ if (frame.kind === 'recording_entry') {
629
+ if (frame.completionState !== 1)
630
+ throw protocolFailure();
631
+ digest.update(value.slice(12));
632
+ entries.push(copyRecordingEntry(frame));
633
+ return;
634
+ }
635
+ if (frame.kind !== 'recording_list_end')
636
+ throw protocolFailure();
637
+ if (frame.count !== entries.length
638
+ || frame.listRevision === 0
639
+ || !equalBytes(frame.listSha256, digest.sha256Snapshot()))
640
+ throw integrityFailure();
641
+ completed.resolve({
642
+ entries: entries.map(copyRecordingEntry),
643
+ listRevision: frame.listRevision,
644
+ });
645
+ }
646
+ catch (error) {
647
+ completed.reject(normalizeHostError(error));
648
+ }
649
+ });
650
+ throwIfCancelled(false, signal);
651
+ errorSubscription = await this.transport.subscribe(this.device, BOTA_STORAGE_SERVICE, RECORDING_TRANSFER_V2_CHARACTERISTIC, ({ characteristicUuid, value }) => {
652
+ if (characteristicUuid.toLowerCase()
653
+ !== RECORDING_TRANSFER_V2_CHARACTERISTIC)
654
+ return;
655
+ try {
656
+ queuedBytes += value.byteLength;
657
+ if (queuedBytes > MAXIMUM_QUEUED_BYTES)
658
+ throw protocolFailure();
659
+ const frame = this.core.decodeEncryptedUploadV2Transfer(value);
660
+ if (frame.transportSessionId !== transportSessionId)
661
+ return;
662
+ if (frame.kind !== 'error'
663
+ || frame.result === 0
664
+ || frame.failedMessageType !== 0x25)
665
+ throw protocolFailure();
666
+ completed.reject(new BotaSDKError('protocol_error', 'transfer_recording', { protocolStatus: frame.result }));
667
+ }
668
+ catch (error) {
669
+ completed.reject(normalizeHostError(error));
670
+ }
671
+ });
672
+ throwIfCancelled(false, signal);
673
+ const request = this.core.encodeEncryptedUploadV2Transfer({
674
+ kind: 'list',
675
+ flags: 0,
676
+ transportSessionId,
677
+ });
678
+ await this.writeControl(request);
679
+ result = await abortable(completed.promise, signal);
680
+ }
681
+ catch (error) {
682
+ primary = error;
683
+ }
684
+ const cleanup = (async () => {
685
+ let cleanupError = null;
686
+ for (const subscription of [errorSubscription, listSubscription]) {
687
+ try {
688
+ await subscription?.remove();
689
+ }
690
+ catch (error) {
691
+ cleanupError ??= error;
692
+ }
693
+ }
694
+ if (cleanupError !== null)
695
+ throw cleanupError;
696
+ })();
697
+ const cleanupSettlement = settle(cleanup);
698
+ const bounded = await boundedSettlement(cleanupSettlement, this.cleanupTimeoutMs);
699
+ if (bounded === null) {
700
+ this.onOwnershipUncertain();
701
+ void cleanupSettlement.then(() => {
702
+ this.active = false;
703
+ });
704
+ throw ownershipUnknown();
705
+ }
706
+ this.active = false;
707
+ if (bounded.error !== null) {
708
+ this.onOwnershipUncertain();
709
+ throw ownershipUnknown(bounded.error);
710
+ }
711
+ if (primary !== null)
712
+ throw normalizeHostError(primary);
713
+ if (!result)
714
+ throw protocolFailure();
715
+ return result;
716
+ }
717
+ async writeControl(frame) {
718
+ if (frame.byteLength > Math.min(this.transport.maximumWriteValueLength, MAXIMUM_FRAME_BYTES))
719
+ throw protocolFailure();
720
+ await this.writes.run(async () => await this.transport.write(this.device, BOTA_STORAGE_SERVICE, TRANSFER_CONTROL_V2_CHARACTERISTIC, frame, true));
721
+ }
722
+ }
723
+ class BoundedFrameQueue {
724
+ values = [];
725
+ waiters = [];
726
+ bufferedBytes = 0;
727
+ terminalError = null;
728
+ closed = false;
729
+ push(frame, rawLength) {
730
+ if (this.closed || this.terminalError !== null)
731
+ throw protocolFailure();
732
+ const waiter = this.waiters.shift();
733
+ if (waiter) {
734
+ waiter.resolve({ done: false, value: frame });
735
+ return;
736
+ }
737
+ if (this.bufferedBytes + rawLength > MAXIMUM_QUEUED_BYTES) {
738
+ const error = protocolFailure();
739
+ this.fail(error);
740
+ throw error;
741
+ }
742
+ this.bufferedBytes += rawLength;
743
+ this.values.push({ frame, rawLength });
744
+ }
745
+ fail(error) {
746
+ if (this.terminalError !== null || this.closed)
747
+ return;
748
+ this.terminalError = error;
749
+ this.values.length = 0;
750
+ this.bufferedBytes = 0;
751
+ for (const waiter of this.waiters.splice(0))
752
+ waiter.reject(error);
753
+ }
754
+ close() {
755
+ if (this.closed)
756
+ return;
757
+ this.closed = true;
758
+ this.values.length = 0;
759
+ this.bufferedBytes = 0;
760
+ for (const waiter of this.waiters.splice(0)) {
761
+ waiter.resolve({ done: true, value: undefined });
762
+ }
763
+ }
764
+ [Symbol.asyncIterator]() {
765
+ return {
766
+ next: async () => {
767
+ const value = this.values.shift();
768
+ if (value) {
769
+ this.bufferedBytes -= value.rawLength;
770
+ return { done: false, value: value.frame };
771
+ }
772
+ if (this.terminalError !== null)
773
+ throw this.terminalError;
774
+ if (this.closed)
775
+ return { done: true, value: undefined };
776
+ return await new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
777
+ },
778
+ };
779
+ }
780
+ }
781
+ export class EncryptedUploadV2Host {
782
+ core;
783
+ storage;
784
+ blob;
785
+ material;
786
+ state;
787
+ fetcher;
788
+ callbacks;
789
+ recoveryPhase;
790
+ expectedReceiptSha256Hex;
791
+ signedWriter;
792
+ transferControl;
793
+ uploadAbort = new AbortController();
794
+ receiver = null;
795
+ pendingNativeCheckpoint = null;
796
+ persistedCoreCheckpoint = null;
797
+ completedManifest = null;
798
+ completedEvidence = null;
799
+ acceptedReceipt = null;
800
+ acceptedReceiptSha256 = null;
801
+ boundaryTarget = null;
802
+ boundaryGate = null;
803
+ startBoundaryTarget = null;
804
+ pumpPromise = null;
805
+ preparedAuthorizationSha256 = null;
806
+ cancelledValue = false;
807
+ abortPromise = null;
808
+ constructor(options) {
809
+ this.core = options.core;
810
+ this.storage = options.storage;
811
+ this.blob = options.blob;
812
+ this.material = options.material;
813
+ this.state = copyPersistedState(options.state);
814
+ this.fetcher = options.fetcher;
815
+ this.callbacks = options.callbacks;
816
+ this.recoveryPhase = options.recoveryPhase;
817
+ this.expectedReceiptSha256Hex = options.expectedReceiptSha256Hex;
818
+ this.signedWriter = new EncryptedUploadV2SignedBlobWriter(options.core, options.transport, options.device, options.onOwnershipUncertain);
819
+ this.transferControl = new EncryptedUploadV2TransferControl(options.core, options.transport, options.device, options.onOwnershipUncertain);
820
+ }
821
+ async execute(envelope, context) {
822
+ const effect = envelope.effect;
823
+ switch (effect.kind) {
824
+ case 'encrypted_upload_v2_load_checkpoint': {
825
+ this.validateCheckpointOwner(effect.serialNumber, uuidString(effect.recordingUuid), effect.recordingGeneration, uuidString(effect.uploadSessionId), effect.ownerRevision);
826
+ return {
827
+ requestId: envelope.requestId,
828
+ kind: 'encrypted_upload_v2_checkpoint_loaded',
829
+ checkpoint: this.state.coreCheckpoint
830
+ ? copyCoreCheckpoint(this.state.coreCheckpoint)
831
+ : null,
832
+ };
833
+ }
834
+ case 'encrypted_upload_v2_delete_checkpoint':
835
+ if (uuidString(effect.uploadSessionId) !== this.state.uploadSessionId) {
836
+ throw identityFailure();
837
+ }
838
+ this.state.coreCheckpoint = null;
839
+ this.state.highestContiguousSequence = null;
840
+ await this.storage.saveEncryptedUploadV2Checkpoint(this.state.operationId, copyPersistedState(this.state));
841
+ return null;
842
+ case 'encrypted_upload_v2_truncate_sink': {
843
+ if (effect.sinkId !== this.state.sinkId)
844
+ throw identityFailure();
845
+ const offset = safeNumber(effect.nextCiphertextOffset);
846
+ const size = await this.blob.size();
847
+ if (size < offset)
848
+ throw integrityFailure();
849
+ if (size > offset)
850
+ await this.blob.truncate(offset);
851
+ return {
852
+ requestId: envelope.requestId,
853
+ kind: 'encrypted_upload_v2_sink_truncated',
854
+ };
855
+ }
856
+ case 'encrypted_upload_v2_prepare_session': {
857
+ if (effect.materialId !== this.state.materialId)
858
+ throw identityFailure();
859
+ const source = this.material.authorization;
860
+ if (source.byteLength !== 408)
861
+ throw protocolFailure();
862
+ const digest = sha256(this.core, source);
863
+ await this.signedWriter.send('authorization', nextSignedBlobWriteId(), source, this.state.maximumSignedBlobBytes, context.signal, () => source.fill(0));
864
+ this.preparedAuthorizationSha256 = digest;
865
+ return {
866
+ requestId: envelope.requestId,
867
+ kind: 'encrypted_upload_v2_session_prepared',
868
+ authorizationSha256: digest.slice(),
869
+ };
870
+ }
871
+ case 'encrypted_upload_v2_start_transfer':
872
+ return await this.startTransfer(envelope.requestId, effect, context);
873
+ case 'encrypted_upload_v2_repair_window':
874
+ return await this.repairWindow(envelope.requestId, effect.missingSequences);
875
+ case 'encrypted_upload_v2_save_checkpoint':
876
+ return await this.saveCheckpoint(envelope.requestId, effect.checkpoint);
877
+ case 'encrypted_upload_v2_acknowledge_window':
878
+ return await this.acknowledgeWindow(envelope.requestId, effect.checkpoint, context);
879
+ case 'encrypted_upload_v2_stage_artifacts':
880
+ return await this.stageArtifacts(envelope.requestId, effect.sinkId, effect.materialId, effect.evidence, context.signal);
881
+ case 'encrypted_upload_v2_await_completion_receipt':
882
+ return await this.awaitReceipt(envelope.requestId, effect.materialId, effect.evidence, context.signal);
883
+ case 'encrypted_upload_v2_confirm_with_receipt':
884
+ return await this.confirm(envelope.requestId, envelope.cancellationId, effect.materialId, effect.receiptSha256);
885
+ case 'encrypted_upload_v2_abort':
886
+ if (effect.materialId !== this.state.materialId)
887
+ throw identityFailure();
888
+ await this.abortState();
889
+ return null;
890
+ default:
891
+ throw new BotaSDKError('internal_error', 'transfer_recording');
892
+ }
893
+ }
894
+ async confirmationAttemptedOrClaimCancellation() {
895
+ return await this.transferControl.confirmationAttemptedOrClaimCancellation();
896
+ }
897
+ async cancel() {
898
+ await this.abortState();
899
+ }
900
+ async startTransfer(requestId, effect, context) {
901
+ this.validateStartEffect(effect);
902
+ const nativeCheckpoint = effect.checkpoint
903
+ ? {
904
+ revision: effect.checkpoint.checkpointRevision,
905
+ nextCiphertextOffset: effect.checkpoint.nextCiphertextOffset,
906
+ prefixSha256: effect.checkpoint.prefixSha256.slice(),
907
+ highestContiguousSequence: this.state.highestContiguousSequence,
908
+ }
909
+ : initialEncryptedUploadV2Checkpoint();
910
+ this.receiver = new EncryptedUploadV2TransferReceiver(this.core, this.blob, {
911
+ transportSessionId: this.state.transportSessionId,
912
+ expectedCiphertextLength: this.state.recording.ciphertextLength,
913
+ expectedCiphertextSha256: this.state.recording.ciphertextSha256,
914
+ maximumDataPayloadBytes: this.state.dataPayloadBytes,
915
+ maximumWindowPackets: this.state.windowPackets,
916
+ maximumMissingSequences: this.state.maximumMissingSequences,
917
+ checkpoint: nativeCheckpoint,
918
+ });
919
+ await this.receiver.prepare();
920
+ const opened = await this.transferControl.open({
921
+ transportSessionId: this.state.transportSessionId,
922
+ uploadSessionUuid: this.state.uploadSessionId,
923
+ recordingUuid: this.state.recording.uuid,
924
+ recordingGeneration: this.state.recording.generation,
925
+ authorizationSha256: effect.authorizationSha256,
926
+ expectedCiphertextLength: this.state.recording.ciphertextLength,
927
+ expectedCiphertextSha256: this.state.recording.ciphertextSha256,
928
+ expectedCheckpointIntervalBlocks: this.state.checkpointIntervalBlocks,
929
+ windowPackets: this.state.windowPackets,
930
+ dataPayloadBytes: this.state.dataPayloadBytes,
931
+ }, effect.checkpoint ? nativeCheckpoint : null, context.signal);
932
+ if (opened.kind === 'resume_rejected') {
933
+ await context.dispatch({
934
+ requestId,
935
+ kind: 'encrypted_upload_v2_resume_rejected',
936
+ });
937
+ return null;
938
+ }
939
+ const target = this.dispatchTarget(requestId, context);
940
+ this.startBoundaryTarget = target;
941
+ this.boundaryTarget = target;
942
+ await context.dispatch({
943
+ requestId,
944
+ kind: 'encrypted_upload_v2_transfer_started',
945
+ });
946
+ this.pumpPromise = this.pump(opened.notifications);
947
+ void this.pumpPromise.catch(() => undefined);
948
+ return null;
949
+ }
950
+ async repairWindow(requestId, missingSequences) {
951
+ const receiver = this.requireReceiver();
952
+ const frame = receiver.repairAcknowledgement(missingSequences);
953
+ const boundary = deferred();
954
+ this.boundaryTarget = async (event) => boundary.resolve(event);
955
+ const gate = this.boundaryGate;
956
+ if (!gate)
957
+ throw protocolFailure();
958
+ await this.transferControl.sendActiveFrame(frame, {
959
+ kind: 'repair',
960
+ missingSequences,
961
+ });
962
+ gate.resolve(undefined);
963
+ return boundaryHostEvent(requestId, await boundary.promise);
964
+ }
965
+ async saveCheckpoint(requestId, checkpoint) {
966
+ const native = this.pendingNativeCheckpoint;
967
+ if (!native || !sameCoreAndNativeCheckpoint(checkpoint, native, this.state)) {
968
+ throw integrityFailure();
969
+ }
970
+ this.state.coreCheckpoint = copyCoreCheckpoint(checkpoint);
971
+ this.state.highestContiguousSequence = native.highestContiguousSequence;
972
+ await this.storage.saveEncryptedUploadV2Checkpoint(this.state.operationId, copyPersistedState(this.state));
973
+ this.requireReceiver().checkpointDidPersist(native);
974
+ this.persistedCoreCheckpoint = copyCoreCheckpoint(checkpoint);
975
+ return {
976
+ requestId,
977
+ kind: 'encrypted_upload_v2_checkpoint_saved',
978
+ };
979
+ }
980
+ async acknowledgeWindow(requestId, checkpoint, context) {
981
+ if (!this.persistedCoreCheckpoint
982
+ || !sameCoreCheckpoint(this.persistedCoreCheckpoint, checkpoint)
983
+ || !this.pendingNativeCheckpoint)
984
+ throw integrityFailure();
985
+ const frame = this.requireReceiver().windowAcknowledgement(this.pendingNativeCheckpoint);
986
+ this.boundaryTarget = this.startBoundaryTarget;
987
+ const gate = this.boundaryGate;
988
+ if (!gate || !this.boundaryTarget)
989
+ throw protocolFailure();
990
+ await this.transferControl.sendActiveFrame(frame, checkpoint.nextCiphertextOffset === this.state.recording.ciphertextLength
991
+ ? { kind: 'manifest' }
992
+ : { kind: 'window' });
993
+ this.pendingNativeCheckpoint = null;
994
+ this.persistedCoreCheckpoint = null;
995
+ this.boundaryGate = null;
996
+ await context.dispatch({
997
+ requestId,
998
+ kind: 'encrypted_upload_v2_window_acknowledged',
999
+ checkpoint: copyCoreCheckpoint(checkpoint),
1000
+ });
1001
+ gate.resolve(undefined);
1002
+ return null;
1003
+ }
1004
+ async stageArtifacts(requestId, sinkId, materialId, evidence, signal) {
1005
+ this.validateCompletion(sinkId, materialId, evidence);
1006
+ if (this.recoveryPhase !== 'cloud_completed') {
1007
+ throwIfCancelled(this.cancelledValue, signal);
1008
+ await this.callbacks.uploading();
1009
+ throwIfCancelled(this.cancelledValue, signal);
1010
+ const request = await awaitProviderCall(this.material.stagingRequest(providerEvidence(evidence), signal), signal, 'upload');
1011
+ validateUploadRequest(request);
1012
+ await this.uploadCiphertext(request, signal);
1013
+ throwIfCancelled(this.cancelledValue, signal);
1014
+ await awaitProviderCall(this.material.submitManifest(this.requireCompletedManifest().slice(), providerEvidence(evidence), signal), signal, 'upload');
1015
+ }
1016
+ return {
1017
+ requestId,
1018
+ kind: 'encrypted_upload_v2_artifacts_staged',
1019
+ };
1020
+ }
1021
+ async awaitReceipt(requestId, materialId, evidence, signal) {
1022
+ this.validateCompletion(this.state.sinkId, materialId, evidence);
1023
+ if (this.recoveryPhase !== 'cloud_completed') {
1024
+ throwIfCancelled(this.cancelledValue, signal);
1025
+ await awaitProviderCall(this.material.finalize(providerEvidence(evidence), signal), signal, 'upload');
1026
+ }
1027
+ throwIfCancelled(this.cancelledValue, signal);
1028
+ const pendingReceipt = this.material.completionReceipt(providerEvidence(evidence), signal);
1029
+ let receipt;
1030
+ try {
1031
+ receipt = await awaitProviderCall(pendingReceipt, signal, 'upload');
1032
+ }
1033
+ catch (error) {
1034
+ void pendingReceipt.then((lateReceipt) => {
1035
+ if (lateReceipt instanceof Uint8Array)
1036
+ lateReceipt.fill(0);
1037
+ }, () => undefined);
1038
+ throw error;
1039
+ }
1040
+ if (this.cancelledValue || signal.aborted) {
1041
+ receipt.fill(0);
1042
+ throw cancelled();
1043
+ }
1044
+ if (!(receipt instanceof Uint8Array))
1045
+ throw integrityFailure();
1046
+ if (receipt.byteLength !== 336) {
1047
+ receipt.fill(0);
1048
+ throw integrityFailure();
1049
+ }
1050
+ const digest = sha256(this.core, receipt);
1051
+ if (this.expectedReceiptSha256Hex !== null
1052
+ && hexString(digest) !== this.expectedReceiptSha256Hex) {
1053
+ receipt.fill(0);
1054
+ throw integrityFailure();
1055
+ }
1056
+ this.acceptedReceipt = receipt;
1057
+ this.acceptedReceiptSha256 = digest;
1058
+ await this.callbacks.cloudCompleted(digest.slice());
1059
+ return {
1060
+ requestId,
1061
+ kind: 'encrypted_upload_v2_completion_receipt_accepted',
1062
+ receiptSha256: digest.slice(),
1063
+ };
1064
+ }
1065
+ async confirm(requestId, _cancellationId, materialId, receiptSha256) {
1066
+ if (materialId !== this.state.materialId
1067
+ || !this.acceptedReceipt
1068
+ || !this.acceptedReceiptSha256
1069
+ || !equalBytes(receiptSha256, this.acceptedReceiptSha256))
1070
+ throw integrityFailure();
1071
+ const receipt = this.acceptedReceipt;
1072
+ await this.signedWriter.send('receipt', nextSignedBlobWriteId(), receipt, this.state.maximumSignedBlobBytes, undefined, () => {
1073
+ receipt.fill(0);
1074
+ if (this.acceptedReceipt === receipt)
1075
+ this.acceptedReceipt = null;
1076
+ });
1077
+ const frame = {
1078
+ kind: 'confirm',
1079
+ flags: 0,
1080
+ transportSessionId: this.state.transportSessionId,
1081
+ uploadSessionUuid: this.state.uploadSessionId,
1082
+ recordingUuid: this.state.recording.uuid,
1083
+ recordingGeneration: this.state.recording.generation,
1084
+ ownerRevision: this.state.ownerRevision,
1085
+ receiptSha256: receiptSha256.slice(),
1086
+ };
1087
+ await this.transferControl.confirm(frame, async () => {
1088
+ await this.callbacks.confirmed();
1089
+ });
1090
+ return {
1091
+ requestId,
1092
+ kind: 'encrypted_upload_v2_recording_confirmed',
1093
+ };
1094
+ }
1095
+ async pump(notifications) {
1096
+ try {
1097
+ for await (const frame of notifications) {
1098
+ const event = await this.requireReceiver().receive(frame);
1099
+ if (!event)
1100
+ continue;
1101
+ const target = this.boundaryTarget;
1102
+ if (!target)
1103
+ throw protocolFailure();
1104
+ if (event.kind === 'window_staged') {
1105
+ this.pendingNativeCheckpoint = copyNativeCheckpoint(event.checkpoint);
1106
+ const checkpoint = coreCheckpoint(this.state.serialNumber, uuidBytes(this.state.recording.uuid), this.state.recording.generation, uuidBytes(this.state.uploadSessionId), this.state.ownerRevision, this.state.transportSessionId, event.checkpoint, this.state.windowPackets, this.state.dataPayloadBytes);
1107
+ const gate = deferred();
1108
+ this.boundaryGate = gate;
1109
+ await target({
1110
+ kind: 'encrypted_upload_v2_window_staged',
1111
+ checkpoint,
1112
+ missingSequences: [...event.missingSequences],
1113
+ });
1114
+ await gate.promise;
1115
+ continue;
1116
+ }
1117
+ this.completedManifest = event.manifest.slice();
1118
+ this.completedEvidence = copyEvidence(event.evidence);
1119
+ this.state.evidence = copyEvidence(event.evidence);
1120
+ await this.storage.saveEncryptedUploadV2Checkpoint(this.state.operationId, copyPersistedState(this.state));
1121
+ if (this.recoveryPhase !== 'cloud_completed') {
1122
+ await this.callbacks.transferCompleted(copyEvidence(event.evidence));
1123
+ }
1124
+ await target({
1125
+ kind: 'encrypted_upload_v2_transfer_completed',
1126
+ evidence: copyEvidence(event.evidence),
1127
+ });
1128
+ return;
1129
+ }
1130
+ throw protocolFailure();
1131
+ }
1132
+ catch (error) {
1133
+ if (this.cancelledValue)
1134
+ return;
1135
+ const target = this.boundaryTarget;
1136
+ if (target) {
1137
+ await target({
1138
+ kind: 'encrypted_upload_v2_failed',
1139
+ error: coreProtocolError(error),
1140
+ }).catch(() => undefined);
1141
+ }
1142
+ throw error;
1143
+ }
1144
+ }
1145
+ dispatchTarget(requestId, context) {
1146
+ return async (event) => await context.dispatch(boundaryHostEvent(requestId, event));
1147
+ }
1148
+ validateCheckpointOwner(serialNumber, recordingUuid, recordingGeneration, uploadSessionId, ownerRevision) {
1149
+ if (serialNumber !== this.state.serialNumber
1150
+ || recordingUuid !== this.state.recording.uuid
1151
+ || recordingGeneration !== this.state.recording.generation
1152
+ || uploadSessionId !== this.state.uploadSessionId
1153
+ || ownerRevision !== this.state.ownerRevision)
1154
+ throw identityFailure();
1155
+ }
1156
+ validateStartEffect(effect) {
1157
+ this.validateCheckpointOwner(effect.serialNumber, uuidString(effect.recordingUuid), effect.recordingGeneration, uuidString(effect.uploadSessionId), effect.ownerRevision);
1158
+ if (effect.storageFormat !== this.state.recording.storageFormat
1159
+ || effect.transportSessionId !== this.state.transportSessionId
1160
+ || effect.materialId !== this.state.materialId
1161
+ || effect.sinkId !== this.state.sinkId
1162
+ || effect.policy !== this.state.policy
1163
+ || effect.windowPackets !== this.state.windowPackets
1164
+ || effect.dataPayloadBytes !== this.state.dataPayloadBytes
1165
+ || effect.ciphertextLength !== this.state.recording.ciphertextLength
1166
+ || !equalBytes(effect.ciphertextSha256, this.state.recording.ciphertextSha256)
1167
+ || !this.preparedAuthorizationSha256
1168
+ || !equalBytes(effect.authorizationSha256, this.preparedAuthorizationSha256)
1169
+ || !sameOptionalCoreCheckpoint(effect.checkpoint, this.state.coreCheckpoint))
1170
+ throw integrityFailure();
1171
+ }
1172
+ validateCompletion(sinkId, materialId, evidence) {
1173
+ if (sinkId !== this.state.sinkId
1174
+ || materialId !== this.state.materialId
1175
+ || !this.completedEvidence
1176
+ || !sameEvidence(evidence, this.completedEvidence))
1177
+ throw integrityFailure();
1178
+ }
1179
+ requireReceiver() {
1180
+ if (!this.receiver)
1181
+ throw protocolFailure();
1182
+ return this.receiver;
1183
+ }
1184
+ requireCompletedManifest() {
1185
+ if (!this.completedManifest)
1186
+ throw protocolFailure();
1187
+ return this.completedManifest;
1188
+ }
1189
+ async uploadCiphertext(request, signal) {
1190
+ const combinedSignal = AbortSignal.any([
1191
+ signal,
1192
+ this.uploadAbort.signal,
1193
+ ]);
1194
+ const body = blobReadableStream(this.blob, combinedSignal);
1195
+ const init = {
1196
+ method: 'PUT',
1197
+ headers: { ...request.headers },
1198
+ body,
1199
+ signal: combinedSignal,
1200
+ redirect: 'error',
1201
+ duplex: 'half',
1202
+ };
1203
+ const pendingResponse = this.fetcher(request.url, init);
1204
+ let response;
1205
+ try {
1206
+ response = await abortable(pendingResponse, combinedSignal);
1207
+ }
1208
+ catch (error) {
1209
+ if (combinedSignal.aborted) {
1210
+ void pendingResponse.then(async (lateResponse) => {
1211
+ await lateResponse.body?.cancel().catch(() => undefined);
1212
+ }, () => undefined);
1213
+ }
1214
+ throw error;
1215
+ }
1216
+ throwIfCancelled(this.cancelledValue, combinedSignal);
1217
+ if (!response.ok) {
1218
+ await response.body?.cancel().catch(() => undefined);
1219
+ throw new BotaSDKError('upload_failed', 'upload', { retryable: true });
1220
+ }
1221
+ }
1222
+ async abortState() {
1223
+ if (this.abortPromise)
1224
+ return await this.abortPromise;
1225
+ this.cancelledValue = true;
1226
+ this.abortPromise = (async () => {
1227
+ this.uploadAbort.abort();
1228
+ this.boundaryGate?.resolve(undefined);
1229
+ let failure = null;
1230
+ let signedTerminalJoined = false;
1231
+ try {
1232
+ await this.signedWriter.cancel();
1233
+ signedTerminalJoined = true;
1234
+ }
1235
+ catch (error) {
1236
+ failure = error;
1237
+ }
1238
+ if (signedTerminalJoined) {
1239
+ this.material.authorization.fill(0);
1240
+ this.acceptedReceipt?.fill(0);
1241
+ this.acceptedReceipt = null;
1242
+ }
1243
+ try {
1244
+ await this.transferControl.abort();
1245
+ }
1246
+ catch (error) {
1247
+ failure ??= error;
1248
+ }
1249
+ try {
1250
+ await awaitProviderCall(this.material.cancel(this.uploadAbort.signal), this.uploadAbort.signal, 'upload');
1251
+ }
1252
+ catch (error) {
1253
+ if (!this.uploadAbort.signal.aborted)
1254
+ failure ??= error;
1255
+ }
1256
+ if (failure !== null)
1257
+ throw normalizeHostError(failure);
1258
+ })();
1259
+ return await this.abortPromise;
1260
+ }
1261
+ }
1262
+ export class EncryptedUploadV2TransferReceiver {
1263
+ core;
1264
+ blob;
1265
+ options;
1266
+ checkpoint;
1267
+ packets = new Map();
1268
+ pendingWindow = null;
1269
+ manifest = new Uint8Array(MANIFEST_LENGTH);
1270
+ manifestPresent = new Uint8Array(MANIFEST_LENGTH);
1271
+ manifestSha256 = null;
1272
+ prepared = false;
1273
+ terminal = false;
1274
+ completed = false;
1275
+ bufferedBytes = 0;
1276
+ constructor(core, blob, options) {
1277
+ this.core = core;
1278
+ this.blob = blob;
1279
+ this.options = {
1280
+ ...options,
1281
+ expectedCiphertextSha256: options.expectedCiphertextSha256.slice(),
1282
+ checkpoint: copyNativeCheckpoint(options.checkpoint),
1283
+ };
1284
+ this.checkpoint = copyNativeCheckpoint(options.checkpoint);
1285
+ if (options.transportSessionId <= 0n
1286
+ || options.expectedCiphertextLength <= 0n
1287
+ || options.expectedCiphertextLength > BigInt(Number.MAX_SAFE_INTEGER)
1288
+ || options.expectedCiphertextSha256.byteLength !== 32
1289
+ || options.maximumDataPayloadBytes <= 0
1290
+ || options.maximumWindowPackets <= 0
1291
+ || options.maximumMissingSequences <= 0
1292
+ || options.checkpoint.nextCiphertextOffset < 0n
1293
+ || options.checkpoint.nextCiphertextOffset > options.expectedCiphertextLength
1294
+ || options.checkpoint.prefixSha256.byteLength !== 32
1295
+ || ((options.checkpoint.nextCiphertextOffset === 0n)
1296
+ !== (options.checkpoint.highestContiguousSequence === null)))
1297
+ throw protocolFailure();
1298
+ }
1299
+ async prepare() {
1300
+ const checkpointOffset = safeNumber(this.checkpoint.nextCiphertextOffset);
1301
+ const size = await this.blob.size();
1302
+ if (size < checkpointOffset)
1303
+ throw integrityFailure();
1304
+ if (size !== checkpointOffset)
1305
+ await this.blob.truncate(checkpointOffset);
1306
+ const digest = await hashBlobPrefix(this.core, this.blob, checkpointOffset);
1307
+ if (!equalBytes(digest, this.checkpoint.prefixSha256)) {
1308
+ throw integrityFailure();
1309
+ }
1310
+ this.prepared = true;
1311
+ }
1312
+ async receive(frame) {
1313
+ if (!this.prepared || this.terminal || this.completed)
1314
+ throw protocolFailure();
1315
+ if (frame.transportSessionId !== this.options.transportSessionId) {
1316
+ this.terminal = true;
1317
+ throw identityFailure();
1318
+ }
1319
+ try {
1320
+ switch (frame.kind) {
1321
+ case 'data':
1322
+ this.receiveData(frame);
1323
+ return null;
1324
+ case 'window_end':
1325
+ return await this.receiveWindowEnd(frame);
1326
+ case 'manifest_chunk':
1327
+ this.receiveManifest(frame);
1328
+ return null;
1329
+ case 'eof': {
1330
+ const completed = await this.receiveEof(frame);
1331
+ this.completed = true;
1332
+ return completed;
1333
+ }
1334
+ case 'error':
1335
+ throw new BotaSDKError('protocol_error', 'transfer_recording', { protocolStatus: frame.result });
1336
+ default:
1337
+ throw protocolFailure();
1338
+ }
1339
+ }
1340
+ catch (error) {
1341
+ this.terminal = true;
1342
+ throw normalizeHostError(error);
1343
+ }
1344
+ }
1345
+ repairAcknowledgement(missingSequences) {
1346
+ const pending = this.pendingWindow;
1347
+ if (!pending
1348
+ || pending.missingSequences.length === 0
1349
+ || !equalNumbers(pending.missingSequences, missingSequences))
1350
+ throw protocolFailure();
1351
+ return this.acknowledgement(pending, pending.highestContiguousSequence, pending.contiguousOffset, pending.contiguousSha256, this.checkpoint.revision, missingSequences);
1352
+ }
1353
+ checkpointDidPersist(checkpoint) {
1354
+ const pending = this.pendingWindow;
1355
+ if (!pending
1356
+ || pending.missingSequences.length !== 0
1357
+ || !sameCheckpoint(pending.checkpoint, checkpoint))
1358
+ throw protocolFailure();
1359
+ pending.persisted = true;
1360
+ }
1361
+ windowAcknowledgement(checkpoint) {
1362
+ const pending = this.pendingWindow;
1363
+ if (!pending
1364
+ || pending.missingSequences.length !== 0
1365
+ || !sameCheckpoint(pending.checkpoint, checkpoint)
1366
+ || !pending.persisted)
1367
+ throw protocolFailure();
1368
+ const acknowledgement = this.acknowledgement(pending, pending.boundary.lastSequence, checkpoint.nextCiphertextOffset, checkpoint.prefixSha256, checkpoint.revision, []);
1369
+ this.checkpoint = copyNativeCheckpoint(checkpoint);
1370
+ this.packets.clear();
1371
+ this.bufferedBytes = 0;
1372
+ this.pendingWindow = null;
1373
+ return acknowledgement;
1374
+ }
1375
+ receiveData(frame) {
1376
+ const end = frame.offset + BigInt(frame.data.byteLength);
1377
+ const repair = this.pendingWindow?.missingSequences ?? null;
1378
+ if (frame.data.byteLength === 0
1379
+ || frame.data.byteLength > this.options.maximumDataPayloadBytes
1380
+ || frame.offset < this.checkpoint.nextCiphertextOffset
1381
+ || end > this.options.expectedCiphertextLength
1382
+ || (repair !== null && !repair.includes(frame.sequence)))
1383
+ throw protocolFailure();
1384
+ const metadata = {
1385
+ offset: frame.offset,
1386
+ bytes: frame.data.slice(),
1387
+ sha256: sha256(this.core, frame.data),
1388
+ };
1389
+ const existing = this.packets.get(frame.sequence);
1390
+ if (existing) {
1391
+ if (existing.offset !== metadata.offset
1392
+ || existing.bytes.byteLength !== metadata.bytes.byteLength
1393
+ || !equalBytes(existing.sha256, metadata.sha256)
1394
+ || !equalBytes(existing.bytes, metadata.bytes))
1395
+ throw integrityFailure();
1396
+ return;
1397
+ }
1398
+ if (this.packets.size >= this.options.maximumWindowPackets
1399
+ || [...this.packets.values()].some((candidate) => overlaps(candidate, metadata))
1400
+ || this.bufferedBytes + metadata.bytes.byteLength + 80 > MAXIMUM_QUEUED_BYTES)
1401
+ throw protocolFailure();
1402
+ this.packets.set(frame.sequence, metadata);
1403
+ this.bufferedBytes += metadata.bytes.byteLength + 80;
1404
+ }
1405
+ async receiveWindowEnd(frame) {
1406
+ if (this.pendingWindow && !sameWindow(this.pendingWindow.boundary, frame)) {
1407
+ throw protocolFailure();
1408
+ }
1409
+ const previous = this.checkpoint.highestContiguousSequence;
1410
+ const follows = previous === null
1411
+ || (previous < 0xffffffff && frame.firstSequence === previous + 1);
1412
+ const span = frame.lastSequence - frame.firstSequence;
1413
+ if (frame.firstSequence > frame.lastSequence
1414
+ || !follows
1415
+ || frame.checkpointRevision <= this.checkpoint.revision
1416
+ || frame.nextCiphertextOffset <= this.checkpoint.nextCiphertextOffset
1417
+ || frame.nextCiphertextOffset > this.options.expectedCiphertextLength
1418
+ || frame.prefixSha256.byteLength !== 32
1419
+ || span >= this.options.maximumWindowPackets
1420
+ || [...this.packets.keys()].some((sequence) => sequence < frame.firstSequence || sequence > frame.lastSequence))
1421
+ throw protocolFailure();
1422
+ const missing = [];
1423
+ for (let sequence = frame.firstSequence; sequence <= frame.lastSequence; sequence += 1) {
1424
+ if (!this.packets.has(sequence))
1425
+ missing.push(sequence);
1426
+ }
1427
+ if (missing.length > this.options.maximumMissingSequences) {
1428
+ throw protocolFailure();
1429
+ }
1430
+ let contiguousOffset = this.checkpoint.nextCiphertextOffset;
1431
+ let highest = frame.firstSequence === 0 ? 0 : frame.firstSequence - 1;
1432
+ for (let sequence = frame.firstSequence; sequence <= frame.lastSequence; sequence += 1) {
1433
+ const packet = this.packets.get(sequence);
1434
+ if (!packet)
1435
+ break;
1436
+ if (packet.offset !== contiguousOffset)
1437
+ throw integrityFailure();
1438
+ contiguousOffset += BigInt(packet.bytes.byteLength);
1439
+ highest = sequence;
1440
+ }
1441
+ const contiguousSha256 = await this.hashProspectivePrefix(frame.firstSequence, highest);
1442
+ const checkpoint = {
1443
+ revision: frame.checkpointRevision,
1444
+ nextCiphertextOffset: frame.nextCiphertextOffset,
1445
+ prefixSha256: frame.prefixSha256.slice(),
1446
+ highestContiguousSequence: frame.lastSequence,
1447
+ };
1448
+ if (missing.length === 0) {
1449
+ if (contiguousOffset !== frame.nextCiphertextOffset
1450
+ || !equalBytes(contiguousSha256, frame.prefixSha256))
1451
+ throw integrityFailure();
1452
+ await this.flushWindow(frame.firstSequence, frame.lastSequence);
1453
+ }
1454
+ this.pendingWindow = {
1455
+ boundary: copyWindow(frame),
1456
+ checkpoint,
1457
+ missingSequences: missing,
1458
+ highestContiguousSequence: highest,
1459
+ contiguousOffset,
1460
+ contiguousSha256,
1461
+ persisted: false,
1462
+ };
1463
+ return {
1464
+ kind: 'window_staged',
1465
+ checkpoint: copyNativeCheckpoint(checkpoint),
1466
+ missingSequences: [...missing],
1467
+ };
1468
+ }
1469
+ receiveManifest(frame) {
1470
+ const end = frame.chunkOffset + frame.chunk.byteLength;
1471
+ if (this.pendingWindow !== null
1472
+ || frame.totalManifestLength !== MANIFEST_LENGTH
1473
+ || frame.manifestSha256.byteLength !== 32
1474
+ || frame.chunk.byteLength === 0
1475
+ || end > MANIFEST_LENGTH
1476
+ || (this.manifestSha256 !== null
1477
+ && !equalBytes(this.manifestSha256, frame.manifestSha256)))
1478
+ throw protocolFailure();
1479
+ for (let relative = 0; relative < frame.chunk.byteLength; relative += 1) {
1480
+ const index = frame.chunkOffset + relative;
1481
+ if (this.manifestPresent[index] && this.manifest[index] !== frame.chunk[relative]) {
1482
+ throw integrityFailure();
1483
+ }
1484
+ this.manifest[index] = frame.chunk[relative] ?? 0;
1485
+ this.manifestPresent[index] = 1;
1486
+ }
1487
+ this.manifestSha256 = frame.manifestSha256.slice();
1488
+ }
1489
+ async receiveEof(frame) {
1490
+ const size = await this.blob.size();
1491
+ const digest = await hashBlobPrefix(this.core, this.blob, size);
1492
+ if (this.pendingWindow !== null
1493
+ || this.packets.size !== 0
1494
+ || this.checkpoint.highestContiguousSequence !== frame.finalSequence
1495
+ || frame.blockCount === 0
1496
+ || frame.ciphertextLength !== this.options.expectedCiphertextLength
1497
+ || !equalBytes(frame.ciphertextSha256, this.options.expectedCiphertextSha256)
1498
+ || this.manifestSha256 === null
1499
+ || !equalBytes(frame.manifestSha256, this.manifestSha256)
1500
+ || this.manifestPresent.some((present) => present !== 1)
1501
+ || !equalBytes(sha256(this.core, this.manifest), frame.manifestSha256)
1502
+ || BigInt(size) !== this.options.expectedCiphertextLength
1503
+ || !equalBytes(digest, this.options.expectedCiphertextSha256))
1504
+ throw integrityFailure();
1505
+ return {
1506
+ kind: 'completed',
1507
+ manifest: this.manifest.slice(),
1508
+ evidence: {
1509
+ ciphertextLength: frame.ciphertextLength,
1510
+ ciphertextSha256: frame.ciphertextSha256.slice(),
1511
+ manifestLength: MANIFEST_LENGTH,
1512
+ manifestSha256: frame.manifestSha256.slice(),
1513
+ blockCount: frame.blockCount,
1514
+ },
1515
+ };
1516
+ }
1517
+ async flushWindow(firstSequence, lastSequence) {
1518
+ let offset = safeNumber(this.checkpoint.nextCiphertextOffset);
1519
+ for (let sequence = firstSequence; sequence <= lastSequence; sequence += 1) {
1520
+ const packet = this.packets.get(sequence);
1521
+ if (!packet || safeNumber(packet.offset) !== offset)
1522
+ throw integrityFailure();
1523
+ await this.blob.write(offset, packet.bytes);
1524
+ offset += packet.bytes.byteLength;
1525
+ }
1526
+ }
1527
+ async hashProspectivePrefix(firstSequence, highestSequence) {
1528
+ const hasher = this.core.createIntegrityHasher();
1529
+ const prefixLength = safeNumber(this.checkpoint.nextCiphertextOffset);
1530
+ for await (const chunk of this.blob.stream(64 * 1024)) {
1531
+ if (hasher.length() + BigInt(chunk.byteLength) > BigInt(prefixLength)) {
1532
+ const remaining = prefixLength - safeNumber(hasher.length());
1533
+ if (remaining > 0)
1534
+ hasher.update(chunk.slice(0, remaining));
1535
+ break;
1536
+ }
1537
+ hasher.update(chunk);
1538
+ }
1539
+ if (hasher.length() !== BigInt(prefixLength))
1540
+ throw integrityFailure();
1541
+ for (let sequence = firstSequence; sequence <= highestSequence; sequence += 1) {
1542
+ const packet = this.packets.get(sequence);
1543
+ if (!packet)
1544
+ break;
1545
+ hasher.update(packet.bytes);
1546
+ }
1547
+ return hasher.sha256Snapshot();
1548
+ }
1549
+ acknowledgement(pending, highestContiguousSequence, nextCiphertextOffset, prefixSha256, checkpointRevision, missingSequences) {
1550
+ return {
1551
+ kind: 'window_ack',
1552
+ flags: 0,
1553
+ transportSessionId: this.options.transportSessionId,
1554
+ windowIndex: pending.boundary.windowIndex,
1555
+ highestContiguousSequence,
1556
+ nextCiphertextOffset,
1557
+ prefixSha256: prefixSha256.slice(),
1558
+ checkpointRevision,
1559
+ missingSequences: [...missingSequences],
1560
+ };
1561
+ }
1562
+ }
1563
+ export function initialEncryptedUploadV2Checkpoint() {
1564
+ return {
1565
+ revision: 0,
1566
+ nextCiphertextOffset: 0n,
1567
+ prefixSha256: hexBytes(EMPTY_SHA256_HEX),
1568
+ highestContiguousSequence: null,
1569
+ };
1570
+ }
1571
+ export function coreCheckpoint(serialNumber, recordingUuid, recordingGeneration, uploadSessionId, ownerRevision, transportSessionId, checkpoint, windowPackets, dataPayloadBytes) {
1572
+ return {
1573
+ serialNumber,
1574
+ recordingUuid: recordingUuid.slice(),
1575
+ recordingGeneration,
1576
+ uploadSessionId: uploadSessionId.slice(),
1577
+ ownerRevision,
1578
+ transportSessionId,
1579
+ checkpointRevision: checkpoint.revision,
1580
+ nextCiphertextOffset: checkpoint.nextCiphertextOffset,
1581
+ prefixSha256: checkpoint.prefixSha256.slice(),
1582
+ windowPackets,
1583
+ dataPayloadBytes,
1584
+ };
1585
+ }
1586
+ function copyRecordingEntry(frame) {
1587
+ return { ...frame, ciphertextSha256: frame.ciphertextSha256.slice() };
1588
+ }
1589
+ function copyNativeCheckpoint(checkpoint) {
1590
+ return { ...checkpoint, prefixSha256: checkpoint.prefixSha256.slice() };
1591
+ }
1592
+ function copyWindow(frame) {
1593
+ return { ...frame, prefixSha256: frame.prefixSha256.slice() };
1594
+ }
1595
+ function copyTransferRequest(request) {
1596
+ return {
1597
+ ...request,
1598
+ authorizationSha256: request.authorizationSha256.slice(),
1599
+ expectedCiphertextSha256: request.expectedCiphertextSha256.slice(),
1600
+ };
1601
+ }
1602
+ function validateTransferRequest(request) {
1603
+ if (request.transportSessionId <= 0n
1604
+ || request.authorizationSha256.byteLength !== 32
1605
+ || request.expectedCiphertextSha256.byteLength !== 32
1606
+ || request.expectedCiphertextLength <= 0n
1607
+ || !isUint32(request.recordingGeneration)
1608
+ || !isUint32(request.expectedCheckpointIntervalBlocks)
1609
+ || request.expectedCheckpointIntervalBlocks === 0
1610
+ || request.windowPackets <= 0
1611
+ || request.windowPackets > 0xffff
1612
+ || request.dataPayloadBytes <= 0
1613
+ || request.dataPayloadBytes > 0xffff)
1614
+ throw protocolFailure();
1615
+ }
1616
+ function validateStartAcknowledgement(request, response) {
1617
+ if (response.uploadSessionUuid !== request.uploadSessionUuid
1618
+ || response.recordingUuid !== request.recordingUuid
1619
+ || response.recordingGeneration !== request.recordingGeneration
1620
+ || response.ciphertextLength !== request.expectedCiphertextLength
1621
+ || !equalBytes(response.ciphertextSha256, request.expectedCiphertextSha256)
1622
+ || response.windowPackets !== request.windowPackets
1623
+ || response.dataPayloadBytes !== request.dataPayloadBytes
1624
+ || response.checkpointIntervalBlocks
1625
+ !== request.expectedCheckpointIntervalBlocks
1626
+ || response.checkpointRevision !== 0
1627
+ || response.nextCiphertextOffset !== 0n
1628
+ || !equalBytes(response.prefixSha256, initialEncryptedUploadV2Checkpoint().prefixSha256))
1629
+ throw identityFailure();
1630
+ }
1631
+ function validateResumeAcknowledgement(request, checkpoint, response) {
1632
+ if (response.uploadSessionUuid !== request.uploadSessionUuid
1633
+ || response.recordingUuid !== request.recordingUuid
1634
+ || response.recordingGeneration !== request.recordingGeneration
1635
+ || response.checkpointRevision !== checkpoint.revision
1636
+ || response.nextCiphertextOffset !== checkpoint.nextCiphertextOffset
1637
+ || !equalBytes(response.prefixSha256, checkpoint.prefixSha256)
1638
+ || response.windowPackets !== request.windowPackets
1639
+ || response.dataPayloadBytes !== request.dataPayloadBytes)
1640
+ throw identityFailure();
1641
+ }
1642
+ function sameWindow(left, right) {
1643
+ return left.transportSessionId === right.transportSessionId
1644
+ && left.windowIndex === right.windowIndex
1645
+ && left.firstSequence === right.firstSequence
1646
+ && left.lastSequence === right.lastSequence
1647
+ && left.nextCiphertextOffset === right.nextCiphertextOffset
1648
+ && left.checkpointRevision === right.checkpointRevision
1649
+ && equalBytes(left.prefixSha256, right.prefixSha256);
1650
+ }
1651
+ function sameCheckpoint(left, right) {
1652
+ return left.revision === right.revision
1653
+ && left.nextCiphertextOffset === right.nextCiphertextOffset
1654
+ && left.highestContiguousSequence === right.highestContiguousSequence
1655
+ && equalBytes(left.prefixSha256, right.prefixSha256);
1656
+ }
1657
+ function overlaps(left, right) {
1658
+ const leftEnd = left.offset + BigInt(left.bytes.byteLength);
1659
+ const rightEnd = right.offset + BigInt(right.bytes.byteLength);
1660
+ return left.offset < rightEnd && right.offset < leftEnd;
1661
+ }
1662
+ async function hashBlobPrefix(core, blob, length) {
1663
+ const hasher = core.createIntegrityHasher();
1664
+ for await (const chunk of blob.stream(64 * 1024)) {
1665
+ const remaining = length - safeNumber(hasher.length());
1666
+ if (remaining <= 0)
1667
+ break;
1668
+ hasher.update(chunk.byteLength <= remaining ? chunk : chunk.slice(0, remaining));
1669
+ }
1670
+ if (hasher.length() !== BigInt(length))
1671
+ throw integrityFailure();
1672
+ return hasher.sha256Snapshot();
1673
+ }
1674
+ function sha256(core, value) {
1675
+ const hasher = core.createIntegrityHasher();
1676
+ hasher.update(value);
1677
+ return hasher.sha256Snapshot();
1678
+ }
1679
+ function hexBytes(value) {
1680
+ return Uint8Array.from(value.match(/../g) ?? [], (byte) => Number.parseInt(byte, 16));
1681
+ }
1682
+ function equalBytes(left, right) {
1683
+ if (left.byteLength !== right.byteLength)
1684
+ return false;
1685
+ let different = 0;
1686
+ for (let index = 0; index < left.byteLength; index += 1) {
1687
+ different |= (left[index] ?? 0) ^ (right[index] ?? 0);
1688
+ }
1689
+ return different === 0;
1690
+ }
1691
+ function equalNumbers(left, right) {
1692
+ return left.length === right.length
1693
+ && left.every((value, index) => value === right[index]);
1694
+ }
1695
+ function boundaryHostEvent(requestId, event) {
1696
+ switch (event.kind) {
1697
+ case 'encrypted_upload_v2_window_staged':
1698
+ return { requestId, ...event };
1699
+ case 'encrypted_upload_v2_transfer_completed':
1700
+ return { requestId, ...event };
1701
+ case 'encrypted_upload_v2_mixed_profile':
1702
+ return { requestId, ...event };
1703
+ case 'encrypted_upload_v2_failed':
1704
+ return { requestId, ...event };
1705
+ }
1706
+ }
1707
+ function coreProtocolError(error) {
1708
+ const normalized = normalizeHostError(error);
1709
+ return {
1710
+ code: normalized.code === 'integrity_failed'
1711
+ ? 'IntegrityFailed'
1712
+ : normalized.code === 'identity_mismatch'
1713
+ ? 'IdentityMismatch'
1714
+ : 'ProtocolRejected',
1715
+ operation: 'TransferRecording',
1716
+ retryable: normalized.retryable,
1717
+ protocol_status: normalized.protocolStatus ?? undefined,
1718
+ detail: 'encrypted_upload_v2_browser_host_failure',
1719
+ };
1720
+ }
1721
+ function sameEvidence(left, right) {
1722
+ return left.ciphertextLength === right.ciphertextLength
1723
+ && equalBytes(left.ciphertextSha256, right.ciphertextSha256)
1724
+ && left.manifestLength === right.manifestLength
1725
+ && equalBytes(left.manifestSha256, right.manifestSha256)
1726
+ && left.blockCount === right.blockCount;
1727
+ }
1728
+ function copyEvidence(evidence) {
1729
+ return {
1730
+ ...evidence,
1731
+ ciphertextSha256: evidence.ciphertextSha256.slice(),
1732
+ manifestSha256: evidence.manifestSha256.slice(),
1733
+ };
1734
+ }
1735
+ function providerEvidence(evidence) {
1736
+ return copyEvidence(evidence);
1737
+ }
1738
+ function copyCoreCheckpoint(checkpoint) {
1739
+ return {
1740
+ ...checkpoint,
1741
+ recordingUuid: checkpoint.recordingUuid.slice(),
1742
+ uploadSessionId: checkpoint.uploadSessionId.slice(),
1743
+ prefixSha256: checkpoint.prefixSha256.slice(),
1744
+ };
1745
+ }
1746
+ function sameCoreCheckpoint(left, right) {
1747
+ return left.serialNumber === right.serialNumber
1748
+ && equalBytes(left.recordingUuid, right.recordingUuid)
1749
+ && left.recordingGeneration === right.recordingGeneration
1750
+ && equalBytes(left.uploadSessionId, right.uploadSessionId)
1751
+ && left.ownerRevision === right.ownerRevision
1752
+ && left.transportSessionId === right.transportSessionId
1753
+ && left.checkpointRevision === right.checkpointRevision
1754
+ && left.nextCiphertextOffset === right.nextCiphertextOffset
1755
+ && equalBytes(left.prefixSha256, right.prefixSha256)
1756
+ && left.windowPackets === right.windowPackets
1757
+ && left.dataPayloadBytes === right.dataPayloadBytes;
1758
+ }
1759
+ function sameOptionalCoreCheckpoint(left, right) {
1760
+ return left === null || right === null
1761
+ ? left === right
1762
+ : sameCoreCheckpoint(left, right);
1763
+ }
1764
+ function sameCoreAndNativeCheckpoint(core, native, state) {
1765
+ return core.serialNumber === state.serialNumber
1766
+ && uuidString(core.recordingUuid) === state.recording.uuid
1767
+ && core.recordingGeneration === state.recording.generation
1768
+ && uuidString(core.uploadSessionId) === state.uploadSessionId
1769
+ && core.ownerRevision === state.ownerRevision
1770
+ && core.transportSessionId === state.transportSessionId
1771
+ && core.checkpointRevision === native.revision
1772
+ && core.nextCiphertextOffset === native.nextCiphertextOffset
1773
+ && equalBytes(core.prefixSha256, native.prefixSha256)
1774
+ && core.windowPackets === state.windowPackets
1775
+ && core.dataPayloadBytes === state.dataPayloadBytes;
1776
+ }
1777
+ function copyPersistedState(state) {
1778
+ return {
1779
+ ...state,
1780
+ recording: {
1781
+ ...state.recording,
1782
+ ciphertextSha256: state.recording.ciphertextSha256.slice(),
1783
+ },
1784
+ coreCheckpoint: state.coreCheckpoint
1785
+ ? copyCoreCheckpoint(state.coreCheckpoint)
1786
+ : null,
1787
+ evidence: state.evidence ? copyEvidence(state.evidence) : null,
1788
+ };
1789
+ }
1790
+ export function parsePersistedEncryptedUploadV2State(value) {
1791
+ if (!isRecord(value) || value.schemaVersion !== 1)
1792
+ throw integrityFailure();
1793
+ const recording = value.recording;
1794
+ const coreCheckpointValue = value.coreCheckpoint;
1795
+ const evidenceValue = value.evidence;
1796
+ if (!isRecord(recording)
1797
+ || typeof recording.uuid !== 'string'
1798
+ || !isUuid(recording.uuid)
1799
+ || !isUint32(recording.generation)
1800
+ || typeof recording.storageFormat !== 'number'
1801
+ || !Number.isInteger(recording.storageFormat)
1802
+ || recording.storageFormat <= 0
1803
+ || recording.storageFormat > 0xff
1804
+ || typeof recording.ciphertextLength !== 'bigint'
1805
+ || recording.ciphertextLength <= 0n
1806
+ || recording.ciphertextLength > BigInt(Number.MAX_SAFE_INTEGER)
1807
+ || !(recording.ciphertextSha256 instanceof Uint8Array)
1808
+ || recording.ciphertextSha256.byteLength !== 32
1809
+ || typeof value.operationId !== 'string'
1810
+ || value.operationId.length === 0
1811
+ || typeof value.serialNumber !== 'string'
1812
+ || value.serialNumber.length === 0
1813
+ || typeof value.materialId !== 'string'
1814
+ || value.materialId.length === 0
1815
+ || typeof value.recordingId !== 'string'
1816
+ || value.recordingId.length === 0
1817
+ || typeof value.uploadSessionId !== 'string'
1818
+ || !isUuid(value.uploadSessionId)
1819
+ || !isUint32(value.ownerRevision)
1820
+ || value.ownerRevision === 0
1821
+ || !isUploadPolicy(value.policy)
1822
+ || typeof value.transportSessionId !== 'bigint'
1823
+ || value.transportSessionId <= 0n
1824
+ || typeof value.sinkId !== 'string'
1825
+ || value.sinkId.length === 0
1826
+ || !isPositiveU16(value.windowPackets)
1827
+ || !isPositiveU16(value.dataPayloadBytes)
1828
+ || !isPositiveU16(value.maximumSignedBlobBytes)
1829
+ || !isPositiveU16(value.maximumMissingSequences)
1830
+ || !isUint32(value.checkpointIntervalBlocks)
1831
+ || value.checkpointIntervalBlocks === 0
1832
+ || typeof value.capabilitySha256Hex !== 'string'
1833
+ || !/^[0-9a-f]{64}$/.test(value.capabilitySha256Hex)
1834
+ || (value.highestContiguousSequence !== null
1835
+ && !isUint32(value.highestContiguousSequence)))
1836
+ throw integrityFailure();
1837
+ if (coreCheckpointValue !== null)
1838
+ validateCoreCheckpoint(coreCheckpointValue);
1839
+ if (evidenceValue !== null)
1840
+ validateEvidence(evidenceValue);
1841
+ const state = {
1842
+ schemaVersion: 1,
1843
+ operationId: value.operationId,
1844
+ serialNumber: value.serialNumber,
1845
+ recording: {
1846
+ uuid: recording.uuid,
1847
+ generation: recording.generation,
1848
+ storageFormat: recording.storageFormat,
1849
+ ciphertextLength: recording.ciphertextLength,
1850
+ ciphertextSha256: recording.ciphertextSha256.slice(),
1851
+ },
1852
+ materialId: value.materialId,
1853
+ recordingId: value.recordingId,
1854
+ uploadSessionId: value.uploadSessionId,
1855
+ ownerRevision: value.ownerRevision,
1856
+ policy: value.policy,
1857
+ transportSessionId: value.transportSessionId,
1858
+ sinkId: value.sinkId,
1859
+ windowPackets: value.windowPackets,
1860
+ dataPayloadBytes: value.dataPayloadBytes,
1861
+ maximumSignedBlobBytes: value.maximumSignedBlobBytes,
1862
+ maximumMissingSequences: value.maximumMissingSequences,
1863
+ checkpointIntervalBlocks: value.checkpointIntervalBlocks,
1864
+ capabilitySha256Hex: value.capabilitySha256Hex,
1865
+ coreCheckpoint: coreCheckpointValue === null
1866
+ ? null
1867
+ : copyCoreCheckpoint(coreCheckpointValue),
1868
+ highestContiguousSequence: value.highestContiguousSequence,
1869
+ evidence: evidenceValue === null
1870
+ ? null
1871
+ : copyEvidence(evidenceValue),
1872
+ };
1873
+ validatePersistedStateBindings(state);
1874
+ return state;
1875
+ }
1876
+ function validatePersistedStateBindings(state) {
1877
+ const checkpoint = state.coreCheckpoint;
1878
+ if (!checkpoint) {
1879
+ if (state.highestContiguousSequence !== null) {
1880
+ throw integrityFailure();
1881
+ }
1882
+ if (state.evidence
1883
+ && (state.evidence.ciphertextLength !== state.recording.ciphertextLength
1884
+ || !equalBytes(state.evidence.ciphertextSha256, state.recording.ciphertextSha256)))
1885
+ throw integrityFailure();
1886
+ return;
1887
+ }
1888
+ if (checkpoint.serialNumber !== state.serialNumber
1889
+ || uuidString(checkpoint.recordingUuid) !== state.recording.uuid
1890
+ || checkpoint.recordingGeneration !== state.recording.generation
1891
+ || uuidString(checkpoint.uploadSessionId) !== state.uploadSessionId
1892
+ || checkpoint.ownerRevision !== state.ownerRevision
1893
+ || checkpoint.transportSessionId !== state.transportSessionId
1894
+ || checkpoint.checkpointRevision === 0
1895
+ || checkpoint.nextCiphertextOffset <= 0n
1896
+ || checkpoint.nextCiphertextOffset > state.recording.ciphertextLength
1897
+ || checkpoint.windowPackets !== state.windowPackets
1898
+ || checkpoint.dataPayloadBytes !== state.dataPayloadBytes
1899
+ || state.highestContiguousSequence === null)
1900
+ throw integrityFailure();
1901
+ const evidence = state.evidence;
1902
+ if (evidence
1903
+ && (evidence.ciphertextLength !== state.recording.ciphertextLength
1904
+ || !equalBytes(evidence.ciphertextSha256, state.recording.ciphertextSha256)
1905
+ || checkpoint.nextCiphertextOffset !== state.recording.ciphertextLength
1906
+ || !equalBytes(checkpoint.prefixSha256, state.recording.ciphertextSha256)))
1907
+ throw integrityFailure();
1908
+ }
1909
+ function validateCoreCheckpoint(value) {
1910
+ if (!isRecord(value)
1911
+ || typeof value.serialNumber !== 'string'
1912
+ || !(value.recordingUuid instanceof Uint8Array)
1913
+ || value.recordingUuid.byteLength !== 16
1914
+ || !isUint32(value.recordingGeneration)
1915
+ || !(value.uploadSessionId instanceof Uint8Array)
1916
+ || value.uploadSessionId.byteLength !== 16
1917
+ || !isUint32(value.ownerRevision)
1918
+ || typeof value.transportSessionId !== 'bigint'
1919
+ || !isUint32(value.checkpointRevision)
1920
+ || typeof value.nextCiphertextOffset !== 'bigint'
1921
+ || !(value.prefixSha256 instanceof Uint8Array)
1922
+ || value.prefixSha256.byteLength !== 32
1923
+ || !isPositiveU16(value.windowPackets)
1924
+ || !isPositiveU16(value.dataPayloadBytes))
1925
+ throw integrityFailure();
1926
+ }
1927
+ function validateEvidence(value) {
1928
+ if (!isRecord(value)
1929
+ || typeof value.ciphertextLength !== 'bigint'
1930
+ || !(value.ciphertextSha256 instanceof Uint8Array)
1931
+ || value.ciphertextSha256.byteLength !== 32
1932
+ || value.manifestLength !== MANIFEST_LENGTH
1933
+ || !(value.manifestSha256 instanceof Uint8Array)
1934
+ || value.manifestSha256.byteLength !== 32
1935
+ || !isUint32(value.blockCount)
1936
+ || value.blockCount === 0)
1937
+ throw integrityFailure();
1938
+ }
1939
+ function validateUploadRequest(request) {
1940
+ if (!isRecord(request) || request.method !== 'PUT') {
1941
+ throw new BotaSDKError('upload_failed', 'upload');
1942
+ }
1943
+ if (typeof request.url !== 'string') {
1944
+ throw new BotaSDKError('upload_failed', 'upload');
1945
+ }
1946
+ let url;
1947
+ try {
1948
+ url = new URL(request.url);
1949
+ }
1950
+ catch {
1951
+ throw new BotaSDKError('upload_failed', 'upload');
1952
+ }
1953
+ if (url.protocol !== 'https:' || !isRecord(request.headers)) {
1954
+ throw new BotaSDKError('upload_failed', 'upload');
1955
+ }
1956
+ for (const [name, value] of Object.entries(request.headers)) {
1957
+ if (name.length === 0
1958
+ || typeof value !== 'string'
1959
+ || /[\r\n]/.test(name)
1960
+ || /[\r\n]/.test(value))
1961
+ throw new BotaSDKError('upload_failed', 'upload');
1962
+ }
1963
+ }
1964
+ function blobReadableStream(blob, signal) {
1965
+ const iterator = blob.stream(64 * 1024)[Symbol.asyncIterator]();
1966
+ return new ReadableStream({
1967
+ pull: async (controller) => {
1968
+ try {
1969
+ if (signal.aborted)
1970
+ throw cancelled();
1971
+ const next = await iterator.next();
1972
+ if (next.done)
1973
+ controller.close();
1974
+ else
1975
+ controller.enqueue(next.value);
1976
+ }
1977
+ catch (error) {
1978
+ controller.error(error);
1979
+ }
1980
+ },
1981
+ cancel: async () => {
1982
+ await iterator.return?.();
1983
+ },
1984
+ });
1985
+ }
1986
+ function uuidBytes(value) {
1987
+ if (!isUuid(value)) {
1988
+ throw protocolFailure();
1989
+ }
1990
+ return hexBytes(value.replaceAll('-', ''));
1991
+ }
1992
+ function isUuid(value) {
1993
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1994
+ }
1995
+ function uuidString(value) {
1996
+ if (value.byteLength !== 16)
1997
+ throw protocolFailure();
1998
+ const hex = hexString(value);
1999
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2000
+ }
2001
+ function hexString(value) {
2002
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
2003
+ }
2004
+ function isRecord(value) {
2005
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
2006
+ }
2007
+ function isPositiveU16(value) {
2008
+ return typeof value === 'number'
2009
+ && Number.isInteger(value)
2010
+ && value > 0
2011
+ && value <= 0xffff;
2012
+ }
2013
+ function isUploadPolicy(value) {
2014
+ return value === 'legacy_allowed'
2015
+ || value === 'v2_preferred'
2016
+ || value === 'v2_required';
2017
+ }
2018
+ function safeNumber(value) {
2019
+ if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
2020
+ throw protocolFailure();
2021
+ }
2022
+ return Number(value);
2023
+ }
2024
+ function isUint32(value) {
2025
+ return typeof value === 'number'
2026
+ && Number.isInteger(value)
2027
+ && value >= 0
2028
+ && value <= 0xffffffff;
2029
+ }
2030
+ function throwIfCancelled(cancelledValue, signal) {
2031
+ if (cancelledValue || signal?.aborted)
2032
+ throw cancelled();
2033
+ }
2034
+ function abortable(promise, signal) {
2035
+ if (!signal)
2036
+ return promise;
2037
+ if (signal.aborted)
2038
+ return Promise.reject(cancelled());
2039
+ return new Promise((resolve, reject) => {
2040
+ const abort = () => reject(cancelled());
2041
+ signal.addEventListener('abort', abort, { once: true });
2042
+ void promise.then((value) => {
2043
+ signal.removeEventListener('abort', abort);
2044
+ resolve(value);
2045
+ }, (error) => {
2046
+ signal.removeEventListener('abort', abort);
2047
+ reject(error);
2048
+ });
2049
+ });
2050
+ }
2051
+ class SerializedGattWrites {
2052
+ tail = Promise.resolve();
2053
+ run(write) {
2054
+ const result = this.tail.then(write);
2055
+ this.tail = result.then(() => undefined, () => undefined);
2056
+ return result;
2057
+ }
2058
+ }
2059
+ function settle(promise) {
2060
+ return promise.then(() => ({ error: null }), (error) => ({ error }));
2061
+ }
2062
+ async function boundedSettlement(settlement, timeoutMs) {
2063
+ let timer = null;
2064
+ const timeout = new Promise((resolve) => {
2065
+ timer = setTimeout(() => resolve(null), timeoutMs);
2066
+ });
2067
+ const result = await Promise.race([settlement, timeout]);
2068
+ if (timer !== null)
2069
+ clearTimeout(timer);
2070
+ return result;
2071
+ }
2072
+ function rejectAfter(promise, timeoutMs, error) {
2073
+ return new Promise((resolve, reject) => {
2074
+ const timer = setTimeout(() => reject(error()), timeoutMs);
2075
+ void promise.then((value) => {
2076
+ clearTimeout(timer);
2077
+ resolve(value);
2078
+ }, (reason) => {
2079
+ clearTimeout(timer);
2080
+ reject(reason);
2081
+ });
2082
+ });
2083
+ }
2084
+ function positiveTimeout(value, fallback) {
2085
+ return value !== undefined
2086
+ && Number.isFinite(value)
2087
+ && value > 0
2088
+ ? value
2089
+ : fallback;
2090
+ }
2091
+ function deferred() {
2092
+ let resolve;
2093
+ let reject;
2094
+ const promise = new Promise((resolvePromise, rejectPromise) => {
2095
+ resolve = resolvePromise;
2096
+ reject = rejectPromise;
2097
+ });
2098
+ return { promise, resolve, reject };
2099
+ }
2100
+ function normalizeHostError(error) {
2101
+ if (error instanceof BotaSDKError)
2102
+ return error;
2103
+ if (error instanceof BrowserStorageError) {
2104
+ return new BotaSDKError(error.code, 'transfer_recording');
2105
+ }
2106
+ if (error instanceof BrowserTransportError) {
2107
+ const code = error.code === 'disconnected'
2108
+ ? 'device_disconnected'
2109
+ : error.code === 'permission_denied'
2110
+ ? 'permission_denied'
2111
+ : 'bluetooth_unavailable';
2112
+ return new BotaSDKError(code, 'transfer_recording');
2113
+ }
2114
+ return protocolFailure(error);
2115
+ }
2116
+ function protocolFailure(cause) {
2117
+ return new BotaSDKError('protocol_error', 'transfer_recording', { cause });
2118
+ }
2119
+ function identityFailure() {
2120
+ return new BotaSDKError('identity_mismatch', 'transfer_recording');
2121
+ }
2122
+ function integrityFailure() {
2123
+ return new BotaSDKError('integrity_failed', 'transfer_recording');
2124
+ }
2125
+ function cancelled() {
2126
+ return new BotaSDKError('cancelled', 'transfer_recording');
2127
+ }
2128
+ function operationInProgress() {
2129
+ return new BotaSDKError('operation_in_progress', 'transfer_recording');
2130
+ }
2131
+ function ownershipUnknown(cause) {
2132
+ return new BotaSDKError('integrity_failed', 'transfer_recording', { cause });
2133
+ }
2134
+ function signedResultTimeout() {
2135
+ return new BotaSDKError('connection_failed', 'transfer_recording', { retryable: true });
2136
+ }