@ledgerhq/device-transport-kit-web-ble 1.0.0 → 1.2.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/README.md +2 -2
- package/lib/esm/api/data/WebBleConfig.js +1 -1
- package/lib/esm/api/data/WebBleConfig.js.map +3 -3
- package/lib/esm/api/model/BleDevice.stub.js +1 -1
- package/lib/esm/api/model/BleDevice.stub.js.map +2 -2
- package/lib/esm/api/transport/WebBleApduSender.js +2 -0
- package/lib/esm/api/transport/WebBleApduSender.js.map +7 -0
- package/lib/esm/api/transport/WebBleApduSender.test.js +2 -0
- package/lib/esm/api/transport/WebBleApduSender.test.js.map +7 -0
- package/lib/esm/api/transport/WebBleTransport.js +1 -1
- package/lib/esm/api/transport/WebBleTransport.js.map +3 -3
- package/lib/esm/api/transport/WebBleTransport.test.js +1 -1
- package/lib/esm/api/transport/WebBleTransport.test.js.map +3 -3
- package/lib/esm/package.json +16 -16
- package/lib/types/api/data/WebBleConfig.d.ts +5 -1
- package/lib/types/api/data/WebBleConfig.d.ts.map +1 -1
- package/lib/types/api/model/BleDevice.stub.d.ts.map +1 -1
- package/lib/types/api/transport/WebBleApduSender.d.ts +39 -0
- package/lib/types/api/transport/WebBleApduSender.d.ts.map +1 -0
- package/lib/types/api/transport/WebBleApduSender.test.d.ts +2 -0
- package/lib/types/api/transport/WebBleApduSender.test.d.ts.map +1 -0
- package/lib/types/api/transport/WebBleTransport.d.ts +23 -72
- package/lib/types/api/transport/WebBleTransport.d.ts.map +1 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/lib/esm/api/transport/BleDeviceConnection.js +0 -2
- package/lib/esm/api/transport/BleDeviceConnection.js.map +0 -7
- package/lib/esm/api/transport/BleDeviceConnection.test.js +0 -2
- package/lib/esm/api/transport/BleDeviceConnection.test.js.map +0 -7
- package/lib/types/api/transport/BleDeviceConnection.d.ts +0 -98
- package/lib/types/api/transport/BleDeviceConnection.d.ts.map +0 -1
- package/lib/types/api/transport/BleDeviceConnection.test.d.ts +0 -2
- package/lib/types/api/transport/BleDeviceConnection.test.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ This library works in [any browser supporting the Web Bluetooth API](https://dev
|
|
|
32
32
|
|
|
33
33
|
### Pre-requisites
|
|
34
34
|
|
|
35
|
-
To use this transport, ensure you have the Device
|
|
35
|
+
To use this transport, ensure you have the Device Management Kit installed in your project.
|
|
36
36
|
|
|
37
37
|
### Main Features
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ To use this transport, ensure you have the Device Magement Kit installed in your
|
|
|
41
41
|
|
|
42
42
|
### How To
|
|
43
43
|
|
|
44
|
-
To use the transport, you need to inject it in the DeviceManagementKitBuilder before the build. This will allow the Device
|
|
44
|
+
To use the transport, you need to inject it in the DeviceManagementKitBuilder before the build. This will allow the Device Management Kit to find and interact with devices on the Web BLE protocol.
|
|
45
45
|
|
|
46
46
|
```typescript
|
|
47
47
|
import { DeviceManagementKitBuilder } from "@ledgerhq/device-management-kit"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const E=
|
|
1
|
+
const E=15e3,o=400,t=1200,I=3e3,O=500;export{o as ADVERTISING_DELAY,t as ADVERTISING_TIMEOUT,O as RECONNECTION_LOOP_BACKOFF,E as RECONNECT_DEVICE_TIMEOUT,I as REDISCOVER_TIMEOUT};
|
|
2
2
|
//# sourceMappingURL=WebBleConfig.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/api/data/WebBleConfig.ts"],
|
|
4
|
-
"sourcesContent": ["export const RECONNECT_DEVICE_TIMEOUT =
|
|
5
|
-
"mappings": "AAAO,MAAMA,EAA2B",
|
|
6
|
-
"names": ["RECONNECT_DEVICE_TIMEOUT"]
|
|
4
|
+
"sourcesContent": ["export const RECONNECT_DEVICE_TIMEOUT = 15000;\nexport const ADVERTISING_DELAY = 400;\nexport const ADVERTISING_TIMEOUT = 1200;\nexport const REDISCOVER_TIMEOUT = 3000;\nexport const RECONNECTION_LOOP_BACKOFF = 500;\n"],
|
|
5
|
+
"mappings": "AAAO,MAAMA,EAA2B,KAC3BC,EAAoB,IACpBC,EAAsB,KACtBC,EAAqB,IACrBC,EAA4B",
|
|
6
|
+
"names": ["RECONNECT_DEVICE_TIMEOUT", "ADVERTISING_DELAY", "ADVERTISING_TIMEOUT", "REDISCOVER_TIMEOUT", "RECONNECTION_LOOP_BACKOFF"]
|
|
7
7
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const
|
|
1
|
+
const i={name:"Ledger Nano X",id:"42",forget:vi.fn(),watchAdvertisements:vi.fn(),dispatchEvent:vi.fn(),watchingAdvertisements:!1,addEventListener:vi.fn(),removeEventListener:vi.fn(),onadvertisementreceived:vi.fn(),ongattserverdisconnected:vi.fn(),oncharacteristicvaluechanged:vi.fn(),onserviceadded:vi.fn(),onservicechanged:vi.fn(),onserviceremoved:vi.fn()},t={device:i,uuid:"13d63400-2c97-0004-0000-4c6564676572",isPrimary:!0,getCharacteristic:vi.fn(()=>Promise.resolve(n())),getCharacteristics:vi.fn(),getIncludedService:vi.fn(),getIncludedServices:vi.fn(),addEventListener:vi.fn(),dispatchEvent:vi.fn(),removeEventListener:vi.fn(),oncharacteristicvaluechanged:vi.fn(),onserviceadded:vi.fn(),onservicechanged:vi.fn(),onserviceremoved:vi.fn()},n=(e={})=>({...e,addEventListener:vi.fn(),removeEventListener:vi.fn(),startNotifications:vi.fn(),writeValueWithResponse:vi.fn(),writeValueWithoutResponse:vi.fn()}),v=(e={})=>({...i,gatt:{device:i,connected:!0,connect:vi.fn(),disconnect:vi.fn(),getPrimaryService:vi.fn(),getPrimaryServices:vi.fn(()=>Promise.resolve([t]))},...e});export{n as bleCharacteristicStubBuilder,v as bleDeviceStubBuilder};
|
|
2
2
|
//# sourceMappingURL=BleDevice.stub.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/api/model/BleDevice.stub.ts"],
|
|
4
|
-
"sourcesContent": ["const bleDeviceWithoutGatt: BluetoothDevice = {\n name: \"Ledger Nano X\",\n id: \"42\",\n forget:
|
|
5
|
-
"mappings": "AAAA,MAAMA,EAAwC,CAC5C,KAAM,gBACN,GAAI,KACJ,OAAQ,
|
|
4
|
+
"sourcesContent": ["const bleDeviceWithoutGatt: BluetoothDevice = {\n name: \"Ledger Nano X\",\n id: \"42\",\n forget: vi.fn(),\n watchAdvertisements: vi.fn(),\n dispatchEvent: vi.fn(),\n watchingAdvertisements: false,\n addEventListener: vi.fn(),\n removeEventListener: vi.fn(),\n onadvertisementreceived: vi.fn(),\n ongattserverdisconnected: vi.fn(),\n oncharacteristicvaluechanged: vi.fn(),\n onserviceadded: vi.fn(),\n onservicechanged: vi.fn(),\n onserviceremoved: vi.fn(),\n};\n\nconst bluetoothGattPrimaryService: BluetoothRemoteGATTService = {\n device: bleDeviceWithoutGatt,\n uuid: \"13d63400-2c97-0004-0000-4c6564676572\",\n isPrimary: true,\n getCharacteristic: vi.fn(() =>\n Promise.resolve(bleCharacteristicStubBuilder()),\n ),\n getCharacteristics: vi.fn(),\n getIncludedService: vi.fn(),\n getIncludedServices: vi.fn(),\n addEventListener: vi.fn(),\n dispatchEvent: vi.fn(),\n removeEventListener: vi.fn(),\n oncharacteristicvaluechanged: vi.fn(),\n onserviceadded: vi.fn(),\n onservicechanged: vi.fn(),\n onserviceremoved: vi.fn(),\n};\n\nexport const bleCharacteristicStubBuilder = (\n props: Partial<BluetoothRemoteGATTCharacteristic> = {},\n): BluetoothRemoteGATTCharacteristic =>\n ({\n ...props,\n addEventListener: vi.fn(),\n removeEventListener: vi.fn(),\n startNotifications: vi.fn(),\n writeValueWithResponse: vi.fn(),\n writeValueWithoutResponse: vi.fn(),\n }) as BluetoothRemoteGATTCharacteristic;\n\nexport const bleDeviceStubBuilder = (\n props: Partial<BluetoothDevice> = {},\n): BluetoothDevice => ({\n ...bleDeviceWithoutGatt,\n gatt: {\n device: bleDeviceWithoutGatt,\n connected: true,\n connect: vi.fn(),\n disconnect: vi.fn(),\n getPrimaryService: vi.fn(),\n getPrimaryServices: vi.fn(() =>\n Promise.resolve([bluetoothGattPrimaryService]),\n ),\n },\n ...props,\n});\n"],
|
|
5
|
+
"mappings": "AAAA,MAAMA,EAAwC,CAC5C,KAAM,gBACN,GAAI,KACJ,OAAQ,GAAG,GAAG,EACd,oBAAqB,GAAG,GAAG,EAC3B,cAAe,GAAG,GAAG,EACrB,uBAAwB,GACxB,iBAAkB,GAAG,GAAG,EACxB,oBAAqB,GAAG,GAAG,EAC3B,wBAAyB,GAAG,GAAG,EAC/B,yBAA0B,GAAG,GAAG,EAChC,6BAA8B,GAAG,GAAG,EACpC,eAAgB,GAAG,GAAG,EACtB,iBAAkB,GAAG,GAAG,EACxB,iBAAkB,GAAG,GAAG,CAC1B,EAEMC,EAA0D,CAC9D,OAAQD,EACR,KAAM,uCACN,UAAW,GACX,kBAAmB,GAAG,GAAG,IACvB,QAAQ,QAAQE,EAA6B,CAAC,CAChD,EACA,mBAAoB,GAAG,GAAG,EAC1B,mBAAoB,GAAG,GAAG,EAC1B,oBAAqB,GAAG,GAAG,EAC3B,iBAAkB,GAAG,GAAG,EACxB,cAAe,GAAG,GAAG,EACrB,oBAAqB,GAAG,GAAG,EAC3B,6BAA8B,GAAG,GAAG,EACpC,eAAgB,GAAG,GAAG,EACtB,iBAAkB,GAAG,GAAG,EACxB,iBAAkB,GAAG,GAAG,CAC1B,EAEaA,EAA+B,CAC1CC,EAAoD,CAAC,KAEpD,CACC,GAAGA,EACH,iBAAkB,GAAG,GAAG,EACxB,oBAAqB,GAAG,GAAG,EAC3B,mBAAoB,GAAG,GAAG,EAC1B,uBAAwB,GAAG,GAAG,EAC9B,0BAA2B,GAAG,GAAG,CACnC,GAEWC,EAAuB,CAClCD,EAAkC,CAAC,KACd,CACrB,GAAGH,EACH,KAAM,CACJ,OAAQA,EACR,UAAW,GACX,QAAS,GAAG,GAAG,EACf,WAAY,GAAG,GAAG,EAClB,kBAAmB,GAAG,GAAG,EACzB,mBAAoB,GAAG,GAAG,IACxB,QAAQ,QAAQ,CAACC,CAA2B,CAAC,CAC/C,CACF,EACA,GAAGE,CACL",
|
|
6
6
|
"names": ["bleDeviceWithoutGatt", "bluetoothGattPrimaryService", "bleCharacteristicStubBuilder", "props", "bleDeviceStubBuilder"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DeviceDisconnectedWhileSendingError as o,DeviceNotInitializedError as h,SendApduTimeoutError as l}from"@ledgerhq/device-management-kit";import{Left as c,Maybe as n,Right as f}from"purify-ts";import{BehaviorSubject as _}from"rxjs";const u=8;class w{_dependencies;_apduFrameSegmenter=n.empty();_apduSenderFactory;_apduReceiverFactory;_apduFrameReceiver;_logger;_mtuNegotiated$=new _(!1);_notificationsReady=!1;_mtuRequestInProgress=!1;_pendingResponseResolver=n.empty();constructor(t,e){this._dependencies={writeCharacteristic:t.writeCharacteristic,notifyCharacteristic:t.notifyCharacteristic},this._apduSenderFactory=t.apduSenderFactory,this._apduReceiverFactory=t.apduReceiverFactory,this._apduFrameReceiver=t.apduReceiverFactory(),this._logger=e("WebBleApduSender")}async sendApdu(t,e,i){try{const a=Math.max(1800,i??0);await this._waitUntilMtuNegotiated(a)}catch(a){return c(a)}if(!this._isGattConnected())return this._markLinkUnavailable(),c(new o("GATT not connected"));if(this._apduFrameSegmenter.isNothing())return c(new h("Unknown MTU / sender not ready"));let r;const s=new Promise(a=>{this._pendingResponseResolver=n.of(p=>{r&&clearTimeout(r),a(p)})}),d=this._apduFrameSegmenter.map(a=>a.getFrames(t)).orDefault([]);for(const a of d)try{await this._writeToGattCharacteristic(a.getRawData().slice().buffer)}catch(p){const m=e?"Frame write failed during expected drop":"Frame write failed";this._logger[e?"debug":"error"](m,{data:{e:p}}),this._failPendingSend(new o("Write failed"));break}return i&&(r=setTimeout(()=>{this._logger.debug("[sendApdu] Abort timeout triggered"),this._pendingResponseResolver.map(a=>a(c(new l("Abort timeout"))))},i)),s}closeConnection(){try{this._failPendingSend(new o("Connection closed")),this._notificationsReady&&(this._dependencies.notifyCharacteristic.removeEventListener("characteristicvaluechanged",this._handleNotification),this._dependencies.notifyCharacteristic.stopNotifications().catch(()=>{}),this._notificationsReady=!1),this._dependencies.notifyCharacteristic.service.device.gatt?.disconnect()}catch{this._logger.error("Failed to disconnect from device")}finally{this._mtuNegotiated$.next(!1),this._apduFrameSegmenter=n.empty()}}getDependencies(){return this._dependencies}setDependencies(t){this._failPendingSend(new o("Link changed"));try{this._notificationsReady&&(this._dependencies.notifyCharacteristic.removeEventListener("characteristicvaluechanged",this._handleNotification),this._dependencies.notifyCharacteristic.stopNotifications().catch(()=>{}))}catch{}this._notificationsReady=!1,this._mtuNegotiated$.next(!1),this._apduFrameSegmenter=n.empty(),this._pendingResponseResolver=n.empty(),this._dependencies=t,this._apduFrameReceiver=this._apduReceiverFactory()}async setupConnection(){const t=this._dependencies.notifyCharacteristic;this._notificationsReady||(await t.startNotifications(),this._logger.debug("Notify armed",{data:{notifyUuid:this._dependencies.notifyCharacteristic.uuid,writeUuid:this._dependencies.writeCharacteristic.uuid,props:this._dependencies.writeCharacteristic.properties}}),this._notificationsReady=!0,t.addEventListener("characteristicvaluechanged",this._handleNotification)),await this._sleep(120),this._mtuRequestInProgress=!0,this._mtuNegotiated$.next(!1),this._apduFrameSegmenter=n.empty();const e=new Uint8Array([u,0,0,0,0]);try{await this._writeToGattCharacteristic(e.buffer),await Promise.race([new Promise((i,r)=>{const s=setTimeout(()=>r(new Error("MTU negotiation timeout")),2e3),d=this._mtuNegotiated$.subscribe(a=>{a&&(clearTimeout(s),d.unsubscribe(),i())})}),this._sleep(2300).then(()=>{if(!this._isGattConnected())throw new o("Link dropped during MTU")})])}catch(i){try{t.removeEventListener("characteristicvaluechanged",this._handleNotification),await t.stopNotifications().catch(()=>{})}finally{this._notificationsReady=!1,this._mtuNegotiated$.next(!1),this._apduFrameSegmenter=n.empty()}throw i}finally{this._mtuRequestInProgress=!1}}_isGattConnected(){try{return!!this._dependencies.notifyCharacteristic.service.device.gatt?.connected}catch{return!1}}_isGattDisconnectedError(t){const e=t,i=(typeof e=="object"&&e!==null&&"name"in e?e.name??"":"").toString(),r=(typeof e=="object"&&e!==null&&"message"in e?e.message??"":"").toString().toLowerCase();return i==="NetworkError"||r.includes("gatt server is disconnected")||r.includes("not connected")||r.includes("cannot perform gatt operations")}_failPendingSend(t){this._pendingResponseResolver.map(e=>e(c(t))),this._pendingResponseResolver=n.empty()}_markLinkUnavailable(){this._notificationsReady&&(this._dependencies.notifyCharacteristic.removeEventListener("characteristicvaluechanged",this._handleNotification),this._dependencies.notifyCharacteristic.stopNotifications().catch(()=>{}),this._notificationsReady=!1),this._mtuNegotiated$.next(!1),this._apduFrameSegmenter=n.empty(),this._pendingResponseResolver=n.empty()}async _sleep(t){return new Promise(e=>setTimeout(e,t))}_handleNotification=t=>{const e=t.target;if(!e.value)return;const i=new Uint8Array(e.value.buffer);if(!this._mtuNegotiated$.value){if(!this._mtuRequestInProgress){this._logger.debug("Dropping pre-handshake frame",{data:{data:i}});return}if(i.length<6||i[0]!==u){this._logger.debug("Non-MTU frame during handshake; dropping",{data:{data:i}});return}this._handleMtuNegotiationFrame(i);return}this._handleApduFrame(i)};_handleMtuNegotiationFrame(t){const e=t[5];if(e===void 0||!Number.isFinite(e)||e<=0)throw new Error("MTU negotiation failed: invalid MTU");const i=e;this._apduFrameSegmenter=n.of(this._apduSenderFactory({frameSize:i})),this._mtuNegotiated$.next(!0)}_handleApduFrame(t){this._apduFrameReceiver.handleFrame(t).map(e=>e.map(i=>{this._logger.debug("Received APDU",{data:{resp:i}}),this._pendingResponseResolver.map(r=>r(f(i))),this._pendingResponseResolver=n.empty()})).mapLeft(e=>{this._pendingResponseResolver.map(i=>i(c(e))),this._pendingResponseResolver=n.empty()})}async _writeToGattCharacteristic(t){const e=this._dependencies.writeCharacteristic;if(!this._isGattConnected())throw this._markLinkUnavailable(),new o("GATT not connected");const i=typeof e.writeValueWithoutResponse=="function",r=typeof e.writeValueWithResponse=="function";if(e.properties.writeWithoutResponse&&i)try{await e.writeValueWithoutResponse(t);return}catch(s){if(this._isGattDisconnectedError(s)||!this._isGattConnected())throw this._markLinkUnavailable(),new o("Write failed")}if(e.properties.write&&r){await e.writeValueWithResponse(t);return}throw new Error("No supported write method for characteristic")}async _waitUntilMtuNegotiated(t=2e3){if(!(this._notificationsReady&&this._mtuNegotiated$.value&&this._isGattConnected()))return new Promise((e,i)=>{const r=this._mtuNegotiated$.subscribe(d=>{d&&this._notificationsReady&&this._isGattConnected()&&(clearTimeout(s),r.unsubscribe(),e())}),s=setTimeout(()=>{r.unsubscribe(),i(new h("Link not ready"))},t);this._notificationsReady&&this._mtuNegotiated$.value&&this._isGattConnected()&&(clearTimeout(s),r.unsubscribe(),e())})}}export{u as MTU_OP,w as WebBleApduSender};
|
|
2
|
+
//# sourceMappingURL=WebBleApduSender.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/api/transport/WebBleApduSender.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n type ApduReceiverService,\n type ApduReceiverServiceFactory,\n type ApduResponse,\n type ApduSenderService,\n type ApduSenderServiceFactory,\n type DeviceApduSender,\n DeviceDisconnectedWhileSendingError,\n DeviceNotInitializedError,\n type DmkError,\n type LoggerPublisherService,\n SendApduTimeoutError,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Maybe, Right } from \"purify-ts\";\nimport { BehaviorSubject } from \"rxjs\";\n\nexport type WebBleApduSenderDependencies = {\n writeCharacteristic: BluetoothRemoteGATTCharacteristic;\n notifyCharacteristic: BluetoothRemoteGATTCharacteristic;\n};\n\nexport const MTU_OP = 0x08;\n\nexport class WebBleApduSender\n implements DeviceApduSender<WebBleApduSenderDependencies>\n{\n private _dependencies: WebBleApduSenderDependencies;\n private _apduFrameSegmenter: Maybe<ApduSenderService> = Maybe.empty();\n private _apduSenderFactory: ApduSenderServiceFactory;\n private _apduReceiverFactory: ApduReceiverServiceFactory;\n private _apduFrameReceiver: ApduReceiverService;\n private _logger: LoggerPublisherService;\n\n private _mtuNegotiated$ = new BehaviorSubject<boolean>(false);\n private _notificationsReady = false;\n private _mtuRequestInProgress = false;\n\n private _pendingResponseResolver: Maybe<\n (r: Either<DmkError, ApduResponse>) => void\n > = Maybe.empty();\n\n constructor(\n initialDependencies: WebBleApduSenderDependencies & {\n apduSenderFactory: ApduSenderServiceFactory;\n apduReceiverFactory: ApduReceiverServiceFactory;\n },\n loggerFactory: (tag: string) => LoggerPublisherService,\n ) {\n this._dependencies = {\n writeCharacteristic: initialDependencies.writeCharacteristic,\n notifyCharacteristic: initialDependencies.notifyCharacteristic,\n };\n this._apduSenderFactory = initialDependencies.apduSenderFactory;\n this._apduReceiverFactory = initialDependencies.apduReceiverFactory;\n this._apduFrameReceiver = initialDependencies.apduReceiverFactory();\n this._logger = loggerFactory(\"WebBleApduSender\");\n }\n\n public async sendApdu(\n apdu: Uint8Array,\n triggersDisconnection?: boolean,\n abortTimeout?: number,\n ): Promise<Either<DmkError, ApduResponse>> {\n try {\n const waitBudget = Math.max(1800, abortTimeout ?? 0);\n await this._waitUntilMtuNegotiated(waitBudget);\n } catch (e) {\n return Left(e as DmkError);\n }\n\n if (!this._isGattConnected()) {\n this._markLinkUnavailable();\n return Left(\n new DeviceDisconnectedWhileSendingError(\n \"GATT not connected\",\n ) as unknown as DmkError,\n );\n }\n\n if (this._apduFrameSegmenter.isNothing()) {\n return Left(\n new DeviceNotInitializedError(\n \"Unknown MTU / sender not ready\",\n ) as unknown as DmkError,\n );\n }\n\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const responsePromise = new Promise<Either<DmkError, ApduResponse>>(\n (resolve) => {\n this._pendingResponseResolver = Maybe.of((result) => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n resolve(result);\n });\n },\n );\n\n const frames = this._apduFrameSegmenter\n .map((segmenter) => segmenter.getFrames(apdu))\n .orDefault([]);\n\n for (const frame of frames) {\n try {\n await this._writeToGattCharacteristic(\n frame.getRawData().slice().buffer,\n );\n } catch (e) {\n const msg = triggersDisconnection\n ? \"Frame write failed during expected drop\"\n : \"Frame write failed\";\n this._logger[triggersDisconnection ? \"debug\" : \"error\"](msg, {\n data: { e },\n });\n this._failPendingSend(\n new DeviceDisconnectedWhileSendingError(\"Write failed\"),\n );\n break;\n }\n }\n\n if (abortTimeout) {\n timeoutHandle = setTimeout(() => {\n this._logger.debug(\"[sendApdu] Abort timeout triggered\");\n this._pendingResponseResolver.map((resolve) =>\n resolve(Left(new SendApduTimeoutError(\"Abort timeout\"))),\n );\n }, abortTimeout);\n }\n\n return responsePromise;\n }\n\n public closeConnection(): void {\n try {\n this._failPendingSend(\n new DeviceDisconnectedWhileSendingError(\"Connection closed\"),\n );\n if (this._notificationsReady) {\n this._dependencies.notifyCharacteristic.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotification,\n );\n this._dependencies.notifyCharacteristic\n .stopNotifications()\n .catch(() => {});\n this._notificationsReady = false;\n }\n this._dependencies.notifyCharacteristic.service.device.gatt?.disconnect();\n } catch {\n this._logger.error(\"Failed to disconnect from device\");\n } finally {\n this._mtuNegotiated$.next(false);\n this._apduFrameSegmenter = Maybe.empty();\n }\n }\n\n public getDependencies(): WebBleApduSenderDependencies {\n return this._dependencies;\n }\n\n public setDependencies(deps: WebBleApduSenderDependencies): void {\n this._failPendingSend(\n new DeviceDisconnectedWhileSendingError(\"Link changed\"),\n );\n\n try {\n if (this._notificationsReady) {\n this._dependencies.notifyCharacteristic.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotification,\n );\n this._dependencies.notifyCharacteristic\n .stopNotifications()\n .catch(() => {});\n }\n } catch {\n // ignore\n }\n\n this._notificationsReady = false;\n this._mtuNegotiated$.next(false);\n this._apduFrameSegmenter = Maybe.empty();\n this._pendingResponseResolver = Maybe.empty();\n\n this._dependencies = deps;\n this._apduFrameReceiver = this._apduReceiverFactory();\n }\n\n public async setupConnection(): Promise<void> {\n const notifyCharacteristic = this._dependencies.notifyCharacteristic;\n if (!this._notificationsReady) {\n await notifyCharacteristic.startNotifications();\n this._logger.debug(\"Notify armed\", {\n data: {\n notifyUuid: this._dependencies.notifyCharacteristic.uuid,\n writeUuid: this._dependencies.writeCharacteristic.uuid,\n props: this._dependencies.writeCharacteristic.properties,\n },\n });\n this._notificationsReady = true;\n notifyCharacteristic.addEventListener(\n \"characteristicvaluechanged\",\n this._handleNotification,\n );\n }\n\n // Avoids possible drops on the very first notification if we write immediately\n await this._sleep(120);\n\n this._mtuRequestInProgress = true;\n this._mtuNegotiated$.next(false);\n this._apduFrameSegmenter = Maybe.empty();\n\n const mtuRequestFrame = new Uint8Array([MTU_OP, 0, 0, 0, 0]);\n\n try {\n await this._writeToGattCharacteristic(mtuRequestFrame.buffer);\n\n await Promise.race([\n new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(\n () => reject(new Error(\"MTU negotiation timeout\")),\n 2000,\n );\n const sub = this._mtuNegotiated$.subscribe((ready) => {\n if (ready) {\n clearTimeout(timeout);\n sub.unsubscribe();\n resolve();\n }\n });\n }),\n this._sleep(2300).then(() => {\n if (!this._isGattConnected()) {\n throw new DeviceDisconnectedWhileSendingError(\n \"Link dropped during MTU\",\n );\n }\n }),\n ]);\n } catch (e) {\n try {\n notifyCharacteristic.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotification,\n );\n await notifyCharacteristic.stopNotifications().catch(() => {});\n } finally {\n this._notificationsReady = false;\n this._mtuNegotiated$.next(false);\n this._apduFrameSegmenter = Maybe.empty();\n }\n throw e;\n } finally {\n this._mtuRequestInProgress = false;\n }\n }\n\n private _isGattConnected(): boolean {\n try {\n return !!this._dependencies.notifyCharacteristic.service.device.gatt\n ?.connected;\n } catch {\n return false;\n }\n }\n\n private _isGattDisconnectedError(e: unknown): boolean {\n const err = e as Error | { name?: string; message?: string };\n const name = (\n typeof err === \"object\" && err !== null && \"name\" in err\n ? (err.name ?? \"\")\n : \"\"\n ).toString();\n const msg = (\n typeof err === \"object\" && err !== null && \"message\" in err\n ? (err.message ?? \"\")\n : \"\"\n )\n .toString()\n .toLowerCase();\n return (\n name === \"NetworkError\" ||\n msg.includes(\"gatt server is disconnected\") ||\n msg.includes(\"not connected\") ||\n msg.includes(\"cannot perform gatt operations\")\n );\n }\n\n private _failPendingSend(err: DmkError) {\n this._pendingResponseResolver.map((resolve) => resolve(Left(err)));\n this._pendingResponseResolver = Maybe.empty();\n }\n\n private _markLinkUnavailable(): void {\n if (this._notificationsReady) {\n this._dependencies.notifyCharacteristic.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotification,\n );\n this._dependencies.notifyCharacteristic\n .stopNotifications()\n .catch(() => {});\n this._notificationsReady = false;\n }\n\n this._mtuNegotiated$.next(false);\n this._apduFrameSegmenter = Maybe.empty();\n this._pendingResponseResolver = Maybe.empty();\n }\n\n private async _sleep(ms: number) {\n return new Promise((r) => setTimeout(r, ms));\n }\n\n private _handleNotification = (event: Event) => {\n const characteristic = event.target as BluetoothRemoteGATTCharacteristic;\n if (!characteristic.value) return;\n const data = new Uint8Array(characteristic.value.buffer);\n\n if (!this._mtuNegotiated$.value) {\n if (!this._mtuRequestInProgress) {\n this._logger.debug(\"Dropping pre-handshake frame\", { data: { data } });\n return;\n }\n if (data.length < 6 || data[0] !== MTU_OP) {\n this._logger.debug(\"Non-MTU frame during handshake; dropping\", {\n data: { data },\n });\n return;\n }\n this._handleMtuNegotiationFrame(data);\n return;\n }\n\n this._handleApduFrame(data);\n };\n\n private _handleMtuNegotiationFrame(mtuResponseBuffer: Uint8Array) {\n const ledgerMtu = mtuResponseBuffer[5];\n if (\n ledgerMtu === undefined ||\n !Number.isFinite(ledgerMtu) ||\n ledgerMtu <= 0\n ) {\n throw new Error(\"MTU negotiation failed: invalid MTU\");\n }\n\n const frameSize = ledgerMtu;\n this._apduFrameSegmenter = Maybe.of(this._apduSenderFactory({ frameSize }));\n this._mtuNegotiated$.next(true);\n }\n\n private _handleApduFrame(incomingFrame: Uint8Array) {\n this._apduFrameReceiver\n .handleFrame(incomingFrame)\n .map((maybeResponse) =>\n maybeResponse.map((resp) => {\n this._logger.debug(\"Received APDU\", { data: { resp } });\n this._pendingResponseResolver.map((resolve) => resolve(Right(resp)));\n this._pendingResponseResolver = Maybe.empty();\n }),\n )\n .mapLeft((err) => {\n this._pendingResponseResolver.map((resolve) => resolve(Left(err)));\n this._pendingResponseResolver = Maybe.empty();\n });\n }\n\n private async _writeToGattCharacteristic(buf: ArrayBuffer) {\n const ch = this._dependencies.writeCharacteristic;\n\n if (!this._isGattConnected()) {\n this._markLinkUnavailable();\n throw new DeviceDisconnectedWhileSendingError(\"GATT not connected\");\n }\n\n const hasWnr = typeof ch.writeValueWithoutResponse === \"function\";\n const hasWr = typeof ch.writeValueWithResponse === \"function\";\n\n // Prefer WNR for Ledger throughput\n if (ch.properties.writeWithoutResponse && hasWnr) {\n try {\n await ch.writeValueWithoutResponse(buf);\n return;\n } catch (e) {\n if (this._isGattDisconnectedError(e) || !this._isGattConnected()) {\n this._markLinkUnavailable();\n throw new DeviceDisconnectedWhileSendingError(\"Write failed\");\n }\n // otherwise, try WR\n }\n }\n\n if (ch.properties.write && hasWr) {\n await ch.writeValueWithResponse(buf);\n return;\n }\n\n throw new Error(\"No supported write method for characteristic\");\n }\n\n private async _waitUntilMtuNegotiated(maxMs = 2000): Promise<void> {\n if (\n this._notificationsReady &&\n this._mtuNegotiated$.value &&\n this._isGattConnected()\n )\n return;\n\n return new Promise<void>((resolve, reject) => {\n const subscription = this._mtuNegotiated$.subscribe((ready) => {\n if (!ready) return;\n if (this._notificationsReady && this._isGattConnected()) {\n clearTimeout(timer);\n subscription.unsubscribe();\n resolve();\n }\n });\n\n const timer = setTimeout(() => {\n subscription.unsubscribe();\n reject(new DeviceNotInitializedError(\"Link not ready\"));\n }, maxMs);\n\n if (\n this._notificationsReady &&\n this._mtuNegotiated$.value &&\n this._isGattConnected()\n ) {\n clearTimeout(timer);\n subscription.unsubscribe();\n resolve();\n }\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAOE,uCAAAA,EACA,6BAAAC,EAGA,wBAAAC,MACK,kCACP,OAAsB,QAAAC,EAAM,SAAAC,EAAO,SAAAC,MAAa,YAChD,OAAS,mBAAAC,MAAuB,OAOzB,MAAMC,EAAS,EAEf,MAAMC,CAEb,CACU,cACA,oBAAgDJ,EAAM,MAAM,EAC5D,mBACA,qBACA,mBACA,QAEA,gBAAkB,IAAIE,EAAyB,EAAK,EACpD,oBAAsB,GACtB,sBAAwB,GAExB,yBAEJF,EAAM,MAAM,EAEhB,YACEK,EAIAC,EACA,CACA,KAAK,cAAgB,CACnB,oBAAqBD,EAAoB,oBACzC,qBAAsBA,EAAoB,oBAC5C,EACA,KAAK,mBAAqBA,EAAoB,kBAC9C,KAAK,qBAAuBA,EAAoB,oBAChD,KAAK,mBAAqBA,EAAoB,oBAAoB,EAClE,KAAK,QAAUC,EAAc,kBAAkB,CACjD,CAEA,MAAa,SACXC,EACAC,EACAC,EACyC,CACzC,GAAI,CACF,MAAMC,EAAa,KAAK,IAAI,KAAMD,GAAgB,CAAC,EACnD,MAAM,KAAK,wBAAwBC,CAAU,CAC/C,OAASC,EAAG,CACV,OAAOZ,EAAKY,CAAa,CAC3B,CAEA,GAAI,CAAC,KAAK,iBAAiB,EACzB,YAAK,qBAAqB,EACnBZ,EACL,IAAIH,EACF,oBACF,CACF,EAGF,GAAI,KAAK,oBAAoB,UAAU,EACrC,OAAOG,EACL,IAAIF,EACF,gCACF,CACF,EAGF,IAAIe,EACJ,MAAMC,EAAkB,IAAI,QACzBC,GAAY,CACX,KAAK,yBAA2Bd,EAAM,GAAIe,GAAW,CAC/CH,GAAe,aAAaA,CAAa,EAC7CE,EAAQC,CAAM,CAChB,CAAC,CACH,CACF,EAEMC,EAAS,KAAK,oBACjB,IAAKC,GAAcA,EAAU,UAAUV,CAAI,CAAC,EAC5C,UAAU,CAAC,CAAC,EAEf,UAAWW,KAASF,EAClB,GAAI,CACF,MAAM,KAAK,2BACTE,EAAM,WAAW,EAAE,MAAM,EAAE,MAC7B,CACF,OAASP,EAAG,CACV,MAAMQ,EAAMX,EACR,0CACA,qBACJ,KAAK,QAAQA,EAAwB,QAAU,OAAO,EAAEW,EAAK,CAC3D,KAAM,CAAE,EAAAR,CAAE,CACZ,CAAC,EACD,KAAK,iBACH,IAAIf,EAAoC,cAAc,CACxD,EACA,KACF,CAGF,OAAIa,IACFG,EAAgB,WAAW,IAAM,CAC/B,KAAK,QAAQ,MAAM,oCAAoC,EACvD,KAAK,yBAAyB,IAAKE,GACjCA,EAAQf,EAAK,IAAID,EAAqB,eAAe,CAAC,CAAC,CACzD,CACF,EAAGW,CAAY,GAGVI,CACT,CAEO,iBAAwB,CAC7B,GAAI,CACF,KAAK,iBACH,IAAIjB,EAAoC,mBAAmB,CAC7D,EACI,KAAK,sBACP,KAAK,cAAc,qBAAqB,oBACtC,6BACA,KAAK,mBACP,EACA,KAAK,cAAc,qBAChB,kBAAkB,EAClB,MAAM,IAAM,CAAC,CAAC,EACjB,KAAK,oBAAsB,IAE7B,KAAK,cAAc,qBAAqB,QAAQ,OAAO,MAAM,WAAW,CAC1E,MAAQ,CACN,KAAK,QAAQ,MAAM,kCAAkC,CACvD,QAAE,CACA,KAAK,gBAAgB,KAAK,EAAK,EAC/B,KAAK,oBAAsBI,EAAM,MAAM,CACzC,CACF,CAEO,iBAAgD,CACrD,OAAO,KAAK,aACd,CAEO,gBAAgBoB,EAA0C,CAC/D,KAAK,iBACH,IAAIxB,EAAoC,cAAc,CACxD,EAEA,GAAI,CACE,KAAK,sBACP,KAAK,cAAc,qBAAqB,oBACtC,6BACA,KAAK,mBACP,EACA,KAAK,cAAc,qBAChB,kBAAkB,EAClB,MAAM,IAAM,CAAC,CAAC,EAErB,MAAQ,CAER,CAEA,KAAK,oBAAsB,GAC3B,KAAK,gBAAgB,KAAK,EAAK,EAC/B,KAAK,oBAAsBI,EAAM,MAAM,EACvC,KAAK,yBAA2BA,EAAM,MAAM,EAE5C,KAAK,cAAgBoB,EACrB,KAAK,mBAAqB,KAAK,qBAAqB,CACtD,CAEA,MAAa,iBAAiC,CAC5C,MAAMC,EAAuB,KAAK,cAAc,qBAC3C,KAAK,sBACR,MAAMA,EAAqB,mBAAmB,EAC9C,KAAK,QAAQ,MAAM,eAAgB,CACjC,KAAM,CACJ,WAAY,KAAK,cAAc,qBAAqB,KACpD,UAAW,KAAK,cAAc,oBAAoB,KAClD,MAAO,KAAK,cAAc,oBAAoB,UAChD,CACF,CAAC,EACD,KAAK,oBAAsB,GAC3BA,EAAqB,iBACnB,6BACA,KAAK,mBACP,GAIF,MAAM,KAAK,OAAO,GAAG,EAErB,KAAK,sBAAwB,GAC7B,KAAK,gBAAgB,KAAK,EAAK,EAC/B,KAAK,oBAAsBrB,EAAM,MAAM,EAEvC,MAAMsB,EAAkB,IAAI,WAAW,CAACnB,EAAQ,EAAG,EAAG,EAAG,CAAC,CAAC,EAE3D,GAAI,CACF,MAAM,KAAK,2BAA2BmB,EAAgB,MAAM,EAE5D,MAAM,QAAQ,KAAK,CACjB,IAAI,QAAc,CAACR,EAASS,IAAW,CACrC,MAAMC,EAAU,WACd,IAAMD,EAAO,IAAI,MAAM,yBAAyB,CAAC,EACjD,GACF,EACME,EAAM,KAAK,gBAAgB,UAAWC,GAAU,CAChDA,IACF,aAAaF,CAAO,EACpBC,EAAI,YAAY,EAChBX,EAAQ,EAEZ,CAAC,CACH,CAAC,EACD,KAAK,OAAO,IAAI,EAAE,KAAK,IAAM,CAC3B,GAAI,CAAC,KAAK,iBAAiB,EACzB,MAAM,IAAIlB,EACR,yBACF,CAEJ,CAAC,CACH,CAAC,CACH,OAASe,EAAG,CACV,GAAI,CACFU,EAAqB,oBACnB,6BACA,KAAK,mBACP,EACA,MAAMA,EAAqB,kBAAkB,EAAE,MAAM,IAAM,CAAC,CAAC,CAC/D,QAAE,CACA,KAAK,oBAAsB,GAC3B,KAAK,gBAAgB,KAAK,EAAK,EAC/B,KAAK,oBAAsBrB,EAAM,MAAM,CACzC,CACA,MAAMW,CACR,QAAE,CACA,KAAK,sBAAwB,EAC/B,CACF,CAEQ,kBAA4B,CAClC,GAAI,CACF,MAAO,CAAC,CAAC,KAAK,cAAc,qBAAqB,QAAQ,OAAO,MAC5D,SACN,MAAQ,CACN,MAAO,EACT,CACF,CAEQ,yBAAyBA,EAAqB,CACpD,MAAMgB,EAAMhB,EACNiB,GACJ,OAAOD,GAAQ,UAAYA,IAAQ,MAAQ,SAAUA,EAChDA,EAAI,MAAQ,GACb,IACJ,SAAS,EACLR,GACJ,OAAOQ,GAAQ,UAAYA,IAAQ,MAAQ,YAAaA,EACnDA,EAAI,SAAW,GAChB,IAEH,SAAS,EACT,YAAY,EACf,OACEC,IAAS,gBACTT,EAAI,SAAS,6BAA6B,GAC1CA,EAAI,SAAS,eAAe,GAC5BA,EAAI,SAAS,gCAAgC,CAEjD,CAEQ,iBAAiBQ,EAAe,CACtC,KAAK,yBAAyB,IAAKb,GAAYA,EAAQf,EAAK4B,CAAG,CAAC,CAAC,EACjE,KAAK,yBAA2B3B,EAAM,MAAM,CAC9C,CAEQ,sBAA6B,CAC/B,KAAK,sBACP,KAAK,cAAc,qBAAqB,oBACtC,6BACA,KAAK,mBACP,EACA,KAAK,cAAc,qBAChB,kBAAkB,EAClB,MAAM,IAAM,CAAC,CAAC,EACjB,KAAK,oBAAsB,IAG7B,KAAK,gBAAgB,KAAK,EAAK,EAC/B,KAAK,oBAAsBA,EAAM,MAAM,EACvC,KAAK,yBAA2BA,EAAM,MAAM,CAC9C,CAEA,MAAc,OAAO6B,EAAY,CAC/B,OAAO,IAAI,QAASC,GAAM,WAAWA,EAAGD,CAAE,CAAC,CAC7C,CAEQ,oBAAuBE,GAAiB,CAC9C,MAAMC,EAAiBD,EAAM,OAC7B,GAAI,CAACC,EAAe,MAAO,OAC3B,MAAMC,EAAO,IAAI,WAAWD,EAAe,MAAM,MAAM,EAEvD,GAAI,CAAC,KAAK,gBAAgB,MAAO,CAC/B,GAAI,CAAC,KAAK,sBAAuB,CAC/B,KAAK,QAAQ,MAAM,+BAAgC,CAAE,KAAM,CAAE,KAAAC,CAAK,CAAE,CAAC,EACrE,MACF,CACA,GAAIA,EAAK,OAAS,GAAKA,EAAK,CAAC,IAAM9B,EAAQ,CACzC,KAAK,QAAQ,MAAM,2CAA4C,CAC7D,KAAM,CAAE,KAAA8B,CAAK,CACf,CAAC,EACD,MACF,CACA,KAAK,2BAA2BA,CAAI,EACpC,MACF,CAEA,KAAK,iBAAiBA,CAAI,CAC5B,EAEQ,2BAA2BC,EAA+B,CAChE,MAAMC,EAAYD,EAAkB,CAAC,EACrC,GACEC,IAAc,QACd,CAAC,OAAO,SAASA,CAAS,GAC1BA,GAAa,EAEb,MAAM,IAAI,MAAM,qCAAqC,EAGvD,MAAMC,EAAYD,EAClB,KAAK,oBAAsBnC,EAAM,GAAG,KAAK,mBAAmB,CAAE,UAAAoC,CAAU,CAAC,CAAC,EAC1E,KAAK,gBAAgB,KAAK,EAAI,CAChC,CAEQ,iBAAiBC,EAA2B,CAClD,KAAK,mBACF,YAAYA,CAAa,EACzB,IAAKC,GACJA,EAAc,IAAKC,GAAS,CAC1B,KAAK,QAAQ,MAAM,gBAAiB,CAAE,KAAM,CAAE,KAAAA,CAAK,CAAE,CAAC,EACtD,KAAK,yBAAyB,IAAKzB,GAAYA,EAAQb,EAAMsC,CAAI,CAAC,CAAC,EACnE,KAAK,yBAA2BvC,EAAM,MAAM,CAC9C,CAAC,CACH,EACC,QAAS2B,GAAQ,CAChB,KAAK,yBAAyB,IAAKb,GAAYA,EAAQf,EAAK4B,CAAG,CAAC,CAAC,EACjE,KAAK,yBAA2B3B,EAAM,MAAM,CAC9C,CAAC,CACL,CAEA,MAAc,2BAA2BwC,EAAkB,CACzD,MAAMC,EAAK,KAAK,cAAc,oBAE9B,GAAI,CAAC,KAAK,iBAAiB,EACzB,WAAK,qBAAqB,EACpB,IAAI7C,EAAoC,oBAAoB,EAGpE,MAAM8C,EAAS,OAAOD,EAAG,2BAA8B,WACjDE,EAAQ,OAAOF,EAAG,wBAA2B,WAGnD,GAAIA,EAAG,WAAW,sBAAwBC,EACxC,GAAI,CACF,MAAMD,EAAG,0BAA0BD,CAAG,EACtC,MACF,OAAS7B,EAAG,CACV,GAAI,KAAK,yBAAyBA,CAAC,GAAK,CAAC,KAAK,iBAAiB,EAC7D,WAAK,qBAAqB,EACpB,IAAIf,EAAoC,cAAc,CAGhE,CAGF,GAAI6C,EAAG,WAAW,OAASE,EAAO,CAChC,MAAMF,EAAG,uBAAuBD,CAAG,EACnC,MACF,CAEA,MAAM,IAAI,MAAM,8CAA8C,CAChE,CAEA,MAAc,wBAAwBI,EAAQ,IAAqB,CACjE,GACE,OAAK,qBACL,KAAK,gBAAgB,OACrB,KAAK,iBAAiB,GAIxB,OAAO,IAAI,QAAc,CAAC9B,EAASS,IAAW,CAC5C,MAAMsB,EAAe,KAAK,gBAAgB,UAAWnB,GAAU,CACxDA,GACD,KAAK,qBAAuB,KAAK,iBAAiB,IACpD,aAAaoB,CAAK,EAClBD,EAAa,YAAY,EACzB/B,EAAQ,EAEZ,CAAC,EAEKgC,EAAQ,WAAW,IAAM,CAC7BD,EAAa,YAAY,EACzBtB,EAAO,IAAI1B,EAA0B,gBAAgB,CAAC,CACxD,EAAG+C,CAAK,EAGN,KAAK,qBACL,KAAK,gBAAgB,OACrB,KAAK,iBAAiB,IAEtB,aAAaE,CAAK,EAClBD,EAAa,YAAY,EACzB/B,EAAQ,EAEZ,CAAC,CACH,CACF",
|
|
6
|
+
"names": ["DeviceDisconnectedWhileSendingError", "DeviceNotInitializedError", "SendApduTimeoutError", "Left", "Maybe", "Right", "BehaviorSubject", "MTU_OP", "WebBleApduSender", "initialDependencies", "loggerFactory", "apdu", "triggersDisconnection", "abortTimeout", "waitBudget", "e", "timeoutHandle", "responsePromise", "resolve", "result", "frames", "segmenter", "frame", "msg", "deps", "notifyCharacteristic", "mtuRequestFrame", "reject", "timeout", "sub", "ready", "err", "name", "ms", "r", "event", "characteristic", "data", "mtuResponseBuffer", "ledgerMtu", "frameSize", "incomingFrame", "maybeResponse", "resp", "buf", "ch", "hasWnr", "hasWr", "maxMs", "subscription", "timer"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Maybe as B,Right as L}from"purify-ts";import{afterEach as V,beforeEach as x,describe as S,expect as n,it as u,vi as t}from"vitest";import{MTU_OP as h,WebBleApduSender as U}from"./WebBleApduSender";class W{subscribers=[];tag;constructor(r,o){this.subscribers=r,this.tag=o}debug=t.fn();info=t.fn();warn=t.fn();error=t.fn()}function p(){const e=t.fn(),r=t.fn().mockResolvedValue(void 0);return{service:{device:{gatt:{connected:!0,disconnect:e}}},uuid:"mock-uuid",properties:{broadcast:!1,read:!1,writeWithoutResponse:!1,write:!1,notify:!1,indicate:!1,authenticatedSignedWrites:!1,reliableWrite:!1,writableAuxiliaries:!1},getDescriptor:t.fn().mockResolvedValue(void 0),getDescriptors:t.fn().mockResolvedValue([]),startNotifications:t.fn().mockResolvedValue(void 0),addEventListener:t.fn(),removeEventListener:t.fn(),stopNotifications:r,writeValueWithResponse:t.fn().mockResolvedValue(void 0),writeValueWithoutResponse:t.fn().mockResolvedValue(void 0),readValue:t.fn().mockResolvedValue(new DataView(new ArrayBuffer(0))),writeValue:t.fn().mockResolvedValue(void 0),dispatchEvent:t.fn(),oncharacteristicvaluechanged:null}}let d,a,C,b,E,s;const v=()=>new Promise(e=>setImmediate(e)),m=e=>new Promise(r=>setTimeout(r,e));x(()=>{d=p(),a=p(),d.properties.write=!0,C=t.fn().mockReturnValue({getFrames:e=>[{getRawData:()=>e}]}),b=t.fn().mockReturnValue({handleFrame:t.fn(e=>L(B.of({data:new Uint8Array([144,0])})))}),E=e=>new W([],e),s=new U({writeCharacteristic:d,notifyCharacteristic:a,apduSenderFactory:C,apduReceiverFactory:b},E)});V(()=>{t.restoreAllMocks()});S("WebBleApduSender",()=>{u("getDependencies returns initial chars",()=>{const e=s.getDependencies();n(e.writeCharacteristic).toBe(d),n(e.notifyCharacteristic).toBe(a)}),u("setupConnection negotiates MTU and listens",async()=>{const e=s.setupConnection();n(a.startNotifications).toHaveBeenCalled(),await v();const o=a.addEventListener.mock.calls.filter(([l])=>l==="characteristicvaluechanged")[0];if(!o)throw new Error("No event registered for 'characteristicvaluechanged'");const i=o[1];await m(150);const c=new Uint8Array([h,0,0,0,0,32]).buffer;i({target:{value:{buffer:c}}}),await n(e).resolves.toBeUndefined()}),u("sendApdu writes frames and resolves on notification",async()=>{const e=s.setupConnection();await v();const r=a.addEventListener.mock.calls.find(([w])=>w==="characteristicvaluechanged");if(!r)throw new Error("No event registered for 'characteristicvaluechanged'");const o=r[1];await m(150),o({target:{value:{buffer:new Uint8Array([h,0,0,0,0,32]).buffer}}}),await e;const i=new Uint8Array([1,2,3]),c=s.sendApdu(i);await v(),n(d.writeValueWithResponse).toHaveBeenCalled();const l=d.writeValueWithResponse.mock.calls.at(-1)?.[0];n(l).toBeInstanceOf(ArrayBuffer),n(Array.from(new Uint8Array(l))).toEqual(Array.from(i));const f=a.addEventListener.mock.calls.filter(([w])=>w==="characteristicvaluechanged"),g=f[f.length-1];if(!g)throw new Error("No APDU handler registered.");const[,R]=g,k=new Uint8Array([144,0]).buffer;R({target:{value:{buffer:k}}});const y=await c;n(y.isRight()).toBe(!0);const A=y.extract();n(A).toHaveProperty("data"),n(A.data).toEqual(new Uint8Array([144,0]))}),u("closeConnection calls disconnect",()=>{s.closeConnection(),n(a.service.device.gatt.disconnect).toHaveBeenCalled()}),u("setDependencies swaps characteristics and resets link (does not arm)",async()=>{const e=s.setupConnection();await v();const r=a.addEventListener.mock.calls.find(([f])=>f==="characteristicvaluechanged");if(!r)throw new Error("No event registered for 'characteristicvaluechanged'");const o=r[1];await m(150),o({target:{value:{buffer:new Uint8Array([h,0,0,0,0,32]).buffer}}}),await e;const i=p(),c=p();c.properties.write=!0,s.setDependencies({writeCharacteristic:c,notifyCharacteristic:i}),n(a.removeEventListener).toHaveBeenCalledWith("characteristicvaluechanged",n.any(Function)),n(i.startNotifications).not.toHaveBeenCalled(),n(i.addEventListener).not.toHaveBeenCalled();const l=s.getDependencies();n(l.notifyCharacteristic).toBe(i),n(l.writeCharacteristic).toBe(c)})});
|
|
2
|
+
//# sourceMappingURL=WebBleApduSender.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/api/transport/WebBleApduSender.test.ts"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-unsafe-assignment */\n/* eslint-disable @typescript-eslint/no-unsafe-member-access */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n type ApduReceiverServiceFactory,\n type ApduResponse,\n type ApduSenderServiceFactory,\n type LoggerPublisherService,\n} from \"@ledgerhq/device-management-kit\";\nimport { Maybe, Right } from \"purify-ts\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { MTU_OP, WebBleApduSender } from \"./WebBleApduSender\";\n\ntype EventListener = (event: Event) => void;\nclass LoggerStub implements LoggerPublisherService {\n subscribers: any[] = [];\n tag: string;\n constructor(subs: any[], tag: string) {\n this.subscribers = subs;\n this.tag = tag;\n }\n debug = vi.fn();\n info = vi.fn();\n warn = vi.fn();\n error = vi.fn();\n}\n\nfunction makeCharacteristic() {\n const disconnect = vi.fn();\n const stopNotifications = vi.fn().mockResolvedValue(undefined);\n return {\n service: { device: { gatt: { connected: true, disconnect } } },\n uuid: \"mock-uuid\",\n properties: {\n broadcast: false,\n read: false,\n writeWithoutResponse: false,\n write: false,\n notify: false,\n indicate: false,\n authenticatedSignedWrites: false,\n reliableWrite: false,\n writableAuxiliaries: false,\n },\n getDescriptor: vi.fn().mockResolvedValue(undefined),\n getDescriptors: vi.fn().mockResolvedValue([]),\n startNotifications: vi.fn().mockResolvedValue(undefined),\n addEventListener: vi.fn(),\n removeEventListener: vi.fn(),\n stopNotifications,\n writeValueWithResponse: vi.fn().mockResolvedValue(undefined),\n writeValueWithoutResponse: vi.fn().mockResolvedValue(undefined),\n readValue: vi.fn().mockResolvedValue(new DataView(new ArrayBuffer(0))),\n writeValue: vi.fn().mockResolvedValue(undefined),\n dispatchEvent: vi.fn(),\n oncharacteristicvaluechanged: null,\n };\n}\n\nlet writeChar: any;\nlet notifyChar: any;\nlet apduSenderFactory: ApduSenderServiceFactory;\nlet apduReceiverFactory: ApduReceiverServiceFactory;\nlet loggerFactory: (tag: string) => LoggerPublisherService;\nlet sender: WebBleApduSender;\n\nconst flushPromises = () =>\n new Promise<void>((resolve) => setImmediate(resolve));\nconst wait = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\nbeforeEach(() => {\n writeChar = makeCharacteristic();\n notifyChar = makeCharacteristic();\n\n // it requires the characteristic to advertise \"write\" or \"writeWithoutResponse\" to pick a write mode\n writeChar.properties.write = true;\n\n // class calls getRawData().slice().buffer, return a Uint8Array\n apduSenderFactory = vi.fn().mockReturnValue({\n getFrames: (apdu: Uint8Array) => [{ getRawData: () => apdu }],\n });\n\n apduReceiverFactory = vi.fn().mockReturnValue({\n handleFrame: vi.fn((_frame: Uint8Array) =>\n Right(Maybe.of({ data: new Uint8Array([0x90, 0x00]) } as ApduResponse)),\n ),\n });\n\n loggerFactory = (tag: string) => new LoggerStub([], tag);\n\n sender = new WebBleApduSender(\n {\n writeCharacteristic: writeChar,\n notifyCharacteristic: notifyChar,\n apduSenderFactory,\n apduReceiverFactory,\n },\n loggerFactory,\n );\n});\n\nafterEach(() => {\n vi.restoreAllMocks();\n});\n\ndescribe(\"WebBleApduSender\", () => {\n it(\"getDependencies returns initial chars\", () => {\n const deps = sender.getDependencies();\n expect(deps.writeCharacteristic).toBe(writeChar);\n expect(deps.notifyCharacteristic).toBe(notifyChar);\n });\n\n it(\"setupConnection negotiates MTU and listens\", async () => {\n // when\n const promise = sender.setupConnection();\n\n // then\n expect(notifyChar.startNotifications).toHaveBeenCalled();\n await flushPromises();\n\n const filteredCalls = (\n notifyChar.addEventListener.mock.calls as [string, EventListener][]\n ).filter(([event]) => event === \"characteristicvaluechanged\");\n\n const firstCall = filteredCalls[0];\n if (!firstCall) {\n throw new Error(\"No event registered for 'characteristicvaluechanged'\");\n }\n const handler = firstCall[1];\n\n // wait for the internal sleep so the handshake flag is set\n await wait(150);\n\n const mtuBuf = new Uint8Array([MTU_OP, 0, 0, 0, 0, 0x20]).buffer;\n\n handler({ target: { value: { buffer: mtuBuf } } } as unknown as Event);\n\n await expect(promise).resolves.toBeUndefined();\n });\n\n it(\"sendApdu writes frames and resolves on notification\", async () => {\n // given\n const setupPromise = sender.setupConnection();\n await flushPromises();\n\n const mtuCall = (\n notifyChar.addEventListener.mock.calls as [string, EventListener][]\n ).find(([event]) => event === \"characteristicvaluechanged\");\n if (!mtuCall)\n throw new Error(\"No event registered for 'characteristicvaluechanged'\");\n\n const mtuHandler = mtuCall[1];\n\n // wait past the handshake delay, then send the proper MTU frame\n await wait(150);\n mtuHandler({\n target: {\n value: { buffer: new Uint8Array([MTU_OP, 0, 0, 0, 0, 0x20]).buffer },\n },\n } as unknown as Event);\n\n await setupPromise;\n\n const apduCmd = new Uint8Array([0x01, 0x02, 0x03]);\n\n // when\n const promise = sender.sendApdu(apduCmd);\n\n // then check that the last write matches the APDU bytes\n await flushPromises();\n expect(writeChar.writeValueWithResponse).toHaveBeenCalled();\n\n const lastArg = writeChar.writeValueWithResponse.mock.calls.at(\n -1,\n )?.[0] as ArrayBuffer;\n expect(lastArg).toBeInstanceOf(ArrayBuffer);\n expect(Array.from(new Uint8Array(lastArg))).toEqual(Array.from(apduCmd));\n\n const filteredCalls = (\n notifyChar.addEventListener.mock.calls as [string, EventListener][]\n ).filter(([event]) => event === \"characteristicvaluechanged\");\n const lastCall = filteredCalls[filteredCalls.length - 1];\n if (!lastCall) throw new Error(\"No APDU handler registered.\");\n\n const [, apduHandler] = lastCall;\n\n // when\n const respBuf = new Uint8Array([0x90, 0x00]).buffer;\n apduHandler({ target: { value: { buffer: respBuf } } } as unknown as Event);\n\n // then\n const result = await promise;\n expect(result.isRight()).toBe(true);\n const extractedResult = result.extract();\n expect(extractedResult).toHaveProperty(\"data\");\n expect((extractedResult as ApduResponse).data).toEqual(\n new Uint8Array([0x90, 0x00]),\n );\n });\n\n it(\"closeConnection calls disconnect\", () => {\n sender.closeConnection();\n expect(notifyChar.service.device.gatt.disconnect).toHaveBeenCalled();\n });\n\n it(\"setDependencies swaps characteristics and resets link (does not arm)\", async () => {\n // given\n const setupPromise = sender.setupConnection();\n await flushPromises();\n\n const mtuCall = (\n notifyChar.addEventListener.mock.calls as [string, EventListener][]\n ).find(([event]) => event === \"characteristicvaluechanged\");\n if (!mtuCall)\n throw new Error(\"No event registered for 'characteristicvaluechanged'\");\n const mtuHandler = mtuCall[1];\n\n await wait(150);\n mtuHandler({\n target: {\n value: { buffer: new Uint8Array([MTU_OP, 0, 0, 0, 0, 0x20]).buffer },\n },\n } as unknown as Event);\n await setupPromise;\n\n const newNotify = makeCharacteristic();\n const newWrite = makeCharacteristic();\n newWrite.properties.write = true;\n\n // when\n sender.setDependencies({\n writeCharacteristic:\n newWrite as unknown as BluetoothRemoteGATTCharacteristic,\n notifyCharacteristic:\n newNotify as unknown as BluetoothRemoteGATTCharacteristic,\n });\n\n // then\n expect(notifyChar.removeEventListener).toHaveBeenCalledWith(\n \"characteristicvaluechanged\",\n expect.any(Function),\n );\n expect(newNotify.startNotifications).not.toHaveBeenCalled();\n expect(newNotify.addEventListener).not.toHaveBeenCalled();\n\n const deps = sender.getDependencies();\n expect(deps.notifyCharacteristic).toBe(newNotify);\n expect(deps.writeCharacteristic).toBe(newWrite);\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Maybe", "Right", "afterEach", "beforeEach", "describe", "expect", "it", "vi", "MTU_OP", "WebBleApduSender", "LoggerStub", "subs", "tag", "makeCharacteristic", "disconnect", "stopNotifications", "writeChar", "notifyChar", "apduSenderFactory", "apduReceiverFactory", "loggerFactory", "sender", "flushPromises", "resolve", "wait", "ms", "apdu", "_frame", "deps", "promise", "firstCall", "event", "handler", "mtuBuf", "setupPromise", "mtuCall", "mtuHandler", "apduCmd", "lastArg", "filteredCalls", "lastCall", "apduHandler", "respBuf", "result", "extractedResult", "newNotify", "newWrite"]
|
|
7
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{DeviceAlreadyConnectedError as
|
|
1
|
+
import{DeviceAlreadyConnectedError as D,DeviceConnectionStateMachine as f,GeneralDmkError as S,OpeningConnectionError as l,TransportConnectedDevice as m,UnknownDeviceError as h}from"@ledgerhq/device-management-kit";import{Left as g,Right as p}from"purify-ts";import{BehaviorSubject as w,from as _}from"rxjs";import{switchMap as T}from"rxjs/operators";import{ADVERTISING_DELAY as I,ADVERTISING_TIMEOUT as C,RECONNECT_DEVICE_TIMEOUT as b,RECONNECTION_LOOP_BACKOFF as E,REDISCOVER_TIMEOUT as B}from"../data/WebBleConfig";import{WebBleApduSender as M}from"./WebBleApduSender";const u="WEB-BLE-RN-STYLE";class R{constructor(e,i,t,n){this._deviceModelDataSource=e;this.loggerFactory=i;this._apduSenderFactory=t;this._apduReceiverFactory=n;this._logger=i("WebBleTransportRnStyle")}_logger;_connectionStateMachinesByDeviceId=new Map;_deviceRegistryById=new Map;_discoveredDevices$=new w([]);isSupported(){return typeof navigator<"u"&&!!navigator.bluetooth}getIdentifier(){return u}startDiscovering(){const e=this._deviceModelDataSource.getBluetoothServices().map(t=>({services:[t]})),i=this._deviceModelDataSource.getBluetoothServices();return _(navigator.bluetooth.requestDevice({filters:e,optionalServices:i})).pipe(T(async t=>{const{serviceUuid:n,ledgerServiceInfo:r}=await this._identifyLedgerGattService(t),c={id:t.id,deviceModel:r.deviceModel,transport:u};return this._deviceRegistryById.set(t.id,{device:t,serviceUuid:n,ledgerServiceInfo:r,discoveredDevice:c}),this._publishDiscoveredDevices(),c}))}stopDiscovering(){}listenToAvailableDevices(){return this._publishDiscoveredDevices(),this._discoveredDevices$.asObservable()}async connect(e){const i=this._deviceRegistryById.get(e.deviceId);if(!i)return g(new h(`Unknown device ${e.deviceId}`));if(this._connectionStateMachinesByDeviceId.has(e.deviceId))return g(new D(`Device ${e.deviceId} already connected`));try{const t=i.device;if(!t.gatt)throw new l("No GATT server available on device");t.gatt.connected||(await this._withTimeout(t.gatt.connect(),6e3,"GATT connect timed out"),await this._sleep(150));const{service:n,ledgerServiceInfo:r}=await this._getPrimaryLedgerGattService(t),{writeCharacteristic:c,notifyCharacteristic:d}=await this._resolveLedgerServiceCharacteristics(n,r),o=new M({writeCharacteristic:c,notifyCharacteristic:d,apduSenderFactory:this._apduSenderFactory,apduReceiverFactory:this._apduReceiverFactory},this.loggerFactory),v=new f({deviceId:e.deviceId,deviceApduSender:o,timeoutDuration:b,tryToReconnect:()=>{this._tryToReconnect(e.deviceId,e.onReconnect).catch(a=>this._logger.error("tryToReconnect() threw",{data:{e:a}}))},onTerminated:()=>{try{this._connectionStateMachinesByDeviceId.get(e.deviceId)?.closeConnection(),e.onDisconnect(e.deviceId)}finally{this._connectionStateMachinesByDeviceId.delete(e.deviceId);const a=this._deviceRegistryById.get(e.deviceId);a?.gattDisconnectListener&&(a.device.removeEventListener("gattserverdisconnected",a.gattDisconnectListener),a.gattDisconnectListener=void 0),this._publishDiscoveredDevices()}}});await o.setupConnection(),this._connectionStateMachinesByDeviceId.set(e.deviceId,v),i.serviceUuid=n.uuid,i.ledgerServiceInfo=r;const s=a=>this._handleGattServerDisconnected(e.deviceId);return i.gattDisconnectListener=s,t.addEventListener("gattserverdisconnected",s),this._publishDiscoveredDevices(),p(new m({id:e.deviceId,deviceModel:r.deviceModel,type:"BLE",transport:u,sendApdu:(...a)=>v.sendApdu(...a)}))}catch(t){return this._logger.error("connect() error",{data:{e:t}}),g(new l(t))}}async disconnect(e){const i=e.connectedDevice.id,t=this._connectionStateMachinesByDeviceId.get(i),n=this._deviceRegistryById.get(i);if(!t)return g(new h(`Unknown device ${i}`));try{return t.closeConnection(),this._connectionStateMachinesByDeviceId.delete(i),n?.gattDisconnectListener&&(n.device.removeEventListener("gattserverdisconnected",n.gattDisconnectListener),n.gattDisconnectListener=void 0),await this._safelyCancelGattConnection(i),this._publishDiscoveredDevices(),p(void 0)}catch(r){return g(new S({originalError:r}))}}_handleGattServerDisconnected(e){this._logger.debug(`[${e}] gattserverdisconnected`);const i=this._connectionStateMachinesByDeviceId.get(e);i&&i.eventDeviceDisconnected()}async _waitForAdvertisementInRange(e,i={}){const{startTimeoutMs:t=500,advTimeoutMs:n=1500}=i;if(typeof e.watchAdvertisements!="function")return!1;const r=new AbortController,c=setTimeout(()=>r.abort(),t),d=await e.watchAdvertisements({signal:r.signal}).then(()=>!0).catch(()=>!1);if(clearTimeout(c),!d)return!1;const o=await new Promise(v=>{const s=()=>{e.removeEventListener("advertisementreceived",s),v(!0)};e.addEventListener("advertisementreceived",s),setTimeout(()=>{e.removeEventListener("advertisementreceived",s),v(!1)},n)});return r.abort(),o}async _rediscoverPermittedDevice(e){if(typeof navigator.bluetooth.getDevices!="function")return null;try{this._logger.debug(`Attempting to rediscover device ${e}`);const t=(await navigator.bluetooth.getDevices()).find(n=>n.id===e)??null;if(t){const n=await this._waitForAdvertisementInRange(t,{startTimeoutMs:I,advTimeoutMs:C});this._logger.debug(`Rediscovered ${t.id}, inRange=${n}`)}return t}catch{return null}}async _tryToReconnect(e,i){for(await this._safelyCancelGattConnection(e);;){const t=this._deviceRegistryById.get(e),n=this._connectionStateMachinesByDeviceId.get(e);if(!t||!n){this._logger.debug(`[${e}] aborting reconnect: registry or state machine missing`);return}try{const r=await this._withTimeout(this._rediscoverPermittedDevice(e),B,"rediscovery timeout");if(!r)throw new Error("Device not found");if(!r.gatt)throw new Error("No GATT on device");try{await r.gatt.connect()}catch(a){this._logger.error(`[${e}] gatt.connect() failed`,{data:{e:a}}),r.gatt.connected&&r.gatt.disconnect()}const{service:c,ledgerServiceInfo:d}=await this._getPrimaryLedgerGattService(r),{writeCharacteristic:o,notifyCharacteristic:v}=await this._resolveLedgerServiceCharacteristics(c,d);n.setDependencies({writeCharacteristic:o,notifyCharacteristic:v}),await n.setupConnection(),n.eventDeviceConnected(),t.serviceUuid=c.uuid,t.ledgerServiceInfo=d,t.gattDisconnectListener&&r.removeEventListener("gattserverdisconnected",t.gattDisconnectListener);const s=a=>this._handleGattServerDisconnected(e);r.addEventListener("gattserverdisconnected",s),t.gattDisconnectListener=s,await i?.(e),this._publishDiscoveredDevices();return}catch(r){this._logger.error(`[${e}] reconnect attempt failed`,{data:{e:r}}),t?.device.gatt?.connected&&t.device.gatt.disconnect(),await this._sleep(E);continue}}}async _safelyCancelGattConnection(e){const i=this._deviceRegistryById.get(e);i&&(i.device.gatt?.connected&&i.device.gatt.disconnect(),await this._sleep(100))}async _identifyLedgerGattService(e){if(!e.gatt)throw new l("No GATT server available on device");try{await this._withTimeout(e.gatt.connect(),6e3,"connect timeout");const{service:i,ledgerServiceInfo:t}=await this._getPrimaryLedgerGattService(e);return{serviceUuid:i.uuid,ledgerServiceInfo:t}}finally{e.gatt?.disconnect(),await this._sleep(200)}}async _getPrimaryLedgerGattService(e){const i=this._deviceModelDataSource.getBluetoothServices(),t=this._deviceRegistryById.get(e.id)?.serviceUuid,n=t?[t,...i.filter(r=>r!==t)]:i.slice();for(const r of n)try{const c=await e.gatt.getPrimaryService(r),o=this._deviceModelDataSource.getBluetoothServicesInfos()[c.uuid];if(!o)throw new h(e.name||"");return{service:c,ledgerServiceInfo:o}}catch(c){if(c?.name==="SecurityError")throw new l(`Missing Web Bluetooth permission for service ${r}. Add it to optionalServices in requestDevice().`);try{const o=(await e.gatt.getPrimaryServices()).find(v=>v.uuid.toLowerCase()===r.toLowerCase());if(o){const s=this._deviceModelDataSource.getBluetoothServicesInfos()[o.uuid];if(!s)throw new h(e.name||"");return{service:o,ledgerServiceInfo:s}}}catch{this._logger.error("Failed to get primary services",{data:{deviceId:e.id}})}}throw new l("Ledger GATT service not found")}async _resolveLedgerServiceCharacteristics(e,i){const t=await e.getCharacteristic(i.notifyUuid);return{writeCharacteristic:await this._findWritableCharacteristic(e,i),notifyCharacteristic:t}}async _findWritableCharacteristic(e,i){const t=[i.writeCmdUuid,i.writeUuid].filter(Boolean),n=[];for(const r of t)try{const c=await e.getCharacteristic(r);if(n.push(c),c.properties.writeWithoutResponse||c.properties.write)return c}catch{this._logger.error("Failed to get write characteristic",{data:{deviceId:e.device.id}})}throw new l("No write characteristic available")}_publishDiscoveredDevices(){const e=[];for(const[t]of this._connectionStateMachinesByDeviceId){const n=this._deviceRegistryById.get(t);n?.ledgerServiceInfo&&e.push({id:t,deviceModel:n.ledgerServiceInfo.deviceModel,transport:u})}const i=Array.from(this._deviceRegistryById.values()).map(t=>t.discoveredDevice).filter(t=>!this._connectionStateMachinesByDeviceId.has(t.id));this._discoveredDevices$.next([...e,...i])}_withTimeout(e,i,t,n){return new Promise((r,c)=>{const d=setTimeout(()=>{c(new l(t)),n?.()},i);e.then(o=>{clearTimeout(d),r(o)},o=>{this._logger.error("withTimeout() promise rejected",{data:{e:o}}),clearTimeout(d),c(o)})})}_sleep(e){return new Promise(i=>setTimeout(i,e))}}const F=({deviceModelDataSource:y,loggerServiceFactory:e,apduSenderServiceFactory:i,apduReceiverServiceFactory:t})=>new R(y,e,i,t);export{R as WebBleTransport,u as webBleIdentifier,F as webBleTransportFactory};
|
|
2
2
|
//# sourceMappingURL=WebBleTransport.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/api/transport/WebBleTransport.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type ApduReceiverServiceFactory,\n type ApduSenderServiceFactory,\n type BleDeviceInfos,\n type ConnectError,\n type ConnectionType,\n DeviceAlreadyConnectedError,\n type DeviceId,\n type DeviceModelDataSource,\n DeviceNotRecognizedError,\n type DisconnectHandler,\n type DmkError,\n type LoggerPublisherService,\n NoAccessibleDeviceError,\n OpeningConnectionError,\n type Transport,\n TransportConnectedDevice,\n type TransportDiscoveredDevice,\n type TransportFactory,\n type TransportIdentifier,\n UnknownDeviceError,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, EitherAsync, Left, Maybe, Right } from \"purify-ts\";\nimport { from, type Observable, switchMap, timer } from \"rxjs\";\nimport { v4 as uuid } from \"uuid\";\n\nimport { RECONNECT_DEVICE_TIMEOUT } from \"@api/data/WebBleConfig\";\nimport {\n BleDeviceGattServerError,\n BleTransportNotSupportedError,\n} from \"@api/model/Errors\";\nimport { BleDeviceConnection } from \"@api/transport/BleDeviceConnection\";\n\ntype PromptDeviceAccessError =\n | NoAccessibleDeviceError\n | BleTransportNotSupportedError;\n\n// An attempt to manage the state of several devices with one transport. Not final.\ntype WebBleInternalDevice = {\n id: DeviceId;\n bleDevice: BluetoothDevice;\n bleDeviceInfos: BleDeviceInfos;\n bleGattService: BluetoothRemoteGATTService;\n discoveredDevice: TransportDiscoveredDevice;\n};\n\nexport const webBleIdentifier: TransportIdentifier = \"WEB-BLE\";\n\nexport class WebBleTransport implements Transport {\n private readonly _connectedDevices: Array<BluetoothDevice>;\n private readonly _internalDevicesById: Map<DeviceId, WebBleInternalDevice>;\n private _deviceConnectionById: Map<DeviceId, BleDeviceConnection>;\n private _disconnectionHandlersById: Map<DeviceId, () => void>;\n private _logger: LoggerPublisherService;\n private readonly connectionType: ConnectionType = \"BLE\";\n private readonly identifier: TransportIdentifier = webBleIdentifier;\n\n constructor(\n private readonly _deviceModelDataSource: DeviceModelDataSource,\n private readonly _loggerServiceFactory: (\n tag: string,\n ) => LoggerPublisherService,\n private readonly _apduSenderFactory: ApduSenderServiceFactory,\n private readonly _apduReceiverFactory: ApduReceiverServiceFactory,\n ) {\n this._connectedDevices = [];\n this._internalDevicesById = new Map();\n this._deviceConnectionById = new Map();\n this._disconnectionHandlersById = new Map();\n this._logger = _loggerServiceFactory(\"WebBleTransport\");\n }\n\n /**\n * Get the Bluetooth API if supported or error\n * @returns `Either<BleTransportNotSupportedError, Bluetooth>`\n */\n private getBluetoothApi(): Either<BleTransportNotSupportedError, Bluetooth> {\n if (this.isSupported()) {\n return Right(navigator.bluetooth);\n }\n\n return Left(new BleTransportNotSupportedError(\"WebBle not supported\"));\n }\n\n isSupported(): boolean {\n try {\n const result = !!navigator?.bluetooth;\n return result;\n } catch {\n return false;\n }\n }\n\n getIdentifier(): TransportIdentifier {\n return this.identifier;\n }\n\n listenToKnownDevices(): Observable<TransportDiscoveredDevice[]> {\n return from([]);\n }\n\n /**\n * Get Bluetooth GATT Primary service that is used to get writeCharacteristic and notifyCharacteristic\n * @param bleDevice\n * @private\n */\n private async getBleGattService(\n bleDevice: BluetoothDevice,\n ): Promise<Either<BleDeviceGattServerError, BluetoothRemoteGATTService>> {\n if (!bleDevice.gatt) {\n return Left(new BleDeviceGattServerError(\"Device gatt not found\"));\n }\n try {\n const [bleGattService] = await bleDevice.gatt.getPrimaryServices();\n if (!bleGattService) {\n return Left(\n new BleDeviceGattServerError(\"bluetooth service not found\"),\n );\n }\n return Right(bleGattService);\n } catch (e) {\n return Left(new BleDeviceGattServerError(e));\n }\n }\n\n /**\n * BleDeviceInfos to map primary service uuid to device model & characteristics uuid\n * @param bleGattService\n * @private\n */\n private getBleDeviceInfos(\n bleGattService: BluetoothRemoteGATTService,\n ): Either<DeviceNotRecognizedError, BleDeviceInfos> {\n const serviceToBleInfos =\n this._deviceModelDataSource.getBluetoothServicesInfos();\n const bleDeviceInfos = serviceToBleInfos[bleGattService.uuid];\n\n if (!bleDeviceInfos) {\n this._logger.error(\n `Device not recognized: ${bleGattService.device.name}`,\n );\n return Left(\n new DeviceNotRecognizedError(\n `Device not recognized: ${bleGattService.device.name}`,\n ),\n );\n }\n return Right(bleDeviceInfos);\n }\n\n /**\n * Prompt device selection in navigator\n *\n * @private\n */\n private promptDeviceAccess(): EitherAsync<\n PromptDeviceAccessError,\n BluetoothDevice\n > {\n return EitherAsync(async ({ liftEither, throwE }) => {\n const bluetoothApi = await liftEither(this.getBluetoothApi());\n let bleDevice: BluetoothDevice;\n\n try {\n bleDevice = await bluetoothApi.requestDevice({\n filters: this._deviceModelDataSource\n .getBluetoothServices()\n .map((serviceUuid) => ({\n services: [serviceUuid],\n })),\n });\n } catch (error) {\n return throwE(new NoAccessibleDeviceError(error));\n }\n\n return bleDevice;\n });\n }\n\n /**\n * Generate a discovered device from BluetoothDevice, BleGATT primary service and BLE device infos\n * @param bleDeviceInfos\n * @private\n */\n private getDiscoveredDeviceFrom(\n bleDeviceInfos: BleDeviceInfos,\n ): TransportDiscoveredDevice {\n return {\n id: uuid(),\n deviceModel: bleDeviceInfos.deviceModel,\n transport: this.identifier,\n };\n }\n\n /**\n * Generate an InternalDevice from a unique id, a BluetoothDevice, BleGATT primary service and BLE device infos\n * @param discoveredDevice\n * @param bleDevice\n * @param bleDeviceInfos\n * @param bleGattService\n * @private\n */\n private setInternalDeviceFrom(\n discoveredDevice: TransportDiscoveredDevice,\n bleDevice: BluetoothDevice,\n bleDeviceInfos: BleDeviceInfos,\n bleGattService: BluetoothRemoteGATTService,\n ) {\n const internalDevice: WebBleInternalDevice = {\n id: discoveredDevice.id,\n bleDevice,\n bleGattService,\n bleDeviceInfos,\n discoveredDevice,\n };\n\n this._logger.debug(\n `Discovered device ${internalDevice.id} ${discoveredDevice.deviceModel.productName}`,\n );\n this._internalDevicesById.set(internalDevice.id, internalDevice);\n }\n\n /**\n * Main method to get a device from a button click handler\n * The GATT connection is done here in order to populate TransportDiscoveredDevice with deviceModel\n */\n startDiscovering(): Observable<TransportDiscoveredDevice> {\n this._logger.debug(\"startDiscovering\");\n\n return from(this.promptDeviceAccess()).pipe(\n switchMap(async (errorOrBleDevice) =>\n EitherAsync(async ({ liftEither, fromPromise }) => {\n const bleDevice = await liftEither(errorOrBleDevice);\n if (bleDevice.gatt) {\n try {\n await bleDevice.gatt.connect();\n } catch (error) {\n throw new OpeningConnectionError(error);\n }\n }\n try {\n const bleGattService = await fromPromise(\n this.getBleGattService(bleDevice),\n );\n const bleDeviceInfos = await liftEither(\n this.getBleDeviceInfos(bleGattService),\n );\n const discoveredDevice =\n this.getDiscoveredDeviceFrom(bleDeviceInfos);\n this.setInternalDeviceFrom(\n discoveredDevice,\n bleDevice,\n bleDeviceInfos,\n bleGattService,\n );\n return discoveredDevice;\n } catch (error) {\n this._logger.error(\"Error while discovering device\", {\n data: { error, bleDevice },\n });\n if (bleDevice.forget) {\n await bleDevice.forget();\n }\n throw error;\n }\n }).caseOf({\n Right: (discoveredDevice) => discoveredDevice,\n Left: (error) => {\n this._logger.error(\"Error while getting accessible device\", {\n data: { error },\n });\n throw error;\n },\n }),\n ),\n );\n }\n\n stopDiscovering(): void {\n this._logger.debug(\"stopDiscovering\");\n }\n\n /**\n * Connect to a BLE device and update the internal state of the associated device\n * Handle ondisconnect event on the device in order to try a reconnection\n */\n async connect({\n deviceId,\n onDisconnect,\n }: {\n deviceId: DeviceId;\n onDisconnect: DisconnectHandler;\n }): Promise<Either<ConnectError, TransportConnectedDevice>> {\n const internalDevice = this._internalDevicesById.get(deviceId);\n\n if (!internalDevice) {\n this._logger.error(`Unknown device ${deviceId}`, {\n data: { internalDevices: this._internalDevicesById },\n });\n this._logger.debug(\"Available devices\", {\n data: { devices: this._internalDevicesById },\n });\n return Left(new UnknownDeviceError(`Unknown device ${deviceId}`));\n }\n // if device already connected, remove device id from internal state and remove error\n if (this._connectedDevices.includes(internalDevice.bleDevice)) {\n this._internalDevicesById.delete(deviceId);\n return Left(new DeviceAlreadyConnectedError(\"Device already connected\"));\n }\n\n const {\n discoveredDevice: { deviceModel },\n } = internalDevice;\n\n try {\n const [writeCharacteristic, notifyCharacteristic] = await Promise.all([\n internalDevice.bleGattService.getCharacteristic(\n internalDevice.bleDeviceInfos.writeCmdUuid,\n ),\n internalDevice.bleGattService.getCharacteristic(\n internalDevice.bleDeviceInfos.notifyUuid,\n ),\n ]);\n\n const deviceConnection = new BleDeviceConnection(\n {\n writeCharacteristic,\n notifyCharacteristic,\n apduReceiverFactory: this._apduReceiverFactory,\n apduSenderFactory: this._apduSenderFactory,\n },\n this._loggerServiceFactory,\n );\n\n await deviceConnection.setup();\n\n const connectedDevice = new TransportConnectedDevice({\n sendApdu: (apdu, triggersDisconnection) =>\n deviceConnection.sendApdu(apdu, triggersDisconnection),\n deviceModel,\n id: deviceId,\n type: this.connectionType,\n transport: this.identifier,\n });\n\n internalDevice.bleDevice.ongattserverdisconnected =\n this._getDeviceDisconnectedHandler(internalDevice, deviceConnection);\n\n this._deviceConnectionById.set(internalDevice.id, deviceConnection);\n this._disconnectionHandlersById.set(internalDevice.id, () => {\n this.disconnect({ connectedDevice }).then(() => onDisconnect(deviceId));\n });\n\n this._connectedDevices.push(internalDevice.bleDevice);\n\n return Right(connectedDevice);\n } catch (error) {\n if (internalDevice.bleDevice.forget) {\n await internalDevice.bleDevice.forget();\n }\n\n this._internalDevicesById.delete(deviceId);\n\n this._logger.error(\"Error while getting characteristics\", {\n data: { error },\n });\n\n return Left(new OpeningConnectionError(error));\n }\n }\n\n /**\n * Get the device disconnected handler\n * @param internalDevice WebBleInternalDevice\n * @param deviceConnection BleDeviceConnection\n * @returns async () => void\n * @private\n */\n private _getDeviceDisconnectedHandler(\n internalDevice: WebBleInternalDevice,\n deviceConnection: BleDeviceConnection,\n ) {\n return async () => {\n // start a timer to disconnect the device if it does not reconnect\n const disconnectObserver = timer(RECONNECT_DEVICE_TIMEOUT).subscribe(\n () => {\n this._logger.debug(\"disconnection timer over\");\n // retrieve the disconnect handler and call it\n const disconnectHandler = Maybe.fromNullable(\n this._disconnectionHandlersById.get(internalDevice.id),\n );\n disconnectHandler.map((handler) => handler());\n },\n );\n\n // connect to the navigator device\n await internalDevice.bleDevice.gatt?.connect();\n\n // cancel disconnection timeout\n disconnectObserver.unsubscribe();\n\n // retrieve new ble characteristics\n const service = await this.getBleGattService(internalDevice.bleDevice);\n\n if (service.isRight()) {\n const [writeC, notifyC] = await Promise.all([\n service\n .extract()\n .getCharacteristic(internalDevice.bleDeviceInfos.writeCmdUuid),\n service\n .extract()\n .getCharacteristic(internalDevice.bleDeviceInfos.notifyUuid),\n ]);\n\n // reconnect device connection\n await deviceConnection.reconnect(writeC, notifyC);\n }\n };\n }\n\n /**\n * Disconnect from a BLE device and delete its handlers\n *\n * @param params { connectedDevice: TransportConnectedDevice }\n */\n async disconnect(params: {\n connectedDevice: TransportConnectedDevice;\n }): Promise<Either<DmkError, void>> {\n // retrieve internal device\n const maybeInternalDevice = Maybe.fromNullable(\n this._internalDevicesById.get(params.connectedDevice.id),\n );\n\n this._logger.debug(\"disconnect device\", {\n data: { connectedDevice: params.connectedDevice },\n });\n\n if (maybeInternalDevice.isNothing()) {\n this._logger.error(`Unknown device ${params.connectedDevice.id}`);\n\n return Promise.resolve(\n Left(\n new UnknownDeviceError(`Unknown device ${params.connectedDevice.id}`),\n ),\n );\n }\n\n maybeInternalDevice.map((device) => {\n const { bleDevice } = device;\n\n // retrieve device connection and disconnect it\n const maybeDeviceConnection = Maybe.fromNullable(\n this._deviceConnectionById.get(device.id),\n );\n\n maybeDeviceConnection.map((dConnection) => dConnection.disconnect());\n\n // disconnect device gatt server\n if (bleDevice.gatt?.connected) {\n bleDevice.gatt.disconnect();\n }\n // clean up objects\n this._internalDevicesById.delete(device.id);\n this._deviceConnectionById.delete(device.id);\n this._disconnectionHandlersById.delete(device.id);\n\n if (this._connectedDevices.includes(bleDevice)) {\n delete this._connectedDevices[\n this._connectedDevices.indexOf(bleDevice)\n ];\n }\n });\n\n return Promise.resolve(Right(undefined));\n }\n}\n\nexport const webBleTransportFactory: TransportFactory = ({\n deviceModelDataSource,\n loggerServiceFactory,\n apduSenderServiceFactory,\n apduReceiverServiceFactory,\n}) =>\n new WebBleTransport(\n deviceModelDataSource,\n loggerServiceFactory,\n apduSenderServiceFactory,\n apduReceiverServiceFactory,\n );\n"],
|
|
5
|
-
"mappings": "AAAA,
|
|
6
|
-
"names": ["DeviceAlreadyConnectedError", "
|
|
4
|
+
"sourcesContent": ["import {\n type ApduReceiverServiceFactory,\n type ApduSenderServiceFactory,\n type ConnectError,\n DeviceAlreadyConnectedError,\n DeviceConnectionStateMachine,\n type DeviceModelDataSource,\n type DmkError,\n GeneralDmkError,\n type LoggerPublisherService,\n OpeningConnectionError,\n type Transport,\n TransportConnectedDevice,\n type TransportDeviceModel,\n type TransportDiscoveredDevice,\n type TransportFactory,\n type TransportIdentifier,\n UnknownDeviceError,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Right } from \"purify-ts\";\nimport { BehaviorSubject, from, type Observable } from \"rxjs\";\nimport { switchMap } from \"rxjs/operators\";\n\nimport {\n ADVERTISING_DELAY,\n ADVERTISING_TIMEOUT,\n RECONNECT_DEVICE_TIMEOUT,\n RECONNECTION_LOOP_BACKOFF,\n REDISCOVER_TIMEOUT,\n} from \"@api/data/WebBleConfig\";\n\nimport {\n WebBleApduSender,\n type WebBleApduSenderDependencies,\n} from \"./WebBleApduSender\";\n\nexport const webBleIdentifier: TransportIdentifier = \"WEB-BLE-RN-STYLE\";\n\ntype DeviceRegistryEntry = {\n device: BluetoothDevice;\n serviceUuid?: string;\n ledgerServiceInfo?: {\n writeCmdUuid: string;\n writeUuid?: string;\n notifyUuid: string;\n deviceModel: TransportDeviceModel;\n };\n discoveredDevice: TransportDiscoveredDevice;\n gattDisconnectListener?: (ev: Event) => void;\n};\n\nexport class WebBleTransport implements Transport {\n private _logger: LoggerPublisherService;\n\n private _connectionStateMachinesByDeviceId = new Map<\n string,\n DeviceConnectionStateMachine<WebBleApduSenderDependencies>\n >();\n private _deviceRegistryById = new Map<string, DeviceRegistryEntry>();\n private _discoveredDevices$ = new BehaviorSubject<\n TransportDiscoveredDevice[]\n >([]);\n\n constructor(\n private readonly _deviceModelDataSource: DeviceModelDataSource,\n private loggerFactory: (tag: string) => LoggerPublisherService,\n private readonly _apduSenderFactory: ApduSenderServiceFactory,\n private readonly _apduReceiverFactory: ApduReceiverServiceFactory,\n ) {\n this._logger = loggerFactory(\"WebBleTransportRnStyle\");\n }\n\n isSupported(): boolean {\n return typeof navigator !== \"undefined\" && !!navigator.bluetooth;\n }\n\n getIdentifier(): TransportIdentifier {\n return webBleIdentifier;\n }\n\n startDiscovering(): Observable<TransportDiscoveredDevice> {\n const bluetoothServiceUuids = this._deviceModelDataSource\n .getBluetoothServices()\n .map((serviceUuid) => ({ services: [serviceUuid] }));\n const allOptionalServiceUuids =\n this._deviceModelDataSource.getBluetoothServices();\n\n return from(\n navigator.bluetooth.requestDevice({\n filters: bluetoothServiceUuids,\n optionalServices: allOptionalServiceUuids,\n }),\n ).pipe(\n switchMap(async (bluetoothDevice) => {\n const { serviceUuid, ledgerServiceInfo } =\n await this._identifyLedgerGattService(bluetoothDevice);\n\n const discoveredDevice: TransportDiscoveredDevice = {\n id: bluetoothDevice.id,\n deviceModel: ledgerServiceInfo.deviceModel,\n transport: webBleIdentifier,\n };\n\n this._deviceRegistryById.set(bluetoothDevice.id, {\n device: bluetoothDevice,\n serviceUuid,\n ledgerServiceInfo,\n discoveredDevice,\n });\n\n this._publishDiscoveredDevices();\n return discoveredDevice;\n }),\n );\n }\n\n stopDiscovering(): void {\n /* no-op on Web Bluetooth */\n }\n\n listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]> {\n this._publishDiscoveredDevices();\n return this._discoveredDevices$.asObservable();\n }\n\n async connect(params: {\n deviceId: string;\n onDisconnect: (deviceId: string) => void;\n onReconnect?: (deviceId: string) => Promise<void> | void;\n }): Promise<Either<ConnectError, TransportConnectedDevice>> {\n const registryEntry = this._deviceRegistryById.get(params.deviceId);\n if (!registryEntry)\n return Left(new UnknownDeviceError(`Unknown device ${params.deviceId}`));\n\n if (this._connectionStateMachinesByDeviceId.has(params.deviceId)) {\n return Left(\n new DeviceAlreadyConnectedError(\n `Device ${params.deviceId} already connected`,\n ),\n );\n }\n\n try {\n const bluetoothDevice = registryEntry.device;\n if (!bluetoothDevice.gatt) {\n throw new OpeningConnectionError(\"No GATT server available on device\");\n }\n\n if (!bluetoothDevice.gatt.connected) {\n await this._withTimeout(\n bluetoothDevice.gatt.connect(),\n 6000,\n \"GATT connect timed out\",\n );\n await this._sleep(150);\n }\n\n const { service: ledgerService, ledgerServiceInfo } =\n await this._getPrimaryLedgerGattService(bluetoothDevice);\n const {\n writeCharacteristic: gattWriteCharacteristic,\n notifyCharacteristic: gattNotifyCharacteristic,\n } = await this._resolveLedgerServiceCharacteristics(\n ledgerService,\n ledgerServiceInfo,\n );\n\n const apduSender = new WebBleApduSender(\n {\n writeCharacteristic: gattWriteCharacteristic,\n notifyCharacteristic: gattNotifyCharacteristic,\n apduSenderFactory: this._apduSenderFactory,\n apduReceiverFactory: this._apduReceiverFactory,\n },\n this.loggerFactory,\n );\n\n const connectionStateMachine =\n new DeviceConnectionStateMachine<WebBleApduSenderDependencies>({\n deviceId: params.deviceId,\n deviceApduSender: apduSender,\n timeoutDuration: RECONNECT_DEVICE_TIMEOUT,\n tryToReconnect: () => {\n this._tryToReconnect(params.deviceId, params.onReconnect).catch(\n (e) =>\n this._logger.error(\"tryToReconnect() threw\", { data: { e } }),\n );\n },\n onTerminated: () => {\n try {\n this._connectionStateMachinesByDeviceId\n .get(params.deviceId)\n ?.closeConnection();\n params.onDisconnect(params.deviceId);\n } finally {\n this._connectionStateMachinesByDeviceId.delete(params.deviceId);\n const entry = this._deviceRegistryById.get(params.deviceId);\n if (entry?.gattDisconnectListener) {\n entry.device.removeEventListener(\n \"gattserverdisconnected\",\n entry.gattDisconnectListener,\n );\n entry.gattDisconnectListener = undefined;\n }\n this._publishDiscoveredDevices();\n }\n },\n });\n\n await apduSender.setupConnection();\n\n this._connectionStateMachinesByDeviceId.set(\n params.deviceId,\n connectionStateMachine,\n );\n registryEntry.serviceUuid = ledgerService.uuid;\n registryEntry.ledgerServiceInfo = ledgerServiceInfo;\n\n const onGattDisconnected = (_ev: Event) =>\n this._handleGattServerDisconnected(params.deviceId);\n registryEntry.gattDisconnectListener = onGattDisconnected;\n bluetoothDevice.addEventListener(\n \"gattserverdisconnected\",\n onGattDisconnected,\n );\n\n this._publishDiscoveredDevices();\n\n return Right(\n new TransportConnectedDevice({\n id: params.deviceId,\n deviceModel: ledgerServiceInfo.deviceModel,\n type: \"BLE\",\n transport: webBleIdentifier,\n sendApdu: (...apduArgs) =>\n connectionStateMachine.sendApdu(...apduArgs),\n }),\n );\n } catch (e) {\n this._logger.error(\"connect() error\", { data: { e } });\n return Left(new OpeningConnectionError(e));\n }\n }\n\n async disconnect(params: {\n connectedDevice: TransportConnectedDevice;\n }): Promise<Either<DmkError, void>> {\n const deviceId = params.connectedDevice.id;\n const connectionStateMachine =\n this._connectionStateMachinesByDeviceId.get(deviceId);\n const registryEntry = this._deviceRegistryById.get(deviceId);\n if (!connectionStateMachine)\n return Left(new UnknownDeviceError(`Unknown device ${deviceId}`));\n\n try {\n connectionStateMachine.closeConnection();\n this._connectionStateMachinesByDeviceId.delete(deviceId);\n if (registryEntry?.gattDisconnectListener) {\n registryEntry.device.removeEventListener(\n \"gattserverdisconnected\",\n registryEntry.gattDisconnectListener,\n );\n registryEntry.gattDisconnectListener = undefined;\n }\n await this._safelyCancelGattConnection(deviceId);\n this._publishDiscoveredDevices();\n return Right(undefined);\n } catch (e) {\n return Left(new GeneralDmkError({ originalError: e }));\n }\n }\n\n private _handleGattServerDisconnected(deviceId: string) {\n this._logger.debug(`[${deviceId}] gattserverdisconnected`);\n const connectionStateMachine =\n this._connectionStateMachinesByDeviceId.get(deviceId);\n if (!connectionStateMachine) return;\n connectionStateMachine.eventDeviceDisconnected();\n }\n\n private async _waitForAdvertisementInRange(\n device: BluetoothDevice,\n opts: { startTimeoutMs?: number; advTimeoutMs?: number } = {},\n ): Promise<boolean> {\n const { startTimeoutMs = 500, advTimeoutMs = 1500 } = opts;\n if (typeof device.watchAdvertisements !== \"function\") return false;\n\n const abortController = new AbortController();\n const startTimer = setTimeout(\n () => abortController.abort(),\n startTimeoutMs,\n );\n const advertisementsStarted = await device\n .watchAdvertisements({ signal: abortController.signal })\n .then(() => true)\n .catch(() => false);\n clearTimeout(startTimer);\n if (!advertisementsStarted) return false;\n\n const advertisementSeen = await new Promise<boolean>((resolve) => {\n const handleAdvertisement = () => {\n device.removeEventListener(\n \"advertisementreceived\",\n handleAdvertisement,\n );\n resolve(true);\n };\n device.addEventListener(\"advertisementreceived\", handleAdvertisement);\n setTimeout(() => {\n device.removeEventListener(\n \"advertisementreceived\",\n handleAdvertisement,\n );\n resolve(false);\n }, advTimeoutMs);\n });\n\n abortController.abort();\n return advertisementSeen;\n }\n\n private async _rediscoverPermittedDevice(\n targetDeviceId: string,\n ): Promise<BluetoothDevice | null> {\n if (typeof navigator.bluetooth.getDevices !== \"function\") return null;\n try {\n this._logger.debug(`Attempting to rediscover device ${targetDeviceId}`);\n const permittedDevices = await navigator.bluetooth.getDevices();\n const matchingDevice =\n permittedDevices.find((d) => d.id === targetDeviceId) ?? null;\n\n if (matchingDevice) {\n const isInRange = await this._waitForAdvertisementInRange(\n matchingDevice,\n {\n startTimeoutMs: ADVERTISING_DELAY,\n advTimeoutMs: ADVERTISING_TIMEOUT,\n },\n );\n this._logger.debug(\n `Rediscovered ${matchingDevice.id}, inRange=${isInRange}`,\n );\n }\n return matchingDevice;\n } catch {\n return null;\n }\n }\n\n private async _tryToReconnect(\n deviceId: string,\n onReconnect?: (id: string) => Promise<void> | void,\n ) {\n await this._safelyCancelGattConnection(deviceId);\n\n while (true) {\n const registryEntry = this._deviceRegistryById.get(deviceId);\n const connectionStateMachine =\n this._connectionStateMachinesByDeviceId.get(deviceId);\n\n if (!registryEntry || !connectionStateMachine) {\n this._logger.debug(\n `[${deviceId}] aborting reconnect: registry or state machine missing`,\n );\n return;\n }\n\n try {\n const rediscoveredDevice = await this._withTimeout(\n this._rediscoverPermittedDevice(deviceId),\n REDISCOVER_TIMEOUT,\n \"rediscovery timeout\",\n );\n\n if (!rediscoveredDevice) throw new Error(\"Device not found\");\n\n if (!rediscoveredDevice.gatt) throw new Error(\"No GATT on device\");\n\n try {\n await rediscoveredDevice.gatt.connect();\n } catch (e) {\n this._logger.error(`[${deviceId}] gatt.connect() failed`, {\n data: { e },\n });\n if (rediscoveredDevice.gatt.connected)\n rediscoveredDevice.gatt.disconnect();\n }\n\n const { service: ledgerService, ledgerServiceInfo } =\n await this._getPrimaryLedgerGattService(rediscoveredDevice);\n\n const {\n writeCharacteristic: gattWriteCharacteristic,\n notifyCharacteristic: gattNotifyCharacteristic,\n } = await this._resolveLedgerServiceCharacteristics(\n ledgerService,\n ledgerServiceInfo,\n );\n\n connectionStateMachine.setDependencies({\n writeCharacteristic: gattWriteCharacteristic,\n notifyCharacteristic: gattNotifyCharacteristic,\n });\n await connectionStateMachine.setupConnection();\n connectionStateMachine.eventDeviceConnected();\n\n registryEntry.serviceUuid = ledgerService.uuid;\n registryEntry.ledgerServiceInfo = ledgerServiceInfo;\n\n if (registryEntry.gattDisconnectListener) {\n rediscoveredDevice.removeEventListener(\n \"gattserverdisconnected\",\n registryEntry.gattDisconnectListener,\n );\n }\n\n const onDisconnectedCallback = (_: Event) =>\n this._handleGattServerDisconnected(deviceId);\n\n rediscoveredDevice.addEventListener(\n \"gattserverdisconnected\",\n onDisconnectedCallback,\n );\n\n registryEntry.gattDisconnectListener = onDisconnectedCallback;\n\n await onReconnect?.(deviceId);\n\n this._publishDiscoveredDevices();\n\n return;\n } catch (e) {\n this._logger.error(`[${deviceId}] reconnect attempt failed`, {\n data: { e },\n });\n\n if (registryEntry?.device.gatt?.connected)\n registryEntry.device.gatt.disconnect();\n\n await this._sleep(RECONNECTION_LOOP_BACKOFF);\n continue;\n }\n }\n }\n\n private async _safelyCancelGattConnection(deviceId: string) {\n const registryEntry = this._deviceRegistryById.get(deviceId);\n if (!registryEntry) return;\n if (registryEntry.device.gatt?.connected)\n registryEntry.device.gatt.disconnect();\n await this._sleep(100);\n }\n\n private async _identifyLedgerGattService(device: BluetoothDevice): Promise<{\n serviceUuid: string;\n ledgerServiceInfo: NonNullable<DeviceRegistryEntry[\"ledgerServiceInfo\"]>;\n }> {\n if (!device.gatt) {\n throw new OpeningConnectionError(\"No GATT server available on device\");\n }\n try {\n await this._withTimeout(device.gatt.connect(), 6000, \"connect timeout\");\n const { service, ledgerServiceInfo } =\n await this._getPrimaryLedgerGattService(device);\n return { serviceUuid: service.uuid, ledgerServiceInfo };\n } finally {\n device.gatt?.disconnect();\n await this._sleep(200);\n }\n }\n\n private async _getPrimaryLedgerGattService(device: BluetoothDevice): Promise<{\n service: BluetoothRemoteGATTService;\n ledgerServiceInfo: NonNullable<DeviceRegistryEntry[\"ledgerServiceInfo\"]>;\n }> {\n const knownLedgerServiceUuids =\n this._deviceModelDataSource.getBluetoothServices();\n const lastSuccessfulServiceUuid = this._deviceRegistryById.get(\n device.id,\n )?.serviceUuid;\n const preferredSearchOrder = lastSuccessfulServiceUuid\n ? [\n lastSuccessfulServiceUuid,\n ...knownLedgerServiceUuids.filter(\n (u) => u !== lastSuccessfulServiceUuid,\n ),\n ]\n : knownLedgerServiceUuids.slice();\n\n for (const candidateUuid of preferredSearchOrder) {\n try {\n const primaryService =\n await device.gatt!.getPrimaryService(candidateUuid);\n const ledgerServiceInfoMap =\n this._deviceModelDataSource.getBluetoothServicesInfos();\n const ledgerServiceInfo = ledgerServiceInfoMap[primaryService.uuid];\n if (!ledgerServiceInfo) throw new UnknownDeviceError(device.name || \"\");\n return { service: primaryService, ledgerServiceInfo };\n } catch (e: unknown) {\n if ((e as Error)?.name === \"SecurityError\") {\n throw new OpeningConnectionError(\n `Missing Web Bluetooth permission for service ${candidateUuid}. ` +\n `Add it to optionalServices in requestDevice().`,\n );\n }\n try {\n const allPrimaryServices = await device.gatt!.getPrimaryServices();\n const matchedService = allPrimaryServices.find(\n (s) => s.uuid.toLowerCase() === candidateUuid.toLowerCase(),\n );\n if (matchedService) {\n const ledgerServiceInfoMap =\n this._deviceModelDataSource.getBluetoothServicesInfos();\n const ledgerServiceInfo = ledgerServiceInfoMap[matchedService.uuid];\n if (!ledgerServiceInfo)\n throw new UnknownDeviceError(device.name || \"\");\n return { service: matchedService, ledgerServiceInfo };\n }\n } catch {\n this._logger.error(\"Failed to get primary services\", {\n data: { deviceId: device.id },\n });\n }\n }\n }\n throw new OpeningConnectionError(\"Ledger GATT service not found\");\n }\n\n private async _resolveLedgerServiceCharacteristics(\n service: BluetoothRemoteGATTService,\n ledgerServiceInfo: NonNullable<DeviceRegistryEntry[\"ledgerServiceInfo\"]>,\n ): Promise<{\n writeCharacteristic: BluetoothRemoteGATTCharacteristic;\n notifyCharacteristic: BluetoothRemoteGATTCharacteristic;\n }> {\n const notifyCharacteristic = await service.getCharacteristic(\n ledgerServiceInfo.notifyUuid,\n );\n const writeCharacteristic = await this._findWritableCharacteristic(\n service,\n ledgerServiceInfo,\n );\n return { writeCharacteristic, notifyCharacteristic };\n }\n\n private async _findWritableCharacteristic(\n service: BluetoothRemoteGATTService,\n ledgerServiceInfo: NonNullable<DeviceRegistryEntry[\"ledgerServiceInfo\"]>,\n ): Promise<BluetoothRemoteGATTCharacteristic> {\n const preferredCharacteristicUuids = [\n ledgerServiceInfo.writeCmdUuid,\n ledgerServiceInfo.writeUuid,\n ].filter(Boolean) as string[];\n\n const attemptedCharacteristics: BluetoothRemoteGATTCharacteristic[] = [];\n\n for (const uuid of preferredCharacteristicUuids) {\n try {\n const characteristic = await service.getCharacteristic(uuid);\n attemptedCharacteristics.push(characteristic);\n\n if (characteristic.properties.writeWithoutResponse)\n return characteristic;\n if (characteristic.properties.write) return characteristic;\n } catch {\n this._logger.error(\"Failed to get write characteristic\", {\n data: { deviceId: service.device.id },\n });\n }\n }\n\n throw new OpeningConnectionError(\"No write characteristic available\");\n }\n\n private _publishDiscoveredDevices() {\n const connectedSummaries: TransportDiscoveredDevice[] = [];\n for (const [deviceId] of this._connectionStateMachinesByDeviceId) {\n const registryEntry = this._deviceRegistryById.get(deviceId);\n if (registryEntry?.ledgerServiceInfo) {\n connectedSummaries.push({\n id: deviceId,\n deviceModel: registryEntry.ledgerServiceInfo.deviceModel,\n transport: webBleIdentifier,\n });\n }\n }\n const scannedSummaries = Array.from(this._deviceRegistryById.values())\n .map((entry) => entry.discoveredDevice)\n .filter(\n (summary) => !this._connectionStateMachinesByDeviceId.has(summary.id),\n );\n\n this._discoveredDevices$.next([...connectedSummaries, ...scannedSummaries]);\n }\n\n private _withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n onTimeoutCancel?: () => void,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timeoutHandle = setTimeout(() => {\n reject(new OpeningConnectionError(timeoutMessage));\n onTimeoutCancel?.();\n }, timeoutMs);\n promise.then(\n (value) => {\n clearTimeout(timeoutHandle);\n resolve(value);\n },\n (error) => {\n this._logger.error(\"withTimeout() promise rejected\", {\n data: { e: error },\n });\n clearTimeout(timeoutHandle);\n reject(error);\n },\n );\n });\n }\n\n private _sleep(ms: number) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\nexport const webBleTransportFactory: TransportFactory = ({\n deviceModelDataSource,\n loggerServiceFactory,\n apduSenderServiceFactory,\n apduReceiverServiceFactory,\n}) =>\n new WebBleTransport(\n deviceModelDataSource,\n loggerServiceFactory,\n apduSenderServiceFactory,\n apduReceiverServiceFactory,\n );\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["DeviceAlreadyConnectedError", "DeviceConnectionStateMachine", "GeneralDmkError", "OpeningConnectionError", "TransportConnectedDevice", "UnknownDeviceError", "Left", "Right", "BehaviorSubject", "from", "switchMap", "ADVERTISING_DELAY", "ADVERTISING_TIMEOUT", "RECONNECT_DEVICE_TIMEOUT", "RECONNECTION_LOOP_BACKOFF", "REDISCOVER_TIMEOUT", "WebBleApduSender", "webBleIdentifier", "WebBleTransport", "_deviceModelDataSource", "loggerFactory", "_apduSenderFactory", "_apduReceiverFactory", "bluetoothServiceUuids", "serviceUuid", "allOptionalServiceUuids", "bluetoothDevice", "ledgerServiceInfo", "discoveredDevice", "params", "registryEntry", "ledgerService", "gattWriteCharacteristic", "gattNotifyCharacteristic", "apduSender", "connectionStateMachine", "e", "entry", "onGattDisconnected", "_ev", "apduArgs", "deviceId", "device", "opts", "startTimeoutMs", "advTimeoutMs", "abortController", "startTimer", "advertisementsStarted", "advertisementSeen", "resolve", "handleAdvertisement", "targetDeviceId", "matchingDevice", "d", "isInRange", "onReconnect", "rediscoveredDevice", "onDisconnectedCallback", "_", "service", "knownLedgerServiceUuids", "lastSuccessfulServiceUuid", "preferredSearchOrder", "u", "candidateUuid", "primaryService", "matchedService", "s", "notifyCharacteristic", "preferredCharacteristicUuids", "attemptedCharacteristics", "uuid", "characteristic", "connectedSummaries", "scannedSummaries", "summary", "promise", "timeoutMs", "timeoutMessage", "onTimeoutCancel", "reject", "timeoutHandle", "value", "error", "ms", "webBleTransportFactory", "deviceModelDataSource", "loggerServiceFactory", "apduSenderServiceFactory", "apduReceiverServiceFactory"]
|
|
7
7
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{DeviceAlreadyConnectedError as y,OpeningConnectionError as u,StaticDeviceModelDataSource as b,UnknownDeviceError as w}from"@ledgerhq/device-management-kit";import{Left as g,Right as h}from"purify-ts";import{firstValueFrom as a}from"rxjs";import{bleDeviceStubBuilder as d}from"../model/BleDevice.stub";import{WebBleTransport as S}from"./WebBleTransport";vi.mock("./WebBleApduSender",async()=>({WebBleApduSender:class{constructor(o,n){}setupConnection=vi.fn().mockResolvedValue(void 0)}}));vi.mock("@ledgerhq/device-management-kit",async()=>({...await vi.importActual("@ledgerhq/device-management-kit"),DeviceConnectionStateMachine:class{constructor(n){}setupConnection=vi.fn().mockResolvedValue(void 0);setDependencies=vi.fn();sendApdu=vi.fn();closeConnection=vi.fn();eventDeviceDisconnected=vi.fn();eventDeviceConnected=vi.fn();eventDeviceDetached=vi.fn();eventDeviceAttached=vi.fn()}}));class k{subscribers=[];tag;constructor(n,s){this.subscribers=n,this.tag=s}error=vi.fn();warn=vi.fn();info=vi.fn();debug=vi.fn()}const l=new b,D=new k([],"WebBleTransport");let c,m,p;beforeEach(()=>{vi.useRealTimers(),m=vi.fn(),p=vi.fn(),c=new S(l,()=>D,p,m)});afterEach(()=>{vi.restoreAllMocks()});describe("WebBleTransport",()=>{describe("isSupported",()=>{it("returns false when navigator.bluetooth is undefined",()=>{delete globalThis.navigator;const o=c.isSupported();expect(o).toBe(!1)}),it("returns true when navigator.bluetooth exists",()=>{globalThis.navigator={bluetooth:{}};const o=c.isSupported();expect(o).toBe(!0)})}),describe("startDiscovering",()=>{let o,n,s;const v=l.getBluetoothServices()[0],f=e=>{const t={connected:!1,connect:vi.fn().mockImplementation(async()=>(t.connected=!0,t)),disconnect:vi.fn().mockImplementation(()=>{t.connected=!1}),getPrimaryService:vi.fn().mockImplementation(async i=>{if(i.toLowerCase()===e.uuid.toLowerCase())return e;const r=new Error("NotFoundError");throw r.name="NotFoundError",r}),getPrimaryServices:vi.fn().mockResolvedValue([e])};return t};beforeEach(()=>{n=d(),s={uuid:v,device:n,getCharacteristic:vi.fn().mockResolvedValue({})};const e=f(s);Object.defineProperty(n,"gatt",{value:e,writable:!0,configurable:!0}),n.addEventListener||(n.addEventListener=vi.fn(),n.removeEventListener=vi.fn()),o=vi.fn(),globalThis.navigator={bluetooth:{requestDevice:o}}}),it("emits a discovered device when a known device is returned",async()=>{o.mockResolvedValueOnce(n);const e=await a(c.startDiscovering()),t=l.getBluetoothServicesInfos()[v]?.deviceModel.id;expect(e).toEqual(expect.objectContaining({deviceModel:expect.objectContaining({id:t}),transport:c.getIdentifier()}))}),it("emits UnknownDeviceError when deviceInfo is missing",async()=>{const e=d(),t={uuid:"invalid-uuid",device:e,getCharacteristic:vi.fn()},i={connected:!1,connect:vi.fn().mockImplementation(async()=>(i.connected=!0,i)),disconnect:vi.fn(),getPrimaryService:vi.fn().mockResolvedValue(t),getPrimaryServices:vi.fn().mockResolvedValue([t])};Object.defineProperty(e,"gatt",{value:i,writable:!0,configurable:!0}),o.mockResolvedValueOnce(e),await expect(a(c.startDiscovering())).rejects.toBeInstanceOf(u)}),it("throws OpeningConnectionError when device has no GATT server",async()=>{const e=d();Object.defineProperty(e,"gatt",{value:void 0,writable:!0,configurable:!0}),o.mockResolvedValueOnce(e),await expect(a(c.startDiscovering())).rejects.toBeInstanceOf(u)}),it("throws OpeningConnectionError when no GATT services are found",async()=>{const e=d(),t={connected:!1,connect:vi.fn().mockImplementation(async()=>t),disconnect:vi.fn(),getPrimaryService:vi.fn().mockRejectedValue(new Error("nope")),getPrimaryServices:vi.fn().mockResolvedValue([])};Object.defineProperty(e,"gatt",{value:t,writable:!0,configurable:!0}),o.mockResolvedValueOnce(e),await expect(a(c.startDiscovering())).rejects.toBeInstanceOf(u)})}),describe("connect/disconnect flow",()=>{let o,n,s;const v=l.getBluetoothServices()[0],f=e=>{const t={connected:!1,connect:vi.fn().mockImplementation(async()=>(t.connected=!0,t)),disconnect:vi.fn().mockImplementation(()=>{t.connected=!1}),getPrimaryService:vi.fn().mockImplementation(async i=>{if(i.toLowerCase()===e.uuid.toLowerCase())return e;const r=new Error("NotFoundError");throw r.name="NotFoundError",r}),getPrimaryServices:vi.fn().mockResolvedValue([e])};return t};beforeEach(()=>{n=d();const e={properties:{}},t={properties:{write:!0}};s={uuid:v,device:n,getCharacteristic:vi.fn().mockResolvedValueOnce(e).mockResolvedValue(t)};const i=f(s);Object.defineProperty(n,"gatt",{value:i,writable:!0,configurable:!0}),n.addEventListener||(n.addEventListener=vi.fn(),n.removeEventListener=vi.fn()),o=vi.fn().mockResolvedValue(n),globalThis.navigator={bluetooth:{requestDevice:o}}}),it("returns UnknownDeviceError when connecting an unknown deviceId",async()=>{const e="nonexistent",t=await c.connect({deviceId:e,onDisconnect:vi.fn()});expect(t).toEqual(g(new w(`Unknown device ${e}`)))}),it("connects and returns a connected device on success",async()=>{const e=await a(c.startDiscovering()),t=await c.connect({deviceId:e.id,onDisconnect:vi.fn()});expect(t.isRight()).toBe(!0);const i=t.extract();expect(i.id).toBe(e.id)}),it("does not allow connecting twice to the same device",async()=>{const e=await a(c.startDiscovering());await c.connect({deviceId:e.id,onDisconnect:vi.fn()});const t=await c.connect({deviceId:e.id,onDisconnect:vi.fn()});expect(t).toEqual(g(new y(`Device ${e.id} already connected`)))}),it("returns OpeningConnectionError when characteristic retrieval fails",async()=>{const e={uuid:v,device:n,getCharacteristic:vi.fn().mockRejectedValue(new Error("boom"))},t={...n.gatt,getPrimaryService:vi.fn().mockResolvedValue(e),getPrimaryServices:vi.fn().mockResolvedValue([e])};Object.defineProperty(n,"gatt",{value:t,writable:!0,configurable:!0});const i=await a(c.startDiscovering()),r=await c.connect({deviceId:i.id,onDisconnect:vi.fn()});expect(r.isLeft()).toBe(!0),expect(r.extract()).toBeInstanceOf(u)}),it("disconnects successfully",async()=>{const e=await a(c.startDiscovering()),i=(await c.connect({deviceId:e.id,onDisconnect:vi.fn()})).extract(),r=await c.disconnect({connectedDevice:i});expect(r).toEqual(h(void 0))})})});
|
|
2
2
|
//# sourceMappingURL=WebBleTransport.test.js.map
|