@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.
- package/LICENSE +21 -0
- package/README.md +517 -0
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.js +21 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +153 -0
- package/dist/controlManager.d.ts +38 -0
- package/dist/controlManager.js +344 -0
- package/dist/core.d.ts +698 -0
- package/dist/core.js +13 -0
- package/dist/deviceManager.d.ts +52 -0
- package/dist/deviceManager.js +491 -0
- package/dist/encryptedUploadV2Host.d.ts +239 -0
- package/dist/encryptedUploadV2Host.js +2136 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +230 -0
- package/dist/gatt.d.ts +39 -0
- package/dist/gatt.js +57 -0
- package/dist/generated/bota_device_sdk_core.d.ts +202 -0
- package/dist/generated/bota_device_sdk_core.js +1025 -0
- package/dist/generated/bota_device_sdk_core_bg.wasm +0 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -0
- package/dist/indexedDbWorkflowStore.d.ts +52 -0
- package/dist/indexedDbWorkflowStore.js +763 -0
- package/dist/logManager.d.ts +24 -0
- package/dist/logManager.js +205 -0
- package/dist/models.d.ts +217 -0
- package/dist/models.js +1 -0
- package/dist/opfsBlobStore.d.ts +12 -0
- package/dist/opfsBlobStore.js +250 -0
- package/dist/otaManager.d.ts +49 -0
- package/dist/otaManager.js +951 -0
- package/dist/providerCancellation.d.ts +2 -0
- package/dist/providerCancellation.js +23 -0
- package/dist/providers.d.ts +138 -0
- package/dist/providers.js +1 -0
- package/dist/provisioningManager.d.ts +48 -0
- package/dist/provisioningManager.js +753 -0
- package/dist/recordingManager.d.ts +53 -0
- package/dist/recordingManager.js +1397 -0
- package/dist/storage.d.ts +103 -0
- package/dist/storage.js +60 -0
- package/dist/transport.d.ts +33 -0
- package/dist/transport.js +8 -0
- package/dist/wasmCore.d.ts +3 -0
- package/dist/wasmCore.js +1651 -0
- package/dist/webBluetoothTransport.d.ts +23 -0
- package/dist/webBluetoothTransport.js +369 -0
- package/dist/wifiManager.d.ts +54 -0
- package/dist/wifiManager.js +528 -0
- package/dist/workflowRuntime.d.ts +115 -0
- package/dist/workflowRuntime.js +1508 -0
- 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
|
+
}
|