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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +517 -0
  3. package/dist/capabilities.d.ts +8 -0
  4. package/dist/capabilities.js +21 -0
  5. package/dist/client.d.ts +39 -0
  6. package/dist/client.js +153 -0
  7. package/dist/controlManager.d.ts +38 -0
  8. package/dist/controlManager.js +344 -0
  9. package/dist/core.d.ts +698 -0
  10. package/dist/core.js +13 -0
  11. package/dist/deviceManager.d.ts +52 -0
  12. package/dist/deviceManager.js +491 -0
  13. package/dist/encryptedUploadV2Host.d.ts +239 -0
  14. package/dist/encryptedUploadV2Host.js +2136 -0
  15. package/dist/errors.d.ts +24 -0
  16. package/dist/errors.js +230 -0
  17. package/dist/gatt.d.ts +39 -0
  18. package/dist/gatt.js +57 -0
  19. package/dist/generated/bota_device_sdk_core.d.ts +202 -0
  20. package/dist/generated/bota_device_sdk_core.js +1025 -0
  21. package/dist/generated/bota_device_sdk_core_bg.wasm +0 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.js +2 -0
  24. package/dist/indexedDbWorkflowStore.d.ts +52 -0
  25. package/dist/indexedDbWorkflowStore.js +763 -0
  26. package/dist/logManager.d.ts +24 -0
  27. package/dist/logManager.js +205 -0
  28. package/dist/models.d.ts +217 -0
  29. package/dist/models.js +1 -0
  30. package/dist/opfsBlobStore.d.ts +12 -0
  31. package/dist/opfsBlobStore.js +250 -0
  32. package/dist/otaManager.d.ts +49 -0
  33. package/dist/otaManager.js +951 -0
  34. package/dist/providerCancellation.d.ts +2 -0
  35. package/dist/providerCancellation.js +23 -0
  36. package/dist/providers.d.ts +138 -0
  37. package/dist/providers.js +1 -0
  38. package/dist/provisioningManager.d.ts +48 -0
  39. package/dist/provisioningManager.js +753 -0
  40. package/dist/recordingManager.d.ts +53 -0
  41. package/dist/recordingManager.js +1397 -0
  42. package/dist/storage.d.ts +103 -0
  43. package/dist/storage.js +60 -0
  44. package/dist/transport.d.ts +33 -0
  45. package/dist/transport.js +8 -0
  46. package/dist/wasmCore.d.ts +3 -0
  47. package/dist/wasmCore.js +1651 -0
  48. package/dist/webBluetoothTransport.d.ts +23 -0
  49. package/dist/webBluetoothTransport.js +369 -0
  50. package/dist/wifiManager.d.ts +54 -0
  51. package/dist/wifiManager.js +528 -0
  52. package/dist/workflowRuntime.d.ts +115 -0
  53. package/dist/workflowRuntime.js +1508 -0
  54. package/package.json +46 -0
