@ledgerhq/device-transport-kit-web-ble 0.0.0-ledger-button-20250808181454 → 0.0.0-legacy-speculos-datasource-20250819074123
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/lib/esm/api/data/WebBleConfig.js +1 -1
- package/lib/esm/api/data/WebBleConfig.js.map +3 -3
- 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/types/api/data/WebBleConfig.d.ts +3 -1
- package/lib/types/api/data/WebBleConfig.d.ts.map +1 -1
- package/lib/types/api/transport/WebBleApduSender.d.ts +34 -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 +21 -76
- package/lib/types/api/transport/WebBleTransport.d.ts.map +1 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +6 -6
- 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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const E=
|
|
1
|
+
const E=15e3,N=2e3,T=5;export{T as RECONNECTION_RETRY_COUNT,E as RECONNECT_DEVICE_TIMEOUT,N as SINGLE_RECONNECTION_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 SINGLE_RECONNECTION_TIMEOUT = 2000;\nexport const RECONNECTION_RETRY_COUNT = 5;\n"],
|
|
5
|
+
"mappings": "AAAO,MAAMA,EAA2B,KAC3BC,EAA8B,IAC9BC,EAA2B",
|
|
6
|
+
"names": ["RECONNECT_DEVICE_TIMEOUT", "SINGLE_RECONNECTION_TIMEOUT", "RECONNECTION_RETRY_COUNT"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DeviceDisconnectedWhileSendingError as n}from"@ledgerhq/device-management-kit";import{Left as o,Maybe as r,Right as d}from"purify-ts";import{BehaviorSubject as p}from"rxjs";class h{_characteristics;_apduSender=r.empty();_apduSenderFactory;_apduReceiver;_logger;_isDeviceReady=new p(!1);_sendResolver=r.empty();_notificationsActive=!1;_apduReceiverFactory;_mtuHandshakeInFlight=!1;static MTU_OP=8;constructor(e,t){this._characteristics={writeCharacteristic:e.writeCharacteristic,notifyCharacteristic:e.notifyCharacteristic},this._apduSenderFactory=e.apduSenderFactory,this._apduReceiverFactory=e.apduReceiverFactory,this._apduReceiver=e.apduReceiverFactory(),this._logger=t("WebBleApduSender")}_failPendingSend(e){this._sendResolver.map(t=>t(o(e))),this._sendResolver=r.empty()}_handleNotify=e=>{const t=e.target;if(!t.value)return;const i=new Uint8Array(t.value.buffer);if(this._isDeviceReady.value)this._onReceiveApdu(i);else{if(!this._mtuHandshakeInFlight){this._logger.debug("Dropping pre-handshake frame",{data:{data:i}});return}if(i.length<6||i[0]!==h.MTU_OP){this._logger.debug("Non-MTU frame during handshake; dropping",{data:{data:i}});return}this._onReceiveSetup(i)}};_onReceiveSetup(e){const t=e[5];if(typeof t!="number"||t<=0)throw new Error("MTU negotiation failed: invalid MTU");this._apduSender=r.of(this._apduSenderFactory({frameSize:t})),this._isDeviceReady.next(!0)}_onReceiveApdu(e){this._apduReceiver.handleFrame(e).map(t=>t.map(i=>{this._logger.debug("Received APDU",{data:{resp:i}}),this._sendResolver.map(a=>a(d(i))),this._sendResolver=r.empty()})).mapLeft(t=>{this._sendResolver.map(i=>i(o(t))),this._sendResolver=r.empty()})}async _write(e){const t=this._characteristics.writeCharacteristic.properties;if(t.write)await this._characteristics.writeCharacteristic.writeValueWithResponse(e);else if(t.writeWithoutResponse)await this._characteristics.writeCharacteristic.writeValueWithoutResponse(e);else try{await this._characteristics.writeCharacteristic.writeValueWithResponse(e)}catch{await this._characteristics.writeCharacteristic.writeValueWithoutResponse(e)}}async setupConnection(){const e=this._characteristics.notifyCharacteristic;this._notificationsActive||(await e.startNotifications(),this._notificationsActive=!0,e.addEventListener("characteristicvaluechanged",this._handleNotify)),this._mtuHandshakeInFlight=!0;const t=new Uint8Array([h.MTU_OP,0,0,0,0]);try{await this._write(t.buffer),await Promise.race([new Promise((i,a)=>{const c=this._isDeviceReady.subscribe({next:s=>{s&&(c.unsubscribe(),i())},error:s=>{c.unsubscribe(),a(s)}})}),new Promise((i,a)=>setTimeout(()=>a(new Error("MTU negotiation timeout")),2e3))])}catch(i){try{e.removeEventListener("characteristicvaluechanged",this._handleNotify),await e.stopNotifications().catch(()=>{})}finally{this._notificationsActive=!1,this._isDeviceReady.next(!1),this._apduSender=r.empty()}throw i}finally{this._mtuHandshakeInFlight=!1}}async sendApdu(e,t,i){if(this._apduSender.isNothing())return o(new n("Link not ready (no MTU / sender)"));const a=new Promise(c=>{this._sendResolver=r.of(c)});for(const c of this._apduSender.map(s=>s.getFrames(e)).orDefault([]))try{await this._write(c.getRawData().buffer)}catch(s){this._logger.error("Frame write failed",{data:{e:s}}),this._failPendingSend(new n("Write failed"));break}return a}closeConnection(){try{this._failPendingSend(new n("Connection closed")),this._notificationsActive&&(this._characteristics.notifyCharacteristic.removeEventListener("characteristicvaluechanged",this._handleNotify),this._characteristics.notifyCharacteristic.stopNotifications().catch(()=>{}),this._notificationsActive=!1),this._characteristics.notifyCharacteristic.service.device.gatt?.disconnect()}catch{this._logger.error("Failed to disconnect from device")}}getDependencies(){return this._characteristics}async setDependencies(e){const t=this._characteristics.notifyCharacteristic;if(this._failPendingSend(new n("Link changed")),this._notificationsActive&&t.service.device.gatt?.connected)try{await t.stopNotifications()}catch{this._logger.warn("Failed to stop notifications on old characteristic")}t.removeEventListener("characteristicvaluechanged",this._handleNotify),this._notificationsActive=!1,this._characteristics=e,this._apduReceiver=this._apduReceiverFactory(),this._isDeviceReady.next(!1),this._apduSender=r.empty(),this._sendResolver=r.empty(),await e.notifyCharacteristic.startNotifications(),this._notificationsActive=!0,e.notifyCharacteristic.addEventListener("characteristicvaluechanged",this._handleNotify)}}export{h 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 ApduReceiverServiceFactory,\n type ApduResponse,\n type ApduSenderServiceFactory,\n type DeviceApduSender,\n DeviceDisconnectedWhileSendingError,\n type DmkError,\n type LoggerPublisherService,\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 class WebBleApduSender\n implements DeviceApduSender<WebBleApduSenderDependencies>\n{\n private _characteristics: WebBleApduSenderDependencies;\n private _apduSender: Maybe<ReturnType<ApduSenderServiceFactory>> =\n Maybe.empty();\n private _apduSenderFactory: ApduSenderServiceFactory;\n private _apduReceiver: ReturnType<ApduReceiverServiceFactory>;\n private _logger: LoggerPublisherService;\n private _isDeviceReady = new BehaviorSubject<boolean>(false);\n private _sendResolver: Maybe<\n (result: Either<DmkError, ApduResponse>) => void\n > = Maybe.empty();\n private _notificationsActive = false;\n private _apduReceiverFactory: ApduReceiverServiceFactory;\n private _mtuHandshakeInFlight = false;\n private static readonly MTU_OP = 0x08; // adjust to your proto\n\n constructor(\n deps: WebBleApduSenderDependencies & {\n apduSenderFactory: ApduSenderServiceFactory;\n apduReceiverFactory: ApduReceiverServiceFactory;\n },\n loggerFactory: (tag: string) => LoggerPublisherService,\n ) {\n this._characteristics = {\n writeCharacteristic: deps.writeCharacteristic,\n notifyCharacteristic: deps.notifyCharacteristic,\n };\n this._apduSenderFactory = deps.apduSenderFactory;\n this._apduReceiverFactory = deps.apduReceiverFactory;\n this._apduReceiver = deps.apduReceiverFactory();\n this._logger = loggerFactory(\"WebBleApduSender\");\n }\n\n private _failPendingSend(err: DmkError) {\n this._sendResolver.map((r) => r(Left(err)));\n this._sendResolver = Maybe.empty();\n }\n\n private _handleNotify = (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._isDeviceReady.value) {\n if (!this._mtuHandshakeInFlight) {\n this._logger.debug(\"Dropping pre-handshake frame\", { data: { data } });\n return;\n }\n if (data.length < 6 || data[0] !== WebBleApduSender.MTU_OP) {\n this._logger.debug(\"Non-MTU frame during handshake; dropping\", {\n data: { data },\n });\n return;\n }\n this._onReceiveSetup(data);\n } else {\n this._onReceiveApdu(data);\n }\n };\n\n private _onReceiveSetup(mtuResponseBuffer: Uint8Array) {\n const negotiatedMtu = mtuResponseBuffer[5]; // adjust if MTU is 16-bit in your proto\n if (typeof negotiatedMtu !== \"number\" || negotiatedMtu <= 0) {\n throw new Error(\"MTU negotiation failed: invalid MTU\");\n }\n this._apduSender = Maybe.of(\n this._apduSenderFactory({ frameSize: negotiatedMtu }),\n );\n this._isDeviceReady.next(true);\n }\n\n private _onReceiveApdu(incomingFrame: Uint8Array) {\n this._apduReceiver\n .handleFrame(incomingFrame)\n .map((respOpt) =>\n respOpt.map((resp) => {\n this._logger.debug(\"Received APDU\", { data: { resp } });\n this._sendResolver.map((r) => r(Right(resp)));\n this._sendResolver = Maybe.empty();\n }),\n )\n .mapLeft((err) => {\n this._sendResolver.map((r) => r(Left(err)));\n this._sendResolver = Maybe.empty();\n });\n }\n\n private async _write(buf: ArrayBuffer) {\n const props = this._characteristics.writeCharacteristic.properties;\n if (props.write) {\n await this._characteristics.writeCharacteristic.writeValueWithResponse(\n buf,\n );\n } else if (props.writeWithoutResponse) {\n await this._characteristics.writeCharacteristic.writeValueWithoutResponse(\n buf,\n );\n } else {\n // fallback to trying with response, then without\n try {\n await this._characteristics.writeCharacteristic.writeValueWithResponse(\n buf,\n );\n } catch {\n await this._characteristics.writeCharacteristic.writeValueWithoutResponse(\n buf,\n );\n }\n }\n }\n\n public async setupConnection(): Promise<void> {\n const notifyChar = this._characteristics.notifyCharacteristic;\n\n if (!this._notificationsActive) {\n await notifyChar.startNotifications();\n this._notificationsActive = true;\n notifyChar.addEventListener(\n \"characteristicvaluechanged\",\n this._handleNotify,\n );\n }\n\n this._mtuHandshakeInFlight = true;\n const mtuReq = new Uint8Array([WebBleApduSender.MTU_OP, 0, 0, 0, 0]);\n\n try {\n await this._write(mtuReq.buffer);\n\n await Promise.race([\n new Promise<void>((res, rej) => {\n const sub = this._isDeviceReady.subscribe({\n next: (ready) => {\n if (ready) {\n sub.unsubscribe();\n res();\n }\n },\n error: (e) => {\n sub.unsubscribe();\n rej(e);\n },\n });\n }),\n new Promise<void>((_, rej) =>\n setTimeout(() => rej(new Error(\"MTU negotiation timeout\")), 2000),\n ),\n ]);\n } catch (e) {\n try {\n notifyChar.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotify,\n );\n await notifyChar.stopNotifications().catch(() => {});\n } finally {\n this._notificationsActive = false;\n this._isDeviceReady.next(false);\n this._apduSender = Maybe.empty();\n }\n throw e;\n } finally {\n this._mtuHandshakeInFlight = false;\n }\n }\n\n public async sendApdu(\n apdu: Uint8Array,\n _triggersDisconnection?: boolean,\n _abortTimeout?: number,\n ): Promise<Either<DmkError, ApduResponse>> {\n if (this._apduSender.isNothing()) {\n return Left(\n new DeviceDisconnectedWhileSendingError(\n \"Link not ready (no MTU / sender)\",\n ) as unknown as DmkError,\n );\n }\n\n const promise = new Promise<Either<DmkError, ApduResponse>>((resolve) => {\n this._sendResolver = Maybe.of(resolve);\n });\n\n for (const frame of this._apduSender\n .map((s) => s.getFrames(apdu))\n .orDefault([])) {\n try {\n await this._write(frame.getRawData().buffer);\n } catch (e) {\n this._logger.error(\"Frame write failed\", { data: { e } });\n this._failPendingSend(\n new DeviceDisconnectedWhileSendingError(\"Write failed\"),\n );\n break;\n }\n }\n\n return promise;\n }\n\n public closeConnection(): void {\n try {\n this._failPendingSend(\n new DeviceDisconnectedWhileSendingError(\"Connection closed\"),\n );\n\n if (this._notificationsActive) {\n this._characteristics.notifyCharacteristic.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotify,\n );\n this._characteristics.notifyCharacteristic\n .stopNotifications()\n .catch(() => {});\n this._notificationsActive = false;\n }\n this._characteristics.notifyCharacteristic.service.device.gatt?.disconnect();\n } catch {\n this._logger.error(\"Failed to disconnect from device\");\n }\n }\n\n public getDependencies(): WebBleApduSenderDependencies {\n return this._characteristics;\n }\n\n public async setDependencies(\n deps: WebBleApduSenderDependencies,\n ): Promise<void> {\n const oldNotify = this._characteristics.notifyCharacteristic;\n\n // fail any in-flight APDU before swapping the link\n this._failPendingSend(\n new DeviceDisconnectedWhileSendingError(\"Link changed\"),\n );\n\n if (this._notificationsActive && oldNotify.service.device.gatt?.connected) {\n try {\n await oldNotify.stopNotifications();\n } catch {\n this._logger.warn(\"Failed to stop notifications on old characteristic\");\n }\n }\n oldNotify.removeEventListener(\n \"characteristicvaluechanged\",\n this._handleNotify,\n );\n this._notificationsActive = false;\n\n // Swap in new characteristics\n this._characteristics = deps;\n\n // Reset parsing/sending state across reconnect\n this._apduReceiver = this._apduReceiverFactory(); // fresh deframer\n this._isDeviceReady.next(false);\n this._apduSender = Maybe.empty();\n this._sendResolver = Maybe.empty();\n\n // Bind notifications for the new characteristic\n await deps.notifyCharacteristic.startNotifications();\n this._notificationsActive = true;\n deps.notifyCharacteristic.addEventListener(\n \"characteristicvaluechanged\",\n this._handleNotify,\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["DeviceDisconnectedWhileSendingError", "Left", "Maybe", "Right", "BehaviorSubject", "WebBleApduSender", "deps", "loggerFactory", "err", "r", "event", "characteristic", "data", "mtuResponseBuffer", "negotiatedMtu", "incomingFrame", "respOpt", "resp", "buf", "props", "notifyChar", "mtuReq", "res", "rej", "sub", "ready", "e", "_", "apdu", "_triggersDisconnection", "_abortTimeout", "promise", "resolve", "frame", "oldNotify"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Maybe as A,Right as R}from"purify-ts";import{afterEach as E,beforeEach as k,describe as B,expect as n,it as o,vi as t}from"vitest";import{WebBleApduSender as V}from"./WebBleApduSender";class L{subscribers=[];tag;constructor(r,i){this.subscribers=r,this.tag=i}debug=t.fn();info=t.fn();warn=t.fn();error=t.fn()}function d(){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 u,a,g,m,y,s;const h=()=>new Promise(e=>setImmediate(e));k(()=>{u=d(),a=d(),g=t.fn().mockReturnValue({getFrames:e=>[{getRawData:()=>new DataView(e.buffer)}]}),m=t.fn().mockReturnValue({handleFrame:t.fn(e=>R(A.of({data:new Uint8Array([144,0])})))}),y=e=>new L([],e),s=new V({writeCharacteristic:u,notifyCharacteristic:a,apduSenderFactory:g,apduReceiverFactory:m},y)});E(()=>{t.restoreAllMocks()});B("WebBleApduSender",()=>{o("getDependencies returns initial chars",()=>{const e=s.getDependencies();n(e.writeCharacteristic).toBe(u),n(e.notifyCharacteristic).toBe(a)}),o("setupConnection negotiates MTU and listens",async()=>{const e=s.setupConnection();n(a.startNotifications).toHaveBeenCalled(),await h();const i=a.addEventListener.mock.calls.filter(([l])=>l==="characteristicvaluechanged")[0];if(!i)throw new Error("No event registered for 'characteristicvaluechanged'");const c=i[1],f=new Uint8Array([0,0,0,0,0,32]).buffer;c({target:{value:{buffer:f}}}),await n(e).resolves.toBeUndefined()}),o("sendApdu writes frames and resolves on notification",async()=>{const e=s.setupConnection();await h();const r=a.addEventListener.mock.calls.find(([v])=>v==="characteristicvaluechanged");if(!r)throw new Error("No event registered for 'characteristicvaluechanged'");const i=r[1];i({target:{value:{buffer:new Uint8Array([0,0,0,0,0,32]).buffer}}}),await e;const c=new Uint8Array([1,2,3]),f=s.sendApdu(c);n(u.writeValueWithResponse).toHaveBeenCalledWith(c.buffer),await h();const l=a.addEventListener.mock.calls.filter(([v])=>v==="characteristicvaluechanged"),p=l[l.length-1];if(!p)throw new Error("No APDU handler registered.");const[,C]=p,b=new Uint8Array([144,0]).buffer;C({target:{value:{buffer:b}}});const w=await f;n(w.isRight()).toBe(!0),n(w.extract().data).toEqual(new Uint8Array([144,0]))}),o("closeConnection calls disconnect",()=>{s.closeConnection(),n(a.service.device.gatt.disconnect).toHaveBeenCalled()}),o("setDependencies swaps characteristics",async()=>{const e=d(),r=d();a.service.device.gatt.connected=!0,await s.setDependencies({writeCharacteristic:r,notifyCharacteristic:e}),n(a.removeEventListener).toHaveBeenCalled(),n(e.startNotifications).toHaveBeenCalled(),n(e.addEventListener).toHaveBeenCalledWith("characteristicvaluechanged",n.any(Function));const i=s.getDependencies();n(i.notifyCharacteristic).toBe(e),n(i.writeCharacteristic).toBe(r)})});
|
|
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-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 { 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));\n\nbeforeEach(() => {\n writeChar = makeCharacteristic();\n notifyChar = makeCharacteristic();\n\n apduSenderFactory = vi.fn().mockReturnValue({\n getFrames: (apdu: Uint8Array) => [\n { getRawData: () => new DataView(apdu.buffer) },\n ],\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 // when\n const deps = sender.getDependencies();\n\n // then\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\n if (!firstCall) {\n throw new Error(\"No event registered for 'characteristicvaluechanged'\");\n }\n\n const handler = firstCall[1];\n\n // given\n const mtuBuf = new Uint8Array([0, 0, 0, 0, 0, 0x20]).buffer;\n\n // when\n handler({ target: { value: { buffer: mtuBuf } } } as unknown as Event);\n\n // then\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\n if (!mtuCall)\n throw new Error(\"No event registered for 'characteristicvaluechanged'\");\n\n const mtuHandler = mtuCall[1];\n\n mtuHandler({\n target: {\n value: { buffer: new Uint8Array([0, 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\n expect(writeChar.writeValueWithResponse).toHaveBeenCalledWith(\n apduCmd.buffer,\n );\n await flushPromises();\n\n const filteredCalls = (\n notifyChar.addEventListener.mock.calls as [string, EventListener][]\n ).filter(([event]) => event === \"characteristicvaluechanged\");\n\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\n expect(result.isRight()).toBe(true);\n expect((result.extract() as ApduResponse).data).toEqual(\n new Uint8Array([0x90, 0x00]),\n );\n });\n\n it(\"closeConnection calls disconnect\", () => {\n // when\n sender.closeConnection();\n\n // then\n expect(notifyChar.service.device.gatt.disconnect).toHaveBeenCalled();\n });\n\n it(\"setDependencies swaps characteristics\", async () => {\n // given\n const newNotify = makeCharacteristic();\n const newWrite = makeCharacteristic();\n notifyChar.service.device.gatt.connected = true;\n\n // when\n await 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).toHaveBeenCalled();\n expect(newNotify.startNotifications).toHaveBeenCalled();\n expect(newNotify.addEventListener).toHaveBeenCalledWith(\n \"characteristicvaluechanged\",\n expect.any(Function),\n );\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", "WebBleApduSender", "LoggerStub", "subs", "tag", "makeCharacteristic", "disconnect", "stopNotifications", "writeChar", "notifyChar", "apduSenderFactory", "apduReceiverFactory", "loggerFactory", "sender", "flushPromises", "resolve", "apdu", "_frame", "deps", "promise", "firstCall", "event", "handler", "mtuBuf", "setupPromise", "mtuCall", "mtuHandler", "apduCmd", "filteredCalls", "lastCall", "apduHandler", "respBuf", "result", "newNotify", "newWrite"]
|
|
7
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{DeviceAlreadyConnectedError as
|
|
1
|
+
import{DeviceAlreadyConnectedError as C,DeviceDisconnectedWhileSendingError as M,OpeningConnectionError as h,TransportConnectedDevice as E,UnknownDeviceError as l}from"@ledgerhq/device-management-kit";import{DeviceConnectionStateMachine as R}from"@ledgerhq/device-management-kit";import{Left as v,Right as u}from"purify-ts";import{defer as A,from as p,Subject as P,throwError as B}from"rxjs";import{concatMap as T,retryWhen as F,scan as U,tap as L,timeout as O}from"rxjs/operators";import{switchMap as W}from"rxjs/operators";import{RECONNECT_DEVICE_TIMEOUT as S,RECONNECTION_RETRY_COUNT as N,SINGLE_RECONNECTION_TIMEOUT as w}from"../data/WebBleConfig";import{WebBleApduSender as $}from"./WebBleApduSender";const _="WEB-BLE";class x{constructor(e,t,i,n){this.loggerFactory=t;this._deviceModelDataSource=e,this._apduSenderFactory=i,this._apduReceiverFactory=n,this._logger=t("WebBleTransport")}_logger;_deviceModelDataSource;_apduSenderFactory;_apduReceiverFactory;_deviceRegistry=new Map;_connectionMachines=new Map;_discoveredDevicesSubject=new P;_reconnectionPromises=new Map;_reconnectionTimers=new Map;isSupported(){return typeof navigator<"u"&&!!navigator.bluetooth}getIdentifier(){return _}startDiscovering(){const e=this._deviceModelDataSource.getBluetoothServices().map(t=>({services:[t]}));return p(navigator.bluetooth.requestDevice({filters:e})).pipe(W(async t=>{if(!t.gatt)throw new h("No GATT server available on device");const i=await t.gatt.connect(),n=this._deviceModelDataSource.getBluetoothServices();let r=null;for(const o of n)try{r=await i.getPrimaryService(o);break}catch{this._logger.warn(`Service ${o} not found on device ${t.name}`)}if(!r)throw new h("Ledger GATT service not found");const a=this._deviceModelDataSource.getBluetoothServicesInfos()[r.uuid];if(!a)throw new l(t.name||"");const d=t.id,s={id:d,deviceModel:a.deviceModel,transport:_};return this._deviceRegistry.set(d,{device:t,service:r,infos:a,discovered:s}),s}))}listenToAvailableDevices(){return this.startDiscovering().subscribe({next:e=>this._discoveredDevicesSubject.next([e]),error:e=>this._logger.error("Scan error",{data:{e}})}),this._discoveredDevicesSubject.asObservable()}stopDiscovering(){}async connect(e){const t=this._deviceRegistry.get(e.deviceId);if(this._connectionMachines.has(e.deviceId))return v(new C("Device already connected"));if(!t)return v(new l(`Unknown device ${e.deviceId}`));try{const{device:i,service:n,infos:r,discovered:a}=t,d=new $({writeCharacteristic:await n.getCharacteristic(r.writeCmdUuid),notifyCharacteristic:await n.getCharacteristic(r.notifyUuid),apduSenderFactory:this._apduSenderFactory,apduReceiverFactory:this._apduReceiverFactory},this.loggerFactory),s=this.handleDisconnect(e.deviceId,d),o=new R({deviceId:e.deviceId,deviceApduSender:d,timeoutDuration:S,tryToReconnect:()=>{s()},onTerminated:()=>{try{e.onDisconnect(e.deviceId),this._connectionMachines.delete(e.deviceId);const c=this._reconnectionTimers.get(e.deviceId);c&&(clearTimeout(c),this._reconnectionTimers.delete(e.deviceId))}catch(c){this._logger.error("Error during onTerminated cleanup",{data:{error:c}})}}});return await o.setupConnection(),t.onDisconnect=()=>e.onDisconnect(e.deviceId),t.onReconnect=e.onReconnect&&(()=>e.onReconnect(e.deviceId)),this._connectionMachines.set(e.deviceId,o),t.listener=s,i.addEventListener("gattserverdisconnected",t.listener),u(new E({id:e.deviceId,deviceModel:a.deviceModel,type:"BLE",transport:_,sendApdu:async(c,g,b)=>{const f=this._reconnectionPromises.get(e.deviceId);if(f)try{await f.promise}catch{this._logger.error("Reconnection failed")}const m=this._connectionMachines.get(e.deviceId);return m?(await m.sendApdu(c,g,b)).chainLeft(D=>D instanceof M&&g?u({statusCode:new Uint8Array([144,0]),data:new Uint8Array(0)}):v(D)):v(new l(`Unknown device ${e.deviceId}`))}}))}catch(i){return this._deviceRegistry.delete(e.deviceId),this._logger.error("Connection error",{data:{error:i}}),v(new h(i))}}handleDisconnect(e,t){return()=>{const i=this._connectionMachines.get(e),n=this._deviceRegistry.get(e);if(!i||!n)return;if(this._reconnectionPromises.has(e)){this._logger.debug("reconnection already in flight, ignoring");return}const r={};r.promise=new Promise(o=>r.resolve=o),this._reconnectionPromises.set(e,r),i.eventDeviceDisconnected(),this._reconnectionTimers.has(e)&&clearTimeout(this._reconnectionTimers.get(e));const a=setTimeout(()=>{n.onDisconnect?.(),this._cleanupConnection(e)},S);this._reconnectionTimers.set(e,a);const s=A(()=>(this._logger.debug("attempt gatt.connect()"),n.device.gatt?.connected&&n.device.gatt.disconnect(),p(n.device.gatt.connect()))).pipe(O({first:w,with:()=>B(()=>new h(`connect timed out after ${w} ms`))}),T(async()=>{try{return await n.device.gatt.getPrimaryService(n.service.uuid)}catch{for(const o of this._deviceModelDataSource.getBluetoothServices())try{return await n.device.gatt.getPrimaryService(o)}catch{this._logger.warn(`Service ${o} not found on device ${n.device.name}`)}throw new h("Ledger GATT service not found on reconnect")}}),T(o=>p(this._rebindCharacteristics(e,o,t,i))),F(o=>o.pipe(L(c=>this._logger.warn(`retrying #${e} (${c instanceof Error?c.message:c})`)),U(c=>{if(c+1>=N)throw new Error("max retries reached");return c+1},0)))).subscribe({next:async()=>{this._logger.debug(`reconnect SUCCESS for ${e}`);const o=this._reconnectionTimers.get(e);o&&(clearTimeout(o),this._reconnectionTimers.delete(e)),this._connectionMachines.get(e)?.eventDeviceConnected();try{n?.onReconnect&&await n.onReconnect()}catch(g){this._logger.error("onReconnect callback threw",{data:{error:g}})}this._reconnectionPromises.get(e)?.resolve(),this._reconnectionPromises.delete(e),s.unsubscribe()},error:()=>{const o=this._reconnectionTimers.get(e);o&&(clearTimeout(o),this._reconnectionTimers.delete(e)),this._cleanupDevice(e),s.unsubscribe()}})}}_cleanupDevice(e){try{const t=this._connectionMachines.get(e),i=this._deviceRegistry.get(e),n=this._reconnectionPromises.get(e);if(t)try{t.eventDeviceDisconnected(),t.closeConnection()}catch(r){this._logger.error("Error closing state machine",{data:{error:r}})}i&&(i.listener&&i.device.removeEventListener("gattserverdisconnected",i.listener),i.device.gatt?.connected&&i.device.gatt.disconnect()),this._connectionMachines.delete(e),this._deviceRegistry.delete(e),n&&(n.resolve(),this._reconnectionPromises.delete(e)),this._reconnectionTimers.has(e)&&(clearTimeout(this._reconnectionTimers.get(e)),this._reconnectionTimers.delete(e))}catch(t){this._logger.error("Unexpected error during device cleanup",{data:{error:t}})}}_cleanupConnection(e){const t=this._connectionMachines.get(e);if(t){try{t.closeConnection()}catch(n){this._logger.error("Error closing state machine",{data:{error:n}})}this._connectionMachines.delete(e)}const i=this._reconnectionPromises.get(e);i&&(i.resolve(),this._reconnectionPromises.delete(e)),this._reconnectionTimers.has(e)&&(clearTimeout(this._reconnectionTimers.get(e)),this._reconnectionTimers.delete(e))}async _rebindCharacteristics(e,t,i,n){const r=this._deviceRegistry.get(e);r.listener&&r.device.removeEventListener("gattserverdisconnected",r.listener),r.listener=this.handleDisconnect(e,i),r.device.addEventListener("gattserverdisconnected",r.listener),r.service=t,await i.setDependencies({writeCharacteristic:await t.getCharacteristic(r.infos.writeCmdUuid),notifyCharacteristic:await t.getCharacteristic(r.infos.notifyUuid)}),await n.setupConnection()}async disconnect(e){const t=e.connectedDevice.id;if(!this._connectionMachines.get(t))return v(new l(`Unknown device ${t}`));const n=this._deviceRegistry.get(t);if(n&&n.onDisconnect)try{n.onDisconnect()}catch(r){this._logger.error("Error in onDisconnect callback",{data:{error:r}})}return this._cleanupDevice(t),Promise.resolve(u(void 0))}}const J=({deviceModelDataSource:y,loggerServiceFactory:e,apduSenderServiceFactory:t,apduReceiverServiceFactory:i})=>new x(y,e,t,i);export{x as WebBleTransport,_ as webBleIdentifier,J 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 listenToAvailableDevices(): 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 DeviceDisconnectedWhileSendingError,\n type DeviceModelDataSource,\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 { DeviceConnectionStateMachine } from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Right } from \"purify-ts\";\nimport { defer, from, type Observable, Subject, throwError } from \"rxjs\";\nimport { concatMap, retryWhen, scan, tap, timeout } from \"rxjs/operators\";\nimport { switchMap } from \"rxjs/operators\";\n\nimport {\n RECONNECT_DEVICE_TIMEOUT,\n RECONNECTION_RETRY_COUNT,\n SINGLE_RECONNECTION_TIMEOUT,\n} from \"@api/data/WebBleConfig\";\n\nimport {\n WebBleApduSender,\n type WebBleApduSenderDependencies,\n} from \"./WebBleApduSender\";\n\nexport const webBleIdentifier: TransportIdentifier = \"WEB-BLE\";\n\nexport class WebBleTransport implements Transport {\n private _logger: LoggerPublisherService;\n private _deviceModelDataSource: DeviceModelDataSource;\n private _apduSenderFactory: ApduSenderServiceFactory;\n private _apduReceiverFactory: ApduReceiverServiceFactory;\n\n private _deviceRegistry = new Map<\n string,\n {\n device: BluetoothDevice;\n service: BluetoothRemoteGATTService;\n infos: {\n writeCmdUuid: string;\n notifyUuid: string;\n deviceModel: TransportDeviceModel;\n };\n discovered: TransportDiscoveredDevice;\n onDisconnect?: () => void;\n onReconnect?: () => Promise<void> | void;\n listener?: (ev: Event) => void;\n }\n >();\n\n private _connectionMachines = new Map<\n string,\n DeviceConnectionStateMachine<WebBleApduSenderDependencies>\n >();\n private _discoveredDevicesSubject = new Subject<\n TransportDiscoveredDevice[]\n >();\n private _reconnectionPromises = new Map<\n string,\n { promise: Promise<void>; resolve: () => void }\n >();\n private _reconnectionTimers = new Map<\n string,\n ReturnType<typeof setTimeout>\n >();\n\n constructor(\n deviceModelDataSource: DeviceModelDataSource,\n private loggerFactory: (tag: string) => LoggerPublisherService,\n apduSenderFactory: ApduSenderServiceFactory,\n apduReceiverFactory: ApduReceiverServiceFactory,\n ) {\n this._deviceModelDataSource = deviceModelDataSource;\n this._apduSenderFactory = apduSenderFactory;\n this._apduReceiverFactory = apduReceiverFactory;\n this._logger = loggerFactory(\"WebBleTransport\");\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 filters = this._deviceModelDataSource\n .getBluetoothServices()\n .map((s) => ({ services: [s] }));\n\n return from(navigator.bluetooth.requestDevice({ filters })).pipe(\n switchMap(async (device) => {\n if (!device.gatt)\n throw new OpeningConnectionError(\n \"No GATT server available on device\",\n );\n\n const server = await device.gatt.connect();\n const knownUuids = this._deviceModelDataSource.getBluetoothServices();\n let service: BluetoothRemoteGATTService | null = null;\n\n for (const uuid of knownUuids) {\n try {\n service = await server.getPrimaryService(uuid);\n break;\n } catch {\n this._logger.warn(\n `Service ${uuid} not found on device ${device.name}`,\n );\n }\n }\n if (!service)\n throw new OpeningConnectionError(\"Ledger GATT service not found\");\n\n const infos =\n this._deviceModelDataSource.getBluetoothServicesInfos()[service.uuid];\n if (!infos) throw new UnknownDeviceError(device.name || \"\");\n\n const id = device.id;\n const discovered: TransportDiscoveredDevice = {\n id,\n deviceModel: infos.deviceModel,\n transport: webBleIdentifier,\n };\n\n this._deviceRegistry.set(id, { device, service, infos, discovered });\n return discovered;\n }),\n );\n }\n\n listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]> {\n this.startDiscovering().subscribe({\n next: (d) => this._discoveredDevicesSubject.next([d]),\n error: (e) => this._logger.error(\"Scan error\", { data: { e } }),\n });\n return this._discoveredDevicesSubject.asObservable();\n }\n\n stopDiscovering(): void {\n // browser prompt cannot be cancelled\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 deviceEntry = this._deviceRegistry.get(params.deviceId);\n\n if (this._connectionMachines.has(params.deviceId)) {\n return Left(new DeviceAlreadyConnectedError(\"Device already connected\"));\n }\n if (!deviceEntry) {\n return Left(new UnknownDeviceError(`Unknown device ${params.deviceId}`));\n }\n\n try {\n const { device, service, infos, discovered } = deviceEntry;\n\n const apduSender = new WebBleApduSender(\n {\n writeCharacteristic: await service.getCharacteristic(\n infos.writeCmdUuid,\n ),\n notifyCharacteristic: await service.getCharacteristic(\n infos.notifyUuid,\n ),\n apduSenderFactory: this._apduSenderFactory,\n apduReceiverFactory: this._apduReceiverFactory,\n },\n this.loggerFactory,\n );\n\n const disconnectHandler = this.handleDisconnect(\n params.deviceId,\n apduSender,\n );\n\n const connectionMachine =\n new DeviceConnectionStateMachine<WebBleApduSenderDependencies>({\n deviceId: params.deviceId,\n deviceApduSender: apduSender,\n timeoutDuration: RECONNECT_DEVICE_TIMEOUT,\n tryToReconnect: () => {\n disconnectHandler();\n },\n onTerminated: () => {\n try {\n params.onDisconnect(params.deviceId);\n this._connectionMachines.delete(params.deviceId);\n const t = this._reconnectionTimers.get(params.deviceId);\n if (t) {\n clearTimeout(t);\n this._reconnectionTimers.delete(params.deviceId);\n }\n } catch (e) {\n this._logger.error(\"Error during onTerminated cleanup\", {\n data: { error: e },\n });\n }\n },\n });\n\n await connectionMachine.setupConnection();\n\n deviceEntry.onDisconnect = () => params.onDisconnect(params.deviceId);\n deviceEntry.onReconnect =\n params.onReconnect && (() => params.onReconnect!(params.deviceId));\n this._connectionMachines.set(params.deviceId, connectionMachine);\n\n deviceEntry.listener = disconnectHandler;\n device.addEventListener(\"gattserverdisconnected\", deviceEntry.listener);\n\n return Right(\n new TransportConnectedDevice({\n id: params.deviceId,\n deviceModel: discovered.deviceModel,\n type: \"BLE\",\n transport: webBleIdentifier,\n sendApdu: async (apdu, triggersDisconnection, abortTimeout) => {\n const pending = this._reconnectionPromises.get(params.deviceId);\n if (pending) {\n try {\n await pending.promise;\n } catch {\n this._logger.error(\"Reconnection failed\");\n }\n }\n\n const machine = this._connectionMachines.get(params.deviceId);\n if (!machine) {\n return Left(\n new UnknownDeviceError(`Unknown device ${params.deviceId}`),\n );\n }\n\n const res = await machine.sendApdu(\n apdu,\n triggersDisconnection,\n abortTimeout,\n );\n return res.chainLeft((err) => {\n if (\n err instanceof DeviceDisconnectedWhileSendingError &&\n triggersDisconnection\n ) {\n return Right({\n statusCode: new Uint8Array([0x90, 0x00]),\n data: new Uint8Array(0),\n });\n }\n return Left(err);\n });\n },\n }),\n );\n } catch (e) {\n this._deviceRegistry.delete(params.deviceId);\n this._logger.error(\"Connection error\", { data: { error: e } });\n return Left(new OpeningConnectionError(e));\n }\n }\n\n private handleDisconnect(\n deviceId: string,\n apduSender: WebBleApduSender,\n ): (ev?: Event) => void {\n return () => {\n const machine = this._connectionMachines.get(deviceId);\n const entry = this._deviceRegistry.get(deviceId);\n if (!machine || !entry) return;\n if (this._reconnectionPromises.has(deviceId)) {\n this._logger.debug(\"reconnection already in flight, ignoring\");\n return;\n }\n\n const pending = {} as { promise: Promise<void>; resolve: () => void };\n pending.promise = new Promise<void>((res) => (pending.resolve = res));\n this._reconnectionPromises.set(deviceId, pending);\n\n machine.eventDeviceDisconnected();\n\n if (this._reconnectionTimers.has(deviceId))\n clearTimeout(this._reconnectionTimers.get(deviceId)!);\n\n const disconnectTimer = setTimeout(() => {\n entry.onDisconnect?.();\n this._cleanupConnection(deviceId);\n }, RECONNECT_DEVICE_TIMEOUT);\n this._reconnectionTimers.set(deviceId, disconnectTimer);\n\n const reconnectionSubject = defer(() => {\n this._logger.debug(\"attempt gatt.connect()\");\n if (entry.device.gatt?.connected) entry.device.gatt.disconnect();\n return from(entry.device.gatt!.connect());\n }).pipe(\n timeout({\n first: SINGLE_RECONNECTION_TIMEOUT,\n with: () =>\n throwError(\n () =>\n new OpeningConnectionError(\n `connect timed out after ${SINGLE_RECONNECTION_TIMEOUT} ms`,\n ),\n ),\n }),\n concatMap(async () => {\n try {\n return await entry.device.gatt!.getPrimaryService(\n entry.service.uuid,\n );\n } catch {\n for (const uuid of this._deviceModelDataSource.getBluetoothServices()) {\n try {\n return await entry.device.gatt!.getPrimaryService(uuid);\n } catch {\n this._logger.warn(\n `Service ${uuid} not found on device ${entry.device.name}`,\n );\n }\n }\n throw new OpeningConnectionError(\n \"Ledger GATT service not found on reconnect\",\n );\n }\n }),\n concatMap((svc) =>\n from(this._rebindCharacteristics(deviceId, svc, apduSender, machine)),\n ),\n retryWhen((err$) =>\n err$.pipe(\n tap((err) =>\n this._logger.warn(\n `retrying #${deviceId} (${\n err instanceof Error ? err.message : err\n })`,\n ),\n ),\n scan((count) => {\n if (count + 1 >= RECONNECTION_RETRY_COUNT) {\n throw new Error(\"max retries reached\");\n }\n return count + 1;\n }, 0),\n ),\n ),\n );\n\n const reconnectionSub = reconnectionSubject.subscribe({\n next: async () => {\n this._logger.debug(`reconnect SUCCESS for ${deviceId}`);\n\n const t = this._reconnectionTimers.get(deviceId);\n if (t) {\n clearTimeout(t);\n this._reconnectionTimers.delete(deviceId);\n }\n\n this._connectionMachines.get(deviceId)?.eventDeviceConnected();\n\n try {\n if (entry?.onReconnect) {\n await entry.onReconnect(); // wait for SCP reset\n }\n } catch (e) {\n this._logger.error(\"onReconnect callback threw\", {\n data: { error: e },\n });\n }\n\n // Unblock pending sends *after* SCP reset\n const pendingSenders = this._reconnectionPromises.get(deviceId);\n pendingSenders?.resolve();\n this._reconnectionPromises.delete(deviceId);\n\n reconnectionSub.unsubscribe();\n },\n error: () => {\n const t = this._reconnectionTimers.get(deviceId);\n if (t) {\n clearTimeout(t);\n this._reconnectionTimers.delete(deviceId);\n }\n this._cleanupDevice(deviceId);\n reconnectionSub.unsubscribe();\n },\n });\n };\n }\n\n private _cleanupDevice(deviceId: string): void {\n try {\n const machine = this._connectionMachines.get(deviceId);\n const entry = this._deviceRegistry.get(deviceId);\n const pending = this._reconnectionPromises.get(deviceId);\n\n if (machine) {\n try {\n machine.eventDeviceDisconnected();\n machine.closeConnection();\n } catch (e) {\n this._logger.error(\"Error closing state machine\", {\n data: { error: e },\n });\n }\n }\n\n if (entry) {\n if (entry.listener) {\n entry.device.removeEventListener(\n \"gattserverdisconnected\",\n entry.listener,\n );\n }\n if (entry.device.gatt?.connected) {\n entry.device.gatt.disconnect();\n }\n }\n\n this._connectionMachines.delete(deviceId);\n this._deviceRegistry.delete(deviceId);\n\n if (pending) {\n pending.resolve();\n this._reconnectionPromises.delete(deviceId);\n }\n\n if (this._reconnectionTimers.has(deviceId)) {\n clearTimeout(this._reconnectionTimers.get(deviceId)!);\n this._reconnectionTimers.delete(deviceId);\n }\n } catch (e) {\n this._logger.error(\"Unexpected error during device cleanup\", {\n data: { error: e },\n });\n }\n }\n\n private _cleanupConnection(deviceId: string): void {\n const machine = this._connectionMachines.get(deviceId);\n if (machine) {\n try {\n machine.closeConnection();\n } catch (e) {\n this._logger.error(\"Error closing state machine\", {\n data: { error: e },\n });\n }\n this._connectionMachines.delete(deviceId);\n }\n\n const pending = this._reconnectionPromises.get(deviceId);\n if (pending) {\n pending.resolve();\n this._reconnectionPromises.delete(deviceId);\n }\n\n if (this._reconnectionTimers.has(deviceId)) {\n clearTimeout(this._reconnectionTimers.get(deviceId)!);\n this._reconnectionTimers.delete(deviceId);\n }\n }\n\n private async _rebindCharacteristics(\n deviceId: string,\n freshSvc: BluetoothRemoteGATTService,\n apduSender: WebBleApduSender,\n connectionMachine: DeviceConnectionStateMachine<WebBleApduSenderDependencies>,\n ) {\n const deviceEntry = this._deviceRegistry.get(deviceId)!;\n\n if (deviceEntry.listener)\n deviceEntry.device.removeEventListener(\n \"gattserverdisconnected\",\n deviceEntry.listener,\n );\n\n deviceEntry.listener = this.handleDisconnect(deviceId, apduSender);\n deviceEntry.device.addEventListener(\n \"gattserverdisconnected\",\n deviceEntry.listener,\n );\n\n deviceEntry.service = freshSvc;\n\n await apduSender.setDependencies({\n writeCharacteristic: await freshSvc.getCharacteristic(\n deviceEntry.infos.writeCmdUuid,\n ),\n notifyCharacteristic: await freshSvc.getCharacteristic(\n deviceEntry.infos.notifyUuid,\n ),\n });\n\n await connectionMachine.setupConnection();\n }\n\n public async disconnect(params: {\n connectedDevice: TransportConnectedDevice;\n }) {\n const id = params.connectedDevice.id;\n const connectionMachine = this._connectionMachines.get(id);\n if (!connectionMachine)\n return Left(new UnknownDeviceError(`Unknown device ${id}`));\n\n const entry = this._deviceRegistry.get(id);\n\n if (entry && entry.onDisconnect) {\n try {\n entry.onDisconnect();\n } catch (e) {\n this._logger.error(\"Error in onDisconnect callback\", {\n data: { error: e },\n });\n }\n }\n\n this._cleanupDevice(id);\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": "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",
|
|
6
|
+
"names": ["DeviceAlreadyConnectedError", "DeviceDisconnectedWhileSendingError", "OpeningConnectionError", "TransportConnectedDevice", "UnknownDeviceError", "DeviceConnectionStateMachine", "Left", "Right", "defer", "from", "Subject", "throwError", "concatMap", "retryWhen", "scan", "tap", "timeout", "switchMap", "RECONNECT_DEVICE_TIMEOUT", "RECONNECTION_RETRY_COUNT", "SINGLE_RECONNECTION_TIMEOUT", "WebBleApduSender", "webBleIdentifier", "WebBleTransport", "deviceModelDataSource", "loggerFactory", "apduSenderFactory", "apduReceiverFactory", "filters", "s", "device", "server", "knownUuids", "service", "uuid", "infos", "id", "discovered", "d", "params", "deviceEntry", "apduSender", "disconnectHandler", "connectionMachine", "t", "e", "apdu", "triggersDisconnection", "abortTimeout", "pending", "machine", "err", "deviceId", "entry", "res", "disconnectTimer", "reconnectionSub", "svc", "err$", "count", "freshSvc", "webBleTransportFactory", "loggerServiceFactory", "apduSenderServiceFactory", "apduReceiverServiceFactory"]
|
|
7
7
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{DeviceAlreadyConnectedError as m,OpeningConnectionError as l,StaticDeviceModelDataSource as y,UnknownDeviceError as g}from"@ledgerhq/device-management-kit";import{Left as p,Right as w}from"purify-ts";import{firstValueFrom as i}from"rxjs";import{bleDeviceStubBuilder as v}from"../model/BleDevice.stub";import{WebBleTransport as h}from"./WebBleTransport";vi.mock("./WebBleApduSender",async()=>({WebBleApduSender:class{constructor(n,r){}setDependencies=vi.fn().mockResolvedValue(void 0)}}));vi.mock("@ledgerhq/device-management-kit",async()=>({...await vi.importActual("@ledgerhq/device-management-kit"),DeviceConnectionStateMachine:class{constructor(r){}setupConnection=vi.fn().mockResolvedValue(void 0);sendApdu=vi.fn();closeConnection=vi.fn();eventDeviceDetached=vi.fn();eventDeviceAttached=vi.fn()}}));class S{subscribers=[];tag;constructor(r,o){this.subscribers=r,this.tag=o}error=vi.fn();warn=vi.fn();info=vi.fn();debug=vi.fn()}const u=new y,D=new S([],"WebBleTransport");let t,f,b;beforeEach(()=>{vi.useFakeTimers(),f=vi.fn(),b=vi.fn(),t=new h(u,()=>D,b,f)});afterEach(()=>{vi.restoreAllMocks()});describe("WebBleTransport",()=>{describe("isSupported",()=>{it("returns false when navigator.bluetooth is undefined",()=>{delete globalThis.navigator;const n=t.isSupported();expect(n).toBe(!1)}),it("returns true when navigator.bluetooth exists",()=>{globalThis.navigator={bluetooth:{}};const n=t.isSupported();expect(n).toBe(!0)})}),describe("startDiscovering",()=>{let n,r,o,s;beforeEach(()=>{const e=u.getBluetoothServices()[0];r=v(),o={uuid:e,getCharacteristic:vi.fn().mockResolvedValue({})},s={getPrimaryServices:()=>Promise.resolve([o])},Object.defineProperty(r,"gatt",{value:{connect:()=>Promise.resolve(s)},writable:!0,configurable:!0}),n=vi.fn(),globalThis.navigator={bluetooth:{requestDevice:n}}}),it("emits a discovered device when a known device is returned",async()=>{n.mockResolvedValueOnce(r);const e=await i(t.startDiscovering());expect(e).toEqual(expect.objectContaining({deviceModel:expect.objectContaining({id:"stax"}),transport:t.getIdentifier()}))}),it("emits UnknownDeviceError when deviceInfo is missing",async()=>{const e=v();Object.defineProperty(e,"gatt",{value:{connect:()=>Promise.resolve({getPrimaryServices:()=>Promise.resolve([{uuid:"invalid"}])})},writable:!0,configurable:!0}),n.mockResolvedValueOnce(e),await expect(i(t.startDiscovering())).rejects.toBeInstanceOf(g)}),it("throws OpeningConnectionError when device has no GATT server",async()=>{const e=v();Object.defineProperty(e,"gatt",{value:void 0,writable:!0,configurable:!0}),n.mockResolvedValueOnce(e),await expect(i(t.startDiscovering())).rejects.toBeInstanceOf(l)}),it("throws OpeningConnectionError when no GATT services are found",async()=>{const e=v(),c={getPrimaryServices:()=>Promise.resolve([])};Object.defineProperty(e,"gatt",{value:{connect:()=>Promise.resolve(c)},writable:!0,configurable:!0}),n.mockResolvedValueOnce(e),await expect(i(t.startDiscovering())).rejects.toBeInstanceOf(l)})}),describe("connect/disconnect flow",()=>{let n,r,o,s;beforeEach(()=>{const e=u.getBluetoothServices()[0];r=v(),o={uuid:e,getCharacteristic:vi.fn().mockResolvedValue({})},s={getPrimaryServices:()=>Promise.resolve([o])},Object.defineProperty(r,"gatt",{value:{connect:()=>Promise.resolve(s)},writable:!0,configurable:!0}),n=vi.fn().mockResolvedValue(r),globalThis.navigator={bluetooth:{requestDevice:n}}}),it("returns UnknownDeviceError when connecting an unknown deviceId",async()=>{const e="nonexistent",c=await t.connect({deviceId:e,onDisconnect:vi.fn()});expect(c).toEqual(p(new g(`Unknown device ${e}`)))}),it("connects and returns a connected device on success",async()=>{const e=await i(t.startDiscovering()),c=await t.connect({deviceId:e.id,onDisconnect:vi.fn()});expect(c.isRight()).toBe(!0);const a=c.extract();expect(a.id).toBe(e.id)}),it("does not allow connecting twice to the same device",async()=>{const e=await i(t.startDiscovering());await t.connect({deviceId:e.id,onDisconnect:vi.fn()});const c=await t.connect({deviceId:e.id,onDisconnect:vi.fn()});expect(c).toEqual(p(new m("Device already connected")))}),it("returns OpeningConnectionError when characteristic retrieval fails",async()=>{const e={uuid:u.getBluetoothServices()[0],getCharacteristic:vi.fn().mockRejectedValue(new Error("boom"))},c={getPrimaryServices:()=>Promise.resolve([e])};Object.defineProperty(r,"gatt",{value:{connect:()=>Promise.resolve(c)},writable:!0,configurable:!0});const a=await i(t.startDiscovering()),d=await t.connect({deviceId:a.id,onDisconnect:vi.fn()});expect(d.isLeft()).toBe(!0),expect(d.extract()).toBeInstanceOf(l)}),it("disconnects successfully",async()=>{const e=await i(t.startDiscovering()),a=(await t.connect({deviceId:e.id,onDisconnect:vi.fn()})).extract(),d=await t.disconnect({connectedDevice:a});expect(d).toEqual(w(void 0))})})});
|
|
2
2
|
//# sourceMappingURL=WebBleTransport.test.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/api/transport/WebBleTransport.test.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type ApduReceiverServiceFactory,\n type ApduSenderServiceFactory,\n type DeviceModel,\n type LoggerPublisherService,\n type LoggerSubscriberService,\n NoAccessibleDeviceError,\n OpeningConnectionError,\n StaticDeviceModelDataSource,\n type TransportDiscoveredDevice,\n UnknownDeviceError,\n} from \"@ledgerhq/device-management-kit\";\nimport { Left, Right } from \"purify-ts\";\n\nimport { RECONNECT_DEVICE_TIMEOUT } from \"@api/data/WebBleConfig\";\nimport { bleDeviceStubBuilder } from \"@api/model/BleDevice.stub\";\nimport { BleTransportNotSupportedError } from \"@api/model/Errors\";\nimport { BleDeviceGattServerError } from \"@api/model/Errors\";\n\nimport { WebBleTransport } from \"./WebBleTransport\";\n\nclass LoggerPublisherServiceStub implements LoggerPublisherService {\n subscribers: LoggerSubscriberService[] = [];\n tag: string;\n constructor(subscribers: LoggerSubscriberService[], tag: string) {\n this.subscribers = subscribers;\n this.tag = tag;\n }\n error = vi.fn();\n warn = vi.fn();\n info = vi.fn();\n debug = vi.fn();\n}\n\n// Our StaticDeviceModelDataSource can directly be used in our unit tests\nconst bleDeviceModelDataSource = new StaticDeviceModelDataSource();\nconst logger = new LoggerPublisherServiceStub([], \"web-ble\");\n\nconst stubDevice: BluetoothDevice = bleDeviceStubBuilder();\n\ndescribe(\"WebBleTransport\", () => {\n let transport: WebBleTransport;\n let apduReceiverServiceFactoryStub: ApduReceiverServiceFactory;\n let apduSenderServiceFactoryStub: ApduSenderServiceFactory;\n\n beforeEach(() => {\n apduReceiverServiceFactoryStub = vi.fn();\n apduSenderServiceFactoryStub = vi.fn();\n transport = new WebBleTransport(\n bleDeviceModelDataSource,\n () => logger,\n apduSenderServiceFactoryStub,\n apduReceiverServiceFactoryStub,\n );\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.restoreAllMocks();\n });\n\n const discoverDevice = (\n onSuccess: (discoveredDevice: TransportDiscoveredDevice) => void,\n onError?: (error: unknown) => void,\n ) => {\n transport.startDiscovering().subscribe({\n next: onSuccess,\n error: onError,\n });\n };\n\n describe(\"When Web bluetooth API is not supported\", () => {\n it(\"should not support the transport\", () => {\n expect(transport.isSupported()).toBe(false);\n });\n\n it(\"should emit a startDiscovering error\", () =>\n new Promise<void>((resolve, reject) => {\n discoverDevice(\n () => {\n reject(\"Should not emit any value\");\n },\n (error) => {\n expect(error).toBeInstanceOf(BleTransportNotSupportedError);\n resolve();\n },\n );\n }));\n });\n\n describe(\"When Web Bluetooth API is supported\", () => {\n const mockedRequestDevice = vi.fn();\n\n beforeAll(() => {\n global.navigator = {\n bluetooth: {\n requestDevice: mockedRequestDevice,\n },\n } as unknown as Navigator;\n });\n\n afterAll(() => {\n vi.restoreAllMocks();\n global.navigator = undefined as unknown as Navigator;\n });\n\n it(\"should support the transport\", () => {\n expect(transport.isSupported()).toBe(true);\n });\n\n describe(\"startDiscovering\", () => {\n it(\"should emit device if one new grant access\", () =>\n new Promise<void>((resolve, reject) => {\n mockedRequestDevice.mockResolvedValueOnce(stubDevice);\n\n discoverDevice(\n (discoveredDevice) => {\n try {\n expect(discoveredDevice).toEqual(\n expect.objectContaining({\n deviceModel: expect.objectContaining({\n id: \"nanoX\",\n productName: \"Ledger Nano X\",\n }) as DeviceModel,\n }),\n );\n\n resolve();\n } catch (expectError) {\n reject(expectError as Error);\n }\n },\n (error) => {\n reject(error as Error);\n },\n );\n }));\n\n it(\"should throw DeviceNotRecognizedError if the device is not recognized\", () =>\n new Promise<void>((resolve, reject) => {\n mockedRequestDevice.mockResolvedValueOnce({\n ...stubDevice,\n gatt: {\n ...stubDevice.gatt,\n getPrimaryServices: vi.fn(() => Promise.resolve([])),\n },\n productId: 0x4242,\n });\n\n discoverDevice(\n () => {\n reject(\"should not return a device\");\n },\n (error) => {\n expect(error).toBeInstanceOf(BleDeviceGattServerError);\n resolve();\n },\n );\n }));\n\n it(\"should emit an error if the request device is in error\", () =>\n new Promise<void>((resolve, reject) => {\n const message = \"request device error\";\n mockedRequestDevice.mockImplementationOnce(() => {\n throw new Error(message);\n });\n\n discoverDevice(\n () => {\n reject(\"should not return a device\");\n },\n (error) => {\n expect(error).toBeInstanceOf(NoAccessibleDeviceError);\n expect(error).toStrictEqual(\n new NoAccessibleDeviceError(new Error(message)),\n );\n resolve();\n },\n );\n }));\n\n it(\"should emit an error if the user did not grant us access to a device (clicking on cancel on the browser popup for ex)\", () =>\n new Promise<void>((resolve, reject) => {\n mockedRequestDevice.mockResolvedValueOnce({ forget: vi.fn() });\n\n discoverDevice(\n (discoveredDevice) => {\n reject(\n `Should not emit any value, but emitted ${JSON.stringify(\n discoveredDevice,\n )}`,\n );\n },\n (error) => {\n try {\n expect(error).toBeInstanceOf(BleDeviceGattServerError);\n resolve();\n } catch (expectError) {\n reject(expectError as Error);\n }\n },\n );\n }));\n });\n\n describe(\"connect\", () => {\n it(\"should throw UnknownDeviceError if no internal device\", async () => {\n const connectParams = {\n deviceId: \"fake\",\n onDisconnect: vi.fn(),\n };\n\n const connect = await transport.connect(connectParams);\n\n expect(connect).toStrictEqual(\n Left(new UnknownDeviceError(\"Unknown device fake\")),\n );\n });\n\n it(\"should throw OpeningConnectionError if the device is already opened\", async () => {\n const device = {\n deviceId: \"fake\",\n onDisconnect: vi.fn(),\n };\n\n const connect = await transport.connect(device);\n\n expect(connect).toStrictEqual(\n Left(new UnknownDeviceError(\"Unknown device fake\")),\n );\n });\n\n it(\"should throw OpeningConnectionError if the device cannot be opened\", () =>\n new Promise<void>((resolve, reject) => {\n const message = \"cannot be opened\";\n mockedRequestDevice.mockResolvedValueOnce({\n ...stubDevice,\n gatt: {\n connect: () => {\n throw new Error(message);\n },\n },\n });\n\n discoverDevice(\n () => {\n reject(\"Should not emit any value\");\n },\n (error) => {\n expect(error).toBeInstanceOf(OpeningConnectionError);\n resolve();\n },\n );\n }));\n\n it(\"should return the opened device\", () =>\n new Promise<void>((resolve, reject) => {\n mockedRequestDevice.mockResolvedValueOnce({\n ...stubDevice,\n gatt: {\n ...stubDevice.gatt,\n connected: true,\n },\n });\n\n discoverDevice(\n (discoveredDevice) => {\n transport\n .connect({\n deviceId: discoveredDevice.id,\n onDisconnect: vi.fn(),\n })\n .then((connectedDevice) => {\n connectedDevice\n .ifRight((device) => {\n expect(device).toEqual(\n expect.objectContaining({ id: discoveredDevice.id }),\n );\n resolve();\n })\n .ifLeft(() => {\n reject(connectedDevice);\n });\n })\n .catch((error) => {\n reject(error);\n });\n },\n (error) => {\n reject(error as Error);\n },\n );\n }));\n\n it(\"should return a device if available\", () =>\n new Promise<void>((resolve, reject) => {\n mockedRequestDevice.mockResolvedValueOnce(stubDevice);\n\n discoverDevice(\n (discoveredDevice) => {\n transport\n .connect({\n deviceId: discoveredDevice.id,\n onDisconnect: vi.fn(),\n })\n .then((connectedDevice) => {\n connectedDevice\n .ifRight((device) => {\n expect(device).toEqual(\n expect.objectContaining({ id: discoveredDevice.id }),\n );\n resolve();\n })\n .ifLeft(() => {\n reject(connectedDevice);\n });\n })\n .catch((error) => {\n reject(error);\n });\n },\n (error) => {\n reject(error as Error);\n },\n );\n }));\n });\n\n describe(\"disconnect\", () => {\n it(\"should disconnect the device\", () =>\n new Promise<void>((resolve, reject) => {\n mockedRequestDevice.mockResolvedValueOnce(stubDevice);\n\n const onDisconnect = vi.fn();\n\n discoverDevice(\n (discoveredDevice) => {\n transport\n .connect({\n deviceId: discoveredDevice.id,\n onDisconnect,\n })\n .then((connectedDevice) => {\n connectedDevice.ifRight((device) => {\n transport\n .disconnect({ connectedDevice: device })\n .then((value) => {\n expect(value).toStrictEqual(Right(undefined));\n resolve();\n })\n .catch((error) => {\n reject(error);\n });\n });\n });\n },\n (error) => {\n reject(error as Error);\n },\n );\n }));\n\n it(\"should call disconnect handler if device is hardware disconnected\", () =>\n new Promise<void>((resolve, reject) => {\n const onDisconnect = vi.fn();\n const disconnectSpy = vi.spyOn(transport, \"disconnect\");\n mockedRequestDevice.mockResolvedValueOnce(stubDevice);\n\n discoverDevice(\n (discoveredDevice) => {\n transport\n .connect({\n deviceId: discoveredDevice.id,\n onDisconnect,\n })\n .then(() => {\n stubDevice.ongattserverdisconnected(new Event(\"\"));\n vi.advanceTimersByTime(RECONNECT_DEVICE_TIMEOUT);\n expect(disconnectSpy).toHaveBeenCalled();\n resolve();\n });\n },\n (error) => {\n reject(error as Error);\n },\n );\n }));\n });\n\n describe(\"reconnect\", () => {\n it(\"should not call disconnection if reconnection happen\", () =>\n new Promise<void>((resolve, reject) => {\n // given\n const onDisconnect = vi.fn();\n const disconnectSpy = vi.spyOn(transport, \"disconnect\");\n mockedRequestDevice.mockResolvedValueOnce(stubDevice);\n\n // when\n discoverDevice((discoveredDevice) => {\n transport\n .connect({\n deviceId: discoveredDevice.id,\n onDisconnect,\n })\n .then(() => {\n stubDevice.ongattserverdisconnected(new Event(\"\"));\n\n vi.advanceTimersByTime(RECONNECT_DEVICE_TIMEOUT / 3);\n\n // then\n expect(disconnectSpy).toHaveBeenCalledTimes(0);\n resolve();\n })\n .catch((error) => {\n reject(error);\n });\n });\n }));\n });\n });\n});\n"],
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": ["
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-unsafe-return */\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nvi.mock(\"./WebBleApduSender\", async () => ({\n WebBleApduSender: class {\n constructor(_deps: any, _loggerFactory: any) {}\n setDependencies = vi.fn().mockResolvedValue(undefined);\n },\n}));\n\nvi.mock(\"@ledgerhq/device-management-kit\", async () => {\n const actual = (await vi.importActual(\n \"@ledgerhq/device-management-kit\",\n )) as any;\n return {\n ...actual,\n DeviceConnectionStateMachine: class {\n constructor(_opts: any) {}\n setupConnection = vi.fn().mockResolvedValue(undefined);\n sendApdu = vi.fn();\n closeConnection = vi.fn();\n eventDeviceDetached = vi.fn();\n eventDeviceAttached = vi.fn();\n },\n };\n});\n\nimport {\n type ApduReceiverServiceFactory,\n type ApduSenderServiceFactory,\n DeviceAlreadyConnectedError,\n type LoggerPublisherService,\n type LoggerSubscriberService,\n OpeningConnectionError,\n StaticDeviceModelDataSource,\n type TransportConnectedDevice,\n UnknownDeviceError,\n} from \"@ledgerhq/device-management-kit\";\nimport { Left, Right } from \"purify-ts\";\nimport { firstValueFrom } from \"rxjs\";\n\nimport { bleDeviceStubBuilder } from \"@api/model/BleDevice.stub\";\n\nimport { WebBleTransport } from \"./WebBleTransport\";\n\nclass LoggerPublisherServiceStub implements LoggerPublisherService {\n subscribers: LoggerSubscriberService[] = [];\n tag: string;\n constructor(subscribers: LoggerSubscriberService[], tag: string) {\n this.subscribers = subscribers;\n this.tag = tag;\n }\n error = vi.fn();\n warn = vi.fn();\n info = vi.fn();\n debug = vi.fn();\n}\n\nconst bleDeviceModelDataSource = new StaticDeviceModelDataSource();\nconst logger = new LoggerPublisherServiceStub([], \"WebBleTransport\");\n\nlet transport: WebBleTransport;\nlet apduReceiverFactory: ApduReceiverServiceFactory;\nlet apduSenderFactory: ApduSenderServiceFactory;\n\nbeforeEach(() => {\n vi.useFakeTimers();\n apduReceiverFactory = vi.fn();\n apduSenderFactory = vi.fn();\n transport = new WebBleTransport(\n bleDeviceModelDataSource,\n () => logger,\n apduSenderFactory,\n apduReceiverFactory,\n );\n});\n\nafterEach(() => {\n vi.restoreAllMocks();\n});\n\ndescribe(\"WebBleTransport\", () => {\n describe(\"isSupported\", () => {\n it(\"returns false when navigator.bluetooth is undefined\", () => {\n // given\n delete (globalThis as any).navigator;\n\n // when\n const result = transport.isSupported();\n\n // then\n expect(result).toBe(false);\n });\n\n it(\"returns true when navigator.bluetooth exists\", () => {\n // given\n (globalThis as any).navigator = { bluetooth: {} };\n\n // when\n const result = transport.isSupported();\n\n // then\n expect(result).toBe(true);\n });\n });\n\n describe(\"startDiscovering\", () => {\n let requestDevice: ReturnType<typeof vi.fn>;\n let stubDevice: BluetoothDevice;\n let mockService: any;\n let mockServer: any;\n\n beforeEach(() => {\n const serviceUuid = bleDeviceModelDataSource.getBluetoothServices()[0];\n stubDevice = bleDeviceStubBuilder();\n mockService = {\n uuid: serviceUuid,\n getCharacteristic: vi.fn().mockResolvedValue({}),\n };\n mockServer = {\n getPrimaryServices: () => Promise.resolve([mockService]),\n };\n Object.defineProperty(stubDevice, \"gatt\", {\n value: { connect: () => Promise.resolve(mockServer) },\n writable: true,\n configurable: true,\n });\n\n requestDevice = vi.fn();\n (globalThis as any).navigator = {\n bluetooth: { requestDevice },\n };\n });\n\n it(\"emits a discovered device when a known device is returned\", async () => {\n // given\n requestDevice.mockResolvedValueOnce(stubDevice);\n\n // when\n const device = await firstValueFrom(transport.startDiscovering());\n\n // then\n expect(device).toEqual(\n expect.objectContaining({\n deviceModel: expect.objectContaining({ id: \"stax\" }),\n transport: transport.getIdentifier(),\n }),\n );\n });\n\n it(\"emits UnknownDeviceError when deviceInfo is missing\", async () => {\n // given\n const unknownStub = bleDeviceStubBuilder();\n Object.defineProperty(unknownStub, \"gatt\", {\n value: {\n connect: () =>\n Promise.resolve({\n getPrimaryServices: () => Promise.resolve([{ uuid: \"invalid\" }]),\n } as any),\n },\n writable: true,\n configurable: true,\n });\n requestDevice.mockResolvedValueOnce(unknownStub);\n\n // when / then\n await expect(\n firstValueFrom(transport.startDiscovering()),\n ).rejects.toBeInstanceOf(UnknownDeviceError);\n });\n\n it(\"throws OpeningConnectionError when device has no GATT server\", async () => {\n // given\n const noGattStub = bleDeviceStubBuilder();\n Object.defineProperty(noGattStub, \"gatt\", {\n value: undefined,\n writable: true,\n configurable: true,\n });\n requestDevice.mockResolvedValueOnce(noGattStub);\n\n // when / then\n await expect(\n firstValueFrom(transport.startDiscovering()),\n ).rejects.toBeInstanceOf(OpeningConnectionError);\n });\n\n it(\"throws OpeningConnectionError when no GATT services are found\", async () => {\n // given\n const noServiceStub = bleDeviceStubBuilder();\n const emptyServer = {\n getPrimaryServices: () => Promise.resolve([]),\n } as any;\n Object.defineProperty(noServiceStub, \"gatt\", {\n value: { connect: () => Promise.resolve(emptyServer) },\n writable: true,\n configurable: true,\n });\n requestDevice.mockResolvedValueOnce(noServiceStub);\n\n // when / then\n await expect(\n firstValueFrom(transport.startDiscovering()),\n ).rejects.toBeInstanceOf(OpeningConnectionError);\n });\n });\n\n describe(\"connect/disconnect flow\", () => {\n let requestDevice: ReturnType<typeof vi.fn>;\n let stubDevice: BluetoothDevice;\n let mockService: any;\n let mockServer: any;\n\n beforeEach(() => {\n const serviceUuid = bleDeviceModelDataSource.getBluetoothServices()[0];\n stubDevice = bleDeviceStubBuilder();\n mockService = {\n uuid: serviceUuid,\n getCharacteristic: vi.fn().mockResolvedValue({}),\n };\n mockServer = {\n getPrimaryServices: () => Promise.resolve([mockService]),\n };\n Object.defineProperty(stubDevice, \"gatt\", {\n value: { connect: () => Promise.resolve(mockServer) },\n writable: true,\n configurable: true,\n });\n\n requestDevice = vi.fn().mockResolvedValue(stubDevice);\n (globalThis as any).navigator = {\n bluetooth: { requestDevice },\n };\n });\n\n it(\"returns UnknownDeviceError when connecting an unknown deviceId\", async () => {\n // given\n const deviceId = \"nonexistent\";\n\n // when\n const result = await transport.connect({\n deviceId,\n onDisconnect: vi.fn(),\n });\n\n // then\n expect(result).toEqual(\n Left(new UnknownDeviceError(`Unknown device ${deviceId}`)),\n );\n });\n\n it(\"connects and returns a connected device on success\", async () => {\n // given\n const discovered = await firstValueFrom(transport.startDiscovering());\n\n // when\n const result = await transport.connect({\n deviceId: discovered.id,\n onDisconnect: vi.fn(),\n });\n\n // then\n expect(result.isRight()).toBe(true);\n const conn = result.extract() as TransportConnectedDevice;\n expect(conn.id).toBe(discovered.id);\n });\n\n it(\"does not allow connecting twice to the same device\", async () => {\n // given\n const discovered = await firstValueFrom(transport.startDiscovering());\n await transport.connect({\n deviceId: discovered.id,\n onDisconnect: vi.fn(),\n });\n\n // when\n const secondAttempt = await transport.connect({\n deviceId: discovered.id,\n onDisconnect: vi.fn(),\n });\n\n // then\n expect(secondAttempt).toEqual(\n Left(new DeviceAlreadyConnectedError(\"Device already connected\")),\n );\n });\n\n it(\"returns OpeningConnectionError when characteristic retrieval fails\", async () => {\n // given\n const badCharacteristicService = {\n uuid: bleDeviceModelDataSource.getBluetoothServices()[0],\n getCharacteristic: vi.fn().mockRejectedValue(new Error(\"boom\")),\n };\n const badServer = {\n getPrimaryServices: () => Promise.resolve([badCharacteristicService]),\n };\n Object.defineProperty(stubDevice, \"gatt\", {\n value: { connect: () => Promise.resolve(badServer) },\n writable: true,\n configurable: true,\n });\n\n const discovered = await firstValueFrom(transport.startDiscovering());\n\n // when\n const result = await transport.connect({\n deviceId: discovered.id,\n onDisconnect: vi.fn(),\n });\n\n // then\n expect(result.isLeft()).toBe(true);\n expect(result.extract()).toBeInstanceOf(OpeningConnectionError);\n });\n\n it(\"disconnects successfully\", async () => {\n // given\n const discovered = await firstValueFrom(transport.startDiscovering());\n const result = await transport.connect({\n deviceId: discovered.id,\n onDisconnect: vi.fn(),\n });\n const conn = result.extract() as TransportConnectedDevice;\n\n // when\n const disc = await transport.disconnect({ connectedDevice: conn });\n\n // then\n expect(disc).toEqual(Right(undefined));\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["DeviceAlreadyConnectedError", "OpeningConnectionError", "StaticDeviceModelDataSource", "UnknownDeviceError", "Left", "Right", "firstValueFrom", "bleDeviceStubBuilder", "WebBleTransport", "_deps", "_loggerFactory", "_opts", "LoggerPublisherServiceStub", "subscribers", "tag", "bleDeviceModelDataSource", "logger", "transport", "apduReceiverFactory", "apduSenderFactory", "result", "requestDevice", "stubDevice", "mockService", "mockServer", "serviceUuid", "device", "unknownStub", "noGattStub", "noServiceStub", "emptyServer", "deviceId", "discovered", "conn", "secondAttempt", "badCharacteristicService", "badServer", "disc"]
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebBleConfig.d.ts","sourceRoot":"","sources":["../../../../src/api/data/WebBleConfig.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"WebBleConfig.d.ts","sourceRoot":"","sources":["../../../../src/api/data/WebBleConfig.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAC9C,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAChD,eAAO,MAAM,wBAAwB,IAAI,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ApduReceiverServiceFactory, type ApduResponse, type ApduSenderServiceFactory, type DeviceApduSender, type DmkError, type LoggerPublisherService } from "@ledgerhq/device-management-kit";
|
|
2
|
+
import { type Either } from "purify-ts";
|
|
3
|
+
export type WebBleApduSenderDependencies = {
|
|
4
|
+
writeCharacteristic: BluetoothRemoteGATTCharacteristic;
|
|
5
|
+
notifyCharacteristic: BluetoothRemoteGATTCharacteristic;
|
|
6
|
+
};
|
|
7
|
+
export declare class WebBleApduSender implements DeviceApduSender<WebBleApduSenderDependencies> {
|
|
8
|
+
private _characteristics;
|
|
9
|
+
private _apduSender;
|
|
10
|
+
private _apduSenderFactory;
|
|
11
|
+
private _apduReceiver;
|
|
12
|
+
private _logger;
|
|
13
|
+
private _isDeviceReady;
|
|
14
|
+
private _sendResolver;
|
|
15
|
+
private _notificationsActive;
|
|
16
|
+
private _apduReceiverFactory;
|
|
17
|
+
private _mtuHandshakeInFlight;
|
|
18
|
+
private static readonly MTU_OP;
|
|
19
|
+
constructor(deps: WebBleApduSenderDependencies & {
|
|
20
|
+
apduSenderFactory: ApduSenderServiceFactory;
|
|
21
|
+
apduReceiverFactory: ApduReceiverServiceFactory;
|
|
22
|
+
}, loggerFactory: (tag: string) => LoggerPublisherService);
|
|
23
|
+
private _failPendingSend;
|
|
24
|
+
private _handleNotify;
|
|
25
|
+
private _onReceiveSetup;
|
|
26
|
+
private _onReceiveApdu;
|
|
27
|
+
private _write;
|
|
28
|
+
setupConnection(): Promise<void>;
|
|
29
|
+
sendApdu(apdu: Uint8Array, _triggersDisconnection?: boolean, _abortTimeout?: number): Promise<Either<DmkError, ApduResponse>>;
|
|
30
|
+
closeConnection(): void;
|
|
31
|
+
getDependencies(): WebBleApduSenderDependencies;
|
|
32
|
+
setDependencies(deps: WebBleApduSenderDependencies): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=WebBleApduSender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebBleApduSender.d.ts","sourceRoot":"","sources":["../../../../src/api/transport/WebBleApduSender.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,YAAY,EACjB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAC5B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,KAAK,MAAM,EAAsB,MAAM,WAAW,CAAC;AAG5D,MAAM,MAAM,4BAA4B,GAAG;IACzC,mBAAmB,EAAE,iCAAiC,CAAC;IACvD,oBAAoB,EAAE,iCAAiC,CAAC;CACzD,CAAC;AAEF,qBAAa,gBACX,YAAW,gBAAgB,CAAC,4BAA4B,CAAC;IAEzD,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,WAAW,CACH;IAChB,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,aAAa,CAAyC;IAC9D,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,aAAa,CAEH;IAClB,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,oBAAoB,CAA6B;IACzD,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAQ;gBAGpC,IAAI,EAAE,4BAA4B,GAAG;QACnC,iBAAiB,EAAE,wBAAwB,CAAC;QAC5C,mBAAmB,EAAE,0BAA0B,CAAC;KACjD,EACD,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,sBAAsB;IAYxD,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,aAAa,CAoBnB;IAEF,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,cAAc;YAgBR,MAAM;IAwBP,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAuDhC,QAAQ,CACnB,IAAI,EAAE,UAAU,EAChB,sBAAsB,CAAC,EAAE,OAAO,EAChC,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IA8BnC,eAAe,IAAI,IAAI;IAsBvB,eAAe,IAAI,4BAA4B;IAIzC,eAAe,CAC1B,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,IAAI,CAAC;CAsCjB"}
|