@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,24 @@
1
+ import type { CoreBridge } from './core.ts';
2
+ import { DeviceManager } from './deviceManager.ts';
3
+ import type { DeviceLogLine, DeviceLogSubscription } from './models.ts';
4
+ import { BrowserWorkflowRuntime } from './workflowRuntime.ts';
5
+ interface LogManagerOptions {
6
+ core: CoreBridge;
7
+ runtime: BrowserWorkflowRuntime;
8
+ devices: DeviceManager;
9
+ }
10
+ export declare class LogManager {
11
+ private readonly core;
12
+ private readonly runtime;
13
+ private readonly devices;
14
+ private activeOwner;
15
+ private destroyed;
16
+ private destroyPromise;
17
+ constructor(options: LogManagerOptions);
18
+ subscribe(listener: (line: DeviceLogLine) => void): Promise<DeviceLogSubscription>;
19
+ destroy(): Promise<void>;
20
+ private deliver;
21
+ private closeOwner;
22
+ private finishOwner;
23
+ }
24
+ export {};
@@ -0,0 +1,205 @@
1
+ import { verifyActiveDeviceSerial, } from "./deviceManager.js";
2
+ import { BotaSDKError, normalizeCoreError } from "./errors.js";
3
+ import { BOTA_DIAGNOSTICS_SERVICE, DEVICE_LOG_DATA_CHARACTERISTIC, } from "./gatt.js";
4
+ import { createBrowserPersistenceHost, } from "./workflowRuntime.js";
5
+ export class LogManager {
6
+ core;
7
+ runtime;
8
+ devices;
9
+ activeOwner = null;
10
+ destroyed = false;
11
+ destroyPromise = null;
12
+ constructor(options) {
13
+ this.core = options.core;
14
+ this.runtime = options.runtime;
15
+ this.devices = options.devices;
16
+ }
17
+ async subscribe(listener) {
18
+ if (this.destroyed) {
19
+ throw new BotaSDKError('cancelled', 'read_device_logs');
20
+ }
21
+ if (typeof listener !== 'function') {
22
+ throw new BotaSDKError('invalid_input', 'read_device_logs');
23
+ }
24
+ if (this.activeOwner) {
25
+ throw new BotaSDKError('operation_in_progress', 'read_device_logs');
26
+ }
27
+ const connected = await verifyActiveDeviceSerial(this.devices, 'read_device_logs');
28
+ const device = this.runtime.connectedDeviceHandle;
29
+ if (!device || connected.id !== device.id) {
30
+ throw new BotaSDKError('device_disconnected', 'read_device_logs');
31
+ }
32
+ let lease;
33
+ try {
34
+ lease = this.runtime.claimCharacteristicLease('read_device_logs', device, BOTA_DIAGNOSTICS_SERVICE, DEVICE_LOG_DATA_CHARACTERISTIC);
35
+ }
36
+ catch (error) {
37
+ throw managerError(error);
38
+ }
39
+ const cancellationId = randomBytes(16);
40
+ const operationId = `read_device_logs:${bytesHex(cancellationId)}`;
41
+ const owner = {
42
+ operationId,
43
+ cancellationId,
44
+ lease,
45
+ listener,
46
+ ready: deferred(),
47
+ outcome: Promise.resolve({
48
+ kind: 'failed',
49
+ error: new BotaSDKError('internal_error', 'read_device_logs'),
50
+ }),
51
+ settled: Promise.resolve(),
52
+ closing: false,
53
+ closed: false,
54
+ terminalError: null,
55
+ closePromise: null,
56
+ };
57
+ this.activeOwner = owner;
58
+ const workflow = this.runtime.run(operationId, cancellationId, () => this.core.startDeviceLogs({
59
+ serialNumber: connected.serialNumber,
60
+ cancellationId: cancellationId.slice(),
61
+ }), { persistence: createBrowserPersistenceHost(null) }, {
62
+ onNotification: (notification) => this.deliver(owner, notification),
63
+ onRunning: () => owner.ready.resolve(undefined),
64
+ });
65
+ owner.outcome = workflow.then((result) => ({ kind: 'completed', result }), (error) => ({ kind: 'failed', error: managerError(error) }));
66
+ owner.settled = owner.outcome.then((outcome) => {
67
+ this.finishOwner(owner, outcome);
68
+ });
69
+ try {
70
+ await Promise.race([
71
+ owner.ready.promise,
72
+ owner.outcome.then((outcome) => {
73
+ if (outcome.kind === 'failed')
74
+ throw outcome.error;
75
+ throw unexpectedStreamEnd();
76
+ }),
77
+ ]);
78
+ if (this.destroyed || owner.closing || owner.closed) {
79
+ await this.closeOwner(owner);
80
+ throw owner.terminalError
81
+ ?? new BotaSDKError('cancelled', 'read_device_logs');
82
+ }
83
+ return {
84
+ remove: async () => await this.closeOwner(owner),
85
+ };
86
+ }
87
+ catch (error) {
88
+ if (!owner.closed) {
89
+ await this.closeOwner(owner).catch(() => undefined);
90
+ }
91
+ throw managerError(error);
92
+ }
93
+ }
94
+ destroy() {
95
+ if (this.destroyPromise)
96
+ return this.destroyPromise;
97
+ this.destroyed = true;
98
+ const owner = this.activeOwner;
99
+ this.destroyPromise = owner
100
+ ? this.closeOwner(owner).catch(() => undefined)
101
+ : Promise.resolve();
102
+ return this.destroyPromise;
103
+ }
104
+ deliver(owner, notification) {
105
+ if (notification.kind !== 'device_log'
106
+ || owner.closing
107
+ || owner.closed
108
+ || this.activeOwner !== owner)
109
+ return;
110
+ if (typeof notification.message !== 'string'
111
+ || typeof notification.isBacklog !== 'boolean') {
112
+ owner.terminalError ??= new BotaSDKError('protocol_error', 'read_device_logs');
113
+ void this.closeOwner(owner).catch(() => undefined);
114
+ return;
115
+ }
116
+ const listener = owner.listener;
117
+ if (!listener)
118
+ return;
119
+ try {
120
+ const completion = listener({
121
+ message: notification.message,
122
+ isBacklog: notification.isBacklog,
123
+ });
124
+ if (isPromiseLike(completion)) {
125
+ void Promise.resolve(completion).catch(() => {
126
+ void this.closeOwner(owner).catch(() => undefined);
127
+ });
128
+ }
129
+ }
130
+ catch {
131
+ void this.closeOwner(owner).catch(() => undefined);
132
+ }
133
+ }
134
+ closeOwner(owner) {
135
+ if (owner.closePromise)
136
+ return owner.closePromise;
137
+ owner.closing = true;
138
+ owner.listener = null;
139
+ owner.closePromise = (async () => {
140
+ let cancellationError = null;
141
+ if (!owner.closed) {
142
+ try {
143
+ await this.runtime.cancel(owner.operationId);
144
+ }
145
+ catch (error) {
146
+ cancellationError = managerError(error);
147
+ }
148
+ await owner.settled;
149
+ }
150
+ if (owner.terminalError)
151
+ throw owner.terminalError;
152
+ if (cancellationError)
153
+ throw cancellationError;
154
+ })();
155
+ return owner.closePromise;
156
+ }
157
+ finishOwner(owner, outcome) {
158
+ if (owner.closed)
159
+ return;
160
+ owner.closed = true;
161
+ owner.listener = null;
162
+ if (!owner.closing) {
163
+ owner.terminalError = outcome.kind === 'failed'
164
+ ? outcome.error
165
+ : unexpectedStreamEnd();
166
+ }
167
+ else if (outcome.kind === 'failed'
168
+ && outcome.error.code !== 'cancelled'
169
+ && owner.terminalError === null) {
170
+ owner.terminalError = outcome.error;
171
+ }
172
+ owner.cancellationId.fill(0);
173
+ owner.lease.release();
174
+ if (this.activeOwner === owner)
175
+ this.activeOwner = null;
176
+ }
177
+ }
178
+ function unexpectedStreamEnd() {
179
+ return new BotaSDKError('connection_failed', 'read_device_logs', {
180
+ retryable: true,
181
+ });
182
+ }
183
+ function managerError(error) {
184
+ return normalizeCoreError(error, 'read_device_logs');
185
+ }
186
+ function randomBytes(length) {
187
+ const value = new Uint8Array(length);
188
+ globalThis.crypto.getRandomValues(value);
189
+ return value;
190
+ }
191
+ function bytesHex(value) {
192
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
193
+ }
194
+ function deferred() {
195
+ let resolve;
196
+ const promise = new Promise((resolvePromise) => {
197
+ resolve = resolvePromise;
198
+ });
199
+ return { promise, resolve };
200
+ }
201
+ function isPromiseLike(value) {
202
+ return typeof value === 'object'
203
+ && value !== null
204
+ && typeof value.then === 'function';
205
+ }
@@ -0,0 +1,217 @@
1
+ export type RecordingJournalPhase = 'prepared' | 'transferring' | 'staged' | 'uploading' | 'cloud_completed' | 'confirmed';
2
+ export type DeviceState = 'idle' | 'recording' | 'syncing' | 'uploading' | 'charging' | 'low_battery' | 'storage_full' | 'error' | 'unknown';
3
+ export interface DeviceFlags {
4
+ charging: boolean;
5
+ lowBattery: boolean;
6
+ storageFull: boolean;
7
+ wifiConnected: boolean;
8
+ lteConnected: boolean;
9
+ syncActive: boolean;
10
+ }
11
+ export interface ModemInfo {
12
+ imei: string | null;
13
+ iccid: string | null;
14
+ operator: string | null;
15
+ rat: string | null;
16
+ band: string | null;
17
+ apn: string | null;
18
+ simStatus: string | null;
19
+ csq: number | null;
20
+ ipAddress: string | null;
21
+ voltageMillivolts: number | null;
22
+ firmware: string | null;
23
+ roaming: boolean | null;
24
+ }
25
+ export interface DeviceStatus {
26
+ batteryPercent: number;
27
+ batteryMillivolts: number | null;
28
+ storageTotalMb: number;
29
+ storageUsedMb: number;
30
+ state: DeviceState;
31
+ stateRaw?: number;
32
+ pendingRecordings: number;
33
+ lastTimeSyncTimestamp: number;
34
+ flags: DeviceFlags;
35
+ lteStatusRaw: number;
36
+ lteSignalQuality: number | null;
37
+ wifiStatusRaw: number | null;
38
+ modemInfo: ModemInfo | null;
39
+ }
40
+ export interface EncryptedUploadV2Capabilities {
41
+ flags: number;
42
+ maximumSignedBlobBytes: number;
43
+ maximumManifestBytes: number;
44
+ maximumDataPayloadBytes: number;
45
+ maximumWindowPackets: number;
46
+ durableCheckpointIntervalBlocks: number;
47
+ maximumMissingSequences: number;
48
+ }
49
+ export interface ConnectedDevice {
50
+ id: string;
51
+ name: string | null;
52
+ serialNumber: string;
53
+ }
54
+ export interface DeviceSnapshot {
55
+ identity: {
56
+ serialNumber: string;
57
+ modelNumber: string | null;
58
+ hardwareRevision: string | null;
59
+ firmwareRevision: string | null;
60
+ };
61
+ status: DeviceStatus;
62
+ capabilities: {
63
+ encryptedUploadV2: EncryptedUploadV2Capabilities | null;
64
+ };
65
+ capturedAt: Date;
66
+ }
67
+ export interface ConnectOptions {
68
+ expectedSerialNumber: string;
69
+ }
70
+ export interface ReconnectOptions {
71
+ expectedSerialNumber: string;
72
+ }
73
+ export interface ProvisionRequest {
74
+ attemptId: string;
75
+ signal?: AbortSignal;
76
+ }
77
+ export interface DeprovisionRequest {
78
+ grant: Uint8Array;
79
+ signal?: AbortSignal;
80
+ }
81
+ export interface DeprovisionResult {
82
+ success: boolean;
83
+ error?: 'invalid_token' | 'storage_error' | 'chunk_error' | 'already_paired' | 'unknown';
84
+ errorRaw?: number;
85
+ }
86
+ export interface DeviceConnectionSettings {
87
+ enabledConnections: {
88
+ wifi: boolean;
89
+ cellular: boolean;
90
+ };
91
+ heartbeatEnabledConnections: {
92
+ wifi: boolean;
93
+ cellular: boolean;
94
+ };
95
+ uploadNetworkPreference: Array<'wifi' | 'ble' | 'cellular'>;
96
+ powerManagement: {
97
+ cellularIdleTimeoutSeconds: number;
98
+ wifiIdleTimeoutSeconds: number;
99
+ };
100
+ streamingEnabled: boolean;
101
+ streamingFlushIntervalSeconds: number;
102
+ }
103
+ export interface WiFiCredentials {
104
+ ssid: string;
105
+ password: string;
106
+ }
107
+ export interface WiFiScanNetwork {
108
+ ssid: string;
109
+ quality: number;
110
+ isCurrent: boolean;
111
+ isOpen: boolean;
112
+ }
113
+ export interface WiFiScanResult {
114
+ networks: WiFiScanNetwork[];
115
+ currentSsid: string | null;
116
+ }
117
+ export interface WiFiStatusInfo {
118
+ status: 'idle' | 'connecting' | 'connected' | 'failed' | 'disconnected' | 'unknown';
119
+ statusRaw: number;
120
+ signalStrength?: number;
121
+ ssid?: string;
122
+ lastError?: string;
123
+ }
124
+ export type WiFiConfigResult = {
125
+ success: true;
126
+ } | {
127
+ success: false;
128
+ error: 'invalid_grant' | 'grant_expired' | 'decryption_error' | 'storage_error' | 'unknown';
129
+ errorRaw?: number;
130
+ };
131
+ export interface WiFiStatusSubscription {
132
+ remove(): Promise<void>;
133
+ }
134
+ export interface DeviceLogLine {
135
+ message: string;
136
+ isBacklog: boolean;
137
+ }
138
+ export interface DeviceLogSubscription {
139
+ remove(): Promise<void>;
140
+ }
141
+ export interface RecordingControlRequest {
142
+ authorityId: string;
143
+ operationId?: string;
144
+ signal?: AbortSignal;
145
+ }
146
+ export type RecordingControlResult = {
147
+ success: true;
148
+ } | {
149
+ success: false;
150
+ error: 'already_recording' | 'not_recording' | 'invalid_grant' | 'invalid_state' | 'invalid_response' | 'unknown_error';
151
+ errorRaw?: number;
152
+ };
153
+ export interface BrowserCapabilities {
154
+ readonly bluetooth: boolean;
155
+ readonly authorizedDeviceReconnect: boolean;
156
+ readonly durableStorage: boolean;
157
+ readonly largeRecordingSync: boolean;
158
+ readonly firmwareUpdate: boolean;
159
+ }
160
+ export interface FirmwareImageDescriptor {
161
+ imageId: string;
162
+ version: string;
163
+ sizeBytes: number;
164
+ crc32: number;
165
+ sha256Hex: string;
166
+ }
167
+ export interface FirmwareUpdateOptions {
168
+ operationId?: string;
169
+ signal?: AbortSignal;
170
+ onProgress?: (progress: FirmwareUpdateProgress) => void;
171
+ }
172
+ export interface FirmwareUpdateProgress {
173
+ phase: 'downloading' | 'awaiting_device' | 'transferring' | 'verifying' | 'rebooting' | 'reconnecting' | 'complete';
174
+ completedBytes: bigint;
175
+ totalBytes: bigint;
176
+ }
177
+ export interface DeviceRecording {
178
+ uuid: string;
179
+ startedAtTimestampSeconds: number;
180
+ durationMilliseconds: bigint;
181
+ fileSizeBytes: bigint;
182
+ codec: 'pcm_16k' | 'pcm_8k' | 'opus_16k' | 'opus_8k' | 'unknown';
183
+ codecRaw?: number;
184
+ encrypted: boolean;
185
+ encryptedUploadV2: {
186
+ generation: number;
187
+ storageFormat: number;
188
+ plaintextLength: bigint;
189
+ ciphertextLength: bigint;
190
+ ciphertextSha256: Uint8Array;
191
+ } | null;
192
+ }
193
+ export interface RecordingSyncProgress {
194
+ phase: RecordingJournalPhase;
195
+ completedBytes: bigint;
196
+ totalBytes: bigint;
197
+ }
198
+ export interface RecordingSyncResult {
199
+ operationId: string;
200
+ recordingUuid: string;
201
+ profile: 'legacy' | 'encrypted_upload_v2';
202
+ cloudCompletionId: string;
203
+ }
204
+ export interface RecordingSyncOptions {
205
+ profile: 'legacy' | 'encrypted_upload_v2';
206
+ operationId?: string;
207
+ signal?: AbortSignal;
208
+ onProgress?: (progress: RecordingSyncProgress) => void;
209
+ }
210
+ export interface RecordingJournalSummary {
211
+ operationId: string;
212
+ serialNumber: string;
213
+ recordingUuid: string;
214
+ profile: 'legacy' | 'encrypted_upload_v2';
215
+ phase: RecordingJournalPhase;
216
+ updatedAtEpochMs: number;
217
+ }
package/dist/models.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { CoreIntegrityHasher } from './core.ts';
2
+ import type { BrowserBlobHandle } from './storage.ts';
3
+ export declare class OpfsBlobStore {
4
+ private readonly root;
5
+ private readonly createIntegrityHasher;
6
+ private readonly namespaceDirectoryName;
7
+ constructor(namespace: string, root: FileSystemDirectoryHandle, createIntegrityHasher: () => CoreIntegrityHasher);
8
+ open(blobId: string): Promise<BrowserBlobHandle>;
9
+ clear(): Promise<void>;
10
+ private namespaceDirectory;
11
+ private hashName;
12
+ }