@@ -0,0 +1,52 @@
1
+ import type { CoreBridge } from './core.ts';
2
+ import { type BotaOperation } from './errors.ts';
3
+ import type { BrowserCapabilities, ConnectOptions, ConnectedDevice, DeviceSnapshot, ReconnectOptions } from './models.ts';
4
+ import { type BrowserSdkStorage } from './storage.ts';
5
+ import { type BrowserBluetoothTransport } from './transport.ts';
6
+ import { BrowserWorkflowRuntime, type WorkflowResult } from './workflowRuntime.ts';
7
+ interface DeviceManagerOptions {
8
+ runtime?: BrowserWorkflowRuntime;
9
+ storage?: BrowserSdkStorage | null;
10
+ }
11
+ export declare class DeviceManager {
12
+ private readonly core;
13
+ private readonly transport;
14
+ private readonly runtime;
15
+ private readonly storage;
16
+ private readonly capabilities;
17
+ private readonly persistenceHost;
18
+ private activeDevice;
19
+ private verifiedDevice;
20
+ private removeDisconnectListener;
21
+ private operationActive;
22
+ private connectionStartup;
23
+ private destroyed;
24
+ private destroyPromise;
25
+ constructor(core: CoreBridge, transport: BrowserBluetoothTransport, options?: DeviceManagerOptions);
26
+ get isSupported(): boolean;
27
+ get connectedDevice(): ConnectedDevice | null;
28
+ getCapabilities(): BrowserCapabilities;
29
+ adoptWorkflowConnection(result: WorkflowResult, expectedSerialNumber: string): ConnectedDevice;
30
+ connect(options: ConnectOptions): Promise<ConnectedDevice>;
31
+ reconnect(options: ReconnectOptions): Promise<ConnectedDevice>;
32
+ disconnect(): Promise<void>;
33
+ readSnapshot(): Promise<DeviceSnapshot>;
34
+ private verifyActiveSerialInternal;
35
+ destroy(): Promise<void>;
36
+ private destroyAfterInternal;
37
+ private validateConnectionRequest;
38
+ private claimConnectionStart;
39
+ private finishConnectionStart;
40
+ private publishConnectedDevice;
41
+ private installActiveDevice;
42
+ private cleanupFailedConnection;
43
+ private disconnectForIdentityMismatch;
44
+ private clearConnection;
45
+ private read;
46
+ private readRequiredText;
47
+ private readOptionalText;
48
+ private readCapabilities;
49
+ }
50
+ export declare function verifyActiveDeviceSerial(manager: DeviceManager, operation: BotaOperation): Promise<ConnectedDevice>;
51
+ export declare function destroyDeviceManagerAfter(manager: DeviceManager, priorCleanup: Promise<unknown>): Promise<void>;
52
+ export {};
@@ -0,0 +1,491 @@
1
+ import { detectBrowserCapabilities } from "./capabilities.js";
2
+ import { BotaSDKError, normalizeCoreError } from "./errors.js";
3
+ import { BOTA_CONTROL_SERVICE, BOTA_STORAGE_SERVICE, DEVICE_INFORMATION_SERVICE, DEVICE_STATUS_CHARACTERISTIC, FIRMWARE_REVISION_CHARACTERISTIC, HARDWARE_REVISION_CHARACTERISTIC, MODEL_NUMBER_CHARACTERISTIC, SERIAL_NUMBER_CHARACTERISTIC, STORAGE_TRANSFER_CAPABILITIES_V2_CHARACTERISTIC, } from "./gatt.js";
4
+ import { BrowserStorageError, } from "./storage.js";
5
+ import { BrowserTransportError, } from "./transport.js";
6
+ import { BrowserWorkflowRuntime, createBrowserPersistenceHost, } from "./workflowRuntime.js";
7
+ const SERIAL_PATTERN = /^[A-Za-z0-9]{1,64}$/;
8
+ const CONNECTION_TIMEOUT_MS = 15000n;
9
+ export class DeviceManager {
10
+ core;
11
+ transport;
12
+ runtime;
13
+ storage;
14
+ capabilities;
15
+ persistenceHost;
16
+ activeDevice = null;
17
+ verifiedDevice = null;
18
+ removeDisconnectListener = null;
19
+ operationActive = false;
20
+ connectionStartup = null;
21
+ destroyed = false;
22
+ destroyPromise = null;
23
+ constructor(core, transport, options = {}) {
24
+ this.core = core;
25
+ this.transport = transport;
26
+ this.runtime = options.runtime ?? new BrowserWorkflowRuntime(core, transport);
27
+ this.storage = options.storage ?? null;
28
+ this.capabilities = detectBrowserCapabilities(transport);
29
+ this.persistenceHost = createBrowserPersistenceHost(this.storage);
30
+ }
31
+ get isSupported() {
32
+ return this.transport.isSupported;
33
+ }
34
+ get connectedDevice() {
35
+ return this.verifiedDevice;
36
+ }
37
+ getCapabilities() {
38
+ return this.capabilities;
39
+ }
40
+ adoptWorkflowConnection(result, expectedSerialNumber) {
41
+ return this.publishConnectedDevice(result, expectedSerialNumber, 'update_firmware');
42
+ }
43
+ async connect(options) {
44
+ this.validateConnectionRequest(options.expectedSerialNumber, 'connect');
45
+ if (!this.isSupported) {
46
+ throw new BotaSDKError('unsupported_browser', 'connect');
47
+ }
48
+ this.claimConnectionStart('connect');
49
+ let selected = null;
50
+ try {
51
+ const picked = await this.transport.requestDevice().catch((error) => {
52
+ throw pickerError(error);
53
+ });
54
+ selected = picked;
55
+ if (this.destroyed)
56
+ throw new BotaSDKError('cancelled', 'connect');
57
+ this.runtime.registerDevice(picked);
58
+ this.installActiveDevice(picked);
59
+ const cancellationId = randomCancellationId();
60
+ const result = await this.runtime.run(operationId('connect', cancellationId), cancellationId, () => this.core.startExactConnection({
61
+ expectedSerialNumber: options.expectedSerialNumber,
62
+ peripheralId: picked.id,
63
+ name: picked.name,
64
+ cancellationId,
65
+ }), { persistence: this.persistenceHost });
66
+ return this.publishConnectedDevice(result, options.expectedSerialNumber, 'connect');
67
+ }
68
+ catch (error) {
69
+ const lifecycleCancelled = this.destroyed;
70
+ if (lifecycleCancelled && this.activeDevice) {
71
+ await this.runtime.waitForPendingConnection(this.activeDevice.id);
72
+ }
73
+ await this.cleanupFailedConnection(this.activeDevice ? null : selected);
74
+ if (lifecycleCancelled) {
75
+ throw new BotaSDKError('cancelled', 'connect', { cause: error });
76
+ }
77
+ throw normalizeManagerError(error, 'connect');
78
+ }
79
+ finally {
80
+ this.finishConnectionStart();
81
+ }
82
+ }
83
+ async reconnect(options) {
84
+ this.validateConnectionRequest(options.expectedSerialNumber, 'reconnect');
85
+ if (!this.isSupported) {
86
+ throw new BotaSDKError('unsupported_browser', 'reconnect');
87
+ }
88
+ if (!this.transport.supportsAuthorizedDevices) {
89
+ throw new BotaSDKError('picker_required', 'reconnect');
90
+ }
91
+ if (!this.storage)
92
+ throw new BotaSDKError('picker_required', 'reconnect');
93
+ this.claimConnectionStart('reconnect');
94
+ let attemptedDeviceId = null;
95
+ try {
96
+ const hint = await this.storage.loadVerifiedDevice(options.expectedSerialNumber);
97
+ if (!hint)
98
+ throw new BotaSDKError('picker_required', 'reconnect');
99
+ if (this.destroyed)
100
+ throw new BotaSDKError('cancelled', 'reconnect');
101
+ const hintedDevice = {
102
+ id: hint.browserDeviceId,
103
+ name: hint.name,
104
+ };
105
+ attemptedDeviceId = hintedDevice.id;
106
+ this.activeDevice = hintedDevice;
107
+ this.runtime.registerDevice(hintedDevice);
108
+ const cancellationId = randomCancellationId();
109
+ const result = await this.runtime.run(operationId('reconnect', cancellationId), cancellationId, () => this.core.startReconnect({
110
+ expectedSerialNumber: options.expectedSerialNumber,
111
+ hint: {
112
+ storedPeripheralId: hint.browserDeviceId,
113
+ advertisedAddress: null,
114
+ storedName: hint.name,
115
+ scanTimeoutMs: CONNECTION_TIMEOUT_MS,
116
+ connectionTimeoutMs: CONNECTION_TIMEOUT_MS,
117
+ },
118
+ cancellationId,
119
+ }), { persistence: this.persistenceHost });
120
+ return this.publishConnectedDevice(result, options.expectedSerialNumber, 'reconnect');
121
+ }
122
+ catch (error) {
123
+ const lifecycleCancelled = this.destroyed;
124
+ if (lifecycleCancelled && attemptedDeviceId) {
125
+ await this.runtime.waitForPendingConnection(attemptedDeviceId);
126
+ }
127
+ await this.cleanupFailedConnection();
128
+ if (lifecycleCancelled) {
129
+ throw new BotaSDKError('cancelled', 'reconnect', { cause: error });
130
+ }
131
+ throw normalizeManagerError(error, 'reconnect');
132
+ }
133
+ finally {
134
+ this.finishConnectionStart();
135
+ }
136
+ }
137
+ async disconnect() {
138
+ if (this.destroyed)
139
+ throw new BotaSDKError('cancelled', 'disconnect');
140
+ if (this.operationActive) {
141
+ throw new BotaSDKError('operation_in_progress', 'disconnect');
142
+ }
143
+ const device = this.runtime.connectedDeviceHandle ?? this.activeDevice;
144
+ if (!device) {
145
+ this.clearConnection();
146
+ return;
147
+ }
148
+ try {
149
+ await this.runtime.runExclusive('disconnect', async (signal) => {
150
+ this.runtime.markDeviceDisconnected(device.id);
151
+ this.clearConnection();
152
+ await awaitDirectStep(this.transport.disconnect(device), signal, 'disconnect');
153
+ });
154
+ }
155
+ catch (error) {
156
+ throw normalizeManagerError(error, 'disconnect');
157
+ }
158
+ }
159
+ async readSnapshot() {
160
+ if (this.destroyed) {
161
+ throw new BotaSDKError('cancelled', 'read_snapshot');
162
+ }
163
+ if (this.operationActive) {
164
+ throw new BotaSDKError('operation_in_progress', 'read_snapshot');
165
+ }
166
+ const device = this.activeDevice;
167
+ const connected = this.verifiedDevice;
168
+ if (!device
169
+ || !connected
170
+ || this.runtime.connectedDeviceHandle?.id !== device.id) {
171
+ throw new BotaSDKError('device_disconnected', 'read_snapshot');
172
+ }
173
+ try {
174
+ return await this.runtime.runExclusive('read_snapshot', async (signal) => {
175
+ const serialNumber = await this.readRequiredText(device, DEVICE_INFORMATION_SERVICE, SERIAL_NUMBER_CHARACTERISTIC, signal);
176
+ if (serialNumber !== connected.serialNumber) {
177
+ await this.disconnectForIdentityMismatch(device);
178
+ throw new BotaSDKError('identity_mismatch', 'read_snapshot');
179
+ }
180
+ const modelNumber = await this.readOptionalText(device, DEVICE_INFORMATION_SERVICE, MODEL_NUMBER_CHARACTERISTIC, signal);
181
+ const hardwareRevision = await this.readOptionalText(device, DEVICE_INFORMATION_SERVICE, HARDWARE_REVISION_CHARACTERISTIC, signal);
182
+ const firmwareRevision = await this.readOptionalText(device, DEVICE_INFORMATION_SERVICE, FIRMWARE_REVISION_CHARACTERISTIC, signal);
183
+ const statusBytes = await this.read(device, BOTA_CONTROL_SERVICE, DEVICE_STATUS_CHARACTERISTIC, signal);
184
+ const status = this.core.decodeDeviceStatus(statusBytes);
185
+ const encryptedUploadV2 = await this.readCapabilities(device, signal);
186
+ return {
187
+ identity: {
188
+ serialNumber,
189
+ modelNumber,
190
+ hardwareRevision,
191
+ firmwareRevision,
192
+ },
193
+ status,
194
+ capabilities: { encryptedUploadV2 },
195
+ capturedAt: new Date(),
196
+ };
197
+ });
198
+ }
199
+ catch (error) {
200
+ throw snapshotError(error);
201
+ }
202
+ }
203
+ async verifyActiveSerialInternal(operation) {
204
+ if (this.destroyed)
205
+ throw new BotaSDKError('cancelled', operation);
206
+ const connected = this.verifiedDevice;
207
+ const device = this.activeDevice;
208
+ if (!connected
209
+ || !device
210
+ || connected.id !== device.id
211
+ || this.runtime.connectedDeviceHandle?.id !== device.id) {
212
+ throw new BotaSDKError('device_disconnected', operation);
213
+ }
214
+ try {
215
+ return await this.runtime.runExclusive(operation, async (signal) => {
216
+ const encoded = await awaitDirectStep(this.transport.read(device, DEVICE_INFORMATION_SERVICE, SERIAL_NUMBER_CHARACTERISTIC), signal, operation);
217
+ const serialNumber = decodeRequiredText(encoded, operation);
218
+ if (serialNumber !== connected.serialNumber) {
219
+ await this.disconnectForIdentityMismatch(device);
220
+ throw new BotaSDKError('identity_mismatch', operation);
221
+ }
222
+ return connected;
223
+ });
224
+ }
225
+ catch (error) {
226
+ throw normalizeManagerError(error, operation);
227
+ }
228
+ }
229
+ destroy() {
230
+ return this.destroyAfterInternal(Promise.resolve());
231
+ }
232
+ destroyAfterInternal(priorCleanup) {
233
+ if (this.destroyPromise)
234
+ return this.destroyPromise;
235
+ this.destroyed = true;
236
+ const connectionSettlement = this.connectionStartup?.settled
237
+ ?? Promise.resolve();
238
+ this.destroyPromise = (async () => {
239
+ const failures = [];
240
+ await collectTeardownFailure(() => priorCleanup, failures);
241
+ await collectTeardownFailure(() => this.runtime.destroy(), failures);
242
+ await collectTeardownFailure(() => connectionSettlement, failures);
243
+ const connected = this.runtime.connectedDeviceHandle ?? this.activeDevice;
244
+ if (connected) {
245
+ this.runtime.markDeviceDisconnected(connected.id);
246
+ this.clearConnection();
247
+ await collectTeardownFailure(() => this.transport.disconnect(connected), failures);
248
+ }
249
+ else {
250
+ this.clearConnection();
251
+ }
252
+ const failure = failures[0];
253
+ if (failure)
254
+ throw normalizeManagerError(failure, 'disconnect');
255
+ })();
256
+ return this.destroyPromise;
257
+ }
258
+ validateConnectionRequest(expectedSerialNumber, operation) {
259
+ if (this.destroyed)
260
+ throw new BotaSDKError('cancelled', operation);
261
+ if (!SERIAL_PATTERN.test(expectedSerialNumber)) {
262
+ throw new BotaSDKError('invalid_input', operation);
263
+ }
264
+ }
265
+ claimConnectionStart(operation) {
266
+ if (this.operationActive || this.activeDevice) {
267
+ throw new BotaSDKError('operation_in_progress', operation);
268
+ }
269
+ this.operationActive = true;
270
+ let settle;
271
+ const settled = new Promise((resolve) => {
272
+ settle = resolve;
273
+ });
274
+ this.connectionStartup = { settled, settle };
275
+ }
276
+ finishConnectionStart() {
277
+ this.operationActive = false;
278
+ const startup = this.connectionStartup;
279
+ this.connectionStartup = null;
280
+ startup?.settle();
281
+ }
282
+ publishConnectedDevice(result, expectedSerialNumber, operation) {
283
+ if (this.destroyed)
284
+ throw new BotaSDKError('cancelled', operation);
285
+ const established = [...result.notifications].reverse().find((notification) => notification.kind === 'connection_established');
286
+ if (!established || established.kind !== 'connection_established') {
287
+ throw new BotaSDKError('internal_error', operation);
288
+ }
289
+ if (established.serialNumber !== expectedSerialNumber) {
290
+ throw new BotaSDKError('identity_mismatch', operation);
291
+ }
292
+ const device = this.runtime.registeredDevice(established.candidate.peripheralId);
293
+ if (!device || this.runtime.connectedDeviceHandle?.id !== device.id) {
294
+ throw new BotaSDKError('internal_error', operation);
295
+ }
296
+ this.installActiveDevice(device);
297
+ this.verifiedDevice = {
298
+ id: device.id,
299
+ name: device.name,
300
+ serialNumber: established.serialNumber,
301
+ };
302
+ return this.verifiedDevice;
303
+ }
304
+ installActiveDevice(device) {
305
+ this.removeDisconnectListener?.();
306
+ this.activeDevice = device;
307
+ this.removeDisconnectListener = this.transport.onDisconnected(device, () => {
308
+ if (this.activeDevice?.id !== device.id)
309
+ return;
310
+ this.runtime.markDeviceDisconnected(device.id);
311
+ this.clearConnection();
312
+ });
313
+ }
314
+ async cleanupFailedConnection(candidate = null) {
315
+ const connected = this.runtime.connectedDeviceHandle;
316
+ if (connected) {
317
+ this.runtime.markDeviceDisconnected(connected.id);
318
+ await this.transport.disconnect(connected).catch(() => undefined);
319
+ }
320
+ else if (candidate) {
321
+ await this.transport.disconnect(candidate).catch(() => undefined);
322
+ }
323
+ this.clearConnection();
324
+ }
325
+ async disconnectForIdentityMismatch(device) {
326
+ this.runtime.markDeviceDisconnected(device.id);
327
+ this.clearConnection();
328
+ await this.transport.disconnect(device).catch(() => undefined);
329
+ }
330
+ clearConnection() {
331
+ const deviceId = this.activeDevice?.id;
332
+ this.removeDisconnectListener?.();
333
+ this.removeDisconnectListener = null;
334
+ this.activeDevice = null;
335
+ this.verifiedDevice = null;
336
+ if (deviceId)
337
+ this.runtime.unregisterDevice(deviceId);
338
+ }
339
+ async read(device, serviceUuid, characteristicUuid, signal) {
340
+ try {
341
+ return await awaitDirectStep(this.transport.read(device, serviceUuid, characteristicUuid), signal, 'read_snapshot');
342
+ }
343
+ catch (error) {
344
+ if (error instanceof BrowserTransportError
345
+ && error.code === 'disconnected') {
346
+ this.runtime.markDeviceDisconnected(device.id);
347
+ this.clearConnection();
348
+ }
349
+ throw error;
350
+ }
351
+ }
352
+ async readRequiredText(device, serviceUuid, characteristicUuid, signal) {
353
+ const value = await this.read(device, serviceUuid, characteristicUuid, signal);
354
+ try {
355
+ const decoded = new TextDecoder('utf-8', { fatal: true })
356
+ .decode(value)
357
+ .replace(/^[\0\s]+|[\0\s]+$/g, '');
358
+ if (!decoded)
359
+ throw new Error('empty');
360
+ return decoded;
361
+ }
362
+ catch (error) {
363
+ throw new BotaSDKError('protocol_error', 'read_snapshot', { cause: error });
364
+ }
365
+ }
366
+ async readOptionalText(device, serviceUuid, characteristicUuid, signal) {
367
+ try {
368
+ return await this.readRequiredText(device, serviceUuid, characteristicUuid, signal);
369
+ }
370
+ catch (error) {
371
+ if (error instanceof BrowserTransportError
372
+ && error.code === 'characteristic_not_found') {
373
+ return null;
374
+ }
375
+ throw error;
376
+ }
377
+ }
378
+ async readCapabilities(device, signal) {
379
+ try {
380
+ const bytes = await this.read(device, BOTA_STORAGE_SERVICE, STORAGE_TRANSFER_CAPABILITIES_V2_CHARACTERISTIC, signal);
381
+ return this.core.decodeEncryptedUploadV2Capabilities(bytes);
382
+ }
383
+ catch (error) {
384
+ if (error instanceof BrowserTransportError
385
+ && error.code === 'characteristic_not_found') {
386
+ return null;
387
+ }
388
+ throw error;
389
+ }
390
+ }
391
+ }
392
+ function randomCancellationId() {
393
+ const cancellationId = new Uint8Array(16);
394
+ globalThis.crypto.getRandomValues(cancellationId);
395
+ return cancellationId;
396
+ }
397
+ function operationId(operation, cancellationId) {
398
+ const suffix = [...cancellationId]
399
+ .map((value) => value.toString(16).padStart(2, '0'))
400
+ .join('');
401
+ return `${operation}:${suffix}`;
402
+ }
403
+ async function awaitDirectStep(promise, signal, operation) {
404
+ const settled = promise.then((value) => ({ kind: 'completed', value }), (error) => ({ kind: 'failed', error }));
405
+ const result = await settled;
406
+ if (signal.aborted)
407
+ throw new BotaSDKError('cancelled', operation);
408
+ if (result.kind === 'failed')
409
+ throw result.error;
410
+ return result.value;
411
+ }
412
+ export async function verifyActiveDeviceSerial(manager, operation) {
413
+ return await manager.verifyActiveSerialInternal(operation);
414
+ }
415
+ export function destroyDeviceManagerAfter(manager, priorCleanup) {
416
+ return manager.destroyAfterInternal(priorCleanup);
417
+ }
418
+ async function collectTeardownFailure(cleanup, failures) {
419
+ try {
420
+ await cleanup();
421
+ }
422
+ catch (error) {
423
+ failures.push(error);
424
+ }
425
+ }
426
+ function decodeRequiredText(value, operation) {
427
+ try {
428
+ const decoded = new TextDecoder('utf-8', { fatal: true })
429
+ .decode(value)
430
+ .replace(/^[\0\s]+|[\0\s]+$/g, '');
431
+ if (!decoded)
432
+ throw new Error('empty');
433
+ return decoded;
434
+ }
435
+ catch (error) {
436
+ throw new BotaSDKError('protocol_error', operation, { cause: error });
437
+ }
438
+ }
439
+ function pickerError(error) {
440
+ if (error instanceof BrowserTransportError) {
441
+ if (error.code === 'picker_cancelled') {
442
+ return new BotaSDKError('picker_cancelled', 'connect', { cause: error });
443
+ }
444
+ if (error.code === 'permission_denied') {
445
+ return new BotaSDKError('permission_denied', 'connect', { cause: error });
446
+ }
447
+ }
448
+ const name = typeof error === 'object' && error !== null && 'name' in error
449
+ ? String(error.name)
450
+ : '';
451
+ if (name === 'NotFoundError' || name === 'AbortError') {
452
+ return new BotaSDKError('picker_cancelled', 'connect', { cause: error });
453
+ }
454
+ if (name === 'NotAllowedError' || name === 'SecurityError') {
455
+ return new BotaSDKError('permission_denied', 'connect', { cause: error });
456
+ }
457
+ return new BotaSDKError('bluetooth_unavailable', 'connect', { cause: error });
458
+ }
459
+ function normalizeManagerError(error, operation) {
460
+ if (error instanceof BotaSDKError)
461
+ return error;
462
+ if (error instanceof BrowserStorageError) {
463
+ return new BotaSDKError(error.code, operation, { cause: error });
464
+ }
465
+ if (error instanceof BrowserTransportError) {
466
+ const code = error.code === 'disconnected'
467
+ ? 'device_disconnected'
468
+ : error.code === 'permission_denied'
469
+ ? 'permission_denied'
470
+ : 'bluetooth_unavailable';
471
+ return new BotaSDKError(code, operation, { cause: error });
472
+ }
473
+ return normalizeCoreError(error, operation);
474
+ }
475
+ function snapshotError(error) {
476
+ if (error instanceof BotaSDKError)
477
+ return error;
478
+ if (error instanceof BrowserTransportError) {
479
+ if (error.code === 'disconnected') {
480
+ return new BotaSDKError('device_disconnected', 'read_snapshot', {
481
+ cause: error,
482
+ });
483
+ }
484
+ if (error.code === 'unavailable') {
485
+ return new BotaSDKError('bluetooth_unavailable', 'read_snapshot', {
486
+ cause: error,
487
+ });
488
+ }
489
+ }
490
+ return normalizeCoreError(error, 'read_snapshot');
491
+ }